panels-origin/scripts/verify-work-program-load.sql
Alberto Martinez 0045ccd8ed fix: programado vs costo (warehouse_movements.project_id no existe)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Problema

Tras cargar el programa, la pantalla pide en paralelo get / concepts / partidas / **vs-cost**. El vs-cost fallaba:

`fn_work_program_vs_cost (sqlstate=42703): column m.project_id does not exist`

`warehouse_movements` no tiene `project_id`. El proyecto está en `warehouses.project_id`.

## Cambio (changeset 038)

- Salidas de almacén por `JOIN warehouses w ON w.id = m.warehouse_id WHERE w.project_id = …`
- Ejecutado por partida **sin duplicar** al cruzar varias semanas (el join anterior multiplicaba el monto)

## Prueba local (hecha antes de pedir merge)

En Postgres 16 corrí las **4 RPCs** que usa la página, con presupuesto, 2 semanas, gasto $50 y salida de almacén $25:

- `fn_work_program_get` — curva 400 / 600
- `fn_work_program_list_concepts` — items
- `fn_work_program_list_partidas` — A001
- `fn_work_program_vs_cost` — ejecutado semanal **75**, por partida **75** (no 150)

Script: `scripts/verify-work-program-load.sh`

## Cómo probar en tu ambiente

1. Mergear y aplicar Liquibase (038).
2. Recargar Programa de obra en la obra ya importada. Sin toast rojo.
3. La pestaña / bloque de programado vs costo debe abrir.

<!-- 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>
2026-09-08 21:55:34 +00:00

114 lines
4.5 KiB
PL/PgSQL

-- Verifica las 4 lecturas que carga Programa de obra (get, concepts, partidas, vs-cost).
-- Esquema mínimo con las columnas reales. No es Liquibase.
DROP SCHEMA IF EXISTS core CASCADE;
CREATE SCHEMA core;
SET search_path = core;
CREATE FUNCTION rpc_generic_messages() RETURNS text[] LANGUAGE sql IMMUTABLE AS $$
SELECT ARRAY['OK']::text[];
$$;
CREATE FUNCTION rpc_assert_message(p_message text) RETURNS void LANGUAGE plpgsql IMMUTABLE AS $$
BEGIN
IF p_message IS NULL OR btrim(p_message) = '' THEN RAISE EXCEPTION 'empty'; END IF;
END;
$$;
CREATE FUNCTION rpc_ok(p_data jsonb, p_message text, p_ctx jsonb DEFAULT '{}'::jsonb) RETURNS jsonb
LANGUAGE plpgsql IMMUTABLE AS $$
BEGIN
PERFORM rpc_assert_message(p_message);
RETURN jsonb_build_object('ok', true, 'code', 'OK', 'message', p_message, 'data', p_data);
END;
$$;
CREATE FUNCTION rpc_err(p_code text, p_message text, p_ctx jsonb DEFAULT '{}'::jsonb, p_errors jsonb DEFAULT NULL) RETURNS jsonb
LANGUAGE plpgsql IMMUTABLE AS $$
BEGIN
PERFORM rpc_assert_message(p_message);
RETURN jsonb_build_object('ok', false, 'code', p_code, 'message', p_message, 'data', NULL);
END;
$$;
CREATE FUNCTION rpc_from_exception(p_fn text, p_sqlstate text, p_message text, p_detail text DEFAULT NULL) RETURNS jsonb
LANGUAGE plpgsql IMMUTABLE AS $$
BEGIN
RETURN rpc_err('INTERNAL', format('Error inesperado en %s (sqlstate=%s): %s', p_fn, p_sqlstate, p_message),
jsonb_build_object('fn', p_fn, 'sqlstate', p_sqlstate));
END;
$$;
CREATE TABLE companies (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text);
CREATE TABLE projects (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text, status text DEFAULT 'activo', tenant_id int, start_date date, end_date date
);
CREATE TABLE budget_chapters (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
project_id BIGINT REFERENCES projects(id),
code text, name text, sort_order int DEFAULT 0
);
CREATE TABLE budget_items (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
project_id BIGINT REFERENCES projects(id),
chapter_id BIGINT REFERENCES budget_chapters(id),
code text, description text, unit text DEFAULT '', amount numeric(14,2) DEFAULT 0, sort_order int DEFAULT 0
);
CREATE TABLE work_programs (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
project_id BIGINT UNIQUE REFERENCES projects(id),
start_date date, end_date date, duration_days int, source text, status text
);
CREATE TABLE work_program_periods (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
program_id BIGINT REFERENCES work_programs(id),
period_index int, label text, start_date date, end_date date
);
CREATE TABLE work_program_partida_amounts (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
program_id BIGINT REFERENCES work_programs(id),
budget_chapter_id BIGINT REFERENCES budget_chapters(id),
period_id BIGINT REFERENCES work_program_periods(id),
amount numeric(14,2)
);
CREATE TABLE work_program_concept_slots (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
program_id BIGINT, budget_item_id BIGINT, period_id BIGINT, start_pct numeric, end_pct numeric
);
CREATE TABLE work_program_progress (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
program_id BIGINT, budget_item_id BIGINT, period_id BIGINT, pct_complete numeric
);
CREATE TABLE expense_entries (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
project_id BIGINT, expense_date date, subtotal numeric(14,2),
status text, counts_toward_budget boolean, budget_item_id BIGINT
);
CREATE TABLE warehouses (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_id int, kind text, project_id BIGINT
);
CREATE TABLE warehouse_materials (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, tenant_id int, code text, name text
);
CREATE TABLE warehouse_movements (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_id int,
warehouse_id BIGINT REFERENCES warehouses(id),
material_id BIGINT,
movement_type text,
movement_date date,
quantity numeric,
total_cost numeric(14,2),
budget_item_id BIGINT
);
CREATE FUNCTION _cost_executed_amount(p_budget_item_id bigint) RETURNS numeric
LANGUAGE sql STABLE SET search_path = core AS $$
SELECT round(COALESCE(
(SELECT SUM(e.subtotal) FROM expense_entries e
WHERE e.budget_item_id = p_budget_item_id AND e.status = 'confirmed' AND e.counts_toward_budget = true),
0
) + COALESCE(
(SELECT SUM(m.total_cost) FROM warehouse_movements m
WHERE m.budget_item_id = p_budget_item_id AND m.movement_type = 'exit'),
0
), 2);
$$;