feat: deliver schema 15 monthly settlement lifecycle

This commit is contained in:
2026-10-17 15:19:46 +09:00
parent dda76b3bbd
commit 38a854d525
79 changed files with 4003 additions and 1831 deletions
@@ -68,13 +68,65 @@ def get_monthly_cashflow_report(month: str = "", db_path: str | None = None) ->
ON lc.active=1 AND t.category=lc.category AND t.subcategory=lc.subcategory
WHERE t.status='active' AND t.transaction_date>=? AND t.transaction_date<?
GROUP BY {CASHFLOW_KIND_SQL}""", (start, next_month)).fetchall()
category_rows = conn.execute(f"""SELECT {CASHFLOW_KIND_SQL} AS cashflow_kind,
t.category, t.subcategory, t.currency,
SUM(t.amount) AS amount, COUNT(*) AS count
FROM transactions t
LEFT JOIN ledger_categories lc
ON lc.active=1 AND t.category=lc.category AND t.subcategory=lc.subcategory
WHERE t.status='active' AND t.transaction_date>=? AND t.transaction_date<?
GROUP BY {CASHFLOW_KIND_SQL}, t.category, t.subcategory, t.currency
ORDER BY cashflow_kind, amount DESC""", (start, next_month)).fetchall()
type_totals = {r["type"]: int(r["total"] or 0) for r in type_rows}
cashflow_totals = {r["cashflow_kind"]: int(r["total"] or 0) for r in cashflow_rows}
income = cashflow_totals.get("income", 0)
expenses = cashflow_totals.get("expense", 0)
net_cashflow = income - expenses
savings_rate = round(net_cashflow / income, 4) if income else None
return ok(month=month_norm, income=income, expenses=expenses, net_cashflow=net_cashflow, savings_rate=savings_rate, by_type=type_totals, by_cashflow_kind=cashflow_totals)
categories = [row_to_dict(r) for r in category_rows]
unknown_total = {}
unknown_category_items = []
for row in categories:
if row.get("cashflow_kind") == "unknown":
currency = row.get("currency") or "미지정"
unknown_total[currency] = unknown_total.get(currency, 0) + int(row.get("amount") or 0)
unknown_category_items.append(row)
return ok(month=month_norm, income=income, expenses=expenses, net_cashflow=net_cashflow, savings_rate=savings_rate, by_type=type_totals, by_cashflow_kind=cashflow_totals, categories=categories, unknown_total=unknown_total, unknown_category_items=unknown_category_items)
@read_operation
def get_settlement_cashflow_snapshot(monthly_settlement_id: str, db_path: str | None = None) -> dict[str, Any]:
"""Read only the persisted cashflow rows for a closed settlement."""
with read_db(db_path) as conn:
rows = conn.execute("""SELECT cashflow_kind, category, subcategory, amount,
transaction_count AS count, currency
FROM settlement_cashflow_snapshots
WHERE monthly_settlement_id=?
ORDER BY cashflow_kind, category, subcategory, currency""", (monthly_settlement_id,)).fetchall()
settlement = conn.execute("SELECT status FROM monthly_settlement WHERE id=?", (monthly_settlement_id,)).fetchone()
categories = [row_to_dict(row) for row in rows]
if not categories:
saved = bool(settlement and settlement["status"] == "closed")
return ok(saved=saved, categories=[], income=0 if saved else None, expenses=0 if saved else None,
net_cashflow=0 if saved else None, month="", message="저장된 현금흐름이 비어 있습니다." if saved else "저장된 현금흐름 스냅샷이 없습니다.")
by_currency = {}
for row in categories:
cur = row.get("currency") or "미지정"
bucket = by_currency.setdefault(cur, {"income": 0, "expenses": 0, "net_cashflow": 0})
if row["cashflow_kind"] == "income": bucket["income"] += int(row["amount"] or 0)
elif row["cashflow_kind"] == "expense": bucket["expenses"] += int(row["amount"] or 0)
for bucket in by_currency.values(): bucket["net_cashflow"] = bucket["income"] - bucket["expenses"]
only = next(iter(by_currency.values())) if len(by_currency) == 1 else None
unknown_total = {}
unknown_category_items = []
for row in categories:
if row.get("cashflow_kind") == "unknown":
currency = row.get("currency") or "미지정"
unknown_total[currency] = unknown_total.get(currency, 0) + int(row.get("amount") or 0)
unknown_category_items.append(row)
return ok(saved=True, categories=categories, unknown_total=unknown_total, unknown_category_items=unknown_category_items, by_currency=by_currency,
income=only["income"] if only else None,
expenses=only["expenses"] if only else None,
net_cashflow=only["net_cashflow"] if only else None, month="")
@read_operation
def get_category_spending_report(month: str = "", start_date: str = "", end_date: str = "", person: str = "", db_path: str | None = None) -> dict[str, Any]: