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 78b5365..5918a0d 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
@@ -119,11 +119,40 @@ def build_report_context(order_ref: str) -> dict[str, Any]:
(order_ref,),
)
+ margin = fetch_one(
+ """
+ SELECT *
+ FROM mv_reports_ucepsa_demo.order_margin_report_v1
+ WHERE odoo_order_ref = %s
+ """,
+ (order_ref,),
+ )
+
+ raw_materials = fetch_all(
+ """
+ SELECT
+ raw_product_default_code,
+ raw_product_name,
+ planned_qty,
+ consumed_qty,
+ raw_uom,
+ material_cost_eur_per_kg,
+ material_cost_estimated_eur_rounded,
+ material_cost_status
+ FROM mv_reports_ucepsa_demo.order_raw_material_costs_v1
+ WHERE odoo_order_ref = %s
+ ORDER BY raw_product_default_code, raw_product_name
+ """,
+ (order_ref,),
+ )
+
return {
"title": REPORT_TITLE,
"report": report,
"stops": stops,
"timeline": timeline,
+ "margin": margin,
+ "raw_materials": raw_materials,
}
@@ -172,3 +201,199 @@ def report_pdf(order_ref: str = Query(..., description="Odoo order reference, e.
"Content-Disposition": f'inline; filename="{filename}"'
},
)
+
+
+@app.get("/api/cost-readiness")
+def api_cost_readiness(limit: int = Query(100, ge=1, le=500)):
+ summary = fetch_one(
+ """
+ SELECT *
+ FROM mv_reports_ucepsa_demo.cost_readiness_summary_v1
+ """,
+ (),
+ )
+
+ orders = fetch_all(
+ """
+ SELECT
+ odoo_order_ref,
+ odoo_state,
+ sale_order_ref,
+ customer_name,
+ product_default_code,
+ product_name,
+ machine_id,
+ revenue_net_eur,
+ raw_total_consumed_qty,
+ raw_materials_summary,
+ cost_readiness_score,
+ cost_readiness_status,
+ cost_readiness_description,
+ technical_quality_level,
+ final_report_status,
+ margin_status
+ FROM mv_reports_ucepsa_demo.cost_readiness_orders_v1
+ ORDER BY cost_readiness_score ASC, odoo_order_ref DESC
+ LIMIT %s
+ """,
+ (limit,),
+ )
+
+ raw_materials = fetch_all(
+ """
+ SELECT
+ raw_product_default_code,
+ raw_product_name,
+ raw_uom,
+ affected_order_count,
+ affected_orders,
+ affected_sale_orders,
+ affected_customers,
+ total_consumed_qty,
+ current_material_cost_eur_per_kg,
+ raw_material_readiness_status,
+ recommended_action
+ FROM mv_reports_ucepsa_demo.cost_readiness_raw_materials_v1
+ ORDER BY affected_order_count DESC, raw_product_default_code
+ """,
+ (),
+ )
+
+ return jsonable_encoder(
+ {
+ "summary": summary or {},
+ "orders": orders,
+ "raw_materials": raw_materials,
+ }
+ )
+
+
+@app.get("/cost-readiness.html", response_class=HTMLResponse)
+def cost_readiness_html(request: Request, limit: int = Query(100, ge=1, le=500)):
+ summary = fetch_one(
+ """
+ SELECT *
+ FROM mv_reports_ucepsa_demo.cost_readiness_summary_v1
+ """,
+ (),
+ )
+
+ orders = fetch_all(
+ """
+ SELECT
+ odoo_order_ref,
+ odoo_state,
+ sale_order_ref,
+ customer_name,
+ product_default_code,
+ product_name,
+ machine_id,
+ revenue_net_eur,
+ raw_total_consumed_qty,
+ raw_materials_summary,
+ cost_readiness_score,
+ cost_readiness_status,
+ cost_readiness_description,
+ technical_quality_level,
+ final_report_status,
+ margin_status
+ FROM mv_reports_ucepsa_demo.cost_readiness_orders_v1
+ ORDER BY cost_readiness_score ASC, odoo_order_ref DESC
+ LIMIT %s
+ """,
+ (limit,),
+ )
+
+ raw_materials = fetch_all(
+ """
+ SELECT
+ raw_product_default_code,
+ raw_product_name,
+ raw_uom,
+ affected_order_count,
+ affected_orders,
+ affected_sale_orders,
+ affected_customers,
+ total_consumed_qty,
+ current_material_cost_eur_per_kg,
+ raw_material_readiness_status,
+ recommended_action
+ FROM mv_reports_ucepsa_demo.cost_readiness_raw_materials_v1
+ ORDER BY affected_order_count DESC, raw_product_default_code
+ """,
+ (),
+ )
+
+ return templates.TemplateResponse(
+ "cost_readiness.html",
+ {
+ "request": request,
+ "title": REPORT_TITLE,
+ "summary": summary or {},
+ "orders": orders,
+ "raw_materials": raw_materials,
+ },
+ )
+
+
+@app.get("/api/orders")
+def api_orders(limit: int = Query(50, ge=1, le=200)):
+ rows = fetch_all(
+ """
+ SELECT
+ odoo_order_ref,
+ odoo_state,
+ product,
+ machine_id,
+ start_ts,
+ end_ts,
+ window_min,
+ real_cycle_delta,
+ kwh_consumed,
+ stop_count,
+ stop_min,
+ technical_quality_level,
+ final_report_status
+ FROM mv_reports_ucepsa_demo.order_execution_report_full_v1
+ ORDER BY start_ts DESC
+ LIMIT %s
+ """,
+ (limit,),
+ )
+ return jsonable_encoder({"orders": rows})
+
+
+@app.get("/orders.html", response_class=HTMLResponse)
+def orders_html(request: Request, limit: int = Query(50, ge=1, le=200)):
+ orders = fetch_all(
+ """
+ SELECT
+ odoo_order_ref,
+ odoo_state,
+ product,
+ machine_id,
+ start_ts,
+ end_ts,
+ window_min,
+ real_cycle_delta,
+ kwh_consumed,
+ stop_count,
+ stop_min,
+ technical_quality_level,
+ final_report_status
+ FROM mv_reports_ucepsa_demo.order_execution_report_full_v1
+ ORDER BY start_ts DESC
+ LIMIT %s
+ """,
+ (limit,),
+ )
+
+ return templates.TemplateResponse(
+ "orders_index.html",
+ {
+ "request": request,
+ "title": REPORT_TITLE,
+ "orders": orders,
+ },
+ )
+
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
new file mode 100644
index 0000000..cdc9e23
--- /dev/null
+++ b/ucepsa/edge-oee-demo/services/order-report-api/app/templates/cost_readiness.html
@@ -0,0 +1,250 @@
+
+
+
+
+ {{ title }} - Cost Readiness
+
+
+
+
+
+
Cost Readiness Cockpit
+
{{ title }}
+
+ 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.
+
+
+
+
+
Órdenes totales
+
{{ summary.total_orders or 0 }}
+
+
+
Con ingreso
+
{{ summary.orders_with_revenue or 0 }}
+
+
+
Sin ingreso
+
{{ summary.orders_without_revenue or 0 }}
+
+
+
Sin coste material
+
{{ summary.orders_missing_raw_material_cost or 0 }}
+
+
+
Listas para margen
+
{{ summary.orders_ready_for_margin or 0 }}
+
+
+
Score medio
+
{{ summary.avg_cost_readiness_score|num(2) if summary.avg_cost_readiness_score is not none else "0.00" }}
+
+
+
+
1. Estado por orden
+
+
+ | Orden |
+ Pedido venta |
+ Cliente |
+ Producto |
+ Máquina |
+ Ingreso neto |
+ Kg materia prima |
+ Score |
+ Estado |
+ Bloqueo / acción |
+ Informe |
+
+ {% for order in orders %}
+
+ | {{ 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 }} |
+ {{ order.revenue_net_eur|num(2) if order.revenue_net_eur is not none else "" }} € |
+ {{ order.raw_total_consumed_qty|num(3) if order.raw_total_consumed_qty is not none else "" }} |
+
+ {% if order.cost_readiness_score >= 80 %}
+ {{ order.cost_readiness_score }}
+ {% elif order.cost_readiness_score >= 50 %}
+ {{ order.cost_readiness_score }}
+ {% else %}
+ {{ order.cost_readiness_score }}
+ {% endif %}
+ |
+
+ {% if order.cost_readiness_status == 'LISTO_PARA_MARGEN' %}
+ Listo para margen
+ {% elif order.cost_readiness_status == 'BLOQUEADO_FALTA_COSTE_MATERIA_PRIMA' %}
+ Falta coste materia prima
+ {% elif order.cost_readiness_status == 'BLOQUEADO_SIN_INGRESO' %}
+ Sin ingreso
+ {% elif order.cost_readiness_status == 'BLOQUEADO_DATOS_TECNICOS' %}
+ Datos técnicos débiles
+ {% else %}
+ {{ order.cost_readiness_status }}
+ {% endif %}
+ |
+ {{ order.cost_readiness_description }} |
+
+ HTML
+ |
+ PDF
+ |
+
+ {% else %}
+ | No hay órdenes disponibles. |
+ {% endfor %}
+
+
+
2. Materias primas pendientes de coste
+
+
+ | Código |
+ Materia prima |
+ UdM |
+ Órdenes afectadas |
+ Pedidos venta |
+ Clientes |
+ Cantidad consumida |
+ Coste €/kg actual |
+ Estado |
+ Acción recomendada |
+
+ {% for raw in raw_materials %}
+
+ | {{ raw.raw_product_default_code or "" }} |
+ {{ raw.raw_product_name or "" }} |
+ {{ raw.raw_uom or "" }} |
+
+ {{ raw.affected_order_count }}
+ {{ raw.affected_orders }}
+ |
+ {{ raw.affected_sale_orders }} |
+ {{ raw.affected_customers }} |
+ {{ raw.total_consumed_qty|num(3) }} |
+ {{ raw.current_material_cost_eur_per_kg|num(4) if raw.current_material_cost_eur_per_kg is not none else "" }} |
+
+ {% if raw.raw_material_readiness_status == 'COSTE_DISPONIBLE' %}
+ Coste disponible
+ {% else %}
+ Pendiente coste
+ {% endif %}
+ |
+ {{ raw.recommended_action }} |
+
+ {% else %}
+ | No hay materias primas detectadas. |
+ {% endfor %}
+
+
+
+
+
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
new file mode 100644
index 0000000..329fa43
--- /dev/null
+++ b/ucepsa/edge-oee-demo/services/order-report-api/app/templates/orders_index.html
@@ -0,0 +1,120 @@
+
+
+
+
+ {{ title }} - Informes de orden
+
+
+
+
+
Informes de ejecución de orden
+
{{ title }}
+
+
+ Abrir Cost Readiness Cockpit
+
+
+
+
+ | Orden |
+ Estado |
+ Producto |
+ Máquina |
+ Inicio |
+ Fin |
+ Duración min |
+ Ciclos |
+ kWh |
+ Paros |
+ Min paro |
+ Calidad |
+ Estado informe |
+ Acciones |
+
+ {% for order in orders %}
+
+ | {{ order.odoo_order_ref }} |
+ {{ order.odoo_state }} |
+ {{ order.product }} |
+ {{ order.machine_id }} |
+ {{ order.start_ts|dt }} |
+ {{ order.end_ts|dt }} |
+ {{ order.window_min|num(2) }} |
+ {{ order.real_cycle_delta }} |
+ {{ order.kwh_consumed|num(3) }} |
+ {{ order.stop_count }} |
+ {{ order.stop_min|num(2) }} |
+
+ {{ order.technical_quality_level }}
+ |
+ {{ order.final_report_status }} |
+
+ HTML
+ |
+ PDF
+ |
+
+ {% else %}
+
+ | No hay órdenes disponibles. |
+
+ {% endfor %}
+
+
+
+
diff --git a/ucepsa/edge-oee-demo/sql/reports/080_margin_intelligence_cost_readiness.sql b/ucepsa/edge-oee-demo/sql/reports/080_margin_intelligence_cost_readiness.sql
new file mode 100644
index 0000000..b9d6ee8
--- /dev/null
+++ b/ucepsa/edge-oee-demo/sql/reports/080_margin_intelligence_cost_readiness.sql
@@ -0,0 +1,1186 @@
+-- MESAVAULT Edge-OEE UCEPSA
+-- Margin Intelligence + Cost Readiness Cockpit
+-- Schema-only export. No data. No secrets.
+
+DROP VIEW IF EXISTS mv_reports_ucepsa_demo.cost_readiness_summary_v1;
+DROP VIEW IF EXISTS mv_reports_ucepsa_demo.cost_readiness_raw_materials_v1;
+DROP VIEW IF EXISTS mv_reports_ucepsa_demo.cost_readiness_orders_v1;
+
+DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_margin_report_v1;
+DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_raw_material_costs_summary_v1;
+DROP VIEW IF EXISTS mv_reports_ucepsa_demo.order_raw_material_costs_v1;
+
+DROP TABLE IF EXISTS mv_edge_oee_ucepsa_demo.raw_material_cost_parameters;
+DROP TABLE IF EXISTS mv_edge_oee_ucepsa_demo.margin_cost_parameters;
+DROP TABLE IF EXISTS mv_edge_oee_ucepsa_demo.odoo_order_commercials;
+
+--
+-- PostgreSQL database dump
+--
+
+\restrict prs5NerySlAyEA4mTL6Sg3Uqx00yOGe9h66uyBfmfApVdToFu1LKFfY74XjwGca
+
+-- 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: margin_cost_parameters; Type: TABLE; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+CREATE TABLE mv_edge_oee_ucepsa_demo.margin_cost_parameters (
+ id bigint NOT NULL,
+ tenant text DEFAULT 'ucepsa'::text NOT NULL,
+ site text DEFAULT 'ucepsa_test'::text NOT NULL,
+ scope_level text DEFAULT 'GLOBAL'::text NOT NULL,
+ machine_id text,
+ product_default_code text,
+ energy_cost_eur_per_kwh numeric(14,6),
+ material_cost_eur_per_kg numeric(14,6),
+ machine_run_cost_eur_per_h numeric(14,4),
+ labor_cost_eur_per_h numeric(14,4),
+ overhead_cost_eur_per_h numeric(14,4),
+ valid_from timestamp with time zone DEFAULT now() NOT NULL,
+ valid_to timestamp with time zone,
+ is_active boolean DEFAULT true NOT NULL,
+ notes text,
+ created_at timestamp with time zone DEFAULT now() NOT NULL
+);
+
+
+--
+-- Name: margin_cost_parameters_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+CREATE SEQUENCE mv_edge_oee_ucepsa_demo.margin_cost_parameters_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: margin_cost_parameters_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+ALTER SEQUENCE mv_edge_oee_ucepsa_demo.margin_cost_parameters_id_seq OWNED BY mv_edge_oee_ucepsa_demo.margin_cost_parameters.id;
+
+
+--
+-- Name: odoo_order_commercials; Type: TABLE; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+CREATE TABLE mv_edge_oee_ucepsa_demo.odoo_order_commercials (
+ id bigint NOT NULL,
+ tenant text DEFAULT 'ucepsa'::text NOT NULL,
+ site text DEFAULT 'ucepsa_test'::text NOT NULL,
+ odoo_instance text NOT NULL,
+ odoo_production_id integer NOT NULL,
+ odoo_order_ref text NOT NULL,
+ odoo_origin text,
+ odoo_state text,
+ product_id integer,
+ product_default_code text,
+ product_name text,
+ product_uom text,
+ product_standard_price numeric(14,4),
+ product_list_price numeric(14,4),
+ production_qty numeric(14,3),
+ qty_producing numeric(14,3),
+ production_uom text,
+ sale_line_id integer,
+ sale_order_id integer,
+ sale_order_ref text,
+ customer_id integer,
+ customer_name text,
+ sale_state text,
+ sale_date_order timestamp with time zone,
+ currency text,
+ sale_line_qty numeric(14,3),
+ sale_line_qty_delivered numeric(14,3),
+ sale_price_unit numeric(14,4),
+ sale_discount_pct numeric(10,2),
+ revenue_net_eur numeric(14,4),
+ revenue_total_eur numeric(14,4),
+ raw_material_value_eur numeric(14,4),
+ finished_value_eur numeric(14,4),
+ material_cost_status text DEFAULT 'UNAVAILABLE'::text NOT NULL,
+ stock_raw_moves_json jsonb DEFAULT '[]'::jsonb NOT NULL,
+ stock_finished_moves_json jsonb DEFAULT '[]'::jsonb NOT NULL,
+ raw_json jsonb DEFAULT '{}'::jsonb NOT NULL,
+ snapshot_at timestamp with time zone DEFAULT now() NOT NULL
+);
+
+
+--
+-- Name: odoo_order_commercials_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+CREATE SEQUENCE mv_edge_oee_ucepsa_demo.odoo_order_commercials_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: odoo_order_commercials_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+ALTER SEQUENCE mv_edge_oee_ucepsa_demo.odoo_order_commercials_id_seq OWNED BY mv_edge_oee_ucepsa_demo.odoo_order_commercials.id;
+
+
+--
+-- Name: raw_material_cost_parameters; Type: TABLE; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+CREATE TABLE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters (
+ id bigint NOT NULL,
+ tenant text DEFAULT 'ucepsa'::text NOT NULL,
+ site text DEFAULT 'ucepsa_test'::text NOT NULL,
+ raw_product_default_code text NOT NULL,
+ raw_product_name text,
+ material_cost_eur_per_kg numeric(14,6) NOT NULL,
+ source text DEFAULT 'MANUAL'::text NOT NULL,
+ valid_from timestamp with time zone DEFAULT now() NOT NULL,
+ valid_to timestamp with time zone,
+ is_active boolean DEFAULT true NOT NULL,
+ notes text,
+ created_at timestamp with time zone DEFAULT now() NOT NULL
+);
+
+
+--
+-- Name: raw_material_cost_parameters_id_seq; Type: SEQUENCE; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+CREATE SEQUENCE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: raw_material_cost_parameters_id_seq; Type: SEQUENCE OWNED BY; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+ALTER SEQUENCE mv_edge_oee_ucepsa_demo.raw_material_cost_parameters_id_seq OWNED BY mv_edge_oee_ucepsa_demo.raw_material_cost_parameters.id;
+
+
+--
+-- Name: order_raw_material_costs_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: -
+--
+
+CREATE VIEW mv_reports_ucepsa_demo.order_raw_material_costs_v1 AS
+ WITH raw_moves AS (
+ SELECT c.id AS commercial_row_id,
+ c.tenant,
+ c.site,
+ c.odoo_instance,
+ c.odoo_production_id,
+ c.odoo_order_ref,
+ c.sale_order_ref,
+ c.customer_name,
+ c.product_default_code AS finished_product_default_code,
+ c.product_name AS finished_product_name,
+ ((rm.raw_move ->> 'id'::text))::integer AS raw_move_id,
+ (NULLIF(((rm.raw_move -> 'product_id'::text) ->> 0), ''::text))::integer AS raw_product_id,
+ ((rm.raw_move -> 'product_id'::text) ->> 1) AS raw_product_name,
+ "substring"(((rm.raw_move -> 'product_id'::text) ->> 1), '\[([^\]]+)\]'::text) AS raw_product_default_code,
+ (NULLIF((rm.raw_move ->> 'product_uom_qty'::text), ''::text))::numeric AS planned_qty,
+ (NULLIF((rm.raw_move ->> 'quantity'::text), ''::text))::numeric AS consumed_qty,
+ ((rm.raw_move -> 'product_uom'::text) ->> 1) AS raw_uom,
+ (NULLIF((rm.raw_move ->> 'price_unit'::text), ''::text))::numeric AS odoo_price_unit,
+ (NULLIF((rm.raw_move ->> 'value'::text), ''::text))::numeric AS odoo_value,
+ (rm.raw_move ->> 'state'::text) AS move_state,
+ rm.raw_move AS raw_move_json
+ FROM (mv_edge_oee_ucepsa_demo.odoo_order_commercials c
+ CROSS JOIN LATERAL jsonb_array_elements(c.stock_raw_moves_json) rm(raw_move))
+ ), costed AS (
+ SELECT r.commercial_row_id,
+ r.tenant,
+ r.site,
+ r.odoo_instance,
+ r.odoo_production_id,
+ r.odoo_order_ref,
+ r.sale_order_ref,
+ r.customer_name,
+ r.finished_product_default_code,
+ r.finished_product_name,
+ r.raw_move_id,
+ r.raw_product_id,
+ r.raw_product_name,
+ r.raw_product_default_code,
+ r.planned_qty,
+ r.consumed_qty,
+ r.raw_uom,
+ r.odoo_price_unit,
+ r.odoo_value,
+ r.move_state,
+ r.raw_move_json,
+ p.id AS raw_material_cost_parameter_id,
+ p.material_cost_eur_per_kg,
+ p.source AS raw_material_cost_source,
+ p.notes AS raw_material_cost_notes,
+ CASE
+ WHEN (COALESCE(r.odoo_value, (0)::numeric) <> (0)::numeric) THEN abs(r.odoo_value)
+ WHEN ((lower(COALESCE(r.raw_uom, ''::text)) = 'kg'::text) AND (p.material_cost_eur_per_kg IS NOT NULL) AND (r.consumed_qty IS NOT NULL)) THEN (r.consumed_qty * p.material_cost_eur_per_kg)
+ ELSE NULL::numeric
+ END AS material_cost_estimated_eur,
+ CASE
+ WHEN (COALESCE(r.odoo_value, (0)::numeric) <> (0)::numeric) THEN 'FROM_ODOO_STOCK_MOVE_VALUE'::text
+ WHEN (r.raw_product_default_code IS NULL) THEN 'MISSING_RAW_PRODUCT_CODE'::text
+ WHEN (lower(COALESCE(r.raw_uom, ''::text)) <> 'kg'::text) THEN 'UNSUPPORTED_RAW_UOM'::text
+ WHEN (p.material_cost_eur_per_kg IS NULL) THEN 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text
+ WHEN (r.consumed_qty IS NULL) THEN 'MISSING_CONSUMED_QTY'::text
+ ELSE 'FROM_RAW_MATERIAL_COST_PARAMETER'::text
+ END AS material_cost_status
+ FROM (raw_moves r
+ LEFT JOIN LATERAL ( SELECT p_1.id,
+ p_1.tenant,
+ p_1.site,
+ p_1.raw_product_default_code,
+ p_1.raw_product_name,
+ p_1.material_cost_eur_per_kg,
+ p_1.source,
+ p_1.valid_from,
+ p_1.valid_to,
+ p_1.is_active,
+ p_1.notes,
+ p_1.created_at
+ FROM mv_edge_oee_ucepsa_demo.raw_material_cost_parameters p_1
+ WHERE ((p_1.tenant = r.tenant) AND (p_1.site = r.site) AND (p_1.is_active = true) AND (p_1.raw_product_default_code = r.raw_product_default_code) AND (p_1.valid_from <= now()) AND ((p_1.valid_to IS NULL) OR (p_1.valid_to > now())))
+ ORDER BY p_1.valid_from DESC, p_1.id DESC
+ LIMIT 1) p ON (true))
+ )
+ SELECT commercial_row_id,
+ tenant,
+ site,
+ odoo_instance,
+ odoo_production_id,
+ odoo_order_ref,
+ sale_order_ref,
+ customer_name,
+ finished_product_default_code,
+ finished_product_name,
+ raw_move_id,
+ raw_product_id,
+ raw_product_name,
+ raw_product_default_code,
+ planned_qty,
+ consumed_qty,
+ raw_uom,
+ odoo_price_unit,
+ odoo_value,
+ move_state,
+ raw_move_json,
+ raw_material_cost_parameter_id,
+ material_cost_eur_per_kg,
+ raw_material_cost_source,
+ raw_material_cost_notes,
+ material_cost_estimated_eur,
+ material_cost_status,
+ round(material_cost_estimated_eur, 4) AS material_cost_estimated_eur_rounded
+ FROM costed;
+
+
+--
+-- Name: order_raw_material_costs_summary_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: -
+--
+
+CREATE VIEW mv_reports_ucepsa_demo.order_raw_material_costs_summary_v1 AS
+ SELECT tenant,
+ site,
+ odoo_instance,
+ odoo_production_id,
+ odoo_order_ref,
+ sale_order_ref,
+ customer_name,
+ finished_product_default_code,
+ finished_product_name,
+ count(*) AS raw_material_line_count,
+ round(sum(COALESCE(consumed_qty, (0)::numeric)), 3) AS total_consumed_qty,
+ string_agg(concat(COALESCE(raw_product_default_code, 'sin_codigo'::text), ' - ', COALESCE(raw_product_name, 'sin_nombre'::text), ': ', COALESCE((consumed_qty)::text, 'sin_cantidad'::text), ' ', COALESCE(raw_uom, ''::text)), '; '::text ORDER BY raw_product_default_code) AS raw_materials_summary,
+ round(sum(COALESCE(material_cost_estimated_eur, (0)::numeric)), 4) AS material_cost_estimated_eur,
+ count(*) FILTER (WHERE (material_cost_estimated_eur IS NOT NULL)) AS costed_raw_material_line_count,
+ count(*) FILTER (WHERE ((material_cost_status <> 'FROM_RAW_MATERIAL_COST_PARAMETER'::text) AND (material_cost_status <> 'FROM_ODOO_STOCK_MOVE_VALUE'::text))) AS missing_or_invalid_raw_material_cost_count,
+ string_agg(DISTINCT material_cost_status, ', '::text ORDER BY material_cost_status) AS material_cost_status_summary
+ FROM mv_reports_ucepsa_demo.order_raw_material_costs_v1
+ GROUP BY tenant, site, odoo_instance, odoo_production_id, odoo_order_ref, sale_order_ref, customer_name, finished_product_default_code, finished_product_name;
+
+
+--
+-- Name: order_margin_report_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: -
+--
+
+CREATE VIEW mv_reports_ucepsa_demo.order_margin_report_v1 AS
+ WITH joined AS (
+ SELECT r.order_row_id,
+ r.tenant,
+ r.site,
+ r.odoo_instance,
+ r.odoo_workorder_id,
+ r.odoo_production_id,
+ r.odoo_order_ref,
+ r.odoo_state,
+ r.machine_id,
+ r.asset,
+ r.product AS edge_product,
+ r.start_ts,
+ r.end_ts,
+ r.window_min,
+ r.run_min,
+ r.stop_min,
+ r.stop_cost_eur,
+ r.real_cycle_delta,
+ r.kwh_consumed,
+ r.kwh_per_cycle,
+ r.technical_quality_level,
+ r.final_report_status,
+ c.odoo_origin,
+ c.sale_order_ref,
+ c.customer_id,
+ c.customer_name,
+ c.sale_state,
+ c.sale_date_order,
+ c.currency,
+ c.product_id,
+ c.product_default_code,
+ c.product_name,
+ c.production_qty,
+ c.qty_producing,
+ c.production_uom,
+ c.sale_line_id,
+ c.sale_order_id,
+ c.sale_line_qty,
+ c.sale_line_qty_delivered,
+ c.sale_price_unit,
+ c.sale_discount_pct,
+ c.revenue_net_eur,
+ c.revenue_total_eur,
+ c.product_standard_price,
+ c.product_list_price,
+ c.raw_material_value_eur,
+ c.finished_value_eur,
+ c.material_cost_status AS odoo_material_cost_status,
+ c.snapshot_at AS commercial_snapshot_at,
+ rms.raw_material_line_count,
+ rms.total_consumed_qty AS raw_total_consumed_qty,
+ rms.raw_materials_summary,
+ rms.material_cost_estimated_eur AS raw_material_cost_estimated_eur,
+ rms.costed_raw_material_line_count,
+ rms.missing_or_invalid_raw_material_cost_count,
+ rms.material_cost_status_summary AS raw_material_cost_status_summary
+ FROM ((mv_reports_ucepsa_demo.order_execution_report_full_v1 r
+ LEFT JOIN mv_edge_oee_ucepsa_demo.odoo_order_commercials c ON (((c.odoo_instance = r.odoo_instance) AND (c.odoo_production_id = r.odoo_production_id))))
+ LEFT JOIN mv_reports_ucepsa_demo.order_raw_material_costs_summary_v1 rms ON (((rms.odoo_instance = r.odoo_instance) AND (rms.odoo_production_id = r.odoo_production_id))))
+ ), with_params AS (
+ SELECT j.order_row_id,
+ j.tenant,
+ j.site,
+ j.odoo_instance,
+ j.odoo_workorder_id,
+ j.odoo_production_id,
+ j.odoo_order_ref,
+ j.odoo_state,
+ j.machine_id,
+ j.asset,
+ j.edge_product,
+ j.start_ts,
+ j.end_ts,
+ j.window_min,
+ j.run_min,
+ j.stop_min,
+ j.stop_cost_eur,
+ j.real_cycle_delta,
+ j.kwh_consumed,
+ j.kwh_per_cycle,
+ j.technical_quality_level,
+ j.final_report_status,
+ j.odoo_origin,
+ j.sale_order_ref,
+ j.customer_id,
+ j.customer_name,
+ j.sale_state,
+ j.sale_date_order,
+ j.currency,
+ j.product_id,
+ j.product_default_code,
+ j.product_name,
+ j.production_qty,
+ j.qty_producing,
+ j.production_uom,
+ j.sale_line_id,
+ j.sale_order_id,
+ j.sale_line_qty,
+ j.sale_line_qty_delivered,
+ j.sale_price_unit,
+ j.sale_discount_pct,
+ j.revenue_net_eur,
+ j.revenue_total_eur,
+ j.product_standard_price,
+ j.product_list_price,
+ j.raw_material_value_eur,
+ j.finished_value_eur,
+ j.odoo_material_cost_status,
+ j.commercial_snapshot_at,
+ j.raw_material_line_count,
+ j.raw_total_consumed_qty,
+ j.raw_materials_summary,
+ j.raw_material_cost_estimated_eur,
+ j.costed_raw_material_line_count,
+ j.missing_or_invalid_raw_material_cost_count,
+ j.raw_material_cost_status_summary,
+ p.id AS cost_parameter_id,
+ p.scope_level AS cost_parameter_scope_level,
+ p.energy_cost_eur_per_kwh,
+ p.material_cost_eur_per_kg AS fallback_material_cost_eur_per_kg,
+ p.machine_run_cost_eur_per_h,
+ p.labor_cost_eur_per_h,
+ p.overhead_cost_eur_per_h,
+ p.notes AS cost_parameter_notes
+ FROM (joined j
+ LEFT JOIN LATERAL ( SELECT p_1.id,
+ p_1.tenant,
+ p_1.site,
+ p_1.scope_level,
+ p_1.machine_id,
+ p_1.product_default_code,
+ p_1.energy_cost_eur_per_kwh,
+ p_1.material_cost_eur_per_kg,
+ p_1.machine_run_cost_eur_per_h,
+ p_1.labor_cost_eur_per_h,
+ p_1.overhead_cost_eur_per_h,
+ p_1.valid_from,
+ p_1.valid_to,
+ p_1.is_active,
+ p_1.notes,
+ p_1.created_at
+ FROM mv_edge_oee_ucepsa_demo.margin_cost_parameters p_1
+ WHERE ((p_1.is_active = true) AND (p_1.tenant = j.tenant) AND (p_1.site = j.site) AND (p_1.valid_from <= now()) AND ((p_1.valid_to IS NULL) OR (p_1.valid_to > now())) AND ((p_1.machine_id IS NULL) OR (p_1.machine_id = j.machine_id)) AND ((p_1.product_default_code IS NULL) OR (p_1.product_default_code = j.product_default_code)))
+ ORDER BY
+ CASE
+ WHEN ((p_1.product_default_code IS NOT NULL) AND (p_1.machine_id IS NOT NULL)) THEN 1
+ WHEN (p_1.product_default_code IS NOT NULL) THEN 2
+ WHEN (p_1.machine_id IS NOT NULL) THEN 3
+ ELSE 4
+ END, p_1.valid_from DESC, p_1.id DESC
+ LIMIT 1) p ON (true))
+ ), costs AS (
+ SELECT with_params.order_row_id,
+ with_params.tenant,
+ with_params.site,
+ with_params.odoo_instance,
+ with_params.odoo_workorder_id,
+ with_params.odoo_production_id,
+ with_params.odoo_order_ref,
+ with_params.odoo_state,
+ with_params.machine_id,
+ with_params.asset,
+ with_params.edge_product,
+ with_params.start_ts,
+ with_params.end_ts,
+ with_params.window_min,
+ with_params.run_min,
+ with_params.stop_min,
+ with_params.stop_cost_eur,
+ with_params.real_cycle_delta,
+ with_params.kwh_consumed,
+ with_params.kwh_per_cycle,
+ with_params.technical_quality_level,
+ with_params.final_report_status,
+ with_params.odoo_origin,
+ with_params.sale_order_ref,
+ with_params.customer_id,
+ with_params.customer_name,
+ with_params.sale_state,
+ with_params.sale_date_order,
+ with_params.currency,
+ with_params.product_id,
+ with_params.product_default_code,
+ with_params.product_name,
+ with_params.production_qty,
+ with_params.qty_producing,
+ with_params.production_uom,
+ with_params.sale_line_id,
+ with_params.sale_order_id,
+ with_params.sale_line_qty,
+ with_params.sale_line_qty_delivered,
+ with_params.sale_price_unit,
+ with_params.sale_discount_pct,
+ with_params.revenue_net_eur,
+ with_params.revenue_total_eur,
+ with_params.product_standard_price,
+ with_params.product_list_price,
+ with_params.raw_material_value_eur,
+ with_params.finished_value_eur,
+ with_params.odoo_material_cost_status,
+ with_params.commercial_snapshot_at,
+ with_params.raw_material_line_count,
+ with_params.raw_total_consumed_qty,
+ with_params.raw_materials_summary,
+ with_params.raw_material_cost_estimated_eur,
+ with_params.costed_raw_material_line_count,
+ with_params.missing_or_invalid_raw_material_cost_count,
+ with_params.raw_material_cost_status_summary,
+ with_params.cost_parameter_id,
+ with_params.cost_parameter_scope_level,
+ with_params.energy_cost_eur_per_kwh,
+ with_params.fallback_material_cost_eur_per_kg,
+ with_params.machine_run_cost_eur_per_h,
+ with_params.labor_cost_eur_per_h,
+ with_params.overhead_cost_eur_per_h,
+ with_params.cost_parameter_notes,
+ COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric)) AS costing_qty_kg,
+ CASE
+ WHEN ((with_params.raw_material_line_count > 0) AND (COALESCE(with_params.missing_or_invalid_raw_material_cost_count, (0)::bigint) = 0) AND (with_params.raw_material_cost_estimated_eur IS NOT NULL)) THEN with_params.raw_material_cost_estimated_eur
+ WHEN ((with_params.raw_material_line_count > 0) AND (COALESCE(with_params.missing_or_invalid_raw_material_cost_count, (0)::bigint) > 0)) THEN NULL::numeric
+ WHEN ((with_params.raw_material_value_eur IS NOT NULL) AND (with_params.raw_material_value_eur <> (0)::numeric)) THEN abs(with_params.raw_material_value_eur)
+ WHEN ((with_params.product_standard_price IS NOT NULL) AND (with_params.product_standard_price <> (0)::numeric) AND (COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric)) IS NOT NULL)) THEN (with_params.product_standard_price * COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric)))
+ WHEN ((with_params.fallback_material_cost_eur_per_kg IS NOT NULL) AND (COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric)) IS NOT NULL)) THEN (with_params.fallback_material_cost_eur_per_kg * COALESCE(NULLIF(with_params.qty_producing, (0)::numeric), NULLIF(with_params.production_qty, (0)::numeric)))
+ ELSE NULL::numeric
+ END AS material_cost_estimated_eur,
+ CASE
+ WHEN ((with_params.raw_material_line_count > 0) AND (COALESCE(with_params.missing_or_invalid_raw_material_cost_count, (0)::bigint) = 0)) THEN 'FROM_RAW_MATERIAL_COSTS'::text
+ WHEN ((with_params.raw_material_line_count > 0) AND (COALESCE(with_params.missing_or_invalid_raw_material_cost_count, (0)::bigint) > 0)) THEN with_params.raw_material_cost_status_summary
+ WHEN ((with_params.raw_material_value_eur IS NOT NULL) AND (with_params.raw_material_value_eur <> (0)::numeric)) THEN 'FROM_ODOO_RAW_MATERIAL_VALUE'::text
+ WHEN ((with_params.product_standard_price IS NOT NULL) AND (with_params.product_standard_price <> (0)::numeric)) THEN 'FROM_PRODUCT_STANDARD_PRICE'::text
+ WHEN (with_params.fallback_material_cost_eur_per_kg IS NOT NULL) THEN 'FROM_FALLBACK_MARGIN_PARAMETER'::text
+ ELSE 'MISSING_MATERIAL_COST'::text
+ END AS material_cost_source_status,
+ CASE
+ WHEN ((with_params.kwh_consumed IS NOT NULL) AND (with_params.energy_cost_eur_per_kwh IS NOT NULL)) THEN (with_params.kwh_consumed * with_params.energy_cost_eur_per_kwh)
+ ELSE NULL::numeric
+ END AS energy_cost_estimated_eur,
+ CASE
+ WHEN ((with_params.run_min IS NOT NULL) AND (with_params.machine_run_cost_eur_per_h IS NOT NULL)) THEN ((with_params.run_min / 60.0) * with_params.machine_run_cost_eur_per_h)
+ ELSE NULL::numeric
+ END AS machine_run_cost_estimated_eur,
+ CASE
+ WHEN ((with_params.window_min IS NOT NULL) AND (with_params.labor_cost_eur_per_h IS NOT NULL)) THEN ((with_params.window_min / 60.0) * with_params.labor_cost_eur_per_h)
+ ELSE NULL::numeric
+ END AS labor_cost_estimated_eur,
+ CASE
+ WHEN ((with_params.window_min IS NOT NULL) AND (with_params.overhead_cost_eur_per_h IS NOT NULL)) THEN ((with_params.window_min / 60.0) * with_params.overhead_cost_eur_per_h)
+ ELSE NULL::numeric
+ END AS overhead_cost_estimated_eur
+ FROM with_params
+ )
+ SELECT order_row_id,
+ tenant,
+ site,
+ odoo_instance,
+ odoo_workorder_id,
+ odoo_production_id,
+ odoo_order_ref,
+ odoo_state,
+ odoo_origin,
+ sale_order_ref,
+ customer_id,
+ customer_name,
+ sale_state,
+ sale_date_order,
+ currency,
+ machine_id,
+ asset,
+ product_id,
+ product_default_code,
+ COALESCE(product_name, edge_product) AS product_name,
+ production_qty,
+ qty_producing,
+ production_uom,
+ costing_qty_kg,
+ sale_line_id,
+ sale_order_id,
+ sale_line_qty,
+ sale_line_qty_delivered,
+ sale_price_unit,
+ sale_discount_pct,
+ revenue_net_eur,
+ revenue_total_eur,
+ start_ts,
+ end_ts,
+ window_min,
+ run_min,
+ stop_min,
+ stop_cost_eur,
+ real_cycle_delta,
+ kwh_consumed,
+ kwh_per_cycle,
+ product_standard_price,
+ product_list_price,
+ raw_material_value_eur,
+ finished_value_eur,
+ odoo_material_cost_status,
+ raw_material_line_count,
+ raw_total_consumed_qty,
+ raw_materials_summary,
+ raw_material_cost_status_summary,
+ costed_raw_material_line_count,
+ missing_or_invalid_raw_material_cost_count,
+ cost_parameter_id,
+ cost_parameter_scope_level,
+ energy_cost_eur_per_kwh,
+ fallback_material_cost_eur_per_kg,
+ machine_run_cost_eur_per_h,
+ labor_cost_eur_per_h,
+ overhead_cost_eur_per_h,
+ round(material_cost_estimated_eur, 4) AS material_cost_estimated_eur,
+ material_cost_source_status,
+ round(energy_cost_estimated_eur, 4) AS energy_cost_estimated_eur,
+ round(machine_run_cost_estimated_eur, 4) AS machine_run_cost_estimated_eur,
+ round(labor_cost_estimated_eur, 4) AS labor_cost_estimated_eur,
+ round(overhead_cost_estimated_eur, 4) AS overhead_cost_estimated_eur,
+ round((((((COALESCE(material_cost_estimated_eur, (0)::numeric) + COALESCE(energy_cost_estimated_eur, (0)::numeric)) + COALESCE(machine_run_cost_estimated_eur, (0)::numeric)) + COALESCE(labor_cost_estimated_eur, (0)::numeric)) + COALESCE(overhead_cost_estimated_eur, (0)::numeric)) + COALESCE(stop_cost_eur, (0)::numeric)), 4) AS known_or_estimated_cost_eur,
+ round(
+ CASE
+ WHEN (revenue_net_eur IS NOT NULL) THEN (revenue_net_eur - (((((COALESCE(material_cost_estimated_eur, (0)::numeric) + COALESCE(energy_cost_estimated_eur, (0)::numeric)) + COALESCE(machine_run_cost_estimated_eur, (0)::numeric)) + COALESCE(labor_cost_estimated_eur, (0)::numeric)) + COALESCE(overhead_cost_estimated_eur, (0)::numeric)) + COALESCE(stop_cost_eur, (0)::numeric)))
+ ELSE NULL::numeric
+ END, 4) AS partial_margin_after_known_costs_eur,
+ round(
+ CASE
+ WHEN ((revenue_net_eur IS NOT NULL) AND (material_cost_estimated_eur IS NOT NULL)) THEN (revenue_net_eur - (((((COALESCE(material_cost_estimated_eur, (0)::numeric) + COALESCE(energy_cost_estimated_eur, (0)::numeric)) + COALESCE(machine_run_cost_estimated_eur, (0)::numeric)) + COALESCE(labor_cost_estimated_eur, (0)::numeric)) + COALESCE(overhead_cost_estimated_eur, (0)::numeric)) + COALESCE(stop_cost_eur, (0)::numeric)))
+ ELSE NULL::numeric
+ END, 4) AS estimated_margin_eur,
+ round(
+ CASE
+ WHEN ((revenue_net_eur IS NOT NULL) AND (revenue_net_eur <> (0)::numeric) AND (material_cost_estimated_eur IS NOT NULL)) THEN (((revenue_net_eur - (((((COALESCE(material_cost_estimated_eur, (0)::numeric) + COALESCE(energy_cost_estimated_eur, (0)::numeric)) + COALESCE(machine_run_cost_estimated_eur, (0)::numeric)) + COALESCE(labor_cost_estimated_eur, (0)::numeric)) + COALESCE(overhead_cost_estimated_eur, (0)::numeric)) + COALESCE(stop_cost_eur, (0)::numeric))) / revenue_net_eur) * 100.0)
+ ELSE NULL::numeric
+ END, 2) AS estimated_margin_pct,
+ CASE
+ WHEN (revenue_net_eur IS NULL) THEN 'NO_REVENUE_LINKED'::text
+ ELSE 'REVENUE_LINKED'::text
+ END AS revenue_status,
+ CASE
+ WHEN ((raw_material_line_count > 0) AND (COALESCE(missing_or_invalid_raw_material_cost_count, (0)::bigint) > 0)) THEN 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text
+ WHEN (material_cost_estimated_eur IS NULL) THEN 'MISSING_MATERIAL_COST'::text
+ WHEN (energy_cost_eur_per_kwh IS NULL) THEN 'MISSING_ENERGY_COST'::text
+ WHEN (machine_run_cost_eur_per_h IS NULL) THEN 'MISSING_MACHINE_RUN_COST'::text
+ ELSE 'COST_PARAMETERS_AVAILABLE'::text
+ END AS cost_status,
+ CASE
+ WHEN (revenue_net_eur IS NULL) THEN 'NO_MARGIN_WITHOUT_REVENUE'::text
+ WHEN ((raw_material_line_count > 0) AND (COALESCE(missing_or_invalid_raw_material_cost_count, (0)::bigint) > 0)) THEN 'PARTIAL_MARGIN_MISSING_RAW_MATERIAL_COST'::text
+ WHEN (material_cost_estimated_eur IS NULL) THEN 'PARTIAL_MARGIN_MISSING_MATERIAL'::text
+ ELSE 'ESTIMATED_MARGIN_AVAILABLE'::text
+ END AS margin_status,
+ technical_quality_level,
+ final_report_status,
+ commercial_snapshot_at
+ FROM costs;
+
+
+--
+-- Name: cost_readiness_orders_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: -
+--
+
+CREATE VIEW mv_reports_ucepsa_demo.cost_readiness_orders_v1 AS
+ WITH 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.odoo_origin,
+ m.sale_order_ref,
+ m.customer_id,
+ m.customer_name,
+ m.sale_state,
+ m.sale_date_order,
+ m.currency,
+ m.machine_id,
+ m.asset,
+ m.product_id,
+ m.product_default_code,
+ m.product_name,
+ m.production_qty,
+ m.qty_producing,
+ m.production_uom,
+ m.costing_qty_kg,
+ m.sale_line_id,
+ m.sale_order_id,
+ m.sale_line_qty,
+ m.sale_line_qty_delivered,
+ m.sale_price_unit,
+ m.sale_discount_pct,
+ m.revenue_net_eur,
+ m.revenue_total_eur,
+ m.start_ts,
+ m.end_ts,
+ m.window_min,
+ m.run_min,
+ m.stop_min,
+ m.stop_cost_eur,
+ m.real_cycle_delta,
+ m.kwh_consumed,
+ m.kwh_per_cycle,
+ m.product_standard_price,
+ m.product_list_price,
+ m.raw_material_value_eur,
+ m.finished_value_eur,
+ m.odoo_material_cost_status,
+ m.raw_material_line_count,
+ m.raw_total_consumed_qty,
+ m.raw_materials_summary,
+ m.raw_material_cost_status_summary,
+ m.costed_raw_material_line_count,
+ m.missing_or_invalid_raw_material_cost_count,
+ m.cost_parameter_id,
+ m.cost_parameter_scope_level,
+ m.energy_cost_eur_per_kwh,
+ m.fallback_material_cost_eur_per_kg,
+ m.machine_run_cost_eur_per_h,
+ m.labor_cost_eur_per_h,
+ m.overhead_cost_eur_per_h,
+ m.material_cost_estimated_eur,
+ m.material_cost_source_status,
+ m.energy_cost_estimated_eur,
+ m.machine_run_cost_estimated_eur,
+ m.labor_cost_estimated_eur,
+ m.overhead_cost_estimated_eur,
+ m.known_or_estimated_cost_eur,
+ m.partial_margin_after_known_costs_eur,
+ m.estimated_margin_eur,
+ m.estimated_margin_pct,
+ m.revenue_status,
+ m.cost_status,
+ m.margin_status,
+ m.technical_quality_level,
+ m.final_report_status,
+ m.commercial_snapshot_at,
+ CASE
+ WHEN (m.revenue_net_eur IS NOT NULL) THEN true
+ ELSE false
+ END AS has_revenue,
+ CASE
+ WHEN (NULLIF(TRIM(BOTH FROM COALESCE(m.customer_name, ''::text)), ''::text) IS NOT NULL) THEN true
+ ELSE false
+ END AS has_customer,
+ CASE
+ WHEN (NULLIF(TRIM(BOTH FROM COALESCE(m.sale_order_ref, ''::text)), ''::text) IS NOT NULL) THEN true
+ ELSE false
+ END AS has_sale_order,
+ CASE
+ WHEN (COALESCE(m.raw_material_line_count, (0)::bigint) > 0) THEN true
+ ELSE false
+ END AS has_raw_materials,
+ CASE
+ WHEN ((COALESCE(m.raw_material_line_count, (0)::bigint) > 0) AND (COALESCE(m.missing_or_invalid_raw_material_cost_count, (0)::bigint) = 0) AND (m.material_cost_estimated_eur IS NOT NULL)) THEN true
+ ELSE false
+ END AS has_raw_material_cost,
+ CASE
+ WHEN (m.technical_quality_level = ANY (ARRAY['FIABLE'::text, 'ACEPTABLE_CON_OBSERVACIONES'::text])) THEN true
+ ELSE false
+ END AS has_usable_technical_data,
+ CASE
+ WHEN (m.final_report_status = ANY (ARRAY['LISTO_INFORME_FINAL'::text, 'LISTO_TECNICO_REVISAR_CONVERSION'::text, 'LISTO_CON_OBSERVACIONES'::text])) THEN true
+ ELSE false
+ END AS has_closed_or_usable_order_report,
+ CASE
+ WHEN ((m.kwh_consumed IS NULL) OR (m.energy_cost_eur_per_kwh IS NOT NULL)) THEN true
+ ELSE false
+ END AS has_energy_cost_parameter,
+ CASE
+ WHEN ((m.run_min IS NULL) OR (m.machine_run_cost_eur_per_h IS NOT NULL)) THEN true
+ ELSE false
+ END AS has_machine_cost_parameter
+ FROM mv_reports_ucepsa_demo.order_margin_report_v1 m
+ ), scored 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.odoo_origin,
+ b.sale_order_ref,
+ b.customer_id,
+ b.customer_name,
+ b.sale_state,
+ b.sale_date_order,
+ b.currency,
+ b.machine_id,
+ b.asset,
+ b.product_id,
+ b.product_default_code,
+ b.product_name,
+ b.production_qty,
+ b.qty_producing,
+ b.production_uom,
+ b.costing_qty_kg,
+ b.sale_line_id,
+ b.sale_order_id,
+ b.sale_line_qty,
+ b.sale_line_qty_delivered,
+ b.sale_price_unit,
+ b.sale_discount_pct,
+ b.revenue_net_eur,
+ b.revenue_total_eur,
+ b.start_ts,
+ b.end_ts,
+ b.window_min,
+ b.run_min,
+ b.stop_min,
+ b.stop_cost_eur,
+ b.real_cycle_delta,
+ b.kwh_consumed,
+ b.kwh_per_cycle,
+ b.product_standard_price,
+ b.product_list_price,
+ b.raw_material_value_eur,
+ b.finished_value_eur,
+ b.odoo_material_cost_status,
+ b.raw_material_line_count,
+ b.raw_total_consumed_qty,
+ b.raw_materials_summary,
+ b.raw_material_cost_status_summary,
+ b.costed_raw_material_line_count,
+ b.missing_or_invalid_raw_material_cost_count,
+ b.cost_parameter_id,
+ b.cost_parameter_scope_level,
+ b.energy_cost_eur_per_kwh,
+ b.fallback_material_cost_eur_per_kg,
+ b.machine_run_cost_eur_per_h,
+ b.labor_cost_eur_per_h,
+ b.overhead_cost_eur_per_h,
+ b.material_cost_estimated_eur,
+ b.material_cost_source_status,
+ b.energy_cost_estimated_eur,
+ b.machine_run_cost_estimated_eur,
+ b.labor_cost_estimated_eur,
+ b.overhead_cost_estimated_eur,
+ b.known_or_estimated_cost_eur,
+ b.partial_margin_after_known_costs_eur,
+ b.estimated_margin_eur,
+ b.estimated_margin_pct,
+ b.revenue_status,
+ b.cost_status,
+ b.margin_status,
+ b.technical_quality_level,
+ b.final_report_status,
+ b.commercial_snapshot_at,
+ b.has_revenue,
+ b.has_customer,
+ b.has_sale_order,
+ b.has_raw_materials,
+ b.has_raw_material_cost,
+ b.has_usable_technical_data,
+ b.has_closed_or_usable_order_report,
+ b.has_energy_cost_parameter,
+ b.has_machine_cost_parameter,
+ GREATEST(0, (((((((((100 -
+ CASE
+ WHEN (b.has_revenue = false) THEN 35
+ ELSE 0
+ END) -
+ CASE
+ WHEN (b.has_customer = false) THEN 5
+ ELSE 0
+ END) -
+ CASE
+ WHEN (b.has_sale_order = false) THEN 5
+ ELSE 0
+ END) -
+ CASE
+ WHEN (b.has_usable_technical_data = false) THEN 25
+ ELSE 0
+ END) -
+ CASE
+ WHEN (b.has_closed_or_usable_order_report = false) THEN 15
+ ELSE 0
+ END) -
+ CASE
+ WHEN (b.has_raw_materials = false) THEN 20
+ ELSE 0
+ END) -
+ CASE
+ WHEN ((b.has_raw_materials = true) AND (b.has_raw_material_cost = false)) THEN 35
+ ELSE 0
+ END) -
+ CASE
+ WHEN (b.has_energy_cost_parameter = false) THEN 5
+ ELSE 0
+ END) -
+ CASE
+ WHEN (b.has_machine_cost_parameter = false) THEN 5
+ ELSE 0
+ END)) AS cost_readiness_score,
+ concat_ws('; '::text,
+ CASE
+ WHEN (b.has_revenue = false) THEN 'Falta ingreso de venta vinculado'::text
+ ELSE NULL::text
+ END,
+ CASE
+ WHEN (b.has_customer = false) THEN 'Falta cliente'::text
+ ELSE NULL::text
+ END,
+ CASE
+ WHEN (b.has_sale_order = false) THEN 'Falta pedido de venta'::text
+ ELSE NULL::text
+ END,
+ CASE
+ WHEN (b.has_usable_technical_data = false) THEN 'Datos técnicos no defendibles'::text
+ ELSE NULL::text
+ END,
+ CASE
+ WHEN (b.has_closed_or_usable_order_report = false) THEN 'Informe técnico parcial o bloqueado'::text
+ ELSE NULL::text
+ END,
+ CASE
+ WHEN (b.has_raw_materials = false) THEN 'No se detecta materia prima consumida'::text
+ ELSE NULL::text
+ END,
+ CASE
+ WHEN ((b.has_raw_materials = true) AND (b.has_raw_material_cost = false)) THEN 'Falta coste de materia prima'::text
+ ELSE NULL::text
+ END,
+ CASE
+ WHEN (b.has_energy_cost_parameter = false) THEN 'Falta coste €/kWh'::text
+ ELSE NULL::text
+ END,
+ CASE
+ WHEN (b.has_machine_cost_parameter = false) THEN 'Falta coste hora máquina'::text
+ ELSE NULL::text
+ END) AS cost_readiness_blockers
+ FROM base b
+ )
+ SELECT order_row_id,
+ tenant,
+ site,
+ odoo_instance,
+ odoo_order_ref,
+ odoo_state,
+ sale_order_ref,
+ customer_name,
+ product_default_code,
+ product_name,
+ machine_id,
+ revenue_net_eur,
+ revenue_total_eur,
+ raw_material_line_count,
+ raw_total_consumed_qty,
+ raw_materials_summary,
+ raw_material_cost_status_summary,
+ missing_or_invalid_raw_material_cost_count,
+ material_cost_estimated_eur,
+ material_cost_source_status,
+ stop_cost_eur,
+ kwh_consumed,
+ run_min,
+ window_min,
+ technical_quality_level,
+ final_report_status,
+ margin_status,
+ has_revenue,
+ has_customer,
+ has_sale_order,
+ has_raw_materials,
+ has_raw_material_cost,
+ has_usable_technical_data,
+ has_closed_or_usable_order_report,
+ has_energy_cost_parameter,
+ has_machine_cost_parameter,
+ cost_readiness_score,
+ CASE
+ WHEN (has_revenue = false) THEN 'BLOQUEADO_SIN_INGRESO'::text
+ WHEN (has_usable_technical_data = false) THEN 'BLOQUEADO_DATOS_TECNICOS'::text
+ WHEN (has_closed_or_usable_order_report = false) THEN 'PARCIAL_INFORME_TECNICO'::text
+ WHEN (has_raw_materials = false) THEN 'BLOQUEADO_SIN_MATERIA_PRIMA'::text
+ WHEN ((has_raw_materials = true) AND (has_raw_material_cost = false)) THEN 'BLOQUEADO_FALTA_COSTE_MATERIA_PRIMA'::text
+ WHEN ((has_energy_cost_parameter = false) OR (has_machine_cost_parameter = false)) THEN 'PARCIAL_FALTAN_COSTES_OPERATIVOS'::text
+ ELSE 'LISTO_PARA_MARGEN'::text
+ END AS cost_readiness_status,
+ CASE
+ WHEN (cost_readiness_blockers = ''::text) THEN 'Orden lista para cálculo de margen estimado.'::text
+ ELSE cost_readiness_blockers
+ END AS cost_readiness_description
+ FROM scored;
+
+
+--
+-- Name: cost_readiness_raw_materials_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: -
+--
+
+CREATE VIEW mv_reports_ucepsa_demo.cost_readiness_raw_materials_v1 AS
+ SELECT tenant,
+ site,
+ raw_product_default_code,
+ raw_product_name,
+ raw_uom,
+ count(DISTINCT odoo_order_ref) AS affected_order_count,
+ string_agg(DISTINCT odoo_order_ref, ', '::text ORDER BY odoo_order_ref) AS affected_orders,
+ string_agg(DISTINCT COALESCE(sale_order_ref, 'sin_pedido_venta'::text), ', '::text ORDER BY COALESCE(sale_order_ref, 'sin_pedido_venta'::text)) AS affected_sale_orders,
+ string_agg(DISTINCT COALESCE(customer_name, 'sin_cliente'::text), ', '::text ORDER BY COALESCE(customer_name, 'sin_cliente'::text)) AS affected_customers,
+ round(sum(COALESCE(consumed_qty, (0)::numeric)), 3) AS total_consumed_qty,
+ count(*) AS raw_material_line_count,
+ count(*) FILTER (WHERE (material_cost_status = 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text)) AS missing_cost_line_count,
+ max(material_cost_eur_per_kg) AS current_material_cost_eur_per_kg,
+ CASE
+ WHEN (count(*) FILTER (WHERE (material_cost_status = 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text)) > 0) THEN 'PENDIENTE_COSTE_MATERIA_PRIMA'::text
+ ELSE 'COSTE_DISPONIBLE'::text
+ END AS raw_material_readiness_status,
+ CASE
+ WHEN (count(*) FILTER (WHERE (material_cost_status = 'MISSING_RAW_MATERIAL_COST_PARAMETER'::text)) > 0) THEN concat('Falta coste €/kg para ', COALESCE(raw_product_default_code, 'sin_codigo'::text), ' - ', COALESCE(raw_product_name, 'sin_nombre'::text))
+ ELSE 'Coste de materia prima disponible.'::text
+ END AS recommended_action
+ FROM mv_reports_ucepsa_demo.order_raw_material_costs_v1
+ GROUP BY tenant, site, raw_product_default_code, raw_product_name, raw_uom;
+
+
+--
+-- Name: cost_readiness_summary_v1; Type: VIEW; Schema: mv_reports_ucepsa_demo; Owner: -
+--
+
+CREATE VIEW mv_reports_ucepsa_demo.cost_readiness_summary_v1 AS
+ SELECT tenant,
+ site,
+ count(*) AS total_orders,
+ count(*) FILTER (WHERE (has_revenue = true)) AS orders_with_revenue,
+ count(*) FILTER (WHERE (has_revenue = false)) AS orders_without_revenue,
+ count(*) FILTER (WHERE (has_raw_materials = true)) AS orders_with_raw_materials,
+ count(*) FILTER (WHERE (has_raw_materials = false)) AS orders_without_raw_materials,
+ count(*) FILTER (WHERE ((has_raw_materials = true) AND (has_raw_material_cost = false))) AS orders_missing_raw_material_cost,
+ count(*) FILTER (WHERE (has_usable_technical_data = false)) AS orders_with_bad_technical_data,
+ count(*) FILTER (WHERE (cost_readiness_status = 'LISTO_PARA_MARGEN'::text)) AS orders_ready_for_margin,
+ count(*) FILTER (WHERE (cost_readiness_status = 'BLOQUEADO_FALTA_COSTE_MATERIA_PRIMA'::text)) AS orders_blocked_missing_raw_material_cost,
+ count(*) FILTER (WHERE (cost_readiness_status = 'BLOQUEADO_SIN_INGRESO'::text)) AS orders_blocked_no_revenue,
+ round(avg(cost_readiness_score), 2) AS avg_cost_readiness_score,
+ min(cost_readiness_score) AS min_cost_readiness_score,
+ max(cost_readiness_score) AS max_cost_readiness_score
+ FROM mv_reports_ucepsa_demo.cost_readiness_orders_v1
+ GROUP BY tenant, site;
+
+
+--
+-- Name: margin_cost_parameters id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.margin_cost_parameters ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_demo.margin_cost_parameters_id_seq'::regclass);
+
+
+--
+-- Name: odoo_order_commercials id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_order_commercials ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_demo.odoo_order_commercials_id_seq'::regclass);
+
+
+--
+-- Name: raw_material_cost_parameters id; Type: DEFAULT; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.raw_material_cost_parameters ALTER COLUMN id SET DEFAULT nextval('mv_edge_oee_ucepsa_demo.raw_material_cost_parameters_id_seq'::regclass);
+
+
+--
+-- Name: margin_cost_parameters margin_cost_parameters_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.margin_cost_parameters
+ ADD CONSTRAINT margin_cost_parameters_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: odoo_order_commercials odoo_order_commercials_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_order_commercials
+ ADD CONSTRAINT odoo_order_commercials_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: raw_material_cost_parameters raw_material_cost_parameters_pkey; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.raw_material_cost_parameters
+ ADD CONSTRAINT raw_material_cost_parameters_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: odoo_order_commercials ux_odoo_order_commercials_instance_production; Type: CONSTRAINT; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+ALTER TABLE ONLY mv_edge_oee_ucepsa_demo.odoo_order_commercials
+ ADD CONSTRAINT ux_odoo_order_commercials_instance_production UNIQUE (odoo_instance, odoo_production_id);
+
+
+--
+-- Name: idx_margin_cost_parameters_scope; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+CREATE INDEX idx_margin_cost_parameters_scope ON mv_edge_oee_ucepsa_demo.margin_cost_parameters USING btree (tenant, site, is_active, machine_id, product_default_code);
+
+
+--
+-- Name: idx_odoo_order_commercials_customer; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+CREATE INDEX idx_odoo_order_commercials_customer ON mv_edge_oee_ucepsa_demo.odoo_order_commercials USING btree (customer_name);
+
+
+--
+-- Name: idx_odoo_order_commercials_order_ref; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+CREATE INDEX idx_odoo_order_commercials_order_ref ON mv_edge_oee_ucepsa_demo.odoo_order_commercials USING btree (odoo_order_ref);
+
+
+--
+-- Name: idx_odoo_order_commercials_sale_order_ref; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+CREATE INDEX idx_odoo_order_commercials_sale_order_ref ON mv_edge_oee_ucepsa_demo.odoo_order_commercials USING btree (sale_order_ref);
+
+
+--
+-- Name: idx_raw_material_cost_parameters_lookup; Type: INDEX; Schema: mv_edge_oee_ucepsa_demo; Owner: -
+--
+
+CREATE INDEX idx_raw_material_cost_parameters_lookup ON mv_edge_oee_ucepsa_demo.raw_material_cost_parameters USING btree (tenant, site, raw_product_default_code, is_active, valid_from DESC);
+
+
+--
+-- PostgreSQL database dump complete
+--
+
+\unrestrict prs5NerySlAyEA4mTL6Sg3Uqx00yOGe9h66uyBfmfApVdToFu1LKFfY74XjwGca
+
+
+-- Read-only grants for Grafana / reporting API user
+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.odoo_order_commercials TO grafana_ucepsa_ro;
+GRANT SELECT ON mv_edge_oee_ucepsa_demo.margin_cost_parameters TO grafana_ucepsa_ro;
+GRANT SELECT ON mv_edge_oee_ucepsa_demo.raw_material_cost_parameters TO grafana_ucepsa_ro;
+
+GRANT SELECT ON mv_reports_ucepsa_demo.order_raw_material_costs_v1 TO grafana_ucepsa_ro;
+GRANT SELECT ON mv_reports_ucepsa_demo.order_raw_material_costs_summary_v1 TO grafana_ucepsa_ro;
+GRANT SELECT ON mv_reports_ucepsa_demo.order_margin_report_v1 TO grafana_ucepsa_ro;
+
+GRANT SELECT ON mv_reports_ucepsa_demo.cost_readiness_orders_v1 TO grafana_ucepsa_ro;
+GRANT SELECT ON mv_reports_ucepsa_demo.cost_readiness_raw_materials_v1 TO grafana_ucepsa_ro;
+GRANT SELECT ON mv_reports_ucepsa_demo.cost_readiness_summary_v1 TO grafana_ucepsa_ro;