Implement document reconciliation v2

This commit is contained in:
plx
2026-08-05 23:55:33 +00:00
parent 4a60607503
commit 29382ec6c1
25 changed files with 2082 additions and 343 deletions

View File

@@ -665,8 +665,8 @@ def operation_cockpit_html(opportunity_id: str, opportunity: dict, snapshot: dic
and str(doc.get("status") or "").lower() not in {"cancelled", "failed", "rejected"}
and str(doc.get("role") or "current") not in {"superseded"}
]
invoice_document = next((doc for doc in invoice_candidates if bool(doc.get("is_primary", False))), invoice_candidates[0] if invoice_candidates else None)
quotation_document = next((doc for doc in quotation_candidates if bool(doc.get("is_primary", False))), quotation_candidates[0] if quotation_candidates else None)
invoice_document = next((doc for doc in invoice_candidates if doc.get("relationship") == "PRIMARY"), None)
quotation_document = next((doc for doc in quotation_candidates if doc.get("relationship") == "PRIMARY"), None)
except Exception:
invoice_document = None
quotation_document = None
@@ -1335,23 +1335,30 @@ def jasmin_documents_html(opportunity_id: str, *, notice: str = "", error_notice
number = commercial_document_display_number(doc, fallback="número por atualizar")
amount = doc.get("total_amount") if doc.get("total_amount") is not None else doc.get("amount")
kind = kind_labels.get(str(doc.get("document_kind") or ""), doc.get("document_kind") or "Documento")
doc_id = str(doc.get("id") or "")
role = str(doc.get("role") or "current")
is_primary = bool(doc.get("is_primary"))
make_primary_action = "" if (role in {"current", "accepted"} and is_primary) else f'<form method="post" action="/commercial-documents/{esc(doc_id)}/role" hx-post="/commercial-documents/{esc(doc_id)}/role" hx-target="#jasmin-documents-panel" hx-swap="outerHTML"><input type="hidden" name="opportunity_id" value="{esc(opportunity_id)}"><input type="hidden" name="role" value="current"><input type="hidden" name="make_primary" value="1"><button class="btn btn-sm btn-outline-success" type="submit">Definir principal</button></form>'
historical_action = "" if role == "historical" else f'<form method="post" action="/commercial-documents/{esc(doc_id)}/role" hx-post="/commercial-documents/{esc(doc_id)}/role" hx-target="#jasmin-documents-panel" hx-swap="outerHTML"><input type="hidden" name="opportunity_id" value="{esc(opportunity_id)}"><input type="hidden" name="role" value="historical"><input type="hidden" name="make_primary" value="0"><button class="btn btn-sm btn-outline-secondary" type="submit">Histórico</button></form>'
# Every document action targets commercial_documents.id. link_id is
# audit provenance only and must never leak into document routes.
doc_id = str(doc.get("document_id") or doc.get("id") or "")
relationship = str(doc.get("relationship") or "").upper()
role = str(doc.get("role") or relationship.lower() or "review_required")
is_primary = relationship == "PRIMARY"
make_primary_action = "" if relationship == "PRIMARY" else f'<form method="post" action="/opportunities/{esc(opportunity_id)}/documents/{esc(doc_id)}/relationship" hx-post="/opportunities/{esc(opportunity_id)}/documents/{esc(doc_id)}/relationship" hx-target="#jasmin-documents-panel" hx-swap="outerHTML"><input type="hidden" name="relationship" value="PRIMARY"><input class="form-control form-control-sm" name="reason" required placeholder="Motivo obrigatório"><button class="btn btn-sm btn-outline-success" type="submit">Tornar principal</button></form>'
historical_action = "" if relationship == "HISTORICAL" else f'<form method="post" action="/opportunities/{esc(opportunity_id)}/documents/{esc(doc_id)}/relationship" hx-post="/opportunities/{esc(opportunity_id)}/documents/{esc(doc_id)}/relationship" hx-target="#jasmin-documents-panel" hx-swap="outerHTML"><input type="hidden" name="relationship" value="HISTORICAL"><input type="hidden" name="reason" value="Classificado como histórico pelo operador"><button class="btn btn-sm btn-outline-secondary" type="submit">Marcar histórico</button></form>'
secondary_action = "" if relationship == "SECONDARY" else f'<form method="post" action="/opportunities/{esc(opportunity_id)}/documents/{esc(doc_id)}/relationship" hx-post="/opportunities/{esc(opportunity_id)}/documents/{esc(doc_id)}/relationship" hx-target="#jasmin-documents-panel" hx-swap="outerHTML"><input type="hidden" name="relationship" value="SECONDARY"><input type="hidden" name="reason" value="Classificado como secundário pelo operador"><button class="btn btn-sm btn-outline-secondary" type="submit">Marcar secundário</button></form>'
unlink_action = f'<form method="post" action="/commercial-documents/{esc(doc_id)}/unlink-from-opportunity" hx-post="/commercial-documents/{esc(doc_id)}/unlink-from-opportunity" hx-target="#jasmin-documents-panel" hx-swap="outerHTML" hx-confirm="Desassociar apenas este documento desta oportunidade? Não apaga nada no Jasmin; remove só a ligação local e as linhas importadas deste documento."><input type="hidden" name="opportunity_id" value="{esc(opportunity_id)}"><input type="hidden" name="remove_imported_lines" value="1"><input type="hidden" name="note" value="Documento pertence a outra compra/processo; desassociado manualmente."><button class="btn btn-sm btn-outline-danger" type="submit">Desassociar este</button></form>'
refresh_action = f'<form method="post" action="/commercial-documents/{esc(doc_id)}/refresh" hx-post="/commercial-documents/{esc(doc_id)}/refresh" hx-target="#jasmin-documents-panel" hx-swap="outerHTML"><input type="hidden" name="opportunity_id" value="{esc(opportunity_id)}"><button class="btn btn-sm btn-outline-secondary" type="submit">Atualizar nº</button></form>'
pdf_action = f'<a class="btn btn-sm btn-outline-primary" href="/commercial-documents/{esc(doc_id)}/pdf" target="_blank" rel="noopener">PDF</a>'
actions = f'<div class="d-flex flex-wrap gap-1 justify-content-end">{make_primary_action}{historical_action}{unlink_action}{refresh_action}{pdf_action}</div>'
history_action = f'<a class="btn btn-sm btn-outline-secondary" hx-get="/opportunities/{esc(opportunity_id)}/documents/{esc(doc_id)}/history" hx-target="#document-history-panel" hx-swap="outerHTML">Ver histórico</a>'
actions = f'<div class="d-flex flex-wrap gap-1 justify-content-end">{make_primary_action}{secondary_action}{historical_action}{unlink_action}{history_action}{refresh_action}{pdf_action}</div>'
role_label = {
"current": "Atual",
"accepted": "Aceite",
"historical": "Histórico",
"cancelled": "Cancelado",
"related": "Relacionado",
}.get(str(doc.get("role") or "current"), str(doc.get("role") or "current"))
role_class = "text-bg-primary" if str(doc.get("role") or "current") in {"current", "accepted"} and doc.get("is_primary") else "text-bg-light"
"primary": "PRIMARY", "secondary": "SECONDARY", "review_required": "REVIEW_REQUIRED",
"ignored": "IGNORED", "removed": "REMOVED",
}.get(role, relationship or role)
role_class = "text-bg-danger" if relationship == "REVIEW_REQUIRED" else ("text-bg-primary" if relationship == "PRIMARY" else "text-bg-light")
external_id_html = ""
if not doc.get("document_number") and doc.get("external_id") and not is_uuid_text(doc.get("external_id")):
external_id_html = f"<div class='small text-secondary text-break'>ref. externa {esc(doc.get('external_id') or '')}</div>"
@@ -1361,7 +1368,7 @@ def jasmin_documents_html(opportunity_id: str, *, notice: str = "", error_notice
"<tr>"
f"<td><strong>{esc(kind)}</strong><div class='small text-secondary'>v{esc(doc.get('version_number') or '')}</div><span class='badge {role_class}'>{esc(role_label)}</span></td>"
f"<td><code>{esc(number)}</code>{external_id_html}</td>"
f"<td>{operation_status_badge(str(doc.get('status') or 'created'))}</td>"
f"<td>{operation_status_badge(str(doc.get('status') or 'created'))}<div class='small'>Relação: {esc(relationship or role_label)}</div><div class='small text-secondary'>{esc(doc.get('decision_reason') or '')} · {esc(doc.get('decided_by') or '')}</div></td>"
f"<td>{money_html(amount or 0)}</td>"
f"<td class='small text-secondary'>{esc(fmt_dt(doc.get('created_at')))}</td>"
f"<td class='text-end'>{actions}</td>"