Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
81
scripts/repair_missing_send_invoice_tasks.py
Normal file
81
scripts/repair_missing_send_invoice_tasks.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db import engine
|
||||
from app.opportunity_action_task_materializer import ensure_pending_task_for_next_action
|
||||
from app.opportunity_next_action_service import get_opportunity_next_action
|
||||
|
||||
|
||||
def _candidate_opportunities(limit: int) -> list[dict[str, Any]]:
|
||||
sql = text("""
|
||||
SELECT DISTINCT o.id::text, o.title, o.customer_name, max(d.created_at) AS last_doc_at
|
||||
FROM opportunities o
|
||||
JOIN commercial_documents d ON d.opportunity_id = o.id
|
||||
WHERE COALESCE(o.status, 'open') = 'open'
|
||||
AND d.system = 'jasmin'
|
||||
AND d.document_kind = 'invoice'
|
||||
AND COALESCE(d.is_active, TRUE) = TRUE
|
||||
AND COALESCE(d.role, 'current') IN ('current', 'accepted')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM tasks t
|
||||
WHERE t.opportunity_id = o.id
|
||||
AND t.status = 'pending'
|
||||
AND t.action_code = 'SEND_INVOICE'
|
||||
)
|
||||
GROUP BY o.id, o.title, o.customer_name
|
||||
ORDER BY max(d.created_at) DESC NULLS LAST
|
||||
LIMIT :limit
|
||||
""")
|
||||
with engine.begin() as conn:
|
||||
return [dict(r) for r in conn.execute(sql, {"limit": limit}).mappings().all()]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Cria tasks SEND_INVOICE em oportunidades cuja próxima ação central é enviar fatura.")
|
||||
parser.add_argument("--apply", action="store_true", help="grava alterações; sem isto apenas mostra dry-run")
|
||||
parser.add_argument("--limit", type=int, default=200)
|
||||
args = parser.parse_args()
|
||||
|
||||
candidates = _candidate_opportunities(args.limit)
|
||||
print(f"apply={args.apply} candidates={len(candidates)}")
|
||||
|
||||
to_create: list[tuple[dict[str, Any], dict[str, Any]]] = []
|
||||
skipped = 0
|
||||
errors = 0
|
||||
for opp in candidates:
|
||||
try:
|
||||
decision = get_opportunity_next_action(opp["id"])
|
||||
code = str(decision.get("action_code") or decision.get("next_action", {}).get("code") or "").upper()
|
||||
if code == "SEND_INVOICE":
|
||||
to_create.append((opp, decision))
|
||||
else:
|
||||
skipped += 1
|
||||
except Exception as exc:
|
||||
errors += 1
|
||||
print(f"ERROR opp={opp.get('id')} title={opp.get('title')} error={exc}")
|
||||
|
||||
print(f"SEND_INVOICE_TO_CREATE={len(to_create)} skipped={skipped} errors={errors}")
|
||||
for opp, decision in to_create:
|
||||
doc = decision.get("document_number") or decision.get("next_action", {}).get("document_number") or ""
|
||||
print(f"- opp={opp['id']} doc={doc} title={opp.get('title')} customer={opp.get('customer_name')}")
|
||||
if args.apply:
|
||||
result = ensure_pending_task_for_next_action(
|
||||
opp["id"],
|
||||
decision,
|
||||
source="repair_missing_send_invoice_tasks",
|
||||
actor="repair-script",
|
||||
)
|
||||
print(f" result={result}")
|
||||
|
||||
print("SUMMARY")
|
||||
print(f"created_or_existing={len(to_create) if args.apply else 0}")
|
||||
print(f"dry_run={not args.apply}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user