from __future__ import annotations import argparse import sqlite3 import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from runtime_config import DB_PATH DEFAULT_STATUSES = ( "voucher_matched", "voucher_unmatched", "voucher_recheck", "voucher_excepted", ) def latest_signature(conn: sqlite3.Connection, start_year: int, end_year: int) -> str: row = conn.execute( """ SELECT signature FROM wehago_compare_query_groups WHERE start_year = ? AND end_year = ? GROUP BY signature ORDER BY MAX(updated_at) DESC LIMIT 1 """, (start_year, end_year), ).fetchone() return str(row[0] or "") if row else "" def status_summary( conn: sqlite3.Connection, start_year: int, end_year: int, signature: str, status_key: str, ) -> dict[str, int | float]: row = conn.execute( """ WITH per_group AS ( SELECT g.group_index, COUNT(DISTINCT CASE WHEN COALESCE(r.ledger_account_name, '') <> '' OR COALESCE(r.ledger_desc, '') <> '' OR ABS(COALESCE(r.ledger_debit, 0)) >= 0.5 OR ABS(COALESCE(r.ledger_credit, 0)) >= 0.5 THEN COALESCE(r.ledger_date, '') || '|' || COALESCE(r.voucher_no, '') END) AS wehago_keys, COUNT(DISTINCT CASE WHEN COALESCE(r.voucher_account_name, '') <> '' OR COALESCE(r.voucher_desc, '') <> '' OR ABS(COALESCE(r.voucher_debit, 0)) >= 0.5 OR ABS(COALESCE(r.voucher_credit, 0)) >= 0.5 THEN COALESCE(r.draft_no, '') END) AS erp_drafts, COUNT(*) AS row_count FROM wehago_compare_query_groups g LEFT JOIN wehago_compare_query_rows r ON 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 WHERE g.start_year = ? AND g.end_year = ? AND g.signature = ? AND g.status_key = ? GROUP BY g.group_index ) SELECT COUNT(*) AS groups, SUM(CASE WHEN wehago_keys > 1 THEN 1 ELSE 0 END) AS mixed_wehago_groups, SUM(CASE WHEN erp_drafts > 1 THEN 1 ELSE 0 END) AS multi_erp_groups, COALESCE(MAX(wehago_keys), 0) AS max_wehago_keys, COALESCE(MAX(erp_drafts), 0) AS max_erp_drafts, COALESCE(MAX(row_count), 0) AS max_rows, COALESCE(AVG(row_count), 0) AS avg_rows FROM per_group """, (start_year, end_year, signature, status_key), ).fetchone() return { "groups": int(row["groups"] or 0), "mixed_wehago_groups": int(row["mixed_wehago_groups"] or 0), "multi_erp_groups": int(row["multi_erp_groups"] or 0), "max_wehago_keys": int(row["max_wehago_keys"] or 0), "max_erp_drafts": int(row["max_erp_drafts"] or 0), "max_rows": int(row["max_rows"] or 0), "avg_rows": round(float(row["avg_rows"] or 0), 2), } def sample_mixed_groups( conn: sqlite3.Connection, start_year: int, end_year: int, signature: str, status_key: str, limit: int, ) -> list[dict[str, object]]: rows = conn.execute( """ SELECT g.group_index, g.ledger_date, g.voucher_no, COUNT(DISTINCT CASE WHEN COALESCE(r.ledger_account_name, '') <> '' OR COALESCE(r.ledger_desc, '') <> '' OR ABS(COALESCE(r.ledger_debit, 0)) >= 0.5 OR ABS(COALESCE(r.ledger_credit, 0)) >= 0.5 THEN COALESCE(r.ledger_date, '') || '|' || COALESCE(r.voucher_no, '') END) AS wehago_keys, COUNT(*) AS row_count FROM wehago_compare_query_groups g JOIN wehago_compare_query_rows r ON 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 WHERE g.start_year = ? AND g.end_year = ? AND g.signature = ? AND g.status_key = ? GROUP BY g.group_index HAVING wehago_keys > 1 ORDER BY wehago_keys DESC, row_count DESC, g.group_index ASC LIMIT ? """, (start_year, end_year, signature, status_key, limit), ).fetchall() return [dict(row) for row in rows] def main() -> None: parser = argparse.ArgumentParser(description="Validate WEHAGO voucher projection anchoring.") parser.add_argument("--start-year", type=int, default=2025) parser.add_argument("--end-year", type=int, default=2025) parser.add_argument("--signature", default="") parser.add_argument("--sample", type=int, default=5) parser.add_argument("--statuses", nargs="*", default=list(DEFAULT_STATUSES)) args = parser.parse_args() conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row signature = args.signature or latest_signature(conn, args.start_year, args.end_year) if not signature: raise SystemExit("No query projection signature found.") print({"db": str(DB_PATH), "signature": signature}) for status_key in args.statuses: summary = status_summary(conn, args.start_year, args.end_year, signature, status_key) print({status_key: summary}) samples = sample_mixed_groups( conn, args.start_year, args.end_year, signature, status_key, args.sample, ) if samples: print({"mixed_samples": status_key, "rows": samples}) if __name__ == "__main__": main()