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(