From 01a6e197cee7ba9b5a06e0c1ac83d3196017b3dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=ED=98=9C=EC=9D=B8?= Date: Tue, 7 Apr 2026 10:56:03 +0900 Subject: [PATCH] feat: add manage dashboard preview --- PTC/management_dashboard_preview.html | 6758 +++++++++++++++++++++++++ server/ptc_api_server.py | 1112 +++- 2 files changed, 7856 insertions(+), 14 deletions(-) create mode 100644 PTC/management_dashboard_preview.html diff --git a/PTC/management_dashboard_preview.html b/PTC/management_dashboard_preview.html new file mode 100644 index 0000000..0c4b1cf --- /dev/null +++ b/PTC/management_dashboard_preview.html @@ -0,0 +1,6758 @@ + + + + + + PTC 관리 계정 시안 + + + + + + + + +
+
+
PTC 화면을 준비하는 중입니다.
+
+ 이 메시지가 계속 보이면 브라우저에서 필수 스크립트나 API 서버 연결이 막힌 상태입니다. +
+
+ 초기 스크립트를 불러오는 중입니다. +
+
+ 접속 주소 예시: `http://localhost:4000/PTC/` 또는 `PTC/index.html?apiBase=http://localhost:4000` +
+
+
+ + + diff --git a/server/ptc_api_server.py b/server/ptc_api_server.py index dad24dc..afdbad1 100644 --- a/server/ptc_api_server.py +++ b/server/ptc_api_server.py @@ -20,8 +20,12 @@ METHOD_XLSX_PATH = BASE_DIR / "PTC공법.xlsx" DB_PATH = BASE_DIR / "db" / "ptc_local.sqlite3" FRONTEND_INDEX_PATH = BASE_DIR / "PTC" / "index.html" FRONTEND_DASHBOARD_PREVIEW_PATH = BASE_DIR / "PTC" / "dashboard_preview.html" +FRONTEND_ADMIN_DASHBOARD_PATH = BASE_DIR / "PTC" / "admin_dashboard.html" +FRONTEND_MANAGEMENT_DASHBOARD_PATH = BASE_DIR / "PTC" / "management_dashboard_preview.html" FRONTEND_CACHE: dict[str, str | int] = {"mtime_ns": -1, "html": ""} FRONTEND_PREVIEW_CACHE: dict[str, str | int] = {"mtime_ns": -1, "html": ""} +FRONTEND_ADMIN_CACHE: dict[str, str | int] = {"mtime_ns": -1, "html": ""} +FRONTEND_MANAGEMENT_CACHE: dict[str, str | int] = {"mtime_ns": -1, "html": ""} NS = {"a": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"} PROJECT_TYPE_OPTIONS = ["관리", "영업", "시공", "설계", "개발", "기술", "교휴", "기타"] METHOD_FAMILY_OPTIONS = ["복합말뚝", "합성형라멘", "강관거더", "가시설"] @@ -38,6 +42,7 @@ METHOD_FAMILY_MAP = { "RSW": "가시설", } METHOD_OPTIONS = ["HCP", "CFT", "DDH", "GC", "PB", "IT", "DR", "SGC", "RSD", "RSW"] +MANAGEMENT_ACCOUNT_CATEGORY_ORDER = ["일반운영비", "법정,의무", "외부전문,전략", "안전관리비", "인건비"] ACCOUNT_MASTER = { "711": {"project_type": "시공", "category": "자재비", "name": "강관"}, "712": {"project_type": "시공", "category": "자재비", "name": "PHC"}, @@ -158,6 +163,31 @@ SPECIAL_ACCOUNT_MASTER = { "293": {"section": "부채", "group": "비유동부채", "category": "장기차입금", "name": "장기차입금"}, "294": {"section": "부채", "group": "비유동부채", "category": "임대보증금", "name": "임대보증금"}, } +MANAGEMENT_EXCLUDED_ACCOUNT_CODES = { + "110", # 받을어음 + "124", # 매도가능증권 + "135", # 매입부가세 + "191", # 출자금 + "192", # 임차보증금 + "194", # 전도금 + "195", # 보증금 + "196", # 대여금 + "206", # 기계장치 + "208", # 차량운반구 + "212", # 비품 + "219", # 시설장치 + "258", # 매출부가세 + "259", # 선수금 + "260", # 단기차입금 + "294", # 임대보증금 + "901", # 이자수입 + "902", # 국고보조금 + "903", # 잡이익 + "904", # 배당수익 + "961", # 이자비용 + "962", # 잡손실 + "999", # 법인세등 +} ACCOUNT_STRUCTURE_TEMPLATE = [ {"section": "수입", "group": "수입", "categories": ["공사수입", "용역수입", "기타수입", "당좌자산"]}, {"section": "영업외 수지", "group": "영업외수익", "categories": ["이자수입", "잡이익", "배당수익"]}, @@ -207,6 +237,28 @@ def get_frontend_dashboard_preview_html() -> str: return str(FRONTEND_PREVIEW_CACHE["html"]) +def get_frontend_admin_dashboard_html() -> str: + if not FRONTEND_ADMIN_DASHBOARD_PATH.exists(): + raise FileNotFoundError("PTC admin dashboard frontend not found") + + mtime_ns = FRONTEND_ADMIN_DASHBOARD_PATH.stat().st_mtime_ns + if FRONTEND_ADMIN_CACHE["mtime_ns"] != mtime_ns: + FRONTEND_ADMIN_CACHE["mtime_ns"] = mtime_ns + FRONTEND_ADMIN_CACHE["html"] = FRONTEND_ADMIN_DASHBOARD_PATH.read_text(encoding="utf-8") + return str(FRONTEND_ADMIN_CACHE["html"]) + + +def get_frontend_management_dashboard_html() -> str: + if not FRONTEND_MANAGEMENT_DASHBOARD_PATH.exists(): + raise FileNotFoundError("PTC management dashboard frontend not found") + + mtime_ns = FRONTEND_MANAGEMENT_DASHBOARD_PATH.stat().st_mtime_ns + if FRONTEND_MANAGEMENT_CACHE["mtime_ns"] != mtime_ns: + FRONTEND_MANAGEMENT_CACHE["mtime_ns"] = mtime_ns + FRONTEND_MANAGEMENT_CACHE["html"] = FRONTEND_MANAGEMENT_DASHBOARD_PATH.read_text(encoding="utf-8") + return str(FRONTEND_MANAGEMENT_CACHE["html"]) + + def normalize_dashboard_family(value: str) -> str: text = (value or "").strip() if not text or text.upper() == "NULL": @@ -1084,6 +1136,18 @@ class Handler(BaseHTTPRequestHandler): return self._send_html(200, get_frontend_dashboard_preview_html()) + def _send_frontend_admin_dashboard(self) -> None: + if not FRONTEND_ADMIN_DASHBOARD_PATH.exists(): + self._send_html(404, "

PTC admin dashboard frontend not found

") + return + self._send_html(200, get_frontend_admin_dashboard_html()) + + def _send_frontend_management_dashboard(self) -> None: + if not FRONTEND_MANAGEMENT_DASHBOARD_PATH.exists(): + self._send_html(404, "

PTC management dashboard frontend not found

") + return + self._send_html(200, get_frontend_management_dashboard_html()) + def _send(self, status: int, payload: dict) -> None: body = json.dumps(payload, ensure_ascii=False).encode("utf-8") self.send_response(status) @@ -1471,6 +1535,14 @@ class Handler(BaseHTTPRequestHandler): params = parse_qs(parsed.query) conn = get_conn() try: + if parsed.path in {"/PTC-admin", "/PTC-admin/", "/PTC/admin_dashboard.html"}: + self._send_frontend_admin_dashboard() + return + + if parsed.path in {"/PTC-lab-manage", "/PTC-lab-manage/", "/PTC/management_dashboard_preview.html"}: + self._send_frontend_management_dashboard() + return + if parsed.path in {"/PTC-lab", "/PTC-lab/", "/PTC/dashboard_preview.html"}: self._send_frontend_dashboard_preview() return @@ -1683,11 +1755,26 @@ class Handler(BaseHTTPRequestHandler): if parsed.path == "/api/dashboard-prototype": project_type = params.get("project_type", ["전체"])[0] - clauses = ["t.project_code is not null", "t.project_code <> ''"] - values: list[str] = [] + date_from = params.get("date_from", [""])[0].strip() + date_to = params.get("date_to", [""])[0].strip() + exclude_asset_accounts = params.get("exclude_asset_accounts", ["0"])[0].strip() in {"1", "true", "yes"} + base_clauses = ["t.project_code is not null", "t.project_code <> ''"] + base_values: list[str] = [] if project_type and project_type != "전체": - clauses.append("coalesce(pm.project_type, t.project_type) = ?") - values.append(project_type) + base_clauses.append("coalesce(pm.project_type, t.project_type) = ?") + base_values.append(project_type) + if exclude_asset_accounts and MANAGEMENT_EXCLUDED_ACCOUNT_CODES: + excluded_placeholders = ",".join("?" for _ in MANAGEMENT_EXCLUDED_ACCOUNT_CODES) + base_clauses.append(f"coalesce(t.account_code_final, '') not in ({excluded_placeholders})") + base_values.extend(sorted(MANAGEMENT_EXCLUDED_ACCOUNT_CODES)) + clauses = list(base_clauses) + values = list(base_values) + if date_from: + clauses.append("coalesce(t.transaction_date, '') >= ?") + values.append(date_from) + if date_to: + clauses.append("coalesce(t.transaction_date, '') <= ?") + values.append(date_to) where = " where " + " and ".join(clauses) rows = conn.execute( f""" @@ -1697,6 +1784,7 @@ class Handler(BaseHTTPRequestHandler): coalesce(pm.project_type, max(t.project_type)) as project_type, coalesce(pm.construction_family, '') as construction_family, coalesce(pm.construction_method, '') as construction_method, + coalesce(pb.revenue_budget_total, 0) as revenue_budget_total, coalesce(pp.progress_rate, 0) as progress_rate, coalesce(pp.contract_pile_count, 0) as contract_pile_count, coalesce(pp.constructed_pile_count, 0) as constructed_pile_count, @@ -1705,9 +1793,15 @@ class Handler(BaseHTTPRequestHandler): count(*) as txn_count from ptc_transactions t left join project_master pm on pm.project_code = t.project_code + left join ( + select project_code, coalesce(sum(budget_amount), 0) as revenue_budget_total + from project_budget_lines + where section = '수입' + group by project_code + ) pb on pb.project_code = t.project_code left join project_progress pp on pp.project_code = t.project_code {where} - group by t.project_code, pm.project_name, pm.project_type, pm.construction_family, pm.construction_method, pp.progress_rate, pp.contract_pile_count, pp.constructed_pile_count + group by t.project_code, pm.project_name, pm.project_type, pm.construction_family, pm.construction_method, pb.revenue_budget_total, pp.progress_rate, pp.contract_pile_count, pp.constructed_pile_count """, values, ).fetchall() @@ -1775,6 +1869,7 @@ class Handler(BaseHTTPRequestHandler): progress_rate = float(row["progress_rate"] or 0) contract_pile_count = float(row["contract_pile_count"] or 0) constructed_pile_count = float(row["constructed_pile_count"] or 0) + revenue_budget_total = float(row["revenue_budget_total"] or 0) amount_bucket_key = bucket_amount(income_supply) status_key = classify_status(income_supply, expense_supply, progress_rate, contract_pile_count, constructed_pile_count) @@ -1836,6 +1931,7 @@ class Handler(BaseHTTPRequestHandler): "construction_method": method, "construction_family": family, "progress_rate": progress_rate, + "revenue_budget_total": revenue_budget_total, "income_supply": income_supply, "expense_supply": expense_supply, "profit_supply": profit_supply, @@ -1851,12 +1947,163 @@ class Handler(BaseHTTPRequestHandler): item["cells"] = [item["cells"][bucket["key"]] for bucket in amount_buckets] method_items.append(item) + loan_clauses = ["account_code_final = '196'"] + loan_values: list[str] = [] + if date_from: + loan_clauses.append("coalesce(transaction_date, '') >= ?") + loan_values.append(date_from) + if date_to: + loan_clauses.append("coalesce(transaction_date, '') <= ?") + loan_values.append(date_to) + loan_where = " where " + " and ".join(loan_clauses) + + loan_summary = conn.execute( + f""" + select + coalesce(sum(case when in_out = '입금' then supply_amount else 0 end), 0) as recovered_supply, + coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) as loaned_supply, + sum(case when in_out = '입금' then 1 else 0 end) as recovered_count, + sum(case when in_out = '출금' then 1 else 0 end) as loaned_count, + min(transaction_date) as min_date, + max(transaction_date) as max_date + from ptc_transactions + {loan_where} + """, + loan_values, + ).fetchone() + + loan_project_rows = conn.execute( + f""" + select + project_code, + max(project_name) as project_name, + coalesce(sum(case when in_out = '입금' then supply_amount else 0 end), 0) as recovered_supply, + coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) as loaned_supply, + coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) - coalesce(sum(case when in_out = '입금' then supply_amount else 0 end), 0) as outstanding_supply, + count(*) as txn_count + from ptc_transactions + {loan_where} + group by project_code + having coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) > 0 + order by outstanding_supply desc, loaned_supply desc, project_code desc + limit 8 + """, + loan_values, + ).fetchall() + + loan_vendor_rows = conn.execute( + f""" + select + coalesce(vendor_name, '거래처없음') as vendor_name, + coalesce(sum(case when in_out = '입금' then supply_amount else 0 end), 0) as recovered_supply, + coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) as loaned_supply, + coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) - coalesce(sum(case when in_out = '입금' then supply_amount else 0 end), 0) as outstanding_supply, + count(*) as txn_count + from ptc_transactions + {loan_where} + group by coalesce(vendor_name, '거래처없음') + having coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) > 0 + order by outstanding_supply desc, loaned_supply desc, vendor_name + limit 8 + """, + loan_values, + ).fetchall() + + loan_summary_dict = dict(loan_summary) if loan_summary else {} + recovered_supply = float(loan_summary_dict.get("recovered_supply") or 0) + loaned_supply = float(loan_summary_dict.get("loaned_supply") or 0) + loan_summary_dict["outstanding_supply"] = max(loaned_supply - recovered_supply, 0) + + yearly_rows = conn.execute( + f""" + select + substr(coalesce(t.transaction_date, ''), 1, 4) as year, + coalesce(sum(case when t.in_out = '입금' then t.supply_amount else 0 end), 0) as income_supply, + coalesce(sum(case when t.in_out = '출금' then t.supply_amount else 0 end), 0) as expense_supply + from ptc_transactions t + left join project_master pm on pm.project_code = t.project_code + {where} + group by substr(coalesce(t.transaction_date, ''), 1, 4) + having coalesce(substr(coalesce(t.transaction_date, ''), 1, 4), '') <> '' + order by year asc + """, + values, + ).fetchall() + yearly_items = [] + for row in yearly_rows: + income_supply = float(row["income_supply"] or 0) + expense_supply = float(row["expense_supply"] or 0) + profit_supply = income_supply - expense_supply + yearly_items.append( + { + "year": row["year"], + "income_supply": income_supply, + "expense_supply": expense_supply, + "profit_supply": profit_supply, + "margin_rate": (profit_supply / income_supply * 100) if income_supply > 0 else 0.0, + } + ) + + month_anchor = datetime.today().replace(day=1) + ongoing_month_index = (month_anchor.year * 12 + month_anchor.month - 1) - 5 + ongoing_start_year = ongoing_month_index // 12 + ongoing_start_month = (ongoing_month_index % 12) + 1 + ongoing_start = f"{ongoing_start_year:04d}-{ongoing_start_month:02d}-01" + + selected_project_codes = {str(row["project_code"] or "").strip() for row in rows if str(row["project_code"] or "").strip()} + + ongoing_rows = conn.execute( + f""" + select + t.project_code, + coalesce(pm.project_type, max(t.project_type)) as project_type, + coalesce(pm.project_name, max(t.project_name)) as project_name, + max(coalesce(t.transaction_date, '')) as latest_transaction_date + from ptc_transactions t + left join project_master pm on pm.project_code = t.project_code + {" where " + " and ".join(base_clauses)} + group by t.project_code, pm.project_name, pm.project_type + """, + base_values, + ).fetchall() + ongoing_project_count = 0 + ongoing_project_codes: list[str] = [] + for row in ongoing_rows: + project_code = str(row["project_code"] or "").strip() + project_type_value = str(row["project_type"] or "").strip() + project_name = str(row["project_name"] or "").strip() + latest_transaction_date = str(row["latest_transaction_date"] or "").strip() + if not ( + "-시공-" in project_code + and "-관리-" not in project_code + and "-설계-" not in project_code + and "시공관리" not in project_name + and project_type_value not in {"관리", "설계"} + ): + continue + if selected_project_codes and project_code not in selected_project_codes: + continue + if latest_transaction_date >= ongoing_start: + ongoing_project_count += 1 + ongoing_project_codes.append(project_code) + self._send( 200, { "overview": overview, + "yearly_overview": yearly_items, + "ongoing_project_count": ongoing_project_count, + "ongoing_project_codes": ongoing_project_codes, + "ongoing_window_start": ongoing_start, "amount_buckets": amount_buckets, "status_bands": status_bands, + "date_from": date_from, + "date_to": date_to, + "loan_risk": { + "summary": loan_summary_dict, + "projects": rows_to_dicts(loan_project_rows), + "vendors": rows_to_dicts(loan_vendor_rows), + }, "methods": method_items, "projects": sorted(project_items, key=lambda item: (-item["income_supply"], item["project_code"])), }, @@ -1873,6 +2120,8 @@ class Handler(BaseHTTPRequestHandler): if parsed.path == "/api/projects": keyword = params.get("keyword", [""])[0].strip().lower() project_type = params.get("project_type", ["전체"])[0] + date_from = params.get("date_from", [""])[0].strip() + date_to = params.get("date_to", [""])[0].strip() clauses = ["project_code is not null", "project_code <> ''"] values = [] if keyword: @@ -1890,6 +2139,12 @@ class Handler(BaseHTTPRequestHandler): if project_type and project_type != "전체": clauses.append("project_type = ?") values.append(project_type) + if date_from: + clauses.append("coalesce(transaction_date, '') >= ?") + values.append(date_from) + if date_to: + clauses.append("coalesce(transaction_date, '') <= ?") + values.append(date_to) where = " where " + " and ".join(clauses) rows = conn.execute( f""" @@ -1948,6 +2203,8 @@ class Handler(BaseHTTPRequestHandler): if parsed.path == "/api/vendors": keyword = params.get("keyword", [""])[0].strip().lower() + date_from = params.get("date_from", [""])[0].strip() + date_to = params.get("date_to", [""])[0].strip() clauses = ["coalesce(vendor_name, '') <> ''"] values: list[str] = [] if keyword: @@ -1962,6 +2219,12 @@ class Handler(BaseHTTPRequestHandler): ) like = f"%{keyword}%" values.extend([like, like, like]) + if date_from: + clauses.append("coalesce(transaction_date, '') >= ?") + values.append(date_from) + if date_to: + clauses.append("coalesce(transaction_date, '') <= ?") + values.append(date_to) where = f"where {' and '.join(clauses)}" rows = conn.execute( @@ -1984,6 +2247,8 @@ class Handler(BaseHTTPRequestHandler): if parsed.path == "/api/accounts": keyword = params.get("keyword", [""])[0].strip().lower() + date_from = params.get("date_from", [""])[0].strip() + date_to = params.get("date_to", [""])[0].strip() clauses = ["coalesce(account_code_final, '') <> ''"] values: list[str] = [] if keyword: @@ -1998,6 +2263,12 @@ class Handler(BaseHTTPRequestHandler): ) like = f"%{keyword}%" values.extend([like, like, like]) + if date_from: + clauses.append("coalesce(transaction_date, '') >= ?") + values.append(date_from) + if date_to: + clauses.append("coalesce(transaction_date, '') <= ?") + values.append(date_to) where = f"where {' and '.join(clauses)}" rows = conn.execute( @@ -2019,9 +2290,622 @@ class Handler(BaseHTTPRequestHandler): self._send(200, {"items": rows_to_dicts(rows)}) return + if parsed.path == "/api/management-accounts": + keyword = params.get("keyword", [""])[0].strip().lower() + date_from = params.get("date_from", [""])[0].strip() + date_to = params.get("date_to", [""])[0].strip() + clauses = [ + "coalesce(account_code_final, '') <> ''", + "(coalesce(project_type, '') = '관리' or coalesce(project_code, '') like '%-관리-%')", + ] + values: list[str] = [] + if MANAGEMENT_EXCLUDED_ACCOUNT_CODES: + excluded_placeholders = ",".join("?" for _ in MANAGEMENT_EXCLUDED_ACCOUNT_CODES) + clauses.append(f"coalesce(account_code_final, '') not in ({excluded_placeholders})") + values.extend(sorted(MANAGEMENT_EXCLUDED_ACCOUNT_CODES)) + if keyword: + clauses.append( + """ + ( + lower(coalesce(account_code_final, '')) like ? + or lower(coalesce(account_name_final, '')) like ? + or lower(coalesce(vendor_name, '')) like ? + ) + """ + ) + like = f"%{keyword}%" + values.extend([like, like, like]) + if date_from: + clauses.append("coalesce(transaction_date, '') >= ?") + values.append(date_from) + if date_to: + clauses.append("coalesce(transaction_date, '') <= ?") + values.append(date_to) + where = f"where {' and '.join(clauses)}" + + rows = conn.execute( + f""" + select + account_code_final as account_code, + account_name_final as account_name, + count(*) as txn_count, + sum(case when in_out = '입금' then 1 else 0 end) as income_count, + sum(case when in_out = '출금' then 1 else 0 end) as expense_count, + coalesce(sum(case when in_out = '입금' then supply_amount else 0 end), 0) as income_supply_sum, + coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) as expense_supply_sum, + coalesce(sum(supply_amount), 0) as supply_sum, + min(transaction_date) as min_date, + max(transaction_date) as max_date + from ptc_transactions + {where} + group by account_code_final, account_name_final + order by cast(account_code_final as integer) asc, account_code_final asc + """, + values, + ).fetchall() + self._send(200, {"items": rows_to_dicts(rows)}) + return + + if parsed.path == "/api/management-overview": + date_from = params.get("date_from", [""])[0].strip() + date_to = params.get("date_to", [""])[0].strip() + clauses = [ + "(coalesce(project_type, '') = '관리' or coalesce(project_code, '') like '%-관리-%')", + "coalesce(account_code_final, '') <> ''", + ] + values: list[str] = [] + if MANAGEMENT_EXCLUDED_ACCOUNT_CODES: + excluded_placeholders = ",".join("?" for _ in MANAGEMENT_EXCLUDED_ACCOUNT_CODES) + clauses.append(f"coalesce(account_code_final, '') not in ({excluded_placeholders})") + values.extend(sorted(MANAGEMENT_EXCLUDED_ACCOUNT_CODES)) + if date_from: + clauses.append("coalesce(transaction_date, '') >= ?") + values.append(date_from) + if date_to: + clauses.append("coalesce(transaction_date, '') <= ?") + values.append(date_to) + where = f"where {' and '.join(clauses)}" + + rows = conn.execute( + f""" + select + substr(coalesce(transaction_date, ''), 1, 4) as year, + account_code_final as account_code, + coalesce(sum(case when in_out = '입금' then supply_amount else 0 end), 0) as income_supply, + coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) as expense_supply + from ptc_transactions + {where} + group by substr(coalesce(transaction_date, ''), 1, 4), account_code_final + order by year asc, account_code_final asc + """, + values, + ).fetchall() + + excluded_rows = [] + if MANAGEMENT_EXCLUDED_ACCOUNT_CODES: + excluded_placeholders = ",".join("?" for _ in MANAGEMENT_EXCLUDED_ACCOUNT_CODES) + excluded_clauses = [ + "(coalesce(project_type, '') = '관리' or coalesce(project_code, '') like '%-관리-%')", + "coalesce(account_code_final, '') in (" + excluded_placeholders + ")", + ] + excluded_values: list[str] = list(sorted(MANAGEMENT_EXCLUDED_ACCOUNT_CODES)) + if date_from: + excluded_clauses.append("coalesce(transaction_date, '') >= ?") + excluded_values.append(date_from) + if date_to: + excluded_clauses.append("coalesce(transaction_date, '') <= ?") + excluded_values.append(date_to) + excluded_where = f"where {' and '.join(excluded_clauses)}" + excluded_rows = conn.execute( + f""" + select + substr(coalesce(transaction_date, ''), 1, 4) as year, + account_code_final as account_code, + coalesce(max(account_name_final), '') as account_name, + coalesce(sum(case when in_out = '입금' then supply_amount else 0 end), 0) as income_supply, + coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) as expense_supply, + count(*) as txn_count + from ptc_transactions + {excluded_where} + group by substr(coalesce(transaction_date, ''), 1, 4), account_code_final + order by year asc, cast(account_code_final as integer) asc, account_code_final asc + """, + excluded_values, + ).fetchall() + + by_year: dict[str, dict] = {} + for row in rows: + year = (row["year"] or "").strip() or "미상" + account_code = (row["account_code"] or "").strip() + master = ACCOUNT_MASTER.get(account_code) or {} + if master.get("project_type") != "관리": + continue + category = master.get("category") or "기타" + if category not in MANAGEMENT_ACCOUNT_CATEGORY_ORDER: + continue + if year not in by_year: + by_year[year] = { + "year": year, + "income_supply": 0.0, + "total_expense": 0.0, + "categories": {key: 0.0 for key in MANAGEMENT_ACCOUNT_CATEGORY_ORDER}, + "excluded_total": 0.0, + "excluded_income_total": 0.0, + "excluded_expense_total": 0.0, + "excluded_accounts": [], + } + income_amount = float(row["income_supply"] or 0) + amount = float(row["expense_supply"] or 0) + by_year[year]["income_supply"] += income_amount + by_year[year]["total_expense"] += amount + by_year[year]["categories"][category] += amount + + for row in excluded_rows: + year = (row["year"] or "").strip() or "미상" + if year not in by_year: + by_year[year] = { + "year": year, + "income_supply": 0.0, + "total_expense": 0.0, + "categories": {key: 0.0 for key in MANAGEMENT_ACCOUNT_CATEGORY_ORDER}, + "excluded_total": 0.0, + "excluded_income_total": 0.0, + "excluded_expense_total": 0.0, + "excluded_accounts": [], + } + income_supply = float(row["income_supply"] or 0) + expense_supply = float(row["expense_supply"] or 0) + total_supply = income_supply + expense_supply + by_year[year]["excluded_total"] += total_supply + by_year[year]["excluded_income_total"] += income_supply + by_year[year]["excluded_expense_total"] += expense_supply + by_year[year]["excluded_accounts"].append( + { + "account_code": row["account_code"], + "account_name": row["account_name"] or (ACCOUNT_MASTER.get((row["account_code"] or "").strip()) or {}).get("name") or "", + "income_supply": income_supply, + "expense_supply": expense_supply, + "total_supply": total_supply, + "txn_count": int(row["txn_count"] or 0), + } + ) + + items = [] + for year in sorted(by_year.keys()): + item = by_year[year] + items.append({ + "year": item["year"], + "income_supply": item["income_supply"], + "total_expense": item["total_expense"], + "categories": [ + {"name": category, "amount": item["categories"][category]} + for category in MANAGEMENT_ACCOUNT_CATEGORY_ORDER + ], + "excluded_total": item["excluded_total"], + "excluded_income_total": item["excluded_income_total"], + "excluded_expense_total": item["excluded_expense_total"], + "excluded_accounts": item["excluded_accounts"], + }) + + company_clauses = ["coalesce(account_code_final, '') <> ''"] + company_values: list[str] = [] + if MANAGEMENT_EXCLUDED_ACCOUNT_CODES: + company_excluded_placeholders = ",".join("?" for _ in MANAGEMENT_EXCLUDED_ACCOUNT_CODES) + company_clauses.append(f"coalesce(account_code_final, '') not in ({company_excluded_placeholders})") + company_values.extend(sorted(MANAGEMENT_EXCLUDED_ACCOUNT_CODES)) + if date_from: + company_clauses.append("coalesce(transaction_date, '') >= ?") + company_values.append(date_from) + if date_to: + company_clauses.append("coalesce(transaction_date, '') <= ?") + company_values.append(date_to) + company_where = f"where {' and '.join(company_clauses)}" + + yearly_profit_rows = conn.execute( + f""" + select + substr(coalesce(transaction_date, ''), 1, 4) as year, + coalesce(sum(case when in_out = '입금' then supply_amount else 0 end), 0) as income_supply, + coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) as expense_supply + from ptc_transactions + {company_where} + group by substr(coalesce(transaction_date, ''), 1, 4) + having coalesce(substr(coalesce(transaction_date, ''), 1, 4), '') <> '' + order by year asc + """, + company_values, + ).fetchall() + yearly_profit_items = [] + for row in yearly_profit_rows: + income_supply = float(row["income_supply"] or 0) + expense_supply = float(row["expense_supply"] or 0) + yearly_profit_items.append( + { + "year": row["year"], + "income_supply": income_supply, + "expense_supply": expense_supply, + "profit_supply": income_supply - expense_supply, + } + ) + + construction_clauses = [ + "coalesce(account_code_final, '') <> ''", + "coalesce(project_code, '') like '%-시공-%'", + "coalesce(project_name, '') not like '%시공관리%'", + "coalesce(project_type, '') not in ('관리', '설계')", + ] + construction_values: list[str] = [] + if MANAGEMENT_EXCLUDED_ACCOUNT_CODES: + construction_excluded_placeholders = ",".join("?" for _ in MANAGEMENT_EXCLUDED_ACCOUNT_CODES) + construction_clauses.append(f"coalesce(account_code_final, '') not in ({construction_excluded_placeholders})") + construction_values.extend(sorted(MANAGEMENT_EXCLUDED_ACCOUNT_CODES)) + if date_from: + construction_clauses.append("coalesce(transaction_date, '') >= ?") + construction_values.append(date_from) + if date_to: + construction_clauses.append("coalesce(transaction_date, '') <= ?") + construction_values.append(date_to) + construction_where = f"where {' and '.join(construction_clauses)}" + + yearly_construction_rows = conn.execute( + f""" + select + substr(coalesce(transaction_date, ''), 1, 4) as year, + coalesce(sum(case when in_out = '입금' then supply_amount else 0 end), 0) as income_supply, + coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) as expense_supply + from ptc_transactions + {construction_where} + group by substr(coalesce(transaction_date, ''), 1, 4) + having coalesce(substr(coalesce(transaction_date, ''), 1, 4), '') <> '' + order by year asc + """, + construction_values, + ).fetchall() + yearly_construction_margin_items = [] + for row in yearly_construction_rows: + income_supply = float(row["income_supply"] or 0) + expense_supply = float(row["expense_supply"] or 0) + profit_supply = income_supply - expense_supply + yearly_construction_margin_items.append( + { + "year": row["year"], + "income_supply": income_supply, + "expense_supply": expense_supply, + "profit_supply": profit_supply, + "margin_rate": (profit_supply / income_supply * 100) if income_supply > 0 else 0.0, + } + ) + + self._send( + 200, + { + "items": items, + "category_order": MANAGEMENT_ACCOUNT_CATEGORY_ORDER, + "yearly_profit_items": yearly_profit_items, + "yearly_construction_margin_items": yearly_construction_margin_items, + }, + ) + return + + if parsed.path == "/api/management-company-overview": + date_from = params.get("date_from", [""])[0].strip() + date_to = params.get("date_to", [""])[0].strip() + project_type_case = """ + case + when coalesce(t.project_code, '') like '%-시공-%' then '시공' + when coalesce(t.project_code, '') like '%-영업-%' then '영업' + when coalesce(t.project_code, '') like '%-설계-%' then '설계' + when coalesce(t.project_code, '') like '%-관리-%' then '관리' + else coalesce(pm.project_type, t.project_type, '미지정') + end + """ + clauses = ["coalesce(t.account_code_final, '') <> ''"] + values: list[str] = [] + if MANAGEMENT_EXCLUDED_ACCOUNT_CODES: + excluded_placeholders = ",".join("?" for _ in MANAGEMENT_EXCLUDED_ACCOUNT_CODES) + clauses.append(f"coalesce(t.account_code_final, '') not in ({excluded_placeholders})") + values.extend(sorted(MANAGEMENT_EXCLUDED_ACCOUNT_CODES)) + if date_from: + clauses.append("coalesce(t.transaction_date, '') >= ?") + values.append(date_from) + if date_to: + clauses.append("coalesce(t.transaction_date, '') <= ?") + values.append(date_to) + where = f"where {' and '.join(clauses)}" + + rows = conn.execute( + f""" + select + substr(coalesce(t.transaction_date, ''), 1, 4) as year, + {project_type_case} as project_type, + coalesce(sum(case when t.in_out = '입금' then t.supply_amount else 0 end), 0) as income_supply, + coalesce(sum(case when t.in_out = '출금' then t.supply_amount else 0 end), 0) as expense_supply + from ptc_transactions t + left join project_master pm on pm.project_code = t.project_code + {where} + group by substr(coalesce(t.transaction_date, ''), 1, 4), {project_type_case} + having coalesce(substr(coalesce(t.transaction_date, ''), 1, 4), '') <> '' + order by year asc, project_type asc + """, + values, + ).fetchall() + + preferred_types = ["시공", "영업", "설계", "관리", "미지정"] + by_year: dict[str, dict] = {} + discovered_types: set[str] = set() + + for row in rows: + year = (row["year"] or "").strip() or "미상" + project_type = (row["project_type"] or "").strip() or "미지정" + discovered_types.add(project_type) + if year not in by_year: + by_year[year] = { + "year": year, + "income_supply": 0.0, + "expense_supply": 0.0, + "types": {}, + } + income_supply = float(row["income_supply"] or 0) + expense_supply = float(row["expense_supply"] or 0) + by_year[year]["income_supply"] += income_supply + by_year[year]["expense_supply"] += expense_supply + by_year[year]["types"][project_type] = { + "project_type": project_type, + "income_supply": income_supply, + "expense_supply": expense_supply, + } + + ordered_types = [item for item in preferred_types if item in discovered_types] + ordered_types.extend(sorted(discovered_types - set(ordered_types))) + + items = [] + for year in sorted(by_year.keys()): + item = by_year[year] + total_income = float(item["income_supply"] or 0) + total_expense = float(item["expense_supply"] or 0) + type_items = [] + for project_type in ordered_types: + type_item = item["types"].get(project_type) or { + "project_type": project_type, + "income_supply": 0.0, + "expense_supply": 0.0, + } + type_items.append( + { + **type_item, + "income_ratio": (float(type_item["income_supply"] or 0) / total_income * 100) if total_income > 0 else 0.0, + "expense_ratio": (float(type_item["expense_supply"] or 0) / total_expense * 100) if total_expense > 0 else 0.0, + } + ) + items.append( + { + "year": year, + "income_supply": total_income, + "expense_supply": total_expense, + "profit_supply": total_income - total_expense, + "margin_rate": ((total_income - total_expense) / total_income * 100) if total_income > 0 else 0.0, + "types": type_items, + } + ) + + self._send(200, {"project_type_order": ordered_types, "items": items}) + return + + if parsed.path == "/api/management-overview-accounts": + year = params.get("year", [""])[0].strip() + category = params.get("category", [""])[0].strip() + date_from = params.get("date_from", [""])[0].strip() + date_to = params.get("date_to", [""])[0].strip() + if not year or not category: + self._send(400, {"ok": False, "message": "year and category are required"}) + return + if category not in MANAGEMENT_ACCOUNT_CATEGORY_ORDER: + self._send(400, {"ok": False, "message": "invalid category"}) + return + + category_codes = [ + code + for code, meta in ACCOUNT_MASTER.items() + if meta.get("project_type") == "관리" and meta.get("category") == category + ] + if not category_codes: + self._send(200, {"items": []}) + return + + placeholders = ",".join("?" for _ in category_codes) + clauses = [ + "(coalesce(project_type, '') = '관리' or coalesce(project_code, '') like '%-관리-%')", + "in_out = '출금'", + "substr(coalesce(transaction_date, ''), 1, 4) = ?", + f"account_code_final in ({placeholders})", + ] + values: list[str] = [year, *category_codes] + if MANAGEMENT_EXCLUDED_ACCOUNT_CODES: + excluded_placeholders = ",".join("?" for _ in MANAGEMENT_EXCLUDED_ACCOUNT_CODES) + clauses.append(f"coalesce(account_code_final, '') not in ({excluded_placeholders})") + values.extend(sorted(MANAGEMENT_EXCLUDED_ACCOUNT_CODES)) + if date_from: + clauses.append("coalesce(transaction_date, '') >= ?") + values.append(date_from) + if date_to: + clauses.append("coalesce(transaction_date, '') <= ?") + values.append(date_to) + where = " where " + " and ".join(clauses) + + rows = conn.execute( + f""" + select + account_code_final as account_code, + max(account_name_final) as account_name, + count(*) as transaction_count, + coalesce(sum(supply_amount), 0) as expense_amount + from ptc_transactions + {where} + group by account_code_final + order by expense_amount desc, account_code_final asc + """, + values, + ).fetchall() + self._send(200, {"items": rows_to_dicts(rows)}) + return + + if parsed.path == "/api/management-company-accounts": + year = params.get("year", [""])[0].strip() + project_type = params.get("project_type", [""])[0].strip() + if not year or not project_type: + self._send(400, {"ok": False, "message": "year and project_type are required"}) + return + project_type_case = """ + case + when coalesce(t.project_code, '') like '%-시공-%' then '시공' + when coalesce(t.project_code, '') like '%-영업-%' then '영업' + when coalesce(t.project_code, '') like '%-설계-%' then '설계' + when coalesce(t.project_code, '') like '%-관리-%' then '관리' + else coalesce(pm.project_type, t.project_type, '미지정') + end + """ + + clauses = [ + "substr(coalesce(t.transaction_date, ''), 1, 4) = ?", + "coalesce(t.account_code_final, '') <> ''", + f"{project_type_case} = ?", + ] + values: list[str] = [year, project_type] + if MANAGEMENT_EXCLUDED_ACCOUNT_CODES: + excluded_placeholders = ",".join("?" for _ in MANAGEMENT_EXCLUDED_ACCOUNT_CODES) + clauses.append(f"coalesce(t.account_code_final, '') not in ({excluded_placeholders})") + values.extend(sorted(MANAGEMENT_EXCLUDED_ACCOUNT_CODES)) + where = f"where {' and '.join(clauses)}" + + rows = conn.execute( + f""" + select + t.account_code_final as account_code, + max(t.account_name_final) as account_name, + sum(case when t.in_out = '입금' then 1 else 0 end) as income_count, + sum(case when t.in_out = '출금' then 1 else 0 end) as expense_count, + coalesce(sum(case when t.in_out = '입금' then t.supply_amount else 0 end), 0) as income_supply, + coalesce(sum(case when t.in_out = '출금' then t.supply_amount else 0 end), 0) as expense_supply, + count(*) as txn_count + from ptc_transactions t + left join project_master pm on pm.project_code = t.project_code + {where} + group by t.account_code_final + order by (coalesce(sum(case when t.in_out = '입금' then t.supply_amount else 0 end), 0) + + coalesce(sum(case when t.in_out = '출금' then t.supply_amount else 0 end), 0)) desc, + t.account_code_final asc + """, + values, + ).fetchall() + self._send(200, {"items": rows_to_dicts(rows)}) + return + + if parsed.path == "/api/company-account-detail": + year = params.get("year", [""])[0].strip() + project_type = params.get("project_type", [""])[0].strip() + account_code = params.get("account_code", [""])[0].strip() + if not year or not project_type or not account_code: + self._send(400, {"ok": False, "message": "year, project_type and account_code are required"}) + return + project_type_case = """ + case + when coalesce(t.project_code, '') like '%-시공-%' then '시공' + when coalesce(t.project_code, '') like '%-영업-%' then '영업' + when coalesce(t.project_code, '') like '%-설계-%' then '설계' + when coalesce(t.project_code, '') like '%-관리-%' then '관리' + else coalesce(pm.project_type, t.project_type, '미지정') + end + """ + + detail_clauses = [ + "substr(coalesce(t.transaction_date, ''), 1, 4) = ?", + f"{project_type_case} = ?", + "coalesce(t.account_code_final, '') = ?", + ] + detail_values: list[str] = [year, project_type, account_code] + if MANAGEMENT_EXCLUDED_ACCOUNT_CODES: + excluded_placeholders = ",".join("?" for _ in MANAGEMENT_EXCLUDED_ACCOUNT_CODES) + detail_clauses.append(f"coalesce(t.account_code_final, '') not in ({excluded_placeholders})") + detail_values.extend(sorted(MANAGEMENT_EXCLUDED_ACCOUNT_CODES)) + detail_where = " where " + " and ".join(detail_clauses) + + summary = conn.execute( + f""" + select + t.account_code_final as account_code, + max(t.account_name_final) as account_name, + count(*) as txn_count, + sum(case when t.in_out = '입금' then 1 else 0 end) as income_count, + sum(case when t.in_out = '출금' then 1 else 0 end) as expense_count, + coalesce(sum(case when t.in_out = '입금' then t.supply_amount else 0 end), 0) as income_supply_sum, + coalesce(sum(case when t.in_out = '출금' then t.supply_amount else 0 end), 0) as expense_supply_sum, + coalesce(sum(t.supply_amount), 0) as supply_sum, + min(t.transaction_date) as min_date, + max(t.transaction_date) as max_date + from ptc_transactions t + left join project_master pm on pm.project_code = t.project_code + {detail_where} + group by t.account_code_final + """, + detail_values, + ).fetchone() + + project_rows = conn.execute( + f""" + select + t.project_code, + max(t.project_name) as project_name, + count(*) as txn_count, + sum(case when t.in_out = '입금' then 1 else 0 end) as income_count, + sum(case when t.in_out = '출금' then 1 else 0 end) as expense_count, + coalesce(sum(case when t.in_out = '입금' then t.supply_amount else 0 end), 0) as income_supply_sum, + coalesce(sum(case when t.in_out = '출금' then t.supply_amount else 0 end), 0) as expense_supply_sum, + coalesce(sum(t.supply_amount), 0) as supply_sum + from ptc_transactions t + left join project_master pm on pm.project_code = t.project_code + {detail_where} + group by t.project_code + order by supply_sum desc, t.project_code desc + limit 50 + """, + detail_values, + ).fetchall() + + transaction_rows = conn.execute( + f""" + select + t.source_row_no, + t.transaction_date, + t.in_out, + t.project_code, + t.project_name, + t.vendor_name, + t.description, + t.supply_amount + from ptc_transactions t + left join project_master pm on pm.project_code = t.project_code + {detail_where} + order by t.transaction_date desc, t.source_row_no desc + limit 100 + """, + detail_values, + ).fetchall() + + self._send( + 200, + { + "summary": dict(summary) if summary else None, + "projects": rows_to_dicts(project_rows), + "transactions": rows_to_dicts(transaction_rows), + }, + ) + return + if parsed.path == "/api/account-detail": account_code = params.get("account_code", [""])[0].strip() project_code = params.get("project_code", [""])[0].strip() + date_from = params.get("date_from", [""])[0].strip() + date_to = params.get("date_to", [""])[0].strip() if not account_code: self._send(400, {"ok": False, "message": "account_code is required"}) return @@ -2031,6 +2915,12 @@ class Handler(BaseHTTPRequestHandler): if project_code: detail_clauses.append("project_code = ?") detail_values.append(project_code) + if date_from: + detail_clauses.append("coalesce(transaction_date, '') >= ?") + detail_values.append(date_from) + if date_to: + detail_clauses.append("coalesce(transaction_date, '') <= ?") + detail_values.append(date_to) detail_where = " where " + " and ".join(detail_clauses) summary = conn.execute( @@ -2054,7 +2944,7 @@ class Handler(BaseHTTPRequestHandler): ).fetchone() project_rows = conn.execute( - """ + f""" select project_code, max(project_name) as project_name, @@ -2065,12 +2955,12 @@ class Handler(BaseHTTPRequestHandler): coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) as expense_supply_sum, coalesce(sum(supply_amount), 0) as supply_sum from ptc_transactions - where account_code_final = ? + {detail_where} group by project_code order by supply_sum desc, project_code limit 30 """, - (account_code,), + detail_values, ).fetchall() vendor_rows = conn.execute( @@ -2123,10 +3013,198 @@ class Handler(BaseHTTPRequestHandler): ) return + if parsed.path == "/api/management-account-detail": + account_code = params.get("account_code", [""])[0].strip() + date_from = params.get("date_from", [""])[0].strip() + date_to = params.get("date_to", [""])[0].strip() + if not account_code: + self._send(400, {"ok": False, "message": "account_code is required"}) + return + + detail_clauses = [ + "account_code_final = ?", + "(coalesce(project_type, '') = '관리' or coalesce(project_code, '') like '%-관리-%')", + ] + detail_values: list[str] = [account_code] + if MANAGEMENT_EXCLUDED_ACCOUNT_CODES: + excluded_placeholders = ",".join("?" for _ in MANAGEMENT_EXCLUDED_ACCOUNT_CODES) + detail_clauses.append(f"coalesce(account_code_final, '') not in ({excluded_placeholders})") + detail_values.extend(sorted(MANAGEMENT_EXCLUDED_ACCOUNT_CODES)) + if date_from: + detail_clauses.append("coalesce(transaction_date, '') >= ?") + detail_values.append(date_from) + if date_to: + detail_clauses.append("coalesce(transaction_date, '') <= ?") + detail_values.append(date_to) + detail_where = " where " + " and ".join(detail_clauses) + + summary = conn.execute( + f""" + select + account_code_final as account_code, + account_name_final as account_name, + count(*) as txn_count, + sum(case when in_out = '입금' then 1 else 0 end) as income_count, + sum(case when in_out = '출금' then 1 else 0 end) as expense_count, + coalesce(sum(case when in_out = '입금' then supply_amount else 0 end), 0) as income_supply_sum, + coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) as expense_supply_sum, + coalesce(sum(supply_amount), 0) as supply_sum, + min(transaction_date) as min_date, + max(transaction_date) as max_date + from ptc_transactions + {detail_where} + group by account_code_final, account_name_final + """, + detail_values, + ).fetchone() + + project_rows = conn.execute( + f""" + select + project_code, + max(project_name) as project_name, + count(*) as txn_count, + sum(case when in_out = '입금' then 1 else 0 end) as income_count, + sum(case when in_out = '출금' then 1 else 0 end) as expense_count, + coalesce(sum(case when in_out = '입금' then supply_amount else 0 end), 0) as income_supply_sum, + coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) as expense_supply_sum, + coalesce(sum(supply_amount), 0) as supply_sum + from ptc_transactions + {detail_where} + group by project_code + order by supply_sum desc, project_code desc + limit 50 + """, + detail_values, + ).fetchall() + + transaction_rows = conn.execute( + f""" + select + source_row_no, + transaction_date, + in_out, + project_code, + project_name, + vendor_name, + department_name, + description, + supply_amount + from ptc_transactions + {detail_where} + order by transaction_date desc, source_row_no desc + limit 100 + """, + detail_values, + ).fetchall() + + self._send( + 200, + { + "summary": dict(summary) if summary else None, + "projects": rows_to_dicts(project_rows), + "transactions": rows_to_dicts(transaction_rows), + }, + ) + return + + if parsed.path == "/api/management-excluded-account-detail": + account_code = params.get("account_code", [""])[0].strip() + date_from = params.get("date_from", [""])[0].strip() + date_to = params.get("date_to", [""])[0].strip() + if not account_code: + self._send(400, {"ok": False, "message": "account_code is required"}) + return + + detail_clauses = [ + "account_code_final = ?", + "(coalesce(project_type, '') = '관리' or coalesce(project_code, '') like '%-관리-%')", + ] + detail_values: list[str] = [account_code] + if date_from: + detail_clauses.append("coalesce(transaction_date, '') >= ?") + detail_values.append(date_from) + if date_to: + detail_clauses.append("coalesce(transaction_date, '') <= ?") + detail_values.append(date_to) + detail_where = " where " + " and ".join(detail_clauses) + + summary = conn.execute( + f""" + select + account_code_final as account_code, + account_name_final as account_name, + count(*) as txn_count, + sum(case when in_out = '입금' then 1 else 0 end) as income_count, + sum(case when in_out = '출금' then 1 else 0 end) as expense_count, + coalesce(sum(case when in_out = '입금' then supply_amount else 0 end), 0) as income_supply_sum, + coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) as expense_supply_sum, + coalesce(sum(supply_amount), 0) as supply_sum, + min(transaction_date) as min_date, + max(transaction_date) as max_date + from ptc_transactions + {detail_where} + group by account_code_final, account_name_final + """, + detail_values, + ).fetchone() + + project_rows = conn.execute( + f""" + select + project_code, + max(project_name) as project_name, + count(*) as txn_count, + sum(case when in_out = '입금' then 1 else 0 end) as income_count, + sum(case when in_out = '출금' then 1 else 0 end) as expense_count, + coalesce(sum(case when in_out = '입금' then supply_amount else 0 end), 0) as income_supply_sum, + coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) as expense_supply_sum, + coalesce(sum(supply_amount), 0) as supply_sum + from ptc_transactions + {detail_where} + group by project_code + order by supply_sum desc, project_code desc + limit 50 + """, + detail_values, + ).fetchall() + + transaction_rows = conn.execute( + f""" + select + source_row_no, + transaction_date, + in_out, + project_code, + project_name, + vendor_name, + department_name, + description, + supply_amount + from ptc_transactions + {detail_where} + order by transaction_date desc, source_row_no desc + limit 100 + """, + detail_values, + ).fetchall() + + self._send( + 200, + { + "summary": dict(summary) if summary else None, + "projects": rows_to_dicts(project_rows), + "transactions": rows_to_dicts(transaction_rows), + }, + ) + return + if parsed.path == "/api/vendor-detail": vendor_name = params.get("vendor_name", [""])[0].strip() project_code = params.get("project_code", [""])[0].strip() account_code = params.get("account_code", [""])[0].strip() + date_from = params.get("date_from", [""])[0].strip() + date_to = params.get("date_to", [""])[0].strip() if not vendor_name: self._send(400, {"ok": False, "message": "vendor_name is required"}) return @@ -2139,10 +3217,16 @@ class Handler(BaseHTTPRequestHandler): if account_code: detail_clauses.append("account_code_final = ?") detail_values.append(account_code) + if date_from: + detail_clauses.append("coalesce(transaction_date, '') >= ?") + detail_values.append(date_from) + if date_to: + detail_clauses.append("coalesce(transaction_date, '') <= ?") + detail_values.append(date_to) detail_where = " where " + " and ".join(detail_clauses) summary = conn.execute( - """ + f""" select vendor_name, count(*) as txn_count, @@ -2154,14 +3238,14 @@ class Handler(BaseHTTPRequestHandler): min(transaction_date) as min_date, max(transaction_date) as max_date from ptc_transactions - where vendor_name = ? + {detail_where} group by vendor_name """, - (vendor_name,), + detail_values, ).fetchone() project_rows = conn.execute( - """ + f""" select project_code, max(project_name) as project_name, @@ -2172,12 +3256,12 @@ class Handler(BaseHTTPRequestHandler): coalesce(sum(case when in_out = '출금' then supply_amount else 0 end), 0) as expense_supply_sum, coalesce(sum(supply_amount), 0) as supply_sum from ptc_transactions - where vendor_name = ? + {detail_where} group by project_code order by supply_sum desc, project_code limit 20 """, - (vendor_name,), + detail_values, ).fetchall() account_rows = conn.execute(