#!/usr/bin/env python3 """Ignore reconciliation items outside the intended working window. Dry-run by default. Use this after an external sync staged too much history. Examples: PYTHONPATH=. python scripts/cleanup_stale_reconciliation_items.py --days 3 PYTHONPATH=. python scripts/cleanup_stale_reconciliation_items.py --days 3 --apply PYTHONPATH=. python scripts/cleanup_stale_reconciliation_items.py --source odoo --days 3 --apply """ from __future__ import annotations import argparse from sqlalchemy import text from app.db import engine from app.reconciliation_service import ( cleanup_reconciliation_outside_window, ensure_reconciliation_schema, ) def _preview(*, days: int, source_system: str | None, limit: int) -> dict: ensure_reconciliation_schema() from datetime import datetime, timedelta, timezone days = max(int(days or 3), 1) cutoff = (datetime.now(timezone.utc).date() - timedelta(days=days)).isoformat() params = {"cutoff": cutoff, "limit": int(limit)} source_sql = "" if source_system: source_sql = "AND source_system = :source_system" params["source_system"] = source_system with engine.begin() as conn: rows = conn.execute(text(f""" SELECT id::text, source_system, external_type, document_number, customer_name, document_date, title, status FROM reconciliation_items WHERE status IN ('open', 'needs_review') {source_sql} AND document_date IS NOT NULL AND document_date < CAST(:cutoff AS DATE) ORDER BY document_date ASC, updated_at DESC LIMIT :limit """), params).mappings().all() return {"days": days, "cutoff": cutoff, "source_system": source_system or "all", "matched": len(rows), "items": [dict(r) for r in rows]} def main() -> None: parser = argparse.ArgumentParser(description="Ignore stale reconciliation candidates outside a recent working window.") parser.add_argument("--jasmin", action="store_true", help="same as --source jasmin") parser.add_argument("--source", choices=["jasmin", "odoo", "packlink", "manual"], help="only clean one source system") parser.add_argument("--days", type=int, default=3, help="keep open items from the last N days") parser.add_argument("--limit", type=int, default=1000, help="maximum rows to inspect/update") parser.add_argument("--apply", action="store_true", help="apply changes; otherwise dry-run") args = parser.parse_args() source = "jasmin" if args.jasmin else args.source if args.apply: result = cleanup_reconciliation_outside_window(days=args.days, source_system=source, limit=args.limit, actor="cleanup_stale_reconciliation_items") else: result = _preview(days=args.days, source_system=source, limit=args.limit) print(f"Janela operacional: manter itens >= {result['cutoff']} ({result['days']} dias)") print(f"Fonte: {result['source_system']}") print(f"Encontrados para ignorar: {result['matched']}") for row in result.get("items", [])[:50]: print(f"- {row.get('document_date')} · {row.get('source_system')} · {row.get('external_type')} · {row.get('document_number')} · {row.get('customer_name') or ''}") if args.apply: print(f"Aplicado: {result['ignored']} itens marcados como ignored") else: print("Dry-run. Para aplicar, repetir com --apply") if __name__ == "__main__": main()