fix: guardar programa de obra (GET fallaba por agregados SQL anidados)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Problema

La preview del Excel por partida ya funciona. Al pulsar **Importar** el programa sí se guarda (`Programa importado`), pero al recargar `GET /v1/projects/:id/work-program` aparece:

`Error inesperado en fn_work_program_get (sqlstate=42803): aggregate function calls cannot be nested`

Postgres no permite `jsonb_agg(... SUM(...))` en el mismo SELECT. Eso arma la curva semanal después del import.

## Cambio

Changeset `036`: `SUM` va en una subquery y `jsonb_agg` queda afuera. El import no cambia.

## Cómo probar

1. Aplicar migraciones Liquibase (changeset 036).
2. En una obra con presupuesto, importar el Excel por partida (el de 8 partidas / 9 semanas).
3. Tras **Importar**, la pantalla debe mostrar el programa (semanas y curva), sin el toast rojo.
4. Actualizar / volver a entrar a Programa de obra: sigue cargando.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-d8a5d34a-ead9-4912-8296-322cff799783?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-d8a5d34a-ead9-4912-8296-322cff799783&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>
This commit is contained in:
Alberto Martinez 2026-09-08 21:36:05 +00:00
parent 4750dab95f
commit 1a3fb49272
2 changed files with 93 additions and 0 deletions

View file

@ -45,5 +45,6 @@
<include file="changesets/033-catalog-suppliers-units.sql" relativeToChangelogFile="true"/>
<include file="changesets/034-fn-expense-update-reclassify.sql" relativeToChangelogFile="true"/>
<include file="changesets/035-warehouse-materials-catalog.sql" relativeToChangelogFile="true"/>
<include file="changesets/036-fn-work-program-get-curve.sql" relativeToChangelogFile="true"/>
</databaseChangeLog>

View file

@ -0,0 +1,92 @@
--liquibase formatted sql
-- PANELS · core · fn_work_program_get: jsonb_agg no puede anidar SUM (42803)
--changeset panel:core-036-fn-work-program-get-curve splitStatements:false
CREATE OR REPLACE FUNCTION core.fn_work_program_get(payload jsonb)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = core
AS $$
DECLARE
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
v_program jsonb;
v_periods jsonb;
v_budget_total numeric(14,2);
v_scheduled jsonb;
v_cumulative numeric := 0;
v_row jsonb;
v_arr jsonb := '[]'::jsonb;
v_pct_period numeric;
v_pct_cum numeric;
BEGIN
IF v_project_id IS NULL THEN
RETURN core.rpc_err('VALIDATION', 'fn_work_program_get: project_id es obligatorio',
jsonb_build_object('fn', 'fn_work_program_get'));
END IF;
SELECT to_jsonb(wp) INTO v_program
FROM work_programs wp WHERE wp.project_id = v_project_id;
IF v_program IS NULL THEN
SELECT COALESCE(SUM(amount), 0) INTO v_budget_total FROM budget_items WHERE project_id = v_project_id;
RETURN core.rpc_ok(
jsonb_build_object(
'program', NULL,
'periods', '[]'::jsonb,
'budget_subtotal', v_budget_total,
'curve', '[]'::jsonb
),
format('Proyecto %s sin programa de obra', v_project_id),
jsonb_build_object('fn', 'fn_work_program_get', 'project_id', v_project_id)
);
END IF;
SELECT COALESCE(jsonb_agg(to_jsonb(p) ORDER BY p.period_index), '[]'::jsonb) INTO v_periods
FROM work_program_periods p WHERE p.program_id = (v_program->>'id')::bigint;
SELECT COALESCE(SUM(amount), 0) INTO v_budget_total FROM budget_items WHERE project_id = v_project_id;
-- SUM va en la subquery; jsonb_agg afuera. Postgres no permite agregados anidados
-- (sqlstate 42803) y esto petaba el GET justo después de importar por partida.
SELECT COALESCE(jsonb_agg(to_jsonb(x) ORDER BY x.period_index), '[]'::jsonb)
INTO v_scheduled
FROM (
SELECT
pr.period_index,
pr.label,
round(COALESCE(SUM(pa.amount), 0), 2) AS amount
FROM work_program_periods pr
LEFT JOIN work_program_partida_amounts pa ON pa.period_id = pr.id
WHERE pr.program_id = (v_program->>'id')::bigint
GROUP BY pr.period_index, pr.label
) x;
FOR v_row IN SELECT value FROM jsonb_array_elements(v_scheduled) LOOP
v_pct_period := CASE WHEN v_budget_total > 0
THEN round((v_row.value->>'amount')::numeric / v_budget_total, 6) ELSE 0 END;
v_cumulative := v_cumulative + (v_row.value->>'amount')::numeric;
v_pct_cum := CASE WHEN v_budget_total > 0 THEN round(v_cumulative / v_budget_total, 6) ELSE 0 END;
v_arr := v_arr || jsonb_build_array(jsonb_build_object(
'period_index', v_row.value->'period_index',
'label', v_row.value->'label',
'amount', v_row.value->'amount',
'pct_period', v_pct_period,
'pct_cumulative', v_pct_cum
));
END LOOP;
RETURN core.rpc_ok(
jsonb_build_object(
'program', v_program,
'periods', v_periods,
'budget_subtotal', v_budget_total,
'curve', v_arr
),
format('Programa de obra del proyecto %s', v_project_id),
jsonb_build_object('fn', 'fn_work_program_get', 'project_id', v_project_id)
);
EXCEPTION WHEN OTHERS THEN
RETURN core.rpc_from_exception('fn_work_program_get', SQLSTATE, SQLERRM);
END;
$$;