from __future__ import annotations import json import sys from pathlib import Path from typing import Any from openpyxl import load_workbook from sqlalchemy import bindparam, create_engine, text BASE_DIR = Path(__file__).resolve().parent.parent if str(BASE_DIR) not in sys.path: sys.path.insert(0, str(BASE_DIR)) from wehago_compare import ( WEHAGO_SOURCE_ROOT, _fallback_metric_counts_from_db, compute_file_hash, detect_file_kind, import_ledger_rows, import_voucher_rows, infer_year_hint, init_wehago_compare_db, rebuild_comparison_results, upsert_source_file, ) from runtime_config import DB_PATH ENGINE = create_engine(f"sqlite:///{DB_PATH}", connect_args={"check_same_thread": False}) LEDGER_FILES = { 2022: WEHAGO_SOURCE_ROOT / "data_download/2022/ledger_2022_20220101_20221231_260507.xlsx", 2023: WEHAGO_SOURCE_ROOT / "data_download/wehago_account_ledger_20260506_2023_compare/ledger_2023_20230101_20231231_260506.xlsx", 2024: WEHAGO_SOURCE_ROOT / "data_download/wehago_account_ledger_20260506_2024_compare/ledger_2024_20240101_20241231_260504.xlsx", 2025: WEHAGO_SOURCE_ROOT / "data_download/wehago_account_ledger_20260423/ledger_2025_20250101_20251231_260423.xlsx", } VOUCHER_FILES = [ { "path": WEHAGO_SOURCE_ROOT / "voucher_sort_20220101_20221231_260507.xlsx", "expected_year": 2022, "year_hint": 2022, "allowed_years": {2022}, }, { "path": WEHAGO_SOURCE_ROOT / "voucher_sort_20230101_20251231_260424.xlsx", "expected_year": 2023, "year_hint": None, "allowed_years": {2023, 2024}, }, { "path": WEHAGO_SOURCE_ROOT / "data_download/wehago_account_ledger_20260423/voucher_2025_20250101_20251231_260422.xlsx", "expected_year": 2025, "year_hint": 2025, "allowed_years": {2025}, }, ] _DEFAULT_YEAR_HINT = object() def _load_sheet(path: Path): workbook = load_workbook(path, read_only=True, data_only=True) return workbook, workbook.worksheets[0] def _delete_year_data(conn, *, ledger_years: set[int], voucher_years: set[int]) -> dict[str, int]: ledger_deleted = conn.execute( text("DELETE FROM wehago_ledger_rows WHERE fiscal_year IN :years").bindparams(bindparam("years", expanding=True)), {"years": sorted(ledger_years)}, ).rowcount voucher_deleted = conn.execute( text("DELETE FROM wehago_voucher_rows WHERE fiscal_year IN :years").bindparams(bindparam("years", expanding=True)), {"years": sorted(voucher_years)}, ).rowcount unused_source_ids = { int(row[0]) for row in conn.execute( text( """ SELECT sf.id FROM wehago_source_files sf LEFT JOIN wehago_ledger_rows l ON l.source_file_id = sf.id LEFT JOIN wehago_voucher_rows v ON v.source_file_id = sf.id GROUP BY sf.id HAVING COUNT(l.id) = 0 AND COUNT(v.id) = 0 """ ) ) } if unused_source_ids: conn.execute( text("DELETE FROM wehago_source_files WHERE id IN :ids").bindparams(bindparam("ids", expanding=True)), {"ids": sorted(unused_source_ids)}, ) conn.execute( text("DELETE FROM wehago_comparison_results WHERE fiscal_year IN :years").bindparams(bindparam("years", expanding=True)), {"years": sorted(ledger_years | voucher_years)}, ) conn.execute(text("DELETE FROM wehago_metric_count_cache")) conn.execute(text("DELETE FROM wehago_result_row_cache")) conn.execute(text("DELETE FROM wehago_pair_recommend_cache")) conn.execute(text("DELETE FROM wehago_background_jobs WHERE job_type = 'pair_recommend_precompute'")) return {"ledger_deleted": int(ledger_deleted or 0), "voucher_deleted": int(voucher_deleted or 0), "source_deleted": len(unused_source_ids)} def _import_one( conn, path: Path, expected_kind: str, expected_year: int, *, allowed_years: set[int] | None = None, year_hint_override: int | None | object = _DEFAULT_YEAR_HINT, ) -> dict[str, object]: file_kind, header, sheet_name = detect_file_kind(path) if file_kind != expected_kind: raise ValueError(f"{path} 형식이 {expected_kind}가 아닙니다: {file_kind}") workbook, sheet = _load_sheet(path) try: sample_rows = list(sheet.iter_rows(min_row=2, max_row=51, values_only=True)) if year_hint_override is _DEFAULT_YEAR_HINT: year_hint = infer_year_hint(path, file_kind, sample_rows) or expected_year else: year_hint = year_hint_override source_id, _changed = upsert_source_file(conn, path, file_kind, year_hint, sheet_name, header) if file_kind == "ledger": conn.execute(text("DELETE FROM wehago_ledger_rows WHERE source_file_id = :source_id"), {"source_id": source_id}) inserted = import_ledger_rows(conn, source_id, sheet_name, sheet.iter_rows(min_row=2, values_only=True), year_hint) else: conn.execute(text("DELETE FROM wehago_voucher_rows WHERE source_file_id = :source_id"), {"source_id": source_id}) inserted = import_voucher_rows(conn, source_id, sheet_name, sheet.iter_rows(min_row=2, values_only=True), year_hint) allowed = sorted(allowed_years or {expected_year}) if allowed: conn.execute( text( """ DELETE FROM wehago_ledger_rows WHERE source_file_id = :source_id AND fiscal_year NOT IN :allowed_years """ ).bindparams(bindparam("allowed_years", expanding=True)), {"source_id": source_id, "allowed_years": allowed}, ) conn.execute( text( """ DELETE FROM wehago_voucher_rows WHERE source_file_id = :source_id AND fiscal_year NOT IN :allowed_years """ ).bindparams(bindparam("allowed_years", expanding=True)), {"source_id": source_id, "allowed_years": allowed}, ) inserted = int( conn.execute( text(f"SELECT COUNT(*) FROM wehago_{file_kind}_rows WHERE source_file_id = :source_id"), {"source_id": source_id}, ).scalar_one() or 0 ) conn.execute( text("UPDATE wehago_source_files SET row_count = :row_count, imported_at = CURRENT_TIMESTAMP WHERE id = :source_id"), {"row_count": inserted, "source_id": source_id}, ) return { "path": str(path), "kind": file_kind, "year_hint": year_hint, "allowed_years": allowed, "source_id": source_id, "inserted": inserted, "file_hash": compute_file_hash(path), } finally: workbook.close() def _fetch_counts(conn) -> dict[str, object]: counts: dict[str, object] = {} for table in ("wehago_ledger_rows", "wehago_voucher_rows", "wehago_comparison_results"): rows = conn.execute( text(f"SELECT fiscal_year, COUNT(*) AS row_count FROM {table} GROUP BY fiscal_year ORDER BY fiscal_year") ).mappings() counts[table] = {str(row["fiscal_year"]): int(row["row_count"]) for row in rows} return counts def _fetch_quality(conn) -> dict[str, object]: total_rows = conn.execute( text( """ SELECT COUNT(*) FROM wehago_ledger_rows WHERE REPLACE(COALESCE(description, ''), ' ', '') IN ('[월계]', '[누계]', '월계', '누계') OR REPLACE(COALESCE(ledger_date, ''), ' ', '') IN ('[월계]', '[누계]', '월계', '누계') """ ) ).scalar_one() blank_account_rows = conn.execute( text( """ SELECT COUNT(*) FROM wehago_ledger_rows WHERE fiscal_year IN (2022, 2023, 2024) AND (COALESCE(account_code, '') = '' OR COALESCE(account_name, '') = '') """ ) ).scalar_one() account_names = conn.execute( text( """ SELECT fiscal_year, account_code, COUNT(DISTINCT account_name) AS name_count, GROUP_CONCAT(DISTINCT account_name) AS names FROM wehago_ledger_rows WHERE fiscal_year IN (2022, 2023, 2024) AND COALESCE(account_code, '') <> '' GROUP BY fiscal_year, account_code HAVING COUNT(DISTINCT account_name) > 1 ORDER BY fiscal_year, account_code LIMIT 20 """ ) ).mappings() voucher_counts_by_source = conn.execute( text( """ SELECT sf.file_name, v.fiscal_year, COUNT(*) AS row_count FROM wehago_voucher_rows v JOIN wehago_source_files sf ON sf.id = v.source_file_id WHERE v.fiscal_year IN (2022, 2023, 2024, 2025) GROUP BY sf.file_name, v.fiscal_year ORDER BY v.fiscal_year, sf.file_name """ ) ).mappings() return { "monthly_cumulative_rows": int(total_rows or 0), "blank_account_rows_2022_2024": int(blank_account_rows or 0), "multi_name_accounts": [dict(row) for row in account_names], "voucher_counts_by_source": [dict(row) for row in voucher_counts_by_source], } def main() -> None: voucher_paths = [item["path"] for item in VOUCHER_FILES] missing = [path for path in [*LEDGER_FILES.values(), *voucher_paths] if not path.exists()] if missing: raise FileNotFoundError("\\n".join(str(path) for path in missing)) init_wehago_compare_db(ENGINE) summary: dict[str, object] = {"deleted": {}, "imported": []} with ENGINE.begin() as conn: summary["deleted"] = _delete_year_data(conn, ledger_years={2022, 2023, 2024, 2025}, voucher_years={2022, 2023, 2024, 2025}) for year, path in LEDGER_FILES.items(): summary["imported"].append(_import_one(conn, path, "ledger", year, allowed_years={year})) for item in VOUCHER_FILES: summary["imported"].append( _import_one( conn, item["path"], "voucher", int(item["expected_year"]), allowed_years=set(item["allowed_years"]), year_hint_override=item["year_hint"], ) ) rebuild_comparison_results(conn) summary["dashboard_counts"] = { str(year): _fallback_metric_counts_from_db(conn, year, year) for year in range(2022, 2026) } summary["counts"] = _fetch_counts(conn) summary["quality"] = _fetch_quality(conn) print(json.dumps(summary, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()