Add UCEPSA order execution report SQL views

This commit is contained in:
Victor Fraile Garcia 2026-05-28 22:26:40 +02:00
parent 90a0da730b
commit 0ed753587b

View File

@ -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;