#!/usr/bin/env python3 """Administración de casos raíz v0.3.10.""" from __future__ import annotations import argparse import json import os from typing import Any import psycopg2 from psycopg2.extras import Json, RealDictCursor CLASSIFICATIONS = ( "ODOO_DOUBLE_START_ONE_VALID", "ODOO_DOUBLE_START_BOTH_INVALID", "ODOO_CONSECUTIVE_ORDERS_NOT_CLOSED", "AUTHORIZED_LEGACY_PRODUCTION", "TEST_OR_SETUP", "RESIDUAL_MATERIAL", "ODOO_START_OMITTED", "DATA_ISSUE", "NOT_RELEVANT", "OTHER", ) def env_first(*names: str, default: str | None = None) -> str | None: for name in names: value = os.getenv(name) if value not in (None, ""): return value return default def connect(): database = env_first("PGDATABASE", "POSTGRES_DB") user = env_first("PGUSER", "POSTGRES_USER") password = env_first("PGPASSWORD", "POSTGRES_PASSWORD") missing = [ name for name, value in ( ("PGDATABASE", database), ("PGUSER", user), ("PGPASSWORD", password), ) if not value ] if missing: raise RuntimeError( "Faltan variables: " + ", ".join(missing) ) return psycopg2.connect( host=env_first( "PGHOST", default="mv_ucepsa_postgres_hot", ), port=int( env_first("PGPORT", default="5432") or "5432" ), dbname=database, user=user, password=password, connect_timeout=10, application_name="context_review_case_admin_v0310", ) def print_json(value: Any) -> None: print( json.dumps( value, ensure_ascii=False, indent=2, default=str, ) ) def list_cases(limit: int) -> None: with connect() as connection: with connection.cursor( cursor_factory=RealDictCursor, ) as cursor: cursor.execute( """ SELECT backlog_rank, case_id, case_status, max_severity, first_seen_at, last_seen_at, case_duration_min, machine_id, case_title, production_orders, odoo_workorder_ids, linked_evidence_count, review_owner, review_status, question_text, recommended_action FROM mv_reports_ucepsa_prod .li_context_review_case_backlog_v1 ORDER BY backlog_rank LIMIT %s """, (limit,), ) print_json( [dict(row) for row in cursor.fetchall()] ) def show_case(case_id: int) -> None: with connect() as connection: with connection.cursor( cursor_factory=RealDictCursor, ) as cursor: cursor.execute( """ SELECT * FROM mv_reports_ucepsa_prod .li_context_review_case_history_v1 WHERE case_id = %s """, (case_id,), ) case = cursor.fetchone() if not case: raise RuntimeError( f"No existe el caso {case_id}" ) cursor.execute( """ SELECT * FROM mv_reports_ucepsa_prod .li_context_review_case_evidence_v1 WHERE case_id = %s ORDER BY episode_id """, (case_id,), ) evidence = [ dict(row) for row in cursor.fetchall() ] cursor.execute( """ SELECT * FROM mv_loss_intelligence .context_review_case_reviews WHERE case_id = %s ORDER BY reviewed_at, review_id """, (case_id,), ) reviews = [ dict(row) for row in cursor.fetchall() ] print_json({ "case": dict(case), "evidence": evidence, "reviews": reviews, }) def episode_classification( root_classification: str, ) -> str: mapping = { "ODOO_DOUBLE_START_ONE_VALID": "DATA_ISSUE", "ODOO_DOUBLE_START_BOTH_INVALID": "DATA_ISSUE", "ODOO_CONSECUTIVE_ORDERS_NOT_CLOSED": "DATA_ISSUE", "AUTHORIZED_LEGACY_PRODUCTION": "AUTHORIZED_LEGACY_PRODUCTION", "TEST_OR_SETUP": "TEST_OR_SETUP", "RESIDUAL_MATERIAL": "RESIDUAL_MATERIAL", "ODOO_START_OMITTED": "ODOO_START_OMITTED", "DATA_ISSUE": "DATA_ISSUE", "NOT_RELEVANT": "NOT_RELEVANT", "OTHER": "OTHER", } return mapping[root_classification] def review_case( case_id: int, classification: str, reviewed_by: str, notes: str | None, selected_order_ref: str | None, selected_workorder_id: int | None, ) -> None: case_status = ( "DISMISSED" if classification == "NOT_RELEVANT" else "REVIEWED" ) episode_status = ( "DISMISSED" if classification == "NOT_RELEVANT" else "REVIEWED" ) episode_class = episode_classification( classification ) with connect() as connection: with connection.cursor( cursor_factory=RealDictCursor, ) as cursor: cursor.execute( """ SELECT * FROM mv_loss_intelligence .context_review_cases WHERE case_id = %s FOR UPDATE """, (case_id,), ) previous = cursor.fetchone() if not previous: raise RuntimeError( f"No existe el caso {case_id}" ) cursor.execute( """ INSERT INTO mv_loss_intelligence .context_review_case_reviews ( case_id, previous_values, new_review_status, resolution_classification, selected_order_ref, selected_workorder_id, reviewed_by, notes, evidence_json ) VALUES ( %s, %s, %s, %s, %s, %s, %s, %s, %s ) RETURNING * """, ( case_id, Json(dict(previous)), case_status, classification, selected_order_ref, selected_workorder_id, reviewed_by, notes, Json({ "source": "context_review_case_admin_v0310", "cascade_to_episode_reviews": True, }), ), ) case_review = dict(cursor.fetchone()) cursor.execute( """ SELECT h.* FROM mv_loss_intelligence .context_review_case_evidence e JOIN mv_reports_ucepsa_prod .li_shopfloor_context_incident_history_v1 h ON h.episode_id = e.episode_id WHERE e.case_id = %s ORDER BY h.episode_id """, (case_id,), ) episodes = [ dict(row) for row in cursor.fetchall() ] cascaded = [] for episode in episodes: if episode["review_status"] != "PENDING": continue cascade_notes = ( f"Caso raíz {case_id}: " f"{notes or classification}. " f"Orden seleccionada: " f"{selected_order_ref or '—'}; " f"workorder seleccionado: " f"{selected_workorder_id or '—'}." ) cursor.execute( """ INSERT INTO mv_loss_intelligence .shopfloor_context_incident_reviews ( episode_id, previous_values, new_review_status, review_classification, reviewed_by, notes, evidence_json ) VALUES ( %s, %s, %s, %s, %s, %s, %s ) RETURNING review_id """, ( episode["episode_id"], Json({ "review_status": episode["review_status"], "review_classification": episode[ "review_classification" ], "reviewed_by": episode["reviewed_by"], "reviewed_at": episode["reviewed_at"], "review_notes": episode["review_notes"], }), episode_status, episode_class, reviewed_by, cascade_notes, Json({ "source_case_id": case_id, "root_classification": classification, }), ), ) cascaded.append({ "episode_id": episode["episode_id"], "review_id": cursor.fetchone()["review_id"], }) cursor.execute( """ SELECT * FROM mv_reports_ucepsa_prod .li_context_review_case_history_v1 WHERE case_id = %s """, (case_id,), ) current = dict(cursor.fetchone()) print_json({ "case_review": case_review, "case": current, "cascaded_episode_reviews": cascaded, "warning": ( "La revisión documenta la decisión y " "cierra el backlog humano. No modifica Odoo " "ni habilita el Ledger oficial." ), }) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Administración de casos raíz." ) sub = parser.add_subparsers( dest="command", required=True, ) list_cmd = sub.add_parser("list") list_cmd.add_argument( "--limit", type=int, default=50, ) show_cmd = sub.add_parser("show") show_cmd.add_argument( "--case-id", type=int, required=True, ) review_cmd = sub.add_parser("review") review_cmd.add_argument( "--case-id", type=int, required=True, ) review_cmd.add_argument( "--classification", choices=CLASSIFICATIONS, required=True, ) review_cmd.add_argument( "--reviewed-by", required=True, ) review_cmd.add_argument("--notes") review_cmd.add_argument("--selected-order-ref") review_cmd.add_argument( "--selected-workorder-id", type=int, ) return parser.parse_args() def main() -> int: args = parse_args() if args.command == "list": list_cases(args.limit) return 0 if args.command == "show": show_case(args.case_id) return 0 if args.command == "review": review_case( case_id=args.case_id, classification=args.classification, reviewed_by=args.reviewed_by, notes=args.notes, selected_order_ref= args.selected_order_ref, selected_workorder_id= args.selected_workorder_id, ) return 0 raise RuntimeError("Comando no soportado") if __name__ == "__main__": raise SystemExit(main())