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()