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"),
|
"odoo_state": payload.get("odoo_state"),
|
||||||
"reason": payload.get("reason"),
|
"reason": payload.get("reason"),
|
||||||
"workorder_id": payload.get("workorder_id"),
|
"workorder_id": payload.get("workorder_id"),
|
||||||
|
"production_id": payload.get("production_id"),
|
||||||
"production_order": payload.get("production_order"),
|
"production_order": payload.get("production_order"),
|
||||||
|
"product_id": payload.get("product_id"),
|
||||||
"product": payload.get("product"),
|
"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"),
|
"waiting_count": diagnostics.get("waiting_count"),
|
||||||
"ready_count": diagnostics.get("ready_count"),
|
"ready_count": diagnostics.get("ready_count"),
|
||||||
"progress_count": diagnostics.get("progress_count"),
|
"progress_count": diagnostics.get("progress_count"),
|
||||||
@ -133,8 +140,15 @@ def upsert_order_state(topic, payload):
|
|||||||
"odoo_state": payload.get("odoo_state"),
|
"odoo_state": payload.get("odoo_state"),
|
||||||
"reason": payload.get("reason"),
|
"reason": payload.get("reason"),
|
||||||
"workorder_id": str(payload.get("workorder_id")) if payload.get("workorder_id") is not None else None,
|
"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"),
|
"production_order": payload.get("production_order"),
|
||||||
|
"product_id": to_int(payload.get("product_id")),
|
||||||
"product": payload.get("product"),
|
"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")),
|
"waiting_count": to_int(diagnostics.get("waiting_count")),
|
||||||
"ready_count": to_int(diagnostics.get("ready_count")),
|
"ready_count": to_int(diagnostics.get("ready_count")),
|
||||||
"progress_count": to_int(diagnostics.get("progress_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,
|
tenant, site, machine_id, topic, mqtt_ts, received_at,
|
||||||
source, workcenter_id, workcenter_name, workcenter_display_name,
|
source, workcenter_id, workcenter_name, workcenter_display_name,
|
||||||
order_present, order_coherent, odoo_state, reason,
|
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,
|
waiting_count, ready_count, progress_count,
|
||||||
payload_hash, payload_json
|
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(),
|
%(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,
|
%(source)s, %(workcenter_id)s, %(workcenter_name)s, %(workcenter_display_name)s,
|
||||||
%(order_present)s, %(order_coherent)s, %(odoo_state)s, %(reason)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,
|
%(waiting_count)s, %(ready_count)s, %(progress_count)s,
|
||||||
%(payload_hash)s, %(payload_json)s
|
%(payload_hash)s, %(payload_json)s
|
||||||
)
|
)
|
||||||
@ -175,8 +193,15 @@ def upsert_order_state(topic, payload):
|
|||||||
odoo_state = EXCLUDED.odoo_state,
|
odoo_state = EXCLUDED.odoo_state,
|
||||||
reason = EXCLUDED.reason,
|
reason = EXCLUDED.reason,
|
||||||
workorder_id = EXCLUDED.workorder_id,
|
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,
|
production_order = EXCLUDED.production_order,
|
||||||
|
product_id = EXCLUDED.product_id,
|
||||||
product = EXCLUDED.product,
|
product = EXCLUDED.product,
|
||||||
|
qty_production = EXCLUDED.qty_production,
|
||||||
|
date_start = EXCLUDED.date_start,
|
||||||
|
date_finished = EXCLUDED.date_finished,
|
||||||
waiting_count = EXCLUDED.waiting_count,
|
waiting_count = EXCLUDED.waiting_count,
|
||||||
ready_count = EXCLUDED.ready_count,
|
ready_count = EXCLUDED.ready_count,
|
||||||
progress_count = EXCLUDED.progress_count,
|
progress_count = EXCLUDED.progress_count,
|
||||||
@ -191,14 +216,18 @@ def upsert_order_state(topic, payload):
|
|||||||
INSERT INTO mv_hot.odoo_order_state_events (
|
INSERT INTO mv_hot.odoo_order_state_events (
|
||||||
tenant, site, machine_id, topic, mqtt_ts, received_at,
|
tenant, site, machine_id, topic, mqtt_ts, received_at,
|
||||||
odoo_state, order_present, order_coherent,
|
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,
|
waiting_count, ready_count, progress_count,
|
||||||
payload_hash, payload_json
|
payload_hash, payload_json
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
%(tenant)s, %(site)s, %(machine_id)s, %(topic)s, %(mqtt_ts)s, now(),
|
%(tenant)s, %(site)s, %(machine_id)s, %(topic)s, %(mqtt_ts)s, now(),
|
||||||
%(odoo_state)s, %(order_present)s, %(order_coherent)s,
|
%(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,
|
%(waiting_count)s, %(ready_count)s, %(progress_count)s,
|
||||||
%(payload_hash)s, %(payload_json)s
|
%(payload_hash)s, %(payload_json)s
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user