feat(ucepsa): add LossIntelligence shadow foundation
This commit is contained in:
parent
88c84f73f5
commit
2d198e5ddb
@ -0,0 +1,120 @@
|
||||
# UCEPSA — Despliegue LossIntelligence en modo sombra hasta octubre de 2026
|
||||
|
||||
## Objetivo
|
||||
|
||||
Construir y validar entre julio y septiembre la infraestructura reusable de MESAVAULT LossIntelligence sin convertir todavía los paros técnicos en pérdidas oficiales. En octubre se realizará el kickoff y se activará el Ledger con una fecha efectiva acordada.
|
||||
|
||||
## Regla de seguridad funcional
|
||||
|
||||
La captura raw no se modifica:
|
||||
|
||||
```text
|
||||
WISE / RevPi / MQTT / PostgreSQL
|
||||
-> evento físico
|
||||
-> paro raw
|
||||
```
|
||||
|
||||
LossIntelligence trabaja encima:
|
||||
|
||||
```text
|
||||
paro raw
|
||||
-> contexto Odoo histórico
|
||||
-> política de elegibilidad
|
||||
-> exclusión / agrupación / candidato
|
||||
-> Loss Ledger
|
||||
-> acción
|
||||
-> prueba
|
||||
-> resultado
|
||||
```
|
||||
|
||||
Un paro raw no equivale automáticamente a pérdida.
|
||||
|
||||
## Estados
|
||||
|
||||
- `SHADOW`: infraestructura activa, resultados solo para validación interna.
|
||||
- `ACTIVE`: Ledger oficial desde `actual_go_live_at`.
|
||||
- `PAUSED`: captura conservada, promoción de nuevas pérdidas detenida.
|
||||
- `CLOSED`: despliegue cerrado.
|
||||
|
||||
Octubre es una fecha prevista, no una activación automática. La activación requiere kickoff y actualización explícita de `actual_go_live_at`.
|
||||
|
||||
## Parámetros iniciales
|
||||
|
||||
- Coste de capacidad: `36 €/h`, provisional y pendiente de aprobación.
|
||||
- Coste eléctrico variable: `0,128249 €/kWh`, derivado de la factura 6.1TD de junio de 2026.
|
||||
- Bolsas por golpe: `1` o `2` según referencia. Debe configurarse por `product_id`.
|
||||
- Cadencia objetivo: pendiente por referencia o familia. Hasta completarla no se activa pérdida por baja cadencia.
|
||||
|
||||
## Política inicial de paros
|
||||
|
||||
| Duración | Tratamiento en sombra |
|
||||
|---|---|
|
||||
| `<5 s` | Se conserva en raw y se excluye de la cola humana. |
|
||||
| `5 s–5 min` | Se agrupa por máquina, orden, ventana de 15 min y causa. |
|
||||
| `>=5 min` | Se mantiene como candidato individual. |
|
||||
| Paro abierto | Solo monitorización; no entra en Ledger. |
|
||||
| Sin orden Odoo válida y coherente | Excluido. |
|
||||
| Antes del go-live | Sombra, nunca pérdida oficial. |
|
||||
|
||||
Los umbrales deben validarse con producción antes del kickoff.
|
||||
|
||||
## Rutina de revisión futura
|
||||
|
||||
La reunión diaria de 15–30 minutos entre dirección y jefe de producción será el punto de revisión. La cola oficial se limitará a cinco elementos:
|
||||
|
||||
1. pérdidas nuevas con mayor prioridad;
|
||||
2. acciones cuyo plazo vence;
|
||||
3. pruebas en curso que requieren decisión;
|
||||
4. bloqueos de calidad de dato;
|
||||
5. resultados que deben mantenerse, adaptarse, repetirse, descartarse o escalarse.
|
||||
|
||||
## Orden de implantación
|
||||
|
||||
1. Crear rama de trabajo en Gitea.
|
||||
2. Tomar backup de PostgreSQL.
|
||||
3. Aplicar migraciones 093–096.
|
||||
4. Validar que la cola oficial está vacía en modo `SHADOW`.
|
||||
5. Actualizar el sink de `order_state` para guardar campos tipados y un hash histórico completo.
|
||||
6. Observar datos durante al menos dos semanas.
|
||||
7. Completar bolsas por golpe de las referencias activas.
|
||||
8. Validar política de microparos y causas con producción.
|
||||
9. Construir dashboard de sombra y panel de calidad de dato.
|
||||
10. Preparar checklist de kickoff.
|
||||
11. En octubre, acordar `actual_go_live_at` y cambiar `mode` a `ACTIVE`.
|
||||
|
||||
## Kill switch
|
||||
|
||||
Para detener la promoción oficial sin tocar la captura:
|
||||
|
||||
```sql
|
||||
UPDATE mv_loss_intelligence.deployment_config
|
||||
SET mode = 'PAUSED', updated_at = now()
|
||||
WHERE tenant = 'ucepsa'
|
||||
AND site = 'ucepsa_onpremise'
|
||||
AND scope_code = 'CUTTERS';
|
||||
```
|
||||
|
||||
## Activación en kickoff
|
||||
|
||||
Ejemplo; sustituir fecha y hora por las acordadas:
|
||||
|
||||
```sql
|
||||
UPDATE mv_loss_intelligence.deployment_config
|
||||
SET
|
||||
mode = 'ACTIVE',
|
||||
actual_go_live_at = TIMESTAMPTZ '2026-10-05 06:00:00+02',
|
||||
updated_at = now()
|
||||
WHERE tenant = 'ucepsa'
|
||||
AND site = 'ucepsa_onpremise'
|
||||
AND scope_code = 'CUTTERS';
|
||||
```
|
||||
|
||||
La activación no debe hacerse antes de confirmar:
|
||||
|
||||
- Shop Floor estable;
|
||||
- órdenes correctamente cargadas por máquina;
|
||||
- causas y umbrales aceptados;
|
||||
- coste/hora revisado;
|
||||
- productos activos con bolsas por golpe;
|
||||
- responsables y reunión diaria confirmados;
|
||||
- backup y rollback disponibles.
|
||||
@ -0,0 +1,53 @@
|
||||
\pset pager off
|
||||
\echo '=== 1. Configuración de despliegue ==='
|
||||
SELECT tenant, site, scope_code, mode, planned_go_live_date, actual_go_live_at,
|
||||
review_owner_primary, review_owner_secondary, daily_review_limit
|
||||
FROM mv_loss_intelligence.deployment_config
|
||||
WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise';
|
||||
|
||||
\echo '=== 2. Parámetros económicos ==='
|
||||
SELECT parameter_code, parameter_value, unit, confidence, includes_energy,
|
||||
valid_from, source_ref, approved_by
|
||||
FROM mv_loss_intelligence.economic_parameters
|
||||
WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise' AND is_active = true
|
||||
ORDER BY parameter_code, valid_from DESC;
|
||||
|
||||
\echo '=== 3. Calidad de dato ==='
|
||||
SELECT * FROM mv_reports_ucepsa_prod.li_data_quality_v1 ORDER BY check_code;
|
||||
|
||||
\echo '=== 4. Intervalos Odoo más recientes ==='
|
||||
SELECT machine_id, effective_from, effective_to, odoo_state,
|
||||
order_present, order_coherent, production_order, product_id, product,
|
||||
qty_production
|
||||
FROM mv_reports_ucepsa_prod.li_order_state_intervals_v1
|
||||
ORDER BY effective_from DESC
|
||||
LIMIT 20;
|
||||
|
||||
\echo '=== 5. Resumen de elegibilidad en sombra ==='
|
||||
SELECT *
|
||||
FROM mv_reports_ucepsa_prod.li_shadow_summary_v1
|
||||
ORDER BY machine_id, eligibility_status;
|
||||
|
||||
\echo '=== 6. Primeros candidatos de sombra ==='
|
||||
SELECT shadow_rank, machine_id, order_ref, product_name, cause_name,
|
||||
event_count, started_at, duration_min, capacity_loss_eur,
|
||||
queue_status
|
||||
FROM mv_reports_ucepsa_prod.li_shadow_review_queue_v1
|
||||
ORDER BY shadow_rank
|
||||
LIMIT 20;
|
||||
|
||||
\echo '=== 7. Cola oficial: debe estar vacía en SHADOW ==='
|
||||
SELECT COUNT(*) AS official_operator_queue_rows
|
||||
FROM mv_reports_ucepsa_prod.li_operator_stop_queue_v1;
|
||||
|
||||
\echo '=== 8. Cobertura bolsas/golpe y cadencia ==='
|
||||
SELECT *
|
||||
FROM mv_reports_ucepsa_prod.li_product_cycle_coverage_v1
|
||||
ORDER BY latest_stop_started_at DESC NULLS LAST;
|
||||
|
||||
\echo '=== 9. Catálogo de acciones ==='
|
||||
SELECT action_id, source_cause_code, action_type, action_candidate,
|
||||
suggested_owner, test_window, success_criteria
|
||||
FROM mv_loss_intelligence.action_catalog
|
||||
WHERE catalog_code = 'LI-CUTTER-STOPS' AND is_active = true
|
||||
ORDER BY source_cause_code, action_id;
|
||||
@ -0,0 +1,93 @@
|
||||
--- /mnt/data/ucepsa_repo/40-clients/ucepsa/edge-oee-demo/tools/order_state_pg_sink.py 2026-07-16 09:11:42.000000000 +0000
|
||||
+++ /mnt/data/ucepsa_lossintelligence_patch/tools/order_state_pg_sink.py 2026-07-16 10:10:45.826794132 +0000
|
||||
@@ -67,8 +67,15 @@
|
||||
"odoo_state": payload.get("odoo_state"),
|
||||
"reason": payload.get("reason"),
|
||||
"workorder_id": payload.get("workorder_id"),
|
||||
+ "production_id": payload.get("production_id"),
|
||||
"production_order": payload.get("production_order"),
|
||||
+ "product_id": payload.get("product_id"),
|
||||
"product": payload.get("product"),
|
||||
+ "qty_production": payload.get("qty_production"),
|
||||
+ "date_start": payload.get("date_start"),
|
||||
+ "date_finished": payload.get("date_finished"),
|
||||
+ "workorder_name": payload.get("workorder_name"),
|
||||
+ "workorder_state": payload.get("workorder_state"),
|
||||
"waiting_count": diagnostics.get("waiting_count"),
|
||||
"ready_count": diagnostics.get("ready_count"),
|
||||
"progress_count": diagnostics.get("progress_count"),
|
||||
@@ -133,8 +140,15 @@
|
||||
"odoo_state": payload.get("odoo_state"),
|
||||
"reason": payload.get("reason"),
|
||||
"workorder_id": str(payload.get("workorder_id")) if payload.get("workorder_id") is not None else None,
|
||||
+ "workorder_name": payload.get("workorder_name"),
|
||||
+ "workorder_state": payload.get("workorder_state"),
|
||||
+ "production_id": to_int(payload.get("production_id")),
|
||||
"production_order": payload.get("production_order"),
|
||||
+ "product_id": to_int(payload.get("product_id")),
|
||||
"product": payload.get("product"),
|
||||
+ "qty_production": payload.get("qty_production"),
|
||||
+ "date_start": parse_ts(payload.get("date_start")),
|
||||
+ "date_finished": parse_ts(payload.get("date_finished")),
|
||||
"waiting_count": to_int(diagnostics.get("waiting_count")),
|
||||
"ready_count": to_int(diagnostics.get("ready_count")),
|
||||
"progress_count": to_int(diagnostics.get("progress_count")),
|
||||
@@ -149,7 +163,9 @@
|
||||
tenant, site, machine_id, topic, mqtt_ts, received_at,
|
||||
source, workcenter_id, workcenter_name, workcenter_display_name,
|
||||
order_present, order_coherent, odoo_state, reason,
|
||||
- workorder_id, production_order, product,
|
||||
+ workorder_id, workorder_name, workorder_state,
|
||||
+ production_id, production_order, product_id, product,
|
||||
+ qty_production, date_start, date_finished,
|
||||
waiting_count, ready_count, progress_count,
|
||||
payload_hash, payload_json
|
||||
)
|
||||
@@ -157,7 +173,9 @@
|
||||
%(tenant)s, %(site)s, %(machine_id)s, %(topic)s, %(mqtt_ts)s, now(),
|
||||
%(source)s, %(workcenter_id)s, %(workcenter_name)s, %(workcenter_display_name)s,
|
||||
%(order_present)s, %(order_coherent)s, %(odoo_state)s, %(reason)s,
|
||||
- %(workorder_id)s, %(production_order)s, %(product)s,
|
||||
+ %(workorder_id)s, %(workorder_name)s, %(workorder_state)s,
|
||||
+ %(production_id)s, %(production_order)s, %(product_id)s, %(product)s,
|
||||
+ %(qty_production)s, %(date_start)s, %(date_finished)s,
|
||||
%(waiting_count)s, %(ready_count)s, %(progress_count)s,
|
||||
%(payload_hash)s, %(payload_json)s
|
||||
)
|
||||
@@ -175,8 +193,15 @@
|
||||
odoo_state = EXCLUDED.odoo_state,
|
||||
reason = EXCLUDED.reason,
|
||||
workorder_id = EXCLUDED.workorder_id,
|
||||
+ workorder_name = EXCLUDED.workorder_name,
|
||||
+ workorder_state = EXCLUDED.workorder_state,
|
||||
+ production_id = EXCLUDED.production_id,
|
||||
production_order = EXCLUDED.production_order,
|
||||
+ product_id = EXCLUDED.product_id,
|
||||
product = EXCLUDED.product,
|
||||
+ qty_production = EXCLUDED.qty_production,
|
||||
+ date_start = EXCLUDED.date_start,
|
||||
+ date_finished = EXCLUDED.date_finished,
|
||||
waiting_count = EXCLUDED.waiting_count,
|
||||
ready_count = EXCLUDED.ready_count,
|
||||
progress_count = EXCLUDED.progress_count,
|
||||
@@ -191,14 +216,18 @@
|
||||
INSERT INTO mv_hot.odoo_order_state_events (
|
||||
tenant, site, machine_id, topic, mqtt_ts, received_at,
|
||||
odoo_state, order_present, order_coherent,
|
||||
- production_order, product, reason,
|
||||
+ workorder_id, workorder_name, workorder_state,
|
||||
+ production_id, production_order, product_id, product,
|
||||
+ qty_production, date_start, date_finished, reason,
|
||||
waiting_count, ready_count, progress_count,
|
||||
payload_hash, payload_json
|
||||
)
|
||||
VALUES (
|
||||
%(tenant)s, %(site)s, %(machine_id)s, %(topic)s, %(mqtt_ts)s, now(),
|
||||
%(odoo_state)s, %(order_present)s, %(order_coherent)s,
|
||||
- %(production_order)s, %(product)s, %(reason)s,
|
||||
+ %(workorder_id)s, %(workorder_name)s, %(workorder_state)s,
|
||||
+ %(production_id)s, %(production_order)s, %(product_id)s, %(product)s,
|
||||
+ %(qty_production)s, %(date_start)s, %(date_finished)s, %(reason)s,
|
||||
%(waiting_count)s, %(ready_count)s, %(progress_count)s,
|
||||
%(payload_hash)s, %(payload_json)s
|
||||
)
|
||||
@ -0,0 +1,263 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS mv_loss_intelligence;
|
||||
|
||||
COMMENT ON SCHEMA mv_loss_intelligence IS
|
||||
'Capa común MESAVAULT LossIntelligence. Conserva configuración, catálogo de acciones, Loss Ledger, pruebas y decisiones. No sustituye las tablas raw de captura.';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.deployment_config (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant TEXT NOT NULL,
|
||||
site TEXT NOT NULL,
|
||||
scope_code TEXT NOT NULL,
|
||||
scope_name TEXT NOT NULL,
|
||||
mode TEXT NOT NULL DEFAULT 'SHADOW'
|
||||
CHECK (mode IN ('SHADOW', 'ACTIVE', 'PAUSED', 'CLOSED')),
|
||||
planned_go_live_date DATE,
|
||||
actual_go_live_at TIMESTAMPTZ,
|
||||
timezone TEXT NOT NULL DEFAULT 'Europe/Madrid',
|
||||
review_owner_primary TEXT,
|
||||
review_owner_secondary TEXT,
|
||||
review_cadence TEXT NOT NULL DEFAULT 'DAILY',
|
||||
daily_review_limit INTEGER NOT NULL DEFAULT 5
|
||||
CHECK (daily_review_limit BETWEEN 1 AND 20),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (tenant, site, scope_code),
|
||||
CHECK (actual_go_live_at IS NULL OR planned_go_live_date IS NULL OR actual_go_live_at::date >= planned_go_live_date - 31)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.economic_parameters (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant TEXT NOT NULL,
|
||||
site TEXT NOT NULL,
|
||||
scope_code TEXT NOT NULL DEFAULT 'GLOBAL',
|
||||
machine_id TEXT,
|
||||
product_id BIGINT,
|
||||
parameter_code TEXT NOT NULL,
|
||||
parameter_value NUMERIC(18,6) NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
confidence TEXT NOT NULL DEFAULT 'LOW'
|
||||
CHECK (confidence IN ('HIGH', 'MEDIUM', 'LOW', 'NOT_ACCEPTED')),
|
||||
includes_energy BOOLEAN,
|
||||
valid_from TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
valid_to TIMESTAMPTZ,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
source_ref TEXT,
|
||||
approved_by TEXT,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (valid_to IS NULL OR valid_to > valid_from)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_li_economic_parameters_lookup
|
||||
ON mv_loss_intelligence.economic_parameters
|
||||
(tenant, site, parameter_code, is_active, valid_from DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.product_cycle_parameters (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant TEXT NOT NULL,
|
||||
site TEXT NOT NULL,
|
||||
machine_id TEXT,
|
||||
product_id BIGINT,
|
||||
product_default_code TEXT,
|
||||
product_name TEXT,
|
||||
units_per_cycle NUMERIC(10,4) NOT NULL
|
||||
CHECK (units_per_cycle > 0),
|
||||
target_cycle_rate_ppm NUMERIC(12,4)
|
||||
CHECK (target_cycle_rate_ppm IS NULL OR target_cycle_rate_ppm > 0),
|
||||
target_units_per_min NUMERIC(14,4)
|
||||
GENERATED ALWAYS AS (
|
||||
CASE
|
||||
WHEN target_cycle_rate_ppm IS NULL THEN NULL
|
||||
ELSE target_cycle_rate_ppm * units_per_cycle
|
||||
END
|
||||
) STORED,
|
||||
confidence TEXT NOT NULL DEFAULT 'LOW'
|
||||
CHECK (confidence IN ('HIGH', 'MEDIUM', 'LOW')),
|
||||
source_ref TEXT,
|
||||
approved_by TEXT,
|
||||
valid_from TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
valid_to TIMESTAMPTZ,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (product_id IS NOT NULL OR product_default_code IS NOT NULL),
|
||||
CHECK (valid_to IS NULL OR valid_to > valid_from)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_li_product_cycle_parameters_lookup
|
||||
ON mv_loss_intelligence.product_cycle_parameters
|
||||
(tenant, site, product_id, product_default_code, machine_id, is_active, valid_from DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.loss_policies (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant TEXT NOT NULL,
|
||||
site TEXT NOT NULL,
|
||||
policy_code TEXT NOT NULL,
|
||||
source_type TEXT NOT NULL DEFAULT 'MACHINE_STOP',
|
||||
noise_below_s NUMERIC(12,2) NOT NULL DEFAULT 5
|
||||
CHECK (noise_below_s >= 0),
|
||||
aggregate_below_s NUMERIC(12,2) NOT NULL DEFAULT 300
|
||||
CHECK (aggregate_below_s >= noise_below_s),
|
||||
operator_individual_from_s NUMERIC(12,2) NOT NULL DEFAULT 300
|
||||
CHECK (operator_individual_from_s >= noise_below_s),
|
||||
aggregation_window_min INTEGER NOT NULL DEFAULT 15
|
||||
CHECK (aggregation_window_min BETWEEN 1 AND 1440),
|
||||
require_valid_order BOOLEAN NOT NULL DEFAULT true,
|
||||
require_closed_event BOOLEAN NOT NULL DEFAULT true,
|
||||
exclude_planned_by_default BOOLEAN NOT NULL DEFAULT true,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (tenant, site, policy_code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.action_catalog (
|
||||
action_id TEXT PRIMARY KEY,
|
||||
catalog_code TEXT NOT NULL,
|
||||
catalog_version TEXT NOT NULL,
|
||||
tenant TEXT,
|
||||
site TEXT,
|
||||
loss_type TEXT NOT NULL,
|
||||
source_cause_code TEXT,
|
||||
action_type TEXT NOT NULL
|
||||
CHECK (action_type IN ('TECHNICAL', 'METHOD', 'ORGANIZATIONAL', 'PROCESS_DESIGN', 'DATA_QUALITY', 'INVESTMENT_DECISION')),
|
||||
action_candidate TEXT NOT NULL,
|
||||
applies_when JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
does_not_apply_when JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
suggested_owner TEXT,
|
||||
test_window TEXT NOT NULL,
|
||||
metric_before TEXT NOT NULL,
|
||||
metric_after TEXT NOT NULL,
|
||||
success_criteria TEXT NOT NULL,
|
||||
risk_level TEXT NOT NULL DEFAULT 'LOW'
|
||||
CHECK (risk_level IN ('LOW', 'MEDIUM', 'HIGH')),
|
||||
cost_to_test TEXT NOT NULL DEFAULT 'LOW'
|
||||
CHECK (cost_to_test IN ('LOW', 'MEDIUM', 'HIGH')),
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
source_ref TEXT,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_li_action_catalog_lookup
|
||||
ON mv_loss_intelligence.action_catalog
|
||||
(loss_type, source_cause_code, is_active);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.loss_ledger (
|
||||
loss_id BIGSERIAL PRIMARY KEY,
|
||||
tenant TEXT NOT NULL,
|
||||
site TEXT NOT NULL,
|
||||
deployment_id BIGINT REFERENCES mv_loss_intelligence.deployment_config(id),
|
||||
ledger_phase TEXT NOT NULL DEFAULT 'SHADOW'
|
||||
CHECK (ledger_phase IN ('SHADOW', 'ACTIVE')),
|
||||
source_type TEXT NOT NULL,
|
||||
source_key TEXT NOT NULL,
|
||||
machine_id TEXT,
|
||||
asset TEXT,
|
||||
order_ref TEXT,
|
||||
odoo_production_id BIGINT,
|
||||
odoo_workorder_id BIGINT,
|
||||
product_id BIGINT,
|
||||
product_default_code TEXT,
|
||||
product_name TEXT,
|
||||
started_at TIMESTAMPTZ,
|
||||
ended_at TIMESTAMPTZ,
|
||||
event_count INTEGER NOT NULL DEFAULT 1 CHECK (event_count > 0),
|
||||
duration_s NUMERIC(18,2) NOT NULL DEFAULT 0 CHECK (duration_s >= 0),
|
||||
cycle_count NUMERIC(18,4),
|
||||
units_per_cycle NUMERIC(10,4),
|
||||
estimated_units NUMERIC(18,4),
|
||||
loss_type TEXT NOT NULL,
|
||||
cause_code TEXT,
|
||||
classification_status TEXT NOT NULL DEFAULT 'PENDING'
|
||||
CHECK (classification_status IN ('PENDING', 'CLASSIFIED', 'EXCLUDED', 'NOT_REQUIRED')),
|
||||
capacity_loss_eur NUMERIC(18,2),
|
||||
energy_waste_kwh NUMERIC(18,6),
|
||||
energy_waste_eur NUMERIC(18,2),
|
||||
technical_confidence TEXT NOT NULL DEFAULT 'MEDIUM'
|
||||
CHECK (technical_confidence IN ('HIGH', 'MEDIUM', 'LOW')),
|
||||
economic_confidence TEXT NOT NULL DEFAULT 'LOW'
|
||||
CHECK (economic_confidence IN ('HIGH', 'MEDIUM', 'LOW', 'NOT_ACCEPTED')),
|
||||
priority_score NUMERIC(12,2),
|
||||
priority_level TEXT
|
||||
CHECK (priority_level IS NULL OR priority_level IN ('LOW', 'MEDIUM', 'HIGH', 'CRITICAL')),
|
||||
action_catalog_id TEXT REFERENCES mv_loss_intelligence.action_catalog(action_id),
|
||||
action_candidate TEXT,
|
||||
action_final TEXT,
|
||||
review_owner TEXT,
|
||||
review_due_at TIMESTAMPTZ,
|
||||
status TEXT NOT NULL DEFAULT 'DETECTED'
|
||||
CHECK (status IN ('DETECTED', 'CANDIDATE', 'IN_REVIEW', 'IN_TRIAL', 'VALIDATED', 'ADAPTED', 'DISCARDED', 'ESCALATED', 'CLOSED')),
|
||||
learning_reusable TEXT,
|
||||
evidence_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
detected_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
promoted_at TIMESTAMPTZ,
|
||||
closed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (tenant, site, source_type, source_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_li_loss_ledger_review
|
||||
ON mv_loss_intelligence.loss_ledger
|
||||
(tenant, site, status, review_due_at, priority_score DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_li_loss_ledger_order_machine
|
||||
ON mv_loss_intelligence.loss_ledger
|
||||
(tenant, site, machine_id, order_ref, started_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.action_trials (
|
||||
trial_id BIGSERIAL PRIMARY KEY,
|
||||
loss_id BIGINT NOT NULL REFERENCES mv_loss_intelligence.loss_ledger(loss_id) ON DELETE CASCADE,
|
||||
trial_code TEXT NOT NULL,
|
||||
action_final TEXT NOT NULL,
|
||||
owner TEXT NOT NULL,
|
||||
baseline_from TIMESTAMPTZ,
|
||||
baseline_to TIMESTAMPTZ,
|
||||
test_from TIMESTAMPTZ,
|
||||
test_to TIMESTAMPTZ,
|
||||
metric_code TEXT NOT NULL,
|
||||
metric_unit TEXT,
|
||||
metric_before NUMERIC(18,6),
|
||||
metric_after NUMERIC(18,6),
|
||||
change_pct NUMERIC(12,4),
|
||||
success_criteria TEXT NOT NULL,
|
||||
result TEXT
|
||||
CHECK (result IS NULL OR result IN ('PENDING', 'SUCCESS', 'PARTIAL', 'NO_CHANGE', 'WORSE', 'INVALID_TEST')),
|
||||
decision TEXT
|
||||
CHECK (decision IS NULL OR decision IN ('KEEP', 'ADAPT', 'REPEAT', 'DISCARD', 'SCALE')),
|
||||
learning TEXT,
|
||||
evidence_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (loss_id, trial_code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.review_decisions (
|
||||
decision_id BIGSERIAL PRIMARY KEY,
|
||||
loss_id BIGINT REFERENCES mv_loss_intelligence.loss_ledger(loss_id) ON DELETE CASCADE,
|
||||
trial_id BIGINT REFERENCES mv_loss_intelligence.action_trials(trial_id) ON DELETE CASCADE,
|
||||
decision_type TEXT NOT NULL
|
||||
CHECK (decision_type IN ('PROMOTE', 'CLASSIFY', 'ASSIGN_ACTION', 'ADAPT_ACTION', 'DEFER', 'EXCLUDE', 'CLOSE', 'ESCALATE')),
|
||||
decision_text TEXT NOT NULL,
|
||||
decided_by TEXT NOT NULL,
|
||||
decided_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
next_review_at TIMESTAMPTZ,
|
||||
evidence_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
CHECK (loss_id IS NOT NULL OR trial_id IS NOT NULL)
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ucepsa_ro') THEN
|
||||
GRANT USAGE ON SCHEMA mv_loss_intelligence TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA mv_loss_intelligence TO grafana_ucepsa_ro;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@ -0,0 +1,99 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE mv_hot.odoo_order_state_current
|
||||
ADD COLUMN IF NOT EXISTS workorder_name TEXT,
|
||||
ADD COLUMN IF NOT EXISTS workorder_state TEXT,
|
||||
ADD COLUMN IF NOT EXISTS production_id BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS product_id BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS qty_production NUMERIC(18,4),
|
||||
ADD COLUMN IF NOT EXISTS date_start TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS date_finished TIMESTAMPTZ;
|
||||
|
||||
ALTER TABLE mv_hot.odoo_order_state_events
|
||||
ADD COLUMN IF NOT EXISTS workorder_id TEXT,
|
||||
ADD COLUMN IF NOT EXISTS workorder_name TEXT,
|
||||
ADD COLUMN IF NOT EXISTS workorder_state TEXT,
|
||||
ADD COLUMN IF NOT EXISTS production_id BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS product_id BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS qty_production NUMERIC(18,4),
|
||||
ADD COLUMN IF NOT EXISTS date_start TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS date_finished TIMESTAMPTZ;
|
||||
|
||||
UPDATE mv_hot.odoo_order_state_current
|
||||
SET
|
||||
workorder_name = COALESCE(workorder_name, NULLIF(payload_json ->> 'workorder_name', '')),
|
||||
workorder_state = COALESCE(workorder_state, NULLIF(payload_json ->> 'workorder_state', '')),
|
||||
production_id = COALESCE(production_id, NULLIF(payload_json ->> 'production_id', '')::bigint),
|
||||
product_id = COALESCE(product_id, NULLIF(payload_json ->> 'product_id', '')::bigint),
|
||||
qty_production = COALESCE(qty_production, NULLIF(payload_json ->> 'qty_production', '')::numeric),
|
||||
date_start = COALESCE(date_start, NULLIF(payload_json ->> 'date_start', '')::timestamptz),
|
||||
date_finished = COALESCE(date_finished, NULLIF(payload_json ->> 'date_finished', '')::timestamptz)
|
||||
WHERE payload_json IS NOT NULL;
|
||||
|
||||
UPDATE mv_hot.odoo_order_state_events
|
||||
SET
|
||||
workorder_id = COALESCE(workorder_id, NULLIF(payload_json ->> 'workorder_id', '')),
|
||||
workorder_name = COALESCE(workorder_name, NULLIF(payload_json ->> 'workorder_name', '')),
|
||||
workorder_state = COALESCE(workorder_state, NULLIF(payload_json ->> 'workorder_state', '')),
|
||||
production_id = COALESCE(production_id, NULLIF(payload_json ->> 'production_id', '')::bigint),
|
||||
product_id = COALESCE(product_id, NULLIF(payload_json ->> 'product_id', '')::bigint),
|
||||
qty_production = COALESCE(qty_production, NULLIF(payload_json ->> 'qty_production', '')::numeric),
|
||||
date_start = COALESCE(date_start, NULLIF(payload_json ->> 'date_start', '')::timestamptz),
|
||||
date_finished = COALESCE(date_finished, NULLIF(payload_json ->> 'date_finished', '')::timestamptz)
|
||||
WHERE payload_json IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_odoo_order_state_events_machine_effective
|
||||
ON mv_hot.odoo_order_state_events
|
||||
(tenant, site, machine_id, COALESCE(mqtt_ts, received_at));
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_order_state_intervals_v1 AS
|
||||
WITH ordered AS (
|
||||
SELECT
|
||||
e.id,
|
||||
e.tenant,
|
||||
e.site,
|
||||
e.machine_id,
|
||||
e.topic,
|
||||
e.mqtt_ts,
|
||||
e.received_at,
|
||||
COALESCE(e.mqtt_ts, e.received_at) AS effective_from,
|
||||
LEAD(COALESCE(e.mqtt_ts, e.received_at)) OVER (
|
||||
PARTITION BY e.tenant, e.site, e.machine_id
|
||||
ORDER BY COALESCE(e.mqtt_ts, e.received_at), e.id
|
||||
) AS effective_to,
|
||||
e.odoo_state,
|
||||
e.order_present,
|
||||
e.order_coherent,
|
||||
e.workorder_id,
|
||||
e.workorder_name,
|
||||
e.workorder_state,
|
||||
e.production_id,
|
||||
e.production_order,
|
||||
e.product_id,
|
||||
e.product,
|
||||
e.qty_production,
|
||||
e.date_start,
|
||||
e.date_finished,
|
||||
e.reason,
|
||||
e.waiting_count,
|
||||
e.ready_count,
|
||||
e.progress_count,
|
||||
e.payload_hash,
|
||||
e.payload_json
|
||||
FROM mv_hot.odoo_order_state_events e
|
||||
WHERE e.site = 'ucepsa_onpremise'
|
||||
)
|
||||
SELECT *
|
||||
FROM ordered;
|
||||
|
||||
COMMENT ON VIEW mv_reports_ucepsa_prod.li_order_state_intervals_v1 IS
|
||||
'Intervalos históricos de estado Odoo por máquina. Permite fijar qué orden estaba activa en el instante de un evento físico.';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ucepsa_ro') THEN
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_order_state_intervals_v1 TO grafana_ucepsa_ro;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@ -0,0 +1,523 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_stop_candidates_v1 AS
|
||||
WITH stop_base AS (
|
||||
SELECT
|
||||
s.*,
|
||||
COALESCE(
|
||||
s.duration_s,
|
||||
EXTRACT(EPOCH FROM (COALESCE(s.ended_at, now()) - s.started_at))::numeric
|
||||
) AS effective_duration_s,
|
||||
COALESCE(
|
||||
s.cost_eur,
|
||||
ROUND((
|
||||
EXTRACT(EPOCH FROM (COALESCE(s.ended_at, now()) - s.started_at))
|
||||
/ 3600.0 * s.value_hour_eur
|
||||
)::numeric, 2)
|
||||
) AS effective_capacity_cost_eur
|
||||
FROM mv_hot.edge_oee_machine_stops s
|
||||
WHERE s.tenant = 'ucepsa'
|
||||
AND s.site = 'ucepsa_onpremise'
|
||||
), enriched AS (
|
||||
SELECT
|
||||
s.*,
|
||||
d.id AS deployment_id,
|
||||
d.scope_code,
|
||||
d.mode AS deployment_mode,
|
||||
d.planned_go_live_date,
|
||||
d.actual_go_live_at,
|
||||
p.noise_below_s,
|
||||
p.aggregate_below_s,
|
||||
p.operator_individual_from_s,
|
||||
p.aggregation_window_min,
|
||||
p.require_valid_order,
|
||||
p.require_closed_event,
|
||||
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,
|
||||
pc.units_per_cycle,
|
||||
pc.target_cycle_rate_ppm,
|
||||
ep.parameter_value AS configured_capacity_eur_h,
|
||||
ep.confidence AS capacity_cost_confidence
|
||||
FROM stop_base s
|
||||
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_reports_ucepsa_prod.li_order_state_intervals_v1 x
|
||||
WHERE x.tenant = s.tenant
|
||||
AND x.site = s.site
|
||||
AND x.machine_id = s.machine_id
|
||||
AND s.started_at >= x.effective_from
|
||||
AND (x.effective_to IS NULL OR s.started_at < x.effective_to)
|
||||
ORDER BY x.effective_from DESC, x.id DESC
|
||||
LIMIT 1
|
||||
) o 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 = o.product_id
|
||||
AND (x.machine_id IS NULL OR x.machine_id = s.machine_id)
|
||||
AND s.started_at >= x.valid_from
|
||||
AND (x.valid_to IS NULL OR s.started_at < 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.started_at >= x.valid_from
|
||||
AND (x.valid_to IS NULL OR s.started_at < x.valid_to)
|
||||
ORDER BY (x.machine_id IS NOT NULL) DESC, x.valid_from DESC, x.id DESC
|
||||
LIMIT 1
|
||||
) ep ON true
|
||||
)
|
||||
SELECT
|
||||
e.id AS source_stop_id,
|
||||
e.tenant,
|
||||
e.site,
|
||||
e.deployment_id,
|
||||
e.scope_code,
|
||||
e.deployment_mode,
|
||||
e.planned_go_live_date,
|
||||
e.actual_go_live_at,
|
||||
CASE
|
||||
WHEN e.deployment_mode = 'ACTIVE'
|
||||
AND e.actual_go_live_at IS NOT NULL
|
||||
AND e.started_at >= e.actual_go_live_at
|
||||
THEN 'ACTIVE'
|
||||
ELSE 'SHADOW'
|
||||
END AS ledger_phase,
|
||||
e.machine_id,
|
||||
e.asset,
|
||||
e.started_at,
|
||||
e.ended_at,
|
||||
e.status AS source_status,
|
||||
e.classification_status,
|
||||
e.cause_code,
|
||||
c.cause_name,
|
||||
c.is_planned,
|
||||
e.operator_note,
|
||||
e.start_event_name,
|
||||
e.end_event_name,
|
||||
e.effective_duration_s,
|
||||
ROUND((e.effective_duration_s / 60.0)::numeric, 2) AS effective_duration_min,
|
||||
e.value_hour_eur AS source_value_hour_eur,
|
||||
e.configured_capacity_eur_h,
|
||||
COALESCE(e.configured_capacity_eur_h, e.value_hour_eur) AS applied_capacity_eur_h,
|
||||
ROUND((
|
||||
e.effective_duration_s / 3600.0
|
||||
* COALESCE(e.configured_capacity_eur_h, e.value_hour_eur)
|
||||
)::numeric, 2) AS effective_capacity_cost_eur,
|
||||
e.capacity_cost_confidence,
|
||||
e.order_state_event_id,
|
||||
e.order_context_from,
|
||||
e.order_context_to,
|
||||
e.odoo_state,
|
||||
e.order_present,
|
||||
e.order_coherent,
|
||||
e.odoo_workorder_id,
|
||||
e.workorder_name,
|
||||
e.workorder_state,
|
||||
e.odoo_production_id,
|
||||
e.order_ref,
|
||||
e.product_id,
|
||||
e.product_name,
|
||||
e.qty_production,
|
||||
e.odoo_date_start,
|
||||
e.odoo_date_finished,
|
||||
e.units_per_cycle,
|
||||
e.target_cycle_rate_ppm,
|
||||
CASE
|
||||
WHEN e.deployment_id IS NULL THEN 'CONFIG_MISSING'
|
||||
WHEN e.noise_below_s IS NULL THEN 'POLICY_MISSING'
|
||||
WHEN e.status = 'OPEN' THEN 'OPEN_MONITORING'
|
||||
WHEN e.require_closed_event AND e.status <> 'CLOSED' THEN 'EXCLUDED_NOT_CLOSED'
|
||||
WHEN e.order_state_event_id IS NULL THEN 'EXCLUDED_NO_ORDER_CONTEXT'
|
||||
WHEN e.require_valid_order AND COALESCE(e.order_present, false) = false THEN 'EXCLUDED_NO_VALID_ORDER'
|
||||
WHEN e.require_valid_order AND COALESCE(e.order_coherent, false) = false THEN 'EXCLUDED_ORDER_INCOHERENT'
|
||||
WHEN e.effective_duration_s < e.noise_below_s THEN 'EXCLUDED_NOISE'
|
||||
WHEN e.effective_duration_s < e.operator_individual_from_s THEN 'CANDIDATE_GROUP'
|
||||
ELSE 'CANDIDATE_INDIVIDUAL'
|
||||
END AS eligibility_status,
|
||||
CASE
|
||||
WHEN e.status = 'CLOSED'
|
||||
AND e.order_state_event_id IS NOT NULL
|
||||
AND (NOT e.require_valid_order OR (e.order_present = true AND e.order_coherent = true))
|
||||
AND e.effective_duration_s >= e.noise_below_s
|
||||
THEN true
|
||||
ELSE false
|
||||
END AS technically_eligible,
|
||||
CASE
|
||||
WHEN e.deployment_mode = 'ACTIVE'
|
||||
AND e.actual_go_live_at IS NOT NULL
|
||||
AND e.started_at >= e.actual_go_live_at
|
||||
AND e.status = 'CLOSED'
|
||||
AND e.order_state_event_id IS NOT NULL
|
||||
AND (NOT e.require_valid_order OR (e.order_present = true AND e.order_coherent = true))
|
||||
AND e.effective_duration_s >= e.noise_below_s
|
||||
THEN true
|
||||
ELSE false
|
||||
END AS official_ledger_eligible,
|
||||
jsonb_build_object(
|
||||
'source_stop_id', e.id,
|
||||
'source_start_payload', e.start_payload_json,
|
||||
'source_end_payload', e.end_payload_json,
|
||||
'order_state_event_id', e.order_state_event_id,
|
||||
'deployment_mode', e.deployment_mode,
|
||||
'policy', jsonb_build_object(
|
||||
'noise_below_s', e.noise_below_s,
|
||||
'aggregate_below_s', e.aggregate_below_s,
|
||||
'operator_individual_from_s', e.operator_individual_from_s,
|
||||
'aggregation_window_min', e.aggregation_window_min
|
||||
)
|
||||
) AS evidence_json
|
||||
FROM enriched e
|
||||
LEFT JOIN mv_hot.edge_oee_stop_causes c
|
||||
ON c.cause_code = e.cause_code;
|
||||
|
||||
COMMENT ON VIEW mv_reports_ucepsa_prod.li_stop_candidates_v1 IS
|
||||
'Clasifica cada paro raw como excluido, agrupable, individual u abierto. Mantiene separados el modo SHADOW y la elegibilidad oficial.';
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_stop_groups_v1 AS
|
||||
WITH candidates AS (
|
||||
SELECT
|
||||
c.*,
|
||||
COALESCE((c.evidence_json #>> '{policy,aggregation_window_min}')::int, 15) AS aggregation_window_min
|
||||
FROM mv_reports_ucepsa_prod.li_stop_candidates_v1 c
|
||||
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(*)::int AS event_count,
|
||||
MIN(started_at) AS started_at,
|
||||
MAX(ended_at) AS ended_at,
|
||||
SUM(effective_duration_s)::numeric AS duration_s,
|
||||
ROUND(SUM(effective_capacity_cost_eur)::numeric, 2) AS capacity_loss_eur,
|
||||
array_agg(source_stop_id ORDER BY started_at) 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,
|
||||
effective_duration_s::numeric AS duration_s,
|
||||
effective_capacity_cost_eur::numeric AS capacity_loss_eur,
|
||||
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 'STOP:' || source_stop_ids[1]::text
|
||||
ELSE 'STOPGROUP:' || 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;
|
||||
|
||||
COMMENT ON VIEW mv_reports_ucepsa_prod.li_stop_groups_v1 IS
|
||||
'Agrupa microparos por máquina, orden, ventana y causa. Los paros largos permanecen individuales.';
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_shadow_review_queue_v1 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,
|
||||
'SHADOW_ONLY_NOT_OFFICIAL'::text AS queue_status,
|
||||
official_ledger_eligible,
|
||||
source_stop_ids
|
||||
FROM mv_reports_ucepsa_prod.li_stop_groups_v1
|
||||
WHERE ledger_phase = 'SHADOW'
|
||||
ORDER BY shadow_rank;
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_operator_stop_queue_v1 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,
|
||||
source_stop_ids
|
||||
FROM mv_reports_ucepsa_prod.li_stop_groups_v1
|
||||
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_daily_review_queue_v1 AS
|
||||
SELECT
|
||||
row_number() OVER (
|
||||
ORDER BY
|
||||
COALESCE(l.priority_score, 0) DESC,
|
||||
l.review_due_at NULLS LAST,
|
||||
l.detected_at
|
||||
) AS review_rank,
|
||||
l.loss_id,
|
||||
l.machine_id,
|
||||
l.order_ref,
|
||||
l.product_name,
|
||||
l.loss_type,
|
||||
l.cause_code,
|
||||
ROUND((l.duration_s / 60.0)::numeric, 2) AS duration_min,
|
||||
l.capacity_loss_eur,
|
||||
l.energy_waste_eur,
|
||||
l.priority_level,
|
||||
l.action_candidate,
|
||||
l.action_final,
|
||||
l.review_owner,
|
||||
l.review_due_at,
|
||||
l.status,
|
||||
t.trial_id,
|
||||
t.trial_code,
|
||||
t.result AS trial_result,
|
||||
t.decision AS trial_decision
|
||||
FROM mv_loss_intelligence.loss_ledger l
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT x.*
|
||||
FROM mv_loss_intelligence.action_trials x
|
||||
WHERE x.loss_id = l.loss_id
|
||||
ORDER BY x.created_at DESC, x.trial_id DESC
|
||||
LIMIT 1
|
||||
) t ON true
|
||||
WHERE l.tenant = 'ucepsa'
|
||||
AND l.site = 'ucepsa_onpremise'
|
||||
AND l.ledger_phase = 'ACTIVE'
|
||||
AND l.status NOT IN ('CLOSED', 'DISCARDED')
|
||||
ORDER BY review_rank
|
||||
LIMIT 5;
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_shadow_summary_v1 AS
|
||||
SELECT
|
||||
ledger_phase,
|
||||
eligibility_status,
|
||||
machine_id,
|
||||
COUNT(*) AS stop_count,
|
||||
ROUND(SUM(effective_duration_s / 60.0)::numeric, 2) AS total_minutes,
|
||||
ROUND(SUM(effective_capacity_cost_eur)::numeric, 2) AS capacity_cost_eur
|
||||
FROM mv_reports_ucepsa_prod.li_stop_candidates_v1
|
||||
GROUP BY ledger_phase, eligibility_status, machine_id;
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_product_cycle_coverage_v1 AS
|
||||
SELECT
|
||||
c.machine_id,
|
||||
c.odoo_production_id,
|
||||
c.order_ref,
|
||||
c.product_id,
|
||||
c.product_name,
|
||||
c.qty_production,
|
||||
c.units_per_cycle,
|
||||
c.target_cycle_rate_ppm,
|
||||
CASE
|
||||
WHEN c.product_id IS NULL THEN 'MISSING_PRODUCT_ID'
|
||||
WHEN c.units_per_cycle IS NULL THEN 'MISSING_UNITS_PER_CYCLE'
|
||||
WHEN c.target_cycle_rate_ppm IS NULL THEN 'MISSING_TARGET_CYCLE_RATE'
|
||||
ELSE 'READY'
|
||||
END AS cycle_parameter_status,
|
||||
MAX(c.started_at) AS latest_stop_started_at,
|
||||
COUNT(*) AS related_stop_count
|
||||
FROM mv_reports_ucepsa_prod.li_stop_candidates_v1 c
|
||||
WHERE c.order_present = true
|
||||
AND c.order_coherent = true
|
||||
GROUP BY
|
||||
c.machine_id, c.odoo_production_id, c.order_ref, c.product_id,
|
||||
c.product_name, c.qty_production, c.units_per_cycle, c.target_cycle_rate_ppm;
|
||||
|
||||
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_data_quality_v1 AS
|
||||
WITH deployment AS (
|
||||
SELECT COUNT(*) AS n
|
||||
FROM mv_loss_intelligence.deployment_config
|
||||
WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise' AND scope_code = 'CUTTERS'
|
||||
), capacity_cost AS (
|
||||
SELECT COUNT(*) AS n
|
||||
FROM mv_loss_intelligence.economic_parameters
|
||||
WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise'
|
||||
AND parameter_code = 'CAPACITY_HOUR_EUR' AND is_active = true
|
||||
), energy_cost AS (
|
||||
SELECT COUNT(*) AS n
|
||||
FROM mv_loss_intelligence.economic_parameters
|
||||
WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise'
|
||||
AND parameter_code = 'ENERGY_VARIABLE_EUR_KWH' AND is_active = true
|
||||
), current_loaded AS (
|
||||
SELECT COUNT(*) AS n
|
||||
FROM mv_hot.odoo_order_state_current
|
||||
WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise'
|
||||
AND order_present = true AND order_coherent = true
|
||||
), cycle_missing AS (
|
||||
SELECT COUNT(*) AS n
|
||||
FROM mv_hot.odoo_order_state_current o
|
||||
WHERE o.tenant = 'ucepsa' AND o.site = 'ucepsa_onpremise'
|
||||
AND o.order_present = true AND o.order_coherent = true
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM mv_loss_intelligence.product_cycle_parameters p
|
||||
WHERE p.tenant = o.tenant
|
||||
AND p.site = o.site
|
||||
AND p.product_id = o.product_id
|
||||
AND p.is_active = true
|
||||
AND (p.machine_id IS NULL OR p.machine_id = o.machine_id)
|
||||
)
|
||||
), stale_open AS (
|
||||
SELECT COUNT(*) AS n
|
||||
FROM mv_hot.edge_oee_machine_stops
|
||||
WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise'
|
||||
AND status = 'OPEN'
|
||||
AND started_at < now() - interval '12 hours'
|
||||
)
|
||||
SELECT 'DEPLOYMENT_CONFIG'::text AS check_code,
|
||||
CASE WHEN n = 1 THEN 'OK' ELSE 'BLOCKER' END AS status,
|
||||
n AS affected_count,
|
||||
'Debe existir una única configuración CUTTERS.'::text AS description
|
||||
FROM deployment
|
||||
UNION ALL
|
||||
SELECT 'CAPACITY_COST', CASE WHEN n >= 1 THEN 'OK' ELSE 'BLOCKER' END, n,
|
||||
'Debe existir coste de capacidad €/h activo.' FROM capacity_cost
|
||||
UNION ALL
|
||||
SELECT 'ENERGY_COST', CASE WHEN n >= 1 THEN 'OK' ELSE 'BLOCKER' END, n,
|
||||
'Debe existir coste eléctrico variable €/kWh activo.' FROM energy_cost
|
||||
UNION ALL
|
||||
SELECT 'CURRENT_VALID_ORDERS', CASE WHEN n >= 1 THEN 'OK' ELSE 'INFO' END, n,
|
||||
'Órdenes actualmente cargadas y coherentes.' FROM current_loaded
|
||||
UNION ALL
|
||||
SELECT 'MISSING_CYCLE_PARAMETERS', CASE WHEN n = 0 THEN 'OK' ELSE 'WARNING' END, n,
|
||||
'Productos activos sin bolsas por golpe o cadencia configurada.' FROM cycle_missing
|
||||
UNION ALL
|
||||
SELECT 'STALE_OPEN_STOPS', CASE WHEN n = 0 THEN 'OK' ELSE 'WARNING' END, n,
|
||||
'Paros abiertos desde hace más de 12 horas; no equivalen automáticamente a pérdida.' FROM stale_open;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ucepsa_ro') THEN
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_stop_candidates_v1 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_stop_groups_v1 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_shadow_review_queue_v1 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_operator_stop_queue_v1 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_daily_review_queue_v1 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_shadow_summary_v1 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_product_cycle_coverage_v1 TO grafana_ucepsa_ro;
|
||||
GRANT SELECT ON mv_reports_ucepsa_prod.li_data_quality_v1 TO grafana_ucepsa_ro;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@ -0,0 +1,244 @@
|
||||
BEGIN;
|
||||
|
||||
INSERT INTO mv_loss_intelligence.deployment_config (
|
||||
tenant, site, scope_code, scope_name, mode,
|
||||
planned_go_live_date, actual_go_live_at, timezone,
|
||||
review_owner_primary, review_owner_secondary,
|
||||
review_cadence, daily_review_limit, notes
|
||||
)
|
||||
VALUES (
|
||||
'ucepsa', 'ucepsa_onpremise', 'CUTTERS', 'Cortadoras UCEPSA', 'SHADOW',
|
||||
DATE '2026-10-01', NULL, 'Europe/Madrid',
|
||||
'Jefe de producción', 'Gerencia / dirección',
|
||||
'DAILY', 5,
|
||||
'Infraestructura preparada entre julio y septiembre. El Ledger oficial se activará en kickoff; octubre es una previsión, no una activación automática.'
|
||||
)
|
||||
ON CONFLICT (tenant, site, scope_code)
|
||||
DO UPDATE SET
|
||||
scope_name = EXCLUDED.scope_name,
|
||||
mode = 'SHADOW',
|
||||
planned_go_live_date = EXCLUDED.planned_go_live_date,
|
||||
timezone = EXCLUDED.timezone,
|
||||
review_owner_primary = EXCLUDED.review_owner_primary,
|
||||
review_owner_secondary = EXCLUDED.review_owner_secondary,
|
||||
review_cadence = EXCLUDED.review_cadence,
|
||||
daily_review_limit = EXCLUDED.daily_review_limit,
|
||||
notes = EXCLUDED.notes,
|
||||
updated_at = now();
|
||||
|
||||
INSERT INTO mv_loss_intelligence.loss_policies (
|
||||
tenant, site, policy_code, source_type,
|
||||
noise_below_s, aggregate_below_s, operator_individual_from_s,
|
||||
aggregation_window_min, require_valid_order, require_closed_event,
|
||||
exclude_planned_by_default, is_active, notes
|
||||
)
|
||||
VALUES (
|
||||
'ucepsa', 'ucepsa_onpremise', 'CUTTER_STOPS_V1', 'MACHINE_STOP',
|
||||
5, 300, 300,
|
||||
15, true, true,
|
||||
true, true,
|
||||
'Política inicial de modo sombra. <5 s se conserva solo en raw; 5 s–5 min se agrupa; >=5 min se trata individualmente. Validar con producción antes del kickoff.'
|
||||
)
|
||||
ON CONFLICT (tenant, site, policy_code)
|
||||
DO UPDATE SET
|
||||
noise_below_s = EXCLUDED.noise_below_s,
|
||||
aggregate_below_s = EXCLUDED.aggregate_below_s,
|
||||
operator_individual_from_s = EXCLUDED.operator_individual_from_s,
|
||||
aggregation_window_min = EXCLUDED.aggregation_window_min,
|
||||
require_valid_order = EXCLUDED.require_valid_order,
|
||||
require_closed_event = EXCLUDED.require_closed_event,
|
||||
exclude_planned_by_default = EXCLUDED.exclude_planned_by_default,
|
||||
is_active = true,
|
||||
notes = EXCLUDED.notes,
|
||||
updated_at = now();
|
||||
|
||||
INSERT INTO mv_loss_intelligence.economic_parameters (
|
||||
tenant, site, scope_code, machine_id, product_id,
|
||||
parameter_code, parameter_value, unit, confidence,
|
||||
includes_energy, valid_from, is_active, source_ref, approved_by, notes
|
||||
)
|
||||
SELECT
|
||||
'ucepsa', 'ucepsa_onpremise', 'CUTTERS', NULL, NULL,
|
||||
'CAPACITY_HOUR_EUR', 36.000000, 'EUR/h', 'LOW',
|
||||
NULL, TIMESTAMPTZ '2026-06-18 00:00:00+02', true,
|
||||
'Valor demostrativo usado en Edge-OEE', NULL,
|
||||
'Pendiente de validación por gerencia. No se ha confirmado si incorpora energía.'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM mv_loss_intelligence.economic_parameters
|
||||
WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise'
|
||||
AND parameter_code = 'CAPACITY_HOUR_EUR' AND is_active = true
|
||||
);
|
||||
|
||||
INSERT INTO mv_loss_intelligence.economic_parameters (
|
||||
tenant, site, scope_code, machine_id, product_id,
|
||||
parameter_code, parameter_value, unit, confidence,
|
||||
includes_energy, valid_from, is_active, source_ref, approved_by, notes
|
||||
)
|
||||
SELECT
|
||||
'ucepsa', 'ucepsa_onpremise', 'CUTTERS', NULL, NULL,
|
||||
'ENERGY_VARIABLE_EUR_KWH', 0.128249, 'EUR/kWh', 'MEDIUM',
|
||||
false, TIMESTAMPTZ '2026-06-10 00:00:00+02', true,
|
||||
'Factura UCEPSA 6.1TD 10/06/2026–30/06/2026', NULL,
|
||||
'Media ponderada de energía, red variable e impuesto eléctrico. No incluye reparto automático del término fijo de potencia.'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM mv_loss_intelligence.economic_parameters
|
||||
WHERE tenant = 'ucepsa' AND site = 'ucepsa_onpremise'
|
||||
AND parameter_code = 'ENERGY_VARIABLE_EUR_KWH' AND is_active = true
|
||||
);
|
||||
|
||||
INSERT INTO mv_edge_oee_ucepsa_prod.margin_cost_parameters (
|
||||
tenant, site, scope_level, machine_id, product_default_code,
|
||||
energy_cost_eur_per_kwh, material_cost_eur_per_kg,
|
||||
machine_run_cost_eur_per_h, labor_cost_eur_per_h, overhead_cost_eur_per_h,
|
||||
valid_from, valid_to, is_active, notes, created_at
|
||||
)
|
||||
SELECT
|
||||
'ucepsa', 'ucepsa_onpremise', 'GLOBAL', NULL, NULL,
|
||||
0.128249, NULL,
|
||||
36.0000, NULL, NULL,
|
||||
TIMESTAMPTZ '2026-06-10 00:00:00+02', NULL, true,
|
||||
'Parámetros provisionales para Cost Readiness: 36 €/h y 0,128249 €/kWh. Mantener costes separados hasta confirmar si el coste/hora incluye energía.',
|
||||
now()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM mv_edge_oee_ucepsa_prod.margin_cost_parameters
|
||||
WHERE tenant = 'ucepsa'
|
||||
AND site = 'ucepsa_onpremise'
|
||||
AND scope_level = 'GLOBAL'
|
||||
AND is_active = true
|
||||
);
|
||||
|
||||
INSERT INTO mv_loss_intelligence.action_catalog (
|
||||
action_id, catalog_code, catalog_version, tenant, site,
|
||||
loss_type, source_cause_code, action_type, action_candidate,
|
||||
applies_when, does_not_apply_when, suggested_owner,
|
||||
test_window, metric_before, metric_after, success_criteria,
|
||||
risk_level, cost_to_test, source_ref, notes
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'CUTTER-BOBBIN-001', 'LI-CUTTER-STOPS', '0.1', 'ucepsa', 'ucepsa_onpremise',
|
||||
'MACHINE_STOP', 'cambio_bobina', 'METHOD',
|
||||
'Preparar la siguiente bobina y sus elementos auxiliares antes de que termine la bobina en curso.',
|
||||
'["Paros repetidos por cambio de bobina", "Orden y referencia conocidas"]'::jsonb,
|
||||
'["Cambio imprevisto por defecto de material", "Falta de espacio seguro para staging"]'::jsonb,
|
||||
'Producción', '10 cambios de bobina comparables',
|
||||
'Minutos de cambio de bobina por evento', 'Minutos de cambio de bobina por evento',
|
||||
'Reducción >= 20% sin aumentar defectos ni incidencias de seguridad',
|
||||
'LOW', 'LOW', 'MESAVAULT Action Catalog Cutter v0.1',
|
||||
'La acción final debe adaptarse al espacio y método real de UCEPSA.'
|
||||
),
|
||||
(
|
||||
'CUTTER-SPLICE-001', 'LI-CUTTER-STOPS', '0.1', 'ucepsa', 'ucepsa_onpremise',
|
||||
'MACHINE_STOP', 'fallo_empalme', 'METHOD',
|
||||
'Separar el fallo de empalme por tipo de defecto y probar un estándar único de preparación durante 10 órdenes.',
|
||||
'["Fallo de empalme repetido", "La causa puede observarse y clasificarse"]'::jsonb,
|
||||
'["Avería mecánica ajena al empalme"]'::jsonb,
|
||||
'Producción / métodos', '10 órdenes o 20 empalmes',
|
||||
'Eventos y minutos perdidos por empalme', 'Eventos y minutos perdidos por empalme',
|
||||
'Reducción >= 25% de minutos perdidos',
|
||||
'LOW', 'LOW', 'MESAVAULT Action Catalog Cutter v0.1', NULL
|
||||
),
|
||||
(
|
||||
'CUTTER-SEAL-001', 'LI-CUTTER-STOPS', '0.1', 'ucepsa', 'ucepsa_onpremise',
|
||||
'MACHINE_STOP', 'ajuste_sellado', 'METHOD',
|
||||
'Crear y probar una ficha de parámetros de sellado por familia de referencia antes del arranque.',
|
||||
'["Ajustes repetidos de mordazas, temperatura, presión o teflón", "Referencias repetitivas"]'::jsonb,
|
||||
'["Avería mecánica", "Materia prima fuera de especificación no confirmada"]'::jsonb,
|
||||
'Producción / calidad', '10 órdenes de la misma familia',
|
||||
'Minutos de ajuste de sellado por orden', 'Minutos de ajuste de sellado por orden',
|
||||
'Reducción >= 25% manteniendo calidad',
|
||||
'LOW', 'LOW', 'MESAVAULT Action Catalog Cutter v0.1', NULL
|
||||
),
|
||||
(
|
||||
'CUTTER-MATERIAL-001', 'LI-CUTTER-STOPS', '0.1', 'ucepsa', 'ucepsa_onpremise',
|
||||
'MACHINE_STOP', 'falta_material', 'ORGANIZATIONAL',
|
||||
'No iniciar la orden hasta que bobina y material auxiliar estén físicamente preparados junto a la cortadora.',
|
||||
'["Espera de material con orden cargada", "Material identificable antes del arranque"]'::jsonb,
|
||||
'["Rotura o rechazo de material durante producción"]'::jsonb,
|
||||
'Producción / logística', '10 órdenes comparables',
|
||||
'Minutos de espera de material por orden', 'Minutos de espera de material por orden',
|
||||
'Reducción >= 50% de la espera de material',
|
||||
'LOW', 'LOW', 'MESAVAULT Action Catalog Cutter v0.1', NULL
|
||||
),
|
||||
(
|
||||
'CUTTER-PHOTO-001', 'LI-CUTTER-STOPS', '0.1', 'ucepsa', 'ucepsa_onpremise',
|
||||
'MACHINE_STOP', 'limpieza_fotocelula', 'TECHNICAL',
|
||||
'Aplicar una comprobación y limpieza breve de fotocélula antes del turno en las referencias con recurrencia.',
|
||||
'["Limpieza de fotocélula repetida", "Patrón concentrado en máquina o producto"]'::jsonb,
|
||||
'["Fallo eléctrico o sustitución necesaria del sensor"]'::jsonb,
|
||||
'Producción / mantenimiento', '2 semanas',
|
||||
'Eventos y minutos por limpieza de fotocélula', 'Eventos y minutos por limpieza de fotocélula',
|
||||
'Reducción >= 30% de eventos sin aumentar tiempo de arranque',
|
||||
'LOW', 'LOW', 'MESAVAULT Action Catalog Cutter v0.1', NULL
|
||||
),
|
||||
(
|
||||
'CUTTER-FORMAT-001', 'LI-CUTTER-STOPS', '0.1', 'ucepsa', 'ucepsa_onpremise',
|
||||
'MACHINE_STOP', 'cambio_formato', 'METHOD',
|
||||
'Separar preparación externa e interna del cambio de formato y preparar fuera de máquina todo lo posible.',
|
||||
'["Cambios de formato repetidos", "Se pueden comparar referencias o familias similares"]'::jsonb,
|
||||
'["Cambio excepcional no repetible"]'::jsonb,
|
||||
'Producción / métodos', '10 cambios comparables',
|
||||
'Minutos de cambio de formato', 'Minutos de cambio de formato',
|
||||
'Reducción >= 20% del tiempo mediano',
|
||||
'LOW', 'MEDIUM', 'MESAVAULT Action Catalog Cutter v0.1', NULL
|
||||
),
|
||||
(
|
||||
'CUTTER-MECH-001', 'LI-CUTTER-STOPS', '0.1', 'ucepsa', 'ucepsa_onpremise',
|
||||
'MACHINE_STOP', 'averia_mecanica', 'TECHNICAL',
|
||||
'Seleccionar la avería mecánica repetida con mayor coste, intervenir sobre el componente y comprobar recurrencia.',
|
||||
'["Misma avería o síntoma repetido", "Existe responsable de mantenimiento"]'::jsonb,
|
||||
'["Evento único sin diagnóstico", "Paro no confirmado como avería"]'::jsonb,
|
||||
'Mantenimiento', '30 días o siguiente ciclo de recurrencia',
|
||||
'Eventos, minutos y coste de la avería seleccionada', 'Eventos, minutos y coste tras la intervención',
|
||||
'Cero recurrencias o reducción >= 50% del tiempo perdido',
|
||||
'MEDIUM', 'MEDIUM', 'MESAVAULT Action Catalog Cutter v0.1', NULL
|
||||
),
|
||||
(
|
||||
'CUTTER-INSTR-001', 'LI-CUTTER-STOPS', '0.1', 'ucepsa', 'ucepsa_onpremise',
|
||||
'MACHINE_STOP', 'espera_instrucciones', 'ORGANIZATIONAL',
|
||||
'Definir antes del turno qué órdenes, referencias y decisiones requieren autorización para evitar esperas con máquina ocupada.',
|
||||
'["Espera de instrucciones repetida", "Decisión previsible antes del arranque"]'::jsonb,
|
||||
'["Incidencia de seguridad o calidad que exige autorización específica"]'::jsonb,
|
||||
'Jefe de producción', '2 semanas',
|
||||
'Minutos de espera de instrucciones por orden', 'Minutos de espera de instrucciones por orden',
|
||||
'Reducción >= 50% de la espera',
|
||||
'LOW', 'LOW', 'MESAVAULT Action Catalog Cutter v0.1', NULL
|
||||
),
|
||||
(
|
||||
'CUTTER-OTHER-001', 'LI-CUTTER-STOPS', '0.1', 'ucepsa', 'ucepsa_onpremise',
|
||||
'MACHINE_STOP', 'otro', 'DATA_QUALITY',
|
||||
'Revisar semanalmente los paros clasificados como Otro y crear una causa nueva si un patrón se repite.',
|
||||
'["Uso de Otro superior al 10% de minutos o eventos"]'::jsonb,
|
||||
'["Evento aislado sin patrón"]'::jsonb,
|
||||
'Producción / MESAVAULT', '4 semanas',
|
||||
'Porcentaje de eventos y minutos en Otro', 'Porcentaje de eventos y minutos en Otro',
|
||||
'Otro < 10% de eventos y minutos',
|
||||
'LOW', 'LOW', 'MESAVAULT Action Catalog Cutter v0.1',
|
||||
'Es una acción de calidad de dato, no una mejora de proceso por sí misma.'
|
||||
)
|
||||
ON CONFLICT (action_id)
|
||||
DO UPDATE SET
|
||||
catalog_code = EXCLUDED.catalog_code,
|
||||
catalog_version = EXCLUDED.catalog_version,
|
||||
loss_type = EXCLUDED.loss_type,
|
||||
source_cause_code = EXCLUDED.source_cause_code,
|
||||
action_type = EXCLUDED.action_type,
|
||||
action_candidate = EXCLUDED.action_candidate,
|
||||
applies_when = EXCLUDED.applies_when,
|
||||
does_not_apply_when = EXCLUDED.does_not_apply_when,
|
||||
suggested_owner = EXCLUDED.suggested_owner,
|
||||
test_window = EXCLUDED.test_window,
|
||||
metric_before = EXCLUDED.metric_before,
|
||||
metric_after = EXCLUDED.metric_after,
|
||||
success_criteria = EXCLUDED.success_criteria,
|
||||
risk_level = EXCLUDED.risk_level,
|
||||
cost_to_test = EXCLUDED.cost_to_test,
|
||||
source_ref = EXCLUDED.source_ref,
|
||||
notes = EXCLUDED.notes,
|
||||
is_active = true,
|
||||
updated_at = now();
|
||||
|
||||
COMMIT;
|
||||
@ -67,8 +67,15 @@ def state_hash(payload):
|
||||
"odoo_state": payload.get("odoo_state"),
|
||||
"reason": payload.get("reason"),
|
||||
"workorder_id": payload.get("workorder_id"),
|
||||
"production_id": payload.get("production_id"),
|
||||
"production_order": payload.get("production_order"),
|
||||
"product_id": payload.get("product_id"),
|
||||
"product": payload.get("product"),
|
||||
"qty_production": payload.get("qty_production"),
|
||||
"date_start": payload.get("date_start"),
|
||||
"date_finished": payload.get("date_finished"),
|
||||
"workorder_name": payload.get("workorder_name"),
|
||||
"workorder_state": payload.get("workorder_state"),
|
||||
"waiting_count": diagnostics.get("waiting_count"),
|
||||
"ready_count": diagnostics.get("ready_count"),
|
||||
"progress_count": diagnostics.get("progress_count"),
|
||||
@ -133,8 +140,15 @@ def upsert_order_state(topic, payload):
|
||||
"odoo_state": payload.get("odoo_state"),
|
||||
"reason": payload.get("reason"),
|
||||
"workorder_id": str(payload.get("workorder_id")) if payload.get("workorder_id") is not None else None,
|
||||
"workorder_name": payload.get("workorder_name"),
|
||||
"workorder_state": payload.get("workorder_state"),
|
||||
"production_id": to_int(payload.get("production_id")),
|
||||
"production_order": payload.get("production_order"),
|
||||
"product_id": to_int(payload.get("product_id")),
|
||||
"product": payload.get("product"),
|
||||
"qty_production": payload.get("qty_production"),
|
||||
"date_start": parse_ts(payload.get("date_start")),
|
||||
"date_finished": parse_ts(payload.get("date_finished")),
|
||||
"waiting_count": to_int(diagnostics.get("waiting_count")),
|
||||
"ready_count": to_int(diagnostics.get("ready_count")),
|
||||
"progress_count": to_int(diagnostics.get("progress_count")),
|
||||
@ -149,7 +163,9 @@ def upsert_order_state(topic, payload):
|
||||
tenant, site, machine_id, topic, mqtt_ts, received_at,
|
||||
source, workcenter_id, workcenter_name, workcenter_display_name,
|
||||
order_present, order_coherent, odoo_state, reason,
|
||||
workorder_id, production_order, product,
|
||||
workorder_id, workorder_name, workorder_state,
|
||||
production_id, production_order, product_id, product,
|
||||
qty_production, date_start, date_finished,
|
||||
waiting_count, ready_count, progress_count,
|
||||
payload_hash, payload_json
|
||||
)
|
||||
@ -157,7 +173,9 @@ def upsert_order_state(topic, payload):
|
||||
%(tenant)s, %(site)s, %(machine_id)s, %(topic)s, %(mqtt_ts)s, now(),
|
||||
%(source)s, %(workcenter_id)s, %(workcenter_name)s, %(workcenter_display_name)s,
|
||||
%(order_present)s, %(order_coherent)s, %(odoo_state)s, %(reason)s,
|
||||
%(workorder_id)s, %(production_order)s, %(product)s,
|
||||
%(workorder_id)s, %(workorder_name)s, %(workorder_state)s,
|
||||
%(production_id)s, %(production_order)s, %(product_id)s, %(product)s,
|
||||
%(qty_production)s, %(date_start)s, %(date_finished)s,
|
||||
%(waiting_count)s, %(ready_count)s, %(progress_count)s,
|
||||
%(payload_hash)s, %(payload_json)s
|
||||
)
|
||||
@ -175,8 +193,15 @@ def upsert_order_state(topic, payload):
|
||||
odoo_state = EXCLUDED.odoo_state,
|
||||
reason = EXCLUDED.reason,
|
||||
workorder_id = EXCLUDED.workorder_id,
|
||||
workorder_name = EXCLUDED.workorder_name,
|
||||
workorder_state = EXCLUDED.workorder_state,
|
||||
production_id = EXCLUDED.production_id,
|
||||
production_order = EXCLUDED.production_order,
|
||||
product_id = EXCLUDED.product_id,
|
||||
product = EXCLUDED.product,
|
||||
qty_production = EXCLUDED.qty_production,
|
||||
date_start = EXCLUDED.date_start,
|
||||
date_finished = EXCLUDED.date_finished,
|
||||
waiting_count = EXCLUDED.waiting_count,
|
||||
ready_count = EXCLUDED.ready_count,
|
||||
progress_count = EXCLUDED.progress_count,
|
||||
@ -191,14 +216,18 @@ def upsert_order_state(topic, payload):
|
||||
INSERT INTO mv_hot.odoo_order_state_events (
|
||||
tenant, site, machine_id, topic, mqtt_ts, received_at,
|
||||
odoo_state, order_present, order_coherent,
|
||||
production_order, product, reason,
|
||||
workorder_id, workorder_name, workorder_state,
|
||||
production_id, production_order, product_id, product,
|
||||
qty_production, date_start, date_finished, reason,
|
||||
waiting_count, ready_count, progress_count,
|
||||
payload_hash, payload_json
|
||||
)
|
||||
VALUES (
|
||||
%(tenant)s, %(site)s, %(machine_id)s, %(topic)s, %(mqtt_ts)s, now(),
|
||||
%(odoo_state)s, %(order_present)s, %(order_coherent)s,
|
||||
%(production_order)s, %(product)s, %(reason)s,
|
||||
%(workorder_id)s, %(workorder_name)s, %(workorder_state)s,
|
||||
%(production_id)s, %(production_order)s, %(product_id)s, %(product)s,
|
||||
%(qty_production)s, %(date_start)s, %(date_finished)s, %(reason)s,
|
||||
%(waiting_count)s, %(ready_count)s, %(progress_count)s,
|
||||
%(payload_hash)s, %(payload_json)s
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user