Files
clientflow_backend/scripts/repair_assigned_picking_shipment_tasks.py

125 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Repair premature shipment tasks created from Odoo picking state assigned.
Dry-run by default. The script only touches pending CREATE_SHIPMENT tasks when:
- the opportunity has an Odoo physical_status link with an outgoing picking assigned;
- no physical_validation=validated link exists;
- the picking is not done.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from sqlalchemy import text
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from app.db import engine
def _rows(limit: int) -> list[dict]:
with engine.begin() as conn:
rows = conn.execute(text("""
SELECT
t.id::text AS task_id,
o.id::text AS opportunity_id,
o.customer_name,
o.stage,
t.action_code,
t.metadata AS task_metadata,
ps.payload AS physical_payload,
COALESCE(pv.status, '') AS validation_status
FROM tasks t
JOIN opportunities o ON o.id = t.opportunity_id
JOIN operation_links ps
ON ps.opportunity_id = o.id
AND ps.system = 'odoo'
AND ps.external_type = 'physical_status'
LEFT JOIN operation_links pv
ON pv.opportunity_id = o.id
AND pv.system = 'odoo'
AND pv.external_type = 'physical_validation'
WHERE t.status = 'pending'
AND t.action_code = 'CREATE_SHIPMENT'
AND COALESCE(pv.status, '') <> 'validated'
AND EXISTS (
SELECT 1
FROM jsonb_array_elements(
COALESCE(ps.payload->'outgoing_pickings', ps.payload->'pickings', '[]'::jsonb)
) p
WHERE COALESCE(p->>'state', '') = 'assigned'
)
AND NOT EXISTS (
SELECT 1
FROM jsonb_array_elements(
COALESCE(ps.payload->'outgoing_pickings', ps.payload->'pickings', '[]'::jsonb)
) p
WHERE COALESCE(p->>'state', '') = 'done'
)
ORDER BY t.created_at
LIMIT :limit
"""), {"limit": limit}).mappings().all()
return [dict(r) for r in rows]
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument('--apply', action='store_true')
parser.add_argument('--limit', type=int, default=100)
args = parser.parse_args()
rows = _rows(args.limit)
print(f"Premature CREATE_SHIPMENT tasks: {len(rows)}")
for row in rows:
print(f"{row['stage']:18} | {row['customer_name']}")
if not args.apply:
print('Dry-run only. Use --apply after reviewing the tasks.')
return 0
if not rows:
return 0
with engine.begin() as conn:
for row in rows:
metadata = dict(row.get('task_metadata') or {})
metadata.update({
'corrected_by': 'migration_v4928_1_5_130',
'previous_action_code': 'CREATE_SHIPMENT',
'correction_reason': 'Odoo assigned means stock reserved; physical validation still required',
})
conn.execute(text("""
UPDATE tasks
SET action_code = 'VALIDATE_PHYSICAL_ORDER',
route = 'logistica',
action = 'Validar encomenda física',
note = 'O picking está reservado no Odoo. Confirmar produtos, quantidades, embalagem e preparação física antes de criar o envio.',
safe_to_post = FALSE,
metadata = CAST(:metadata AS JSONB),
updated_at = now()
WHERE id = CAST(:task_id AS UUID)
AND status = 'pending'
AND action_code = 'CREATE_SHIPMENT'
"""), {'task_id': row['task_id'], 'metadata': json.dumps(metadata, ensure_ascii=False)})
conn.execute(text("""
UPDATE opportunities
SET stage = CASE WHEN stage = 'READY_TO_SHIP' THEN 'ORDER_PREPARATION' ELSE stage END,
last_action_code = 'VALIDATE_PHYSICAL_ORDER',
updated_at = now(),
metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object(
'assigned_picking_requires_physical_validation', TRUE,
'assigned_picking_gate_version', 'v4928.1.5.130'
)
WHERE id = CAST(:opportunity_id AS UUID)
"""), {'opportunity_id': row['opportunity_id']})
print(f"Tasks converted: {len(rows)}")
return 0
if __name__ == '__main__':
raise SystemExit(main())