feat(ucepsa): add power-aware LossIntelligence shadow v0.2
This commit is contained in:
parent
2d198e5ddb
commit
4586e29afe
@ -0,0 +1,56 @@
|
||||
# UCEPSA LossIntelligence SHADOW v0.2 — elegibilidad power-aware
|
||||
|
||||
## Problema corregido
|
||||
|
||||
En UCEPSA, cuando una cortadora no tiene trabajo se corta su alimentación. El último evento puede ser `machine_stopped` y el paro raw permanece abierto o se cierra varios días después. Esa duración física no equivale a pérdida.
|
||||
|
||||
La v0.2 calcula tiempo elegible mediante la intersección:
|
||||
|
||||
```text
|
||||
paro raw
|
||||
∩ intervalo de orden Odoo válida
|
||||
∩ isla de telemetría real
|
||||
= tiempo candidato a pérdida
|
||||
```
|
||||
|
||||
Cuando desaparece la alimentación, dejan de llegar muestras y la isla de telemetría termina. El tiempo posterior no entra en LossIntelligence.
|
||||
|
||||
## Umbrales iniciales
|
||||
|
||||
- Frecuencia nominal de captura observada: alrededor de 5 s.
|
||||
- Un hueco mayor de 90 s abre una nueva isla de telemetría.
|
||||
- Se toleran 30 s después de la última muestra válida.
|
||||
- Menos de 5 s sigue siendo ruido.
|
||||
- Entre 5 s y 5 min se agrupa.
|
||||
- A partir de 5 min se muestra individualmente.
|
||||
|
||||
Los valores son de modo sombra y deben revisarse antes del kickoff de octubre.
|
||||
|
||||
## Vistas nuevas
|
||||
|
||||
- `li_telemetry_coverage_v1`
|
||||
- `li_stop_order_overlaps_v2`
|
||||
- `li_stop_eligible_slices_v2`
|
||||
- `li_stop_candidates_v2`
|
||||
- `li_stop_exclusion_diagnostics_v2`
|
||||
- `li_stop_groups_v2`
|
||||
- `li_shadow_review_queue_v2`
|
||||
- `li_operator_stop_queue_v2`
|
||||
- `li_shadow_summary_v2`
|
||||
- `li_eligibility_comparison_v1`
|
||||
- `machine_operational_state_v1`
|
||||
- `li_data_quality_v2`
|
||||
|
||||
## Regla de interfaz
|
||||
|
||||
Una máquina sin telemetría y sin orden debe mostrarse como `SIN PRODUCCIÓN`, no como `NO OK`.
|
||||
|
||||
Estados que sí requieren atención:
|
||||
|
||||
- máquina en marcha sin orden válida;
|
||||
- orden válida sin telemetría;
|
||||
- telemetría inválida o comunicación fallida con orden activa.
|
||||
|
||||
## Activación
|
||||
|
||||
La v0.2 no cambia `deployment_config.mode`. Permanece `SHADOW` y la cola oficial v2 debe devolver cero filas.
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,73 @@
|
||||
\pset pager off
|
||||
\echo '=== 1. Política telemetry-aware ==='
|
||||
SELECT policy_code, require_telemetry_coverage, telemetry_gap_s, telemetry_tail_s,
|
||||
noise_below_s, operator_individual_from_s
|
||||
FROM mv_loss_intelligence.loss_policies
|
||||
WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise' AND policy_code = 'CUTTER_STOPS_V1';
|
||||
|
||||
\echo '=== 2. Estado operativo power-aware ==='
|
||||
SELECT machine_id, operational_state, machine_state_label, telemetry_fresh,
|
||||
round(age_s::numeric, 1) AS age_s, machine_running,
|
||||
order_present, order_coherent, production_order
|
||||
FROM mv_reports_ucepsa_prod.machine_operational_state_v1
|
||||
ORDER BY machine_id;
|
||||
|
||||
\echo '=== 3. Islas de telemetría más recientes ==='
|
||||
SELECT machine_id, coverage_from, coverage_to, sample_count, sampled_span_s
|
||||
FROM mv_reports_ucepsa_prod.li_telemetry_coverage_v1
|
||||
ORDER BY coverage_to DESC
|
||||
LIMIT 15;
|
||||
|
||||
\echo '=== 4. Invariantes temporales: debe devolver 0 ==='
|
||||
SELECT COUNT(*) AS invalid_slice_rows
|
||||
FROM mv_reports_ucepsa_prod.li_stop_eligible_slices_v2
|
||||
WHERE eligible_from < order_overlap_from
|
||||
OR eligible_to > order_overlap_to
|
||||
OR eligible_from < telemetry_coverage_from
|
||||
OR eligible_to > telemetry_coverage_to
|
||||
OR eligible_to <= eligible_from
|
||||
OR eligible_duration_s <= 0;
|
||||
|
||||
\echo '=== 5. Duración elegible nunca supera la raw: debe devolver 0 ==='
|
||||
SELECT COUNT(*) AS duration_overflow_rows
|
||||
FROM mv_reports_ucepsa_prod.li_stop_candidates_v2
|
||||
WHERE eligible_duration_s > raw_duration_s + 0.01
|
||||
OR eligible_duration_s > order_overlap_duration_s + 0.01;
|
||||
|
||||
\echo '=== 6. Comparación v0.1 frente a v0.2 ==='
|
||||
SELECT * FROM mv_reports_ucepsa_prod.li_eligibility_comparison_v1;
|
||||
|
||||
\echo '=== 7. Resumen v0.2 ==='
|
||||
SELECT *
|
||||
FROM mv_reports_ucepsa_prod.li_shadow_summary_v2
|
||||
ORDER BY machine_id, eligibility_status;
|
||||
|
||||
\echo '=== 8. Primeros candidatos power-aware ==='
|
||||
SELECT shadow_rank, machine_id, order_ref, product_name, event_count,
|
||||
started_at, ended_at, duration_min, capacity_loss_eur,
|
||||
order_clipped, telemetry_clipped, crosses_order_boundary, queue_status
|
||||
FROM mv_reports_ucepsa_prod.li_shadow_review_queue_v2
|
||||
ORDER BY shadow_rank
|
||||
LIMIT 25;
|
||||
|
||||
\echo '=== 9. Diagnóstico de exclusiones ==='
|
||||
SELECT diagnostic_status, machine_id, COUNT(*) AS stop_count,
|
||||
ROUND(SUM(raw_duration_min)::numeric, 2) AS raw_minutes
|
||||
FROM mv_reports_ucepsa_prod.li_stop_exclusion_diagnostics_v2
|
||||
GROUP BY diagnostic_status, machine_id
|
||||
ORDER BY machine_id, diagnostic_status;
|
||||
|
||||
\echo '=== 10. Cola oficial v2: debe estar vacía en SHADOW ==='
|
||||
SELECT COUNT(*) AS official_operator_queue_v2_rows
|
||||
FROM mv_reports_ucepsa_prod.li_operator_stop_queue_v2;
|
||||
|
||||
\echo '=== 11. Calidad de dato v2 ==='
|
||||
SELECT * FROM mv_reports_ucepsa_prod.li_data_quality_v2
|
||||
ORDER BY check_code;
|
||||
|
||||
\echo '=== 12. Candidatos v2 de más de 12 h para revisión manual ==='
|
||||
SELECT shadow_rank, machine_id, order_ref, started_at, ended_at,
|
||||
duration_min, telemetry_clipped, order_clipped
|
||||
FROM mv_reports_ucepsa_prod.li_shadow_review_queue_v2
|
||||
WHERE duration_min >= 720
|
||||
ORDER BY duration_min DESC;
|
||||
@ -0,0 +1,687 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE mv_loss_intelligence.loss_policies
|
||||
ADD COLUMN IF NOT EXISTS require_telemetry_coverage BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS telemetry_gap_s NUMERIC(12,2) NOT NULL DEFAULT 90
|
||||
CHECK (telemetry_gap_s > 0),
|
||||
ADD COLUMN IF NOT EXISTS telemetry_tail_s NUMERIC(12,2) NOT NULL DEFAULT 30
|
||||
CHECK (telemetry_tail_s >= 0);
|
||||
|
||||
UPDATE mv_loss_intelligence.loss_policies
|
||||
SET require_telemetry_coverage = true,
|
||||
telemetry_gap_s = 90,
|
||||
telemetry_tail_s = 30,
|
||||
notes = concat_ws(' ', notes,
|
||||
'v0.2: la pérdida elegible se limita a la intersección entre paro raw, orden Odoo válida y cobertura real de telemetría. Un hueco >90 s abre una nueva isla; se toleran 30 s tras la última muestra.'),
|
||||
updated_at = now()
|
||||
WHERE tenant = 'ucepsa'
|
||||
AND site = 'ucepsa_onpremise'
|
||||
AND policy_code = 'CUTTER_STOPS_V1';
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_telemetry_coverage_v1 AS
|
||||
WITH policy AS (
|
||||
SELECT
|
||||
COALESCE(telemetry_gap_s, 90)::double precision AS telemetry_gap_s,
|
||||
COALESCE(telemetry_tail_s, 30)::double precision AS telemetry_tail_s
|
||||
FROM mv_loss_intelligence.loss_policies
|
||||
WHERE tenant = 'ucepsa'
|
||||
AND site = 'ucepsa_onpremise'
|
||||
AND policy_code = 'CUTTER_STOPS_V1'
|
||||
AND is_active = true
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
), samples AS (
|
||||
SELECT
|
||||
r.tenant,
|
||||
r.site,
|
||||
r.machine_id,
|
||||
r.ingest_ts,
|
||||
r.comm_ok,
|
||||
CASE
|
||||
WHEN lower(COALESCE(r.payload_json ->> 'sample_valid', 'true'))
|
||||
IN ('false', 'f', '0', 'no') THEN false
|
||||
ELSE true
|
||||
END AS sample_valid,
|
||||
p.telemetry_gap_s,
|
||||
p.telemetry_tail_s,
|
||||
lag(r.ingest_ts) OVER (
|
||||
PARTITION BY r.tenant, r.site, r.machine_id
|
||||
ORDER BY r.ingest_ts
|
||||
) AS previous_ingest_ts
|
||||
FROM mv_hot.edge_oee_eastron_readings r
|
||||
CROSS JOIN policy p
|
||||
WHERE r.tenant = 'ucepsa'
|
||||
AND r.site = 'ucepsa_onpremise'
|
||||
AND r.machine_id IS NOT NULL
|
||||
AND r.comm_ok = true
|
||||
AND CASE
|
||||
WHEN lower(COALESCE(r.payload_json ->> 'sample_valid', 'true'))
|
||||
IN ('false', 'f', '0', 'no') THEN false
|
||||
ELSE true
|
||||
END = true
|
||||
), marked AS (
|
||||
SELECT
|
||||
s.*,
|
||||
CASE
|
||||
WHEN previous_ingest_ts IS NULL THEN 1
|
||||
WHEN ingest_ts - previous_ingest_ts
|
||||
> make_interval(secs => telemetry_gap_s) THEN 1
|
||||
ELSE 0
|
||||
END AS starts_new_island
|
||||
FROM samples s
|
||||
), islanded AS (
|
||||
SELECT
|
||||
m.*,
|
||||
sum(starts_new_island) OVER (
|
||||
PARTITION BY tenant, site, machine_id
|
||||
ORDER BY ingest_ts
|
||||
ROWS UNBOUNDED PRECEDING
|
||||
) AS telemetry_island_id
|
||||
FROM marked m
|
||||
)
|
||||
SELECT
|
||||
tenant,
|
||||
site,
|
||||
machine_id,
|
||||
telemetry_island_id,
|
||||
min(ingest_ts) AS coverage_from,
|
||||
max(ingest_ts) + make_interval(secs => max(telemetry_tail_s)) AS coverage_to,
|
||||
count(*) AS sample_count,
|
||||
round(EXTRACT(EPOCH FROM (max(ingest_ts) - min(ingest_ts)))::numeric, 2) AS sampled_span_s,
|
||||
max(telemetry_gap_s)::numeric(12,2) AS telemetry_gap_s,
|
||||
max(telemetry_tail_s)::numeric(12,2) AS telemetry_tail_s
|
||||
FROM islanded
|
||||
GROUP BY tenant, site, machine_id, telemetry_island_id;
|
||||
|
||||
COMMENT ON VIEW mv_reports_ucepsa_prod.li_telemetry_coverage_v1 IS
|
||||
'Islas de cobertura real de telemetría válida. Cuando una cortadora pierde alimentación, la isla termina y el tiempo posterior no puede computarse como pérdida.';
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_stop_order_overlaps_v2 AS
|
||||
WITH raw_stops AS (
|
||||
SELECT
|
||||
s.*,
|
||||
COALESCE(s.ended_at, now()) AS raw_effective_end,
|
||||
EXTRACT(EPOCH FROM (COALESCE(s.ended_at, now()) - s.started_at))::numeric AS raw_duration_s
|
||||
FROM mv_hot.edge_oee_machine_stops s
|
||||
WHERE s.tenant = 'ucepsa'
|
||||
AND s.site = 'ucepsa_onpremise'
|
||||
AND COALESCE(s.ended_at, now()) > s.started_at
|
||||
)
|
||||
SELECT
|
||||
s.id AS source_stop_id,
|
||||
s.tenant,
|
||||
s.site,
|
||||
s.vertical,
|
||||
s.asset,
|
||||
s.machine_id,
|
||||
s.order_id AS source_order_id,
|
||||
s.started_at AS raw_started_at,
|
||||
s.ended_at AS raw_ended_at,
|
||||
s.raw_effective_end,
|
||||
s.raw_duration_s,
|
||||
s.status AS source_status,
|
||||
s.classification_status,
|
||||
s.cause_code,
|
||||
s.operator_note,
|
||||
s.value_hour_eur,
|
||||
s.start_event_name,
|
||||
s.end_event_name,
|
||||
s.start_payload_json,
|
||||
s.end_payload_json,
|
||||
o.id AS order_state_event_id,
|
||||
o.effective_from AS order_context_from,
|
||||
o.effective_to AS order_context_to,
|
||||
o.odoo_state,
|
||||
o.order_present,
|
||||
o.order_coherent,
|
||||
o.workorder_id AS odoo_workorder_id,
|
||||
o.workorder_name,
|
||||
o.workorder_state,
|
||||
o.production_id AS odoo_production_id,
|
||||
o.production_order AS order_ref,
|
||||
o.product_id,
|
||||
o.product AS product_name,
|
||||
o.qty_production,
|
||||
o.date_start AS odoo_date_start,
|
||||
o.date_finished AS odoo_date_finished,
|
||||
GREATEST(s.started_at, o.effective_from) AS order_overlap_from,
|
||||
LEAST(
|
||||
s.raw_effective_end,
|
||||
COALESCE(o.effective_to, s.raw_effective_end)
|
||||
) AS order_overlap_to,
|
||||
EXTRACT(EPOCH FROM (
|
||||
LEAST(s.raw_effective_end, COALESCE(o.effective_to, s.raw_effective_end))
|
||||
- GREATEST(s.started_at, o.effective_from)
|
||||
))::numeric AS order_overlap_duration_s
|
||||
FROM raw_stops s
|
||||
JOIN mv_reports_ucepsa_prod.li_order_state_intervals_v1 o
|
||||
ON o.tenant = s.tenant
|
||||
AND o.site = s.site
|
||||
AND o.machine_id = s.machine_id
|
||||
AND o.order_present = true
|
||||
AND o.order_coherent = true
|
||||
AND s.raw_effective_end > o.effective_from
|
||||
AND s.started_at < COALESCE(o.effective_to, s.raw_effective_end)
|
||||
WHERE LEAST(s.raw_effective_end, COALESCE(o.effective_to, s.raw_effective_end))
|
||||
> GREATEST(s.started_at, o.effective_from);
|
||||
|
||||
COMMENT ON VIEW mv_reports_ucepsa_prod.li_stop_order_overlaps_v2 IS
|
||||
'Segmenta un paro raw por cada intervalo de orden Odoo válida que solapa. Un paro que atraviesa cambios de orden deja de atribuirse íntegramente a la orden inicial.';
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_stop_eligible_slices_v2 AS
|
||||
SELECT
|
||||
o.*,
|
||||
t.telemetry_island_id,
|
||||
t.coverage_from AS telemetry_coverage_from,
|
||||
t.coverage_to AS telemetry_coverage_to,
|
||||
GREATEST(o.order_overlap_from, t.coverage_from) AS eligible_from,
|
||||
LEAST(o.order_overlap_to, t.coverage_to) AS eligible_to,
|
||||
EXTRACT(EPOCH FROM (
|
||||
LEAST(o.order_overlap_to, t.coverage_to)
|
||||
- GREATEST(o.order_overlap_from, t.coverage_from)
|
||||
))::numeric AS eligible_duration_s
|
||||
FROM mv_reports_ucepsa_prod.li_stop_order_overlaps_v2 o
|
||||
JOIN mv_reports_ucepsa_prod.li_telemetry_coverage_v1 t
|
||||
ON t.tenant = o.tenant
|
||||
AND t.site = o.site
|
||||
AND t.machine_id = o.machine_id
|
||||
AND o.order_overlap_to > t.coverage_from
|
||||
AND o.order_overlap_from < t.coverage_to
|
||||
WHERE LEAST(o.order_overlap_to, t.coverage_to)
|
||||
> GREATEST(o.order_overlap_from, t.coverage_from);
|
||||
|
||||
COMMENT ON VIEW mv_reports_ucepsa_prod.li_stop_eligible_slices_v2 IS
|
||||
'Intersección exacta entre paro raw, orden Odoo válida y telemetría disponible. Es la unidad temporal elegible de LossIntelligence v0.2.';
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_stop_candidates_v2 AS
|
||||
WITH slice_summary AS (
|
||||
SELECT
|
||||
source_stop_id,
|
||||
tenant,
|
||||
site,
|
||||
vertical,
|
||||
asset,
|
||||
machine_id,
|
||||
raw_started_at,
|
||||
raw_ended_at,
|
||||
raw_effective_end,
|
||||
raw_duration_s,
|
||||
source_status,
|
||||
classification_status,
|
||||
cause_code,
|
||||
operator_note,
|
||||
value_hour_eur,
|
||||
start_event_name,
|
||||
end_event_name,
|
||||
start_payload_json,
|
||||
end_payload_json,
|
||||
order_state_event_id,
|
||||
order_context_from,
|
||||
order_context_to,
|
||||
odoo_state,
|
||||
order_present,
|
||||
order_coherent,
|
||||
odoo_workorder_id,
|
||||
workorder_name,
|
||||
workorder_state,
|
||||
odoo_production_id,
|
||||
order_ref,
|
||||
product_id,
|
||||
product_name,
|
||||
qty_production,
|
||||
odoo_date_start,
|
||||
odoo_date_finished,
|
||||
order_overlap_from,
|
||||
order_overlap_to,
|
||||
order_overlap_duration_s,
|
||||
count(*) AS telemetry_slice_count,
|
||||
min(eligible_from) AS eligible_from,
|
||||
max(eligible_to) AS eligible_to,
|
||||
sum(eligible_duration_s)::numeric AS eligible_duration_s,
|
||||
min(telemetry_coverage_from) AS first_telemetry_coverage_from,
|
||||
max(telemetry_coverage_to) AS last_telemetry_coverage_to
|
||||
FROM mv_reports_ucepsa_prod.li_stop_eligible_slices_v2
|
||||
GROUP BY
|
||||
source_stop_id, tenant, site, vertical, asset, machine_id,
|
||||
raw_started_at, raw_ended_at, raw_effective_end, raw_duration_s,
|
||||
source_status, classification_status, cause_code, operator_note,
|
||||
value_hour_eur, start_event_name, end_event_name,
|
||||
start_payload_json, end_payload_json,
|
||||
order_state_event_id, order_context_from, order_context_to,
|
||||
odoo_state, order_present, order_coherent, odoo_workorder_id,
|
||||
workorder_name, workorder_state, odoo_production_id, order_ref,
|
||||
product_id, product_name, qty_production, odoo_date_start,
|
||||
odoo_date_finished, order_overlap_from, order_overlap_to,
|
||||
order_overlap_duration_s
|
||||
), order_counts AS (
|
||||
SELECT source_stop_id, count(*) AS valid_order_overlap_count
|
||||
FROM mv_reports_ucepsa_prod.li_stop_order_overlaps_v2
|
||||
GROUP BY source_stop_id
|
||||
)
|
||||
SELECT
|
||||
s.source_stop_id,
|
||||
s.tenant,
|
||||
s.site,
|
||||
d.id AS deployment_id,
|
||||
d.scope_code,
|
||||
d.mode AS deployment_mode,
|
||||
d.planned_go_live_date,
|
||||
d.actual_go_live_at,
|
||||
CASE
|
||||
WHEN d.mode = 'ACTIVE'
|
||||
AND d.actual_go_live_at IS NOT NULL
|
||||
AND s.eligible_from >= d.actual_go_live_at THEN 'ACTIVE'
|
||||
ELSE 'SHADOW'
|
||||
END AS ledger_phase,
|
||||
s.machine_id,
|
||||
s.asset,
|
||||
s.raw_started_at,
|
||||
s.raw_ended_at,
|
||||
s.raw_duration_s,
|
||||
s.order_overlap_from,
|
||||
s.order_overlap_to,
|
||||
s.order_overlap_duration_s,
|
||||
s.eligible_from AS started_at,
|
||||
s.eligible_to AS ended_at,
|
||||
s.eligible_duration_s,
|
||||
ROUND((s.eligible_duration_s / 60.0)::numeric, 2) AS eligible_duration_min,
|
||||
GREATEST(s.raw_duration_s - s.eligible_duration_s, 0)::numeric AS excluded_duration_s,
|
||||
ROUND((GREATEST(s.raw_duration_s - s.eligible_duration_s, 0) / 60.0)::numeric, 2) AS excluded_duration_min,
|
||||
ROUND((s.eligible_duration_s / NULLIF(s.raw_duration_s, 0))::numeric, 6) AS raw_coverage_ratio,
|
||||
ROUND((s.eligible_duration_s / NULLIF(s.order_overlap_duration_s, 0))::numeric, 6) AS order_telemetry_coverage_ratio,
|
||||
(s.order_overlap_duration_s < s.raw_duration_s - 1) AS order_clipped,
|
||||
(s.eligible_duration_s < s.order_overlap_duration_s - 1) AS telemetry_clipped,
|
||||
(COALESCE(oc.valid_order_overlap_count, 0) > 1) AS crosses_order_boundary,
|
||||
s.telemetry_slice_count,
|
||||
s.first_telemetry_coverage_from,
|
||||
s.last_telemetry_coverage_to,
|
||||
s.source_status,
|
||||
s.classification_status,
|
||||
s.cause_code,
|
||||
c.cause_name,
|
||||
c.is_planned,
|
||||
s.operator_note,
|
||||
s.start_event_name,
|
||||
s.end_event_name,
|
||||
s.order_state_event_id,
|
||||
s.order_context_from,
|
||||
s.order_context_to,
|
||||
s.odoo_state,
|
||||
s.order_present,
|
||||
s.order_coherent,
|
||||
s.odoo_workorder_id,
|
||||
s.workorder_name,
|
||||
s.workorder_state,
|
||||
s.odoo_production_id,
|
||||
s.order_ref,
|
||||
s.product_id,
|
||||
s.product_name,
|
||||
s.qty_production,
|
||||
s.odoo_date_start,
|
||||
s.odoo_date_finished,
|
||||
pc.units_per_cycle,
|
||||
pc.target_cycle_rate_ppm,
|
||||
COALESCE(ep.parameter_value, s.value_hour_eur) AS applied_capacity_eur_h,
|
||||
ep.confidence AS capacity_cost_confidence,
|
||||
ROUND((
|
||||
s.eligible_duration_s / 3600.0
|
||||
* COALESCE(ep.parameter_value, s.value_hour_eur)
|
||||
)::numeric, 2) AS eligible_capacity_cost_eur,
|
||||
CASE
|
||||
WHEN s.source_status = 'OPEN' THEN 'OPEN_MONITORING'
|
||||
WHEN s.eligible_duration_s < p.noise_below_s THEN 'EXCLUDED_NOISE'
|
||||
WHEN s.eligible_duration_s < p.operator_individual_from_s THEN 'CANDIDATE_GROUP'
|
||||
ELSE 'CANDIDATE_INDIVIDUAL'
|
||||
END AS eligibility_status,
|
||||
CASE
|
||||
WHEN s.source_status = 'CLOSED'
|
||||
AND s.eligible_duration_s >= p.noise_below_s THEN true
|
||||
ELSE false
|
||||
END AS technically_eligible,
|
||||
CASE
|
||||
WHEN d.mode = 'ACTIVE'
|
||||
AND d.actual_go_live_at IS NOT NULL
|
||||
AND s.eligible_from >= d.actual_go_live_at
|
||||
AND s.source_status = 'CLOSED'
|
||||
AND s.eligible_duration_s >= p.noise_below_s THEN true
|
||||
ELSE false
|
||||
END AS official_ledger_eligible,
|
||||
jsonb_build_object(
|
||||
'source_stop_id', s.source_stop_id,
|
||||
'raw_started_at', s.raw_started_at,
|
||||
'raw_ended_at', s.raw_ended_at,
|
||||
'raw_duration_s', s.raw_duration_s,
|
||||
'order_overlap_from', s.order_overlap_from,
|
||||
'order_overlap_to', s.order_overlap_to,
|
||||
'order_overlap_duration_s', s.order_overlap_duration_s,
|
||||
'eligible_duration_s', s.eligible_duration_s,
|
||||
'telemetry_slice_count', s.telemetry_slice_count,
|
||||
'order_clipped', (s.order_overlap_duration_s < s.raw_duration_s - 1),
|
||||
'telemetry_clipped', (s.eligible_duration_s < s.order_overlap_duration_s - 1),
|
||||
'crosses_order_boundary', (COALESCE(oc.valid_order_overlap_count, 0) > 1),
|
||||
'source_start_payload', s.start_payload_json,
|
||||
'source_end_payload', s.end_payload_json
|
||||
) AS evidence_json
|
||||
FROM slice_summary s
|
||||
LEFT JOIN order_counts oc ON oc.source_stop_id = s.source_stop_id
|
||||
LEFT JOIN mv_hot.edge_oee_stop_causes c ON c.cause_code = s.cause_code
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT x.*
|
||||
FROM mv_loss_intelligence.deployment_config x
|
||||
WHERE x.tenant = s.tenant
|
||||
AND x.site = s.site
|
||||
AND x.scope_code = 'CUTTERS'
|
||||
ORDER BY x.id DESC
|
||||
LIMIT 1
|
||||
) d ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT x.*
|
||||
FROM mv_loss_intelligence.loss_policies x
|
||||
WHERE x.tenant = s.tenant
|
||||
AND x.site = s.site
|
||||
AND x.policy_code = 'CUTTER_STOPS_V1'
|
||||
AND x.is_active = true
|
||||
ORDER BY x.id DESC
|
||||
LIMIT 1
|
||||
) p ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT x.*
|
||||
FROM mv_loss_intelligence.product_cycle_parameters x
|
||||
WHERE x.tenant = s.tenant
|
||||
AND x.site = s.site
|
||||
AND x.is_active = true
|
||||
AND x.product_id = s.product_id
|
||||
AND (x.machine_id IS NULL OR x.machine_id = s.machine_id)
|
||||
AND s.eligible_from >= x.valid_from
|
||||
AND (x.valid_to IS NULL OR s.eligible_from < x.valid_to)
|
||||
ORDER BY (x.machine_id IS NOT NULL) DESC, x.valid_from DESC, x.id DESC
|
||||
LIMIT 1
|
||||
) pc ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT x.*
|
||||
FROM mv_loss_intelligence.economic_parameters x
|
||||
WHERE x.tenant = s.tenant
|
||||
AND x.site = s.site
|
||||
AND x.parameter_code = 'CAPACITY_HOUR_EUR'
|
||||
AND x.is_active = true
|
||||
AND (x.machine_id IS NULL OR x.machine_id = s.machine_id)
|
||||
AND s.eligible_from >= x.valid_from
|
||||
AND (x.valid_to IS NULL OR s.eligible_from < x.valid_to)
|
||||
ORDER BY (x.machine_id IS NOT NULL) DESC, x.valid_from DESC, x.id DESC
|
||||
LIMIT 1
|
||||
) ep ON true;
|
||||
|
||||
COMMENT ON VIEW mv_reports_ucepsa_prod.li_stop_candidates_v2 IS
|
||||
'Candidatos power-aware: la duración económica solo incluye tiempo con orden válida y telemetría real. Las noches y periodos sin alimentación dejan de inflar el Loss Ledger.';
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_stop_exclusion_diagnostics_v2 AS
|
||||
WITH raw AS (
|
||||
SELECT
|
||||
s.*,
|
||||
COALESCE(s.ended_at, now()) AS raw_effective_end,
|
||||
EXTRACT(EPOCH FROM (COALESCE(s.ended_at, now()) - s.started_at))::numeric AS raw_duration_s
|
||||
FROM mv_hot.edge_oee_machine_stops s
|
||||
WHERE s.tenant = 'ucepsa'
|
||||
AND s.site = 'ucepsa_onpremise'
|
||||
AND COALESCE(s.ended_at, now()) > s.started_at
|
||||
), order_counts AS (
|
||||
SELECT source_stop_id, count(*) AS n
|
||||
FROM mv_reports_ucepsa_prod.li_stop_order_overlaps_v2
|
||||
GROUP BY source_stop_id
|
||||
), slice_counts AS (
|
||||
SELECT source_stop_id, count(*) AS n
|
||||
FROM mv_reports_ucepsa_prod.li_stop_eligible_slices_v2
|
||||
GROUP BY source_stop_id
|
||||
)
|
||||
SELECT
|
||||
r.id AS source_stop_id,
|
||||
r.machine_id,
|
||||
r.started_at,
|
||||
r.ended_at,
|
||||
r.status AS source_status,
|
||||
ROUND((r.raw_duration_s / 60.0)::numeric, 2) AS raw_duration_min,
|
||||
CASE
|
||||
WHEN r.status = 'OPEN' THEN 'OPEN_MONITORING'
|
||||
WHEN COALESCE(o.n, 0) = 0 THEN 'EXCLUDED_NO_VALID_ORDER_WINDOW'
|
||||
WHEN COALESCE(t.n, 0) = 0 THEN 'EXCLUDED_NO_TELEMETRY_COVERAGE'
|
||||
ELSE 'SEGMENTED_IN_V2'
|
||||
END AS diagnostic_status,
|
||||
COALESCE(o.n, 0) AS valid_order_overlap_count,
|
||||
COALESCE(t.n, 0) AS telemetry_slice_count
|
||||
FROM raw r
|
||||
LEFT JOIN order_counts o ON o.source_stop_id = r.id
|
||||
LEFT JOIN slice_counts t ON t.source_stop_id = r.id;
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_stop_groups_v2 AS
|
||||
WITH candidates AS (
|
||||
SELECT
|
||||
c.*,
|
||||
COALESCE(p.aggregation_window_min, 15) AS aggregation_window_min
|
||||
FROM mv_reports_ucepsa_prod.li_stop_candidates_v2 c
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT x.*
|
||||
FROM mv_loss_intelligence.loss_policies x
|
||||
WHERE x.tenant = c.tenant
|
||||
AND x.site = c.site
|
||||
AND x.policy_code = 'CUTTER_STOPS_V1'
|
||||
AND x.is_active = true
|
||||
ORDER BY x.id DESC
|
||||
LIMIT 1
|
||||
) p ON true
|
||||
WHERE c.eligibility_status IN ('CANDIDATE_GROUP', 'CANDIDATE_INDIVIDUAL')
|
||||
), bucketed AS (
|
||||
SELECT
|
||||
c.*,
|
||||
date_bin(
|
||||
make_interval(mins => c.aggregation_window_min),
|
||||
c.started_at,
|
||||
TIMESTAMPTZ '2000-01-01 00:00:00+00'
|
||||
) AS bucket_start
|
||||
FROM candidates c
|
||||
), grouped_micro AS (
|
||||
SELECT
|
||||
tenant,
|
||||
site,
|
||||
deployment_id,
|
||||
ledger_phase,
|
||||
machine_id,
|
||||
asset,
|
||||
order_ref,
|
||||
odoo_production_id,
|
||||
NULLIF(odoo_workorder_id, '')::bigint AS odoo_workorder_id,
|
||||
product_id,
|
||||
product_name,
|
||||
units_per_cycle,
|
||||
target_cycle_rate_ppm,
|
||||
COALESCE(cause_code, 'PENDING') AS cause_code,
|
||||
COALESCE(cause_name, 'Pendiente de clasificar') AS cause_name,
|
||||
classification_status,
|
||||
bucket_start AS group_window_start,
|
||||
COUNT(DISTINCT source_stop_id)::int AS event_count,
|
||||
MIN(started_at) AS started_at,
|
||||
MAX(ended_at) AS ended_at,
|
||||
SUM(eligible_duration_s)::numeric AS duration_s,
|
||||
ROUND(SUM(eligible_capacity_cost_eur)::numeric, 2) AS capacity_loss_eur,
|
||||
bool_or(order_clipped) AS order_clipped,
|
||||
bool_or(telemetry_clipped) AS telemetry_clipped,
|
||||
bool_or(crosses_order_boundary) AS crosses_order_boundary,
|
||||
array_agg(DISTINCT source_stop_id ORDER BY source_stop_id) AS source_stop_ids,
|
||||
bool_and(official_ledger_eligible) AS official_ledger_eligible
|
||||
FROM bucketed
|
||||
WHERE eligibility_status = 'CANDIDATE_GROUP'
|
||||
GROUP BY
|
||||
tenant, site, deployment_id, ledger_phase, machine_id, asset,
|
||||
order_ref, odoo_production_id, odoo_workorder_id, product_id,
|
||||
product_name, units_per_cycle, target_cycle_rate_ppm,
|
||||
COALESCE(cause_code, 'PENDING'),
|
||||
COALESCE(cause_name, 'Pendiente de clasificar'),
|
||||
classification_status, bucket_start
|
||||
), individual AS (
|
||||
SELECT
|
||||
tenant,
|
||||
site,
|
||||
deployment_id,
|
||||
ledger_phase,
|
||||
machine_id,
|
||||
asset,
|
||||
order_ref,
|
||||
odoo_production_id,
|
||||
NULLIF(odoo_workorder_id, '')::bigint AS odoo_workorder_id,
|
||||
product_id,
|
||||
product_name,
|
||||
units_per_cycle,
|
||||
target_cycle_rate_ppm,
|
||||
COALESCE(cause_code, 'PENDING') AS cause_code,
|
||||
COALESCE(cause_name, 'Pendiente de clasificar') AS cause_name,
|
||||
classification_status,
|
||||
started_at AS group_window_start,
|
||||
1::int AS event_count,
|
||||
started_at,
|
||||
ended_at,
|
||||
eligible_duration_s::numeric AS duration_s,
|
||||
eligible_capacity_cost_eur::numeric AS capacity_loss_eur,
|
||||
order_clipped,
|
||||
telemetry_clipped,
|
||||
crosses_order_boundary,
|
||||
ARRAY[source_stop_id]::bigint[] AS source_stop_ids,
|
||||
official_ledger_eligible
|
||||
FROM bucketed
|
||||
WHERE eligibility_status = 'CANDIDATE_INDIVIDUAL'
|
||||
), combined AS (
|
||||
SELECT * FROM grouped_micro
|
||||
UNION ALL
|
||||
SELECT * FROM individual
|
||||
)
|
||||
SELECT
|
||||
CASE
|
||||
WHEN event_count = 1 THEN 'STOPV2:' || source_stop_ids[1]::text || ':'
|
||||
|| COALESCE(odoo_production_id::text, COALESCE(order_ref, 'NO_ORDER'))
|
||||
ELSE 'STOPGROUPV2:' || machine_id || ':'
|
||||
|| COALESCE(odoo_production_id::text, COALESCE(order_ref, 'NO_ORDER')) || ':'
|
||||
|| to_char(group_window_start AT TIME ZONE 'Europe/Madrid', 'YYYYMMDDHH24MI') || ':'
|
||||
|| cause_code
|
||||
END AS source_key,
|
||||
*,
|
||||
ROUND((duration_s / 60.0)::numeric, 2) AS duration_min,
|
||||
CASE
|
||||
WHEN duration_s >= 900 THEN 'HIGH'
|
||||
WHEN duration_s >= 300 THEN 'MEDIUM'
|
||||
ELSE 'LOW'
|
||||
END AS preliminary_priority
|
||||
FROM combined;
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_shadow_review_queue_v2 AS
|
||||
SELECT
|
||||
row_number() OVER (
|
||||
ORDER BY capacity_loss_eur DESC, duration_s DESC, started_at DESC
|
||||
) AS shadow_rank,
|
||||
source_key,
|
||||
machine_id,
|
||||
order_ref,
|
||||
product_name,
|
||||
cause_code,
|
||||
cause_name,
|
||||
event_count,
|
||||
started_at,
|
||||
ended_at,
|
||||
duration_min,
|
||||
capacity_loss_eur,
|
||||
preliminary_priority,
|
||||
order_clipped,
|
||||
telemetry_clipped,
|
||||
crosses_order_boundary,
|
||||
'SHADOW_ONLY_NOT_OFFICIAL'::text AS queue_status,
|
||||
official_ledger_eligible,
|
||||
source_stop_ids
|
||||
FROM mv_reports_ucepsa_prod.li_stop_groups_v2
|
||||
WHERE ledger_phase = 'SHADOW'
|
||||
ORDER BY shadow_rank;
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_operator_stop_queue_v2 AS
|
||||
SELECT
|
||||
source_key,
|
||||
machine_id,
|
||||
order_ref,
|
||||
product_name,
|
||||
cause_code,
|
||||
cause_name,
|
||||
classification_status,
|
||||
event_count,
|
||||
started_at,
|
||||
ended_at,
|
||||
duration_min,
|
||||
capacity_loss_eur,
|
||||
order_clipped,
|
||||
telemetry_clipped,
|
||||
crosses_order_boundary,
|
||||
source_stop_ids
|
||||
FROM mv_reports_ucepsa_prod.li_stop_groups_v2
|
||||
WHERE ledger_phase = 'ACTIVE'
|
||||
AND official_ledger_eligible = true
|
||||
AND classification_status = 'PENDING'
|
||||
ORDER BY capacity_loss_eur DESC, duration_s DESC, started_at DESC;
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_shadow_summary_v2 AS
|
||||
SELECT
|
||||
ledger_phase,
|
||||
eligibility_status,
|
||||
machine_id,
|
||||
COUNT(*) AS candidate_segment_count,
|
||||
COUNT(DISTINCT source_stop_id) AS source_stop_count,
|
||||
ROUND(SUM(eligible_duration_s / 60.0)::numeric, 2) AS eligible_minutes,
|
||||
ROUND(SUM(eligible_capacity_cost_eur)::numeric, 2) AS capacity_cost_eur
|
||||
FROM mv_reports_ucepsa_prod.li_stop_candidates_v2
|
||||
GROUP BY ledger_phase, eligibility_status, machine_id;
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_eligibility_comparison_v1 AS
|
||||
WITH old_calc AS (
|
||||
SELECT
|
||||
machine_id,
|
||||
COUNT(*) FILTER (WHERE eligibility_status IN ('CANDIDATE_GROUP', 'CANDIDATE_INDIVIDUAL')) AS v1_candidate_rows,
|
||||
SUM(effective_duration_s) FILTER (WHERE eligibility_status IN ('CANDIDATE_GROUP', 'CANDIDATE_INDIVIDUAL')) AS v1_candidate_s,
|
||||
SUM(effective_capacity_cost_eur) FILTER (WHERE eligibility_status IN ('CANDIDATE_GROUP', 'CANDIDATE_INDIVIDUAL')) AS v1_candidate_eur
|
||||
FROM mv_reports_ucepsa_prod.li_stop_candidates_v1
|
||||
GROUP BY machine_id
|
||||
), new_calc AS (
|
||||
SELECT
|
||||
machine_id,
|
||||
COUNT(*) FILTER (WHERE eligibility_status IN ('CANDIDATE_GROUP', 'CANDIDATE_INDIVIDUAL')) AS v2_candidate_rows,
|
||||
COUNT(DISTINCT source_stop_id) FILTER (WHERE eligibility_status IN ('CANDIDATE_GROUP', 'CANDIDATE_INDIVIDUAL')) AS v2_source_stops,
|
||||
SUM(eligible_duration_s) FILTER (WHERE eligibility_status IN ('CANDIDATE_GROUP', 'CANDIDATE_INDIVIDUAL')) AS v2_candidate_s,
|
||||
SUM(eligible_capacity_cost_eur) FILTER (WHERE eligibility_status IN ('CANDIDATE_GROUP', 'CANDIDATE_INDIVIDUAL')) AS v2_candidate_eur
|
||||
FROM mv_reports_ucepsa_prod.li_stop_candidates_v2
|
||||
GROUP BY machine_id
|
||||
)
|
||||
SELECT
|
||||
COALESCE(o.machine_id, n.machine_id) AS machine_id,
|
||||
COALESCE(o.v1_candidate_rows, 0) AS v1_candidate_rows,
|
||||
ROUND((COALESCE(o.v1_candidate_s, 0) / 60.0)::numeric, 2) AS v1_candidate_minutes,
|
||||
ROUND(COALESCE(o.v1_candidate_eur, 0)::numeric, 2) AS v1_candidate_eur,
|
||||
COALESCE(n.v2_candidate_rows, 0) AS v2_candidate_segments,
|
||||
COALESCE(n.v2_source_stops, 0) AS v2_source_stops,
|
||||
ROUND((COALESCE(n.v2_candidate_s, 0) / 60.0)::numeric, 2) AS v2_candidate_minutes,
|
||||
ROUND(COALESCE(n.v2_candidate_eur, 0)::numeric, 2) AS v2_candidate_eur,
|
||||
ROUND((
|
||||
100.0 * (1 - COALESCE(n.v2_candidate_s, 0) / NULLIF(o.v1_candidate_s, 0))
|
||||
)::numeric, 2) AS removed_time_pct
|
||||
FROM old_calc o
|
||||
FULL OUTER JOIN new_calc n ON n.machine_id = o.machine_id
|
||||
ORDER BY machine_id;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ucepsa_ro') THEN
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_telemetry_coverage_v1 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_stop_order_overlaps_v2 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_stop_eligible_slices_v2 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_stop_candidates_v2 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_stop_exclusion_diagnostics_v2 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_stop_groups_v2 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_shadow_review_queue_v2 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_operator_stop_queue_v2 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_shadow_summary_v2 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_eligibility_comparison_v1 TO grafana_ucepsa_ro;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@ -0,0 +1,204 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.machine_operational_state_v1 AS
|
||||
WITH latest_telemetry AS (
|
||||
SELECT DISTINCT ON (r.machine_id)
|
||||
r.tenant,
|
||||
r.site,
|
||||
r.machine_id,
|
||||
r.asset,
|
||||
r.ingest_ts,
|
||||
EXTRACT(EPOCH FROM (now() - r.ingest_ts))::numeric AS age_s,
|
||||
r.comm_ok,
|
||||
CASE
|
||||
WHEN lower(COALESCE(r.payload_json ->> 'sample_valid', 'true'))
|
||||
IN ('false', 'f', '0', 'no') THEN false
|
||||
ELSE true
|
||||
END AS sample_valid,
|
||||
CASE
|
||||
WHEN lower(COALESCE(r.payload_json ->> 'machine_running', 'false'))
|
||||
IN ('true', 't', '1', 'yes') THEN true
|
||||
ELSE false
|
||||
END AS machine_running,
|
||||
COALESCE(NULLIF(r.payload_json ->> 'cycle_rate_ppm', '')::numeric, 0) AS cycle_rate_ppm,
|
||||
NULLIF(r.payload_json ->> 'cycle_pulse_count', '')::bigint AS cycle_pulse_count,
|
||||
r.power_total_kw,
|
||||
r.import_kwh,
|
||||
r.frequency_hz,
|
||||
r.payload_json
|
||||
FROM mv_hot.edge_oee_eastron_readings r
|
||||
WHERE r.tenant = 'ucepsa'
|
||||
AND r.site = 'ucepsa_onpremise'
|
||||
AND r.machine_id IS NOT NULL
|
||||
ORDER BY r.machine_id, r.ingest_ts DESC
|
||||
), machines AS (
|
||||
SELECT machine_id FROM latest_telemetry
|
||||
UNION
|
||||
SELECT machine_id
|
||||
FROM mv_hot.odoo_order_state_current
|
||||
WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise'
|
||||
)
|
||||
SELECT
|
||||
m.machine_id,
|
||||
t.asset,
|
||||
t.ingest_ts AS last_telemetry_at,
|
||||
t.age_s,
|
||||
t.comm_ok,
|
||||
t.sample_valid,
|
||||
(COALESCE(t.age_s, 999999999) < 30
|
||||
AND COALESCE(t.comm_ok, false)
|
||||
AND COALESCE(t.sample_valid, false)) AS telemetry_fresh,
|
||||
t.machine_running,
|
||||
t.cycle_rate_ppm,
|
||||
t.cycle_pulse_count,
|
||||
t.power_total_kw,
|
||||
t.import_kwh,
|
||||
t.frequency_hz,
|
||||
o.received_at AS order_state_received_at,
|
||||
o.order_present,
|
||||
o.order_coherent,
|
||||
o.odoo_state,
|
||||
o.production_order,
|
||||
o.product,
|
||||
o.production_id,
|
||||
o.product_id,
|
||||
o.qty_production,
|
||||
CASE
|
||||
WHEN COALESCE(t.age_s, 999999999) < 30
|
||||
AND COALESCE(t.comm_ok, false)
|
||||
AND COALESCE(t.sample_valid, false)
|
||||
AND COALESCE(t.machine_running, false)
|
||||
AND COALESCE(o.order_present, false)
|
||||
AND COALESCE(o.order_coherent, false)
|
||||
THEN 'RUNNING_WITH_ORDER'
|
||||
WHEN COALESCE(t.age_s, 999999999) < 30
|
||||
AND COALESCE(t.comm_ok, false)
|
||||
AND COALESCE(t.sample_valid, false)
|
||||
AND COALESCE(t.machine_running, false)
|
||||
AND NOT (COALESCE(o.order_present, false) AND COALESCE(o.order_coherent, false))
|
||||
THEN 'RUNNING_WITHOUT_VALID_ORDER'
|
||||
WHEN COALESCE(t.age_s, 999999999) < 30
|
||||
AND COALESCE(t.comm_ok, false)
|
||||
AND COALESCE(t.sample_valid, false)
|
||||
AND NOT COALESCE(t.machine_running, false)
|
||||
AND COALESCE(o.order_present, false)
|
||||
AND COALESCE(o.order_coherent, false)
|
||||
THEN 'STOPPED_WITH_ORDER'
|
||||
WHEN COALESCE(t.age_s, 999999999) < 30
|
||||
AND COALESCE(t.comm_ok, false)
|
||||
AND COALESCE(t.sample_valid, false)
|
||||
AND NOT COALESCE(t.machine_running, false)
|
||||
AND NOT (COALESCE(o.order_present, false) AND COALESCE(o.order_coherent, false))
|
||||
THEN 'POWERED_ON_NO_ORDER'
|
||||
WHEN NOT (COALESCE(t.age_s, 999999999) < 30
|
||||
AND COALESCE(t.comm_ok, false)
|
||||
AND COALESCE(t.sample_valid, false))
|
||||
AND COALESCE(o.order_present, false)
|
||||
AND COALESCE(o.order_coherent, false)
|
||||
THEN 'ORDER_WITHOUT_TELEMETRY'
|
||||
ELSE 'POWERED_OFF_NO_ORDER'
|
||||
END AS operational_state,
|
||||
CASE
|
||||
WHEN COALESCE(t.age_s, 999999999) < 30
|
||||
AND COALESCE(t.comm_ok, false)
|
||||
AND COALESCE(t.sample_valid, false) THEN 1
|
||||
WHEN NOT (COALESCE(o.order_present, false) AND COALESCE(o.order_coherent, false)) THEN 0
|
||||
ELSE 2
|
||||
END AS communication_code,
|
||||
CASE
|
||||
WHEN COALESCE(t.age_s, 999999999) < 30
|
||||
AND COALESCE(t.comm_ok, false)
|
||||
AND COALESCE(t.sample_valid, false)
|
||||
AND COALESCE(t.machine_running, false)
|
||||
AND COALESCE(o.order_present, false)
|
||||
AND COALESCE(o.order_coherent, false) THEN 2
|
||||
WHEN COALESCE(t.age_s, 999999999) < 30
|
||||
AND COALESCE(t.comm_ok, false)
|
||||
AND COALESCE(t.sample_valid, false)
|
||||
AND NOT COALESCE(t.machine_running, false)
|
||||
AND COALESCE(o.order_present, false)
|
||||
AND COALESCE(o.order_coherent, false) THEN 1
|
||||
WHEN NOT (COALESCE(o.order_present, false) AND COALESCE(o.order_coherent, false))
|
||||
AND NOT COALESCE(t.machine_running, false) THEN 0
|
||||
ELSE 3
|
||||
END AS machine_state_code,
|
||||
CASE
|
||||
WHEN COALESCE(t.age_s, 999999999) < 30
|
||||
AND COALESCE(t.comm_ok, false)
|
||||
AND COALESCE(t.sample_valid, false)
|
||||
AND COALESCE(t.machine_running, false)
|
||||
AND COALESCE(o.order_present, false)
|
||||
AND COALESCE(o.order_coherent, false) THEN 'EN MARCHA'
|
||||
WHEN COALESCE(t.age_s, 999999999) < 30
|
||||
AND COALESCE(t.comm_ok, false)
|
||||
AND COALESCE(t.sample_valid, false)
|
||||
AND NOT COALESCE(t.machine_running, false)
|
||||
AND COALESCE(o.order_present, false)
|
||||
AND COALESCE(o.order_coherent, false) THEN 'PARADA CON ORDEN'
|
||||
WHEN NOT (COALESCE(o.order_present, false) AND COALESCE(o.order_coherent, false))
|
||||
AND NOT COALESCE(t.machine_running, false) THEN 'SIN PRODUCCIÓN'
|
||||
ELSE 'REVISAR'
|
||||
END AS machine_state_label
|
||||
FROM machines m
|
||||
LEFT JOIN latest_telemetry t ON t.machine_id = m.machine_id
|
||||
LEFT JOIN mv_hot.odoo_order_state_current o
|
||||
ON o.tenant = 'ucepsa'
|
||||
AND o.site = 'ucepsa_onpremise'
|
||||
AND o.machine_id = m.machine_id;
|
||||
|
||||
COMMENT ON VIEW mv_reports_ucepsa_prod.machine_operational_state_v1 IS
|
||||
'Estado power-aware para planta: una máquina sin telemetría y sin orden no se muestra como fallo, sino como SIN PRODUCCIÓN. Una orden sin telemetría o una máquina en marcha sin orden sí exige revisión.';
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_data_quality_v2 AS
|
||||
WITH base AS (
|
||||
SELECT * FROM mv_reports_ucepsa_prod.li_data_quality_v1
|
||||
), historical_cycle_missing AS (
|
||||
SELECT COUNT(*) AS n
|
||||
FROM mv_reports_ucepsa_prod.li_product_cycle_coverage_v1
|
||||
WHERE cycle_parameter_status <> 'READY'
|
||||
), power_off_expected AS (
|
||||
SELECT COUNT(*) AS n
|
||||
FROM mv_reports_ucepsa_prod.machine_operational_state_v1
|
||||
WHERE operational_state = 'POWERED_OFF_NO_ORDER'
|
||||
), running_without_order AS (
|
||||
SELECT COUNT(*) AS n
|
||||
FROM mv_reports_ucepsa_prod.machine_operational_state_v1
|
||||
WHERE operational_state = 'RUNNING_WITHOUT_VALID_ORDER'
|
||||
), order_without_telemetry AS (
|
||||
SELECT COUNT(*) AS n
|
||||
FROM mv_reports_ucepsa_prod.machine_operational_state_v1
|
||||
WHERE operational_state = 'ORDER_WITHOUT_TELEMETRY'
|
||||
)
|
||||
SELECT * FROM base
|
||||
UNION ALL
|
||||
SELECT 'HISTORICAL_CYCLE_PARAMETERS',
|
||||
CASE WHEN n = 0 THEN 'OK' ELSE 'WARNING' END,
|
||||
n,
|
||||
'Productos observados en sombra sin bolsas por golpe o cadencia configurada.'
|
||||
FROM historical_cycle_missing
|
||||
UNION ALL
|
||||
SELECT 'POWERED_OFF_NO_ORDER', 'INFO', n,
|
||||
'Máquinas sin telemetría y sin orden: situación esperada cuando no tienen producción.'
|
||||
FROM power_off_expected
|
||||
UNION ALL
|
||||
SELECT 'RUNNING_WITHOUT_VALID_ORDER',
|
||||
CASE WHEN n = 0 THEN 'OK' ELSE 'WARNING' END,
|
||||
n,
|
||||
'Máquinas produciendo sin orden Odoo válida.'
|
||||
FROM running_without_order
|
||||
UNION ALL
|
||||
SELECT 'ORDER_WITHOUT_TELEMETRY',
|
||||
CASE WHEN n = 0 THEN 'OK' ELSE 'WARNING' END,
|
||||
n,
|
||||
'Orden cargada con máquina o WISE sin telemetría reciente.'
|
||||
FROM order_without_telemetry;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ucepsa_ro') THEN
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.machine_operational_state_v1 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_data_quality_v2 TO grafana_ucepsa_ro;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
Loading…
x
Reference in New Issue
Block a user