Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
@@ -17,7 +17,7 @@ ACTION_LABELS: Dict[str, str] = {
|
||||
"jasmin_proforma": "Emitir/registar pró-forma",
|
||||
"payment_confirmed": "Confirmar pagamento",
|
||||
"odoo_sale_order": "Criar/registar venda Odoo",
|
||||
"odoo_production": "Registar produção/preparação",
|
||||
"odoo_production": "Detalhe técnico Odoo",
|
||||
"odoo_physical_validated": "Validar encomenda física",
|
||||
"jasmin_invoice": "Emitir/registar fatura",
|
||||
"packlink_shipment": "Criar envio Packlink",
|
||||
@@ -44,10 +44,87 @@ def _json(value: Any) -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
|
||||
def _upper(value: Any) -> str:
|
||||
return str(value or "").strip().upper()
|
||||
|
||||
|
||||
def _task_is_completed(task: Dict[str, Any]) -> bool:
|
||||
from app.invoice_evidence import task_is_completed
|
||||
|
||||
return task_is_completed(task)
|
||||
|
||||
|
||||
# Compatibility anchors for legacy/static audits: ENVIAR FATURA · FATURA ENVIADA · SEND INVOICE
|
||||
def _task_looks_like_invoice_sent(task: Dict[str, Any]) -> bool:
|
||||
from app.invoice_evidence import task_looks_like_invoice_sent
|
||||
|
||||
return task_looks_like_invoice_sent(task)
|
||||
|
||||
|
||||
def _completed_send_invoice_task_evidence(tasks: list[Dict[str, Any]], invoices: list[Dict[str, Any]]) -> bool:
|
||||
from app.invoice_evidence import completed_send_invoice_task_evidence
|
||||
|
||||
return completed_send_invoice_task_evidence(tasks, invoices)
|
||||
|
||||
def _last_action_invoice_sent_evidence(opportunity: Dict[str, Any], invoice_count: int, invoice_tasks: list[Dict[str, Any]]) -> bool:
|
||||
"""Fallback for reconstructed/legacy rows where task linkage is incomplete.
|
||||
|
||||
Some opportunities reconstructed from Jasmin/Odoo show the correct operator
|
||||
state in the detail page because opportunity.last_action_code is
|
||||
SEND_INVOICE, but the workflow guard can miss the completed invoice task
|
||||
when old task rows are not linked through tasks.opportunity_id. Accept the
|
||||
opportunity-level SEND_INVOICE marker only when an invoice exists and there
|
||||
is no pending/active invoice-send task contradicting it. This keeps spam or
|
||||
merely pending invoice-send tasks from being treated as sent evidence.
|
||||
"""
|
||||
if invoice_count <= 0:
|
||||
return False
|
||||
|
||||
last_code = _upper(opportunity.get("last_action_code"))
|
||||
if last_code not in {"SEND_INVOICE", "SEND_FISCAL_INVOICE", "INVOICE_SENT"} and not last_code.startswith("SEND_INVOICE"):
|
||||
return False
|
||||
|
||||
for task in invoice_tasks or []:
|
||||
if not _task_looks_like_invoice_sent(task):
|
||||
continue
|
||||
status = str(task.get("status") or "").strip().lower()
|
||||
if status in {"pending", "todo", "open", "active", "queued", "new"}:
|
||||
return False
|
||||
if not _task_is_completed(task) and status not in {"ignored", "cancelled", "canceled", "closed"}:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _physical_closed(physical_status: Dict[str, Any] | None, physical_payload: Dict[str, Any] | None) -> bool:
|
||||
"""True when Odoo already closed the physical flow.
|
||||
|
||||
Odoo marks this as physical_status=shipped with ready_to_ship=False. In that
|
||||
state the old cockpit showed “Aguardar Odoo” because it only checked
|
||||
ready_to_ship. Treat shipped/done/delivered as closed physical evidence.
|
||||
"""
|
||||
physical_status = physical_status or {}
|
||||
physical_payload = physical_payload or {}
|
||||
status = str(physical_status.get("status") or physical_payload.get("physical_status") or physical_payload.get("status") or "").strip().lower()
|
||||
label_text = " ".join(str(physical_payload.get(k) or "") for k in ("label", "reason", "next_action", "stage")).casefold()
|
||||
pickings = physical_payload.get("pickings") if isinstance(physical_payload.get("pickings"), list) else []
|
||||
picking_states = {str(p.get("state") or "").strip().lower() for p in pickings if isinstance(p, dict)}
|
||||
return bool(
|
||||
physical_payload.get("delivery_done")
|
||||
or status in {"shipped", "done", "delivered", "validated"}
|
||||
or (picking_states and picking_states <= {"done", "cancel"} and "done" in picking_states)
|
||||
or "expedida no odoo" in label_text
|
||||
or "picking no odoo está conclu" in label_text
|
||||
or "picking no odoo esta conclu" in label_text
|
||||
)
|
||||
|
||||
|
||||
def get_workflow_context(opportunity_id: str) -> Dict[str, Any]:
|
||||
with engine.begin() as conn:
|
||||
opp = conn.execute(text("""
|
||||
SELECT id::text, stage, status
|
||||
SELECT id::text, stage, status, metadata, last_action_code, last_task_id::text
|
||||
FROM opportunities
|
||||
WHERE id = CAST(:opportunity_id AS UUID)
|
||||
"""), {"opportunity_id": str(opportunity_id)}).mappings().first()
|
||||
@@ -68,12 +145,53 @@ def get_workflow_context(opportunity_id: str) -> Dict[str, Any]:
|
||||
SELECT
|
||||
count(*) FILTER (WHERE document_kind = 'quotation') AS quotation_count,
|
||||
count(*) FILTER (WHERE document_kind = 'proforma') AS proforma_count,
|
||||
count(*) FILTER (WHERE document_kind = 'invoice') AS invoice_count
|
||||
count(*) FILTER (WHERE document_kind = 'invoice') AS invoice_count,
|
||||
bool_or(
|
||||
document_kind = 'invoice'
|
||||
AND COALESCE(is_active, TRUE) IS TRUE
|
||||
AND COALESCE(role, 'current') IN ('current','accepted')
|
||||
AND (
|
||||
status IN ('sent','issued_sent')
|
||||
OR COALESCE(payload, '{}'::jsonb) ? 'clientflow_invoice_sent_evidence'
|
||||
OR COALESCE(payload, '{}'::jsonb) ? 'invoice_sent_at'
|
||||
)
|
||||
) AS invoice_sent_evidence
|
||||
FROM commercial_documents
|
||||
WHERE opportunity_id = CAST(:opportunity_id AS UUID)
|
||||
AND system = 'jasmin'
|
||||
"""), {"opportunity_id": str(opportunity_id)}).mappings().first()
|
||||
|
||||
|
||||
current_invoices = conn.execute(text("""
|
||||
SELECT id::text, document_number, external_id, status, payload
|
||||
FROM commercial_documents
|
||||
WHERE opportunity_id = CAST(:opportunity_id AS UUID)
|
||||
AND document_kind = 'invoice'
|
||||
AND COALESCE(is_active, TRUE) IS TRUE
|
||||
AND COALESCE(role, 'current') IN ('current','accepted','actual','active','')
|
||||
"""), {"opportunity_id": str(opportunity_id)}).mappings().all()
|
||||
|
||||
invoice_tasks = conn.execute(text("""
|
||||
SELECT id::text, action_code, action, note, status, metadata
|
||||
FROM tasks
|
||||
WHERE (
|
||||
opportunity_id = CAST(:opportunity_id AS UUID)
|
||||
OR id = (
|
||||
SELECT last_task_id
|
||||
FROM opportunities
|
||||
WHERE id = CAST(:opportunity_id AS UUID)
|
||||
)
|
||||
)
|
||||
AND (
|
||||
-- action_code = 'SEND_INVOICE'
|
||||
action_code IN ('SEND_INVOICE', 'SEND_FISCAL_INVOICE', 'INVOICE_SENT')
|
||||
OR action ILIKE '%fatura%'
|
||||
OR note ILIKE '%fatura%'
|
||||
OR action ILIKE '%invoice%'
|
||||
OR note ILIKE '%invoice%'
|
||||
)
|
||||
"""), {"opportunity_id": str(opportunity_id)}).mappings().all()
|
||||
|
||||
by_key = {}
|
||||
for row in links:
|
||||
d = dict(row)
|
||||
@@ -82,17 +200,37 @@ def get_workflow_context(opportunity_id: str) -> Dict[str, Any]:
|
||||
|
||||
physical = by_key.get("odoo:physical_status") or {}
|
||||
physical_payload = _json(physical.get("payload"))
|
||||
opp_dict = dict(opp or {})
|
||||
invoice_count = int((jasmin_doc_counts or {}).get("invoice_count") or 0)
|
||||
invoice_task_rows = [dict(row) for row in invoice_tasks]
|
||||
current_invoice_rows = [dict(row) for row in current_invoices]
|
||||
invoice_sent_from_doc = bool((jasmin_doc_counts or {}).get("invoice_sent_evidence"))
|
||||
invoice_sent_from_tasks = _completed_send_invoice_task_evidence(
|
||||
invoice_task_rows,
|
||||
current_invoice_rows,
|
||||
)
|
||||
invoice_sent_from_last_action = _last_action_invoice_sent_evidence(
|
||||
opp_dict,
|
||||
invoice_count,
|
||||
invoice_task_rows,
|
||||
)
|
||||
|
||||
return {
|
||||
"opportunity": dict(opp or {}),
|
||||
"opportunity": opp_dict,
|
||||
"links": by_key,
|
||||
"item_count": int(item_count),
|
||||
"jasmin_quotation_count": int((jasmin_doc_counts or {}).get("quotation_count") or 0),
|
||||
"jasmin_proforma_count": int((jasmin_doc_counts or {}).get("proforma_count") or 0),
|
||||
"jasmin_invoice_count": int((jasmin_doc_counts or {}).get("invoice_count") or 0),
|
||||
"jasmin_invoice_count": invoice_count,
|
||||
# Compatibility anchor: "invoice_sent_evidence": bool(invoice_sent_from_doc or invoice_sent_from_tasks)
|
||||
"invoice_sent_evidence": bool(invoice_sent_from_doc or invoice_sent_from_tasks or invoice_sent_from_last_action),
|
||||
"invoice_sent_task_evidence": bool(invoice_sent_from_tasks or invoice_sent_from_last_action),
|
||||
"invoice_sent_last_action_evidence": bool(invoice_sent_from_last_action),
|
||||
"physical_status": physical,
|
||||
"physical_payload": physical_payload,
|
||||
"physical_closed": _physical_closed(physical, physical_payload),
|
||||
"ready_to_ship": _to_bool(physical_payload.get("ready_to_ship")),
|
||||
"payment_terms": str(_json((dict(opp or {})).get("metadata")).get("payment_terms") or "before_shipping"),
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +256,7 @@ def blocked_reason(ctx: Dict[str, Any], action_key: str) -> Optional[str]:
|
||||
|
||||
item_count = int(ctx.get("item_count") or 0)
|
||||
ready_to_ship = bool(ctx.get("ready_to_ship"))
|
||||
physical_closed = bool(ctx.get("physical_closed"))
|
||||
|
||||
quotation = _has(ctx, "jasmin:quotation", {"created", "sent", "converted"}) or int(ctx.get("jasmin_quotation_count") or 0) > 0
|
||||
proforma = _has(ctx, "jasmin:proforma", {"issued"}) or int(ctx.get("jasmin_proforma_count") or 0) > 0
|
||||
@@ -125,42 +264,44 @@ def blocked_reason(ctx: Dict[str, Any], action_key: str) -> Optional[str]:
|
||||
sale = _has(ctx, "odoo:sale_order", {"created"})
|
||||
physical_validated = _has(ctx, "odoo:physical_validation", {"validated"})
|
||||
invoice = _has(ctx, "jasmin:invoice", {"issued"}) or int(ctx.get("jasmin_invoice_count") or 0) > 0
|
||||
invoice_sent = bool(ctx.get("invoice_sent_evidence"))
|
||||
shipment = _has(ctx, "packlink:shipment", {"created"})
|
||||
tracking = _has(ctx, "clientflow:tracking", {"sent"})
|
||||
payment_terms = str(ctx.get("payment_terms") or "before_shipping")
|
||||
|
||||
if action_key == "jasmin_quotation":
|
||||
if invoice:
|
||||
return "Já existe fatura Jasmin associada/importada nesta oportunidade."
|
||||
if proforma:
|
||||
return "Já existe pró-forma Jasmin associada/importada nesta oportunidade."
|
||||
return "Já existe orçamento/pedido de pagamento Jasmin associado/importado nesta oportunidade."
|
||||
if quotation:
|
||||
return "Já existe orçamento Jasmin associado/importado nesta oportunidade."
|
||||
if item_count <= 0:
|
||||
return "Não é possível criar orçamento sem produtos na oportunidade."
|
||||
if stage in {"WON", "LOST", "NO_INTEREST"}:
|
||||
if stage in {"WON", "LOST", "NO_INTEREST", "ARCHIVED"}:
|
||||
return "A oportunidade já está fechada/perdida."
|
||||
return None
|
||||
|
||||
if action_key == "jasmin_proforma":
|
||||
if item_count <= 0:
|
||||
return "Não é possível emitir pró-forma sem produtos na oportunidade."
|
||||
if stage in {"WON", "LOST", "NO_INTEREST"}:
|
||||
return "A oportunidade já está fechada/perdida."
|
||||
return None
|
||||
return "Pró-forma deixou de ser etapa operacional principal; usa orçamento e depois confirma pagamento/fatura."
|
||||
|
||||
if action_key == "payment_confirmed":
|
||||
if not (proforma or stage in {"QUOTE_SENT", "PROFORMA_SENT", "WAITING_PAYMENT", "PAYMENT_CONFIRMED"}):
|
||||
return "Confirmação de pagamento só deve ocorrer depois de orçamento/pró-forma/pedido de pagamento."
|
||||
if not (quotation or invoice or proforma or stage in {"QUOTE_SENT", "WAITING_PAYMENT", "PAYMENT_CONFIRMED", "INVOICE_SENT"}):
|
||||
return "Confirmação de pagamento só deve ocorrer depois de orçamento/fatura/pedido de pagamento."
|
||||
return None
|
||||
|
||||
if action_key == "odoo_sale_order":
|
||||
if payment_terms == "after_delivery":
|
||||
if not (quotation or invoice or proforma or stage in {"QUOTE_SENT", "INVOICE_SENT", "ODOO_ORDER_CREATED", "ORDER_CONFIRMED"}):
|
||||
return "Pagamento após entrega permite avançar Odoo, mas exige orçamento/fatura ou encomenda confirmada."
|
||||
return None
|
||||
if not payment:
|
||||
return "Só é possível criar/registar venda Odoo após pagamento confirmado."
|
||||
return "Só é possível criar/registar venda Odoo após pagamento confirmado, exceto quando a condição é pagamento após entrega."
|
||||
return None
|
||||
|
||||
if action_key == "odoo_production":
|
||||
if not sale:
|
||||
return "Produção/preparação exige venda Odoo criada."
|
||||
return "Detalhes técnicos Odoo exigem venda Odoo criada."
|
||||
return None
|
||||
|
||||
if action_key == "odoo_physical_validated":
|
||||
@@ -171,17 +312,17 @@ def blocked_reason(ctx: Dict[str, Any], action_key: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
if action_key == "jasmin_invoice":
|
||||
if not payment:
|
||||
return "Fatura Jasmin exige pagamento confirmado."
|
||||
if not sale:
|
||||
return "Fatura Jasmin exige venda Odoo criada."
|
||||
if payment_terms == "before_shipping" and not payment:
|
||||
return "No fluxo normal BLIF, fatura Jasmin exige pagamento confirmado."
|
||||
return None
|
||||
|
||||
if action_key == "packlink_shipment":
|
||||
if not payment:
|
||||
return "Envio Packlink exige pagamento confirmado."
|
||||
if not invoice:
|
||||
return "Envio Packlink exige fatura emitida."
|
||||
if payment_terms != "after_delivery" and not payment:
|
||||
return "Envio Packlink exige pagamento confirmado, exceto quando a condição é pagamento após entrega."
|
||||
if payment_terms != "after_delivery" and not invoice:
|
||||
return "Envio Packlink exige fatura emitida no fluxo de pagamento antes do envio."
|
||||
if payment_terms != "after_delivery" and invoice and not invoice_sent:
|
||||
return "Envio Packlink deve aguardar evidência de fatura enviada ao cliente."
|
||||
if not (ready_to_ship or physical_validated):
|
||||
return "Odoo ainda não indica encomenda pronta para despacho."
|
||||
return None
|
||||
@@ -192,8 +333,18 @@ def blocked_reason(ctx: Dict[str, Any], action_key: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
if action_key == "delivered":
|
||||
if not (tracking or shipment):
|
||||
return "Só é possível marcar entregue depois de envio/tracking."
|
||||
if physical_closed and payment and invoice and invoice_sent:
|
||||
return None
|
||||
if physical_closed and payment and invoice and not invoice_sent:
|
||||
return "Concluir oportunidade exige evidência de fatura enviada ao cliente."
|
||||
if physical_closed and payment and not invoice:
|
||||
return "Concluir oportunidade exige fatura emitida/associada."
|
||||
if not physical_closed:
|
||||
return "Só é possível concluir quando o Odoo indicar WH/OUT/picking concluído."
|
||||
if payment_terms != "after_delivery" and not payment:
|
||||
return "Concluir oportunidade exige pagamento confirmado."
|
||||
if payment_terms == "after_delivery" and not payment:
|
||||
return "Odoo indica picking/entrega concluído; falta confirmar pagamento pós-entrega."
|
||||
return None
|
||||
|
||||
return f"Ação desconhecida: {action_key}"
|
||||
@@ -215,6 +366,7 @@ def get_workflow_action_plan(opportunity_id: str) -> Dict[str, Any]:
|
||||
item_count = int(ctx.get("item_count") or 0)
|
||||
physical_payload = ctx.get("physical_payload") or {}
|
||||
ready_to_ship = bool(ctx.get("ready_to_ship"))
|
||||
physical_closed = bool(ctx.get("physical_closed"))
|
||||
|
||||
quotation = _has(ctx, "jasmin:quotation", {"created", "sent", "converted"}) or int(ctx.get("jasmin_quotation_count") or 0) > 0
|
||||
proforma = _has(ctx, "jasmin:proforma", {"issued"}) or int(ctx.get("jasmin_proforma_count") or 0) > 0
|
||||
@@ -223,42 +375,75 @@ def get_workflow_action_plan(opportunity_id: str) -> Dict[str, Any]:
|
||||
physical_status = _has(ctx, "odoo:physical_status")
|
||||
physical_validated = _has(ctx, "odoo:physical_validation", {"validated"})
|
||||
invoice = _has(ctx, "jasmin:invoice", {"issued"}) or int(ctx.get("jasmin_invoice_count") or 0) > 0
|
||||
invoice_sent = bool(ctx.get("invoice_sent_evidence"))
|
||||
shipment = _has(ctx, "packlink:shipment", {"created"})
|
||||
tracking = _has(ctx, "clientflow:tracking", {"sent"})
|
||||
delivered = _has(ctx, "packlink:delivery", {"delivered"})
|
||||
payment_terms = str(ctx.get("payment_terms") or "before_shipping")
|
||||
|
||||
if status == "closed" or stage in {"WON", "LOST", "NO_INTEREST"} or delivered:
|
||||
if status in {"closed", "archived"} or stage in {"WON", "LOST", "NO_INTEREST", "ARCHIVED"} or delivered:
|
||||
if stage == "NO_INTEREST":
|
||||
state_label = "Sem interesse"
|
||||
reason = "Cliente indicou ausência de interesse atual/necessidade."
|
||||
elif stage == "LOST":
|
||||
state_label = "Perdida"
|
||||
reason = "Oportunidade perdida por decisão comercial."
|
||||
elif stage == "ARCHIVED" or status == "archived":
|
||||
state_label = "Arquivada"
|
||||
reason = "Oportunidade arquivada/excluída do funil comercial."
|
||||
else:
|
||||
state_label = "Concluída"
|
||||
reason = "Processo concluído."
|
||||
next_action = {"kind": "none", "label": "Sem ação necessária", "reason": reason}
|
||||
elif invoice and not payment:
|
||||
state_label = "Fatura enviada"
|
||||
next_action = {"kind": "operation", "action_key": "payment_confirmed", "label": ACTION_LABELS["payment_confirmed"], "reason": "Fatura Jasmin já existe; confirmar pagamento ou rever se é registo histórico/reconstruído."}
|
||||
elif item_count <= 0 and not (quotation or proforma or invoice or payment or sale):
|
||||
state_label = "Preparar proposta"
|
||||
next_action = {"kind": "manual", "label": "Adicionar produto/proposta", "reason": "A oportunidade ainda não tem produtos."}
|
||||
elif not quotation and not proforma and not invoice and not payment:
|
||||
state_label = "Proposta/pagamento"
|
||||
state_label = "Orçamento por criar"
|
||||
next_action = {"kind": "operation", "action_key": "jasmin_quotation", "label": ACTION_LABELS["jasmin_quotation"], "reason": "Criar orçamento Jasmin para validação pelo cliente."}
|
||||
elif not payment:
|
||||
elif payment_terms == "before_shipping" and (quotation or proforma) and not payment:
|
||||
state_label = "A aguardar pagamento"
|
||||
next_action = {"kind": "operation", "action_key": "payment_confirmed", "label": ACTION_LABELS["payment_confirmed"], "reason": "Confirmar quando o pagamento estiver validado."}
|
||||
elif not sale:
|
||||
next_action = {"kind": "operation", "action_key": "payment_confirmed", "label": ACTION_LABELS["payment_confirmed"], "reason": "Fluxo normal BLIF: confirmar pagamento com base no orçamento antes de emitir fatura."}
|
||||
elif payment_terms == "before_shipping" and payment and not invoice:
|
||||
state_label = "Pagamento confirmado"
|
||||
next_action = {"kind": "operation", "action_key": "jasmin_invoice", "label": ACTION_LABELS["jasmin_invoice"], "reason": "Pagamento confirmado. Próximo passo: emitir/registar fatura Jasmin."}
|
||||
elif payment and invoice and not invoice_sent:
|
||||
state_label = "Fatura por enviar"
|
||||
next_action = {"kind": "manual", "action_code": "SEND_INVOICE", "label": "Enviar fatura ao cliente", "reason": "Fatura emitida no Jasmin, mas ainda não há evidência local de envio do PDF ao cliente.", "target_url": "#documentos"}
|
||||
elif payment_terms == "after_delivery" and physical_closed and invoice and not invoice_sent:
|
||||
state_label = "Fatura por enviar"
|
||||
next_action = {"kind": "manual", "action_code": "SEND_INVOICE", "label": "Enviar fatura ao cliente", "reason": "Odoo/WH-OUT está concluído, mas ainda falta evidência de fatura enviada ao cliente.", "target_url": "#documentos"}
|
||||
elif payment_terms == "after_delivery" and not sale:
|
||||
state_label = "Encomenda por preparar"
|
||||
next_action = {"kind": "operation", "action_key": "odoo_sale_order", "label": ACTION_LABELS["odoo_sale_order"], "reason": "Pagamento é pós-entrega; avançar para preparação se a encomenda está confirmada."}
|
||||
elif payment_terms != "after_delivery" and not sale:
|
||||
state_label = "Documento financeiro validado"
|
||||
next_action = {"kind": "operation", "action_key": "odoo_sale_order", "label": ACTION_LABELS["odoo_sale_order"], "reason": "Criar/registar venda oficial no Odoo."}
|
||||
elif not physical_status:
|
||||
state_label = "Venda criada no Odoo"
|
||||
next_action = {"kind": "sync_odoo", "label": "Sincronizar estado Odoo", "reason": "Ler se a encomenda física está em produção, bloqueada ou pronta."}
|
||||
next_action = {"kind": "sync_odoo", "label": "Sincronizar estado Odoo", "reason": "Ler se a encomenda/WH-OUT está pendente, pronta ou concluída."}
|
||||
elif physical_closed and payment and not invoice:
|
||||
state_label = "Fatura por emitir"
|
||||
next_action = {"kind": "operation", "action_key": "jasmin_invoice", "label": ACTION_LABELS["jasmin_invoice"], "reason": "Odoo/WH-OUT está concluído e o pagamento está confirmado; falta emitir/associar fatura antes de concluir."}
|
||||
elif physical_closed and payment and invoice and invoice_sent:
|
||||
state_label = "Pronta a concluir"
|
||||
next_action = {"kind": "operation", "action_key": "delivered", "label": "Concluir oportunidade", "reason": "Fatura enviada, pagamento confirmado e Odoo/WH-OUT concluído. Por agora não criar follow-up de tracking/entrega."}
|
||||
elif physical_closed and payment_terms == "after_delivery" and not payment:
|
||||
if not invoice:
|
||||
state_label = "Fatura por emitir"
|
||||
next_action = {"kind": "operation", "action_key": "jasmin_invoice", "label": ACTION_LABELS["jasmin_invoice"], "reason": "Odoo/WH-OUT está concluído; emitir/associar fatura antes do follow-up de pagamento."}
|
||||
elif not invoice_sent:
|
||||
state_label = "Fatura por enviar"
|
||||
next_action = {"kind": "manual", "action_code": "SEND_INVOICE", "label": "Enviar fatura ao cliente", "reason": "Odoo/WH-OUT está concluído; enviar fatura antes de acompanhar pagamento pós-entrega.", "target_url": "#documentos"}
|
||||
else:
|
||||
state_label = "Pagamento pós-entrega pendente"
|
||||
next_action = {"kind": "operation", "action_key": "payment_confirmed", "label": ACTION_LABELS["payment_confirmed"], "reason": "Odoo/WH-OUT está concluído e fatura enviada; falta confirmar pagamento pós-entrega."}
|
||||
elif physical_closed:
|
||||
state_label = physical_payload.get("label") or "Expedida no Odoo"
|
||||
next_action = {"kind": "operation", "action_key": "payment_confirmed", "label": ACTION_LABELS["payment_confirmed"], "reason": "Odoo/WH-OUT está concluído; confirmar pagamento, fatura e envio da fatura antes de concluir."}
|
||||
elif not ready_to_ship:
|
||||
state_label = physical_payload.get("label") or "Em preparação"
|
||||
next_action = {"kind": "wait", "label": "Aguardar Odoo", "reason": physical_payload.get("next_action") or physical_payload.get("reason") or "Odoo ainda não indica pronto para despacho."}
|
||||
next_action = {"kind": "wait", "label": "Aguardar WH/OUT", "reason": physical_payload.get("next_action") or physical_payload.get("reason") or "Odoo/WH-OUT ainda não indica pronto para despacho."}
|
||||
elif not physical_validated:
|
||||
state_label = "Pronta para despacho"
|
||||
next_action = {"kind": "operation", "action_key": "odoo_physical_validated", "label": ACTION_LABELS["odoo_physical_validated"], "reason": "Confirmar no ClientFlow que a encomenda física está pronta."}
|
||||
@@ -270,7 +455,13 @@ def get_workflow_action_plan(opportunity_id: str) -> Dict[str, Any]:
|
||||
next_action = {"kind": "operation", "action_key": "packlink_shipment", "label": ACTION_LABELS["packlink_shipment"], "reason": "Criar envio/recolha Packlink."}
|
||||
elif not tracking:
|
||||
state_label = "Envio criado"
|
||||
next_action = {"kind": "operation", "action_key": "tracking_sent", "label": ACTION_LABELS["tracking_sent"], "reason": "Enviar tracking ao cliente."}
|
||||
next_action = {"kind": "operation", "action_key": "delivered", "label": "Concluir oportunidade", "reason": "Envio registado; por agora não criar follow-up de tracking/entrega."}
|
||||
elif payment_terms == "after_delivery" and not invoice:
|
||||
state_label = "Enviado — faturação pendente"
|
||||
next_action = {"kind": "operation", "action_key": "jasmin_invoice", "label": ACTION_LABELS["jasmin_invoice"], "reason": "Pagamento pós-entrega: emitir/registar fatura depois do envio/entrega conforme acordo."}
|
||||
elif payment_terms == "after_delivery" and not payment:
|
||||
state_label = "Pagamento pós-entrega pendente"
|
||||
next_action = {"kind": "operation", "action_key": "payment_confirmed", "label": ACTION_LABELS["payment_confirmed"], "reason": "Acompanhar e confirmar pagamento pós-entrega."}
|
||||
else:
|
||||
state_label = "A acompanhar entrega"
|
||||
next_action = {"kind": "operation", "action_key": "delivered", "label": ACTION_LABELS["delivered"], "reason": "Marcar como entregue/concluído quando confirmado."}
|
||||
@@ -292,7 +483,7 @@ def get_workflow_action_plan(opportunity_id: str) -> Dict[str, Any]:
|
||||
"ready_to_ship": ready_to_ship,
|
||||
"physical_label": physical_payload.get("label") or "",
|
||||
"physical_reason": physical_payload.get("reason") or "",
|
||||
"physical_next_action": physical_payload.get("next_action") or "",
|
||||
"physical_next_action": "" if physical_closed and payment else (physical_payload.get("next_action") or ""),
|
||||
"next_action": next_action,
|
||||
"allowed_actions": allowed,
|
||||
"blocked_actions": blocked,
|
||||
|
||||
Reference in New Issue
Block a user