Release v4928.1.4.2 stable
This commit is contained in:
133
scripts/reset_reconciliation_generated.py
Executable file
133
scripts/reset_reconciliation_generated.py
Executable file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reset generated reconciliation staging items so they can be rebuilt.
|
||||
|
||||
Safe by default: dry-run only and only affects generated external candidates
|
||||
from Jasmin/Odoo/Packlink in open/needs_review/ignored states. It does not
|
||||
remove opportunities, commercial documents, payment proofs, operation links or
|
||||
external system records.
|
||||
|
||||
Examples:
|
||||
PYTHONPATH=. python scripts/reset_reconciliation_generated.py
|
||||
PYTHONPATH=. python scripts/reset_reconciliation_generated.py --apply
|
||||
PYTHONPATH=. python scripts/reset_reconciliation_generated.py --source jasmin --source odoo --apply
|
||||
PYTHONPATH=. python scripts/reset_reconciliation_generated.py --days 30 --apply
|
||||
PYTHONPATH=. python scripts/reset_reconciliation_generated.py --include-manual --apply
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db import engine, init_db
|
||||
from app.reconciliation_service import ensure_reconciliation_schema
|
||||
|
||||
DEFAULT_SOURCES = ["jasmin", "odoo", "packlink"]
|
||||
DEFAULT_STATUSES = ["open", "needs_review", "ignored"]
|
||||
|
||||
|
||||
def _backup_table_name() -> str:
|
||||
return "reconciliation_items_reset_backup_" + datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
|
||||
def _build_where(args: argparse.Namespace) -> tuple[str, Dict[str, Any]]:
|
||||
sources = list(args.source or DEFAULT_SOURCES)
|
||||
if args.include_manual and "manual" not in sources:
|
||||
sources.append("manual")
|
||||
statuses = list(args.status or DEFAULT_STATUSES)
|
||||
params: Dict[str, Any] = {"sources": sources, "statuses": statuses}
|
||||
clauses = [
|
||||
"source_system = ANY(CAST(:sources AS TEXT[]))",
|
||||
"status = ANY(CAST(:statuses AS TEXT[]))",
|
||||
]
|
||||
# Extra guard: never touch items already linked to an opportunity unless
|
||||
# the operator explicitly changes the status list and unlinks manually.
|
||||
clauses.append("opportunity_id IS NULL")
|
||||
|
||||
if args.days is not None:
|
||||
days = max(int(args.days), 1)
|
||||
cutoff = (datetime.now(timezone.utc).date() - timedelta(days=days - 1)).isoformat()
|
||||
params["cutoff"] = cutoff
|
||||
clauses.append("(document_date IS NULL OR document_date >= CAST(:cutoff AS DATE))")
|
||||
return " AND ".join(clauses), params
|
||||
|
||||
|
||||
def _summarize(where_sql: str, params: Dict[str, Any], limit: int) -> Dict[str, Any]:
|
||||
with engine.begin() as conn:
|
||||
counts = conn.execute(text(f"""
|
||||
SELECT source_system, external_type, status, COUNT(*) AS total
|
||||
FROM reconciliation_items
|
||||
WHERE {where_sql}
|
||||
GROUP BY source_system, external_type, status
|
||||
ORDER BY source_system, external_type, status
|
||||
"""), params).mappings().all()
|
||||
rows = conn.execute(text(f"""
|
||||
SELECT id::text, source_system, external_type, status, document_number,
|
||||
customer_name, customer_tax_id, document_date, amount, title
|
||||
FROM reconciliation_items
|
||||
WHERE {where_sql}
|
||||
ORDER BY updated_at DESC, created_at DESC
|
||||
LIMIT :limit
|
||||
"""), {**params, "limit": int(limit)}).mappings().all()
|
||||
return {"counts": [dict(r) for r in counts], "items": [dict(r) for r in rows]}
|
||||
|
||||
|
||||
def _apply_reset(where_sql: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
backup_table = _backup_table_name()
|
||||
# backup_table is generated internally from digits/underscore only.
|
||||
with engine.begin() as conn:
|
||||
total = conn.execute(text(f"SELECT COUNT(*) FROM reconciliation_items WHERE {where_sql}"), params).scalar() or 0
|
||||
conn.execute(text(f"CREATE TABLE {backup_table} AS SELECT * FROM reconciliation_items WHERE {where_sql}"), params)
|
||||
deleted = conn.execute(text(f"DELETE FROM reconciliation_items WHERE {where_sql}"), params).rowcount or 0
|
||||
return {"matched": int(total), "deleted": int(deleted), "backup_table": backup_table}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Reset generated reconciliation candidates and keep a DB backup table.")
|
||||
parser.add_argument("--source", action="append", choices=["jasmin", "odoo", "packlink", "manual"], help="source_system to reset; repeatable. Default: jasmin, odoo, packlink")
|
||||
parser.add_argument("--status", action="append", choices=["open", "needs_review", "ignored"], help="status to reset; repeatable. Default: open, needs_review, ignored")
|
||||
parser.add_argument("--days", type=int, help="only reset candidates inside the last N days; default is all dates")
|
||||
parser.add_argument("--include-manual", action="store_true", help="also include source_system=manual; use with care")
|
||||
parser.add_argument("--limit", type=int, default=50, help="preview sample size")
|
||||
parser.add_argument("--apply", action="store_true", help="delete matched staging rows after creating a backup table")
|
||||
args = parser.parse_args()
|
||||
|
||||
init_db()
|
||||
ensure_reconciliation_schema()
|
||||
where_sql, params = _build_where(args)
|
||||
summary = _summarize(where_sql, params, args.limit)
|
||||
|
||||
print("Alvo do reset:")
|
||||
print(f" fontes: {', '.join(params['sources'])}")
|
||||
print(f" estados: {', '.join(params['statuses'])}")
|
||||
print(" proteção: opportunity_id IS NULL")
|
||||
if args.days is not None:
|
||||
print(f" janela: >= {params['cutoff']} ({args.days} dias)")
|
||||
print("\nContagens:")
|
||||
if not summary["counts"]:
|
||||
print(" 0 itens encontrados")
|
||||
for row in summary["counts"]:
|
||||
print(f" {row['source_system']} · {row['external_type']} · {row['status']}: {row['total']}")
|
||||
|
||||
print("\nAmostra:")
|
||||
for row in summary["items"]:
|
||||
print(
|
||||
f" {row.get('document_date') or '-'} · {row.get('source_system')} · {row.get('external_type')} · "
|
||||
f"{row.get('status')} · {row.get('document_number') or '-'} · {row.get('customer_name') or '-'}"
|
||||
)
|
||||
|
||||
if not args.apply:
|
||||
print("\nDry-run. Para aplicar: repetir com --apply")
|
||||
return
|
||||
|
||||
result = _apply_reset(where_sql, params)
|
||||
print("\nReset aplicado:")
|
||||
print(f" encontrados: {result['matched']}")
|
||||
print(f" apagados: {result['deleted']}")
|
||||
print(f" backup: {result['backup_table']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user