177 lines
7.0 KiB
Python
177 lines
7.0 KiB
Python
"""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,
|
|
}
|