40-clients/ucepsa/edge-oee-demo/sql/070_loss_intelligence_v0.sql
2026-05-27 19:46:27 +02:00

286 lines
11 KiB
SQL

DROP VIEW IF EXISTS mv_reports_ucepsa_demo.loss_priority_board;
DROP VIEW IF EXISTS mv_reports_ucepsa_demo.loss_events_current;
CREATE VIEW mv_reports_ucepsa_demo.loss_events_current AS
WITH open_stop_losses AS (
SELECT
now()::timestamptz AS detected_at,
s.machine_id::text AS machine_id,
COALESCE(s.asset, '')::text AS asset,
COALESCE(s.order_id, '')::text AS order_ref,
'OPEN_STOP'::text AS loss_type,
CASE
WHEN COALESCE(s.current_duration_min, 0) >= 15 THEN 'ACTION_REQUIRED'
WHEN COALESCE(s.current_duration_min, 0) >= 5 THEN 'ATTENTION'
ELSE 'INFO'
END AS severity,
CASE
WHEN COALESCE(s.current_duration_min, 0) >= 15 THEN 80
WHEN COALESCE(s.current_duration_min, 0) >= 5 THEN 55
ELSE 30
END::int AS severity_score,
COALESCE(s.current_duration_min, 0)::numeric AS duration_min,
COALESCE(s.current_cost_eur, 0)::numeric AS cost_eur,
COALESCE(s.status, 'OPEN')::text AS status,
'Paro abierto pendiente de cierre o clasificación.'::text AS description,
'Comprobar causa en máquina y clasificar el paro desde la consola.'::text AS recommended_action,
jsonb_build_object(
'stop_id', s.id,
'classification_status', s.classification_status,
'cause_name', s.cause_name,
'started_at', s.started_at,
'source', 'mv_reports_ucepsa_demo.open_stops'
) AS evidence_json
FROM mv_reports_ucepsa_demo.open_stops s
),
unclassified_closed_stop_losses AS (
SELECT
now()::timestamptz AS detected_at,
s.machine_id::text AS machine_id,
COALESCE(s.asset, '')::text AS asset,
COALESCE(s.order_id, '')::text AS order_ref,
'UNCLASSIFIED_STOP'::text AS loss_type,
CASE
WHEN COALESCE(s.duration_min, 0) >= 15 THEN 'ACTION_REQUIRED'
ELSE 'ATTENTION'
END AS severity,
CASE
WHEN COALESCE(s.duration_min, 0) >= 15 THEN 75
ELSE 55
END::int AS severity_score,
COALESCE(s.duration_min, 0)::numeric AS duration_min,
COALESCE(s.cost_eur, 0)::numeric AS cost_eur,
COALESCE(s.status, 'CLOSED')::text AS status,
'Paro cerrado pendiente de clasificación.'::text AS description,
'Asignar causa para evitar pérdida sin explicación.'::text AS recommended_action,
jsonb_build_object(
'stop_id', s.id,
'classification_status', s.classification_status,
'cause_name', s.cause_name,
'started_at', s.started_at,
'ended_at', s.ended_at,
'source', 'mv_reports_ucepsa_demo.stops_recent'
) AS evidence_json
FROM mv_reports_ucepsa_demo.stops_recent s
WHERE s.status = 'CLOSED'
AND s.classification_status = 'PENDING'
),
odoo_machine_losses AS (
SELECT
now()::timestamptz AS detected_at,
o.machine_id::text AS machine_id,
COALESCE(o.asset, '')::text AS asset,
COALESCE(o.odoo_order_ref, '')::text AS order_ref,
o.status_check::text AS loss_type,
CASE
WHEN o.status_check IN ('ORDEN_EN_CURSO_MAQUINA_PARADA', 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA') THEN 'ACTION_REQUIRED'
WHEN o.status_check = 'SIN_TELEMETRIA' THEN 'ATTENTION'
ELSE 'INFO'
END AS severity,
CASE
WHEN o.status_check = 'ORDEN_EN_CURSO_MAQUINA_PARADA' THEN 85
WHEN o.status_check = 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA' THEN 75
WHEN o.status_check = 'SIN_TELEMETRIA' THEN 60
ELSE 20
END::int AS severity_score,
COALESCE(o.stop_min, 0)::numeric AS duration_min,
COALESCE(o.stop_cost_eur, 0)::numeric AS cost_eur,
o.status_check::text AS status,
CASE
WHEN o.status_check = 'ORDEN_EN_CURSO_MAQUINA_PARADA'
THEN 'Odoo indica orden en curso, pero la máquina está parada.'
WHEN o.status_check = 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA'
THEN 'La máquina está en marcha, pero la orden no aparece iniciada en Odoo.'
WHEN o.status_check = 'SIN_TELEMETRIA'
THEN 'No hay telemetría reciente para contrastar Odoo contra máquina.'
ELSE 'Sin discrepancia relevante.'
END AS description,
CASE
WHEN o.status_check = 'ORDEN_EN_CURSO_MAQUINA_PARADA'
THEN 'Revisar paro real, operario, causa pendiente y estado de máquina.'
WHEN o.status_check = 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA'
THEN 'Revisar disciplina de inicio de orden en Odoo o desfase de sincronización.'
WHEN o.status_check = 'SIN_TELEMETRIA'
THEN 'Revisar RevPi, MQTT, VPN/red, contador y sink.'
ELSE 'Sin acción.'
END AS recommended_action,
jsonb_build_object(
'odoo_order_ref', o.odoo_order_ref,
'odoo_state', o.odoo_state,
'machine_running', o.machine_running,
'telemetry_age_s', o.telemetry_age_s,
'real_cycles', o.real_cycles,
'planned_cycles', o.planned_cycles,
'cycle_completion_pct', o.cycle_completion_pct,
'source', 'mv_reports_ucepsa_demo.odoo_vs_machine_demo'
) AS evidence_json
FROM mv_reports_ucepsa_demo.odoo_vs_machine_demo o
WHERE o.status_check IS DISTINCT FROM 'OK'
),
telemetry_losses AS (
SELECT
now()::timestamptz AS detected_at,
l.machine_id::text AS machine_id,
COALESCE(l.asset, '')::text AS asset,
COALESCE(l.order_id, '')::text AS order_ref,
'STALE_OR_BAD_TELEMETRY'::text AS loss_type,
CASE
WHEN COALESCE(l.comm_ok, false) = false THEN 'ACTION_REQUIRED'
WHEN COALESCE(l.age_s, 999999) > 120 THEN 'ACTION_REQUIRED'
WHEN COALESCE(l.age_s, 999999) > 30 THEN 'ATTENTION'
ELSE 'INFO'
END AS severity,
CASE
WHEN COALESCE(l.comm_ok, false) = false THEN 80
WHEN COALESCE(l.age_s, 999999) > 120 THEN 75
WHEN COALESCE(l.age_s, 999999) > 30 THEN 45
ELSE 10
END::int AS severity_score,
0::numeric AS duration_min,
0::numeric AS cost_eur,
CASE
WHEN COALESCE(l.comm_ok, false) = false THEN 'BAD_COMMUNICATION'
WHEN COALESCE(l.age_s, 999999) > 120 THEN 'STALE_TELEMETRY'
WHEN COALESCE(l.age_s, 999999) > 30 THEN 'DELAYED_TELEMETRY'
ELSE 'OK'
END::text AS status,
CASE
WHEN COALESCE(l.comm_ok, false) = false THEN 'La RevPi publica, pero la lectura Modbus no es válida.'
WHEN COALESCE(l.age_s, 999999) > 120 THEN 'No hay telemetría reciente de máquina.'
WHEN COALESCE(l.age_s, 999999) > 30 THEN 'La telemetría llega con retraso.'
ELSE 'Telemetría correcta.'
END AS description,
CASE
WHEN COALESCE(l.comm_ok, false) = false THEN 'Revisar RS-485, SDM120-M, alimentación del contador y logs de la RevPi.'
WHEN COALESCE(l.age_s, 999999) > 120 THEN 'Revisar RevPi, red/VPN, Mosquitto y sink MQTT.'
WHEN COALESCE(l.age_s, 999999) > 30 THEN 'Revisar estabilidad de red y frecuencia de publicación.'
ELSE 'Sin acción.'
END AS recommended_action,
jsonb_build_object(
'age_s', l.age_s,
'comm_ok', l.comm_ok,
'error_text', l.error_text,
'ingest_ts', l.ingest_ts,
'source', 'mv_reports_ucepsa_demo.latest_machine_state_fast'
) AS evidence_json
FROM mv_reports_ucepsa_demo.latest_machine_state_fast l
WHERE COALESCE(l.comm_ok, false) = false
OR COALESCE(l.age_s, 999999) > 30
),
low_cycle_rate_losses AS (
SELECT
now()::timestamptz AS detected_at,
l.machine_id::text AS machine_id,
COALESCE(l.asset, '')::text AS asset,
COALESCE(o.odoo_order_ref, '')::text AS order_ref,
'LOW_CYCLE_RATE'::text AS loss_type,
CASE
WHEN COALESCE(l.cycle_rate_ppm, 0) <= 0 THEN 'ATTENTION'
WHEN COALESCE(l.cycle_rate_ppm, 0) < 7 THEN 'ATTENTION'
ELSE 'INFO'
END AS severity,
CASE
WHEN COALESCE(l.cycle_rate_ppm, 0) <= 0 THEN 50
WHEN COALESCE(l.cycle_rate_ppm, 0) < 7 THEN 40
ELSE 10
END::int AS severity_score,
0::numeric AS duration_min,
0::numeric AS cost_eur,
'LOW_RATE'::text AS status,
'La máquina aparece en marcha, pero la velocidad de ciclos/golpes es baja respecto al objetivo inicial.'::text AS description,
'Revisar golpes reales, sensor I_2, velocidad programada, material, ajuste de máquina y criterio de bolsas por golpe.'::text AS recommended_action,
jsonb_build_object(
'cycle_rate_ppm', l.cycle_rate_ppm,
'cycle_pulse_count', l.cycle_pulse_count,
'machine_running', l.machine_running,
'target_cycle_rate_ppm_assumed', 10,
'odoo_order_ref', o.odoo_order_ref,
'source', 'mv_reports_ucepsa_demo.latest_machine_state_fast'
) AS evidence_json
FROM mv_reports_ucepsa_demo.latest_machine_state_fast l
LEFT JOIN mv_reports_ucepsa_demo.odoo_current_order_demo o
ON o.machine_id = l.machine_id
WHERE COALESCE(l.age_s, 999999) <= 120
AND COALESCE(l.machine_running, false) = true
AND COALESCE(l.cycle_rate_ppm, 0) < 7
)
SELECT *
FROM open_stop_losses
UNION ALL
SELECT *
FROM unclassified_closed_stop_losses
UNION ALL
SELECT *
FROM odoo_machine_losses
UNION ALL
SELECT *
FROM telemetry_losses
UNION ALL
SELECT *
FROM low_cycle_rate_losses;
CREATE VIEW mv_reports_ucepsa_demo.loss_priority_board AS
SELECT
row_number() OVER (
ORDER BY
(
severity_score
+ LEAST((COALESCE(cost_eur, 0) * 10), 50)::int
+ LEAST((COALESCE(duration_min, 0) * 2), 40)::int
) DESC,
detected_at DESC
) AS priority_rank,
(
severity_score
+ LEAST((COALESCE(cost_eur, 0) * 10), 50)::int
+ LEAST((COALESCE(duration_min, 0) * 2), 40)::int
) AS priority_score,
CASE
WHEN (
severity_score
+ LEAST((COALESCE(cost_eur, 0) * 10), 50)::int
+ LEAST((COALESCE(duration_min, 0) * 2), 40)::int
) >= 90 THEN 'CRÍTICA'
WHEN (
severity_score
+ LEAST((COALESCE(cost_eur, 0) * 10), 50)::int
+ LEAST((COALESCE(duration_min, 0) * 2), 40)::int
) >= 60 THEN 'ALTA'
WHEN (
severity_score
+ LEAST((COALESCE(cost_eur, 0) * 10), 50)::int
+ LEAST((COALESCE(duration_min, 0) * 2), 40)::int
) >= 35 THEN 'MEDIA'
ELSE 'BAJA'
END AS priority_level,
detected_at,
machine_id,
asset,
order_ref,
loss_type,
severity,
duration_min,
cost_eur,
status,
description,
recommended_action,
evidence_json
FROM mv_reports_ucepsa_demo.loss_events_current
ORDER BY priority_score DESC, detected_at DESC;
GRANT SELECT ON mv_reports_ucepsa_demo.loss_events_current TO grafana_ucepsa_ro;
GRANT SELECT ON mv_reports_ucepsa_demo.loss_priority_board TO grafana_ucepsa_ro;