40-clients/ucepsa/edge-oee-demo/tools/context_review_case_admin.py
2026-07-20 17:28:10 +02:00

482 lines
13 KiB
Python
Executable File

#!/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())