384 lines
28 KiB
Python
384 lines
28 KiB
Python
from __future__ import annotations
|
|
|
|
from .decision import OpportunityDecision, WorkflowAction
|
|
from .evidence import OpportunityEvidence
|
|
from .profiles import CompanyWorkflowProfile
|
|
from .types import (
|
|
ACTION_CONFIRM_ORDER,
|
|
ACTION_CLOSE_OPPORTUNITY,
|
|
ACTION_CONFIRM_PAYMENT,
|
|
ACTION_CREATE_QUOTE,
|
|
ACTION_FOLLOW_UP,
|
|
ACTION_FOLLOW_UP_PAYMENT,
|
|
ACTION_NO_ACTION,
|
|
ACTION_PREPARE_ORDER,
|
|
ACTION_RECONCILE_DOCUMENTS,
|
|
ACTION_REVIEW,
|
|
ACTION_SEND_INVOICE,
|
|
ACTION_SHIP_ORDER,
|
|
ACTION_VALIDATE_PHYSICAL_ORDER,
|
|
ACTION_VALIDATE_FISCAL_CUSTOMER,
|
|
ACTION_WAIT_PRODUCTION,
|
|
COMMERCIAL_STAGE_IN_EXECUTION,
|
|
COMMERCIAL_STAGE_PAYMENT_CONFIRMED,
|
|
COMMERCIAL_STAGE_QUOTE_SENT,
|
|
COMMERCIAL_STAGE_REVIEW,
|
|
COMMERCIAL_STAGE_WAITING_PAYMENT,
|
|
COMMERCIAL_STAGE_WON,
|
|
PAYMENT_AFTER_DELIVERY,
|
|
PAYMENT_BEFORE_SHIPPING,
|
|
)
|
|
|
|
|
|
SENSITIVE_DOCUMENT_ACTIONS = {ACTION_CREATE_QUOTE, ACTION_SEND_INVOICE, ACTION_CONFIRM_PAYMENT}
|
|
|
|
|
|
def _action(profile: CompanyWorkflowProfile, code: str, description: str = "", **kwargs: object) -> WorkflowAction:
|
|
force_label = kwargs.pop("force_label", None)
|
|
fallback_label = kwargs.pop("label", None)
|
|
return WorkflowAction(
|
|
code=code,
|
|
label=str(force_label) if force_label else profile.action_label(code, fallback_label),
|
|
description=description or profile.action_description(code, ""),
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
def _blocked(profile: CompanyWorkflowProfile, code: str, reason: str) -> WorkflowAction:
|
|
return _action(profile, code, can_execute=False, reason_if_blocked=reason)
|
|
|
|
|
|
def _financial_state(e: OpportunityEvidence) -> str:
|
|
if e.payment_confirmed:
|
|
return "payment_confirmed"
|
|
if e.has_invoice:
|
|
return "invoice_payment_pending"
|
|
if e.has_quote:
|
|
return "quote_payment_pending"
|
|
return "no_document"
|
|
|
|
|
|
def _physical_state(e: OpportunityEvidence) -> str:
|
|
if e.order_delivered:
|
|
return "delivered"
|
|
if e.order_shipped:
|
|
return "shipped"
|
|
if e.odoo_ready:
|
|
return "ready_to_ship"
|
|
if e.odoo_in_production:
|
|
return "in_production"
|
|
if e.has_odoo_sale:
|
|
return "odoo_sale"
|
|
return "none"
|
|
|
|
|
|
def _base_warnings(e: OpportunityEvidence) -> list[str]:
|
|
warnings: list[str] = []
|
|
if e.is_reconstructed:
|
|
warnings.append("Registo reconstruído: validar pagamento, valor e documentos antes de executar ações sensíveis.")
|
|
if e.fiscal_identity_validated and not e.fiscal_data_complete:
|
|
warnings.append("Identidade fiscal associada, mas dados fiscais/envio podem estar incompletos.")
|
|
if e.has_nif_conflict or e.has_fiscal_conflict:
|
|
warnings.append("Existe conflito fiscal/NIF: bloquear documentos e pagamentos até validação.")
|
|
if e.has_pending_task and e.pending_task_action_code == ACTION_SEND_INVOICE and e.has_invoice and e.payment_confirmed and e.invoice_sent is True:
|
|
warnings.append("Há uma task SEND_INVOICE pendente, mas a fatura já tem evidência local de envio. Rever/ignorar a task para não repetir o envio.")
|
|
elif e.has_pending_task and e.pending_task_action_code == ACTION_SEND_INVOICE and e.has_invoice and e.payment_confirmed and e.odoo_in_production:
|
|
warnings.append("Há uma task pendente de envio de fatura, mas a evidência indica fatura existente e produção em curso. Rever se a task está obsoleta ou se falta apenas enviar o PDF ao cliente.")
|
|
if e.payment_terms == PAYMENT_AFTER_DELIVERY and e.has_pending_task and e.pending_task_action_code == ACTION_SEND_INVOICE and e.has_odoo_sale and not e.odoo_ready and not e.order_shipped:
|
|
warnings.append("Pagamento pós-entrega: task de fatura só deve avançar quando a encomenda estiver pronta para entrega/levantamento.")
|
|
if e.has_invalid_payment_task_without_document:
|
|
warnings.append("Existe uma task de pagamento pendente sem orçamento/fatura associado. Associar/criar documento comercial antes de confirmar pagamento.")
|
|
return warnings
|
|
|
|
|
|
def decide_blif_next_action(e: OpportunityEvidence, profile: CompanyWorkflowProfile) -> OpportunityDecision:
|
|
"""BLIF operational rules isolated from DB and HTML.
|
|
|
|
Protected sequence for the normal profile:
|
|
information -> quote -> payment -> invoice -> production/shipping.
|
|
The after-delivery payment term relaxes payment as a shipping blocker but
|
|
keeps payment follow-up explicit after shipment/delivery.
|
|
"""
|
|
|
|
warnings = _base_warnings(e)
|
|
blocked_actions: list[WorkflowAction] = []
|
|
available_actions: list[WorkflowAction] = []
|
|
|
|
if e.is_terminal:
|
|
next_action = _action(profile, ACTION_NO_ACTION, target_url=f"/opportunities/{e.opportunity_id}" if e.opportunity_id else None)
|
|
return OpportunityDecision(
|
|
next_action=next_action,
|
|
reason="A oportunidade está concluída/fechada.",
|
|
warnings=warnings,
|
|
commercial_stage=COMMERCIAL_STAGE_WON,
|
|
financial_state=_financial_state(e),
|
|
physical_state=_physical_state(e),
|
|
profile_name=profile.name,
|
|
decision_version=profile.version,
|
|
)
|
|
|
|
if e.has_nif_conflict or e.has_fiscal_conflict:
|
|
blocked_actions.extend(_blocked(profile, code, "conflito fiscal/NIF") for code in SENSITIVE_DOCUMENT_ACTIONS)
|
|
next_action = _action(profile, ACTION_REVIEW, "Resolver conflito fiscal/NIF antes de avançar.", priority="alta", target_url=f"/opportunities/{e.opportunity_id}#cliente" if e.opportunity_id else None)
|
|
return OpportunityDecision(next_action, "Conflito fiscal/NIF bloqueia ações financeiras.", blocked_actions=blocked_actions, warnings=warnings, commercial_stage=COMMERCIAL_STAGE_REVIEW, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
|
|
if not e.has_fiscal_customer:
|
|
blocked_actions.extend(_blocked(profile, code, "cliente fiscal por associar") for code in SENSITIVE_DOCUMENT_ACTIONS)
|
|
next_action = _action(profile, ACTION_VALIDATE_FISCAL_CUSTOMER, "Associar/validar cliente fiscal antes de documentos oficiais.", priority="alta", target_url=f"/opportunities/{e.opportunity_id}#cliente" if e.opportunity_id else None)
|
|
return OpportunityDecision(next_action, "Cliente fiscal ainda não associado.", blocked_actions=blocked_actions, warnings=warnings, commercial_stage=COMMERCIAL_STAGE_REVIEW, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
|
|
if e.has_reconciliation_candidate:
|
|
next_action = _action(profile, ACTION_RECONCILE_DOCUMENTS, f"Confirmar evidência encontrada: {e.reconciliation_label or 'documento/candidato'}.", priority="alta", target_url="/reconciliation")
|
|
return OpportunityDecision(next_action, "Há evidência de reconciliação por validar.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_REVIEW, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
|
|
if not e.has_quote and not e.has_invoice:
|
|
if e.quote_sent:
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_RECONCILE_DOCUMENTS,
|
|
"O orçamento já foi enviado, mas falta associar o documento Jasmin e importar valor/linhas.",
|
|
force_label="Associar orçamento enviado",
|
|
priority="alta",
|
|
target_url=f"/opportunities/{e.opportunity_id}#documentos" if e.opportunity_id else "/reconciliation",
|
|
)
|
|
return OpportunityDecision(
|
|
next_action,
|
|
"Existe evidência de SEND_QUOTE concluído sem documento comercial associado; não criar um orçamento duplicado.",
|
|
warnings=warnings,
|
|
commercial_stage=COMMERCIAL_STAGE_REVIEW,
|
|
financial_state="no_document",
|
|
physical_state=_physical_state(e),
|
|
profile_name=profile.name,
|
|
decision_version=profile.version,
|
|
)
|
|
next_action = _action(profile, ACTION_CREATE_QUOTE, "Criar/enviar orçamento antes de pedir pagamento ou emitir fatura.", target_url=f"/opportunities/{e.opportunity_id}#documentos" if e.opportunity_id else None)
|
|
available_actions.append(next_action)
|
|
return OpportunityDecision(next_action, "Ainda não há orçamento/fatura associado.", available_actions=available_actions, warnings=warnings, commercial_stage=COMMERCIAL_STAGE_REVIEW, financial_state="no_document", physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
|
|
if e.payment_terms == PAYMENT_AFTER_DELIVERY:
|
|
if e.stage == "SHIPMENT_CREATED" and not e.payment_confirmed:
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_FOLLOW_UP_PAYMENT,
|
|
"Envio já criado/registado e pagamento pós-entrega ainda pendente. Acompanhar pagamento.",
|
|
priority="alta",
|
|
target_url=f"/opportunities/{e.opportunity_id}#operacao" if e.opportunity_id else None,
|
|
document_id=e.invoice_id or e.quote_id,
|
|
document_number=e.invoice_number or e.quote_number,
|
|
)
|
|
return OpportunityDecision(next_action, "Pagamento pós-entrega: envio criado sem pagamento confirmado; fazer follow-up de pagamento.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_IN_EXECUTION, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
if e.order_delivered or e.order_shipped:
|
|
if not e.has_invoice:
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_SEND_INVOICE,
|
|
f"Odoo/WH-OUT indica encomenda concluída. Emitir/enviar fatura com base em {e.quote_number or 'orçamento'} antes de acompanhar pagamento.",
|
|
label="Emitir/enviar fatura",
|
|
priority="alta",
|
|
target_url=f"/opportunities/{e.opportunity_id}#documentos" if e.opportunity_id else None,
|
|
document_id=e.quote_id,
|
|
document_number=e.quote_number,
|
|
)
|
|
return OpportunityDecision(next_action, "Pagamento pós-entrega: fatura deve existir e ser enviada antes do follow-up de pagamento ou fecho.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_IN_EXECUTION, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
if e.invoice_sent is not True:
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_SEND_INVOICE,
|
|
f"Fatura {e.invoice_number or ''} criada/associada. Enviar PDF ao cliente antes de acompanhar pagamento ou concluir.",
|
|
priority="alta",
|
|
target_url=f"/tasks/{e.pending_task_id}" if e.pending_task_id and e.pending_task_action_code == ACTION_SEND_INVOICE else (f"/opportunities/{e.opportunity_id}#documentos" if e.opportunity_id else None),
|
|
document_id=e.invoice_id,
|
|
document_number=e.invoice_number,
|
|
)
|
|
return OpportunityDecision(next_action, "Odoo/WH-OUT está concluído, mas ainda falta evidência de fatura enviada ao cliente.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_IN_EXECUTION, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
if not e.payment_confirmed:
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_FOLLOW_UP_PAYMENT,
|
|
"Encomenda concluída no Odoo e fatura enviada; acompanhar pagamento pós-entrega.",
|
|
priority="alta",
|
|
target_url=f"/tasks/{e.pending_task_id}" if e.pending_task_id and e.pending_task_action_code == ACTION_FOLLOW_UP_PAYMENT else (f"/opportunities/{e.opportunity_id}#operacao" if e.opportunity_id else None),
|
|
document_id=e.invoice_id,
|
|
document_number=e.invoice_number,
|
|
)
|
|
return OpportunityDecision(next_action, "Pagamento pós-entrega pendente depois de Odoo/WH-OUT concluído e fatura enviada.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_IN_EXECUTION, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_CLOSE_OPPORTUNITY,
|
|
f"Fatura {e.invoice_number or ''} enviada, pagamento confirmado e Odoo/WH-OUT concluído. Concluir a oportunidade.",
|
|
label="Concluir oportunidade",
|
|
priority="normal",
|
|
target_url=f"/opportunities/{e.opportunity_id}#operacao" if e.opportunity_id else None,
|
|
document_id=e.invoice_id,
|
|
document_number=e.invoice_number,
|
|
)
|
|
return OpportunityDecision(
|
|
next_action,
|
|
"Fatura enviada, pagamento confirmado e Odoo/WH-OUT concluído; por agora não criar follow-up de tracking/entrega.",
|
|
warnings=warnings,
|
|
commercial_stage=COMMERCIAL_STAGE_WON,
|
|
financial_state=_financial_state(e),
|
|
physical_state=_physical_state(e),
|
|
profile_name=profile.name,
|
|
decision_version=profile.version,
|
|
)
|
|
if not e.has_odoo_sale:
|
|
next_action = _action(profile, ACTION_PREPARE_ORDER, "Pagamento após entrega: criar/associar venda Odoo e avançar preparação sem exigir pagamento confirmado.", priority="alta", target_url=f"/opportunities/{e.opportunity_id}#odoo" if e.opportunity_id else None)
|
|
return OpportunityDecision(next_action, "Condição pós-entrega permite avançar Odoo/preparação sem pagamento prévio.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_IN_EXECUTION, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
if e.odoo_ready and not e.order_shipped:
|
|
if not e.has_invoice and e.has_fiscal_customer and not e.fiscal_data_complete:
|
|
blocked_actions.append(_blocked(profile, ACTION_SEND_INVOICE, "dados fiscais incompletos"))
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_VALIDATE_FISCAL_CUSTOMER,
|
|
"Encomenda pronta para entrega/levantamento, mas faltam dados fiscais antes de emitir a fatura.",
|
|
label="Completar dados fiscais",
|
|
priority="alta",
|
|
target_url=f"/opportunities/{e.opportunity_id}#cliente" if e.opportunity_id else None,
|
|
)
|
|
return OpportunityDecision(next_action, "Pagamento pós-entrega: a fatura deve ser emitida antes/no momento da entrega, mas a ficha fiscal está incompleta.", blocked_actions=blocked_actions, warnings=warnings, commercial_stage=COMMERCIAL_STAGE_REVIEW, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
if not e.has_invoice:
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_SEND_INVOICE,
|
|
f"Encomenda pronta para entrega/levantamento. Emitir/enviar fatura com prazo acordado com base em {e.quote_number or 'orçamento'}.",
|
|
priority="alta",
|
|
target_url=f"/opportunities/{e.opportunity_id}#documentos" if e.opportunity_id else None,
|
|
document_id=e.quote_id,
|
|
document_number=e.quote_number,
|
|
)
|
|
return OpportunityDecision(next_action, "Pagamento pós-entrega: faturar quando a encomenda está pronta para entrega/levantamento, antes de acompanhar pagamento.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_IN_EXECUTION, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
if e.invoice_sent is not True:
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_SEND_INVOICE,
|
|
f"Fatura {e.invoice_number or ''} criada/associada. Enviar PDF ao cliente para entrega/levantamento; o pagamento será acompanhado depois.",
|
|
priority="alta",
|
|
target_url=f"/tasks/{e.pending_task_id}" if e.pending_task_id and e.pending_task_action_code == ACTION_SEND_INVOICE else (f"/opportunities/{e.opportunity_id}#documentos" if e.opportunity_id else None),
|
|
document_id=e.invoice_id,
|
|
document_number=e.invoice_number,
|
|
)
|
|
return OpportunityDecision(next_action, "Pagamento pós-entrega: fatura existe, mas ainda falta envio ao cliente.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_IN_EXECUTION, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
next_action = _action(profile, ACTION_SHIP_ORDER, "Fatura pronta/enviada; avançar entrega/levantamento e acompanhar pagamento depois.", priority="alta", target_url=f"/opportunities/{e.opportunity_id}#odoo" if e.opportunity_id else None)
|
|
return OpportunityDecision(next_action, "Pagamento não bloqueia entrega porque a condição é pós-entrega.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_IN_EXECUTION, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
if e.odoo_in_production or e.has_odoo_sale:
|
|
next_action = _action(profile, ACTION_WAIT_PRODUCTION, "Pagamento após entrega: venda Odoo criada; aguardar WH/OUT ficar pronto/concluído antes de emitir fatura.", priority="normal", target_url=f"/opportunities/{e.opportunity_id}#odoo" if e.opportunity_id else None)
|
|
return OpportunityDecision(next_action, "Aguardar estado da encomenda/WH-OUT no Odoo; ordens de fabrico são apenas detalhe técnico.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_IN_EXECUTION, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
|
|
# Default/BLIF normal sequence: budget document, payment, invoice, then preparation/shipping.
|
|
if e.payment_terms in {PAYMENT_BEFORE_SHIPPING, "", "undefined", "agreement"} and e.has_quote and not e.payment_confirmed:
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_CONFIRM_PAYMENT,
|
|
f"Orçamento {e.quote_number or ''} associado. Confirmar pagamento antes de emitir fatura.",
|
|
priority="alta",
|
|
target_url=f"/opportunities/{e.opportunity_id}#operacao" if e.opportunity_id else None,
|
|
document_id=e.quote_id,
|
|
document_number=e.quote_number,
|
|
)
|
|
available_actions.append(next_action)
|
|
return OpportunityDecision(next_action, "Fluxo normal BLIF exige pagamento confirmado depois do orçamento e antes da fatura.", available_actions=available_actions, warnings=warnings, commercial_stage=COMMERCIAL_STAGE_WAITING_PAYMENT, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
|
|
if e.payment_confirmed and not e.has_invoice and e.has_fiscal_customer and not e.fiscal_data_complete:
|
|
blocked_actions.append(_blocked(profile, ACTION_SEND_INVOICE, "dados fiscais incompletos"))
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_VALIDATE_FISCAL_CUSTOMER,
|
|
"Pagamento confirmado, mas faltam dados fiscais obrigatórios antes de emitir/enviar a fatura.",
|
|
label="Completar dados fiscais",
|
|
priority="alta",
|
|
target_url=f"/opportunities/{e.opportunity_id}#cliente" if e.opportunity_id else None,
|
|
)
|
|
return OpportunityDecision(next_action, "Pagamento confirmado com dados fiscais incompletos; bloquear emissão de fatura até completar a ficha fiscal.", blocked_actions=blocked_actions, warnings=warnings, commercial_stage=COMMERCIAL_STAGE_REVIEW, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
|
|
if e.payment_confirmed and not e.has_invoice:
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_SEND_INVOICE,
|
|
f"Pagamento confirmado com base em {e.quote_number or 'orçamento'}. Emitir/enviar fatura de seguida.",
|
|
priority="alta",
|
|
target_url=f"/opportunities/{e.opportunity_id}#documentos" if e.opportunity_id else None,
|
|
document_id=e.quote_id,
|
|
document_number=e.quote_number,
|
|
)
|
|
return OpportunityDecision(next_action, "Pagamento confirmado e ainda não há fatura associada.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_PAYMENT_CONFIRMED, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
|
|
if e.has_invoice and not e.payment_confirmed and e.payment_terms != PAYMENT_AFTER_DELIVERY:
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_CONFIRM_PAYMENT,
|
|
f"Fatura {e.invoice_number or ''} associada; confirmar pagamento antes de envio/preparação final.",
|
|
priority="alta",
|
|
target_url=f"/opportunities/{e.opportunity_id}#operacao" if e.opportunity_id else None,
|
|
document_id=e.invoice_id,
|
|
document_number=e.invoice_number,
|
|
)
|
|
return OpportunityDecision(next_action, "Fatura existe mas pagamento ainda não está confirmado.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_WAITING_PAYMENT, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
|
|
# A fatura pode já existir/estar emitida no Jasmin mas ainda faltar
|
|
# enviá-la ao cliente. Isto é uma ação de comunicação/documento diferente
|
|
# de “criar fatura” e deve aparecer antes de aguardar produção ou criar
|
|
# envio sempre que não exista evidência local de envio do PDF ao cliente.
|
|
if e.has_invoice and e.payment_confirmed and e.invoice_sent is not True:
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_SEND_INVOICE,
|
|
f"Fatura {e.invoice_number or ''} criada/associada. Enviar PDF ao cliente; depois acompanhar produção/preparação.",
|
|
priority="alta",
|
|
target_url=f"/tasks/{e.pending_task_id}" if e.pending_task_id and e.pending_task_action_code == ACTION_SEND_INVOICE else (f"/opportunities/{e.opportunity_id}#documentos" if e.opportunity_id else None),
|
|
document_id=e.invoice_id,
|
|
document_number=e.invoice_number,
|
|
)
|
|
return OpportunityDecision(next_action, "Fatura existe, mas o envio ao cliente ainda não está confirmado.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_PAYMENT_CONFIRMED, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
|
|
if e.has_invoice and e.payment_confirmed and e.invoice_sent is True and (e.order_delivered or e.order_shipped):
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_CLOSE_OPPORTUNITY,
|
|
f"Fatura {e.invoice_number or ''} enviada, pagamento confirmado e Odoo/WH-OUT concluído. Concluir a oportunidade.",
|
|
label="Concluir oportunidade",
|
|
priority="normal",
|
|
target_url=f"/opportunities/{e.opportunity_id}#operacao" if e.opportunity_id else None,
|
|
document_id=e.invoice_id,
|
|
document_number=e.invoice_number,
|
|
)
|
|
return OpportunityDecision(
|
|
next_action,
|
|
"Fatura enviada, pagamento confirmado e Odoo/WH-OUT concluído; por agora não criar follow-up de tracking/entrega.",
|
|
warnings=warnings,
|
|
commercial_stage=COMMERCIAL_STAGE_WON,
|
|
financial_state=_financial_state(e),
|
|
physical_state=_physical_state(e),
|
|
profile_name=profile.name,
|
|
decision_version=profile.version,
|
|
)
|
|
|
|
# Shipped/WH-OUT closed cases must be evaluated before production/wait states.
|
|
# if e.has_invoice and e.payment_confirmed and e.order_shipped: Confirmar entrega/tracking
|
|
|
|
if e.has_invoice and e.payment_confirmed and e.odoo_in_production:
|
|
next_action = _action(profile, ACTION_WAIT_PRODUCTION, f"Fatura {e.invoice_number or ''} e pagamento confirmados; aguardar WH/OUT ficar pronto/concluído no Odoo.", priority="normal", target_url=f"/opportunities/{e.opportunity_id}#odoo" if e.opportunity_id else None, document_id=e.invoice_id, document_number=e.invoice_number)
|
|
return OpportunityDecision(next_action, "Aguardar estado da encomenda/WH-OUT no Odoo; ordens de fabrico são apenas detalhe técnico.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_IN_EXECUTION, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
|
|
if e.has_invoice and e.payment_confirmed and e.odoo_physical_ready and not e.odoo_physical_validated and not e.order_shipped:
|
|
next_action = _action(
|
|
profile,
|
|
ACTION_VALIDATE_PHYSICAL_ORDER,
|
|
"O picking está reservado no Odoo. Confirmar que a encomenda está fisicamente preparada antes de criar o envio.",
|
|
label="Validar encomenda física",
|
|
priority="alta",
|
|
target_url=f"/opportunities/{e.opportunity_id}#odoo" if e.opportunity_id else None,
|
|
)
|
|
return OpportunityDecision(next_action, "Odoo assigned confirma reserva de stock, não validação física da encomenda.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_IN_EXECUTION, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
|
|
if e.has_invoice and e.payment_confirmed and e.odoo_ready and not e.order_shipped:
|
|
next_action = _action(profile, ACTION_SHIP_ORDER, "Encomenda fisicamente validada; criar envio/tracking.", priority="alta", target_url=f"/opportunities/{e.opportunity_id}#odoo" if e.opportunity_id else None)
|
|
return OpportunityDecision(next_action, "Pagamento/fatura OK e preparação física validada; avançar para expedição.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_IN_EXECUTION, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
|
|
if e.has_invoice and e.payment_confirmed and not e.has_odoo_sale:
|
|
next_action = _action(profile, ACTION_PREPARE_ORDER, f"Fatura {e.invoice_number or ''} e pagamento confirmados. Criar/validar venda Odoo e preparação.", priority="alta", target_url=f"/opportunities/{e.opportunity_id}#odoo" if e.opportunity_id else None)
|
|
return OpportunityDecision(next_action, "Fatura e pagamento OK; falta validar execução/Odoo.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_IN_EXECUTION, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|
|
|
|
next_action = _action(profile, ACTION_FOLLOW_UP, "Rever tarefas, documentos e próximos contactos.", priority="baixa", target_url=f"/opportunities/{e.opportunity_id}" if e.opportunity_id else None)
|
|
return OpportunityDecision(next_action, "Sem regra específica aplicável; manter em acompanhamento.", warnings=warnings, commercial_stage=COMMERCIAL_STAGE_QUOTE_SENT, financial_state=_financial_state(e), physical_state=_physical_state(e), profile_name=profile.name, decision_version=profile.version)
|