DROP VIEW IF EXISTS mv_reports_ucepsa_demo.odoo_discrepancies_demo; DROP VIEW IF EXISTS mv_reports_ucepsa_demo.odoo_vs_machine_demo; DROP VIEW IF EXISTS mv_reports_ucepsa_demo.odoo_current_order_demo; 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_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, CASE WHEN o.odoo_state IN ('progress', 'in_progress', 'started') THEN o.odoo_start ELSE NULL::timestamptz END AS odoo_start, o.odoo_start AS odoo_reference_start, CASE WHEN o.odoo_state IN ('progress', 'in_progress', 'started') THEN true ELSE false END AS odoo_is_started, 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_instance = 'odoo_demo' AND o.odoo_workorder_id IS NOT NULL AND o.odoo_state IN ('progress', 'in_progress', 'started', 'ready', 'waiting') ORDER BY CASE WHEN o.odoo_state IN ('progress', 'in_progress', 'started') THEN 1 WHEN o.odoo_state = 'ready' THEN 2 WHEN o.odoo_state = 'waiting' THEN 3 ELSE 9 END, o.odoo_workorder_id DESC NULLS LAST 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 current_order o LEFT JOIN mv_reports_ucepsa_demo.live_telemetry l ON o.odoo_is_started = true AND l.asset = o.asset AND l.ingest_ts >= o.odoo_start AND l.ingest_ts <= COALESCE(o.odoo_end, now()) AND ( l.machine_running = true OR COALESCE(l.cycle_pulse_count, 0) > 0 ) ), stops_in_order AS ( SELECT COUNT(s.id) 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 current_order o LEFT JOIN mv_reports_ucepsa_demo.stops_recent s ON o.odoo_is_started = true AND 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 current_order o LEFT JOIN mv_reports_ucepsa_demo.live_telemetry l ON o.odoo_is_started = true AND 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_workorder_id, o.odoo_production_id, o.odoo_order_ref, o.odoo_state, o.odoo_is_started, o.product, o.machine_id, o.asset, o.odoo_start, o.odoo_reference_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 o.odoo_is_started = false THEN NULL::numeric WHEN f.first_machine_activity IS NULL THEN NULL::numeric 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, CASE WHEN o.odoo_is_started = true THEN c.real_cycles ELSE 0::numeric END AS real_cycles, o.planned_cycles, CASE WHEN o.odoo_is_started = true AND o.planned_cycles > 0 THEN ROUND(((c.real_cycles / o.planned_cycles) * 100)::numeric, 2) WHEN o.odoo_is_started = false THEN 0::numeric ELSE NULL::numeric END AS cycle_completion_pct, CASE WHEN m.age_s > 30 THEN 'SIN_TELEMETRIA' WHEN o.odoo_is_started = false AND m.machine_running = true THEN 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA' WHEN o.odoo_is_started = false AND COALESCE(m.machine_running, false) = false THEN 'ORDEN_PREPARADA_MAQUINA_PARADA' WHEN o.odoo_is_started = true AND f.first_machine_activity IS NULL THEN 'ORDEN_ACTIVA_SIN_ACTIVIDAD_REAL' WHEN o.odoo_is_started = true AND m.machine_running = false THEN 'ORDEN_ACTIVA_MAQUINA_PARADA' WHEN o.odoo_is_started = true AND 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, odoo_state, odoo_is_started, status_check, CASE WHEN status_check = 'SIN_TELEMETRIA' THEN 'No hay telemetría reciente de la RevPi.' WHEN status_check = 'MAQUINA_EN_MARCHA_SIN_ORDEN_INICIADA' THEN 'La máquina está en marcha, pero la orden de Odoo está preparada o en espera, no iniciada.' WHEN status_check = 'ORDEN_PREPARADA_MAQUINA_PARADA' THEN 'La orden está preparada/en espera y la máquina está parada. No hay desviación productiva activa.' 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 = 'ORDEN_ACTIVA_MAQUINA_PARADA' THEN 'Odoo tiene una orden activa, pero la máquina aparece parada.' 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_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, edge_oee_odoo_sync_writer;