Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
142
scripts/sync_invoice_delivery_context.py
Executable file
142
scripts/sync_invoice_delivery_context.py
Executable file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only/apply repair for SEND_INVOICE tasks whose invoice already exists.
|
||||
|
||||
The script keeps legacy reconciliation tasks aligned with the current document
|
||||
state. It does not send emails. With --apply it updates pending SEND_INVOICE
|
||||
notes/actions when a current Jasmin invoice is linked to the opportunity.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db import engine
|
||||
|
||||
|
||||
def _json(value: Any) -> str:
|
||||
return json.dumps(value or {}, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
def _money(value: Any, currency: str = "EUR") -> str:
|
||||
if value is None or value == "":
|
||||
return ""
|
||||
try:
|
||||
return f"{float(value):,.2f} {currency or 'EUR'}".replace(",", "X").replace(".", ",").replace("X", ".")
|
||||
except Exception:
|
||||
return str(value)
|
||||
|
||||
|
||||
def find_candidates(limit: int = 100) -> list[Dict[str, Any]]:
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(text("""
|
||||
WITH current_invoice AS (
|
||||
SELECT DISTINCT ON (opportunity_id)
|
||||
id::text AS document_id,
|
||||
opportunity_id,
|
||||
document_number,
|
||||
external_id,
|
||||
total_amount,
|
||||
amount,
|
||||
currency,
|
||||
status,
|
||||
system,
|
||||
role,
|
||||
is_primary,
|
||||
created_at
|
||||
FROM commercial_documents
|
||||
WHERE document_kind = 'invoice'
|
||||
AND opportunity_id IS NOT NULL
|
||||
AND COALESCE(is_primary, TRUE) = TRUE
|
||||
AND COALESCE(role, 'current') IN ('current', 'accepted')
|
||||
ORDER BY opportunity_id, created_at DESC, id DESC
|
||||
)
|
||||
SELECT
|
||||
t.id::text AS task_id,
|
||||
t.action_code,
|
||||
t.route,
|
||||
t.action,
|
||||
t.note,
|
||||
t.status AS task_status,
|
||||
t.priority,
|
||||
t.opportunity_id::text,
|
||||
o.title AS opportunity_title,
|
||||
o.customer_name,
|
||||
o.stage AS opportunity_stage,
|
||||
i.document_id,
|
||||
i.document_number,
|
||||
i.external_id,
|
||||
i.total_amount,
|
||||
i.amount,
|
||||
i.currency,
|
||||
i.status AS invoice_status,
|
||||
i.system AS invoice_system
|
||||
FROM tasks t
|
||||
JOIN opportunities o ON o.id = t.opportunity_id
|
||||
JOIN current_invoice i ON i.opportunity_id = t.opportunity_id
|
||||
WHERE t.status = 'pending'
|
||||
AND t.action_code = 'SEND_INVOICE'
|
||||
ORDER BY t.created_at DESC
|
||||
LIMIT :limit
|
||||
"""), {"limit": int(limit)}).mappings().all()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
def apply_candidate(row: Dict[str, Any]) -> int:
|
||||
number = str(row.get("document_number") or row.get("external_id") or "fatura").strip()
|
||||
amount = _money(row.get("total_amount") or row.get("amount"), str(row.get("currency") or "EUR"))
|
||||
action = f"Enviar fatura {number} ao cliente"
|
||||
note = f"Enviar fatura {number} ao cliente." + (f" Valor: {amount}." if amount else "") + " PDF/anexo disponível para envio automático."
|
||||
patch = {
|
||||
"invoice_delivery_context": {
|
||||
"document_id": row.get("document_id"),
|
||||
"document_number": number,
|
||||
"amount": str(row.get("total_amount") or row.get("amount") or ""),
|
||||
"currency": str(row.get("currency") or "EUR"),
|
||||
"pdf_available": True,
|
||||
"synced_by": "sync_invoice_delivery_context",
|
||||
}
|
||||
}
|
||||
with engine.begin() as conn:
|
||||
result = conn.execute(text("""
|
||||
UPDATE tasks
|
||||
SET action = CAST(:action AS TEXT),
|
||||
note = CAST(:note AS TEXT),
|
||||
updated_at = now(),
|
||||
metadata = COALESCE(metadata, '{}'::jsonb) || CAST(:metadata_patch AS JSONB)
|
||||
WHERE id = CAST(:task_id AS UUID)
|
||||
AND status = 'pending'
|
||||
AND action_code = 'SEND_INVOICE'
|
||||
"""), {
|
||||
"task_id": row["task_id"],
|
||||
"action": action,
|
||||
"note": note,
|
||||
"metadata_patch": _json(patch),
|
||||
})
|
||||
return int(getattr(result, "rowcount", 0) or 0)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--apply", action="store_true", help="Update matching pending SEND_INVOICE task notes/actions")
|
||||
parser.add_argument("--limit", type=int, default=100)
|
||||
args = parser.parse_args()
|
||||
|
||||
rows = find_candidates(args.limit)
|
||||
print(f"SEND_INVOICE pendentes com fatura atual ligada: {len(rows)}")
|
||||
updated = 0
|
||||
for row in rows:
|
||||
print("-", row["task_id"], row.get("customer_name"), row.get("document_number") or row.get("external_id"), row.get("total_amount") or row.get("amount"))
|
||||
if args.apply:
|
||||
updated += apply_candidate(row)
|
||||
if args.apply:
|
||||
print(f"updated: {updated}")
|
||||
else:
|
||||
print("dry-run apenas. Usa --apply para atualizar contexto da task.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user