Import ClientFlow production v4928.1.5.132.4

This commit is contained in:
plx
2026-07-29 13:11:01 +00:00
parent 6445044ac6
commit 261d342057
405 changed files with 48373 additions and 1401 deletions

View File

@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Repair stale physical/shipment tasks when Odoo WH/OUT is already done.
Dry-run by default. The repair is deliberately narrow: it requires explicit
Odoo delivery-done evidence for the selected opportunity before changing tasks
or the operational stage.
"""
from __future__ import annotations
import argparse
import json
import uuid
from typing import Any
from sqlalchemy import text
from app.db import engine
from app.odoo_delivery_task_reconciliation import reconcile_odoo_delivery_done
def _json(value: Any) -> str:
return json.dumps(value or {}, ensure_ascii=False, default=str)
def _payload(value: Any) -> dict[str, Any]:
if isinstance(value, dict):
return value
if isinstance(value, str) and value.strip():
try:
parsed = json.loads(value)
return parsed if isinstance(parsed, dict) else {}
except Exception:
return {}
return {}
def _done_evidence(status: str, payload: dict[str, Any]) -> bool:
if str(status or "").lower() in {"shipped", "done", "delivered"}:
return True
if bool(payload.get("delivery_done")):
return True
pickings = payload.get("pickings") or payload.get("outgoing_pickings") or []
outgoing = [p for p in pickings if isinstance(p, dict)]
return bool(outgoing) and all(str(p.get("state") or "").lower() == "done" for p in outgoing)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--opportunity-id", required=True)
parser.add_argument("--apply", action="store_true")
args = parser.parse_args()
with engine.begin() as conn:
opportunity = conn.execute(text("""
SELECT id::text, title, stage, status
FROM opportunities
WHERE id = CAST(:opportunity_id AS UUID)
FOR UPDATE
"""), {"opportunity_id": args.opportunity_id}).mappings().first()
if not opportunity:
raise SystemExit("Oportunidade não encontrada.")
physical = conn.execute(text("""
SELECT status, payload
FROM operation_links
WHERE opportunity_id = CAST(:opportunity_id AS UUID)
AND system = 'odoo'
AND external_type = 'physical_status'
LIMIT 1
"""), {"opportunity_id": args.opportunity_id}).mappings().first() or {}
payload = _payload(physical.get("payload"))
tasks = 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
"""), {"opportunity_id": args.opportunity_id}).mappings().all()
print(f"Opportunity: {opportunity['title']} ({opportunity['stage']})")
print(f"Odoo physical status: {physical.get('status')}")
print(f"Delivery done evidence: {_done_evidence(str(physical.get('status') or ''), payload)}")
print(f"Pending stale actions: {[row['action_code'] for row in tasks]}")
if not _done_evidence(str(physical.get("status") or ""), payload):
raise SystemExit("ABORTADO: não existe evidência inequívoca de WH/OUT concluído.")
if not tasks and str(opportunity.get("stage") or "") == "SHIPPED":
print("Sem alterações necessárias.")
return 0
if not args.apply:
print("SAFE: concluir tasks físicas/expedição pendentes e alinhar fase para SHIPPED.")
print("Dry-run only. Use --apply.")
return 0
result = reconcile_odoo_delivery_done(
conn,
args.opportunity_id,
evidence=payload,
actor="manual_v132_4_odoo_done_repair",
upsert_validation=True,
)
old_stage = str(opportunity.get("stage") or "")
conn.execute(text("""
UPDATE opportunities
SET stage = 'SHIPPED',
status = 'open',
last_action_code = 'CLOSE_OPPORTUNITY',
metadata = COALESCE(metadata, '{}'::jsonb) || CAST(:metadata AS JSONB),
updated_at = now()
WHERE id = CAST(:opportunity_id AS UUID)
AND status = 'open'
AND stage IN (
'ODOO_ORDER_CREATED', 'IN_PRODUCTION', 'ORDER_PREPARATION',
'READY_TO_SHIP', 'SHIPMENT_CREATED', 'SHIPPED'
)
"""), {
"opportunity_id": args.opportunity_id,
"metadata": _json({
"odoo_done_stage_aligned": True,
"odoo_done_stage_from": old_stage,
"odoo_done_stage_to": "SHIPPED",
"odoo_done_stage_version": "v4928.1.5.132.4",
}),
})
conn.execute(text("""
INSERT INTO opportunity_events (
id, opportunity_id, event_type, action_code, from_stage, to_stage,
note, payload, created_by
) VALUES (
CAST(:id AS UUID), CAST(:opportunity_id AS UUID),
'v132_4_odoo_done_repair', 'CLOSE_OPPORTUNITY',
:from_stage, 'SHIPPED', :note, CAST(:payload AS JSONB), :created_by
)
"""), {
"id": str(uuid.uuid4()),
"opportunity_id": args.opportunity_id,
"from_stage": old_stage,
"note": "WH/OUT já concluído no Odoo; fase e tasks operacionais alinhadas.",
"payload": _json(result),
"created_by": "manual_v132_4_odoo_done_repair",
})
print("Resultado:", result)
print("Fase: SHIPPED")
print("Próxima ação esperada: CLOSE_OPPORTUNITY")
return 0
if __name__ == "__main__":
raise SystemExit(main())