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