125 lines
4.8 KiB
Python
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())
|