Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
85
scripts/repair_stale_followups.py
Executable file
85
scripts/repair_stale_followups.py
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit/repair pending follow-ups that contradict the current opportunity stage.
|
||||
|
||||
Dry-run by default. Use --apply to update rows. The repair is idempotent and
|
||||
never sends email or Chatwoot messages.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--apply", action="store_true")
|
||||
args = parser.parse_args()
|
||||
load_dotenv(dotenv_path=Path(".env"), override=False)
|
||||
db_url = os.environ.get("DATABASE_URL")
|
||||
if not db_url:
|
||||
raise SystemExit("DATABASE_URL em falta")
|
||||
engine = create_engine(db_url)
|
||||
|
||||
select_sql = text("""
|
||||
SELECT t.id::text AS task_id,
|
||||
t.opportunity_id::text,
|
||||
o.customer_name,
|
||||
o.stage,
|
||||
t.action_code,
|
||||
t.action,
|
||||
t.route,
|
||||
t.status,
|
||||
EXISTS (
|
||||
SELECT 1 FROM tasks tx
|
||||
WHERE tx.opportunity_id = o.id
|
||||
AND tx.status = 'pending'
|
||||
AND tx.action_code IN ('SEND_INVOICE','CONFIRM_PAYMENT','PREPARE_ORDER','CREATE_SHIPMENT')
|
||||
) AS has_superseding_task
|
||||
FROM tasks t
|
||||
JOIN opportunities o ON o.id = t.opportunity_id
|
||||
WHERE o.status = 'open'
|
||||
AND t.status = 'pending'
|
||||
AND t.action_code IN ('FOLLOW_UP_QUOTE','FOLLOW_UP_PROFORMA')
|
||||
AND (
|
||||
upper(o.stage) IN (
|
||||
'INVOICE_REQUESTED','INVOICE_SENT','WAITING_PAYMENT','PAYMENT_CONFIRMED',
|
||||
'ODOO_ORDER_CREATED','IN_PRODUCTION','ORDER_PREPARATION','READY_TO_SHIP',
|
||||
'INVOICED','SHIPMENT_CREATED','SHIPPED','TRACKING_SENT','DELIVERED','WON'
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM tasks tx
|
||||
WHERE tx.opportunity_id = o.id
|
||||
AND tx.status = 'pending'
|
||||
AND tx.action_code IN ('SEND_INVOICE','CONFIRM_PAYMENT','PREPARE_ORDER','CREATE_SHIPMENT')
|
||||
)
|
||||
)
|
||||
ORDER BY o.updated_at DESC
|
||||
""")
|
||||
with engine.begin() as conn:
|
||||
rows = [dict(r) for r in conn.execute(select_sql).mappings().all()]
|
||||
print(f"Candidatos: {len(rows)}")
|
||||
for row in rows:
|
||||
decision = "close" if row["has_superseding_task"] or row["stage"] != "WAITING_PAYMENT" else "convert_to_payment"
|
||||
print(f"{decision:20} {row['task_id']} | {row['customer_name']} | {row['stage']} | {row['action_code']}")
|
||||
if not args.apply:
|
||||
print("Dry-run: use --apply para corrigir.")
|
||||
return 0
|
||||
|
||||
from app.followup_service import align_pending_followups_for_opportunity
|
||||
opportunity_ids = sorted({r["opportunity_id"] for r in rows})
|
||||
updated = closed = 0
|
||||
for opportunity_id in opportunity_ids:
|
||||
result = align_pending_followups_for_opportunity(opportunity_id=opportunity_id, created_by="repair_stale_followups")
|
||||
updated += int(result.get("updated") or 0)
|
||||
closed += int(result.get("closed") or 0)
|
||||
print(opportunity_id, result)
|
||||
print(f"Aplicado: convertidas={updated}; encerradas={closed}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user