86 lines
3.3 KiB
Python
Executable File
86 lines
3.3 KiB
Python
Executable File
#!/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())
|