mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 10:43:18 +00:00
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> <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> </div>
This commit is contained in:
parent
7ffdd9f7bf
commit
0045ccd8ed
5 changed files with 339 additions and 0 deletions
|
|
@ -47,5 +47,6 @@
|
|||
<include file="changesets/035-warehouse-materials-catalog.sql" relativeToChangelogFile="true"/>
|
||||
<include file="changesets/036-fn-work-program-get-curve.sql" relativeToChangelogFile="true"/>
|
||||
<include file="changesets/037-fn-work-program-get-curve-vrow.sql" relativeToChangelogFile="true"/>
|
||||
<include file="changesets/038-fn-work-program-vs-cost.sql" relativeToChangelogFile="true"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
|
|
|||
103
db/core/changesets/038-fn-work-program-vs-cost.sql
Normal file
103
db/core/changesets/038-fn-work-program-vs-cost.sql
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
--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;
|
||||
$$;
|
||||
91
scripts/verify-work-program-load-seed.sql
Normal file
91
scripts/verify-work-program-load-seed.sql
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
SET search_path = core;
|
||||
|
||||
INSERT INTO projects (name, start_date, end_date) VALUES ('Obra test', '2026-09-07', '2026-09-19');
|
||||
-- id = 1
|
||||
|
||||
INSERT INTO budget_chapters (project_id, code, name, sort_order) VALUES (1, 'A001', 'PRELIMINARES', 1);
|
||||
INSERT INTO budget_items (project_id, chapter_id, code, description, amount, sort_order)
|
||||
VALUES (1, 1, '10301-001', 'Trazo', 1000, 1);
|
||||
|
||||
INSERT INTO work_programs (project_id, start_date, end_date, duration_days, source, status)
|
||||
VALUES (1, '2026-09-07', '2026-09-19', 13, 'import', 'active');
|
||||
|
||||
INSERT INTO work_program_periods (program_id, period_index, label, start_date, end_date) VALUES
|
||||
(1, 1, 'Sem 1', '2026-09-07', '2026-09-13'),
|
||||
(1, 2, 'Sem 2', '2026-09-14', '2026-09-19');
|
||||
|
||||
INSERT INTO work_program_partida_amounts (program_id, budget_chapter_id, period_id, amount) VALUES
|
||||
(1, 1, 1, 400.00),
|
||||
(1, 1, 2, 600.00);
|
||||
|
||||
INSERT INTO work_program_concept_slots (program_id, budget_item_id, period_id, start_pct, end_pct)
|
||||
VALUES (1, 1, 1, 0, 50);
|
||||
|
||||
INSERT INTO expense_entries (project_id, expense_date, subtotal, status, counts_toward_budget, budget_item_id)
|
||||
VALUES (1, '2026-09-08', 50.00, 'confirmed', true, 1);
|
||||
|
||||
INSERT INTO warehouses (tenant_id, kind, project_id) VALUES (1, 'project', 1);
|
||||
INSERT INTO warehouse_materials (tenant_id, code, name) VALUES (1, 'CEM', 'Cemento');
|
||||
INSERT INTO warehouse_movements (tenant_id, warehouse_id, material_id, movement_type, movement_date, quantity, total_cost, budget_item_id)
|
||||
VALUES (1, 1, 1, 'exit', '2026-09-09', 1, 25.00, 1);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_get jsonb;
|
||||
v_concepts jsonb;
|
||||
v_partidas jsonb;
|
||||
v_vs jsonb;
|
||||
v_broken jsonb;
|
||||
BEGIN
|
||||
v_get := fn_work_program_get(jsonb_build_object('project_id', 1));
|
||||
IF NOT COALESCE((v_get->>'ok')::boolean, false) THEN
|
||||
RAISE EXCEPTION 'fn_work_program_get failed: %', v_get->>'message';
|
||||
END IF;
|
||||
IF jsonb_array_length(v_get->'data'->'curve') <> 2 THEN
|
||||
RAISE EXCEPTION 'curve expected 2 periods, got %', v_get->'data'->'curve';
|
||||
END IF;
|
||||
IF (v_get->'data'->'curve'->0->>'amount')::numeric <> 400 THEN
|
||||
RAISE EXCEPTION 'curve week1 amount %, expected 400', v_get->'data'->'curve'->0->>'amount';
|
||||
END IF;
|
||||
IF (v_get->'data'->'curve'->1->>'amount')::numeric <> 600 THEN
|
||||
RAISE EXCEPTION 'curve week2 amount %, expected 600', v_get->'data'->'curve'->1->>'amount';
|
||||
END IF;
|
||||
|
||||
v_concepts := fn_work_program_list_concepts(jsonb_build_object('project_id', 1));
|
||||
IF NOT COALESCE((v_concepts->>'ok')::boolean, false) THEN
|
||||
RAISE EXCEPTION 'fn_work_program_list_concepts failed: %', v_concepts->>'message';
|
||||
END IF;
|
||||
IF jsonb_array_length(v_concepts->'data'->'items') < 1 THEN
|
||||
RAISE EXCEPTION 'concepts empty: %', v_concepts;
|
||||
END IF;
|
||||
|
||||
v_partidas := fn_work_program_list_partidas(jsonb_build_object('project_id', 1));
|
||||
IF NOT COALESCE((v_partidas->>'ok')::boolean, false) THEN
|
||||
RAISE EXCEPTION 'fn_work_program_list_partidas failed: %', v_partidas->>'message';
|
||||
END IF;
|
||||
IF (v_partidas->'data'->'chapters'->0->>'code') <> 'A001' THEN
|
||||
RAISE EXCEPTION 'partida code %, expected A001', v_partidas->'data'->'chapters'->0->>'code';
|
||||
END IF;
|
||||
|
||||
v_vs := fn_work_program_vs_cost(jsonb_build_object('project_id', 1));
|
||||
IF NOT COALESCE((v_vs->>'ok')::boolean, false) THEN
|
||||
RAISE EXCEPTION 'fn_work_program_vs_cost failed: %', v_vs->>'message';
|
||||
END IF;
|
||||
-- Sem 1: gasto 50 + salida almacén 25 = 75
|
||||
IF (v_vs->'data'->'by_period'->0->>'executed_amount')::numeric <> 75 THEN
|
||||
RAISE EXCEPTION 'vs-cost week1 executed %, expected 75. payload=%',
|
||||
v_vs->'data'->'by_period'->0->>'executed_amount', v_vs;
|
||||
END IF;
|
||||
IF (v_vs->'data'->'by_period'->0->>'scheduled_amount')::numeric <> 400 THEN
|
||||
RAISE EXCEPTION 'vs-cost week1 scheduled %, expected 400', v_vs->'data'->'by_period'->0->>'scheduled_amount';
|
||||
END IF;
|
||||
IF (v_vs->'data'->'by_chapter'->0->>'executed_amount')::numeric <> 75 THEN
|
||||
RAISE EXCEPTION 'vs-cost chapter executed %, expected 75', v_vs->'data'->'by_chapter'->0->>'executed_amount';
|
||||
END IF;
|
||||
IF (v_vs->'data'->'by_chapter'->0->>'scheduled_amount')::numeric <> 1000 THEN
|
||||
RAISE EXCEPTION 'vs-cost chapter scheduled %, expected 1000', v_vs->'data'->'by_chapter'->0->>'scheduled_amount';
|
||||
END IF;
|
||||
|
||||
RAISE NOTICE 'OK get+concepts+partidas+vs-cost';
|
||||
END;
|
||||
$$;
|
||||
30
scripts/verify-work-program-load.sh
Executable file
30
scripts/verify-work-program-load.sh
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env bash
|
||||
# Corre las 4 RPCs de carga de Programa de obra contra Postgres local.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
PSQL="${PSQL:-psql}"
|
||||
URL="${1:-}"
|
||||
|
||||
if [[ -z "$URL" ]]; then
|
||||
echo "uso: $0 postgresql://user@host:port/db" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
TMP="$(mktemp /tmp/wp-load-XXXXXX.sql)"
|
||||
{
|
||||
cat "$ROOT/scripts/verify-work-program-load.sql"
|
||||
sed -n '/^CREATE OR REPLACE FUNCTION core.fn_work_program_list_concepts/,/^\$\$;$/p' \
|
||||
"$ROOT/db/core/changesets/030-rpc-work-program.sql"
|
||||
sed -n '/^CREATE OR REPLACE FUNCTION core.fn_work_program_list_partidas/,/^\$\$;$/p' \
|
||||
"$ROOT/db/core/changesets/030-rpc-work-program.sql"
|
||||
sed -n '/^CREATE OR REPLACE FUNCTION core.fn_work_program_get/,/^\$\$;$/p' \
|
||||
"$ROOT/db/core/changesets/037-fn-work-program-get-curve-vrow.sql"
|
||||
sed -n '/^CREATE OR REPLACE FUNCTION core.fn_work_program_vs_cost/,/^\$\$;$/p' \
|
||||
"$ROOT/db/core/changesets/038-fn-work-program-vs-cost.sql"
|
||||
cat "$ROOT/scripts/verify-work-program-load-seed.sql"
|
||||
} > "$TMP"
|
||||
chmod 644 "$TMP"
|
||||
|
||||
"$PSQL" "$URL" -v ON_ERROR_STOP=1 -f "$TMP"
|
||||
rm -f "$TMP"
|
||||
echo "verify-work-program-load: OK"
|
||||
114
scripts/verify-work-program-load.sql
Normal file
114
scripts/verify-work-program-load.sql
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
-- 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);
|
||||
$$;
|
||||
Loading…
Reference in a new issue