Add Data Quality Contract cockpit

This commit is contained in:
Victor Fraile Garcia 2026-05-30 20:30:27 +02:00
parent 94fa0d7026
commit cc951b713d
5 changed files with 1703 additions and 0 deletions

View File

@ -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,
},
)

View File

@ -111,6 +111,9 @@
<h1>Cost Readiness Cockpit</h1>
<p class="muted">{{ title }}</p>
<p>
<a href="/data-quality-contract.html" target="_blank">Abrir Data Quality Contract</a>
</p>
<p>
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.

View File

@ -0,0 +1,296 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>{{ title }} - Data Quality Contract</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 28px;
color: #1f2933;
background: #f7f9fb;
font-size: 13px;
}
.page {
background: white;
padding: 28px;
border-radius: 12px;
max-width: 1700px;
margin: auto;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
h1 {
margin: 0 0 4px 0;
color: #0f172a;
}
h2 {
margin-top: 28px;
color: #0f172a;
border-bottom: 1px solid #cbd5e1;
padding-bottom: 6px;
}
.muted {
color: #64748b;
}
.grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
margin-top: 18px;
margin-bottom: 20px;
}
.card {
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 14px;
background: #ffffff;
}
.label {
color: #64748b;
font-size: 12px;
margin-bottom: 6px;
}
.value {
color: #0f172a;
font-weight: bold;
font-size: 22px;
line-height: 1.1;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 14px;
font-size: 12px;
}
th {
background: #f1f5f9;
text-align: left;
padding: 8px;
border-bottom: 1px solid #cbd5e1;
}
td {
padding: 8px;
border-bottom: 1px solid #e2e8f0;
vertical-align: top;
}
.ok {
color: #166534;
font-weight: bold;
}
.warn {
color: #92400e;
font-weight: bold;
}
.bad {
color: #991b1b;
font-weight: bold;
}
.small {
font-size: 11px;
}
a {
color: #0f766e;
font-weight: bold;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.pill {
display: inline-block;
padding: 3px 7px;
border-radius: 999px;
background: #e2e8f0;
font-size: 11px;
font-weight: bold;
}
</style>
</head>
<body>
<div class="page">
<h1>Data Quality Contract OdooMESAVAULT</h1>
<p class="muted">{{ title }}</p>
<p>
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.
</p>
<p>
<a href="/orders.html" target="_blank">Órdenes</a>
&nbsp;|&nbsp;
<a href="/cost-readiness.html" target="_blank">Cost Readiness</a>
</p>
<div class="grid">
<div class="card">
<div class="label">Órdenes</div>
<div class="value">{{ summary.total_orders or 0 }}</div>
</div>
<div class="card">
<div class="label">Contrato cumplido</div>
<div class="value">{{ summary.contract_ok_orders or 0 }}</div>
</div>
<div class="card">
<div class="label">Con observaciones</div>
<div class="value">{{ summary.contract_warn_orders or 0 }}</div>
</div>
<div class="card">
<div class="label">Bloqueo crítico</div>
<div class="value">{{ summary.blocked_critical_orders or 0 }}</div>
</div>
<div class="card">
<div class="label">Fallos críticos</div>
<div class="value">{{ summary.total_critical_failures or 0 }}</div>
</div>
<div class="card">
<div class="label">Score medio</div>
<div class="value">{{ summary.avg_contract_score|num(2) if summary.avg_contract_score is not none else "0.00" }}</div>
</div>
</div>
<h2>1. Estado del contrato por orden</h2>
<table>
<tr>
<th>Orden</th>
<th>Pedido venta</th>
<th>Cliente</th>
<th>Producto</th>
<th>Máquina</th>
<th>Score</th>
<th>Estado contrato</th>
<th>Fallos</th>
<th>Bloqueo principal</th>
<th>Área responsable</th>
<th>Acción recomendada</th>
<th>Informe</th>
</tr>
{% for order in orders %}
<tr>
<td>{{ order.odoo_order_ref }}</td>
<td>{{ order.sale_order_ref or "" }}</td>
<td>{{ order.customer_name or "" }}</td>
<td>
<strong>{{ order.product_default_code or "" }}</strong><br>
<span class="small">{{ order.product_name or "" }}</span>
</td>
<td>{{ order.machine_id or "" }}</td>
<td>
{% if order.contract_score >= 80 %}
<span class="ok">{{ order.contract_score }}</span>
{% elif order.contract_score >= 50 %}
<span class="warn">{{ order.contract_score }}</span>
{% else %}
<span class="bad">{{ order.contract_score }}</span>
{% endif %}
</td>
<td>
{% if order.contract_status == 'CONTRATO_CUMPLIDO' %}
<span class="ok">Contrato cumplido</span>
{% elif order.contract_status == 'VALIDO_CON_OBSERVACIONES' %}
<span class="warn">Válido con observaciones</span>
{% elif order.contract_status == 'BLOQUEADO_CRITICO' %}
<span class="bad">Bloqueado crítico</span>
{% elif order.contract_status == 'BLOQUEADO_ALTO' %}
<span class="bad">Bloqueado alto</span>
{% elif order.contract_status == 'BLOQUEADO_MEDIO' %}
<span class="warn">Bloqueado medio</span>
{% else %}
<span class="pill">{{ order.contract_status }}</span>
{% endif %}
</td>
<td>
Críticos: {{ order.critical_failures }}<br>
Altos: {{ order.high_failures }}<br>
Medios: {{ order.medium_failures }}<br>
Avisos: {{ order.warn_rules }}
</td>
<td>
<strong>{{ order.main_blocker_rule_code or "" }}</strong><br>
<span class="small">{{ order.main_blocker_rule_name or "" }}</span><br>
<span class="small">{{ order.main_blocker_finding or "" }}</span>
</td>
<td>{{ order.owner_area_to_act or "" }}</td>
<td class="small">{{ order.main_recommended_action or "" }}</td>
<td>
<a href="/report.html?order_ref={{ order.odoo_order_ref | urlencode }}" target="_blank">HTML</a>
|
<a href="/report.pdf?order_ref={{ order.odoo_order_ref | urlencode }}" target="_blank">PDF</a>
</td>
</tr>
{% else %}
<tr><td colspan="12">No hay órdenes evaluadas.</td></tr>
{% endfor %}
</table>
<h2>2. Resumen por área responsable</h2>
<table>
<tr>
<th>Área</th>
<th>Fallos</th>
<th>Avisos</th>
<th>Fallos críticos</th>
<th>Fallos altos</th>
<th>Órdenes afectadas</th>
<th>Score incidencia</th>
<th>Reglas afectadas</th>
<th>Órdenes</th>
</tr>
{% for owner in owners %}
<tr>
<td>{{ owner.owner_area }}</td>
<td>{{ owner.failed_checks }}</td>
<td>{{ owner.warn_checks }}</td>
<td>{{ owner.critical_failed_checks }}</td>
<td>{{ owner.high_failed_checks }}</td>
<td>{{ owner.affected_orders }}</td>
<td>{{ owner.total_issue_score }}</td>
<td>{{ owner.affected_rules }}</td>
<td class="small">{{ owner.affected_order_refs }}</td>
</tr>
{% else %}
<tr><td colspan="9">Sin incidencias por área.</td></tr>
{% endfor %}
</table>
<h2>3. Reglas con incidencias</h2>
<table>
<tr>
<th>Regla</th>
<th>Nombre</th>
<th>Dominio</th>
<th>Severidad</th>
<th>Estado</th>
<th>Área</th>
<th>Órdenes afectadas</th>
<th>Score total</th>
<th>Órdenes</th>
</tr>
{% for rule in rules %}
<tr>
<td>{{ rule.rule_code }}</td>
<td>{{ rule.rule_name }}</td>
<td>{{ rule.domain }}</td>
<td>{{ rule.severity }}</td>
<td>
{% if rule.check_status == 'FAIL' %}
<span class="bad">FAIL</span>
{% elif rule.check_status == 'WARN' %}
<span class="warn">WARN</span>
{% else %}
{{ rule.check_status }}
{% endif %}
</td>
<td>{{ rule.owner_area }}</td>
<td>{{ rule.order_count }}</td>
<td>{{ rule.total_issue_score }}</td>
<td class="small">{{ rule.affected_orders }}</td>
</tr>
{% else %}
<tr><td colspan="9">Sin reglas con incidencias.</td></tr>
{% endfor %}
</table>
</div>
</body>
</html>

View File

@ -64,6 +64,9 @@
<div class="page">
<h1>Informes de ejecución de orden</h1>
<p class="muted">{{ title }}</p>
<p>
<a href="/data-quality-contract.html" target="_blank">Abrir Data Quality Contract</a>
</p>
<p>
<a href="/cost-readiness.html" target="_blank">Abrir Cost Readiness Cockpit</a>

File diff suppressed because it is too large Load Diff