feat(ucepsa): add multi-source production context shadow v0.3
This commit is contained in:
parent
36d67d99b0
commit
d83bacefc4
@ -0,0 +1,29 @@
|
|||||||
|
# ADR — Contexto productivo multifuente durante migraciones ERP
|
||||||
|
|
||||||
|
## Estado
|
||||||
|
|
||||||
|
Aceptado para UCEPSA en modo SHADOW.
|
||||||
|
|
||||||
|
## Problema
|
||||||
|
|
||||||
|
Una máquina puede producir legítimamente sin orden Odoo porque la planta mantiene
|
||||||
|
temporalmente actividad en el ERP anterior.
|
||||||
|
|
||||||
|
Tratar todo `RUNNING_WITHOUT_VALID_ORDER` como error o pérdida produciría falsos
|
||||||
|
positivos y dañaría la implantación.
|
||||||
|
|
||||||
|
## Decisión
|
||||||
|
|
||||||
|
MESAVAULT separa:
|
||||||
|
|
||||||
|
- verdad física: WISE/RevPi;
|
||||||
|
- contexto productivo: Odoo, legacy, manual, mantenimiento o prueba;
|
||||||
|
- pérdida: solo tras intersectar actividad, contexto confirmado y política económica.
|
||||||
|
|
||||||
|
## Consecuencias
|
||||||
|
|
||||||
|
- Odoo sigue siendo la fuente preferida.
|
||||||
|
- Legacy se acepta como excepción explícita.
|
||||||
|
- Sin contexto confirmado se genera una tarea de revisión, no un coste.
|
||||||
|
- Las balizas no cambian de semántica.
|
||||||
|
- El porcentaje Odoo/legacy/sin contexto se convierte en indicador de migración.
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
# UCEPSA — LossIntelligence Production Context SHADOW v0.3
|
||||||
|
|
||||||
|
## Decisión de arquitectura
|
||||||
|
|
||||||
|
Durante la migración conviven dos vías válidas:
|
||||||
|
|
||||||
|
1. Odoo Shop Floor, estándar preferido.
|
||||||
|
2. Producción legacy o manual autorizada.
|
||||||
|
|
||||||
|
`máquina en marcha + sin orden Odoo` no se convierte automáticamente en pérdida.
|
||||||
|
|
||||||
|
Primero se clasifica el contexto:
|
||||||
|
|
||||||
|
- ODOO_CONTEXT;
|
||||||
|
- LEGACY_AUTHORIZED;
|
||||||
|
- MANUAL_AUTHORIZED;
|
||||||
|
- MAINTENANCE;
|
||||||
|
- TEST_SETUP;
|
||||||
|
- UNCONFIRMED_CONTEXT;
|
||||||
|
- CONTEXT_CONFLICT.
|
||||||
|
|
||||||
|
## Protección de las balizas
|
||||||
|
|
||||||
|
Esta versión no modifica:
|
||||||
|
|
||||||
|
- `beacon_controller.py`;
|
||||||
|
- el publicador `order_state`;
|
||||||
|
- DO0 azul;
|
||||||
|
- DO1 rojo.
|
||||||
|
|
||||||
|
Azul fijo continúa significando exclusivamente orden Odoo válida.
|
||||||
|
|
||||||
|
Una producción legacy puede seguir provocando azul intermitente porque está fuera del
|
||||||
|
flujo Odoo, aunque MESAVAULT la reconozca como legacy autorizada.
|
||||||
|
|
||||||
|
## Flujo preventivo recomendado
|
||||||
|
|
||||||
|
Antes de producir por el ERP anterior, producción abre un contexto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/li_context_admin.py open \
|
||||||
|
--machine CORT-02 \
|
||||||
|
--source LEGACY_ERP \
|
||||||
|
--external-order-ref LEGACY-1234 \
|
||||||
|
--product-name "Referencia legacy" \
|
||||||
|
--authorized-by "Juan Pablo" \
|
||||||
|
--notes "Producción autorizada fuera de Odoo durante migración"
|
||||||
|
```
|
||||||
|
|
||||||
|
Al finalizar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/li_context_admin.py close \
|
||||||
|
--session-id 1 \
|
||||||
|
--closed-by "Juan Pablo"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flujo retrospectivo
|
||||||
|
|
||||||
|
Si no se abrió previamente, la vista `li_context_review_queue_v1` muestra un único
|
||||||
|
bloque físico. Tras confirmar el intervalo se crea una sesión retrospectiva.
|
||||||
|
|
||||||
|
## Seguridad SHADOW
|
||||||
|
|
||||||
|
Mientras `deployment_config.mode = SHADOW`, la herramienta fuerza:
|
||||||
|
|
||||||
|
```text
|
||||||
|
official_eligible = false
|
||||||
|
```
|
||||||
|
|
||||||
|
Por tanto, ningún contexto manual puede crear por sí solo una pérdida económica oficial.
|
||||||
|
|
||||||
|
## Qué no resuelve todavía
|
||||||
|
|
||||||
|
- La identidad activa de la operaria en el flujo personalizado de Shop Floor.
|
||||||
|
- El cierre real con declaración de producción.
|
||||||
|
- La extensión de los paros v2 para usar contexto legacy.
|
||||||
|
|
||||||
|
La siguiente subfase será `v0.3.1`: cruzar los paros con los contextos validados tras
|
||||||
|
comprobar que la apertura/cierre legacy funciona correctamente.
|
||||||
@ -0,0 +1,128 @@
|
|||||||
|
\pset pager off
|
||||||
|
|
||||||
|
\echo '=== 1. Política de contexto ==='
|
||||||
|
SELECT
|
||||||
|
policy_code,
|
||||||
|
activity_gap_s,
|
||||||
|
activity_tail_s,
|
||||||
|
stale_open_context_s,
|
||||||
|
allow_legacy_context,
|
||||||
|
require_legacy_authorization,
|
||||||
|
allow_inferred_for_official
|
||||||
|
FROM mv_loss_intelligence.production_context_policies
|
||||||
|
WHERE tenant = 'ucepsa'
|
||||||
|
AND site = 'ucepsa_onpremise'
|
||||||
|
ORDER BY id DESC;
|
||||||
|
|
||||||
|
\echo '=== 2. Seguridad SHADOW ==='
|
||||||
|
SELECT
|
||||||
|
mode,
|
||||||
|
actual_go_live_at
|
||||||
|
FROM mv_loss_intelligence.deployment_config
|
||||||
|
WHERE tenant = 'ucepsa'
|
||||||
|
AND site = 'ucepsa_onpremise'
|
||||||
|
AND scope_code = 'CUTTERS';
|
||||||
|
|
||||||
|
SELECT count(*) AS official_context_sessions_in_shadow
|
||||||
|
FROM mv_loss_intelligence.production_context_sessions s
|
||||||
|
JOIN mv_loss_intelligence.deployment_config d
|
||||||
|
ON d.tenant = s.tenant
|
||||||
|
AND d.site = s.site
|
||||||
|
AND d.scope_code = 'CUTTERS'
|
||||||
|
WHERE d.mode = 'SHADOW'
|
||||||
|
AND s.official_eligible = true;
|
||||||
|
|
||||||
|
\echo '=== 3. Estado actual de migración ==='
|
||||||
|
SELECT
|
||||||
|
machine_id,
|
||||||
|
operational_state,
|
||||||
|
migration_context_state,
|
||||||
|
migration_context_label,
|
||||||
|
telemetry_fresh,
|
||||||
|
machine_running,
|
||||||
|
order_present,
|
||||||
|
order_coherent,
|
||||||
|
production_order,
|
||||||
|
explicit_context_source,
|
||||||
|
explicit_order_ref,
|
||||||
|
explicit_context_started_at
|
||||||
|
FROM mv_reports_ucepsa_prod.machine_migration_context_state_v1
|
||||||
|
ORDER BY machine_id;
|
||||||
|
|
||||||
|
\echo '=== 4. Contextos explícitos ==='
|
||||||
|
SELECT
|
||||||
|
context_session_id,
|
||||||
|
machine_id,
|
||||||
|
source_type,
|
||||||
|
source_ref,
|
||||||
|
external_order_ref,
|
||||||
|
product_name,
|
||||||
|
operator_employee_id,
|
||||||
|
operator_name,
|
||||||
|
started_at,
|
||||||
|
ended_at,
|
||||||
|
status,
|
||||||
|
authorization_status,
|
||||||
|
confidence,
|
||||||
|
official_eligible,
|
||||||
|
authorized_by,
|
||||||
|
closed_by
|
||||||
|
FROM mv_loss_intelligence.production_context_sessions
|
||||||
|
WHERE tenant = 'ucepsa'
|
||||||
|
AND site = 'ucepsa_onpremise'
|
||||||
|
ORDER BY context_session_id DESC
|
||||||
|
LIMIT 30;
|
||||||
|
|
||||||
|
\echo '=== 5. Invariantes de actividad: deben devolver 0 ==='
|
||||||
|
SELECT count(*) AS invalid_activity_rows
|
||||||
|
FROM mv_reports_ucepsa_prod.li_machine_running_activity_v1
|
||||||
|
WHERE activity_to <= activity_from
|
||||||
|
OR activity_duration_s <= 0;
|
||||||
|
|
||||||
|
SELECT count(*) AS invalid_context_segment_rows
|
||||||
|
FROM mv_reports_ucepsa_prod.li_running_activity_context_segments_v1
|
||||||
|
WHERE segment_to <= segment_from
|
||||||
|
OR segment_duration_s <= 0;
|
||||||
|
|
||||||
|
\echo '=== 6. Los segmentos no superan la actividad: debe devolver 0 ==='
|
||||||
|
WITH totals AS (
|
||||||
|
SELECT
|
||||||
|
machine_id,
|
||||||
|
activity_island_id,
|
||||||
|
max(activity_duration_s) AS activity_duration_s,
|
||||||
|
sum(segment_duration_s) AS segmented_duration_s
|
||||||
|
FROM mv_reports_ucepsa_prod.li_running_activity_context_segments_v1
|
||||||
|
GROUP BY machine_id, activity_island_id
|
||||||
|
)
|
||||||
|
SELECT count(*) AS segment_overflow_rows
|
||||||
|
FROM totals
|
||||||
|
WHERE segmented_duration_s > activity_duration_s + 0.01;
|
||||||
|
|
||||||
|
\echo '=== 7. Cobertura de contexto últimos 30 días ==='
|
||||||
|
SELECT *
|
||||||
|
FROM mv_reports_ucepsa_prod.li_production_context_coverage_v1
|
||||||
|
ORDER BY machine_id, hours DESC;
|
||||||
|
|
||||||
|
\echo '=== 8. Cola de revisión de contexto ==='
|
||||||
|
SELECT
|
||||||
|
review_rank,
|
||||||
|
context_review_key,
|
||||||
|
machine_id,
|
||||||
|
started_at,
|
||||||
|
ended_at,
|
||||||
|
duration_min,
|
||||||
|
context_status,
|
||||||
|
allowed_decisions
|
||||||
|
FROM mv_reports_ucepsa_prod.li_context_review_queue_v1
|
||||||
|
ORDER BY review_rank
|
||||||
|
LIMIT 30;
|
||||||
|
|
||||||
|
\echo '=== 9. Calidad de dato v3 ==='
|
||||||
|
SELECT *
|
||||||
|
FROM mv_reports_ucepsa_prod.li_data_quality_v3
|
||||||
|
ORDER BY check_code;
|
||||||
|
|
||||||
|
\echo '=== 10. Las balizas no forman parte de esta migración ==='
|
||||||
|
SELECT
|
||||||
|
'INFO' AS status,
|
||||||
|
'099/100 no modifican beacon_controller, order_state publisher ni lógica DO0/DO1.' AS description;
|
||||||
@ -0,0 +1,193 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.production_context_policies (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant TEXT NOT NULL,
|
||||||
|
site TEXT NOT NULL,
|
||||||
|
policy_code TEXT NOT NULL,
|
||||||
|
activity_gap_s NUMERIC(12,2) NOT NULL DEFAULT 90
|
||||||
|
CHECK (activity_gap_s > 0),
|
||||||
|
activity_tail_s NUMERIC(12,2) NOT NULL DEFAULT 30
|
||||||
|
CHECK (activity_tail_s >= 0),
|
||||||
|
stale_open_context_s NUMERIC(14,2) NOT NULL DEFAULT 43200
|
||||||
|
CHECK (stale_open_context_s > 0),
|
||||||
|
allow_legacy_context BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
require_legacy_authorization BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
allow_inferred_for_official BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE mv_loss_intelligence.production_context_policies IS
|
||||||
|
'Políticas reutilizables para segmentar actividad física y reconocer contextos productivos Odoo, legacy, mantenimiento o prueba.';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.production_context_sessions (
|
||||||
|
context_session_id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant TEXT NOT NULL,
|
||||||
|
site TEXT NOT NULL,
|
||||||
|
machine_id TEXT NOT NULL,
|
||||||
|
source_type TEXT NOT NULL
|
||||||
|
CHECK (source_type IN (
|
||||||
|
'ODOO_SHOPFLOOR',
|
||||||
|
'LEGACY_ERP',
|
||||||
|
'MANUAL_AUTHORIZED',
|
||||||
|
'MAINTENANCE',
|
||||||
|
'TEST_SETUP',
|
||||||
|
'INFERRED_SHADOW'
|
||||||
|
)),
|
||||||
|
source_ref TEXT,
|
||||||
|
external_order_ref TEXT,
|
||||||
|
odoo_production_id BIGINT,
|
||||||
|
odoo_workorder_id BIGINT,
|
||||||
|
product_id BIGINT,
|
||||||
|
product_default_code TEXT,
|
||||||
|
product_name TEXT,
|
||||||
|
operator_employee_id BIGINT,
|
||||||
|
operator_name TEXT,
|
||||||
|
started_at TIMESTAMPTZ NOT NULL,
|
||||||
|
ended_at TIMESTAMPTZ,
|
||||||
|
status TEXT NOT NULL DEFAULT 'OPEN'
|
||||||
|
CHECK (status IN ('OPEN', 'CLOSED', 'CANCELLED', 'SUPERSEDED')),
|
||||||
|
authorization_status TEXT NOT NULL DEFAULT 'PENDING'
|
||||||
|
CHECK (authorization_status IN (
|
||||||
|
'PENDING',
|
||||||
|
'AUTHORIZED',
|
||||||
|
'AUTO_CONFIRMED',
|
||||||
|
'REJECTED'
|
||||||
|
)),
|
||||||
|
confidence TEXT NOT NULL DEFAULT 'LOW'
|
||||||
|
CHECK (confidence IN ('HIGH', 'MEDIUM', 'LOW')),
|
||||||
|
official_eligible BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
authorized_by TEXT,
|
||||||
|
authorized_at TIMESTAMPTZ,
|
||||||
|
closed_by TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
evidence_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
CHECK (ended_at IS NULL OR ended_at > started_at),
|
||||||
|
CHECK (
|
||||||
|
authorization_status NOT IN ('AUTHORIZED', 'AUTO_CONFIRMED')
|
||||||
|
OR authorized_at IS NOT NULL
|
||||||
|
),
|
||||||
|
CHECK (
|
||||||
|
status <> 'CLOSED'
|
||||||
|
OR ended_at IS NOT NULL
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE mv_loss_intelligence.production_context_sessions IS
|
||||||
|
'Contextos productivos explícitos. Durante la migración permite registrar producción legacy autorizada sin fingir que existe una orden Odoo.';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_li_context_sessions_machine_time
|
||||||
|
ON mv_loss_intelligence.production_context_sessions
|
||||||
|
(tenant, site, machine_id, started_at DESC, ended_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_li_context_sessions_status
|
||||||
|
ON mv_loss_intelligence.production_context_sessions
|
||||||
|
(tenant, site, status, authorization_status, source_type);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_li_context_sessions_one_open_explicit
|
||||||
|
ON mv_loss_intelligence.production_context_sessions
|
||||||
|
(tenant, site, machine_id)
|
||||||
|
WHERE status = 'OPEN'
|
||||||
|
AND source_type IN (
|
||||||
|
'LEGACY_ERP',
|
||||||
|
'MANUAL_AUTHORIZED',
|
||||||
|
'MAINTENANCE',
|
||||||
|
'TEST_SETUP'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mv_loss_intelligence.production_context_reviews (
|
||||||
|
review_id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant TEXT NOT NULL,
|
||||||
|
site TEXT NOT NULL,
|
||||||
|
activity_source_key TEXT NOT NULL,
|
||||||
|
machine_id TEXT NOT NULL,
|
||||||
|
activity_started_at TIMESTAMPTZ NOT NULL,
|
||||||
|
activity_ended_at TIMESTAMPTZ NOT NULL,
|
||||||
|
decision_type TEXT NOT NULL
|
||||||
|
CHECK (decision_type IN (
|
||||||
|
'CONFIRM_LEGACY',
|
||||||
|
'CONFIRM_MANUAL',
|
||||||
|
'CONFIRM_MAINTENANCE',
|
||||||
|
'CONFIRM_TEST_SETUP',
|
||||||
|
'ODOO_PROCESS_GAP',
|
||||||
|
'UNKNOWN_CONTEXT',
|
||||||
|
'IGNORE'
|
||||||
|
)),
|
||||||
|
created_context_session_id BIGINT
|
||||||
|
REFERENCES mv_loss_intelligence.production_context_sessions(context_session_id),
|
||||||
|
reviewed_by TEXT NOT NULL,
|
||||||
|
reviewed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
notes TEXT,
|
||||||
|
evidence_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
UNIQUE (tenant, site, activity_source_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE mv_loss_intelligence.production_context_reviews IS
|
||||||
|
'Decisiones retrospectivas sobre bloques de actividad física que inicialmente no tenían contexto Odoo ni legacy registrado.';
|
||||||
|
|
||||||
|
ALTER TABLE mv_loss_intelligence.loss_ledger
|
||||||
|
ADD COLUMN IF NOT EXISTS production_context_session_id BIGINT
|
||||||
|
REFERENCES mv_loss_intelligence.production_context_sessions(context_session_id),
|
||||||
|
ADD COLUMN IF NOT EXISTS production_context_source TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS production_context_confidence TEXT;
|
||||||
|
|
||||||
|
INSERT INTO mv_loss_intelligence.production_context_policies (
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
policy_code,
|
||||||
|
activity_gap_s,
|
||||||
|
activity_tail_s,
|
||||||
|
stale_open_context_s,
|
||||||
|
allow_legacy_context,
|
||||||
|
require_legacy_authorization,
|
||||||
|
allow_inferred_for_official,
|
||||||
|
is_active,
|
||||||
|
notes
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'ucepsa',
|
||||||
|
'ucepsa_onpremise',
|
||||||
|
'CUTTER_CONTEXT_V1',
|
||||||
|
90,
|
||||||
|
30,
|
||||||
|
43200,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
'La actividad física se separa con huecos >90 s y se prolonga 30 s tras la última muestra activa. Legacy es válido solo si está autorizado. La inferencia nunca alimenta el Ledger oficial.'
|
||||||
|
)
|
||||||
|
ON CONFLICT (tenant, site, policy_code)
|
||||||
|
DO UPDATE SET
|
||||||
|
activity_gap_s = EXCLUDED.activity_gap_s,
|
||||||
|
activity_tail_s = EXCLUDED.activity_tail_s,
|
||||||
|
stale_open_context_s = EXCLUDED.stale_open_context_s,
|
||||||
|
allow_legacy_context = EXCLUDED.allow_legacy_context,
|
||||||
|
require_legacy_authorization = EXCLUDED.require_legacy_authorization,
|
||||||
|
allow_inferred_for_official = EXCLUDED.allow_inferred_for_official,
|
||||||
|
is_active = true,
|
||||||
|
notes = EXCLUDED.notes,
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
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 mv_loss_intelligence.production_context_policies
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
GRANT SELECT ON mv_loss_intelligence.production_context_sessions
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
GRANT SELECT ON mv_loss_intelligence.production_context_reviews
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@ -0,0 +1,705 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_machine_running_activity_v1 AS
|
||||||
|
WITH policy AS (
|
||||||
|
SELECT
|
||||||
|
activity_gap_s::double precision AS activity_gap_s,
|
||||||
|
activity_tail_s::double precision AS activity_tail_s
|
||||||
|
FROM mv_loss_intelligence.production_context_policies
|
||||||
|
WHERE tenant = 'ucepsa'
|
||||||
|
AND site = 'ucepsa_onpremise'
|
||||||
|
AND policy_code = 'CUTTER_CONTEXT_V1'
|
||||||
|
AND is_active = true
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1
|
||||||
|
), active_samples AS (
|
||||||
|
SELECT
|
||||||
|
r.tenant,
|
||||||
|
r.site,
|
||||||
|
r.machine_id,
|
||||||
|
r.asset,
|
||||||
|
r.ingest_ts,
|
||||||
|
CASE
|
||||||
|
WHEN lower(COALESCE(
|
||||||
|
r.payload_json ->> 'machine_running',
|
||||||
|
'false'
|
||||||
|
)) IN ('true', 't', '1', 'yes')
|
||||||
|
THEN true
|
||||||
|
ELSE false
|
||||||
|
END AS machine_running,
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(r.payload_json ->> 'cycle_rate_ppm', '')::numeric,
|
||||||
|
0
|
||||||
|
) AS cycle_rate_ppm,
|
||||||
|
p.activity_gap_s,
|
||||||
|
p.activity_tail_s,
|
||||||
|
lag(r.ingest_ts) OVER (
|
||||||
|
PARTITION BY r.tenant, r.site, r.machine_id
|
||||||
|
ORDER BY r.ingest_ts
|
||||||
|
) AS previous_active_ts
|
||||||
|
FROM mv_hot.edge_oee_eastron_readings r
|
||||||
|
CROSS JOIN policy p
|
||||||
|
WHERE r.tenant = 'ucepsa'
|
||||||
|
AND r.site = 'ucepsa_onpremise'
|
||||||
|
AND r.machine_id IS NOT NULL
|
||||||
|
AND r.comm_ok = true
|
||||||
|
AND lower(COALESCE(
|
||||||
|
r.payload_json ->> 'sample_valid',
|
||||||
|
'true'
|
||||||
|
)) NOT IN ('false', 'f', '0', 'no')
|
||||||
|
AND (
|
||||||
|
lower(COALESCE(
|
||||||
|
r.payload_json ->> 'machine_running',
|
||||||
|
'false'
|
||||||
|
)) IN ('true', 't', '1', 'yes')
|
||||||
|
OR COALESCE(
|
||||||
|
NULLIF(r.payload_json ->> 'cycle_rate_ppm', '')::numeric,
|
||||||
|
0
|
||||||
|
) > 0
|
||||||
|
)
|
||||||
|
), marked AS (
|
||||||
|
SELECT
|
||||||
|
s.*,
|
||||||
|
CASE
|
||||||
|
WHEN previous_active_ts IS NULL THEN 1
|
||||||
|
WHEN ingest_ts - previous_active_ts
|
||||||
|
> make_interval(secs => activity_gap_s) THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS starts_new_activity
|
||||||
|
FROM active_samples s
|
||||||
|
), islanded AS (
|
||||||
|
SELECT
|
||||||
|
m.*,
|
||||||
|
sum(starts_new_activity) OVER (
|
||||||
|
PARTITION BY tenant, site, machine_id
|
||||||
|
ORDER BY ingest_ts
|
||||||
|
ROWS UNBOUNDED PRECEDING
|
||||||
|
) AS activity_island_id
|
||||||
|
FROM marked m
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
machine_id,
|
||||||
|
max(asset) AS asset,
|
||||||
|
activity_island_id,
|
||||||
|
'ACTIVITY:' || machine_id || ':' || activity_island_id::text
|
||||||
|
AS activity_source_key,
|
||||||
|
min(ingest_ts) AS activity_from,
|
||||||
|
max(ingest_ts)
|
||||||
|
+ make_interval(secs => max(activity_tail_s)) AS activity_to,
|
||||||
|
count(*) AS sample_count,
|
||||||
|
round(
|
||||||
|
EXTRACT(EPOCH FROM (
|
||||||
|
max(ingest_ts)
|
||||||
|
+ make_interval(secs => max(activity_tail_s))
|
||||||
|
- min(ingest_ts)
|
||||||
|
))::numeric,
|
||||||
|
2
|
||||||
|
) AS activity_duration_s,
|
||||||
|
max(activity_gap_s)::numeric(12,2) AS activity_gap_s,
|
||||||
|
max(activity_tail_s)::numeric(12,2) AS activity_tail_s
|
||||||
|
FROM islanded
|
||||||
|
GROUP BY tenant, site, machine_id, activity_island_id;
|
||||||
|
|
||||||
|
COMMENT ON VIEW mv_reports_ucepsa_prod.li_machine_running_activity_v1 IS
|
||||||
|
'Bloques de actividad física real: marcha o ciclos, separados por huecos superiores a 90 segundos. No presupone que exista Odoo.';
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_production_context_intervals_v1 AS
|
||||||
|
WITH explicit_contexts AS (
|
||||||
|
SELECT
|
||||||
|
s.tenant,
|
||||||
|
s.site,
|
||||||
|
s.machine_id,
|
||||||
|
'SESSION:' || s.context_session_id::text AS context_key,
|
||||||
|
s.context_session_id,
|
||||||
|
s.source_type AS context_source,
|
||||||
|
CASE s.source_type
|
||||||
|
WHEN 'ODOO_SHOPFLOOR' THEN 5
|
||||||
|
WHEN 'LEGACY_ERP' THEN 20
|
||||||
|
WHEN 'MANUAL_AUTHORIZED' THEN 30
|
||||||
|
WHEN 'MAINTENANCE' THEN 40
|
||||||
|
WHEN 'TEST_SETUP' THEN 50
|
||||||
|
ELSE 90
|
||||||
|
END AS context_priority,
|
||||||
|
s.started_at AS context_from,
|
||||||
|
COALESCE(s.ended_at, now()) AS context_to,
|
||||||
|
s.external_order_ref AS order_ref,
|
||||||
|
s.odoo_production_id,
|
||||||
|
s.odoo_workorder_id,
|
||||||
|
s.product_id,
|
||||||
|
s.product_default_code,
|
||||||
|
s.product_name,
|
||||||
|
s.operator_employee_id,
|
||||||
|
s.operator_name,
|
||||||
|
s.confidence,
|
||||||
|
true AS context_confirmed,
|
||||||
|
s.official_eligible AS official_loss_eligible,
|
||||||
|
s.authorization_status,
|
||||||
|
s.status,
|
||||||
|
s.notes,
|
||||||
|
s.evidence_json
|
||||||
|
FROM mv_loss_intelligence.production_context_sessions s
|
||||||
|
WHERE s.tenant = 'ucepsa'
|
||||||
|
AND s.site = 'ucepsa_onpremise'
|
||||||
|
AND s.status IN ('OPEN', 'CLOSED')
|
||||||
|
AND s.authorization_status IN ('AUTHORIZED', 'AUTO_CONFIRMED')
|
||||||
|
AND COALESCE(s.ended_at, now()) > s.started_at
|
||||||
|
), odoo_contexts AS (
|
||||||
|
SELECT
|
||||||
|
o.tenant,
|
||||||
|
o.site,
|
||||||
|
o.machine_id,
|
||||||
|
'ODOO:' || o.machine_id || ':' || o.id::text AS context_key,
|
||||||
|
NULL::bigint AS context_session_id,
|
||||||
|
'ODOO_ORDER_CONTEXT'::text AS context_source,
|
||||||
|
10 AS context_priority,
|
||||||
|
o.effective_from AS context_from,
|
||||||
|
COALESCE(o.effective_to, now()) AS context_to,
|
||||||
|
o.production_order AS order_ref,
|
||||||
|
o.production_id AS odoo_production_id,
|
||||||
|
NULLIF(o.workorder_id, '')::bigint AS odoo_workorder_id,
|
||||||
|
o.product_id,
|
||||||
|
NULL::text AS product_default_code,
|
||||||
|
o.product AS product_name,
|
||||||
|
NULL::bigint AS operator_employee_id,
|
||||||
|
NULL::text AS operator_name,
|
||||||
|
'HIGH'::text AS confidence,
|
||||||
|
true AS context_confirmed,
|
||||||
|
false AS official_loss_eligible,
|
||||||
|
'AUTO_CONFIRMED'::text AS authorization_status,
|
||||||
|
CASE
|
||||||
|
WHEN o.effective_to IS NULL THEN 'OPEN'
|
||||||
|
ELSE 'CLOSED'
|
||||||
|
END AS status,
|
||||||
|
'Contexto de orden Odoo válido. Aún no equivale a sesión oficial de operaria.'::text
|
||||||
|
AS notes,
|
||||||
|
jsonb_build_object(
|
||||||
|
'order_state_event_id', o.id,
|
||||||
|
'odoo_state', o.odoo_state,
|
||||||
|
'workorder_state', o.workorder_state,
|
||||||
|
'qty_production', o.qty_production
|
||||||
|
) AS evidence_json
|
||||||
|
FROM mv_reports_ucepsa_prod.li_order_state_intervals_v1 o
|
||||||
|
WHERE o.tenant = 'ucepsa'
|
||||||
|
AND o.site = 'ucepsa_onpremise'
|
||||||
|
AND o.order_present = true
|
||||||
|
AND o.order_coherent = true
|
||||||
|
AND COALESCE(o.effective_to, now()) > o.effective_from
|
||||||
|
)
|
||||||
|
SELECT * FROM explicit_contexts
|
||||||
|
UNION ALL
|
||||||
|
SELECT * FROM odoo_contexts;
|
||||||
|
|
||||||
|
COMMENT ON VIEW mv_reports_ucepsa_prod.li_production_context_intervals_v1 IS
|
||||||
|
'Contextos reconocidos para la migración: orden Odoo válida y sesiones explícitas legacy/manual/mantenimiento/prueba. Odoo order context todavía no se considera sesión oficial de operaria.';
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_running_activity_context_segments_v1 AS
|
||||||
|
WITH activities AS (
|
||||||
|
SELECT *
|
||||||
|
FROM mv_reports_ucepsa_prod.li_machine_running_activity_v1
|
||||||
|
), overlaps AS (
|
||||||
|
SELECT
|
||||||
|
a.tenant,
|
||||||
|
a.site,
|
||||||
|
a.machine_id,
|
||||||
|
a.asset,
|
||||||
|
a.activity_island_id,
|
||||||
|
a.activity_source_key,
|
||||||
|
a.activity_from,
|
||||||
|
a.activity_to,
|
||||||
|
a.activity_duration_s,
|
||||||
|
c.context_key,
|
||||||
|
c.context_session_id,
|
||||||
|
c.context_source,
|
||||||
|
c.context_priority,
|
||||||
|
c.context_from,
|
||||||
|
c.context_to,
|
||||||
|
c.order_ref,
|
||||||
|
c.odoo_production_id,
|
||||||
|
c.odoo_workorder_id,
|
||||||
|
c.product_id,
|
||||||
|
c.product_default_code,
|
||||||
|
c.product_name,
|
||||||
|
c.operator_employee_id,
|
||||||
|
c.operator_name,
|
||||||
|
c.confidence,
|
||||||
|
c.context_confirmed,
|
||||||
|
c.official_loss_eligible,
|
||||||
|
c.authorization_status,
|
||||||
|
c.status,
|
||||||
|
c.notes,
|
||||||
|
c.evidence_json,
|
||||||
|
greatest(a.activity_from, c.context_from) AS overlap_from,
|
||||||
|
least(a.activity_to, c.context_to) AS overlap_to
|
||||||
|
FROM activities a
|
||||||
|
JOIN mv_reports_ucepsa_prod.li_production_context_intervals_v1 c
|
||||||
|
ON c.tenant = a.tenant
|
||||||
|
AND c.site = a.site
|
||||||
|
AND c.machine_id = a.machine_id
|
||||||
|
AND a.activity_to > c.context_from
|
||||||
|
AND a.activity_from < c.context_to
|
||||||
|
WHERE least(a.activity_to, c.context_to)
|
||||||
|
> greatest(a.activity_from, c.context_from)
|
||||||
|
), boundaries AS (
|
||||||
|
SELECT
|
||||||
|
a.tenant,
|
||||||
|
a.site,
|
||||||
|
a.machine_id,
|
||||||
|
a.asset,
|
||||||
|
a.activity_island_id,
|
||||||
|
a.activity_source_key,
|
||||||
|
a.activity_from,
|
||||||
|
a.activity_to,
|
||||||
|
a.activity_duration_s,
|
||||||
|
a.activity_from AS boundary_at
|
||||||
|
FROM activities a
|
||||||
|
UNION
|
||||||
|
SELECT
|
||||||
|
a.tenant,
|
||||||
|
a.site,
|
||||||
|
a.machine_id,
|
||||||
|
a.asset,
|
||||||
|
a.activity_island_id,
|
||||||
|
a.activity_source_key,
|
||||||
|
a.activity_from,
|
||||||
|
a.activity_to,
|
||||||
|
a.activity_duration_s,
|
||||||
|
a.activity_to AS boundary_at
|
||||||
|
FROM activities a
|
||||||
|
UNION
|
||||||
|
SELECT
|
||||||
|
o.tenant,
|
||||||
|
o.site,
|
||||||
|
o.machine_id,
|
||||||
|
o.asset,
|
||||||
|
o.activity_island_id,
|
||||||
|
o.activity_source_key,
|
||||||
|
o.activity_from,
|
||||||
|
o.activity_to,
|
||||||
|
o.activity_duration_s,
|
||||||
|
o.overlap_from AS boundary_at
|
||||||
|
FROM overlaps o
|
||||||
|
UNION
|
||||||
|
SELECT
|
||||||
|
o.tenant,
|
||||||
|
o.site,
|
||||||
|
o.machine_id,
|
||||||
|
o.asset,
|
||||||
|
o.activity_island_id,
|
||||||
|
o.activity_source_key,
|
||||||
|
o.activity_from,
|
||||||
|
o.activity_to,
|
||||||
|
o.activity_duration_s,
|
||||||
|
o.overlap_to AS boundary_at
|
||||||
|
FROM overlaps o
|
||||||
|
), distinct_boundaries AS (
|
||||||
|
SELECT DISTINCT
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
machine_id,
|
||||||
|
asset,
|
||||||
|
activity_island_id,
|
||||||
|
activity_source_key,
|
||||||
|
activity_from,
|
||||||
|
activity_to,
|
||||||
|
activity_duration_s,
|
||||||
|
boundary_at
|
||||||
|
FROM boundaries
|
||||||
|
), raw_segments AS (
|
||||||
|
SELECT
|
||||||
|
b.*,
|
||||||
|
lead(boundary_at) OVER (
|
||||||
|
PARTITION BY
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
machine_id,
|
||||||
|
activity_island_id
|
||||||
|
ORDER BY boundary_at
|
||||||
|
) AS next_boundary_at
|
||||||
|
FROM distinct_boundaries b
|
||||||
|
), segments AS (
|
||||||
|
SELECT
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
machine_id,
|
||||||
|
asset,
|
||||||
|
activity_island_id,
|
||||||
|
activity_source_key,
|
||||||
|
activity_from,
|
||||||
|
activity_to,
|
||||||
|
activity_duration_s,
|
||||||
|
boundary_at AS segment_from,
|
||||||
|
next_boundary_at AS segment_to
|
||||||
|
FROM raw_segments
|
||||||
|
WHERE next_boundary_at IS NOT NULL
|
||||||
|
AND next_boundary_at > boundary_at
|
||||||
|
), segment_context_rows AS (
|
||||||
|
SELECT
|
||||||
|
s.*,
|
||||||
|
o.context_key,
|
||||||
|
o.context_session_id,
|
||||||
|
o.context_source,
|
||||||
|
o.context_priority,
|
||||||
|
o.order_ref,
|
||||||
|
o.odoo_production_id,
|
||||||
|
o.odoo_workorder_id,
|
||||||
|
o.product_id,
|
||||||
|
o.product_default_code,
|
||||||
|
o.product_name,
|
||||||
|
o.operator_employee_id,
|
||||||
|
o.operator_name,
|
||||||
|
o.confidence,
|
||||||
|
o.context_confirmed,
|
||||||
|
o.official_loss_eligible,
|
||||||
|
o.authorization_status,
|
||||||
|
o.status,
|
||||||
|
o.notes,
|
||||||
|
o.evidence_json,
|
||||||
|
row_number() OVER (
|
||||||
|
PARTITION BY
|
||||||
|
s.tenant,
|
||||||
|
s.site,
|
||||||
|
s.machine_id,
|
||||||
|
s.activity_island_id,
|
||||||
|
s.segment_from,
|
||||||
|
s.segment_to
|
||||||
|
ORDER BY
|
||||||
|
o.context_priority NULLS LAST,
|
||||||
|
o.context_key NULLS LAST
|
||||||
|
) AS context_rank
|
||||||
|
FROM segments s
|
||||||
|
LEFT JOIN overlaps o
|
||||||
|
ON o.tenant = s.tenant
|
||||||
|
AND o.site = s.site
|
||||||
|
AND o.machine_id = s.machine_id
|
||||||
|
AND o.activity_island_id = s.activity_island_id
|
||||||
|
AND s.segment_to > o.overlap_from
|
||||||
|
AND s.segment_from < o.overlap_to
|
||||||
|
), aggregated AS (
|
||||||
|
SELECT
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
machine_id,
|
||||||
|
max(asset) AS asset,
|
||||||
|
activity_island_id,
|
||||||
|
max(activity_source_key) AS activity_source_key,
|
||||||
|
max(activity_from) AS activity_from,
|
||||||
|
max(activity_to) AS activity_to,
|
||||||
|
max(activity_duration_s) AS activity_duration_s,
|
||||||
|
segment_from,
|
||||||
|
segment_to,
|
||||||
|
count(DISTINCT context_key) AS context_count,
|
||||||
|
max(context_key) FILTER (WHERE context_rank = 1) AS primary_context_key,
|
||||||
|
max(context_session_id) FILTER (WHERE context_rank = 1)
|
||||||
|
AS primary_context_session_id,
|
||||||
|
max(context_source) FILTER (WHERE context_rank = 1)
|
||||||
|
AS primary_context_source,
|
||||||
|
max(order_ref) FILTER (WHERE context_rank = 1) AS order_ref,
|
||||||
|
max(odoo_production_id) FILTER (WHERE context_rank = 1)
|
||||||
|
AS odoo_production_id,
|
||||||
|
max(odoo_workorder_id) FILTER (WHERE context_rank = 1)
|
||||||
|
AS odoo_workorder_id,
|
||||||
|
max(product_id) FILTER (WHERE context_rank = 1) AS product_id,
|
||||||
|
max(product_default_code) FILTER (WHERE context_rank = 1)
|
||||||
|
AS product_default_code,
|
||||||
|
max(product_name) FILTER (WHERE context_rank = 1) AS product_name,
|
||||||
|
max(operator_employee_id) FILTER (WHERE context_rank = 1)
|
||||||
|
AS operator_employee_id,
|
||||||
|
max(operator_name) FILTER (WHERE context_rank = 1) AS operator_name,
|
||||||
|
max(confidence) FILTER (WHERE context_rank = 1) AS confidence,
|
||||||
|
bool_or(context_confirmed) FILTER (WHERE context_key IS NOT NULL)
|
||||||
|
AS any_context_confirmed,
|
||||||
|
bool_or(official_loss_eligible) FILTER (WHERE context_key IS NOT NULL)
|
||||||
|
AS any_official_loss_eligible,
|
||||||
|
array_remove(
|
||||||
|
array_agg(context_source ORDER BY context_priority, context_key),
|
||||||
|
NULL
|
||||||
|
) AS context_sources
|
||||||
|
FROM segment_context_rows
|
||||||
|
GROUP BY
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
machine_id,
|
||||||
|
activity_island_id,
|
||||||
|
segment_from,
|
||||||
|
segment_to
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
machine_id,
|
||||||
|
asset,
|
||||||
|
activity_island_id,
|
||||||
|
activity_source_key,
|
||||||
|
activity_from,
|
||||||
|
activity_to,
|
||||||
|
activity_duration_s,
|
||||||
|
segment_from,
|
||||||
|
segment_to,
|
||||||
|
EXTRACT(EPOCH FROM (segment_to - segment_from))::numeric
|
||||||
|
AS segment_duration_s,
|
||||||
|
context_count,
|
||||||
|
primary_context_key,
|
||||||
|
primary_context_session_id,
|
||||||
|
primary_context_source,
|
||||||
|
order_ref,
|
||||||
|
odoo_production_id,
|
||||||
|
odoo_workorder_id,
|
||||||
|
product_id,
|
||||||
|
product_default_code,
|
||||||
|
product_name,
|
||||||
|
operator_employee_id,
|
||||||
|
operator_name,
|
||||||
|
confidence,
|
||||||
|
COALESCE(any_context_confirmed, false) AS context_confirmed,
|
||||||
|
CASE
|
||||||
|
WHEN context_count = 1
|
||||||
|
THEN COALESCE(any_official_loss_eligible, false)
|
||||||
|
ELSE false
|
||||||
|
END AS official_loss_eligible,
|
||||||
|
context_sources,
|
||||||
|
CASE
|
||||||
|
WHEN context_count = 0 THEN 'UNCONFIRMED_CONTEXT'
|
||||||
|
WHEN context_count > 1 THEN 'CONTEXT_CONFLICT'
|
||||||
|
WHEN primary_context_source = 'ODOO_ORDER_CONTEXT'
|
||||||
|
THEN 'ODOO_CONTEXT'
|
||||||
|
WHEN primary_context_source = 'LEGACY_ERP'
|
||||||
|
THEN 'LEGACY_AUTHORIZED'
|
||||||
|
WHEN primary_context_source = 'MANUAL_AUTHORIZED'
|
||||||
|
THEN 'MANUAL_AUTHORIZED'
|
||||||
|
WHEN primary_context_source = 'MAINTENANCE'
|
||||||
|
THEN 'MAINTENANCE'
|
||||||
|
WHEN primary_context_source = 'TEST_SETUP'
|
||||||
|
THEN 'TEST_SETUP'
|
||||||
|
WHEN primary_context_source = 'ODOO_SHOPFLOOR'
|
||||||
|
THEN 'ODOO_SHOPFLOOR'
|
||||||
|
ELSE 'INFERRED_SHADOW'
|
||||||
|
END AS context_status,
|
||||||
|
CASE
|
||||||
|
WHEN context_count = 0
|
||||||
|
THEN 'Confirmar si era legacy autorizada, prueba, mantenimiento o hueco de proceso Odoo.'
|
||||||
|
WHEN context_count > 1
|
||||||
|
THEN 'Hay más de un contexto simultáneo. Revisar antes de usar económicamente.'
|
||||||
|
WHEN primary_context_source = 'ODOO_ORDER_CONTEXT'
|
||||||
|
THEN 'Orden Odoo válida; falta todavía la sesión oficial de operaria.'
|
||||||
|
ELSE 'Contexto explícito reconocido.'
|
||||||
|
END AS context_reason
|
||||||
|
FROM aggregated;
|
||||||
|
|
||||||
|
COMMENT ON VIEW mv_reports_ucepsa_prod.li_running_activity_context_segments_v1 IS
|
||||||
|
'Segmenta toda actividad física por contexto. Sin contexto queda como bloque revisable, no como pérdida económica.';
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_context_review_queue_v1 AS
|
||||||
|
SELECT
|
||||||
|
row_number() OVER (
|
||||||
|
ORDER BY
|
||||||
|
segment_duration_s DESC,
|
||||||
|
segment_from DESC
|
||||||
|
) AS review_rank,
|
||||||
|
activity_source_key
|
||||||
|
|| ':'
|
||||||
|
|| to_char(
|
||||||
|
segment_from AT TIME ZONE 'Europe/Madrid',
|
||||||
|
'YYYYMMDDHH24MISS'
|
||||||
|
) AS context_review_key,
|
||||||
|
machine_id,
|
||||||
|
segment_from AS started_at,
|
||||||
|
segment_to AS ended_at,
|
||||||
|
round((segment_duration_s / 60.0)::numeric, 2) AS duration_min,
|
||||||
|
context_status,
|
||||||
|
context_reason,
|
||||||
|
ARRAY[
|
||||||
|
'LEGACY_AUTHORIZED',
|
||||||
|
'TEST_SETUP',
|
||||||
|
'MAINTENANCE',
|
||||||
|
'ODOO_PROCESS_GAP',
|
||||||
|
'UNKNOWN_CONTEXT'
|
||||||
|
]::text[] AS allowed_decisions,
|
||||||
|
false AS official_loss_eligible
|
||||||
|
FROM mv_reports_ucepsa_prod.li_running_activity_context_segments_v1
|
||||||
|
WHERE context_status IN ('UNCONFIRMED_CONTEXT', 'CONTEXT_CONFLICT')
|
||||||
|
AND segment_duration_s >= 60
|
||||||
|
ORDER BY review_rank;
|
||||||
|
|
||||||
|
COMMENT ON VIEW mv_reports_ucepsa_prod.li_context_review_queue_v1 IS
|
||||||
|
'Una sola fila por bloque físico sin contexto suficiente. Se revisa en producción sin clasificar ciclos o paros individualmente.';
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_production_context_coverage_v1 AS
|
||||||
|
WITH recent AS (
|
||||||
|
SELECT *
|
||||||
|
FROM mv_reports_ucepsa_prod.li_running_activity_context_segments_v1
|
||||||
|
WHERE segment_to >= now() - interval '30 days'
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
machine_id,
|
||||||
|
context_status,
|
||||||
|
count(*) AS segment_count,
|
||||||
|
round(sum(segment_duration_s / 60.0)::numeric, 2) AS minutes,
|
||||||
|
round(sum(segment_duration_s / 3600.0)::numeric, 2) AS hours,
|
||||||
|
round((
|
||||||
|
100.0
|
||||||
|
* sum(segment_duration_s)
|
||||||
|
/ NULLIF(
|
||||||
|
sum(sum(segment_duration_s)) OVER (
|
||||||
|
PARTITION BY machine_id
|
||||||
|
),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)::numeric, 2) AS machine_activity_pct
|
||||||
|
FROM recent
|
||||||
|
GROUP BY machine_id, context_status
|
||||||
|
ORDER BY machine_id, hours DESC;
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.machine_migration_context_state_v1 AS
|
||||||
|
WITH current_manual AS (
|
||||||
|
SELECT DISTINCT ON (s.machine_id)
|
||||||
|
s.machine_id,
|
||||||
|
s.context_session_id,
|
||||||
|
s.source_type,
|
||||||
|
s.external_order_ref,
|
||||||
|
s.product_name,
|
||||||
|
s.operator_employee_id,
|
||||||
|
s.operator_name,
|
||||||
|
s.started_at,
|
||||||
|
s.confidence,
|
||||||
|
s.official_eligible
|
||||||
|
FROM mv_loss_intelligence.production_context_sessions s
|
||||||
|
WHERE s.tenant = 'ucepsa'
|
||||||
|
AND s.site = 'ucepsa_onpremise'
|
||||||
|
AND s.status = 'OPEN'
|
||||||
|
AND s.authorization_status IN ('AUTHORIZED', 'AUTO_CONFIRMED')
|
||||||
|
AND now() >= s.started_at
|
||||||
|
ORDER BY
|
||||||
|
s.machine_id,
|
||||||
|
CASE s.source_type
|
||||||
|
WHEN 'LEGACY_ERP' THEN 1
|
||||||
|
WHEN 'MANUAL_AUTHORIZED' THEN 2
|
||||||
|
WHEN 'MAINTENANCE' THEN 3
|
||||||
|
WHEN 'TEST_SETUP' THEN 4
|
||||||
|
ELSE 9
|
||||||
|
END,
|
||||||
|
s.started_at DESC
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
m.*,
|
||||||
|
c.context_session_id,
|
||||||
|
c.source_type AS explicit_context_source,
|
||||||
|
c.external_order_ref AS explicit_order_ref,
|
||||||
|
c.product_name AS explicit_product_name,
|
||||||
|
c.operator_employee_id AS explicit_operator_employee_id,
|
||||||
|
c.operator_name AS explicit_operator_name,
|
||||||
|
c.started_at AS explicit_context_started_at,
|
||||||
|
CASE
|
||||||
|
WHEN m.operational_state = 'RUNNING_WITH_ORDER'
|
||||||
|
THEN 'RUNNING_ODOO_CONTEXT'
|
||||||
|
WHEN m.operational_state = 'RUNNING_WITHOUT_VALID_ORDER'
|
||||||
|
AND c.source_type = 'LEGACY_ERP'
|
||||||
|
THEN 'RUNNING_LEGACY_AUTHORIZED'
|
||||||
|
WHEN m.operational_state = 'RUNNING_WITHOUT_VALID_ORDER'
|
||||||
|
AND c.source_type = 'MANUAL_AUTHORIZED'
|
||||||
|
THEN 'RUNNING_MANUAL_AUTHORIZED'
|
||||||
|
WHEN m.operational_state = 'RUNNING_WITHOUT_VALID_ORDER'
|
||||||
|
AND c.source_type = 'MAINTENANCE'
|
||||||
|
THEN 'RUNNING_MAINTENANCE'
|
||||||
|
WHEN m.operational_state = 'RUNNING_WITHOUT_VALID_ORDER'
|
||||||
|
AND c.source_type = 'TEST_SETUP'
|
||||||
|
THEN 'RUNNING_TEST_SETUP'
|
||||||
|
WHEN m.operational_state = 'RUNNING_WITHOUT_VALID_ORDER'
|
||||||
|
THEN 'RUNNING_UNCONFIRMED_CONTEXT'
|
||||||
|
ELSE m.operational_state
|
||||||
|
END AS migration_context_state,
|
||||||
|
CASE
|
||||||
|
WHEN m.operational_state = 'RUNNING_WITH_ORDER'
|
||||||
|
THEN 'ODOO'
|
||||||
|
WHEN m.operational_state = 'RUNNING_WITHOUT_VALID_ORDER'
|
||||||
|
AND c.source_type = 'LEGACY_ERP'
|
||||||
|
THEN 'LEGACY AUTORIZADA'
|
||||||
|
WHEN m.operational_state = 'RUNNING_WITHOUT_VALID_ORDER'
|
||||||
|
AND c.source_type = 'MANUAL_AUTHORIZED'
|
||||||
|
THEN 'PRODUCCIÓN AUTORIZADA'
|
||||||
|
WHEN m.operational_state = 'RUNNING_WITHOUT_VALID_ORDER'
|
||||||
|
AND c.source_type = 'MAINTENANCE'
|
||||||
|
THEN 'MANTENIMIENTO'
|
||||||
|
WHEN m.operational_state = 'RUNNING_WITHOUT_VALID_ORDER'
|
||||||
|
AND c.source_type = 'TEST_SETUP'
|
||||||
|
THEN 'PRUEBA / AJUSTE'
|
||||||
|
WHEN m.operational_state = 'RUNNING_WITHOUT_VALID_ORDER'
|
||||||
|
THEN 'CONTEXTO PENDIENTE'
|
||||||
|
ELSE m.machine_state_label
|
||||||
|
END AS migration_context_label
|
||||||
|
FROM mv_reports_ucepsa_prod.machine_operational_state_v1 m
|
||||||
|
LEFT JOIN current_manual c ON c.machine_id = m.machine_id;
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW mv_reports_ucepsa_prod.li_data_quality_v3 AS
|
||||||
|
WITH base AS (
|
||||||
|
SELECT * FROM mv_reports_ucepsa_prod.li_data_quality_v2
|
||||||
|
), unconfirmed AS (
|
||||||
|
SELECT count(*) AS n
|
||||||
|
FROM mv_reports_ucepsa_prod.li_context_review_queue_v1
|
||||||
|
WHERE ended_at >= now() - interval '24 hours'
|
||||||
|
), conflicts AS (
|
||||||
|
SELECT count(*) AS n
|
||||||
|
FROM mv_reports_ucepsa_prod.li_running_activity_context_segments_v1
|
||||||
|
WHERE context_status = 'CONTEXT_CONFLICT'
|
||||||
|
AND segment_to >= now() - interval '24 hours'
|
||||||
|
), stale_manual AS (
|
||||||
|
SELECT count(*) AS n
|
||||||
|
FROM mv_loss_intelligence.production_context_sessions s
|
||||||
|
JOIN mv_loss_intelligence.production_context_policies p
|
||||||
|
ON p.tenant = s.tenant
|
||||||
|
AND p.site = s.site
|
||||||
|
AND p.policy_code = 'CUTTER_CONTEXT_V1'
|
||||||
|
AND p.is_active = true
|
||||||
|
WHERE s.tenant = 'ucepsa'
|
||||||
|
AND s.site = 'ucepsa_onpremise'
|
||||||
|
AND s.status = 'OPEN'
|
||||||
|
AND now() - s.started_at
|
||||||
|
> make_interval(secs => p.stale_open_context_s::double precision)
|
||||||
|
)
|
||||||
|
SELECT * FROM base
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'UNCONFIRMED_PRODUCTION_CONTEXT',
|
||||||
|
CASE WHEN n = 0 THEN 'OK' ELSE 'WARNING' END,
|
||||||
|
n,
|
||||||
|
'Bloques de actividad de las últimas 24 h sin Odoo ni contexto legacy/manual confirmado.'
|
||||||
|
FROM unconfirmed
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'CONTEXT_CONFLICT',
|
||||||
|
CASE WHEN n = 0 THEN 'OK' ELSE 'WARNING' END,
|
||||||
|
n,
|
||||||
|
'Segmentos de las últimas 24 h cubiertos por más de un contexto simultáneo.'
|
||||||
|
FROM conflicts
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'STALE_OPEN_PRODUCTION_CONTEXT',
|
||||||
|
CASE WHEN n = 0 THEN 'OK' ELSE 'WARNING' END,
|
||||||
|
n,
|
||||||
|
'Contextos manuales/legacy abiertos más tiempo que el umbral permitido.'
|
||||||
|
FROM stale_manual;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_roles WHERE rolname = 'grafana_ucepsa_ro'
|
||||||
|
) THEN
|
||||||
|
GRANT SELECT ON mv_reports_ucepsa_prod.li_machine_running_activity_v1
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
GRANT SELECT ON mv_reports_ucepsa_prod.li_production_context_intervals_v1
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
GRANT SELECT ON mv_reports_ucepsa_prod.li_running_activity_context_segments_v1
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
GRANT SELECT ON mv_reports_ucepsa_prod.li_context_review_queue_v1
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
GRANT SELECT ON mv_reports_ucepsa_prod.li_production_context_coverage_v1
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
GRANT SELECT ON mv_reports_ucepsa_prod.machine_migration_context_state_v1
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
GRANT SELECT ON mv_reports_ucepsa_prod.li_data_quality_v3
|
||||||
|
TO grafana_ucepsa_ro;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
439
ucepsa/edge-oee-demo/tools/li_context_admin.py
Normal file
439
ucepsa/edge-oee-demo/tools/li_context_admin.py
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
MESAVAULT LossIntelligence — Production Context Admin v0.3
|
||||||
|
|
||||||
|
Herramienta controlada para abrir, cerrar, crear retrospectivamente y cancelar
|
||||||
|
contextos explícitos de producción.
|
||||||
|
|
||||||
|
Seguridad:
|
||||||
|
- en modo SHADOW fuerza official_eligible=false;
|
||||||
|
- no modifica Odoo, WISE, RevPi ni balizas;
|
||||||
|
- no almacena PINs;
|
||||||
|
- usa la conexión PostgreSQL ya disponible en el entorno UCEPSA.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
import psycopg2
|
||||||
|
import psycopg2.extras
|
||||||
|
|
||||||
|
|
||||||
|
ALLOWED_SOURCES = (
|
||||||
|
"LEGACY_ERP",
|
||||||
|
"MANUAL_AUTHORIZED",
|
||||||
|
"MAINTENANCE",
|
||||||
|
"TEST_SETUP",
|
||||||
|
)
|
||||||
|
|
||||||
|
MACHINES = ("CORT-00", "CORT-01", "CORT-02")
|
||||||
|
|
||||||
|
|
||||||
|
def env(name: str, default: Optional[str] = None) -> str:
|
||||||
|
value = os.getenv(name, default)
|
||||||
|
if value is None or value == "":
|
||||||
|
raise RuntimeError(f"Falta variable de entorno {name}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def connect():
|
||||||
|
return psycopg2.connect(
|
||||||
|
host=env("PGHOST", "mv_ucepsa_postgres_hot"),
|
||||||
|
port=int(env("PGPORT", "5432")),
|
||||||
|
dbname=env("PGDATABASE"),
|
||||||
|
user=env("PGUSER"),
|
||||||
|
password=env("PGPASSWORD"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ts(value: Optional[str]) -> Optional[datetime]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if value.lower() == "now":
|
||||||
|
return datetime.now(timezone.utc)
|
||||||
|
parsed = datetime.fromisoformat(value)
|
||||||
|
if parsed.tzinfo is None:
|
||||||
|
raise ValueError(
|
||||||
|
"La fecha debe incluir zona horaria, por ejemplo 2026-07-17T10:30:00+02:00"
|
||||||
|
)
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def deployment_mode(cur) -> str:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT mode
|
||||||
|
FROM mv_loss_intelligence.deployment_config
|
||||||
|
WHERE tenant = 'ucepsa'
|
||||||
|
AND site = 'ucepsa_onpremise'
|
||||||
|
AND scope_code = 'CUTTERS'
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
raise RuntimeError("No existe deployment_config CUTTERS")
|
||||||
|
return str(row[0])
|
||||||
|
|
||||||
|
|
||||||
|
def print_json(payload: Any) -> None:
|
||||||
|
print(json.dumps(payload, indent=2, ensure_ascii=False, default=str))
|
||||||
|
|
||||||
|
|
||||||
|
def open_context(args) -> None:
|
||||||
|
started_at = parse_ts(args.started_at) or datetime.now(timezone.utc)
|
||||||
|
with connect() as conn:
|
||||||
|
with conn.cursor(
|
||||||
|
cursor_factory=psycopg2.extras.RealDictCursor
|
||||||
|
) as cur:
|
||||||
|
mode = deployment_mode(cur)
|
||||||
|
official = bool(args.official_eligible and mode == "ACTIVE")
|
||||||
|
if args.official_eligible and mode != "ACTIVE":
|
||||||
|
print(
|
||||||
|
"[WARN] El despliegue está en SHADOW: official_eligible se fuerza a false.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO mv_loss_intelligence.production_context_sessions (
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
machine_id,
|
||||||
|
source_type,
|
||||||
|
source_ref,
|
||||||
|
external_order_ref,
|
||||||
|
product_id,
|
||||||
|
product_default_code,
|
||||||
|
product_name,
|
||||||
|
operator_employee_id,
|
||||||
|
operator_name,
|
||||||
|
started_at,
|
||||||
|
status,
|
||||||
|
authorization_status,
|
||||||
|
confidence,
|
||||||
|
official_eligible,
|
||||||
|
authorized_by,
|
||||||
|
authorized_at,
|
||||||
|
notes,
|
||||||
|
evidence_json
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'ucepsa',
|
||||||
|
'ucepsa_onpremise',
|
||||||
|
%(machine_id)s,
|
||||||
|
%(source_type)s,
|
||||||
|
%(source_ref)s,
|
||||||
|
%(external_order_ref)s,
|
||||||
|
%(product_id)s,
|
||||||
|
%(product_default_code)s,
|
||||||
|
%(product_name)s,
|
||||||
|
%(operator_employee_id)s,
|
||||||
|
%(operator_name)s,
|
||||||
|
%(started_at)s,
|
||||||
|
'OPEN',
|
||||||
|
'AUTHORIZED',
|
||||||
|
%(confidence)s,
|
||||||
|
%(official_eligible)s,
|
||||||
|
%(authorized_by)s,
|
||||||
|
now(),
|
||||||
|
%(notes)s,
|
||||||
|
%(evidence_json)s
|
||||||
|
)
|
||||||
|
RETURNING *
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"machine_id": args.machine,
|
||||||
|
"source_type": args.source,
|
||||||
|
"source_ref": args.source_ref,
|
||||||
|
"external_order_ref": args.external_order_ref,
|
||||||
|
"product_id": args.product_id,
|
||||||
|
"product_default_code": args.product_code,
|
||||||
|
"product_name": args.product_name,
|
||||||
|
"operator_employee_id": args.operator_employee_id,
|
||||||
|
"operator_name": args.operator_name,
|
||||||
|
"started_at": started_at,
|
||||||
|
"confidence": args.confidence,
|
||||||
|
"official_eligible": official,
|
||||||
|
"authorized_by": args.authorized_by,
|
||||||
|
"notes": args.notes,
|
||||||
|
"evidence_json": psycopg2.extras.Json({
|
||||||
|
"created_with": "li_context_admin.py",
|
||||||
|
"deployment_mode": mode,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
print_json(cur.fetchone())
|
||||||
|
|
||||||
|
|
||||||
|
def close_context(args) -> None:
|
||||||
|
ended_at = parse_ts(args.ended_at) or datetime.now(timezone.utc)
|
||||||
|
with connect() as conn:
|
||||||
|
with conn.cursor(
|
||||||
|
cursor_factory=psycopg2.extras.RealDictCursor
|
||||||
|
) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
UPDATE mv_loss_intelligence.production_context_sessions
|
||||||
|
SET
|
||||||
|
ended_at = %(ended_at)s,
|
||||||
|
status = 'CLOSED',
|
||||||
|
closed_by = %(closed_by)s,
|
||||||
|
notes = concat_ws(
|
||||||
|
E'\n',
|
||||||
|
notes,
|
||||||
|
%(notes)s
|
||||||
|
),
|
||||||
|
updated_at = now()
|
||||||
|
WHERE context_session_id = %(context_session_id)s
|
||||||
|
AND status = 'OPEN'
|
||||||
|
AND ended_at IS NULL
|
||||||
|
AND %(ended_at)s > started_at
|
||||||
|
RETURNING *
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"context_session_id": args.session_id,
|
||||||
|
"ended_at": ended_at,
|
||||||
|
"closed_by": args.closed_by,
|
||||||
|
"notes": args.notes,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
raise RuntimeError(
|
||||||
|
"No se ha cerrado ninguna sesión. Comprueba ID, estado y fechas."
|
||||||
|
)
|
||||||
|
print_json(row)
|
||||||
|
|
||||||
|
|
||||||
|
def create_retrospective(args) -> None:
|
||||||
|
started_at = parse_ts(args.started_at)
|
||||||
|
ended_at = parse_ts(args.ended_at)
|
||||||
|
if not started_at or not ended_at or ended_at <= started_at:
|
||||||
|
raise ValueError("El intervalo retrospectivo debe tener inicio y fin válidos.")
|
||||||
|
|
||||||
|
with connect() as conn:
|
||||||
|
with conn.cursor(
|
||||||
|
cursor_factory=psycopg2.extras.RealDictCursor
|
||||||
|
) as cur:
|
||||||
|
mode = deployment_mode(cur)
|
||||||
|
official = bool(args.official_eligible and mode == "ACTIVE")
|
||||||
|
if args.official_eligible and mode != "ACTIVE":
|
||||||
|
print(
|
||||||
|
"[WARN] El despliegue está en SHADOW: official_eligible se fuerza a false.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO mv_loss_intelligence.production_context_sessions (
|
||||||
|
tenant,
|
||||||
|
site,
|
||||||
|
machine_id,
|
||||||
|
source_type,
|
||||||
|
source_ref,
|
||||||
|
external_order_ref,
|
||||||
|
product_id,
|
||||||
|
product_default_code,
|
||||||
|
product_name,
|
||||||
|
operator_employee_id,
|
||||||
|
operator_name,
|
||||||
|
started_at,
|
||||||
|
ended_at,
|
||||||
|
status,
|
||||||
|
authorization_status,
|
||||||
|
confidence,
|
||||||
|
official_eligible,
|
||||||
|
authorized_by,
|
||||||
|
authorized_at,
|
||||||
|
closed_by,
|
||||||
|
notes,
|
||||||
|
evidence_json
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'ucepsa',
|
||||||
|
'ucepsa_onpremise',
|
||||||
|
%(machine_id)s,
|
||||||
|
%(source_type)s,
|
||||||
|
%(source_ref)s,
|
||||||
|
%(external_order_ref)s,
|
||||||
|
%(product_id)s,
|
||||||
|
%(product_default_code)s,
|
||||||
|
%(product_name)s,
|
||||||
|
%(operator_employee_id)s,
|
||||||
|
%(operator_name)s,
|
||||||
|
%(started_at)s,
|
||||||
|
%(ended_at)s,
|
||||||
|
'CLOSED',
|
||||||
|
'AUTHORIZED',
|
||||||
|
%(confidence)s,
|
||||||
|
%(official_eligible)s,
|
||||||
|
%(authorized_by)s,
|
||||||
|
now(),
|
||||||
|
%(closed_by)s,
|
||||||
|
%(notes)s,
|
||||||
|
%(evidence_json)s
|
||||||
|
)
|
||||||
|
RETURNING *
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"machine_id": args.machine,
|
||||||
|
"source_type": args.source,
|
||||||
|
"source_ref": args.source_ref,
|
||||||
|
"external_order_ref": args.external_order_ref,
|
||||||
|
"product_id": args.product_id,
|
||||||
|
"product_default_code": args.product_code,
|
||||||
|
"product_name": args.product_name,
|
||||||
|
"operator_employee_id": args.operator_employee_id,
|
||||||
|
"operator_name": args.operator_name,
|
||||||
|
"started_at": started_at,
|
||||||
|
"ended_at": ended_at,
|
||||||
|
"confidence": args.confidence,
|
||||||
|
"official_eligible": official,
|
||||||
|
"authorized_by": args.authorized_by,
|
||||||
|
"closed_by": args.closed_by,
|
||||||
|
"notes": args.notes,
|
||||||
|
"evidence_json": psycopg2.extras.Json({
|
||||||
|
"created_with": "li_context_admin.py",
|
||||||
|
"deployment_mode": mode,
|
||||||
|
"retrospective": True,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
print_json(cur.fetchone())
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_context(args) -> None:
|
||||||
|
with connect() as conn:
|
||||||
|
with conn.cursor(
|
||||||
|
cursor_factory=psycopg2.extras.RealDictCursor
|
||||||
|
) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
UPDATE mv_loss_intelligence.production_context_sessions
|
||||||
|
SET
|
||||||
|
status = 'CANCELLED',
|
||||||
|
ended_at = COALESCE(ended_at, now()),
|
||||||
|
closed_by = %(closed_by)s,
|
||||||
|
notes = concat_ws(E'\n', notes, %(notes)s),
|
||||||
|
updated_at = now()
|
||||||
|
WHERE context_session_id = %(context_session_id)s
|
||||||
|
AND status <> 'CANCELLED'
|
||||||
|
RETURNING *
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"context_session_id": args.session_id,
|
||||||
|
"closed_by": args.closed_by,
|
||||||
|
"notes": args.notes,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
raise RuntimeError("No se ha cancelado ninguna sesión.")
|
||||||
|
print_json(row)
|
||||||
|
|
||||||
|
|
||||||
|
def list_contexts(args) -> None:
|
||||||
|
with connect() as conn:
|
||||||
|
with conn.cursor(
|
||||||
|
cursor_factory=psycopg2.extras.RealDictCursor
|
||||||
|
) as cur:
|
||||||
|
where = [
|
||||||
|
"tenant = 'ucepsa'",
|
||||||
|
"site = 'ucepsa_onpremise'",
|
||||||
|
]
|
||||||
|
params: Dict[str, Any] = {"limit": args.limit}
|
||||||
|
if args.machine:
|
||||||
|
where.append("machine_id = %(machine_id)s")
|
||||||
|
params["machine_id"] = args.machine
|
||||||
|
if args.open_only:
|
||||||
|
where.append("status = 'OPEN'")
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
f"""
|
||||||
|
SELECT *
|
||||||
|
FROM mv_loss_intelligence.production_context_sessions
|
||||||
|
WHERE {' AND '.join(where)}
|
||||||
|
ORDER BY context_session_id DESC
|
||||||
|
LIMIT %(limit)s
|
||||||
|
""",
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
print_json(cur.fetchall())
|
||||||
|
|
||||||
|
|
||||||
|
def add_common_context_args(parser) -> None:
|
||||||
|
parser.add_argument("--machine", required=True, choices=MACHINES)
|
||||||
|
parser.add_argument("--source", required=True, choices=ALLOWED_SOURCES)
|
||||||
|
parser.add_argument("--source-ref")
|
||||||
|
parser.add_argument("--external-order-ref")
|
||||||
|
parser.add_argument("--product-id", type=int)
|
||||||
|
parser.add_argument("--product-code")
|
||||||
|
parser.add_argument("--product-name")
|
||||||
|
parser.add_argument("--operator-employee-id", type=int)
|
||||||
|
parser.add_argument("--operator-name")
|
||||||
|
parser.add_argument(
|
||||||
|
"--confidence",
|
||||||
|
choices=("HIGH", "MEDIUM", "LOW"),
|
||||||
|
default="MEDIUM",
|
||||||
|
)
|
||||||
|
parser.add_argument("--authorized-by", required=True)
|
||||||
|
parser.add_argument("--notes")
|
||||||
|
parser.add_argument("--official-eligible", action="store_true")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Administración controlada de contextos productivos"
|
||||||
|
)
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
p_open = sub.add_parser("open")
|
||||||
|
add_common_context_args(p_open)
|
||||||
|
p_open.add_argument("--started-at", default="now")
|
||||||
|
p_open.set_defaults(func=open_context)
|
||||||
|
|
||||||
|
p_close = sub.add_parser("close")
|
||||||
|
p_close.add_argument("--session-id", type=int, required=True)
|
||||||
|
p_close.add_argument("--ended-at", default="now")
|
||||||
|
p_close.add_argument("--closed-by", required=True)
|
||||||
|
p_close.add_argument("--notes")
|
||||||
|
p_close.set_defaults(func=close_context)
|
||||||
|
|
||||||
|
p_create = sub.add_parser("create-retrospective")
|
||||||
|
add_common_context_args(p_create)
|
||||||
|
p_create.add_argument("--started-at", required=True)
|
||||||
|
p_create.add_argument("--ended-at", required=True)
|
||||||
|
p_create.add_argument("--closed-by", required=True)
|
||||||
|
p_create.set_defaults(func=create_retrospective)
|
||||||
|
|
||||||
|
p_cancel = sub.add_parser("cancel")
|
||||||
|
p_cancel.add_argument("--session-id", type=int, required=True)
|
||||||
|
p_cancel.add_argument("--closed-by", required=True)
|
||||||
|
p_cancel.add_argument("--notes")
|
||||||
|
p_cancel.set_defaults(func=cancel_context)
|
||||||
|
|
||||||
|
p_list = sub.add_parser("list")
|
||||||
|
p_list.add_argument("--machine", choices=MACHINES)
|
||||||
|
p_list.add_argument("--open-only", action="store_true")
|
||||||
|
p_list.add_argument("--limit", type=int, default=30)
|
||||||
|
p_list.set_defaults(func=list_contexts)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.func(args)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
Loading…
x
Reference in New Issue
Block a user