Import ClientFlow production v4928.1.5.132.4

This commit is contained in:
plx
2026-07-29 13:11:01 +00:00
parent 6445044ac6
commit 261d342057
405 changed files with 48373 additions and 1401 deletions

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