From cc951b713d9db44eff55f6d74b56cc0d97a7f406 Mon Sep 17 00:00:00 2001 From: Victor Fraile Garcia Date: Sat, 30 May 2026 20:30:27 +0200 Subject: [PATCH] Add Data Quality Contract cockpit --- .../services/order-report-api/app/main.py | 235 ++++ .../app/templates/cost_readiness.html | 3 + .../app/templates/data_quality_contract.html | 296 +++++ .../app/templates/orders_index.html | 3 + .../sql/reports/090_data_quality_contract.sql | 1166 +++++++++++++++++ 5 files changed, 1703 insertions(+) create mode 100644 ucepsa/edge-oee-demo/services/order-report-api/app/templates/data_quality_contract.html create mode 100644 ucepsa/edge-oee-demo/sql/reports/090_data_quality_contract.sql diff --git a/ucepsa/edge-oee-demo/services/order-report-api/app/main.py b/ucepsa/edge-oee-demo/services/order-report-api/app/main.py index 5918a0d..8c47758 100644 --- a/ucepsa/edge-oee-demo/services/order-report-api/app/main.py +++ b/ucepsa/edge-oee-demo/services/order-report-api/app/main.py @@ -397,3 +397,238 @@ def orders_html(request: Request, limit: int = Query(50, ge=1, le=200)): }, ) + +@app.get("/api/data-quality-contract") +def api_data_quality_contract(limit: int = Query(100, ge=1, le=500)): + summary = fetch_one( + """ + SELECT + count(*) AS total_orders, + count(*) FILTER (WHERE contract_status = 'CONTRATO_CUMPLIDO') AS contract_ok_orders, + count(*) FILTER (WHERE contract_status = 'VALIDO_CON_OBSERVACIONES') AS contract_warn_orders, + count(*) FILTER (WHERE contract_status = 'BLOQUEADO_CRITICO') AS blocked_critical_orders, + count(*) FILTER (WHERE contract_status = 'BLOQUEADO_ALTO') AS blocked_high_orders, + count(*) FILTER (WHERE contract_status = 'BLOQUEADO_MEDIO') AS blocked_medium_orders, + round(avg(contract_score), 2) AS avg_contract_score, + sum(critical_failures) AS total_critical_failures, + sum(high_failures) AS total_high_failures, + sum(medium_failures) AS total_medium_failures, + sum(total_issue_score) AS total_issue_score + FROM mv_reports_ucepsa_demo.data_quality_contract_order_summary_v1 + """, + (), + ) + + orders = fetch_all( + """ + SELECT + odoo_order_ref, + odoo_state, + sale_order_ref, + customer_name, + product_default_code, + product_name, + machine_id, + contract_score, + contract_status, + failed_rules, + warn_rules, + critical_failures, + high_failures, + medium_failures, + main_blocker_rule_code, + main_blocker_rule_name, + main_blocker_domain, + main_blocker_severity, + owner_area_to_act, + main_blocker_finding, + main_recommended_action, + contract_summary + FROM mv_reports_ucepsa_demo.data_quality_contract_order_summary_v1 + ORDER BY contract_score ASC, total_issue_score DESC, odoo_order_ref DESC + LIMIT %s + """, + (limit,), + ) + + owners = fetch_all( + """ + SELECT + owner_area, + failed_checks, + warn_checks, + critical_failed_checks, + high_failed_checks, + affected_orders, + total_issue_score, + affected_rules, + affected_order_refs + FROM mv_reports_ucepsa_demo.data_quality_contract_owner_summary_v1 + WHERE failed_checks > 0 + OR warn_checks > 0 + ORDER BY total_issue_score DESC NULLS LAST, owner_area + """, + (), + ) + + rules = fetch_all( + """ + SELECT + rule_code, + rule_name, + domain, + severity, + owner_area, + check_status, + count(*) AS order_count, + sum(issue_score) AS total_issue_score, + string_agg( + DISTINCT odoo_order_ref, + ', ' + ORDER BY odoo_order_ref + ) AS affected_orders + FROM mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 + WHERE check_status IN ('FAIL', 'WARN') + GROUP BY + rule_code, + rule_name, + domain, + severity, + owner_area, + check_status + ORDER BY + total_issue_score DESC NULLS LAST, + rule_code, + check_status + """, + (), + ) + + return jsonable_encoder( + { + "summary": summary or {}, + "orders": orders, + "owners": owners, + "rules": rules, + } + ) + + +@app.get("/data-quality-contract.html", response_class=HTMLResponse) +def data_quality_contract_html(request: Request, limit: int = Query(100, ge=1, le=500)): + summary = fetch_one( + """ + SELECT + count(*) AS total_orders, + count(*) FILTER (WHERE contract_status = 'CONTRATO_CUMPLIDO') AS contract_ok_orders, + count(*) FILTER (WHERE contract_status = 'VALIDO_CON_OBSERVACIONES') AS contract_warn_orders, + count(*) FILTER (WHERE contract_status = 'BLOQUEADO_CRITICO') AS blocked_critical_orders, + count(*) FILTER (WHERE contract_status = 'BLOQUEADO_ALTO') AS blocked_high_orders, + count(*) FILTER (WHERE contract_status = 'BLOQUEADO_MEDIO') AS blocked_medium_orders, + round(avg(contract_score), 2) AS avg_contract_score, + sum(critical_failures) AS total_critical_failures, + sum(high_failures) AS total_high_failures, + sum(medium_failures) AS total_medium_failures, + sum(total_issue_score) AS total_issue_score + FROM mv_reports_ucepsa_demo.data_quality_contract_order_summary_v1 + """, + (), + ) + + orders = fetch_all( + """ + SELECT + odoo_order_ref, + odoo_state, + sale_order_ref, + customer_name, + product_default_code, + product_name, + machine_id, + contract_score, + contract_status, + failed_rules, + warn_rules, + critical_failures, + high_failures, + medium_failures, + main_blocker_rule_code, + main_blocker_rule_name, + main_blocker_domain, + main_blocker_severity, + owner_area_to_act, + main_blocker_finding, + main_recommended_action, + contract_summary + FROM mv_reports_ucepsa_demo.data_quality_contract_order_summary_v1 + ORDER BY contract_score ASC, total_issue_score DESC, odoo_order_ref DESC + LIMIT %s + """, + (limit,), + ) + + owners = fetch_all( + """ + SELECT + owner_area, + failed_checks, + warn_checks, + critical_failed_checks, + high_failed_checks, + affected_orders, + total_issue_score, + affected_rules, + affected_order_refs + FROM mv_reports_ucepsa_demo.data_quality_contract_owner_summary_v1 + WHERE failed_checks > 0 + OR warn_checks > 0 + ORDER BY total_issue_score DESC NULLS LAST, owner_area + """, + (), + ) + + rules = fetch_all( + """ + SELECT + rule_code, + rule_name, + domain, + severity, + owner_area, + check_status, + count(*) AS order_count, + sum(issue_score) AS total_issue_score, + string_agg( + DISTINCT odoo_order_ref, + ', ' + ORDER BY odoo_order_ref + ) AS affected_orders + FROM mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 + WHERE check_status IN ('FAIL', 'WARN') + GROUP BY + rule_code, + rule_name, + domain, + severity, + owner_area, + check_status + ORDER BY + total_issue_score DESC NULLS LAST, + rule_code, + check_status + """, + (), + ) + + return templates.TemplateResponse( + "data_quality_contract.html", + { + "request": request, + "title": REPORT_TITLE, + "summary": summary or {}, + "orders": orders, + "owners": owners, + "rules": rules, + }, + ) + diff --git a/ucepsa/edge-oee-demo/services/order-report-api/app/templates/cost_readiness.html b/ucepsa/edge-oee-demo/services/order-report-api/app/templates/cost_readiness.html index cdc9e23..03b6245 100644 --- a/ucepsa/edge-oee-demo/services/order-report-api/app/templates/cost_readiness.html +++ b/ucepsa/edge-oee-demo/services/order-report-api/app/templates/cost_readiness.html @@ -111,6 +111,9 @@

Cost Readiness Cockpit

{{ title }}

+

+ Abrir Data Quality Contract +

Esta pantalla muestra qué órdenes están preparadas para cálculo de margen y qué datos lo bloquean. No calcula margen real si falta coste de materia prima. diff --git a/ucepsa/edge-oee-demo/services/order-report-api/app/templates/data_quality_contract.html b/ucepsa/edge-oee-demo/services/order-report-api/app/templates/data_quality_contract.html new file mode 100644 index 0000000..c31e9bc --- /dev/null +++ b/ucepsa/edge-oee-demo/services/order-report-api/app/templates/data_quality_contract.html @@ -0,0 +1,296 @@ + + + + + {{ title }} - Data Quality Contract + + + +

+ +

Data Quality Contract Odoo–MESAVAULT

+

{{ title }}

+

+ Esta pantalla resume si las órdenes cumplen el contrato mínimo de datos para informes técnicos, + margen industrial, pérdidas y readiness económico. +

+ +

+ Órdenes +  |  + Cost Readiness +

+ +
+
+
Órdenes
+
{{ summary.total_orders or 0 }}
+
+
+
Contrato cumplido
+
{{ summary.contract_ok_orders or 0 }}
+
+
+
Con observaciones
+
{{ summary.contract_warn_orders or 0 }}
+
+
+
Bloqueo crítico
+
{{ summary.blocked_critical_orders or 0 }}
+
+
+
Fallos críticos
+
{{ summary.total_critical_failures or 0 }}
+
+
+
Score medio
+
{{ summary.avg_contract_score|num(2) if summary.avg_contract_score is not none else "0.00" }}
+
+
+ +

1. Estado del contrato por orden

+ + + + + + + + + + + + + + + + {% for order in orders %} + + + + + + + + + + + + + + + {% else %} + + {% endfor %} +
OrdenPedido ventaClienteProductoMáquinaScoreEstado contratoFallosBloqueo principalÁrea responsableAcción recomendadaInforme
{{ order.odoo_order_ref }}{{ order.sale_order_ref or "" }}{{ order.customer_name or "" }} + {{ order.product_default_code or "" }}
+ {{ order.product_name or "" }} +
{{ order.machine_id or "" }} + {% if order.contract_score >= 80 %} + {{ order.contract_score }} + {% elif order.contract_score >= 50 %} + {{ order.contract_score }} + {% else %} + {{ order.contract_score }} + {% endif %} + + {% if order.contract_status == 'CONTRATO_CUMPLIDO' %} + Contrato cumplido + {% elif order.contract_status == 'VALIDO_CON_OBSERVACIONES' %} + Válido con observaciones + {% elif order.contract_status == 'BLOQUEADO_CRITICO' %} + Bloqueado crítico + {% elif order.contract_status == 'BLOQUEADO_ALTO' %} + Bloqueado alto + {% elif order.contract_status == 'BLOQUEADO_MEDIO' %} + Bloqueado medio + {% else %} + {{ order.contract_status }} + {% endif %} + + Críticos: {{ order.critical_failures }}
+ Altos: {{ order.high_failures }}
+ Medios: {{ order.medium_failures }}
+ Avisos: {{ order.warn_rules }} +
+ {{ order.main_blocker_rule_code or "" }}
+ {{ order.main_blocker_rule_name or "" }}
+ {{ order.main_blocker_finding or "" }} +
{{ order.owner_area_to_act or "" }}{{ order.main_recommended_action or "" }} + HTML + | + PDF +
No hay órdenes evaluadas.
+ +

2. Resumen por área responsable

+ + + + + + + + + + + + + {% for owner in owners %} + + + + + + + + + + + + {% else %} + + {% endfor %} +
ÁreaFallosAvisosFallos críticosFallos altosÓrdenes afectadasScore incidenciaReglas afectadasÓrdenes
{{ owner.owner_area }}{{ owner.failed_checks }}{{ owner.warn_checks }}{{ owner.critical_failed_checks }}{{ owner.high_failed_checks }}{{ owner.affected_orders }}{{ owner.total_issue_score }}{{ owner.affected_rules }}{{ owner.affected_order_refs }}
Sin incidencias por área.
+ +

3. Reglas con incidencias

+ + + + + + + + + + + + + {% for rule in rules %} + + + + + + + + + + + + {% else %} + + {% endfor %} +
ReglaNombreDominioSeveridadEstadoÁreaÓrdenes afectadasScore totalÓrdenes
{{ rule.rule_code }}{{ rule.rule_name }}{{ rule.domain }}{{ rule.severity }} + {% if rule.check_status == 'FAIL' %} + FAIL + {% elif rule.check_status == 'WARN' %} + WARN + {% else %} + {{ rule.check_status }} + {% endif %} + {{ rule.owner_area }}{{ rule.order_count }}{{ rule.total_issue_score }}{{ rule.affected_orders }}
Sin reglas con incidencias.
+ +
+ + diff --git a/ucepsa/edge-oee-demo/services/order-report-api/app/templates/orders_index.html b/ucepsa/edge-oee-demo/services/order-report-api/app/templates/orders_index.html index 329fa43..5a85006 100644 --- a/ucepsa/edge-oee-demo/services/order-report-api/app/templates/orders_index.html +++ b/ucepsa/edge-oee-demo/services/order-report-api/app/templates/orders_index.html @@ -64,6 +64,9 @@

Informes de ejecución de orden

{{ title }}

+

+ Abrir Data Quality Contract +

Abrir Cost Readiness Cockpit diff --git a/ucepsa/edge-oee-demo/sql/reports/090_data_quality_contract.sql b/ucepsa/edge-oee-demo/sql/reports/090_data_quality_contract.sql new file mode 100644 index 0000000..e58bd57 --- /dev/null +++ b/ucepsa/edge-oee-demo/sql/reports/090_data_quality_contract.sql @@ -0,0 +1,1166 @@ +-- MESAVAULT Edge-OEE UCEPSA +-- Data Quality Contract Odoo-MESAVAULT +-- Schema + seed rules. +-- No secrets. + +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.data_quality_contract_owner_summary_v1; +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.data_quality_contract_order_summary_v1; +DROP VIEW IF EXISTS mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1; + +DROP TABLE IF EXISTS mv_edge_oee_ucepsa_demo.data_quality_contract_rules; + +-- +-- PostgreSQL database dump +-- + +\restrict Jszr4ruwRiCXF4itba8oT9UnTRsL1bJGSsYnj9UgpfOpaiQUSblgKdozAITLZdr + +-- Dumped from database version 16.14 +-- Dumped by pg_dump version 16.14 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: data_quality_contract_rules; Type: TABLE; Schema: mv_edge_oee_ucepsa_demo; Owner: - +-- + +CREATE TABLE mv_edge_oee_ucepsa_demo.data_quality_contract_rules ( + id bigint NOT NULL, + tenant text DEFAULT 'ucepsa'::text NOT NULL, + site text DEFAULT 'ucepsa_test'::text NOT NULL, + rule_code text NOT NULL, + rule_name text NOT NULL, + domain text NOT NULL, + dimension text NOT NULL, + severity text NOT NULL, + owner_area text NOT NULL, + applies_to text NOT NULL, + description text NOT NULL, + expected_condition text NOT NULL, + blocking_for text, + is_active boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: data_quality_contract_rules_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_demo; Owner: - +-- + +CREATE SEQUENCE mv_edge_oee_ucepsa_demo.data_quality_contract_rules_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: data_quality_contract_rules_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_demo; Owner: - +-- + +ALTER SEQUENCE mv_edge_oee_ucepsa_demo.data_quality_contract_rules_id_seq OWNED BY mv_edge_oee_ucepsa_demo.data_quality_contract_rules.id; + + +-- +-- Name: data_quality_contract_order_checks_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - +-- + +CREATE VIEW mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 AS + WITH raw_stats AS ( + SELECT order_raw_material_costs_v1.odoo_instance, + order_raw_material_costs_v1.odoo_production_id, + count(*) AS raw_line_count, + count(*) FILTER (WHERE ((order_raw_material_costs_v1.raw_product_default_code IS NULL) OR (TRIM(BOTH FROM order_raw_material_costs_v1.raw_product_default_code) = ''::text))) AS raw_missing_code_count, + string_agg(DISTINCT concat(COALESCE(order_raw_material_costs_v1.raw_product_default_code, 'sin_codigo'::text), ' - ', COALESCE(order_raw_material_costs_v1.raw_product_name, 'sin_nombre'::text)), '; '::text ORDER BY (concat(COALESCE(order_raw_material_costs_v1.raw_product_default_code, 'sin_codigo'::text), ' - ', COALESCE(order_raw_material_costs_v1.raw_product_name, 'sin_nombre'::text)))) AS raw_materials_detected + FROM mv_reports_ucepsa_demo.order_raw_material_costs_v1 + GROUP BY order_raw_material_costs_v1.odoo_instance, order_raw_material_costs_v1.odoo_production_id + ), base AS ( + SELECT m.order_row_id, + m.tenant, + m.site, + m.odoo_instance, + m.odoo_workorder_id, + m.odoo_production_id, + m.odoo_order_ref, + m.odoo_state, + m.sale_order_ref, + m.customer_name, + m.revenue_net_eur, + m.machine_id, + m.asset, + m.product_default_code, + m.product_name, + m.raw_material_line_count, + m.raw_total_consumed_qty, + m.raw_materials_summary, + m.raw_material_cost_status_summary, + m.missing_or_invalid_raw_material_cost_count, + m.material_cost_estimated_eur, + m.material_cost_source_status, + m.energy_cost_eur_per_kwh, + m.machine_run_cost_eur_per_h, + m.kwh_consumed, + m.run_min, + m.stop_cost_eur, + m.technical_quality_level, + m.final_report_status, + m.margin_status, + r_1.pending_stops, + r_1.production_conversion_status, + COALESCE(rs.raw_line_count, (0)::bigint) AS dqc_raw_line_count, + COALESCE(rs.raw_missing_code_count, (0)::bigint) AS dqc_raw_missing_code_count, + rs.raw_materials_detected + FROM ((mv_reports_ucepsa_demo.order_margin_report_v1 m + LEFT JOIN mv_reports_ucepsa_demo.order_execution_report_full_v1 r_1 ON ((r_1.order_row_id = m.order_row_id))) + LEFT JOIN raw_stats rs ON (((rs.odoo_instance = m.odoo_instance) AND (rs.odoo_production_id = m.odoo_production_id)))) + ), evaluations AS ( + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-ODOO-001'::text AS rule_code, + CASE + WHEN ((b.odoo_order_ref IS NOT NULL) AND (b.odoo_production_id IS NOT NULL)) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN ((b.odoo_order_ref IS NOT NULL) AND (b.odoo_production_id IS NOT NULL)) THEN 'Orden de fabricación correctamente identificada.'::text + ELSE 'Falta referencia de orden o identificador de producción Odoo.'::text + END AS finding, + CASE + WHEN ((b.odoo_order_ref IS NOT NULL) AND (b.odoo_production_id IS NOT NULL)) THEN 'Sin acción.'::text + ELSE 'Revisar sincronización de mrp.production / mrp.workorder desde Odoo.'::text + END AS recommended_action, + jsonb_build_object('odoo_order_ref', b.odoo_order_ref, 'odoo_production_id', b.odoo_production_id) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-ODOO-002'::text AS rule_code, + CASE + WHEN ((b.machine_id IS NOT NULL) AND (TRIM(BOTH FROM b.machine_id) <> ''::text)) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN ((b.machine_id IS NOT NULL) AND (TRIM(BOTH FROM b.machine_id) <> ''::text)) THEN 'Centro de trabajo mapeado a máquina MESAVAULT.'::text + ELSE 'Falta machine_id o mapeo de centro de trabajo.'::text + END AS finding, + CASE + WHEN ((b.machine_id IS NOT NULL) AND (TRIM(BOTH FROM b.machine_id) <> ''::text)) THEN 'Sin acción.'::text + ELSE 'Revisar tabla de mapeo Odoo workcenter → machine_id.'::text + END AS recommended_action, + jsonb_build_object('machine_id', b.machine_id, 'asset', b.asset) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-ODOO-003'::text AS rule_code, + CASE + WHEN (COALESCE(NULLIF(TRIM(BOTH FROM b.product_name), ''::text), NULLIF(TRIM(BOTH FROM b.product_default_code), ''::text)) IS NULL) THEN 'FAIL'::text + WHEN ((b.product_default_code IS NULL) OR (TRIM(BOTH FROM b.product_default_code) = ''::text)) THEN 'WARN'::text + ELSE 'PASS'::text + END AS check_status, + CASE + WHEN (COALESCE(NULLIF(TRIM(BOTH FROM b.product_name), ''::text), NULLIF(TRIM(BOTH FROM b.product_default_code), ''::text)) IS NULL) THEN 'Falta identificación del producto.'::text + WHEN ((b.product_default_code IS NULL) OR (TRIM(BOTH FROM b.product_default_code) = ''::text)) THEN 'Producto identificado por nombre, pero sin código interno.'::text + ELSE 'Producto identificado con nombre y código interno.'::text + END AS finding, + CASE + WHEN (COALESCE(NULLIF(TRIM(BOTH FROM b.product_name), ''::text), NULLIF(TRIM(BOTH FROM b.product_default_code), ''::text)) IS NULL) THEN 'Revisar maestro de producto en Odoo.'::text + WHEN ((b.product_default_code IS NULL) OR (TRIM(BOTH FROM b.product_default_code) = ''::text)) THEN 'Completar código interno/default_code del producto en Odoo.'::text + ELSE 'Sin acción.'::text + END AS recommended_action, + jsonb_build_object('product_default_code', b.product_default_code, 'product_name', b.product_name) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-SALES-001'::text AS rule_code, + CASE + WHEN ((b.sale_order_ref IS NOT NULL) AND (b.revenue_net_eur IS NOT NULL)) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN ((b.sale_order_ref IS NOT NULL) AND (b.revenue_net_eur IS NOT NULL)) THEN 'Ingreso de venta vinculado a la orden.'::text + ELSE 'La orden no tiene ingreso de venta vinculado.'::text + END AS finding, + CASE + WHEN ((b.sale_order_ref IS NOT NULL) AND (b.revenue_net_eur IS NOT NULL)) THEN 'Sin acción.'::text + ELSE 'Revisar sale_line_id, pedido de venta u origen de la orden de fabricación.'::text + END AS recommended_action, + jsonb_build_object('sale_order_ref', b.sale_order_ref, 'revenue_net_eur', b.revenue_net_eur) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-SALES-002'::text AS rule_code, + CASE + WHEN (b.sale_order_ref IS NULL) THEN 'NOT_APPLICABLE'::text + WHEN ((b.customer_name IS NOT NULL) AND (TRIM(BOTH FROM b.customer_name) <> ''::text)) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (b.sale_order_ref IS NULL) THEN 'No aplica porque no hay pedido de venta vinculado.'::text + WHEN ((b.customer_name IS NOT NULL) AND (TRIM(BOTH FROM b.customer_name) <> ''::text)) THEN 'Cliente vinculado correctamente.'::text + ELSE 'Hay pedido de venta, pero falta cliente.'::text + END AS finding, + CASE + WHEN (b.sale_order_ref IS NULL) THEN 'Resolver primero el vínculo de venta.'::text + WHEN ((b.customer_name IS NOT NULL) AND (TRIM(BOTH FROM b.customer_name) <> ''::text)) THEN 'Sin acción.'::text + ELSE 'Revisar partner_id del pedido de venta en Odoo.'::text + END AS recommended_action, + jsonb_build_object('sale_order_ref', b.sale_order_ref, 'customer_name', b.customer_name) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-RAW-001'::text AS rule_code, + CASE + WHEN (COALESCE(b.raw_material_line_count, (0)::bigint) > 0) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (COALESCE(b.raw_material_line_count, (0)::bigint) > 0) THEN 'Materia prima consumida detectada.'::text + ELSE 'No se detecta materia prima consumida para la orden.'::text + END AS finding, + CASE + WHEN (COALESCE(b.raw_material_line_count, (0)::bigint) > 0) THEN 'Sin acción.'::text + ELSE 'Revisar movimientos de stock/MRP asociados a la orden.'::text + END AS recommended_action, + jsonb_build_object('raw_material_line_count', b.raw_material_line_count, 'raw_materials_summary', b.raw_materials_summary) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-RAW-002'::text AS rule_code, + CASE + WHEN (COALESCE(b.dqc_raw_line_count, (0)::bigint) = 0) THEN 'NOT_APPLICABLE'::text + WHEN (COALESCE(b.dqc_raw_missing_code_count, (0)::bigint) = 0) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (COALESCE(b.dqc_raw_line_count, (0)::bigint) = 0) THEN 'No aplica porque no hay materia prima detectada.'::text + WHEN (COALESCE(b.dqc_raw_missing_code_count, (0)::bigint) = 0) THEN 'Todas las materias primas detectadas tienen código identificable.'::text + ELSE 'Hay materias primas sin código identificable.'::text + END AS finding, + CASE + WHEN (COALESCE(b.dqc_raw_line_count, (0)::bigint) = 0) THEN 'Resolver primero la detección de materia prima.'::text + WHEN (COALESCE(b.dqc_raw_missing_code_count, (0)::bigint) = 0) THEN 'Sin acción.'::text + ELSE 'Completar código interno/default_code de materias primas en Odoo.'::text + END AS recommended_action, + jsonb_build_object('raw_line_count', b.dqc_raw_line_count, 'raw_missing_code_count', b.dqc_raw_missing_code_count, 'raw_materials_detected', b.raw_materials_detected) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-COST-001'::text AS rule_code, + CASE + WHEN (b.material_cost_estimated_eur IS NOT NULL) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (b.material_cost_estimated_eur IS NOT NULL) THEN 'Coste de materia prima disponible.'::text + ELSE 'Falta coste de materia prima.'::text + END AS finding, + CASE + WHEN (b.material_cost_estimated_eur IS NOT NULL) THEN 'Sin acción.'::text + ELSE COALESCE(concat('Cargar coste €/kg para materia prima: ', b.raw_materials_summary), 'Cargar coste de materia prima o revisar valoración en Odoo.'::text) + END AS recommended_action, + jsonb_build_object('material_cost_estimated_eur', b.material_cost_estimated_eur, 'material_cost_source_status', b.material_cost_source_status, 'raw_materials_summary', b.raw_materials_summary) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-COST-002'::text AS rule_code, + CASE + WHEN (b.kwh_consumed IS NULL) THEN 'NOT_APPLICABLE'::text + WHEN (b.energy_cost_eur_per_kwh IS NOT NULL) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (b.kwh_consumed IS NULL) THEN 'No aplica porque no hay consumo energético medido.'::text + WHEN (b.energy_cost_eur_per_kwh IS NOT NULL) THEN 'Coste energético parametrizado.'::text + ELSE 'Hay consumo energético, pero falta coste €/kWh.'::text + END AS finding, + CASE + WHEN (b.kwh_consumed IS NULL) THEN 'Sin acción.'::text + WHEN (b.energy_cost_eur_per_kwh IS NOT NULL) THEN 'Sin acción.'::text + ELSE 'Cargar coste €/kWh en margin_cost_parameters.'::text + END AS recommended_action, + jsonb_build_object('kwh_consumed', b.kwh_consumed, 'energy_cost_eur_per_kwh', b.energy_cost_eur_per_kwh) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-COST-003'::text AS rule_code, + CASE + WHEN (b.run_min IS NULL) THEN 'NOT_APPLICABLE'::text + WHEN (b.machine_run_cost_eur_per_h IS NOT NULL) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (b.run_min IS NULL) THEN 'No aplica porque no hay tiempo de máquina utilizable.'::text + WHEN (b.machine_run_cost_eur_per_h IS NOT NULL) THEN 'Coste hora máquina parametrizado.'::text + ELSE 'Hay tiempo de máquina, pero falta coste hora máquina.'::text + END AS finding, + CASE + WHEN (b.run_min IS NULL) THEN 'Sin acción.'::text + WHEN (b.machine_run_cost_eur_per_h IS NOT NULL) THEN 'Sin acción.'::text + ELSE 'Cargar coste hora máquina para la máquina correspondiente.'::text + END AS recommended_action, + jsonb_build_object('machine_id', b.machine_id, 'run_min', b.run_min, 'machine_run_cost_eur_per_h', b.machine_run_cost_eur_per_h) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-EDGE-001'::text AS rule_code, + CASE + WHEN (b.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text])) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (b.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text])) THEN 'Datos técnicos defendibles.'::text + ELSE 'Datos técnicos no defendibles para análisis.'::text + END AS finding, + CASE + WHEN (b.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text])) THEN 'Sin acción.'::text + ELSE 'Revisar telemetría, cobertura Edge-OEE y calidad del informe técnico.'::text + END AS recommended_action, + jsonb_build_object('technical_quality_level', b.technical_quality_level) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-EDGE-002'::text AS rule_code, + CASE + WHEN (b.final_report_status = ANY (ARRAY['LISTO_INFORME_FINAL'::text, 'LISTO_TECNICO_REVISAR_CONVERSION'::text, 'LISTO_CON_OBSERVACIONES'::text])) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (b.final_report_status = ANY (ARRAY['LISTO_INFORME_FINAL'::text, 'LISTO_TECNICO_REVISAR_CONVERSION'::text, 'LISTO_CON_OBSERVACIONES'::text])) THEN 'Informe técnico cerrado o utilizable.'::text + ELSE 'Informe técnico parcial o bloqueado.'::text + END AS finding, + CASE + WHEN (b.final_report_status = ANY (ARRAY['LISTO_INFORME_FINAL'::text, 'LISTO_TECNICO_REVISAR_CONVERSION'::text, 'LISTO_CON_OBSERVACIONES'::text])) THEN 'Sin acción.'::text + ELSE 'Cerrar orden, revisar telemetría o completar clasificación antes de análisis económico final.'::text + END AS recommended_action, + jsonb_build_object('final_report_status', b.final_report_status) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-STOPS-001'::text AS rule_code, + CASE + WHEN (COALESCE(b.pending_stops, (0)::bigint) = 0) THEN 'PASS'::text + ELSE 'FAIL'::text + END AS check_status, + CASE + WHEN (COALESCE(b.pending_stops, (0)::bigint) = 0) THEN 'No hay paros pendientes de clasificación.'::text + ELSE 'Hay paros pendientes de clasificación.'::text + END AS finding, + CASE + WHEN (COALESCE(b.pending_stops, (0)::bigint) = 0) THEN 'Sin acción.'::text + ELSE 'Clasificar paros pendientes desde la consola de paros.'::text + END AS recommended_action, + jsonb_build_object('pending_stops', b.pending_stops) AS evidence_json + FROM base b + UNION ALL + SELECT b.order_row_id, + b.tenant, + b.site, + b.odoo_instance, + b.odoo_workorder_id, + b.odoo_production_id, + b.odoo_order_ref, + b.odoo_state, + b.sale_order_ref, + b.customer_name, + b.revenue_net_eur, + b.machine_id, + b.asset, + b.product_default_code, + b.product_name, + b.raw_material_line_count, + b.raw_total_consumed_qty, + b.raw_materials_summary, + b.raw_material_cost_status_summary, + b.missing_or_invalid_raw_material_cost_count, + b.material_cost_estimated_eur, + b.material_cost_source_status, + b.energy_cost_eur_per_kwh, + b.machine_run_cost_eur_per_h, + b.kwh_consumed, + b.run_min, + b.stop_cost_eur, + b.technical_quality_level, + b.final_report_status, + b.margin_status, + b.pending_stops, + b.production_conversion_status, + b.dqc_raw_line_count, + b.dqc_raw_missing_code_count, + b.raw_materials_detected, + 'DQC-CONV-001'::text AS rule_code, + CASE + WHEN (b.production_conversion_status = 'CICLOS_COHERENTES_CON_OBJETIVO'::text) THEN 'PASS'::text + WHEN (b.production_conversion_status = 'CANTIDAD_ODOO_SIN_CICLOS_EDGE'::text) THEN 'FAIL'::text + ELSE 'WARN'::text + END AS check_status, + CASE + WHEN (b.production_conversion_status = 'CICLOS_COHERENTES_CON_OBJETIVO'::text) THEN 'Conversión producción-ciclos coherente.'::text + WHEN (b.production_conversion_status = 'CANTIDAD_ODOO_SIN_CICLOS_EDGE'::text) THEN 'Odoo muestra cantidad producida, pero Edge no detecta ciclos.'::text + ELSE 'Conversión producción-ciclos pendiente de revisión o no concluyente.'::text + END AS finding, + CASE + WHEN (b.production_conversion_status = 'CICLOS_COHERENTES_CON_OBJETIVO'::text) THEN 'Sin acción.'::text + ELSE 'Validar relación entre kg, bolsas, unidad comercial y ciclos/golpes.'::text + END AS recommended_action, + jsonb_build_object('production_conversion_status', b.production_conversion_status) AS evidence_json + FROM base b + ) + SELECT e.order_row_id, + e.tenant, + e.site, + e.odoo_instance, + e.odoo_workorder_id, + e.odoo_production_id, + e.odoo_order_ref, + e.odoo_state, + e.sale_order_ref, + e.customer_name, + e.product_default_code, + e.product_name, + e.machine_id, + r.rule_code, + r.rule_name, + r.domain, + r.dimension, + r.severity, + r.owner_area, + r.applies_to, + r.blocking_for, + e.check_status, + CASE + WHEN (e.check_status = 'PASS'::text) THEN true + WHEN (e.check_status = 'NOT_APPLICABLE'::text) THEN NULL::boolean + ELSE false + END AS check_passed, + e.finding, + e.recommended_action, + e.evidence_json, + CASE + WHEN ((e.check_status = 'FAIL'::text) AND (r.severity = 'CRITICAL'::text)) THEN 100 + WHEN ((e.check_status = 'FAIL'::text) AND (r.severity = 'HIGH'::text)) THEN 75 + WHEN ((e.check_status = 'FAIL'::text) AND (r.severity = 'MEDIUM'::text)) THEN 50 + WHEN ((e.check_status = 'WARN'::text) AND (r.severity = 'CRITICAL'::text)) THEN 60 + WHEN ((e.check_status = 'WARN'::text) AND (r.severity = 'HIGH'::text)) THEN 45 + WHEN ((e.check_status = 'WARN'::text) AND (r.severity = 'MEDIUM'::text)) THEN 30 + ELSE 0 + END AS issue_score + FROM (evaluations e + JOIN mv_edge_oee_ucepsa_demo.data_quality_contract_rules r ON (((r.tenant = e.tenant) AND (r.site = e.site) AND (r.rule_code = e.rule_code) AND (r.is_active = true)))); + + +-- +-- Name: data_quality_contract_order_summary_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - +-- + +CREATE VIEW mv_reports_ucepsa_demo.data_quality_contract_order_summary_v1 AS + WITH checks AS ( + SELECT data_quality_contract_order_checks_v1.order_row_id, + data_quality_contract_order_checks_v1.tenant, + data_quality_contract_order_checks_v1.site, + data_quality_contract_order_checks_v1.odoo_instance, + data_quality_contract_order_checks_v1.odoo_workorder_id, + data_quality_contract_order_checks_v1.odoo_production_id, + data_quality_contract_order_checks_v1.odoo_order_ref, + data_quality_contract_order_checks_v1.odoo_state, + data_quality_contract_order_checks_v1.sale_order_ref, + data_quality_contract_order_checks_v1.customer_name, + data_quality_contract_order_checks_v1.product_default_code, + data_quality_contract_order_checks_v1.product_name, + data_quality_contract_order_checks_v1.machine_id, + data_quality_contract_order_checks_v1.rule_code, + data_quality_contract_order_checks_v1.rule_name, + data_quality_contract_order_checks_v1.domain, + data_quality_contract_order_checks_v1.dimension, + data_quality_contract_order_checks_v1.severity, + data_quality_contract_order_checks_v1.owner_area, + data_quality_contract_order_checks_v1.applies_to, + data_quality_contract_order_checks_v1.blocking_for, + data_quality_contract_order_checks_v1.check_status, + data_quality_contract_order_checks_v1.check_passed, + data_quality_contract_order_checks_v1.finding, + data_quality_contract_order_checks_v1.recommended_action, + data_quality_contract_order_checks_v1.evidence_json, + data_quality_contract_order_checks_v1.issue_score + FROM mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 + ), agg AS ( + SELECT checks.order_row_id, + checks.tenant, + checks.site, + checks.odoo_instance, + checks.odoo_workorder_id, + checks.odoo_production_id, + checks.odoo_order_ref, + checks.odoo_state, + checks.sale_order_ref, + checks.customer_name, + checks.product_default_code, + checks.product_name, + checks.machine_id, + count(*) AS total_rules, + count(*) FILTER (WHERE (checks.check_status = 'PASS'::text)) AS passed_rules, + count(*) FILTER (WHERE (checks.check_status = 'FAIL'::text)) AS failed_rules, + count(*) FILTER (WHERE (checks.check_status = 'WARN'::text)) AS warn_rules, + count(*) FILTER (WHERE (checks.check_status = 'NOT_APPLICABLE'::text)) AS not_applicable_rules, + count(*) FILTER (WHERE ((checks.check_status = 'FAIL'::text) AND (checks.severity = 'CRITICAL'::text))) AS critical_failures, + count(*) FILTER (WHERE ((checks.check_status = 'FAIL'::text) AND (checks.severity = 'HIGH'::text))) AS high_failures, + count(*) FILTER (WHERE ((checks.check_status = 'FAIL'::text) AND (checks.severity = 'MEDIUM'::text))) AS medium_failures, + count(*) FILTER (WHERE ((checks.check_status = 'WARN'::text) AND (checks.severity = 'CRITICAL'::text))) AS critical_warnings, + count(*) FILTER (WHERE ((checks.check_status = 'WARN'::text) AND (checks.severity = 'HIGH'::text))) AS high_warnings, + count(*) FILTER (WHERE ((checks.check_status = 'WARN'::text) AND (checks.severity = 'MEDIUM'::text))) AS medium_warnings, + COALESCE(sum(checks.issue_score), (0)::bigint) AS total_issue_score, + max(checks.issue_score) AS max_issue_score + FROM checks + GROUP BY checks.order_row_id, checks.tenant, checks.site, checks.odoo_instance, checks.odoo_workorder_id, checks.odoo_production_id, checks.odoo_order_ref, checks.odoo_state, checks.sale_order_ref, checks.customer_name, checks.product_default_code, checks.product_name, checks.machine_id + ), main_issue AS ( + SELECT DISTINCT ON (checks.order_row_id) checks.order_row_id, + checks.rule_code AS main_blocker_rule_code, + checks.rule_name AS main_blocker_rule_name, + checks.domain AS main_blocker_domain, + checks.severity AS main_blocker_severity, + checks.owner_area AS owner_area_to_act, + checks.blocking_for, + checks.check_status AS main_blocker_status, + checks.finding AS main_blocker_finding, + checks.recommended_action AS main_recommended_action, + checks.issue_score AS main_issue_score + FROM checks + WHERE (checks.check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text])) + ORDER BY checks.order_row_id, checks.issue_score DESC, + CASE checks.severity + WHEN 'CRITICAL'::text THEN 1 + WHEN 'HIGH'::text THEN 2 + WHEN 'MEDIUM'::text THEN 3 + ELSE 4 + END, checks.rule_code + ) + SELECT a.order_row_id, + a.tenant, + a.site, + a.odoo_instance, + a.odoo_workorder_id, + a.odoo_production_id, + a.odoo_order_ref, + a.odoo_state, + a.sale_order_ref, + a.customer_name, + a.product_default_code, + a.product_name, + a.machine_id, + a.total_rules, + a.passed_rules, + a.failed_rules, + a.warn_rules, + a.not_applicable_rules, + a.critical_failures, + a.high_failures, + a.medium_failures, + a.critical_warnings, + a.high_warnings, + a.medium_warnings, + a.total_issue_score, + a.max_issue_score, + GREATEST((0)::bigint, (100 - LEAST(a.total_issue_score, (100)::bigint))) AS contract_score, + CASE + WHEN (a.critical_failures > 0) THEN 'BLOQUEADO_CRITICO'::text + WHEN (a.high_failures > 0) THEN 'BLOQUEADO_ALTO'::text + WHEN (a.medium_failures > 0) THEN 'BLOQUEADO_MEDIO'::text + WHEN (a.warn_rules > 0) THEN 'VALIDO_CON_OBSERVACIONES'::text + WHEN ((a.failed_rules = 0) AND (a.warn_rules = 0)) THEN 'CONTRATO_CUMPLIDO'::text + ELSE 'REVISAR'::text + END AS contract_status, + mi.main_blocker_rule_code, + mi.main_blocker_rule_name, + mi.main_blocker_domain, + mi.main_blocker_severity, + mi.owner_area_to_act, + mi.blocking_for, + mi.main_blocker_status, + mi.main_blocker_finding, + mi.main_recommended_action, + mi.main_issue_score, + CASE + WHEN (a.critical_failures > 0) THEN concat('Contrato bloqueado por fallo crítico: ', COALESCE(mi.main_blocker_finding, 'sin detalle'::text)) + WHEN (a.high_failures > 0) THEN concat('Contrato bloqueado por fallo alto: ', COALESCE(mi.main_blocker_finding, 'sin detalle'::text)) + WHEN (a.medium_failures > 0) THEN concat('Contrato bloqueado por fallo medio: ', COALESCE(mi.main_blocker_finding, 'sin detalle'::text)) + WHEN (a.warn_rules > 0) THEN concat('Contrato válido con observaciones: ', COALESCE(mi.main_blocker_finding, 'sin detalle'::text)) + ELSE 'Contrato de datos cumplido para esta orden.'::text + END AS contract_summary + FROM (agg a + LEFT JOIN main_issue mi ON ((mi.order_row_id = a.order_row_id))); + + +-- +-- Name: data_quality_contract_owner_summary_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: - +-- + +CREATE VIEW mv_reports_ucepsa_demo.data_quality_contract_owner_summary_v1 AS + SELECT tenant, + site, + owner_area, + count(*) FILTER (WHERE (check_status = 'FAIL'::text)) AS failed_checks, + count(*) FILTER (WHERE (check_status = 'WARN'::text)) AS warn_checks, + count(*) FILTER (WHERE ((check_status = 'FAIL'::text) AND (severity = 'CRITICAL'::text))) AS critical_failed_checks, + count(*) FILTER (WHERE ((check_status = 'FAIL'::text) AND (severity = 'HIGH'::text))) AS high_failed_checks, + count(DISTINCT odoo_order_ref) FILTER (WHERE (check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text]))) AS affected_orders, + sum(issue_score) FILTER (WHERE (check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text]))) AS total_issue_score, + string_agg(DISTINCT rule_code, ', '::text ORDER BY rule_code) FILTER (WHERE (check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text]))) AS affected_rules, + string_agg(DISTINCT odoo_order_ref, ', '::text ORDER BY odoo_order_ref) FILTER (WHERE (check_status = ANY (ARRAY['FAIL'::text, 'WARN'::text]))) AS affected_order_refs + FROM mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 + GROUP BY tenant, site, owner_area; + + +-- +-- Name: data_quality_contract_rules id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_demo; Owner: - +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.data_quality_contract_rules ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_demo.data_quality_contract_rules_id_seq'::regclass); + + +-- +-- Name: data_quality_contract_rules data_quality_contract_rules_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: - +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.data_quality_contract_rules + ADD CONSTRAINT data_quality_contract_rules_pkey PRIMARY KEY (id); + + +-- +-- Name: data_quality_contract_rules ux_data_quality_contract_rules_code; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: - +-- + +ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.data_quality_contract_rules + ADD CONSTRAINT ux_data_quality_contract_rules_code UNIQUE (tenant, site, rule_code); + + +-- +-- Name: idx_data_quality_contract_rules_active; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: - +-- + +CREATE INDEX idx_data_quality_contract_rules_active ON mv_edge_oee_ucepsa_demo.data_quality_contract_rules USING btree (tenant, site, is_active, domain, severity); + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict Jszr4ruwRiCXF4itba8oT9UnTRsL1bJGSsYnj9UgpfOpaiQUSblgKdozAITLZdr + + +-- Seed rules +-- +-- PostgreSQL database dump +-- + +\restrict 0GULSAN6PrEX9eK1oI9gTcg4MdETx9P93tGMaONyYc1oC6UFbi5FCS1yjEVkVNx + +-- Dumped from database version 16.14 +-- Dumped by pg_dump version 16.14 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Data for Name: data_quality_contract_rules; Type: TABLE DATA; Schema: mv_edge_oee_ucepsa_demo; Owner: - +-- + +INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (1, 'ucepsa', 'ucepsa_test', 'DQC-ODOO-001', 'Orden de fabricación identificada', 'ODOO', 'COMPLETENESS', 'CRITICAL', 'Odoo / Producción', 'Manufacturing Order', 'La orden debe tener referencia de fabricación e identificador de producción Odoo.', 'odoo_order_ref y odoo_production_id deben existir.', 'order_report', true, '2026-05-30 20:18:18.616748+02'); +INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (2, 'ucepsa', 'ucepsa_test', 'DQC-ODOO-002', 'Centro de trabajo mapeado a máquina MESAVAULT', 'ODOO', 'TRACEABILITY', 'CRITICAL', 'Odoo / MESAVAULT', 'Workcenter / Machine', 'El centro de trabajo de Odoo debe estar asociado a un machine_id MESAVAULT.', 'machine_id debe existir y estar mapeado.', 'edge_oee_report', true, '2026-05-30 20:18:18.616748+02'); +INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (3, 'ucepsa', 'ucepsa_test', 'DQC-ODOO-003', 'Producto identificado', 'ODOO', 'COMPLETENESS', 'HIGH', 'Odoo / Maestro de producto', 'Product', 'El producto debe estar identificado con nombre y, cuando aplique, código interno.', 'product_name o product deben existir; product_default_code recomendable.', 'product_margin', true, '2026-05-30 20:18:18.616748+02'); +INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (4, 'ucepsa', 'ucepsa_test', 'DQC-SALES-001', 'Ingreso de venta vinculado', 'SALES', 'TRACEABILITY', 'CRITICAL', 'Ventas / Odoo', 'Sale Order', 'La orden debe estar vinculada a una línea de venta para poder calcular margen.', 'sale_order_ref y revenue_net_eur deben existir.', 'margin', true, '2026-05-30 20:18:18.616748+02'); +INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (5, 'ucepsa', 'ucepsa_test', 'DQC-SALES-002', 'Cliente vinculado', 'SALES', 'COMPLETENESS', 'HIGH', 'Ventas / Odoo', 'Customer', 'La orden debe tener cliente asociado cuando exista pedido de venta.', 'customer_name debe existir si hay sale_order_ref.', 'customer_margin', true, '2026-05-30 20:18:18.616748+02'); +INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (6, 'ucepsa', 'ucepsa_test', 'DQC-RAW-001', 'Materia prima consumida detectada', 'MATERIAL', 'TRACEABILITY', 'CRITICAL', 'Producción / Almacén / Odoo', 'Raw Material', 'La orden debe tener materia prima consumida o movimiento de material asociado.', 'raw_material_line_count debe ser mayor que cero.', 'material_cost', true, '2026-05-30 20:18:18.616748+02'); +INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (7, 'ucepsa', 'ucepsa_test', 'DQC-RAW-002', 'Materia prima con código identificable', 'MATERIAL', 'COMPLETENESS', 'HIGH', 'Maestro de producto / Odoo', 'Raw Material', 'La materia prima debe tener código interno para poder asignar costes y analizar histórico.', 'raw_product_default_code debe existir.', 'material_cost', true, '2026-05-30 20:18:18.616748+02'); +INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (8, 'ucepsa', 'ucepsa_test', 'DQC-COST-001', 'Coste de materia prima disponible', 'COST', 'COMPLETENESS', 'CRITICAL', 'Compras / Administración / Odoo', 'Raw Material Cost', 'La materia prima debe tener coste €/kg o valoración Odoo para calcular margen.', 'material_cost_estimated_eur debe existir o la materia prima debe tener coste parametrizado.', 'margin', true, '2026-05-30 20:18:18.616748+02'); +INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (9, 'ucepsa', 'ucepsa_test', 'DQC-COST-002', 'Coste energético parametrizado', 'COST', 'COMPLETENESS', 'MEDIUM', 'Administración / MESAVAULT', 'Energy Cost', 'Si se usa energía en el margen, debe existir coste €/kWh.', 'energy_cost_eur_per_kwh debe existir cuando kwh_consumed exista.', 'full_margin', true, '2026-05-30 20:18:18.616748+02'); +INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (10, 'ucepsa', 'ucepsa_test', 'DQC-COST-003', 'Coste hora máquina parametrizado', 'COST', 'COMPLETENESS', 'MEDIUM', 'Producción / Administración / MESAVAULT', 'Machine Cost', 'Si se imputa coste de máquina, debe existir coste horario para la máquina.', 'machine_run_cost_eur_per_h debe existir cuando run_min exista.', 'full_margin', true, '2026-05-30 20:18:18.616748+02'); +INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (11, 'ucepsa', 'ucepsa_test', 'DQC-EDGE-001', 'Datos técnicos defendibles', 'EDGE', 'QUALITY', 'CRITICAL', 'MESAVAULT / Producción', 'Telemetry', 'La orden debe tener datos técnicos suficientes para un informe defendible.', 'technical_quality_level debe ser FIABLE o ACEPTABLE_CON_OBSERVACIONES.', 'technical_report', true, '2026-05-30 20:18:18.616748+02'); +INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (12, 'ucepsa', 'ucepsa_test', 'DQC-EDGE-002', 'Informe técnico cerrado o utilizable', 'EDGE', 'TIMELINESS', 'HIGH', 'MESAVAULT / Producción', 'Order Execution Report', 'La orden debe tener informe técnico cerrado o utilizable antes de análisis económico final.', 'final_report_status debe estar en estado listo o con observaciones.', 'margin', true, '2026-05-30 20:18:18.616748+02'); +INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (13, 'ucepsa', 'ucepsa_test', 'DQC-STOPS-001', 'Paros clasificados', 'LOSSES', 'COMPLETENESS', 'HIGH', 'Producción', 'Stops', 'Los paros imputados a una orden deben estar clasificados para explicar pérdidas.', 'pending_stops debe ser cero.', 'loss_analysis', true, '2026-05-30 20:18:18.616748+02'); +INSERT INTO mv_edge_oee_ucepsa_demo.data_quality_contract_rules (id, tenant, site, rule_code, rule_name, domain, dimension, severity, owner_area, applies_to, description, expected_condition, blocking_for, is_active, created_at) VALUES (14, 'ucepsa', 'ucepsa_test', 'DQC-CONV-001', 'Conversión producción-ciclos revisada', 'PRODUCTION', 'CONSISTENCY', 'MEDIUM', 'Odoo / Producción / MESAVAULT', 'Production Conversion', 'La relación entre cantidad Odoo, kg, bolsas y ciclos debe estar validada antes de usar métricas unitarias.', 'production_conversion_status debe ser coherente o marcado como pendiente.', 'unit_margin', true, '2026-05-30 20:18:18.616748+02'); + + +-- +-- Name: data_quality_contract_rules_id_seq; Type: SEQUENCE SET; Schema: mv_edge_oee_ucepsa_demo; Owner: - +-- + +SELECT pg_catalog.setval('mv_edge_oee_ucepsa_demo.data_quality_contract_rules_id_seq', 14, true); + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict 0GULSAN6PrEX9eK1oI9gTcg4MdETx9P93tGMaONyYc1oC6UFbi5FCS1yjEVkVNx + + +-- Read-only grants +GRANT USAGE ON SCHEMA mv_edge_oee_ucepsa_demo TO grafana_ucepsa_ro; +GRANT USAGE ON SCHEMA mv_reports_ucepsa_demo TO grafana_ucepsa_ro; + +GRANT SELECT ON mv_edge_oee_ucepsa_demo.data_quality_contract_rules TO grafana_ucepsa_ro; + +GRANT SELECT ON mv_reports_ucepsa_demo.data_quality_contract_order_checks_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON mv_reports_ucepsa_demo.data_quality_contract_order_summary_v1 TO grafana_ucepsa_ro; +GRANT SELECT ON mv_reports_ucepsa_demo.data_quality_contract_owner_summary_v1 TO grafana_ucepsa_ro;