75 lines
3.0 KiB
Python
75 lines
3.0 KiB
Python
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 MATERIALIZED_ACTIONS, 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, o.updated_at
|
|
FROM opportunities o
|
|
WHERE COALESCE(o.status, 'open') = 'open'
|
|
AND COALESCE(o.stage, '') NOT IN ('WON','LOST','NO_INTEREST')
|
|
ORDER BY o.updated_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 _next_action_code(decision: dict[str, Any]) -> str:
|
|
return str(decision.get("action_code") or (decision.get("next_action") or {}).get("code") or "").upper()
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Cria tasks pendentes para próximas ações humanas materializáveis.")
|
|
parser.add_argument("--apply", action="store_true", help="grava alterações; sem isto apenas mostra dry-run")
|
|
parser.add_argument("--limit", type=int, default=300)
|
|
parser.add_argument("--action", choices=sorted(MATERIALIZED_ACTIONS), help="filtrar por action_code")
|
|
args = parser.parse_args()
|
|
|
|
candidates = _candidate_opportunities(args.limit)
|
|
print(f"apply={args.apply} candidates={len(candidates)} materialized_actions={sorted(MATERIALIZED_ACTIONS)}")
|
|
|
|
to_create: list[tuple[dict[str, Any], dict[str, Any], str]] = []
|
|
skipped = 0
|
|
errors = 0
|
|
for opp in candidates:
|
|
try:
|
|
decision = get_opportunity_next_action(opp["id"])
|
|
code = _next_action_code(decision)
|
|
if code in MATERIALIZED_ACTIONS and (not args.action or code == args.action):
|
|
to_create.append((opp, decision, code))
|
|
else:
|
|
skipped += 1
|
|
except Exception as exc:
|
|
errors += 1
|
|
print(f"ERROR opp={opp.get('id')} title={opp.get('title')} error={exc}")
|
|
|
|
print(f"TO_CREATE_OR_EXISTING={len(to_create)} skipped={skipped} errors={errors}")
|
|
for opp, decision, code in to_create:
|
|
doc = decision.get("document_number") or (decision.get("next_action") or {}).get("document_number") or ""
|
|
print(f"- action={code} 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_materialized_next_action_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()
|