Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
@@ -614,11 +614,11 @@ def _odoo_derive_fulfilment(record: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if delivery_done:
|
||||
physical_status = "shipped"
|
||||
label = "Entrega concluída no Odoo"
|
||||
stage = "SHIPMENT_CREATED"
|
||||
stage = "SHIPPED"
|
||||
elif delivery_ready:
|
||||
physical_status = "ready_to_ship"
|
||||
label = "Pronta para despacho"
|
||||
stage = "READY_TO_SHIP"
|
||||
physical_status = "picking_assigned"
|
||||
label = "Picking reservado — validação física pendente"
|
||||
stage = "ORDER_PREPARATION"
|
||||
elif production_active:
|
||||
physical_status = "in_production"
|
||||
label = "Em produção/preparação"
|
||||
@@ -670,6 +670,86 @@ def _odoo_candidate_from_record(record: Dict[str, Any]) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _existing_odoo_sale_links(external_id: Any, order_name: Any) -> List[Dict[str, Any]]:
|
||||
"""Return exact open opportunity links for one Odoo sale order.
|
||||
|
||||
Matching is deliberately strict: numeric Odoo id or exact sale name. Name,
|
||||
customer and amount suggestions belong to the operator review path and must
|
||||
never auto-resolve a reconciliation item.
|
||||
"""
|
||||
external_id = _clean(external_id)
|
||||
order_name = _clean(order_name)
|
||||
if not external_id and not order_name:
|
||||
return []
|
||||
try:
|
||||
begin = engine.begin
|
||||
except Exception:
|
||||
return []
|
||||
try:
|
||||
with begin() as conn:
|
||||
rows = conn.execute(text("""
|
||||
SELECT DISTINCT
|
||||
ol.opportunity_id::text AS opportunity_id,
|
||||
o.title,
|
||||
o.stage,
|
||||
o.status,
|
||||
ol.external_id,
|
||||
ol.external_name
|
||||
FROM operation_links ol
|
||||
JOIN opportunities o ON o.id = ol.opportunity_id
|
||||
WHERE ol.system = 'odoo'
|
||||
AND ol.external_type = 'sale_order'
|
||||
AND o.status = 'open'
|
||||
AND (
|
||||
(NULLIF(:external_id, '') IS NOT NULL AND ol.external_id = :external_id)
|
||||
OR (NULLIF(:order_name, '') IS NOT NULL AND UPPER(COALESCE(ol.external_name, '')) = UPPER(:order_name))
|
||||
)
|
||||
ORDER BY ol.opportunity_id
|
||||
"""), {"external_id": external_id, "order_name": order_name}).mappings().all()
|
||||
except Exception:
|
||||
# Reconciliation sync must remain conservative when local schema access
|
||||
# is unavailable: keep staging the candidate rather than auto-resolving.
|
||||
return []
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
def _resolve_existing_odoo_reconciliation_item(
|
||||
*,
|
||||
external_id: Any,
|
||||
order_name: Any,
|
||||
opportunity_id: str,
|
||||
) -> int:
|
||||
"""Close stale open candidates without changing opportunity stage or tasks."""
|
||||
external_id = _clean(external_id)
|
||||
order_name = _clean(order_name)
|
||||
with engine.begin() as conn:
|
||||
result = conn.execute(text("""
|
||||
UPDATE reconciliation_items ri
|
||||
SET opportunity_id = CAST(:opportunity_id AS UUID),
|
||||
status = 'linked',
|
||||
resolution_note = 'Resolvido automaticamente: venda Odoo já ligada à oportunidade',
|
||||
resolved_at = now(),
|
||||
updated_at = now(),
|
||||
payload = COALESCE(ri.payload, '{}'::jsonb) || jsonb_build_object(
|
||||
'resolved_as_existing_operation_link', TRUE,
|
||||
'resolved_by', 'odoo_reconciliation_sync_v129',
|
||||
'resolved_sale_order', COALESCE(NULLIF(:order_name, ''), NULLIF(:external_id, ''))
|
||||
)
|
||||
WHERE ri.source_system = 'odoo'
|
||||
AND ri.external_type = 'odoo_sale_order'
|
||||
AND ri.status IN ('open', 'needs_review', 'conflict')
|
||||
AND (
|
||||
(NULLIF(:external_id, '') IS NOT NULL AND ri.external_id = :external_id)
|
||||
OR (NULLIF(:order_name, '') IS NOT NULL AND UPPER(COALESCE(ri.document_number, '')) = UPPER(:order_name))
|
||||
)
|
||||
"""), {
|
||||
"external_id": external_id,
|
||||
"order_name": order_name,
|
||||
"opportunity_id": opportunity_id,
|
||||
})
|
||||
return int(result.rowcount or 0)
|
||||
|
||||
|
||||
def sync_odoo_reconciliation_candidates(*, limit: int = 100, days: int = 3) -> Dict[str, Any]:
|
||||
"""Fetch recent Odoo sale orders and stage unlinked candidates."""
|
||||
if not bool(settings.odoo_enabled):
|
||||
@@ -728,6 +808,9 @@ def sync_odoo_reconciliation_candidates(*, limit: int = 100, days: int = 3) -> D
|
||||
|
||||
seen = 0
|
||||
created = 0
|
||||
already_linked = 0
|
||||
resolved_existing = 0
|
||||
link_conflicts = 0
|
||||
for record in records:
|
||||
partner = record.get("partner_id")
|
||||
if isinstance(partner, (list, tuple)) and partner:
|
||||
@@ -750,9 +833,40 @@ def sync_odoo_reconciliation_candidates(*, limit: int = 100, days: int = 3) -> D
|
||||
record["fulfilment"] = _odoo_derive_fulfilment(record)
|
||||
seen += 1
|
||||
candidate = _odoo_candidate_from_record(record)
|
||||
existing_links = _existing_odoo_sale_links(candidate.get("external_id"), candidate.get("document_number"))
|
||||
if len(existing_links) == 1:
|
||||
already_linked += 1
|
||||
resolved_existing += _resolve_existing_odoo_reconciliation_item(
|
||||
external_id=candidate.get("external_id"),
|
||||
order_name=candidate.get("document_number"),
|
||||
opportunity_id=existing_links[0]["opportunity_id"],
|
||||
)
|
||||
continue
|
||||
if len(existing_links) > 1:
|
||||
link_conflicts += 1
|
||||
candidate["status"] = "conflict"
|
||||
candidate["priority"] = "alta"
|
||||
candidate["description"] = (
|
||||
"A venda Odoo aparece ligada a mais de uma oportunidade. "
|
||||
"Requer correção manual das ligações antes de reconciliar."
|
||||
)
|
||||
candidate["payload"] = {
|
||||
**(candidate.get("payload") or {}),
|
||||
"existing_operation_link_conflict": existing_links,
|
||||
}
|
||||
upsert_reconciliation_item(**candidate)
|
||||
created += 1
|
||||
return {"source": "odoo", "enabled": True, "seen": seen, "created_or_updated": created, "days": max(int(days), 1), "since": since}
|
||||
return {
|
||||
"source": "odoo",
|
||||
"enabled": True,
|
||||
"seen": seen,
|
||||
"created_or_updated": created,
|
||||
"already_linked": already_linked,
|
||||
"resolved_existing": resolved_existing,
|
||||
"link_conflicts": link_conflicts,
|
||||
"days": max(int(days), 1),
|
||||
"since": since,
|
||||
}
|
||||
|
||||
|
||||
def _packlink_candidate_from_record(record: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
Reference in New Issue
Block a user