from __future__ import annotations import argparse import json import sqlite3 import subprocess import sys from pathlib import Path from typing import Any sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from runtime_config import DB_PATH DEFAULT_SAMPLES = ( "2025-02-25-00029", "2025-01-10-00006", "2025-01-15-50027", "2025-01-15-50028", "2025-01-21-50197", "2025-01-21-50198", ) def run_command(command: list[str]) -> str: proc = subprocess.run(command, cwd=Path(__file__).resolve().parents[1], text=True, capture_output=True) if proc.returncode != 0: payload = { "command": command, "returncode": proc.returncode, "stdout": proc.stdout, "stderr": proc.stderr, } raise SystemExit(json.dumps(payload, ensure_ascii=False, indent=2)) return proc.stdout def parse_sample(value: str, default_year: int) -> tuple[int, str, str]: text = value.strip() parts = text.replace("/", "-").split("-") if len(parts) == 4: year, month, day, voucher = parts elif len(parts) == 3: year = str(default_year) month, day, voucher = parts else: raise ValueError(f"Invalid sample format: {value}") return int(year), f"{int(month):02d}-{int(day):02d}", f"{int(voucher):05d}" if voucher.isdigit() else voucher def active_signature(conn: sqlite3.Connection, year: int) -> str: row = conn.execute( """ SELECT setting_json FROM wehago_compare_settings WHERE setting_key = ? LIMIT 1 """, (f"wehago_active_query_projection:{year}:{year}",), ).fetchone() if not row: return "" try: payload = json.loads(row[0] or "{}") except Exception: return "" return str(payload.get("signature") or "") if isinstance(payload, dict) else "" def validate_projection(conn: sqlite3.Connection, year: int, signature: str) -> dict[str, Any]: counts = { row[0]: int(row[1] or 0) for row in conn.execute( """ SELECT status_key, COUNT(*) FROM wehago_compare_query_groups WHERE start_year = ? AND end_year = ? AND signature = ? AND status_key IN ('voucher_matched', 'voucher_unmatched', 'voucher_recheck', 'voucher_excepted') GROUP BY status_key """, (year, year, signature), ).fetchall() } final_counts = { row[0]: int(row[1] or 0) for row in conn.execute( """ SELECT final_status, COUNT(*) FROM wehago_compare_final_status_projection WHERE start_year = ? AND end_year = ? AND signature = ? GROUP BY final_status """, (year, year, signature), ).fetchall() } raw_total = int( conn.execute( """ SELECT COUNT(DISTINCT compare_voucher_no) FROM wehago_ledger_rows WHERE fiscal_year = ? AND COALESCE(compare_voucher_no, '') <> '' """, (year,), ).fetchone()[0] or 0 ) recheck_without_erp = int( conn.execute( """ SELECT COUNT(*) FROM wehago_compare_query_groups g WHERE g.start_year = ? AND g.end_year = ? AND g.signature = ? AND g.status_key = 'voucher_recheck' AND NOT EXISTS ( SELECT 1 FROM wehago_compare_query_rows r WHERE r.start_year = g.start_year AND r.end_year = g.end_year AND r.signature = g.signature AND r.status_key = g.status_key AND r.group_index = g.group_index AND COALESCE(r.voucher_account_name, '') <> '' AND ( ABS(COALESCE(r.voucher_debit, 0)) > 0.0001 OR ABS(COALESCE(r.voucher_credit, 0)) > 0.0001 ) ) """, (year, year, signature), ).fetchone()[0] or 0 ) unmatched_with_erp = int( conn.execute( """ SELECT COUNT(*) FROM wehago_compare_query_groups g WHERE g.start_year = ? AND g.end_year = ? AND g.signature = ? AND g.status_key = 'voucher_unmatched' AND EXISTS ( SELECT 1 FROM wehago_compare_query_rows r WHERE r.start_year = g.start_year AND r.end_year = g.end_year AND r.signature = g.signature AND r.status_key = g.status_key AND r.group_index = g.group_index AND COALESCE(r.voucher_account_name, '') <> '' AND ( ABS(COALESCE(r.voucher_debit, 0)) > 0.0001 OR ABS(COALESCE(r.voucher_credit, 0)) > 0.0001 ) ) """, (year, year, signature), ).fetchone()[0] or 0 ) return { "raw_total": raw_total, "counts": counts, "final_counts": final_counts, "classified_total": sum(counts.values()), "difference": raw_total - sum(counts.values()), "recheck_without_erp": recheck_without_erp, "unmatched_with_erp": unmatched_with_erp, } def sample_statuses(conn: sqlite3.Connection, year: int, signature: str, samples: list[str]) -> list[dict[str, Any]]: result: list[dict[str, Any]] = [] for sample in samples: sample_year, ledger_date, voucher_no = parse_sample(sample, year) rows = [ { "status_key": row[0], "ledger_date": row[1], "voucher_no": row[2], "draft_no": row[3], "review_reason": row[4], } for row in conn.execute( """ SELECT status_key, ledger_date, voucher_no, draft_no, review_reason FROM wehago_compare_query_groups WHERE start_year = ? AND end_year = ? AND signature = ? AND fiscal_year = ? AND ledger_date = ? AND voucher_no = ? ORDER BY status_key, group_index """, (year, year, signature, sample_year, ledger_date, voucher_no), ).fetchall() ] result.append({"sample": sample, "rows": rows}) return result def main() -> None: parser = argparse.ArgumentParser(description="Run a fast WEHAGO logic iteration: compile, reconcile, validate, sample-check, optionally prune.") parser.add_argument("--year", type=int, default=2025) parser.add_argument("--sample", action="append", default=[]) parser.add_argument("--skip-compile", action="store_true") parser.add_argument("--skip-reconcile", action="store_true") parser.add_argument("--prune", action="store_true") args = parser.parse_args() repo_root = Path(__file__).resolve().parents[1] python = str(repo_root / ".venv" / "bin" / "python") if not args.skip_compile: run_command( [ python, "-m", "py_compile", "scripts/reconcile_wehago_projection_to_db.py", "scripts/prune_wehago_projection_history.py", "wehago_compare.py", "main.py", ] ) reconcile_output = "" if not args.skip_reconcile: reconcile_output = run_command([python, "scripts/reconcile_wehago_projection_to_db.py", "--year", str(args.year)]) prune_output = "" if args.prune: prune_output = run_command( [ python, "scripts/prune_wehago_projection_history.py", "--start-year", str(args.year), "--end-year", str(args.year), "--execute", ] ) conn = sqlite3.connect(DB_PATH) try: signature = active_signature(conn, args.year) samples = args.sample or list(DEFAULT_SAMPLES) payload = { "year": args.year, "active_signature": signature, "validation": validate_projection(conn, args.year, signature) if signature else {}, "samples": sample_statuses(conn, args.year, signature, samples) if signature else [], "reconcile_output_tail": reconcile_output.strip().splitlines()[-8:], "prune_output": json.loads(prune_output) if prune_output.strip().startswith("{") else prune_output, } finally: conn.close() print(json.dumps(payload, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()