Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
176
app/odoo_delivery_task_reconciliation.py
Normal file
176
app/odoo_delivery_task_reconciliation.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""Reconcile operator tasks when Odoo proves the outgoing delivery is done.
|
||||
|
||||
A completed WH/OUT is stronger evidence than pending ClientFlow tasks that ask
|
||||
an operator to validate the physical order or create the shipment. Those tasks
|
||||
must stop dominating the workbench once the external evidence is unequivocal.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from typing import Any, Mapping
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
def _json(value: Any) -> str:
|
||||
return json.dumps(value or {}, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
def _picking_names(evidence: Mapping[str, Any] | None) -> list[str]:
|
||||
result: list[str] = []
|
||||
for picking in (evidence or {}).get("pickings", []) or (evidence or {}).get("outgoing_pickings", []) or []:
|
||||
if not isinstance(picking, Mapping):
|
||||
continue
|
||||
if str(picking.get("state") or "").strip().lower() != "done":
|
||||
continue
|
||||
name = str(picking.get("name") or "").strip()
|
||||
if name and name not in result:
|
||||
result.append(name)
|
||||
return result
|
||||
|
||||
|
||||
def reconcile_odoo_delivery_done(
|
||||
conn: Any,
|
||||
opportunity_id: str,
|
||||
*,
|
||||
evidence: Mapping[str, Any] | None = None,
|
||||
actor: str = "odoo_sync",
|
||||
upsert_validation: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Persist Odoo-done evidence and auto-complete superseded pending tasks.
|
||||
|
||||
This deliberately does not call the normal task completion cascade: the
|
||||
Odoo delivery is already beyond both VALIDATE_PHYSICAL_ORDER and
|
||||
CREATE_SHIPMENT, so replaying those cascades would create fresh stale work.
|
||||
"""
|
||||
|
||||
evidence = dict(evidence or {})
|
||||
picking_names = _picking_names(evidence)
|
||||
sale = evidence.get("sale_order") if isinstance(evidence.get("sale_order"), Mapping) else {}
|
||||
external_id = str((sale or {}).get("id") or evidence.get("sale_order_id") or "").strip() or None
|
||||
external_name = str((sale or {}).get("name") or evidence.get("sale_order") or "").strip() or None
|
||||
|
||||
if upsert_validation:
|
||||
conn.execute(text("""
|
||||
INSERT INTO operation_links (
|
||||
opportunity_id, system, external_type, external_id, external_name,
|
||||
external_url, status, payload, last_synced_at, updated_at
|
||||
) VALUES (
|
||||
CAST(:opportunity_id AS UUID), 'odoo', 'physical_validation',
|
||||
:external_id, :external_name, NULL, 'validated', CAST(:payload AS JSONB), now(), now()
|
||||
)
|
||||
ON CONFLICT (opportunity_id, system, external_type)
|
||||
DO UPDATE SET
|
||||
external_id = COALESCE(EXCLUDED.external_id, operation_links.external_id),
|
||||
external_name = COALESCE(EXCLUDED.external_name, operation_links.external_name),
|
||||
status = 'validated',
|
||||
payload = operation_links.payload || EXCLUDED.payload,
|
||||
last_synced_at = now(),
|
||||
updated_at = now()
|
||||
"""), {
|
||||
"opportunity_id": opportunity_id,
|
||||
"external_id": external_id,
|
||||
"external_name": external_name or "WH/OUT concluído no Odoo",
|
||||
"payload": _json({
|
||||
**evidence,
|
||||
"validated_from_odoo_delivery_done": True,
|
||||
"validated_by": actor,
|
||||
"version": "v4928.1.5.132.4",
|
||||
}),
|
||||
})
|
||||
|
||||
rows = conn.execute(text("""
|
||||
SELECT id::text, action_code, status
|
||||
FROM tasks
|
||||
WHERE opportunity_id = CAST(:opportunity_id AS UUID)
|
||||
AND status = 'pending'
|
||||
AND action_code IN ('VALIDATE_PHYSICAL_ORDER', 'CREATE_SHIPMENT')
|
||||
ORDER BY created_at
|
||||
FOR UPDATE
|
||||
"""), {"opportunity_id": opportunity_id}).mappings().all()
|
||||
|
||||
completed: list[dict[str, str]] = []
|
||||
for row in rows:
|
||||
task_id = str(row.get("id") or "")
|
||||
action_code = str(row.get("action_code") or "")
|
||||
if not task_id:
|
||||
continue
|
||||
reason = (
|
||||
"Odoo WH/OUT concluído confirma a preparação física e expedição."
|
||||
if action_code == "VALIDATE_PHYSICAL_ORDER"
|
||||
else "Odoo WH/OUT concluído confirma que a expedição já foi executada."
|
||||
)
|
||||
metadata = {
|
||||
"auto_completed_from_odoo_delivery_done": True,
|
||||
"auto_completed_by": actor,
|
||||
"auto_completed_version": "v4928.1.5.132.4",
|
||||
"auto_completed_reason": "odoo_delivery_done_supersedes_pending_action",
|
||||
"odoo_done_pickings": picking_names,
|
||||
"previous_status": str(row.get("status") or "pending"),
|
||||
}
|
||||
updated = conn.execute(text("""
|
||||
UPDATE tasks
|
||||
SET status = 'done',
|
||||
action_required = FALSE,
|
||||
done_at = COALESCE(done_at, now()),
|
||||
done_by = COALESCE(done_by, :actor),
|
||||
note = CASE
|
||||
WHEN COALESCE(note, '') = '' THEN :reason
|
||||
ELSE note || E'\n\n' || :reason
|
||||
END,
|
||||
metadata = COALESCE(metadata, '{}'::jsonb) || CAST(:metadata AS JSONB),
|
||||
updated_at = now()
|
||||
WHERE id = CAST(:task_id AS UUID)
|
||||
AND status = 'pending'
|
||||
RETURNING id::text
|
||||
"""), {
|
||||
"task_id": task_id,
|
||||
"actor": actor,
|
||||
"reason": reason,
|
||||
"metadata": _json(metadata),
|
||||
}).scalar()
|
||||
if not updated:
|
||||
continue
|
||||
conn.execute(text("""
|
||||
INSERT INTO task_events (task_id, event_type, payload, created_by)
|
||||
VALUES (
|
||||
CAST(:task_id AS UUID), 'task_auto_completed',
|
||||
CAST(:payload AS JSONB), :actor
|
||||
)
|
||||
"""), {
|
||||
"task_id": task_id,
|
||||
"actor": actor,
|
||||
"payload": _json({
|
||||
"action_code": action_code,
|
||||
"reason": "odoo_delivery_done_supersedes_pending_action",
|
||||
"odoo_done_pickings": picking_names,
|
||||
"version": "v4928.1.5.132.4",
|
||||
}),
|
||||
})
|
||||
completed.append({"task_id": task_id, "action_code": action_code})
|
||||
|
||||
if completed:
|
||||
conn.execute(text("""
|
||||
INSERT INTO opportunity_events (
|
||||
id, opportunity_id, event_type, action_code, from_stage, to_stage,
|
||||
note, payload, created_by
|
||||
)
|
||||
SELECT
|
||||
CAST(:id AS UUID), id, 'odoo_delivery_done_tasks_reconciled',
|
||||
'CLOSE_OPPORTUNITY', stage, stage, :note, CAST(:payload AS JSONB), :actor
|
||||
FROM opportunities
|
||||
WHERE id = CAST(:opportunity_id AS UUID)
|
||||
"""), {
|
||||
"id": str(uuid.uuid4()),
|
||||
"opportunity_id": opportunity_id,
|
||||
"actor": actor,
|
||||
"note": "WH/OUT concluído no Odoo; tasks físicas/expedição obsoletas concluídas automaticamente.",
|
||||
"payload": _json({"completed_tasks": completed, "odoo_done_pickings": picking_names, "version": "v4928.1.5.132.4"}),
|
||||
})
|
||||
|
||||
return {
|
||||
"physical_validation": "validated" if upsert_validation else "preserved",
|
||||
"completed_tasks": completed,
|
||||
"odoo_done_pickings": picking_names,
|
||||
}
|
||||
Reference in New Issue
Block a user