Update linked code labels and alias cache
This commit is contained in:
@@ -12,6 +12,7 @@ import html as html_lib
|
|||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
from datetime import datetime, timedelta, time
|
from datetime import datetime, timedelta, time
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from urllib.parse import parse_qs, quote, urlparse
|
from urllib.parse import parse_qs, quote, urlparse
|
||||||
@@ -140,6 +141,14 @@ def init_db(conn):
|
|||||||
shortName TEXT
|
shortName TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS erp_project_alias_cache (
|
||||||
|
sourcePage TEXT NOT NULL,
|
||||||
|
projectCode TEXT NOT NULL,
|
||||||
|
shortName TEXT NOT NULL DEFAULT '',
|
||||||
|
syncedAt TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (sourcePage, projectCode)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS site_worksheet_record (
|
CREATE TABLE IF NOT EXISTS site_worksheet_record (
|
||||||
projectCode TEXT,
|
projectCode TEXT,
|
||||||
workDate TEXT,
|
workDate TEXT,
|
||||||
@@ -288,6 +297,20 @@ def init_db(conn):
|
|||||||
PRIMARY KEY (sourcePage, projectCode)
|
PRIMARY KEY (sourcePage, projectCode)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS erp_linked_code_cache (
|
||||||
|
sourcePage TEXT NOT NULL,
|
||||||
|
projectCode TEXT NOT NULL,
|
||||||
|
projectName TEXT NOT NULL DEFAULT '',
|
||||||
|
businessCode TEXT DEFAULT '',
|
||||||
|
salesCode TEXT DEFAULT '',
|
||||||
|
salesName TEXT DEFAULT '',
|
||||||
|
designCode TEXT DEFAULT '',
|
||||||
|
designName TEXT DEFAULT '',
|
||||||
|
matchedBy TEXT DEFAULT '',
|
||||||
|
syncedAt TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (sourcePage, projectCode)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS naver_address_token_cache (
|
CREATE TABLE IF NOT EXISTS naver_address_token_cache (
|
||||||
address TEXT PRIMARY KEY,
|
address TEXT PRIMARY KEY,
|
||||||
tokenX TEXT NOT NULL,
|
tokenX TEXT NOT NULL,
|
||||||
@@ -328,6 +351,7 @@ def init_db(conn):
|
|||||||
conn.execute("ALTER TABLE dallyproject ADD COLUMN BusinessTripHours REAL DEFAULT 0")
|
conn.execute("ALTER TABLE dallyproject ADD COLUMN BusinessTripHours REAL DEFAULT 0")
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_dally_workdate ON dallyproject(WorkDate)")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_dally_workdate ON dallyproject(WorkDate)")
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_project_alias_code ON project_alias(projectCode)")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_project_alias_code ON project_alias(projectCode)")
|
||||||
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_erp_project_alias_cache_page ON erp_project_alias_cache(sourcePage, projectCode)")
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_site_worksheet_member ON site_worksheet_record(memberNo, workDate)")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_site_worksheet_member ON site_worksheet_record(memberNo, workDate)")
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_site_worksheet_name ON site_worksheet_record(korName, workDate)")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_site_worksheet_name ON site_worksheet_record(korName, workDate)")
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_site_worksheet_sync_member ON site_worksheet_sync(memberNo, yearMonth)")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_site_worksheet_sync_member ON site_worksheet_sync(memberNo, yearMonth)")
|
||||||
@@ -340,6 +364,7 @@ def init_db(conn):
|
|||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_erp_contract_detail_cache_page ON erp_contract_detail_cache(sourcePage, projectCode)")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_erp_contract_detail_cache_page ON erp_contract_detail_cache(sourcePage, projectCode)")
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_erp_bridge_overview_cache_page ON erp_bridge_overview_cache(sourcePage, projectCode, bridgeNo)")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_erp_bridge_overview_cache_page ON erp_bridge_overview_cache(sourcePage, projectCode, bridgeNo)")
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_erp_budget_plan_cache_page ON erp_budget_plan_cache(sourcePage, projectCode)")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_erp_budget_plan_cache_page ON erp_budget_plan_cache(sourcePage, projectCode)")
|
||||||
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_erp_linked_code_cache_page ON erp_linked_code_cache(sourcePage, projectCode)")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
DB_SCHEMA_READY = True
|
DB_SCHEMA_READY = True
|
||||||
|
|
||||||
@@ -853,7 +878,7 @@ def backfill_daily_hours_if_needed(conn):
|
|||||||
return need
|
return need
|
||||||
|
|
||||||
|
|
||||||
def load_project_alias_from_erp():
|
def load_project_alias_entries_from_erp():
|
||||||
login_url = ERP_BASE_URL.rstrip('/') + '/sys/controller/common/main_controller.php'
|
login_url = ERP_BASE_URL.rstrip('/') + '/sys/controller/common/main_controller.php'
|
||||||
s = requests.Session()
|
s = requests.Session()
|
||||||
login_payload = {
|
login_payload = {
|
||||||
@@ -886,8 +911,12 @@ def load_project_alias_from_erp():
|
|||||||
code = _as_text(item.get('ProjectCode')).strip().upper()
|
code = _as_text(item.get('ProjectCode')).strip().upper()
|
||||||
nick = _as_text(item.get('ProjectNickname')).strip()
|
nick = _as_text(item.get('ProjectNickname')).strip()
|
||||||
if code and nick:
|
if code and nick:
|
||||||
out[code] = nick
|
out[code] = {'sourcePage': page, 'projectCode': code, 'shortName': nick}
|
||||||
return sorted(out.items())
|
return sorted(out.values(), key=lambda item: (item['projectCode'], item['sourcePage']))
|
||||||
|
|
||||||
|
|
||||||
|
def load_project_alias_from_erp():
|
||||||
|
return [(item['projectCode'], item['shortName']) for item in load_project_alias_entries_from_erp()]
|
||||||
|
|
||||||
|
|
||||||
def erp_login_session():
|
def erp_login_session():
|
||||||
@@ -1679,6 +1708,7 @@ def fetch_erp_contract_detail(page='const', project_code='', project_name=''):
|
|||||||
'finalContractAmountText': contract_map.get('최종계약금액', ''),
|
'finalContractAmountText': contract_map.get('최종계약금액', ''),
|
||||||
'finalContractAmountValue': _parse_amount_text(contract_map.get('최종계약금액', '')),
|
'finalContractAmountValue': _parse_amount_text(contract_map.get('최종계약금액', '')),
|
||||||
'contractType': contract_map.get('계약종류', ''),
|
'contractType': contract_map.get('계약종류', ''),
|
||||||
|
'linkedCodes': find_erp_linked_codes(code, contract_map.get('사업코드', ''), short_name or contract_map.get('약칭', '')),
|
||||||
'contractFields': contract_map,
|
'contractFields': contract_map,
|
||||||
'scaleRows': scale_rows,
|
'scaleRows': scale_rows,
|
||||||
}
|
}
|
||||||
@@ -1695,6 +1725,7 @@ def replace_erp_contract_detail_cache(conn, detail):
|
|||||||
raw_payload = {
|
raw_payload = {
|
||||||
'contractFields': contract_fields,
|
'contractFields': contract_fields,
|
||||||
'scaleRows': detail.get('scaleRows') or [],
|
'scaleRows': detail.get('scaleRows') or [],
|
||||||
|
'linkedCodes': detail.get('linkedCodes') or {},
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -1743,9 +1774,11 @@ def get_erp_contract_detail_cache(conn, source_page='const', project_code=''):
|
|||||||
if isinstance(raw_contract, dict) and ('contractFields' in raw_contract or 'scaleRows' in raw_contract):
|
if isinstance(raw_contract, dict) and ('contractFields' in raw_contract or 'scaleRows' in raw_contract):
|
||||||
contract_fields = raw_contract.get('contractFields') or {}
|
contract_fields = raw_contract.get('contractFields') or {}
|
||||||
scale_rows = raw_contract.get('scaleRows') or []
|
scale_rows = raw_contract.get('scaleRows') or []
|
||||||
|
linked_codes = _decorate_linked_code_names(raw_contract.get('linkedCodes') or {}, conn)
|
||||||
else:
|
else:
|
||||||
contract_fields = raw_contract if isinstance(raw_contract, dict) else {}
|
contract_fields = raw_contract if isinstance(raw_contract, dict) else {}
|
||||||
scale_rows = []
|
scale_rows = []
|
||||||
|
linked_codes = {}
|
||||||
return {
|
return {
|
||||||
'sourcePage': source_page,
|
'sourcePage': source_page,
|
||||||
'projectCode': project_code,
|
'projectCode': project_code,
|
||||||
@@ -1757,6 +1790,7 @@ def get_erp_contract_detail_cache(conn, source_page='const', project_code=''):
|
|||||||
'finalContractAmountText': row[5] or '',
|
'finalContractAmountText': row[5] or '',
|
||||||
'finalContractAmountValue': row[6] or 0,
|
'finalContractAmountValue': row[6] or 0,
|
||||||
'contractType': row[7] or '',
|
'contractType': row[7] or '',
|
||||||
|
'linkedCodes': linked_codes,
|
||||||
'contractFields': contract_fields,
|
'contractFields': contract_fields,
|
||||||
'scaleRows': scale_rows,
|
'scaleRows': scale_rows,
|
||||||
'syncedAt': row[9] or '',
|
'syncedAt': row[9] or '',
|
||||||
@@ -1768,6 +1802,156 @@ def _clean_html_text(value):
|
|||||||
return re.sub(r'\s+', ' ', text).strip()
|
return re.sub(r'\s+', ' ', text).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_text_key(value):
|
||||||
|
return re.sub(r'\s+', '', _as_text(value)).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_business_code_key(value):
|
||||||
|
text = _as_text(value).strip()
|
||||||
|
match = re.search(r'\d{2}-\d{3}', text)
|
||||||
|
return match.group(0) if match else text
|
||||||
|
|
||||||
|
|
||||||
|
def _load_linked_code_map_from_html():
|
||||||
|
html_path = Path(__file__).with_name('project-codes.html')
|
||||||
|
if not html_path.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
html_text = html_path.read_text(encoding='utf-8')
|
||||||
|
except OSError:
|
||||||
|
return {}
|
||||||
|
match = re.search(r'const\s+LINKED_CODE_BY_BUSINESS\s*=\s*(\{.*?\});', html_text, re.S)
|
||||||
|
if not match:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
parsed = json.loads(match.group(1))
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
return parsed if isinstance(parsed, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _decorate_linked_code_names(linked_codes, conn=None):
|
||||||
|
payload = dict(linked_codes or {})
|
||||||
|
close_conn = False
|
||||||
|
if conn is None:
|
||||||
|
conn = open_db_connection()
|
||||||
|
close_conn = True
|
||||||
|
try:
|
||||||
|
sales_code = _as_text(payload.get('salesCode')).strip()
|
||||||
|
design_code = _as_text(payload.get('designCode')).strip()
|
||||||
|
if sales_code and not _as_text(payload.get('salesName')).strip():
|
||||||
|
payload['salesName'] = get_project_alias_name(conn, sales_code, 'sales')
|
||||||
|
if design_code and not _as_text(payload.get('designName')).strip():
|
||||||
|
payload['designName'] = get_project_alias_name(conn, design_code, 'design')
|
||||||
|
return payload
|
||||||
|
finally:
|
||||||
|
if close_conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_erp_linked_code_rows():
|
||||||
|
session = erp_login_session()
|
||||||
|
report_url = ERP_BASE_URL.rstrip('/') + '/sys/controller/report/design_step_controller.php'
|
||||||
|
response = session.get(report_url, params={'ActionMode': 'REPORT_5'}, timeout=20)
|
||||||
|
html = response.content.decode('utf-8', errors='ignore')
|
||||||
|
tables = _extract_table_rows(html)
|
||||||
|
target_rows = []
|
||||||
|
for rows in tables:
|
||||||
|
if not rows:
|
||||||
|
continue
|
||||||
|
header = rows[0]
|
||||||
|
if '사업코드' in header and '영업코드' in header and '설계코드' in header and '시공코드' in header:
|
||||||
|
target_rows = rows
|
||||||
|
break
|
||||||
|
if not target_rows:
|
||||||
|
return []
|
||||||
|
|
||||||
|
header = target_rows[0]
|
||||||
|
header_index = {label: index for index, label in enumerate(header)}
|
||||||
|
business_index = header_index.get('사업코드')
|
||||||
|
bridge_name_index = header_index.get('교량명')
|
||||||
|
sales_index = header_index.get('영업코드')
|
||||||
|
design_index = header_index.get('설계코드')
|
||||||
|
const_index = header_index.get('시공코드')
|
||||||
|
status_index = header_index.get('진행상태')
|
||||||
|
|
||||||
|
out = []
|
||||||
|
for row in target_rows[1:]:
|
||||||
|
if not row:
|
||||||
|
continue
|
||||||
|
if business_index is None or business_index >= len(row):
|
||||||
|
continue
|
||||||
|
business_code = _as_text(row[business_index]).strip()
|
||||||
|
if not business_code or not re.match(r'^\d{2}-\d{3}', business_code):
|
||||||
|
continue
|
||||||
|
out.append({
|
||||||
|
'businessCode': business_code,
|
||||||
|
'bridgeName': _as_text(row[bridge_name_index]).strip() if bridge_name_index is not None and bridge_name_index < len(row) else '',
|
||||||
|
'status': _as_text(row[status_index]).strip() if status_index is not None and status_index < len(row) else '',
|
||||||
|
'salesCode': _as_text(row[sales_index]).strip() if sales_index is not None and sales_index < len(row) else '',
|
||||||
|
'designCode': _as_text(row[design_index]).strip() if design_index is not None and design_index < len(row) else '',
|
||||||
|
'constCode': _as_text(row[const_index]).strip() if const_index is not None and const_index < len(row) else '',
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def find_erp_linked_codes(project_code='', business_code='', project_name=''):
|
||||||
|
project_code = _as_text(project_code).strip()
|
||||||
|
business_code = _extract_business_code_key(business_code)
|
||||||
|
project_name_key = _normalize_text_key(project_name)
|
||||||
|
html_map = _load_linked_code_map_from_html()
|
||||||
|
|
||||||
|
def html_map_match():
|
||||||
|
if project_code:
|
||||||
|
for key, value in html_map.items():
|
||||||
|
if _as_text((value or {}).get('constCode')).strip() == project_code:
|
||||||
|
return _decorate_linked_code_names({
|
||||||
|
'businessCode': key,
|
||||||
|
'bridgeName': '',
|
||||||
|
'status': '',
|
||||||
|
'salesCode': _as_text((value or {}).get('salesCode')).strip(),
|
||||||
|
'designCode': _as_text((value or {}).get('designCode')).strip(),
|
||||||
|
'constCode': _as_text((value or {}).get('constCode')).strip(),
|
||||||
|
'matchedBy': 'htmlMap.constCode',
|
||||||
|
})
|
||||||
|
if business_code and business_code in html_map:
|
||||||
|
value = html_map.get(business_code) or {}
|
||||||
|
return _decorate_linked_code_names({
|
||||||
|
'businessCode': business_code,
|
||||||
|
'bridgeName': '',
|
||||||
|
'status': '',
|
||||||
|
'salesCode': _as_text(value.get('salesCode')).strip(),
|
||||||
|
'designCode': _as_text(value.get('designCode')).strip(),
|
||||||
|
'constCode': _as_text(value.get('constCode')).strip(),
|
||||||
|
'matchedBy': 'htmlMap.businessCode',
|
||||||
|
})
|
||||||
|
return None
|
||||||
|
|
||||||
|
html_hit = html_map_match()
|
||||||
|
if html_hit:
|
||||||
|
return html_hit
|
||||||
|
|
||||||
|
rows = fetch_erp_linked_code_rows()
|
||||||
|
if not rows:
|
||||||
|
return {'salesCode': '', 'salesName': '', 'designCode': '', 'designName': '', 'constCode': '', 'matchedBy': ''}
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
if project_code and _as_text(row.get('constCode')).strip() == project_code:
|
||||||
|
return _decorate_linked_code_names({**row, 'matchedBy': 'constCode'})
|
||||||
|
|
||||||
|
candidates = [row for row in rows if business_code and _as_text(row.get('businessCode')).strip() == business_code]
|
||||||
|
if len(candidates) == 1:
|
||||||
|
return _decorate_linked_code_names({**candidates[0], 'matchedBy': 'businessCode'})
|
||||||
|
if len(candidates) > 1 and project_name_key:
|
||||||
|
for row in candidates:
|
||||||
|
bridge_name_key = _normalize_text_key(row.get('bridgeName'))
|
||||||
|
if project_name_key and project_name_key in bridge_name_key:
|
||||||
|
return _decorate_linked_code_names({**row, 'matchedBy': 'businessCode+projectName'})
|
||||||
|
return _decorate_linked_code_names({**candidates[0], 'matchedBy': 'businessCode'})
|
||||||
|
|
||||||
|
return {'salesCode': '', 'salesName': '', 'designCode': '', 'designName': '', 'constCode': '', 'matchedBy': ''}
|
||||||
|
|
||||||
|
|
||||||
def _parse_site_worker_rows(html):
|
def _parse_site_worker_rows(html):
|
||||||
rows = []
|
rows = []
|
||||||
for table in re.findall(r'<table[^>]*>(.*?)</table>', html or '', re.I | re.S):
|
for table in re.findall(r'<table[^>]*>(.*?)</table>', html or '', re.I | re.S):
|
||||||
@@ -2726,11 +2910,152 @@ def replace_project_alias(conn, rows):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def replace_erp_project_alias_cache(conn, rows):
|
||||||
|
synced_at = datetime.now().isoformat(timespec='seconds')
|
||||||
|
conn.execute('DELETE FROM erp_project_alias_cache')
|
||||||
|
if rows:
|
||||||
|
conn.executemany(
|
||||||
|
'''
|
||||||
|
INSERT OR REPLACE INTO erp_project_alias_cache (sourcePage, projectCode, shortName, syncedAt)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
''',
|
||||||
|
[
|
||||||
|
(
|
||||||
|
_as_text(row.get('sourcePage')).strip().lower(),
|
||||||
|
_as_text(row.get('projectCode')).strip().upper(),
|
||||||
|
_as_text(row.get('shortName')).strip(),
|
||||||
|
synced_at,
|
||||||
|
)
|
||||||
|
for row in rows
|
||||||
|
if _as_text(row.get('sourcePage')).strip() and _as_text(row.get('projectCode')).strip()
|
||||||
|
],
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return {'count': len(rows), 'syncedAt': synced_at}
|
||||||
|
|
||||||
|
|
||||||
def get_project_alias_map(conn):
|
def get_project_alias_map(conn):
|
||||||
cur = conn.execute('SELECT projectCode, shortName FROM project_alias')
|
cur = conn.execute('SELECT projectCode, shortName FROM project_alias')
|
||||||
return {code: short for code, short in cur.fetchall() if code}
|
return {code: short for code, short in cur.fetchall() if code}
|
||||||
|
|
||||||
|
|
||||||
|
def infer_source_page_from_code(project_code=''):
|
||||||
|
code = _as_text(project_code).strip().upper()
|
||||||
|
if '-교영-' in code:
|
||||||
|
return 'sales'
|
||||||
|
if '-설계-' in code:
|
||||||
|
return 'design'
|
||||||
|
if '-시공-' in code:
|
||||||
|
return 'const'
|
||||||
|
if '-제조-' in code:
|
||||||
|
return 'make'
|
||||||
|
if '-연구-' in code:
|
||||||
|
return 'research'
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_alias_name(conn, project_code='', source_page=''):
|
||||||
|
code = _as_text(project_code).strip().upper()
|
||||||
|
page_name = _as_text(source_page).strip().lower() or infer_source_page_from_code(code)
|
||||||
|
if not code:
|
||||||
|
return ''
|
||||||
|
if page_name:
|
||||||
|
row = conn.execute(
|
||||||
|
'SELECT shortName FROM erp_project_alias_cache WHERE sourcePage = ? AND projectCode = ?',
|
||||||
|
(page_name, code),
|
||||||
|
).fetchone()
|
||||||
|
if row and row[0]:
|
||||||
|
return row[0]
|
||||||
|
row = conn.execute('SELECT shortName FROM project_alias WHERE projectCode = ?', (code,)).fetchone()
|
||||||
|
return row[0] if row and row[0] else ''
|
||||||
|
|
||||||
|
|
||||||
|
def replace_erp_linked_code_cache(conn, source_page, project_code, project_name, business_code, linked_codes):
|
||||||
|
source_page = _as_text(source_page).strip().lower() or 'const'
|
||||||
|
project_code = _as_text(project_code).strip()
|
||||||
|
if not project_code:
|
||||||
|
return None
|
||||||
|
payload = linked_codes or {}
|
||||||
|
synced_at = datetime.now().isoformat(timespec='seconds')
|
||||||
|
conn.execute(
|
||||||
|
'''
|
||||||
|
INSERT OR REPLACE INTO erp_linked_code_cache
|
||||||
|
(sourcePage, projectCode, projectName, businessCode, salesCode, salesName, designCode, designName, matchedBy, syncedAt)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
''',
|
||||||
|
(
|
||||||
|
source_page,
|
||||||
|
project_code,
|
||||||
|
_as_text(project_name).strip(),
|
||||||
|
_extract_business_code_key(business_code),
|
||||||
|
_as_text(payload.get('salesCode')).strip(),
|
||||||
|
_as_text(payload.get('salesName')).strip(),
|
||||||
|
_as_text(payload.get('designCode')).strip(),
|
||||||
|
_as_text(payload.get('designName')).strip(),
|
||||||
|
_as_text(payload.get('matchedBy')).strip(),
|
||||||
|
synced_at,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return {'sourcePage': source_page, 'projectCode': project_code, 'syncedAt': synced_at}
|
||||||
|
|
||||||
|
|
||||||
|
def sync_project_alias_caches(conn):
|
||||||
|
alias_entries = load_project_alias_entries_from_erp()
|
||||||
|
replace_project_alias(conn, [(item['projectCode'], item['shortName']) for item in alias_entries])
|
||||||
|
cache_info = replace_erp_project_alias_cache(conn, alias_entries)
|
||||||
|
return {'entries': alias_entries, 'cacheInfo': cache_info}
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild_erp_linked_code_cache(conn, source_page='const'):
|
||||||
|
source_page = _as_text(source_page).strip().lower() or 'const'
|
||||||
|
rows = conn.execute(
|
||||||
|
'''
|
||||||
|
SELECT p.projectCode,
|
||||||
|
p.projectName,
|
||||||
|
COALESCE(d.businessCode, '')
|
||||||
|
FROM erp_project_code_cache p
|
||||||
|
LEFT JOIN erp_contract_detail_cache d
|
||||||
|
ON d.sourcePage = p.sourcePage
|
||||||
|
AND d.projectCode = p.projectCode
|
||||||
|
WHERE p.sourcePage = ?
|
||||||
|
ORDER BY p.projectCode ASC
|
||||||
|
''',
|
||||||
|
(source_page,),
|
||||||
|
).fetchall()
|
||||||
|
synced_at = datetime.now().isoformat(timespec='seconds')
|
||||||
|
payload_rows = []
|
||||||
|
for project_code, project_name, business_code in rows:
|
||||||
|
linked_codes = find_erp_linked_codes(project_code, business_code, project_name)
|
||||||
|
if linked_codes.get('salesCode') or linked_codes.get('designCode'):
|
||||||
|
payload_rows.append(
|
||||||
|
(
|
||||||
|
source_page,
|
||||||
|
_as_text(project_code).strip(),
|
||||||
|
_as_text(project_name).strip(),
|
||||||
|
_extract_business_code_key(business_code),
|
||||||
|
_as_text(linked_codes.get('salesCode')).strip(),
|
||||||
|
_as_text(linked_codes.get('salesName')).strip(),
|
||||||
|
_as_text(linked_codes.get('designCode')).strip(),
|
||||||
|
_as_text(linked_codes.get('designName')).strip(),
|
||||||
|
_as_text(linked_codes.get('matchedBy')).strip(),
|
||||||
|
synced_at,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
conn.execute('DELETE FROM erp_linked_code_cache WHERE sourcePage = ?', (source_page,))
|
||||||
|
if payload_rows:
|
||||||
|
conn.executemany(
|
||||||
|
'''
|
||||||
|
INSERT OR REPLACE INTO erp_linked_code_cache
|
||||||
|
(sourcePage, projectCode, projectName, businessCode, salesCode, salesName, designCode, designName, matchedBy, syncedAt)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
''',
|
||||||
|
payload_rows,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return len(payload_rows)
|
||||||
|
|
||||||
|
|
||||||
def rebuild_work_calendar_tables(conn):
|
def rebuild_work_calendar_tables(conn):
|
||||||
conn.execute('DELETE FROM work_calendar_detail')
|
conn.execute('DELETE FROM work_calendar_detail')
|
||||||
conn.execute('DELETE FROM work_calendar_day')
|
conn.execute('DELETE FROM work_calendar_day')
|
||||||
@@ -3382,7 +3707,8 @@ def _duration_sql(alias='d'):
|
|||||||
|
|
||||||
|
|
||||||
def get_people_summary(conn, start_date, end_date):
|
def get_people_summary(conn, start_date, end_date):
|
||||||
q = f'''
|
params = (start_date, end_date, start_date, end_date)
|
||||||
|
q_with_site = f'''
|
||||||
WITH d AS (
|
WITH d AS (
|
||||||
SELECT
|
SELECT
|
||||||
MemberNo,
|
MemberNo,
|
||||||
@@ -3417,7 +3743,37 @@ def get_people_summary(conn, start_date, end_date):
|
|||||||
LEFT JOIN s ON s.MemberNo = m.MemberNo
|
LEFT JOIN s ON s.MemberNo = m.MemberNo
|
||||||
ORDER BY CASE WHEN IFNULL(m.korName,'')='' THEN 1 ELSE 0 END, korName ASC, m.MemberNo ASC
|
ORDER BY CASE WHEN IFNULL(m.korName,'')='' THEN 1 ELSE 0 END, korName ASC, m.MemberNo ASC
|
||||||
'''
|
'''
|
||||||
cur = conn.execute(q, (start_date, end_date, start_date, end_date))
|
try:
|
||||||
|
cur = conn.execute(q_with_site, params)
|
||||||
|
except sqlite3.DatabaseError as error:
|
||||||
|
if 'malformed' not in str(error).lower():
|
||||||
|
raise
|
||||||
|
q_without_site = f'''
|
||||||
|
WITH d AS (
|
||||||
|
SELECT
|
||||||
|
MemberNo,
|
||||||
|
ROUND(SUM(TotalHours), 2) AS totalHours,
|
||||||
|
COUNT(*) AS totalRows
|
||||||
|
FROM dallyproject
|
||||||
|
WHERE date(substr(EntryTime,1,10)) BETWEEN date(?) AND date(?)
|
||||||
|
GROUP BY MemberNo
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
m.MemberNo,
|
||||||
|
IFNULL(m.korName, '') AS korName,
|
||||||
|
IFNULL(m.rankName, '') AS rankName,
|
||||||
|
IFNULL(m.teamName, '') AS teamName,
|
||||||
|
IFNULL(m.retireFlag, '') AS retireFlag,
|
||||||
|
IFNULL(m.isRetired, 0) AS isRetired,
|
||||||
|
IFNULL(d.totalHours, 0) AS totalHours,
|
||||||
|
IFNULL(d.totalRows, 0) AS totalRows,
|
||||||
|
0 AS siteHours,
|
||||||
|
0 AS siteRows
|
||||||
|
FROM member m
|
||||||
|
LEFT JOIN d ON d.MemberNo = m.MemberNo
|
||||||
|
ORDER BY CASE WHEN IFNULL(m.korName,'')='' THEN 1 ELSE 0 END, korName ASC, m.MemberNo ASC
|
||||||
|
'''
|
||||||
|
cur = conn.execute(q_without_site, (start_date, end_date))
|
||||||
cols = [d[0] for d in cur.description]
|
cols = [d[0] for d in cur.description]
|
||||||
return [dict(zip(cols, row)) for row in cur.fetchall()]
|
return [dict(zip(cols, row)) for row in cur.fetchall()]
|
||||||
|
|
||||||
@@ -4267,10 +4623,49 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
refresh = q.get('refresh', ['0'])[0] in ('1', 'true', 'yes')
|
refresh = q.get('refresh', ['0'])[0] in ('1', 'true', 'yes')
|
||||||
if refresh:
|
if refresh:
|
||||||
detail = fetch_erp_contract_detail(page_name, project_code, project_name)
|
detail = fetch_erp_contract_detail(page_name, project_code, project_name)
|
||||||
run_db_write(lambda conn: replace_erp_contract_detail_cache(conn, detail))
|
def _write_refresh(conn):
|
||||||
|
replace_erp_contract_detail_cache(conn, detail)
|
||||||
|
replace_erp_linked_code_cache(
|
||||||
|
conn,
|
||||||
|
page_name,
|
||||||
|
project_code,
|
||||||
|
detail.get('projectName', '') or project_name,
|
||||||
|
detail.get('businessCode', ''),
|
||||||
|
detail.get('linkedCodes') or {},
|
||||||
|
)
|
||||||
|
run_db_write(_write_refresh)
|
||||||
with open_db_connection() as conn:
|
with open_db_connection() as conn:
|
||||||
ensure_db_schema(conn)
|
ensure_db_schema(conn)
|
||||||
cached = get_erp_contract_detail_cache(conn, page_name, project_code)
|
cached = get_erp_contract_detail_cache(conn, page_name, project_code)
|
||||||
|
if cached and (
|
||||||
|
(
|
||||||
|
not (cached.get('linkedCodes') or {}).get('salesCode')
|
||||||
|
and not (cached.get('linkedCodes') or {}).get('designCode')
|
||||||
|
) or (
|
||||||
|
(cached.get('linkedCodes') or {}).get('salesCode')
|
||||||
|
and not (cached.get('linkedCodes') or {}).get('salesName')
|
||||||
|
) or (
|
||||||
|
(cached.get('linkedCodes') or {}).get('designCode')
|
||||||
|
and not (cached.get('linkedCodes') or {}).get('designName')
|
||||||
|
)
|
||||||
|
):
|
||||||
|
linked_codes = find_erp_linked_codes(project_code, cached.get('businessCode', ''), cached.get('projectName', '') or project_name)
|
||||||
|
if linked_codes.get('salesCode') or linked_codes.get('designCode'):
|
||||||
|
cached['linkedCodes'] = linked_codes
|
||||||
|
def _write_backfill(conn):
|
||||||
|
replace_erp_contract_detail_cache(conn, cached)
|
||||||
|
replace_erp_linked_code_cache(
|
||||||
|
conn,
|
||||||
|
page_name,
|
||||||
|
project_code,
|
||||||
|
cached.get('projectName', '') or project_name,
|
||||||
|
cached.get('businessCode', ''),
|
||||||
|
linked_codes,
|
||||||
|
)
|
||||||
|
run_db_write(_write_backfill)
|
||||||
|
with open_db_connection() as conn:
|
||||||
|
ensure_db_schema(conn)
|
||||||
|
cached = get_erp_contract_detail_cache(conn, page_name, project_code)
|
||||||
if not cached:
|
if not cached:
|
||||||
return self._json(404, {'ok': False, 'error': '계약정보 캐시가 없습니다.'})
|
return self._json(404, {'ok': False, 'error': '계약정보 캐시가 없습니다.'})
|
||||||
return self._json(
|
return self._json(
|
||||||
@@ -4480,9 +4875,16 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
if parsed.path == '/api/sync-project-aliases':
|
if parsed.path == '/api/sync-project-aliases':
|
||||||
try:
|
try:
|
||||||
with sqlite3.connect(DB_PATH) as conn:
|
with sqlite3.connect(DB_PATH) as conn:
|
||||||
alias_rows = load_project_alias_from_erp()
|
ensure_db_schema(conn)
|
||||||
replace_project_alias(conn, alias_rows)
|
sync_info = sync_project_alias_caches(conn)
|
||||||
return self._json(200, {'ok': True, 'project_alias_source': 'erp', 'project_alias_loaded': len(alias_rows)})
|
linked_count = rebuild_erp_linked_code_cache(conn, 'const')
|
||||||
|
return self._json(200, {
|
||||||
|
'ok': True,
|
||||||
|
'project_alias_source': 'erp',
|
||||||
|
'project_alias_loaded': len(sync_info['entries']),
|
||||||
|
'linked_code_cache_loaded': linked_count,
|
||||||
|
'syncedAt': sync_info['cacheInfo']['syncedAt'],
|
||||||
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return self._json(500, {'ok': False, 'error': str(e)})
|
return self._json(500, {'ok': False, 'error': str(e)})
|
||||||
@@ -4503,7 +4905,11 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
q = parse_qs(parsed.query)
|
q = parse_qs(parsed.query)
|
||||||
page_name = q.get('page', ['const'])[0]
|
page_name = q.get('page', ['const'])[0]
|
||||||
result = fetch_erp_project_codes(page_name)
|
result = fetch_erp_project_codes(page_name)
|
||||||
sync_info = run_db_write(lambda conn: replace_erp_project_code_cache(conn, result['page'], result['rows']))
|
def _write_project_codes(conn):
|
||||||
|
sync_info = replace_erp_project_code_cache(conn, result['page'], result['rows'])
|
||||||
|
linked_count = rebuild_erp_linked_code_cache(conn, result['page']) if result['page'] == 'const' else 0
|
||||||
|
return sync_info, linked_count
|
||||||
|
sync_info, linked_count = run_db_write(_write_project_codes)
|
||||||
return self._json(
|
return self._json(
|
||||||
200,
|
200,
|
||||||
{
|
{
|
||||||
@@ -4511,6 +4917,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
'count': sync_info['count'],
|
'count': sync_info['count'],
|
||||||
'page': sync_info['sourcePage'],
|
'page': sync_info['sourcePage'],
|
||||||
'syncedAt': sync_info['syncedAt'],
|
'syncedAt': sync_info['syncedAt'],
|
||||||
|
'linkedCodeCacheLoaded': linked_count,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -4524,7 +4931,18 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
project_code = q.get('projectCode', [''])[0]
|
project_code = q.get('projectCode', [''])[0]
|
||||||
project_name = q.get('projectName', [''])[0]
|
project_name = q.get('projectName', [''])[0]
|
||||||
detail = fetch_erp_contract_detail(page_name, project_code, project_name)
|
detail = fetch_erp_contract_detail(page_name, project_code, project_name)
|
||||||
sync_info = run_db_write(lambda conn: replace_erp_contract_detail_cache(conn, detail))
|
def _write_contract(conn):
|
||||||
|
sync_info = replace_erp_contract_detail_cache(conn, detail)
|
||||||
|
replace_erp_linked_code_cache(
|
||||||
|
conn,
|
||||||
|
page_name,
|
||||||
|
project_code,
|
||||||
|
detail.get('projectName', '') or project_name,
|
||||||
|
detail.get('businessCode', ''),
|
||||||
|
detail.get('linkedCodes') or {},
|
||||||
|
)
|
||||||
|
return sync_info
|
||||||
|
sync_info = run_db_write(_write_contract)
|
||||||
with open_db_connection() as conn:
|
with open_db_connection() as conn:
|
||||||
ensure_db_schema(conn)
|
ensure_db_schema(conn)
|
||||||
cached = get_erp_contract_detail_cache(conn, page_name, project_code)
|
cached = get_erp_contract_detail_cache(conn, page_name, project_code)
|
||||||
@@ -4588,13 +5006,14 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
ensure_db_schema(conn)
|
ensure_db_schema(conn)
|
||||||
used = load_from_mysql_into_sqlite(conn)
|
used = load_from_mysql_into_sqlite(conn)
|
||||||
try:
|
try:
|
||||||
alias_rows = load_project_alias_from_erp()
|
sync_info = sync_project_alias_caches(conn)
|
||||||
used['project_alias_source'] = 'erp'
|
used['project_alias_source'] = 'erp'
|
||||||
replace_project_alias(conn, alias_rows)
|
used['project_alias_loaded'] = len(sync_info['entries'])
|
||||||
used['project_alias_loaded'] = len(alias_rows)
|
used['linked_code_cache_loaded'] = rebuild_erp_linked_code_cache(conn, 'const')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
used['project_alias_source'] = 'erp_failed_keep_existing'
|
used['project_alias_source'] = 'erp_failed_keep_existing'
|
||||||
used['project_alias_loaded'] = 0
|
used['project_alias_loaded'] = 0
|
||||||
|
used['linked_code_cache_loaded'] = 0
|
||||||
used['project_alias_error'] = str(e)
|
used['project_alias_error'] = str(e)
|
||||||
# member rank는 MySQL member.rankCode -> systemconfig 매핑을 우선 사용
|
# member rank는 MySQL member.rankCode -> systemconfig 매핑을 우선 사용
|
||||||
# (used['member_rank_source'], used['member_rank_loaded']는 load_from_mysql_into_sqlite에서 설정)
|
# (used['member_rank_source'], used['member_rank_loaded']는 load_from_mysql_into_sqlite에서 설정)
|
||||||
@@ -4633,9 +5052,10 @@ if __name__ == '__main__':
|
|||||||
init_db(conn)
|
init_db(conn)
|
||||||
if os.environ.get('STARTUP_MAINTENANCE', '1') not in ('0', 'false', 'False', 'no'):
|
if os.environ.get('STARTUP_MAINTENANCE', '1') not in ('0', 'false', 'False', 'no'):
|
||||||
try:
|
try:
|
||||||
alias_rows = load_project_alias_from_erp()
|
sync_info = sync_project_alias_caches(conn)
|
||||||
print(f'Loaded project aliases from ERP: {len(alias_rows)}')
|
print(f"Loaded project aliases from ERP: {len(sync_info['entries'])}")
|
||||||
replace_project_alias(conn, alias_rows)
|
linked_count = rebuild_erp_linked_code_cache(conn, 'const')
|
||||||
|
print(f'Loaded linked code cache: {linked_count}')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'Load project aliases from ERP failed, keep existing aliases: {e}')
|
print(f'Load project aliases from ERP failed, keep existing aliases: {e}')
|
||||||
# 직급은 MySQL member.rankCode -> systemconfig 매핑 기반으로 /api/load 시 반영
|
# 직급은 MySQL member.rankCode -> systemconfig 매핑 기반으로 /api/load 시 반영
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user