From 0ed753587bc4521df6554bdbd29b5214911f36d3 Mon Sep 17 00:00:00 2001 From: Victor Fraile Garcia Date: Thu, 28 May 2026 22:26:40 +0200 Subject: [PATCH] Add UCEPSA order execution report SQL views --- .../070_order_execution_report_views.sql | 885 ++++++++++++++++++ 1 file changed, 885 insertions(+) create mode 100644 ucepsa/edge-oee-demo/sql/reports/070_order_execution_report_views.sql diff --git a/ucepsa/edge-oee-demo/sql/reports/070_order_execution_report_views.sql b/ucepsa/edge-oee-demo/sql/reports/070_order_execution_report_views.sql new file mode 100644 index 0000000..26f995a --- /dev/null +++ b/ucepsa/edge-oee-demo/sql/reports/070_order_execution_report_views.sql @@ -0,0 +1,885 @@ +-- MESAVAULT Edge-OEE UCEPSA +-- Order execution report views +-- Generated from validated PostgreSQL views. +-- Contains no data and no secrets. + +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_execution_report_full_v1; +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_execution_data_quality_v1; +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_execution_energy_timeseries_v1; +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_execution_timeline_v1; +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_execution_stop_causes_v1; +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_execution_stops_v1; +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_execution_report_v1; + +-- +-- PostgreSQL database dump +-- + +\restrict YbA0VcRIrZ1kSDSxIf4zctoL9eu8TYoJ5cvkbSD5WkFTQ1jRyH26snWSZehz5VJ + +-- Dumped from database version 16.14 +-- Dumped by pg_dump version 16.14 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: order_execution_report_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_execution_report_v1 AS + WITH orders AS ( + SELECT o_1.id, + o_1.tenant, + o_1.site, + o_1.odoo_instance, + o_1.odoo_workorder_id, + o_1.odoo_production_id, + o_1.odoo_order_ref, + o_1.odoo_state, + o_1.odoo_workcenter_code, + o_1.odoo_workcenter_name, + o_1.machine_id, + o_1.asset, + o_1.product, + o_1.qty_planned, + o_1.qty_done, + o_1.uom, + o_1.planned_cycles, + o_1.target_cycle_rate_ppm, + o_1.odoo_start, + o_1.odoo_end, + o_1.notes, + o_1.snapshot_at, + o_1.odoo_start AS start_ts, + CASE + WHEN (o_1.odoo_state = ANY (ARRAY['done'::text, 'to_close'::text, 'cancel'::text])) THEN COALESCE(o_1.odoo_end, o_1.snapshot_at) + ELSE COALESCE(( SELECT max(r.ingest_ts) AS max + FROM mv_hot.edge_oee_eastron_readings r + WHERE ((r.machine_id = o_1.machine_id) AND (r.ingest_ts >= o_1.odoo_start) AND (r.comm_ok = true) AND (r.meter_model = 'Eastron SDM120-M'::text))), o_1.snapshot_at) + END AS end_ts + FROM mv_edge_oee_ucepsa_demo.odoo_workorders_demo o_1 + WHERE ((o_1.odoo_instance = 'odoo_ucepsa_test'::text) AND (o_1.machine_id IS NOT NULL) AND (o_1.odoo_start IS NOT NULL)) + ), energy AS ( + SELECT o_1.id AS order_row_id, + count(r.id) AS energy_samples, + min(r.ingest_ts) AS energy_first_ts, + max(r.ingest_ts) AS energy_last_ts, + min(r.import_kwh) AS start_import_kwh, + max(r.import_kwh) AS end_import_kwh, + round((max(r.import_kwh) - min(r.import_kwh)), 3) AS kwh_consumed, + round(avg(r.power_total_kw), 3) AS avg_kw, + round(max(r.power_total_kw), 3) AS max_kw + FROM (orders o_1 + LEFT JOIN mv_hot.edge_oee_eastron_readings r ON (((r.machine_id = o_1.machine_id) AND (r.ingest_ts >= o_1.start_ts) AND (r.ingest_ts <= o_1.end_ts) AND (r.comm_ok = true) AND (r.meter_model = 'Eastron SDM120-M'::text)))) + GROUP BY o_1.id + ), cycles AS ( + SELECT o_1.id AS order_row_id, + count(r.id) AS telemetry_samples, + min(r.ingest_ts) AS telemetry_first_ts, + max(r.ingest_ts) AS telemetry_last_ts, + min(((r.payload_json ->> 'cycle_pulse_count'::text))::bigint) AS min_cycle_pulse_count, + max(((r.payload_json ->> 'cycle_pulse_count'::text))::bigint) AS max_cycle_pulse_count, + GREATEST((max(((r.payload_json ->> 'cycle_pulse_count'::text))::bigint) - min(((r.payload_json ->> 'cycle_pulse_count'::text))::bigint)), (0)::bigint) AS real_cycle_delta, + round(avg(((r.payload_json ->> 'cycle_rate_ppm'::text))::numeric), 2) AS avg_cycle_rate_ppm, + round(avg(NULLIF(((r.payload_json ->> 'cycle_rate_ppm'::text))::numeric, (0)::numeric)), 2) AS avg_nonzero_cycle_rate_ppm + FROM (orders o_1 + LEFT JOIN mv_hot.edge_oee_eastron_readings r ON (((r.machine_id = o_1.machine_id) AND (r.ingest_ts >= o_1.start_ts) AND (r.ingest_ts <= o_1.end_ts) AND (r.comm_ok = true) AND (r.meter_model = 'Eastron SDM120-M'::text) AND (r.payload_json ? 'cycle_pulse_count'::text)))) + GROUP BY o_1.id + ), stops AS ( + SELECT o_1.id AS order_row_id, + count(ms.id) AS stop_count, + round((COALESCE(sum(GREATEST(EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, o_1.end_ts), o_1.end_ts) - GREATEST(ms.started_at, o_1.start_ts))), (0)::numeric)), (0)::numeric) / 60.0), 2) AS stop_min, + count(ms.id) FILTER (WHERE (ms.classification_status = 'PENDING'::text)) AS pending_stops, + count(ms.id) FILTER (WHERE (ms.classification_status = 'CLASSIFIED'::text)) AS classified_stops, + round(COALESCE(sum(((GREATEST(EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, o_1.end_ts), o_1.end_ts) - GREATEST(ms.started_at, o_1.start_ts))), (0)::numeric) / 3600.0) * ms.value_hour_eur)), (0)::numeric), 2) AS stop_cost_eur + FROM (orders o_1 + LEFT JOIN mv_hot.edge_oee_machine_stops ms ON (((ms.machine_id = o_1.machine_id) AND (ms.started_at < o_1.end_ts) AND (COALESCE(ms.ended_at, now()) > o_1.start_ts)))) + GROUP BY o_1.id + ) + SELECT o.id AS order_row_id, + o.tenant, + o.site, + o.odoo_instance, + o.odoo_workorder_id, + o.odoo_production_id, + o.odoo_order_ref, + o.odoo_state, + o.odoo_workcenter_code, + o.odoo_workcenter_name, + o.machine_id, + o.asset, + o.product, + o.qty_planned, + o.qty_done, + o.uom, + o.planned_cycles, + o.target_cycle_rate_ppm, + o.start_ts, + o.end_ts, + round((EXTRACT(epoch FROM (o.end_ts - o.start_ts)) / 60.0), 2) AS window_min, + COALESCE(c.telemetry_samples, (0)::bigint) AS telemetry_samples, + c.telemetry_first_ts, + c.telemetry_last_ts, + COALESCE(c.min_cycle_pulse_count, (0)::bigint) AS min_cycle_pulse_count, + COALESCE(c.max_cycle_pulse_count, (0)::bigint) AS max_cycle_pulse_count, + COALESCE(c.real_cycle_delta, (0)::bigint) AS real_cycle_delta, + c.avg_cycle_rate_ppm, + c.avg_nonzero_cycle_rate_ppm, + COALESCE(e.energy_samples, (0)::bigint) AS energy_samples, + e.energy_first_ts, + e.energy_last_ts, + e.start_import_kwh, + e.end_import_kwh, + e.kwh_consumed, + e.avg_kw, + e.max_kw, + COALESCE(s.stop_count, (0)::bigint) AS stop_count, + COALESCE(s.stop_min, (0)::numeric) AS stop_min, + COALESCE(s.classified_stops, (0)::bigint) AS classified_stops, + COALESCE(s.pending_stops, (0)::bigint) AS pending_stops, + COALESCE(s.stop_cost_eur, (0)::numeric) AS stop_cost_eur, + CASE + WHEN (COALESCE(c.telemetry_samples, (0)::bigint) = 0) THEN 'SIN_TELEMETRIA_EN_VENTANA'::text + WHEN (COALESCE(e.energy_samples, (0)::bigint) = 0) THEN 'SIN_ENERGIA_EN_VENTANA'::text + WHEN (o.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'PARCIAL_ORDEN_EN_CURSO'::text + WHEN (COALESCE(s.pending_stops, (0)::bigint) > 0) THEN 'PARCIAL_PAROS_PENDIENTES'::text + ELSE 'CERRADO_CALCULABLE'::text + END AS report_status, + CASE + WHEN (COALESCE(c.telemetry_samples, (0)::bigint) = 0) THEN 'No hay telemetría Edge-OEE dentro de la ventana de la orden.'::text + WHEN (COALESCE(e.energy_samples, (0)::bigint) = 0) THEN 'No hay muestras energéticas válidas dentro de la ventana de la orden.'::text + WHEN (o.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'La orden sigue en curso. El informe es parcial.'::text + WHEN (COALESCE(s.pending_stops, (0)::bigint) > 0) THEN 'Existen paros pendientes de clasificación.'::text + ELSE 'Orden cerrada con datos suficientes para informe.'::text + END AS report_status_description + FROM (((orders o + LEFT JOIN cycles c ON ((c.order_row_id = o.id))) + LEFT JOIN energy e ON ((e.order_row_id = o.id))) + LEFT JOIN stops s ON ((s.order_row_id = o.id))); + + +-- +-- Name: order_execution_energy_timeseries_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_execution_energy_timeseries_v1 AS + WITH base AS ( + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.odoo_state, + r.product, + r.machine_id, + r.asset, + r.start_ts AS order_start_ts, + r.end_ts AS order_end_ts, + r.report_status, + e.id AS reading_id, + e.ts AS meter_ts, + e.ingest_ts AS "time", + e.voltage_l1_v, + e.current_l1_a, + e.power_total_kw, + e.frequency_hz, + e.import_kwh, + e.export_kwh, + e.comm_ok, + e.error_text, + CASE + WHEN (e.payload_json ? 'machine_running'::text) THEN ((e.payload_json ->> 'machine_running'::text))::boolean + ELSE NULL::boolean + END AS machine_running, + CASE + WHEN (e.payload_json ? 'machine_auto_signal'::text) THEN ((e.payload_json ->> 'machine_auto_signal'::text))::boolean + ELSE NULL::boolean + END AS machine_auto_signal, + CASE + WHEN (e.payload_json ? 'cycle_pulse_count'::text) THEN (NULLIF((e.payload_json ->> 'cycle_pulse_count'::text), ''::text))::bigint + ELSE NULL::bigint + END AS cycle_pulse_count, + CASE + WHEN (e.payload_json ? 'cycle_rate_ppm'::text) THEN (NULLIF((e.payload_json ->> 'cycle_rate_ppm'::text), ''::text))::numeric + ELSE NULL::numeric + END AS cycle_rate_ppm, + CASE + WHEN (e.payload_json ? 'last_cycle_age_s'::text) THEN (NULLIF((e.payload_json ->> 'last_cycle_age_s'::text), ''::text))::numeric + ELSE NULL::numeric + END AS last_cycle_age_s, + e.payload_json + FROM (mv_reports_ucepsa_demo.order_execution_report_v1 r + JOIN mv_hot.edge_oee_eastron_readings e ON (((e.machine_id = r.machine_id) AND (e.ingest_ts >= r.start_ts) AND (e.ingest_ts <= r.end_ts) AND (e.meter_model = 'Eastron SDM120-M'::text)))) + ), enriched AS ( + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.product, + b.machine_id, + b.asset, + b.order_start_ts, + b.order_end_ts, + b.report_status, + b.reading_id, + b.meter_ts, + b."time", + b.voltage_l1_v, + b.current_l1_a, + b.power_total_kw, + b.frequency_hz, + b.import_kwh, + b.export_kwh, + b.comm_ok, + b.error_text, + b.machine_running, + b.machine_auto_signal, + b.cycle_pulse_count, + b.cycle_rate_ppm, + b.last_cycle_age_s, + b.payload_json, + round((EXTRACT(epoch FROM (b."time" - b.order_start_ts)) / 60.0), 2) AS minute_from_order_start, + round(EXTRACT(epoch FROM (b."time" - lag(b."time") OVER (PARTITION BY b.order_row_id ORDER BY b."time"))), 2) AS sample_gap_s, + min(b.import_kwh) FILTER (WHERE ((b.comm_ok = true) AND (b.import_kwh IS NOT NULL))) OVER (PARTITION BY b.order_row_id) AS order_start_import_kwh, + min(b.cycle_pulse_count) FILTER (WHERE (b.cycle_pulse_count IS NOT NULL)) OVER (PARTITION BY b.order_row_id) AS order_start_cycle_pulse_count + FROM base b + ) + SELECT order_row_id, + tenant, + site, + odoo_instance, + odoo_workorder_id, + odoo_production_id, + odoo_order_ref, + odoo_state, + product, + machine_id, + asset, + order_start_ts, + order_end_ts, + report_status, + reading_id, + meter_ts, + "time", + minute_from_order_start, + sample_gap_s, + voltage_l1_v, + current_l1_a, + power_total_kw, + frequency_hz, + import_kwh, + export_kwh, + round(GREATEST((import_kwh - order_start_import_kwh), (0)::numeric), 3) AS kwh_from_order_start, + comm_ok, + error_text, + machine_running, + machine_auto_signal, + cycle_pulse_count, + GREATEST((cycle_pulse_count - order_start_cycle_pulse_count), (0)::bigint) AS cycles_from_order_start, + cycle_rate_ppm, + last_cycle_age_s, + CASE + WHEN (comm_ok = false) THEN 'BAD_COMMUNICATION'::text + WHEN ((sample_gap_s IS NOT NULL) AND (sample_gap_s > (30)::numeric)) THEN 'TELEMETRY_GAP'::text + WHEN (power_total_kw IS NULL) THEN 'NO_POWER_VALUE'::text + WHEN (import_kwh IS NULL) THEN 'NO_IMPORT_KWH'::text + ELSE 'OK'::text + END AS sample_quality_status, + payload_json + FROM enriched; + + +-- +-- Name: order_execution_data_quality_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_execution_data_quality_v1 AS + WITH base AS ( + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.odoo_state, + r.odoo_workcenter_code, + r.odoo_workcenter_name, + r.machine_id, + r.asset, + r.product, + r.qty_planned, + r.qty_done, + r.uom, + r.planned_cycles, + r.target_cycle_rate_ppm, + r.start_ts, + r.end_ts, + r.window_min, + r.telemetry_samples, + r.telemetry_first_ts, + r.telemetry_last_ts, + r.min_cycle_pulse_count, + r.max_cycle_pulse_count, + r.real_cycle_delta, + r.avg_cycle_rate_ppm, + r.avg_nonzero_cycle_rate_ppm, + r.energy_samples, + r.energy_first_ts, + r.energy_last_ts, + r.start_import_kwh, + r.end_import_kwh, + r.kwh_consumed, + r.avg_kw, + r.max_kw, + r.stop_count, + r.stop_min, + r.classified_stops, + r.pending_stops, + r.stop_cost_eur, + r.report_status, + r.report_status_description, + GREATEST(((floor((EXTRACT(epoch FROM (r.end_ts - r.start_ts)) / 10.0)))::integer + 1), 1) AS expected_samples_10s + FROM mv_reports_ucepsa_demo.order_execution_report_v1 r + ), gap_stats AS ( + SELECT t.order_row_id, + count(*) FILTER (WHERE ((t.sample_gap_s IS NOT NULL) AND (t.sample_gap_s > (30)::numeric))) AS telemetry_gap_count_30s, + round(max(t.sample_gap_s), 2) AS max_sample_gap_s, + round(avg(t.sample_gap_s) FILTER (WHERE (t.sample_gap_s IS NOT NULL)), 2) AS avg_sample_gap_s, + count(*) FILTER (WHERE (t.sample_quality_status <> 'OK'::text)) AS non_ok_samples + FROM mv_reports_ucepsa_demo.order_execution_energy_timeseries_v1 t + GROUP BY t.order_row_id + ), scored AS ( + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.product, + b.machine_id, + b.asset, + b.start_ts, + b.end_ts, + b.window_min, + b.qty_planned, + b.qty_done, + b.uom, + b.planned_cycles, + b.target_cycle_rate_ppm, + b.telemetry_samples, + b.energy_samples, + b.expected_samples_10s, + round((((COALESCE(b.telemetry_samples, (0)::bigint))::numeric / (NULLIF(b.expected_samples_10s, 0))::numeric) * 100.0), 2) AS telemetry_sample_coverage_pct, + b.telemetry_first_ts, + b.telemetry_last_ts, + b.energy_first_ts, + b.energy_last_ts, + b.real_cycle_delta, + b.avg_cycle_rate_ppm, + b.avg_nonzero_cycle_rate_ppm, + b.kwh_consumed, + b.avg_kw, + b.max_kw, + b.stop_count, + b.stop_min, + b.classified_stops, + b.pending_stops, + b.stop_cost_eur, + COALESCE(g.telemetry_gap_count_30s, (0)::bigint) AS telemetry_gap_count_30s, + COALESCE(g.max_sample_gap_s, (0)::numeric) AS max_sample_gap_s, + COALESCE(g.avg_sample_gap_s, (0)::numeric) AS avg_sample_gap_s, + COALESCE(g.non_ok_samples, (0)::bigint) AS non_ok_samples, + b.report_status, + b.report_status_description, + CASE + WHEN (COALESCE(b.planned_cycles, (0)::numeric) <= (0)::numeric) THEN 'SIN_OBJETIVO_CICLOS'::text + WHEN ((COALESCE(b.real_cycle_delta, (0)::bigint) = 0) AND (COALESCE(b.qty_done, (0)::numeric) > (0)::numeric)) THEN 'CANTIDAD_ODOO_SIN_CICLOS_EDGE'::text + WHEN ((COALESCE(b.real_cycle_delta, (0)::bigint) > 0) AND (COALESCE(b.planned_cycles, (0)::numeric) > (0)::numeric) AND ((((COALESCE(b.real_cycle_delta, (0)::bigint))::numeric / NULLIF(b.planned_cycles, (0)::numeric)) >= 0.5) AND (((COALESCE(b.real_cycle_delta, (0)::bigint))::numeric / NULLIF(b.planned_cycles, (0)::numeric)) <= 1.5))) THEN 'CICLOS_COHERENTES_CON_OBJETIVO'::text + ELSE 'REVISAR_CONVERSION_ODOO_CICLOS'::text + END AS production_conversion_status, + GREATEST(0, (((((((100 - + CASE + WHEN (COALESCE(b.telemetry_samples, (0)::bigint) = 0) THEN 60 + ELSE 0 + END) - + CASE + WHEN (COALESCE(b.energy_samples, (0)::bigint) = 0) THEN 50 + ELSE 0 + END) - + CASE + WHEN (b.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 20 + ELSE 0 + END) - + CASE + WHEN (COALESCE(b.pending_stops, (0)::bigint) > 0) THEN 20 + ELSE 0 + END) - + CASE + WHEN (COALESCE(g.telemetry_gap_count_30s, (0)::bigint) > 0) THEN LEAST(((COALESCE(g.telemetry_gap_count_30s, (0)::bigint))::integer * 2), 15) + ELSE 0 + END) - + CASE + WHEN (COALESCE(g.max_sample_gap_s, (0)::numeric) > (60)::numeric) THEN 10 + ELSE 0 + END) - + CASE + WHEN (COALESCE(g.non_ok_samples, (0)::bigint) > 0) THEN 10 + ELSE 0 + END)) AS technical_quality_score + FROM (base b + LEFT JOIN gap_stats g ON ((g.order_row_id = b.order_row_id))) + ) + SELECT order_row_id, + tenant, + site, + odoo_instance, + odoo_workorder_id, + odoo_production_id, + odoo_order_ref, + odoo_state, + product, + machine_id, + asset, + start_ts, + end_ts, + window_min, + qty_planned, + qty_done, + uom, + planned_cycles, + target_cycle_rate_ppm, + telemetry_samples, + energy_samples, + expected_samples_10s, + telemetry_sample_coverage_pct, + telemetry_first_ts, + telemetry_last_ts, + energy_first_ts, + energy_last_ts, + real_cycle_delta, + avg_cycle_rate_ppm, + avg_nonzero_cycle_rate_ppm, + kwh_consumed, + avg_kw, + max_kw, + stop_count, + stop_min, + classified_stops, + pending_stops, + stop_cost_eur, + telemetry_gap_count_30s, + max_sample_gap_s, + avg_sample_gap_s, + non_ok_samples, + report_status, + report_status_description, + production_conversion_status, + technical_quality_score, + CASE + WHEN (technical_quality_score >= 90) THEN 'FIABLE'::text + WHEN (technical_quality_score >= 70) THEN 'ACEPTABLE_CON_OBSERVACIONES'::text + WHEN (technical_quality_score >= 40) THEN 'PARCIAL'::text + ELSE 'NO_DEFENDIBLE'::text + END AS technical_quality_level, + CASE + WHEN (COALESCE(telemetry_samples, (0)::bigint) = 0) THEN 'No hay telemetría Edge-OEE dentro de la ventana de la orden. El informe solo puede apoyarse en Odoo.'::text + WHEN (COALESCE(energy_samples, (0)::bigint) = 0) THEN 'Faltan muestras energéticas válidas dentro de la ventana de la orden.'::text + WHEN (odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'La orden sigue abierta. El informe debe tratarse como parcial.'::text + WHEN (COALESCE(pending_stops, (0)::bigint) > 0) THEN 'Hay paros pendientes de clasificación. Conviene clasificarlos antes de cerrar el informe.'::text + WHEN (COALESCE(telemetry_gap_count_30s, (0)::bigint) > 0) THEN 'Hay gaps de telemetría superiores a 30 segundos. Revisar continuidad de señal.'::text + WHEN (production_conversion_status = 'REVISAR_CONVERSION_ODOO_CICLOS'::text) THEN 'El informe técnico es válido para paros, energía y ciclos reales; revisar la conversión entre cantidad Odoo y ciclos/golpes.'::text + ELSE 'Datos suficientes para generar informe técnico de orden.'::text + END AS recommended_action + FROM scored; + + +-- +-- Name: order_execution_stops_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_execution_stops_v1 AS + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.odoo_state, + r.product, + r.machine_id, + r.asset, + r.start_ts AS order_start_ts, + r.end_ts AS order_end_ts, + r.report_status AS order_report_status, + ms.id AS stop_id, + ms.started_at AS stop_started_at, + ms.ended_at AS stop_ended_at, + GREATEST(ms.started_at, r.start_ts) AS attributed_started_at, + LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) AS attributed_ended_at, + round(ms.duration_s, 2) AS original_duration_s, + round((ms.duration_s / 60.0), 2) AS original_duration_min, + round(EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) - GREATEST(ms.started_at, r.start_ts))), 2) AS attributed_duration_s, + round((EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) - GREATEST(ms.started_at, r.start_ts))) / 60.0), 2) AS attributed_duration_min, + ms.status AS stop_status, + ms.classification_status, + ms.cause_code, + ms.operator_note, + ms.value_hour_eur, + round(((EXTRACT(epoch FROM (LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) - GREATEST(ms.started_at, r.start_ts))) / 3600.0) * ms.value_hour_eur), 2) AS attributed_cost_eur, + ms.start_event_name, + ms.end_event_name, + ms.created_at, + ms.updated_at, + CASE + WHEN (ms.status = 'OPEN'::text) THEN 'PARO_ABIERTO'::text + WHEN (ms.classification_status = 'PENDING'::text) THEN 'PENDIENTE_CLASIFICACION'::text + WHEN (ms.classification_status = 'CLASSIFIED'::text) THEN 'CLASIFICADO'::text + ELSE 'SIN_ESTADO_CLARO'::text + END AS stop_quality_status + FROM (mv_reports_ucepsa_demo.order_execution_report_v1 r + JOIN mv_hot.edge_oee_machine_stops ms ON (((ms.machine_id = r.machine_id) AND (ms.started_at < r.end_ts) AND (COALESCE(ms.ended_at, r.end_ts) > r.start_ts)))) + WHERE (LEAST(COALESCE(ms.ended_at, r.end_ts), r.end_ts) > GREATEST(ms.started_at, r.start_ts)); + + +-- +-- Name: order_execution_stop_causes_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_execution_stop_causes_v1 AS + SELECT order_row_id, + tenant, + site, + odoo_instance, + odoo_workorder_id, + odoo_production_id, + odoo_order_ref, + product, + machine_id, + cause_code, + count(*) AS stop_count, + round(sum(attributed_duration_min), 2) AS total_stop_min, + round(sum(attributed_cost_eur), 2) AS total_stop_cost_eur, + count(*) FILTER (WHERE (classification_status = 'CLASSIFIED'::text)) AS classified_stop_count, + count(*) FILTER (WHERE (classification_status = 'PENDING'::text)) AS pending_stop_count, + min(stop_started_at) AS first_stop_at, + max(stop_ended_at) AS last_stop_at + FROM mv_reports_ucepsa_demo.order_execution_stops_v1 + GROUP BY order_row_id, tenant, site, odoo_instance, odoo_workorder_id, odoo_production_id, odoo_order_ref, product, machine_id, cause_code; + + +-- +-- Name: order_execution_timeline_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_execution_timeline_v1 AS + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.product, + r.machine_id, + r.asset, + r.start_ts AS event_ts, + 'ORDER_START'::text AS event_type, + 'Inicio de orden'::text AS event_label, + 'INFO'::text AS severity, + NULL::bigint AS stop_id, + NULL::bigint AS node_event_id, + NULL::text AS cause_code, + NULL::numeric AS duration_min, + NULL::numeric AS cost_eur, + 'Inicio de la ventana de ejecución de la orden según Odoo.'::text AS description, + jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_report_v1', 'odoo_state', r.odoo_state, 'qty_planned', r.qty_planned, 'qty_done', r.qty_done, 'uom', r.uom) AS evidence_json + FROM mv_reports_ucepsa_demo.order_execution_report_v1 r + WHERE (r.start_ts IS NOT NULL) +UNION ALL + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.product, + r.machine_id, + r.asset, + r.telemetry_first_ts AS event_ts, + 'FIRST_TELEMETRY'::text AS event_type, + 'Primera telemetría Edge-OEE'::text AS event_label, + 'INFO'::text AS severity, + NULL::bigint AS stop_id, + NULL::bigint AS node_event_id, + NULL::text AS cause_code, + NULL::numeric AS duration_min, + NULL::numeric AS cost_eur, + 'Primera muestra de telemetría recibida dentro de la ventana de la orden.'::text AS description, + jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_report_v1', 'telemetry_samples', r.telemetry_samples, 'energy_samples', r.energy_samples) AS evidence_json + FROM mv_reports_ucepsa_demo.order_execution_report_v1 r + WHERE (r.telemetry_first_ts IS NOT NULL) +UNION ALL + SELECT s.order_row_id, + s.tenant, + s.site, + s.odoo_instance, + s.odoo_workorder_id, + s.odoo_production_id, + s.odoo_order_ref, + s.product, + s.machine_id, + s.asset, + s.attributed_started_at AS event_ts, + 'STOP_START'::text AS event_type, + 'Inicio de paro'::text AS event_label, + CASE + WHEN (s.classification_status = 'PENDING'::text) THEN 'WARNING'::text + ELSE 'LOSS'::text + END AS severity, + s.stop_id, + NULL::bigint AS node_event_id, + s.cause_code, + s.attributed_duration_min AS duration_min, + s.attributed_cost_eur AS cost_eur, + concat('Inicio de paro imputado a la orden. Causa: ', COALESCE(s.cause_code, 'sin clasificar'::text), '. Duración imputada: ', COALESCE((s.attributed_duration_min)::text, '0'::text), ' min.') AS description, + jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_stops_v1', 'stop_id', s.stop_id, 'classification_status', s.classification_status, 'operator_note', s.operator_note, 'original_started_at', s.stop_started_at, 'original_ended_at', s.stop_ended_at) AS evidence_json + FROM mv_reports_ucepsa_demo.order_execution_stops_v1 s +UNION ALL + SELECT s.order_row_id, + s.tenant, + s.site, + s.odoo_instance, + s.odoo_workorder_id, + s.odoo_production_id, + s.odoo_order_ref, + s.product, + s.machine_id, + s.asset, + s.attributed_ended_at AS event_ts, + 'STOP_END'::text AS event_type, + 'Fin de paro'::text AS event_label, + CASE + WHEN (s.classification_status = 'PENDING'::text) THEN 'WARNING'::text + ELSE 'INFO'::text + END AS severity, + s.stop_id, + NULL::bigint AS node_event_id, + s.cause_code, + s.attributed_duration_min AS duration_min, + s.attributed_cost_eur AS cost_eur, + concat('Fin de paro imputado a la orden. Causa: ', COALESCE(s.cause_code, 'sin clasificar'::text), '. Coste imputado: ', COALESCE((s.attributed_cost_eur)::text, '0'::text), ' €.') AS description, + jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_stops_v1', 'stop_id', s.stop_id, 'classification_status', s.classification_status, 'operator_note', s.operator_note, 'value_hour_eur', s.value_hour_eur) AS evidence_json + FROM mv_reports_ucepsa_demo.order_execution_stops_v1 s +UNION ALL + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.product, + r.machine_id, + r.asset, + r.telemetry_last_ts AS event_ts, + 'LAST_TELEMETRY'::text AS event_type, + 'Última telemetría Edge-OEE'::text AS event_label, + CASE + WHEN (r.report_status = 'PARCIAL_ORDEN_EN_CURSO'::text) THEN 'WARNING'::text + ELSE 'INFO'::text + END AS severity, + NULL::bigint AS stop_id, + NULL::bigint AS node_event_id, + NULL::text AS cause_code, + NULL::numeric AS duration_min, + NULL::numeric AS cost_eur, + 'Última muestra de telemetría recibida dentro de la ventana analizada.'::text AS description, + jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_report_v1', 'real_cycle_delta', r.real_cycle_delta, 'kwh_consumed', r.kwh_consumed, 'avg_kw', r.avg_kw, 'max_kw', r.max_kw) AS evidence_json + FROM mv_reports_ucepsa_demo.order_execution_report_v1 r + WHERE (r.telemetry_last_ts IS NOT NULL) +UNION ALL + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.product, + r.machine_id, + r.asset, + r.end_ts AS event_ts, + CASE + WHEN (r.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'REPORT_CUTOFF'::text + ELSE 'ORDER_END'::text + END AS event_type, + CASE + WHEN (r.odoo_state = ANY (ARRAY['progress'::text, 'in_progress'::text, 'started'::text])) THEN 'Corte provisional del informe'::text + ELSE 'Fin de orden'::text + END AS event_label, + CASE + WHEN (r.report_status = 'CERRADO_CALCULABLE'::text) THEN 'INFO'::text + ELSE 'WARNING'::text + END AS severity, + NULL::bigint AS stop_id, + NULL::bigint AS node_event_id, + NULL::text AS cause_code, + NULL::numeric AS duration_min, + NULL::numeric AS cost_eur, + r.report_status_description AS description, + jsonb_build_object('source', 'mv_reports_ucepsa_demo.order_execution_report_v1', 'report_status', r.report_status, 'window_min', r.window_min, 'stop_count', r.stop_count, 'stop_min', r.stop_min, 'stop_cost_eur', r.stop_cost_eur) AS evidence_json + FROM mv_reports_ucepsa_demo.order_execution_report_v1 r + WHERE (r.end_ts IS NOT NULL); + + +-- +-- Name: order_execution_report_full_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - +-- + +CREATE VIEW mv_reports_ucepsa_demo.order_execution_report_full_v1 AS + WITH stop_causes_json AS ( + SELECT order_execution_stop_causes_v1.order_row_id, + jsonb_agg(jsonb_build_object('cause_code', order_execution_stop_causes_v1.cause_code, 'stop_count', order_execution_stop_causes_v1.stop_count, 'total_stop_min', order_execution_stop_causes_v1.total_stop_min, 'total_stop_cost_eur', order_execution_stop_causes_v1.total_stop_cost_eur, 'classified_stop_count', order_execution_stop_causes_v1.classified_stop_count, 'pending_stop_count', order_execution_stop_causes_v1.pending_stop_count, 'first_stop_at', order_execution_stop_causes_v1.first_stop_at, 'last_stop_at', order_execution_stop_causes_v1.last_stop_at) ORDER BY order_execution_stop_causes_v1.total_stop_min DESC) AS stop_causes_json, + string_agg(concat(COALESCE(order_execution_stop_causes_v1.cause_code, 'sin_clasificar'::text), ': ', order_execution_stop_causes_v1.stop_count, ' paros / ', order_execution_stop_causes_v1.total_stop_min, ' min / ', order_execution_stop_causes_v1.total_stop_cost_eur, ' €'), '; '::text ORDER BY order_execution_stop_causes_v1.total_stop_min DESC) AS stop_causes_summary + FROM mv_reports_ucepsa_demo.order_execution_stop_causes_v1 + GROUP BY order_execution_stop_causes_v1.order_row_id + ), stops_json AS ( + SELECT order_execution_stops_v1.order_row_id, + jsonb_agg(jsonb_build_object('stop_id', order_execution_stops_v1.stop_id, 'stop_started_at', order_execution_stops_v1.stop_started_at, 'stop_ended_at', order_execution_stops_v1.stop_ended_at, 'attributed_started_at', order_execution_stops_v1.attributed_started_at, 'attributed_ended_at', order_execution_stops_v1.attributed_ended_at, 'attributed_duration_min', order_execution_stops_v1.attributed_duration_min, 'cause_code', order_execution_stops_v1.cause_code, 'operator_note', order_execution_stops_v1.operator_note, 'classification_status', order_execution_stops_v1.classification_status, 'attributed_cost_eur', order_execution_stops_v1.attributed_cost_eur, 'stop_quality_status', order_execution_stops_v1.stop_quality_status) ORDER BY order_execution_stops_v1.attributed_started_at) AS stops_json + FROM mv_reports_ucepsa_demo.order_execution_stops_v1 + GROUP BY order_execution_stops_v1.order_row_id + ), timeline_json AS ( + SELECT order_execution_timeline_v1.order_row_id, + jsonb_agg(jsonb_build_object('event_ts', order_execution_timeline_v1.event_ts, 'event_type', order_execution_timeline_v1.event_type, 'event_label', order_execution_timeline_v1.event_label, 'severity', order_execution_timeline_v1.severity, 'stop_id', order_execution_timeline_v1.stop_id, 'cause_code', order_execution_timeline_v1.cause_code, 'duration_min', order_execution_timeline_v1.duration_min, 'cost_eur', order_execution_timeline_v1.cost_eur, 'description', order_execution_timeline_v1.description) ORDER BY order_execution_timeline_v1.event_ts, order_execution_timeline_v1.event_type) AS timeline_json + FROM mv_reports_ucepsa_demo.order_execution_timeline_v1 + GROUP BY order_execution_timeline_v1.order_row_id + ) + SELECT r.order_row_id, + r.tenant, + r.site, + r.odoo_instance, + r.odoo_workorder_id, + r.odoo_production_id, + r.odoo_order_ref, + r.odoo_state, + r.odoo_workcenter_code, + r.odoo_workcenter_name, + r.machine_id, + r.asset, + r.product, + r.qty_planned, + r.qty_done, + r.uom, + r.planned_cycles, + r.target_cycle_rate_ppm, + r.start_ts, + r.end_ts, + r.window_min, + GREATEST((r.window_min - r.stop_min), (0)::numeric) AS run_min, + CASE + WHEN (r.window_min > (0)::numeric) THEN round(((r.stop_min / r.window_min) * 100.0), 2) + ELSE NULL::numeric + END AS stop_pct, + CASE + WHEN (r.window_min > (0)::numeric) THEN round(((GREATEST((r.window_min - r.stop_min), (0)::numeric) / r.window_min) * 100.0), 2) + ELSE NULL::numeric + END AS availability_pct, + r.telemetry_samples, + r.telemetry_first_ts, + r.telemetry_last_ts, + r.energy_samples, + r.energy_first_ts, + r.energy_last_ts, + r.start_import_kwh, + r.end_import_kwh, + r.kwh_consumed, + r.avg_kw, + r.max_kw, + r.min_cycle_pulse_count, + r.max_cycle_pulse_count, + r.real_cycle_delta, + r.avg_cycle_rate_ppm, + r.avg_nonzero_cycle_rate_ppm, + CASE + WHEN (r.real_cycle_delta > 0) THEN round((r.kwh_consumed / (NULLIF(r.real_cycle_delta, 0))::numeric), 6) + ELSE NULL::numeric + END AS kwh_per_cycle, + CASE + WHEN (GREATEST((r.window_min - r.stop_min), (0)::numeric) > (0)::numeric) THEN round(((r.real_cycle_delta)::numeric / NULLIF(GREATEST((r.window_min - r.stop_min), (0)::numeric), (0)::numeric)), 2) + ELSE NULL::numeric + END AS cycles_per_run_min, + r.stop_count, + r.stop_min, + r.classified_stops, + r.pending_stops, + r.stop_cost_eur, + COALESCE(sc.stop_causes_summary, 'Sin paros imputados'::text) AS stop_causes_summary, + COALESCE(sc.stop_causes_json, '[]'::jsonb) AS stop_causes_json, + COALESCE(sj.stops_json, '[]'::jsonb) AS stops_json, + COALESCE(tj.timeline_json, '[]'::jsonb) AS timeline_json, + q.expected_samples_10s, + q.telemetry_sample_coverage_pct, + q.telemetry_gap_count_30s, + q.max_sample_gap_s, + q.avg_sample_gap_s, + q.non_ok_samples, + q.production_conversion_status, + q.technical_quality_score, + q.technical_quality_level, + r.report_status, + r.report_status_description, + q.recommended_action, + CASE + WHEN ((r.report_status = 'CERRADO_CALCULABLE'::text) AND (q.technical_quality_level = 'FIABLE'::text) AND (q.production_conversion_status = 'CICLOS_COHERENTES_CON_OBJETIVO'::text)) THEN 'LISTO_INFORME_FINAL'::text + WHEN ((r.report_status = 'CERRADO_CALCULABLE'::text) AND (q.technical_quality_level = 'FIABLE'::text) AND (q.production_conversion_status = 'REVISAR_CONVERSION_ODOO_CICLOS'::text)) THEN 'LISTO_TECNICO_REVISAR_CONVERSION'::text + WHEN ((r.report_status = 'CERRADO_CALCULABLE'::text) AND (q.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text]))) THEN 'LISTO_CON_OBSERVACIONES'::text + WHEN (r.report_status = 'PARCIAL_ORDEN_EN_CURSO'::text) THEN 'INFORME_PARCIAL_ORDEN_EN_CURSO'::text + WHEN (r.report_status = 'SIN_TELEMETRIA_EN_VENTANA'::text) THEN 'BLOQUEADO_SIN_TELEMETRIA'::text + WHEN (r.report_status = 'SIN_ENERGIA_EN_VENTANA'::text) THEN 'BLOQUEADO_SIN_ENERGIA'::text + WHEN (r.pending_stops > 0) THEN 'PENDIENTE_CLASIFICAR_PAROS'::text + ELSE 'REVISAR'::text + END AS final_report_status, + concat('Orden ', r.odoo_order_ref, ' en ', r.machine_id, ': duración ', r.window_min, ' min; ciclos reales ', r.real_cycle_delta, '; energía ', COALESCE((r.kwh_consumed)::text, 'sin dato'::text), ' kWh; paros ', r.stop_count, ' (', r.stop_min, ' min); coste estimado ', r.stop_cost_eur, ' €. Estado técnico: ', q.technical_quality_level, '.') AS executive_summary + FROM ((((mv_reports_ucepsa_demo.order_execution_report_v1 r + LEFT JOIN mv_reports_ucepsa_demo.order_execution_data_quality_v1 q ON ((q.order_row_id = r.order_row_id))) + LEFT JOIN stop_causes_json sc ON ((sc.order_row_id = r.order_row_id))) + LEFT JOIN stops_json sj ON ((sj.order_row_id = r.order_row_id))) + LEFT JOIN timeline_json tj ON ((tj.order_row_id = r.order_row_id))); + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict YbA0VcRIrZ1kSDSxIf4zctoL9eu8TYoJ5cvkbSD5WkFTQ1jRyH26snWSZehz5VJ + + +GRANT USAGE ON SCHEMA mv_reports_ucepsa_demo TO grafana_ucepsa_ro; + +GRANT SELECT ON mv_reports_ucepsa_demo.order_execution_report_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON mv_reports_ucepsa_demo.order_execution_stops_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON mv_reports_ucepsa_demo.order_execution_stop_causes_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON mv_reports_ucepsa_demo.order_execution_timeline_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON mv_reports_ucepsa_demo.order_execution_energy_timeseries_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON mv_reports_ucepsa_demo.order_execution_data_quality_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON mv_reports_ucepsa_demo.order_execution_report_full_v1 TO grafana_ucepsa_ro;