Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
@@ -13,7 +13,7 @@ _SCHEMA_READY = False
|
||||
|
||||
OPERATION_ACTIONS: Dict[str, Dict[str, str]] = {
|
||||
"jasmin_quotation": {"label": "Registar orçamento Jasmin", "system": "jasmin", "external_type": "quotation", "status": "created", "stage": "QUOTE_SENT", "note": "Orçamento criado/registado no Jasmin."},
|
||||
"jasmin_proforma": {"label": "Registar pró-forma Jasmin", "system": "jasmin", "external_type": "proforma", "status": "issued", "stage": "PROFORMA_SENT", "note": "Pró-forma registada no fluxo Jasmin."},
|
||||
"jasmin_proforma": {"label": "Registo legado de pró-forma", "system": "jasmin", "external_type": "proforma", "status": "issued", "stage": "QUOTE_SENT", "note": "Registo legado tratado como orçamento/pedido de pagamento."},
|
||||
"payment_confirmed": {"label": "Registar pagamento confirmado", "system": "clientflow", "external_type": "payment", "status": "confirmed", "stage": "PAYMENT_CONFIRMED", "note": "Pagamento confirmado pelo operador."},
|
||||
"odoo_sale_order": {"label": "Registar venda Odoo", "system": "odoo", "external_type": "sale_order", "status": "created", "stage": "ODOO_ORDER_CREATED", "note": "Venda/encomenda registada no Odoo."},
|
||||
"odoo_production": {"label": "Registar produção Odoo", "system": "odoo", "external_type": "production", "status": "in_progress", "stage": "IN_PRODUCTION", "note": "Produção/preparação em curso no Odoo."},
|
||||
@@ -71,7 +71,7 @@ def ensure_operation_schema() -> None:
|
||||
def integration_settings_summary() -> Dict[str, Dict[str, Any]]:
|
||||
return {
|
||||
"odoo": {"enabled": bool(settings.odoo_enabled), "label": "Odoo", "public_url": settings.odoo_public_url or settings.odoo_base_url, "role": "Vendas, stock, produção e validação física"},
|
||||
"jasmin": {"enabled": bool(settings.jasmin_enabled), "label": "Jasmin", "public_url": settings.jasmin_public_url or settings.jasmin_base_url, "role": "Pró-formas, faturas e documentos fiscais"},
|
||||
"jasmin": {"enabled": bool(settings.jasmin_enabled), "label": "Jasmin", "public_url": settings.jasmin_public_url or settings.jasmin_base_url, "role": "Orçamentos, faturas e documentos fiscais"},
|
||||
"packlink": {"enabled": bool(settings.packlink_enabled), "label": "Packlink PRO", "public_url": settings.packlink_public_url or settings.packlink_base_url, "role": "Recolhas, envios, etiquetas e tracking"},
|
||||
"chatwoot": {"enabled": bool(settings.chatwoot_base_url), "label": "Chatwoot", "public_url": settings.chatwoot_public_url or settings.chatwoot_base_url, "role": "Comunicação com cliente"},
|
||||
}
|
||||
@@ -87,12 +87,69 @@ def get_operation_links(opportunity_id: str) -> List[Dict[str, Any]]:
|
||||
"""), {"opportunity_id": opportunity_id}).mappings().all()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def _commercial_document_operation_fallbacks(opportunity_id: str) -> Dict[tuple, Dict[str, Any]]:
|
||||
"""Infer operation cards from linked commercial_documents when operation_links lag.
|
||||
|
||||
Reconstructed opportunities often have commercial_documents imported from
|
||||
Jasmin but no matching operation_links. Without this fallback the flow card
|
||||
can show “Fatura ○” although a current invoice is visibly associated.
|
||||
"""
|
||||
try:
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(text("""
|
||||
SELECT document_kind, document_number, external_id, external_url, status, payload, updated_at
|
||||
FROM commercial_documents
|
||||
WHERE opportunity_id = CAST(:opportunity_id AS UUID)
|
||||
AND COALESCE(is_active, TRUE) IS TRUE
|
||||
AND COALESCE(role, 'current') IN ('current','accepted','historical','history')
|
||||
AND document_kind IN ('quotation','quote','proforma','invoice')
|
||||
ORDER BY
|
||||
CASE WHEN COALESCE(role, 'current') IN ('current','accepted') THEN 0 ELSE 1 END,
|
||||
CASE WHEN document_kind = 'invoice' THEN 0 ELSE 1 END,
|
||||
COALESCE(updated_at, now()) DESC
|
||||
LIMIT 10
|
||||
"""), {"opportunity_id": opportunity_id}).mappings().all()
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
fallbacks: Dict[tuple, Dict[str, Any]] = {}
|
||||
for row in rows:
|
||||
kind = str(row.get("document_kind") or "").lower()
|
||||
if kind == "invoice":
|
||||
key = ("jasmin", "invoice")
|
||||
status = "issued"
|
||||
elif kind in {"quotation", "quote", "proforma"}:
|
||||
key = ("jasmin", "quotation")
|
||||
status = "created"
|
||||
else:
|
||||
continue
|
||||
if key in fallbacks:
|
||||
continue
|
||||
name = row.get("document_number") or row.get("external_id") or "Documento Jasmin"
|
||||
fallbacks[key] = {
|
||||
"id": "",
|
||||
"opportunity_id": opportunity_id,
|
||||
"system": key[0],
|
||||
"external_type": key[1],
|
||||
"external_id": row.get("external_id") or row.get("document_number") or "",
|
||||
"external_name": name,
|
||||
"external_url": row.get("external_url") or "",
|
||||
"status": status,
|
||||
"payload": row.get("payload") or {"source": "commercial_documents_fallback"},
|
||||
"last_synced_at": row.get("updated_at"),
|
||||
"created_at": row.get("updated_at"),
|
||||
"updated_at": row.get("updated_at"),
|
||||
}
|
||||
return fallbacks
|
||||
|
||||
|
||||
def get_operation_snapshot(opportunity_id: str) -> Dict[str, Any]:
|
||||
links = get_operation_links(opportunity_id)
|
||||
by_key = {(x["system"], x["external_type"]): x for x in links}
|
||||
commercial_fallbacks = _commercial_document_operation_fallbacks(opportunity_id)
|
||||
cards = []
|
||||
for card in OPERATION_CARDS:
|
||||
link = by_key.get((card["system"], card["external_type"]))
|
||||
link = by_key.get((card["system"], card["external_type"])) or commercial_fallbacks.get((card["system"], card["external_type"]))
|
||||
if link:
|
||||
status = link.get("status") or "pending"
|
||||
cards.append({**card, **link, "status_label": STATUS_LABELS.get(status, status)})
|
||||
@@ -126,15 +183,60 @@ def register_operation_action(opportunity_id: str, action_key: str, *, external_
|
||||
FROM opportunities WHERE id = CAST(:opportunity_id AS UUID)
|
||||
"""), {"id": str(uuid.uuid4()), "opportunity_id": opportunity_id, "stage": stage, "note": note, "payload": _json({"action_key": action_key, "system": system, "external_type": external_type, "status": status}), "created_by": created_by})
|
||||
set_opportunity_stage(opportunity_id, stage, note=note, created_by=created_by)
|
||||
if str(action_key or "") == "odoo_sale_order":
|
||||
# Manual sales-number registration should close the pending PREPARE_ORDER task
|
||||
# and immediately try to read WH/OUT status from Odoo. This keeps the
|
||||
# opportunity detail coherent after the operator enters S00xxx.
|
||||
try:
|
||||
sale_ref = external_name or external_id or ""
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("""
|
||||
UPDATE tasks
|
||||
SET status = 'done',
|
||||
note = COALESCE(note, '') || CAST(:note AS TEXT),
|
||||
updated_at = NOW()
|
||||
WHERE opportunity_id = CAST(:opportunity_id AS UUID)
|
||||
AND action_code = 'PREPARE_ORDER'
|
||||
AND status = 'pending'
|
||||
"""), {
|
||||
"opportunity_id": opportunity_id,
|
||||
"note": f"\n\nConcluída automaticamente: venda Odoo {sale_ref} associada." if sale_ref else "\n\nConcluída automaticamente: venda Odoo associada.",
|
||||
})
|
||||
except Exception as exc:
|
||||
print(f"ClientFlow prepare-order task cleanup failed for {opportunity_id}: {exc}", flush=True)
|
||||
try:
|
||||
from app.odoo_service import sync_opportunity_odoo_status
|
||||
|
||||
result = dict(row or {})
|
||||
result["odoo_sync"] = sync_opportunity_odoo_status(opportunity_id)
|
||||
return result
|
||||
except Exception as exc:
|
||||
print(f"ClientFlow Odoo status sync after sale link failed for {opportunity_id}: {exc}", flush=True)
|
||||
if str(action_key or "") == "payment_confirmed":
|
||||
try:
|
||||
from app.task_service import mark_obsolete_payment_followup_tasks
|
||||
|
||||
closed = mark_obsolete_payment_followup_tasks(
|
||||
opportunity_id,
|
||||
actor=created_by or "operator",
|
||||
reason="Pagamento confirmado registado; fechar follow-ups/confirmações de pagamento pendentes.",
|
||||
)
|
||||
result = dict(row or {})
|
||||
result["obsolete_tasks_closed"] = closed
|
||||
return result
|
||||
except Exception as exc:
|
||||
print(f"ClientFlow obsolete payment task cleanup failed for {opportunity_id}: {exc}", flush=True)
|
||||
return dict(row or {})
|
||||
|
||||
def operation_next_steps(snapshot: Dict[str, Any], stage: str) -> List[str]:
|
||||
stage = str(stage or "")
|
||||
status_by = {c["key"]: c.get("status") for c in snapshot.get("cards", [])}
|
||||
steps: List[str] = []
|
||||
if stage in {"QUOTE_SENT", "PROFORMA_SENT", "WAITING_PAYMENT"} and status_by.get("payment") != "confirmed":
|
||||
steps.append("Confirmar pagamento antes de criar venda operacional no Odoo.")
|
||||
if stage in {"PAYMENT_CONFIRMED"} and status_by.get("odoo_sale_order") == "not_created":
|
||||
if stage in {"QUOTE_SENT", "WAITING_PAYMENT"} and status_by.get("payment") != "confirmed":
|
||||
steps.append("Confirmar pagamento com base no orçamento antes de emitir fatura e preparar envio.")
|
||||
if stage in {"PAYMENT_CONFIRMED"} and status_by.get("jasmin_invoice") == "not_created":
|
||||
steps.append("Emitir/registar fatura no Jasmin depois do pagamento confirmado.")
|
||||
if stage in {"PAYMENT_CONFIRMED", "INVOICED"} and status_by.get("odoo_sale_order") == "not_created":
|
||||
steps.append("Criar venda/encomenda no Odoo com os dados validados no ClientFlow.")
|
||||
if stage in {"ODOO_ORDER_CREATED", "IN_PRODUCTION"} and status_by.get("physical_validation") != "validated":
|
||||
steps.append("Aguardar produção/preparação e validar a encomenda física no Odoo.")
|
||||
@@ -142,8 +244,8 @@ def operation_next_steps(snapshot: Dict[str, Any], stage: str) -> List[str]:
|
||||
steps.append("Emitir fatura no Jasmin antes do envio, salvo exceção operacional.")
|
||||
if stage in {"READY_TO_SHIP", "INVOICED"} and status_by.get("packlink_shipment") != "created":
|
||||
steps.append("Criar envio/recolha no Packlink quando a encomenda estiver pronta.")
|
||||
if stage == "SHIPMENT_CREATED" and status_by.get("tracking") != "sent":
|
||||
steps.append("Enviar tracking ao cliente pelo Chatwoot/email.")
|
||||
if stage == "SHIPMENT_CREATED":
|
||||
steps.append("Envio registado. Por agora, concluir quando fatura enviada, pagamento confirmado e WH/OUT concluído.")
|
||||
if not steps:
|
||||
steps.append("Fluxo sem bloqueios aparentes. Rever tarefas pendentes e próximos contactos.")
|
||||
return steps
|
||||
|
||||
Reference in New Issue
Block a user