Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
108
scripts/repair_v132_2_post_review_task_reactivation.py
Executable file
108
scripts/repair_v132_2_post_review_task_reactivation.py
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Guarded repair for a validated reconstructed review whose blocked task stayed terminal.
|
||||
|
||||
Dry-run by default. It only reactivates a task when:
|
||||
- the opportunity exists and its explicit reconstructed review is validated;
|
||||
- the central decision still matches the blocked action;
|
||||
- no pending task for that action exists;
|
||||
- a skipped/ignored materialized task for that action exists;
|
||||
- for physical validation, no validation link already exists.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db import engine
|
||||
from app.opportunity_action_task_materializer import ensure_pending_task_for_next_action
|
||||
from app.opportunity_next_action_service import get_opportunity_next_action
|
||||
from app.work_center_action_policy import canonical_action_code, reconstructed_review_status
|
||||
|
||||
|
||||
def inspect(opportunity_id: str) -> dict:
|
||||
with engine.begin() as conn:
|
||||
row = conn.execute(text("""
|
||||
SELECT id::text, title, stage, metadata,
|
||||
metadata #>> '{reconstructed_review,blocked_action_code}' AS blocked_action
|
||||
FROM opportunities
|
||||
WHERE id = CAST(:id AS UUID)
|
||||
"""), {"id": opportunity_id}).mappings().first()
|
||||
if not row:
|
||||
raise SystemExit("Oportunidade não encontrada.")
|
||||
tasks = conn.execute(text("""
|
||||
SELECT id::text, action_code, status, idempotency_key, metadata
|
||||
FROM tasks
|
||||
WHERE opportunity_id = CAST(:id AS UUID)
|
||||
AND action_code IN ('VALIDATE_PHYSICAL_ORDER','CREATE_SHIPMENT','SEND_INVOICE','PREPARE_ORDER')
|
||||
ORDER BY created_at DESC
|
||||
"""), {"id": opportunity_id}).mappings().all()
|
||||
physical = conn.execute(text("""
|
||||
SELECT id::text FROM operation_links
|
||||
WHERE opportunity_id = CAST(:id AS UUID)
|
||||
AND system='odoo' AND external_type='physical_validation'
|
||||
AND status='validated'
|
||||
LIMIT 1
|
||||
"""), {"id": opportunity_id}).scalar()
|
||||
decision = get_opportunity_next_action(opportunity_id)
|
||||
return {"opportunity": dict(row), "tasks": [dict(t) for t in tasks], "physical_validated": bool(physical), "decision": decision}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--opportunity-id", required=True)
|
||||
ap.add_argument("--apply", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
data = inspect(args.opportunity_id)
|
||||
opp = data["opportunity"]
|
||||
review = reconstructed_review_status(opp.get("metadata"))
|
||||
blocked = canonical_action_code(opp.get("blocked_action"))
|
||||
decision_code = canonical_action_code(data["decision"].get("action_code"))
|
||||
pending = [t for t in data["tasks"] if canonical_action_code(t.get("action_code")) == decision_code and str(t.get("status")) == "pending"]
|
||||
terminal = [t for t in data["tasks"] if canonical_action_code(t.get("action_code")) == decision_code and str(t.get("status")) in {"skipped", "ignored"}]
|
||||
|
||||
print(f"Opportunity: {opp.get('title')} ({opp.get('stage')})")
|
||||
print(f"Review: {review}")
|
||||
print(f"Blocked action: {blocked or '-'}")
|
||||
print(f"Central decision: {decision_code}")
|
||||
print(f"Pending same action: {len(pending)}")
|
||||
print(f"Terminal same action: {len(terminal)}")
|
||||
|
||||
if review != "validated":
|
||||
raise SystemExit("ABORTADO: revisão não está validated.")
|
||||
if not blocked or decision_code != blocked:
|
||||
raise SystemExit("ABORTADO: decisão central não coincide com a ação desbloqueada.")
|
||||
if pending:
|
||||
print("OK: já existe task pendente; nenhuma alteração.")
|
||||
return 0
|
||||
if not terminal:
|
||||
raise SystemExit("ABORTADO: não existe task materializada terminal para reativar.")
|
||||
if decision_code == "VALIDATE_PHYSICAL_ORDER" and data["physical_validated"]:
|
||||
raise SystemExit("ABORTADO: já existe validação física confirmada.")
|
||||
|
||||
print(f"SAFE: reativar {terminal[0]['id']} ({terminal[0]['status']})")
|
||||
if not args.apply:
|
||||
print("Dry-run only. Use --apply.")
|
||||
return 0
|
||||
|
||||
result = ensure_pending_task_for_next_action(
|
||||
args.opportunity_id,
|
||||
data["decision"],
|
||||
source="manual_v132_2_reactivation_repair",
|
||||
actor="operator",
|
||||
reactivate_skipped=True,
|
||||
)
|
||||
print("Resultado:", result)
|
||||
if not result.get("reactivated") and result.get("reason") != "pending_exists":
|
||||
return 2
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user