CREATE TABLE IF NOT EXISTS mv_edge_oee_ucepsa_demo.odoo_workorders_demo ( id BIGSERIAL PRIMARY KEY, tenant TEXT NOT NULL DEFAULT 'ucepsa', site TEXT NOT NULL DEFAULT 'demo_edge_oee', odoo_instance TEXT NOT NULL DEFAULT 'odoo_demo', odoo_workorder_id INTEGER, odoo_production_id INTEGER, odoo_order_ref TEXT NOT NULL, odoo_state TEXT NOT NULL, odoo_workcenter_code TEXT NOT NULL, odoo_workcenter_name TEXT NOT NULL, machine_id TEXT NOT NULL, asset TEXT NOT NULL, product TEXT NOT NULL, qty_planned NUMERIC(14,2), qty_done NUMERIC(14,2), uom TEXT, planned_cycles NUMERIC(14,2), target_cycle_rate_ppm NUMERIC(10,2), odoo_start TIMESTAMPTZ NOT NULL, odoo_end TIMESTAMPTZ, notes TEXT, snapshot_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_odoo_workorders_demo_machine_time ON mv_edge_oee_ucepsa_demo.odoo_workorders_demo(machine_id, odoo_start DESC); CREATE INDEX IF NOT EXISTS idx_odoo_workorders_demo_state ON mv_edge_oee_ucepsa_demo.odoo_workorders_demo(odoo_state); DELETE FROM mv_edge_oee_ucepsa_demo.odoo_workorders_demo WHERE tenant = 'ucepsa' AND site = 'demo_edge_oee' AND odoo_instance = 'odoo_demo'; INSERT INTO mv_edge_oee_ucepsa_demo.odoo_workorders_demo ( odoo_order_ref, odoo_state, odoo_workcenter_code, odoo_workcenter_name, machine_id, asset, product, qty_planned, qty_done, uom, planned_cycles, target_cycle_rate_ppm, odoo_start, odoo_end, notes ) VALUES ( 'OF-DEMO-001', 'in_progress', 'CORT-01', 'Cortadora 01', 'CORT-01', 'revpi_oee_node_01', 'Bolsa 300x400', 50000, 0, 'unidades', 500, 10, now() - interval '20 minutes', now() + interval '40 minutes', 'Orden demo activa para cruce Odoo vs máquina.' ), ( 'OF-DEMO-002', 'planned', 'CORT-01', 'Cortadora 01', 'CORT-01', 'revpi_oee_node_01', 'Bolsa 250x350', 42000, 0, 'unidades', 420, 10, now() + interval '60 minutes', now() + interval '120 minutes', 'Orden demo planificada.' ), ( 'OF-DEMO-000', 'done', 'CORT-01', 'Cortadora 01', 'CORT-01', 'revpi_oee_node_01', 'Bolsa 300x400', 52000, 51000, 'unidades', 520, 10, now() - interval '3 hours', now() - interval '2 hours', 'Orden demo ya cerrada.' ); CREATE OR REPLACE VIEW mv_reports_ucepsa_demo.odoo_current_order_demo AS SELECT o.id, o.tenant, o.site, o.odoo_instance, 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.odoo_start, o.odoo_end, o.notes, o.snapshot_at FROM mv_edge_oee_ucepsa_demo.odoo_workorders_demo o WHERE o.machine_id = 'CORT-01' AND o.odoo_state IN ('in_progress', 'progress', 'started') ORDER BY o.odoo_start DESC LIMIT 1; CREATE OR REPLACE VIEW mv_reports_ucepsa_demo.odoo_vs_machine_demo AS WITH current_order AS ( SELECT * FROM mv_reports_ucepsa_demo.odoo_current_order_demo ), machine AS ( SELECT * FROM mv_reports_ucepsa_demo.latest_machine_state_fast ), first_real_activity AS ( SELECT MIN(l.ingest_ts) AS first_machine_activity FROM mv_reports_ucepsa_demo.live_telemetry l CROSS JOIN current_order o WHERE l.asset = o.asset AND l.ingest_ts >= o.odoo_start AND ( l.machine_running = true OR COALESCE(l.cycle_pulse_count, 0) > 0 ) ), stops_in_order AS ( SELECT COUNT(*) AS stop_count, ROUND(COALESCE(SUM(s.duration_min), 0)::numeric, 2) AS stop_min, ROUND(COALESCE(SUM(s.cost_eur), 0)::numeric, 2) AS stop_cost_eur FROM mv_reports_ucepsa_demo.stops_recent s CROSS JOIN current_order o WHERE s.machine_id = o.machine_id AND s.started_at >= o.odoo_start AND s.started_at <= COALESCE(o.odoo_end, now()) ), cycles_in_order AS ( SELECT COALESCE( GREATEST(MAX(l.cycle_pulse_count) - MIN(l.cycle_pulse_count), 0), 0 )::numeric AS real_cycles FROM mv_reports_ucepsa_demo.live_telemetry l CROSS JOIN current_order o WHERE l.asset = o.asset AND l.ingest_ts >= o.odoo_start AND l.ingest_ts <= COALESCE(o.odoo_end, now()) AND l.cycle_pulse_count IS NOT NULL ) SELECT o.odoo_order_ref, o.odoo_state, o.product, o.machine_id, o.asset, o.odoo_start, o.odoo_end, m.machine_running, m.age_s AS telemetry_age_s, m.cycle_pulse_count, m.cycle_rate_ppm, f.first_machine_activity, CASE WHEN f.first_machine_activity IS NULL THEN NULL ELSE ROUND((EXTRACT(EPOCH FROM (f.first_machine_activity - o.odoo_start)) / 60.0)::numeric, 2) END AS start_delay_min, s.stop_count, s.stop_min, s.stop_cost_eur, c.real_cycles, o.planned_cycles, CASE WHEN o.planned_cycles > 0 THEN ROUND(((c.real_cycles / o.planned_cycles) * 100)::numeric, 2) ELSE NULL END AS cycle_completion_pct, CASE WHEN m.age_s > 30 THEN 'SIN_TELEMETRIA' WHEN o.odoo_state IN ('in_progress', 'progress', 'started') AND m.machine_running = false THEN 'ORDEN_ACTIVA_MAQUINA_PARADA' WHEN o.odoo_state IN ('in_progress', 'progress', 'started') AND f.first_machine_activity IS NULL THEN 'ORDEN_ACTIVA_SIN_ACTIVIDAD_REAL' WHEN f.first_machine_activity IS NOT NULL AND EXTRACT(EPOCH FROM (f.first_machine_activity - o.odoo_start)) > 600 THEN 'ARRANQUE_REAL_TARDIO' ELSE 'OK' END AS status_check FROM current_order o LEFT JOIN machine m ON m.asset = o.asset LEFT JOIN first_real_activity f ON true LEFT JOIN stops_in_order s ON true LEFT JOIN cycles_in_order c ON true; CREATE OR REPLACE VIEW mv_reports_ucepsa_demo.odoo_discrepancies_demo AS SELECT now() AS checked_at, odoo_order_ref, product, machine_id, status_check, CASE WHEN status_check = 'SIN_TELEMETRIA' THEN 'No hay telemetría reciente de la RevPi.' WHEN status_check = 'ORDEN_ACTIVA_MAQUINA_PARADA' THEN 'Odoo tiene una orden activa, pero la máquina aparece parada.' WHEN status_check = 'ORDEN_ACTIVA_SIN_ACTIVIDAD_REAL' THEN 'Odoo tiene una orden activa, pero no se detecta actividad real de máquina.' WHEN status_check = 'ARRANQUE_REAL_TARDIO' THEN 'La primera actividad real de máquina llega más de 10 minutos después del inicio Odoo.' ELSE 'Sin discrepancia relevante.' END AS descripcion, start_delay_min, stop_count, stop_min, stop_cost_eur, cycle_completion_pct FROM mv_reports_ucepsa_demo.odoo_vs_machine_demo; GRANT SELECT ON mv_edge_oee_ucepsa_demo.odoo_workorders_demo, mv_reports_ucepsa_demo.odoo_current_order_demo, mv_reports_ucepsa_demo.odoo_vs_machine_demo, mv_reports_ucepsa_demo.odoo_discrepancies_demo TO grafana_ucepsa_ro;