panels-origin/db/core/changesets/038-fn-work-program-vs-cost.sql
Cursor Agent 2d661d7a81
fix: programado vs costo usa el almacén de la obra
warehouse_movements no tiene project_id; el ejecutado semanal se
suma por warehouses.project_id. El ejecutado por partida ya no se
duplica al cruzar varias semanas. Verificado en Postgres con las
cuatro lecturas de la pantalla (get, concepts, partidas, vs-cost).

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 21:54:32 +00:00

103 lines
3.7 KiB
PL/PgSQL

--liquibase formatted sql
-- PANELS · core · fn_work_program_vs_cost: warehouse_movements no tiene project_id
--changeset panel:core-038-fn-work-program-vs-cost splitStatements:false
CREATE OR REPLACE FUNCTION core.fn_work_program_vs_cost(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_id bigint;
v_periods jsonb;
v_by_period jsonb;
v_by_chapter jsonb;
v_budget_total numeric(14,2);
BEGIN
IF v_project_id IS NULL THEN
RETURN core.rpc_err('VALIDATION', 'fn_work_program_vs_cost: project_id es obligatorio',
jsonb_build_object('fn', 'fn_work_program_vs_cost'));
END IF;
SELECT id INTO v_program_id FROM work_programs WHERE project_id = v_project_id;
IF v_program_id IS NULL THEN
RETURN core.rpc_ok(jsonb_build_object('periods', '[]'::jsonb, 'by_period', '[]'::jsonb, 'by_chapter', '[]'::jsonb),
format('Proyecto %s sin programa', v_project_id),
jsonb_build_object('fn', 'fn_work_program_vs_cost', 'project_id', v_project_id));
END IF;
SELECT COALESCE(SUM(amount), 0) INTO v_budget_total FROM budget_items WHERE project_id = v_project_id;
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;
SELECT COALESCE(jsonb_agg(to_jsonb(x) ORDER BY x.period_index), '[]'::jsonb) INTO v_by_period
FROM (
SELECT
pr.period_index,
pr.label,
pr.start_date,
pr.end_date,
round(COALESCE(SUM(pa.amount), 0), 2) AS scheduled_amount,
round(COALESCE((
SELECT SUM(e.subtotal)
FROM expense_entries e
WHERE e.project_id = v_project_id
AND e.status = 'confirmed'
AND e.counts_toward_budget = true
AND e.expense_date BETWEEN pr.start_date AND pr.end_date
), 0) + COALESCE((
SELECT SUM(m.total_cost)
FROM warehouse_movements m
JOIN warehouses w ON w.id = m.warehouse_id
WHERE w.project_id = v_project_id
AND m.movement_type = 'exit'
AND m.movement_date BETWEEN pr.start_date AND pr.end_date
), 0), 2) AS executed_amount
FROM work_program_periods pr
LEFT JOIN work_program_partida_amounts pa ON pa.period_id = pr.id AND pa.program_id = v_program_id
WHERE pr.program_id = v_program_id
GROUP BY pr.id, pr.period_index, pr.label, pr.start_date, pr.end_date
) x;
SELECT COALESCE(jsonb_agg(to_jsonb(y) ORDER BY y.chapter_code), '[]'::jsonb) INTO v_by_chapter
FROM (
SELECT
bc.id AS chapter_id,
bc.code AS chapter_code,
bc.name AS chapter_name,
round(COALESCE((
SELECT SUM(pa.amount)
FROM work_program_partida_amounts pa
WHERE pa.budget_chapter_id = bc.id AND pa.program_id = v_program_id
), 0), 2) AS scheduled_amount,
round(COALESCE((
SELECT SUM(core._cost_executed_amount(bi.id))
FROM budget_items bi
WHERE bi.chapter_id = bc.id
), 0), 2) AS executed_amount,
round(COALESCE((
SELECT SUM(bi.amount)
FROM budget_items bi
WHERE bi.chapter_id = bc.id
), 0), 2) AS budget_amount
FROM budget_chapters bc
WHERE bc.project_id = v_project_id
) y;
RETURN core.rpc_ok(
jsonb_build_object(
'periods', v_periods,
'budget_subtotal', v_budget_total,
'by_period', v_by_period,
'by_chapter', v_by_chapter
),
format('Programado vs ejecutado del proyecto %s', v_project_id),
jsonb_build_object('fn', 'fn_work_program_vs_cost', 'project_id', v_project_id)
);
EXCEPTION WHEN OTHERS THEN
RETURN core.rpc_from_exception('fn_work_program_vs_cost', SQLSTATE, SQLERRM);
END;
$$;