Files
clientflow_backend/scripts/cleanup_stale_reconciliation_items.py
2026-06-09 22:55:58 +01:00

80 lines
3.4 KiB
Python
Executable File

#!/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()