fix(ucepsa): separate eligible and observed stop minutes
This commit is contained in:
parent
48fb79d452
commit
d1f1b9a6c8
@ -0,0 +1,94 @@
|
|||||||
|
# UCEPSA — Semántica del resumen de paros Shop Floor v0.3.4.1
|
||||||
|
|
||||||
|
## Hallazgo
|
||||||
|
|
||||||
|
La validación de v0.3.4 mostró para el workorder 340:
|
||||||
|
|
||||||
|
```text
|
||||||
|
physical_stop_count = 31
|
||||||
|
eligible_stop_count = 28
|
||||||
|
eligible_stop_minutes = 55,25
|
||||||
|
```
|
||||||
|
|
||||||
|
El diagnóstico descomponía esos 55 minutos en:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CANDIDATE_GROUP 32,88 min
|
||||||
|
CANDIDATE_INDIVIDUAL 17,51 min
|
||||||
|
EXCLUDED_NOISE 0,11 min
|
||||||
|
OPEN_MONITORING 4,81 min
|
||||||
|
```
|
||||||
|
|
||||||
|
La vista de resumen sumaba todos los minutos observados en la columna
|
||||||
|
`eligible_stop_minutes`, aunque el importe económico sí excluía correctamente
|
||||||
|
el ruido y el paro abierto.
|
||||||
|
|
||||||
|
## Corrección
|
||||||
|
|
||||||
|
`eligible_stop_minutes` pasa a incluir únicamente filas con:
|
||||||
|
|
||||||
|
```text
|
||||||
|
technically_eligible = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Se añaden columnas separadas:
|
||||||
|
|
||||||
|
- `observed_stop_minutes`
|
||||||
|
- `telemetry_covered_stop_minutes`
|
||||||
|
- `candidate_group_minutes`
|
||||||
|
- `candidate_individual_minutes`
|
||||||
|
- `excluded_noise_minutes`
|
||||||
|
- `open_monitoring_minutes`
|
||||||
|
- `blocked_minutes`
|
||||||
|
|
||||||
|
No se modifica la vista de candidatos, la cola SHADOW, la cola oficial ni los
|
||||||
|
datos raw.
|
||||||
|
|
||||||
|
## Resultado esperado para la sesión 340
|
||||||
|
|
||||||
|
A la hora de la validación inicial, aproximadamente:
|
||||||
|
|
||||||
|
```text
|
||||||
|
observed_stop_minutes 55,3
|
||||||
|
eligible_stop_minutes 50,4
|
||||||
|
candidate_group_minutes 32,9
|
||||||
|
candidate_individual 17,5
|
||||||
|
excluded_noise 0,1
|
||||||
|
open_monitoring 4,8
|
||||||
|
shadow_capacity_loss_eur 30,2
|
||||||
|
```
|
||||||
|
|
||||||
|
El paro abierto puede seguir creciendo hasta que la máquina vuelva a arrancar,
|
||||||
|
por lo que los valores exactos pueden variar.
|
||||||
|
|
||||||
|
## Despliegue
|
||||||
|
|
||||||
|
No es necesario detener servicios.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
FILE=/srv/mesavault/40-clients/ucepsa/edge-oee-demo/sql/versions/107_fix_shopfloor_stop_summary_semantics_v0341.sql
|
||||||
|
|
||||||
|
docker exec -i mv_ucepsa_postgres_hot sh -lc \
|
||||||
|
'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \
|
||||||
|
< "$FILE"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validación
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VALIDATION=/srv/mesavault/40-clients/ucepsa/edge-oee-demo/ops/validate_shopfloor_stop_summary_semantics_v0341.sql
|
||||||
|
|
||||||
|
docker exec -i mv_ucepsa_postgres_hot sh -lc \
|
||||||
|
'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \
|
||||||
|
< "$VALIDATION"
|
||||||
|
```
|
||||||
|
|
||||||
|
Valores obligatorios:
|
||||||
|
|
||||||
|
```text
|
||||||
|
eligible_decomposition_errors = 0
|
||||||
|
summary_candidate_mismatches = 0
|
||||||
|
open_or_noise_inflation_errors = 0
|
||||||
|
official_summary_rows = 0
|
||||||
|
official_operator_queue_rows = 0
|
||||||
|
```
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
\pset pager off
|
||||||
|
|
||||||
|
\echo '=== 1. Columnas semánticas v0.3.4.1 ==='
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
column_name,
|
||||||
|
data_type,
|
||||||
|
ordinal_position
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'mv_reports_ucepsa_prod'
|
||||||
|
AND table_name =
|
||||||
|
'li_shopfloor_session_stop_summary_v1'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
|
|
||||||
|
\echo '=== 2. Resumen corregido de la sesión 340 ==='
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
shopfloor_session_id,
|
||||||
|
machine_id,
|
||||||
|
production_order,
|
||||||
|
odoo_workorder_id,
|
||||||
|
session_status,
|
||||||
|
operator_name,
|
||||||
|
physical_stop_count,
|
||||||
|
eligible_stop_count,
|
||||||
|
observed_stop_minutes,
|
||||||
|
telemetry_covered_stop_minutes,
|
||||||
|
eligible_stop_minutes,
|
||||||
|
candidate_group_minutes,
|
||||||
|
candidate_individual_minutes,
|
||||||
|
excluded_noise_minutes,
|
||||||
|
open_monitoring_minutes,
|
||||||
|
blocked_minutes,
|
||||||
|
shadow_capacity_loss_eur,
|
||||||
|
official_ledger_eligible
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_shopfloor_session_stop_summary_v1
|
||||||
|
WHERE odoo_workorder_id = 340;
|
||||||
|
|
||||||
|
\echo '=== 3. Eligible = grupo + individual ==='
|
||||||
|
|
||||||
|
SELECT count(*) AS eligible_decomposition_errors
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_shopfloor_session_stop_summary_v1
|
||||||
|
WHERE abs(
|
||||||
|
eligible_stop_minutes
|
||||||
|
- (
|
||||||
|
candidate_group_minutes
|
||||||
|
+ candidate_individual_minutes
|
||||||
|
)
|
||||||
|
) > 0.02;
|
||||||
|
|
||||||
|
\echo '=== 4. Resumen coincide con candidatos técnicamente elegibles ==='
|
||||||
|
|
||||||
|
WITH expected AS (
|
||||||
|
SELECT
|
||||||
|
shopfloor_session_id,
|
||||||
|
round(
|
||||||
|
sum(eligible_duration_s) FILTER (
|
||||||
|
WHERE technically_eligible
|
||||||
|
) / 60.0,
|
||||||
|
2
|
||||||
|
) AS expected_eligible_minutes,
|
||||||
|
count(
|
||||||
|
DISTINCT source_stop_id
|
||||||
|
) FILTER (
|
||||||
|
WHERE technically_eligible
|
||||||
|
) AS expected_eligible_count
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_stop_shopfloor_candidates_v1
|
||||||
|
WHERE shopfloor_session_id IS NOT NULL
|
||||||
|
GROUP BY shopfloor_session_id
|
||||||
|
)
|
||||||
|
SELECT count(*) AS summary_candidate_mismatches
|
||||||
|
FROM expected e
|
||||||
|
JOIN
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_shopfloor_session_stop_summary_v1 s
|
||||||
|
ON s.shopfloor_session_id =
|
||||||
|
e.shopfloor_session_id
|
||||||
|
WHERE abs(
|
||||||
|
s.eligible_stop_minutes
|
||||||
|
- coalesce(e.expected_eligible_minutes, 0)
|
||||||
|
) > 0.02
|
||||||
|
OR s.eligible_stop_count
|
||||||
|
<> e.expected_eligible_count;
|
||||||
|
|
||||||
|
\echo '=== 5. Abiertos y ruido no inflan eligible_stop_minutes ==='
|
||||||
|
|
||||||
|
SELECT count(*) AS open_or_noise_inflation_errors
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_shopfloor_session_stop_summary_v1
|
||||||
|
WHERE eligible_stop_minutes
|
||||||
|
> observed_stop_minutes + 0.02
|
||||||
|
OR eligible_stop_minutes
|
||||||
|
+ excluded_noise_minutes
|
||||||
|
+ open_monitoring_minutes
|
||||||
|
+ blocked_minutes
|
||||||
|
> observed_stop_minutes + 0.05;
|
||||||
|
|
||||||
|
\echo '=== 6. Nada oficial ==='
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
count(*) FILTER (
|
||||||
|
WHERE official_ledger_eligible
|
||||||
|
) AS official_summary_rows
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_shopfloor_session_stop_summary_v1;
|
||||||
|
|
||||||
|
\echo '=== 7. Cola oficial original sigue vacía ==='
|
||||||
|
|
||||||
|
SELECT count(*) AS official_operator_queue_rows
|
||||||
|
FROM mv_reports_ucepsa_prod.li_operator_stop_queue_v2;
|
||||||
@ -0,0 +1,158 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW
|
||||||
|
mv_reports_ucepsa_prod.li_shopfloor_session_stop_summary_v1
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
c.shopfloor_session_id,
|
||||||
|
c.machine_id,
|
||||||
|
c.production_chain_key,
|
||||||
|
c.production_order,
|
||||||
|
c.odoo_production_id,
|
||||||
|
c.odoo_workorder_id,
|
||||||
|
c.product_id,
|
||||||
|
c.product_name,
|
||||||
|
c.session_from,
|
||||||
|
c.session_to,
|
||||||
|
c.session_status,
|
||||||
|
c.operator_employee_id,
|
||||||
|
c.operator_name,
|
||||||
|
c.operator_candidate_employee_id,
|
||||||
|
c.operator_candidate_name,
|
||||||
|
c.operator_attribution_status,
|
||||||
|
|
||||||
|
COUNT(
|
||||||
|
DISTINCT c.source_stop_id
|
||||||
|
) AS physical_stop_count,
|
||||||
|
|
||||||
|
COUNT(
|
||||||
|
DISTINCT c.source_stop_id
|
||||||
|
) FILTER (
|
||||||
|
WHERE c.technically_eligible
|
||||||
|
) AS eligible_stop_count,
|
||||||
|
|
||||||
|
COALESCE(
|
||||||
|
ROUND(
|
||||||
|
SUM(c.eligible_duration_s) FILTER (
|
||||||
|
WHERE c.technically_eligible
|
||||||
|
) / 60.0,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
0::numeric
|
||||||
|
) AS eligible_stop_minutes,
|
||||||
|
|
||||||
|
ROUND(
|
||||||
|
SUM(
|
||||||
|
COALESCE(
|
||||||
|
c.shadow_capacity_loss_eur,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
),
|
||||||
|
2
|
||||||
|
) AS shadow_capacity_loss_eur,
|
||||||
|
|
||||||
|
BOOL_OR(c.session_clipped)
|
||||||
|
AS any_session_clipped,
|
||||||
|
|
||||||
|
BOOL_OR(c.telemetry_clipped)
|
||||||
|
AS any_telemetry_clipped,
|
||||||
|
|
||||||
|
BOOL_OR(c.crosses_shopfloor_boundary)
|
||||||
|
AS any_crosses_shopfloor_boundary,
|
||||||
|
|
||||||
|
false AS official_ledger_eligible,
|
||||||
|
|
||||||
|
ROUND(
|
||||||
|
SUM(c.context_segment_duration_s) / 60.0,
|
||||||
|
2
|
||||||
|
) AS observed_stop_minutes,
|
||||||
|
|
||||||
|
ROUND(
|
||||||
|
SUM(c.telemetry_covered_duration_s) / 60.0,
|
||||||
|
2
|
||||||
|
) AS telemetry_covered_stop_minutes,
|
||||||
|
|
||||||
|
COALESCE(
|
||||||
|
ROUND(
|
||||||
|
SUM(c.eligible_duration_s) FILTER (
|
||||||
|
WHERE c.eligibility_status =
|
||||||
|
'CANDIDATE_GROUP'
|
||||||
|
) / 60.0,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
0::numeric
|
||||||
|
) AS candidate_group_minutes,
|
||||||
|
|
||||||
|
COALESCE(
|
||||||
|
ROUND(
|
||||||
|
SUM(c.eligible_duration_s) FILTER (
|
||||||
|
WHERE c.eligibility_status =
|
||||||
|
'CANDIDATE_INDIVIDUAL'
|
||||||
|
) / 60.0,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
0::numeric
|
||||||
|
) AS candidate_individual_minutes,
|
||||||
|
|
||||||
|
COALESCE(
|
||||||
|
ROUND(
|
||||||
|
SUM(c.eligible_duration_s) FILTER (
|
||||||
|
WHERE c.eligibility_status =
|
||||||
|
'EXCLUDED_NOISE'
|
||||||
|
) / 60.0,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
0::numeric
|
||||||
|
) AS excluded_noise_minutes,
|
||||||
|
|
||||||
|
COALESCE(
|
||||||
|
ROUND(
|
||||||
|
SUM(c.eligible_duration_s) FILTER (
|
||||||
|
WHERE c.eligibility_status =
|
||||||
|
'OPEN_MONITORING'
|
||||||
|
) / 60.0,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
0::numeric
|
||||||
|
) AS open_monitoring_minutes,
|
||||||
|
|
||||||
|
COALESCE(
|
||||||
|
ROUND(
|
||||||
|
SUM(c.context_segment_duration_s) FILTER (
|
||||||
|
WHERE c.eligibility_status LIKE
|
||||||
|
'BLOCKED_%'
|
||||||
|
) / 60.0,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
0::numeric
|
||||||
|
) AS blocked_minutes
|
||||||
|
|
||||||
|
FROM
|
||||||
|
mv_reports_ucepsa_prod
|
||||||
|
.li_stop_shopfloor_candidates_v1 c
|
||||||
|
|
||||||
|
WHERE c.shopfloor_session_id IS NOT NULL
|
||||||
|
|
||||||
|
GROUP BY
|
||||||
|
c.shopfloor_session_id,
|
||||||
|
c.machine_id,
|
||||||
|
c.production_chain_key,
|
||||||
|
c.production_order,
|
||||||
|
c.odoo_production_id,
|
||||||
|
c.odoo_workorder_id,
|
||||||
|
c.product_id,
|
||||||
|
c.product_name,
|
||||||
|
c.session_from,
|
||||||
|
c.session_to,
|
||||||
|
c.session_status,
|
||||||
|
c.operator_employee_id,
|
||||||
|
c.operator_name,
|
||||||
|
c.operator_candidate_employee_id,
|
||||||
|
c.operator_candidate_name,
|
||||||
|
c.operator_attribution_status;
|
||||||
|
|
||||||
|
COMMENT ON VIEW
|
||||||
|
mv_reports_ucepsa_prod.li_shopfloor_session_stop_summary_v1 IS
|
||||||
|
'Resumen SHADOW por sesión Shop Floor. eligible_stop_minutes incluye solo paros cerrados técnicamente elegibles; ruido, abiertos y bloqueados se muestran por separado.';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Loading…
x
Reference in New Issue
Block a user