--liquibase formatted sql -- PANELS · core · RPC control presupuestal --changeset panel:core-024a-fn-cost-control-by-item splitStatements:false CREATE OR REPLACE FUNCTION core.fn_cost_control_by_item(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint; v_tid integer := core._cost_tenant_id(payload); v_warn numeric; v_critical numeric; v_rows jsonb; BEGIN IF v_project_id IS NULL THEN RETURN core.rpc_err('VALIDATION', 'fn_cost_control_by_item: project_id es obligatorio', jsonb_build_object('fn', 'fn_cost_control_by_item', 'field', 'project_id')); END IF; SELECT budget_warn_pct, budget_critical_pct INTO v_warn, v_critical FROM core._cost_settings(v_tid); SELECT COALESCE(jsonb_agg(to_jsonb(x) ORDER BY x.code, x.id), '[]'::jsonb) INTO v_rows FROM ( SELECT bi.id, bi.code, bi.description, bi.unit, bi.chapter_id, bi.quantity AS qty_budget, bi.amount AS amount_budget, bi.unit_price, core._cost_executed_qty(bi.id) AS qty_executed, core._cost_executed_amount(bi.id) AS amount_executed, round(CASE WHEN bi.amount > 0 THEN core._cost_executed_amount(bi.id) / bi.amount * 100 ELSE 0 END, 1) AS pct_amount, round(core._cost_executed_amount(bi.id) - bi.amount, 2) AS delta_amount, CASE WHEN bi.amount <= 0 AND core._cost_executed_amount(bi.id) > 0 THEN 'critical' WHEN bi.amount > 0 AND core._cost_executed_amount(bi.id) / bi.amount * 100 > v_critical THEN 'critical' WHEN bi.amount > 0 AND core._cost_executed_amount(bi.id) / bi.amount * 100 > v_warn THEN 'warn' ELSE 'ok' END AS semaphore FROM budget_items bi WHERE bi.project_id = v_project_id ) x; RETURN core.rpc_ok( jsonb_build_object('items', v_rows, 'count', jsonb_array_length(v_rows)), format('Control por concepto del proyecto %s: %s renglón(es)', v_project_id, jsonb_array_length(v_rows)), jsonb_build_object('fn', 'fn_cost_control_by_item', 'project_id', v_project_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_cost_control_by_item', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-024b-fn-cost-control-by-chapter splitStatements:false CREATE OR REPLACE FUNCTION core.fn_cost_control_by_chapter(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint; v_tid integer := core._cost_tenant_id(payload); v_warn numeric; v_critical numeric; v_rows jsonb; BEGIN IF v_project_id IS NULL THEN RETURN core.rpc_err('VALIDATION', 'fn_cost_control_by_chapter: project_id es obligatorio', jsonb_build_object('fn', 'fn_cost_control_by_chapter')); END IF; SELECT budget_warn_pct, budget_critical_pct INTO v_warn, v_critical FROM core._cost_settings(v_tid); WITH chapter_base AS ( SELECT bc.id, bc.parent_id, bc.code, bc.name, bc.wbs, bc.sort_order FROM budget_chapters bc WHERE bc.project_id = v_project_id ) SELECT COALESCE(jsonb_agg(to_jsonb(cs) ORDER BY cs.sort_order, cs.id), '[]'::jsonb) INTO v_rows FROM ( SELECT cb.id, cb.parent_id, cb.code, cb.name, cb.wbs, cb.sort_order, COALESCE(SUM(bi.amount), 0) AS amount_budget, COALESCE(SUM(core._cost_executed_amount(bi.id)), 0) AS amount_executed, round(CASE WHEN COALESCE(SUM(bi.amount), 0) > 0 THEN COALESCE(SUM(core._cost_executed_amount(bi.id)), 0) / COALESCE(SUM(bi.amount), 0) * 100 ELSE 0 END, 1) AS pct_amount, round(COALESCE(SUM(core._cost_executed_amount(bi.id)), 0) - COALESCE(SUM(bi.amount), 0), 2) AS delta_amount, CASE WHEN COALESCE(SUM(bi.amount), 0) <= 0 AND COALESCE(SUM(core._cost_executed_amount(bi.id)), 0) > 0 THEN 'critical' WHEN COALESCE(SUM(bi.amount), 0) > 0 AND COALESCE(SUM(core._cost_executed_amount(bi.id)), 0) / COALESCE(SUM(bi.amount), 0) * 100 > v_critical THEN 'critical' WHEN COALESCE(SUM(bi.amount), 0) > 0 AND COALESCE(SUM(core._cost_executed_amount(bi.id)), 0) / COALESCE(SUM(bi.amount), 0) * 100 > v_warn THEN 'warn' ELSE 'ok' END AS semaphore FROM chapter_base cb LEFT JOIN budget_items bi ON bi.chapter_id = cb.id GROUP BY cb.id, cb.parent_id, cb.code, cb.name, cb.wbs, cb.sort_order ) cs; RETURN core.rpc_ok(jsonb_build_object('chapters', v_rows), format('Control por partida del proyecto %s', v_project_id), jsonb_build_object('fn', 'fn_cost_control_by_chapter', 'project_id', v_project_id)); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_cost_control_by_chapter', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-024c-fn-cost-control-project-summary splitStatements:false CREATE OR REPLACE FUNCTION core.fn_cost_control_project_summary(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint; v_budget numeric; v_executed numeric; v_unclassified numeric; v_in_warehouse numeric; v_red_items integer; v_red_chapters integer; BEGIN IF v_project_id IS NULL THEN RETURN core.rpc_err('VALIDATION', 'fn_cost_control_project_summary: project_id es obligatorio', jsonb_build_object('fn', 'fn_cost_control_project_summary')); END IF; SELECT COALESCE(SUM(amount), 0) INTO v_budget FROM budget_items WHERE project_id = v_project_id; SELECT COALESCE(SUM(core._cost_executed_amount(id)), 0) INTO v_executed FROM budget_items WHERE project_id = v_project_id; SELECT COALESCE(SUM(subtotal), 0) INTO v_unclassified FROM expense_entries WHERE project_id = v_project_id AND status = 'confirmed' AND budget_item_id IS NULL AND counts_toward_budget = true; SELECT COALESCE(SUM(s.qty_on_hand * s.avg_unit_cost), 0) INTO v_in_warehouse FROM warehouse_stock s JOIN warehouses w ON w.id = s.warehouse_id WHERE w.project_id = v_project_id AND w.status = 'open'; SELECT COUNT(*)::integer INTO v_red_items FROM budget_items bi WHERE bi.project_id = v_project_id AND bi.amount > 0 AND core._cost_executed_amount(bi.id) > bi.amount; RETURN core.rpc_ok( jsonb_build_object( 'project_id', v_project_id, 'budget_subtotal', round(v_budget, 2), 'executed_subtotal', round(v_executed, 2), 'pct_executed', round(CASE WHEN v_budget > 0 THEN v_executed / v_budget * 100 ELSE 0 END, 1), 'delta_subtotal', round(v_executed - v_budget, 2), 'unclassified_amount', round(v_unclassified, 2), 'warehouse_stock_value', round(v_in_warehouse, 2), 'concepts_over_budget', v_red_items ), format('Resumen de control del proyecto %s: %s%% ejecutado', v_project_id, round(CASE WHEN v_budget > 0 THEN v_executed / v_budget * 100 ELSE 0 END, 1)), jsonb_build_object('fn', 'fn_cost_control_project_summary', 'project_id', v_project_id) ); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_cost_control_project_summary', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-024d-fn-cost-control-top-deviations splitStatements:false CREATE OR REPLACE FUNCTION core.fn_cost_control_top_deviations(payload jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = core AS $$ DECLARE v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint; v_limit integer := COALESCE((payload->>'limit')::integer, 10); v_rows jsonb; BEGIN SELECT COALESCE(jsonb_agg(to_jsonb(x)), '[]'::jsonb) INTO v_rows FROM ( SELECT bi.id, bi.code, bi.description, bi.amount AS amount_budget, core._cost_executed_amount(bi.id) AS amount_executed, round(core._cost_executed_amount(bi.id) - bi.amount, 2) AS delta_amount FROM budget_items bi WHERE bi.project_id = v_project_id AND bi.amount > 0 AND core._cost_executed_amount(bi.id) > bi.amount ORDER BY core._cost_executed_amount(bi.id) - bi.amount DESC LIMIT v_limit ) x; RETURN core.rpc_ok(jsonb_build_object('deviations', v_rows), format('Top %s desviaciones del proyecto %s', v_limit, v_project_id), jsonb_build_object('fn', 'fn_cost_control_top_deviations', 'project_id', v_project_id)); EXCEPTION WHEN OTHERS THEN RETURN core.rpc_from_exception('fn_cost_control_top_deviations', SQLSTATE, SQLERRM); END; $$; --changeset panel:core-024e-cost-control-grants endDelimiter:; splitStatements:true GRANT EXECUTE ON FUNCTION core.fn_cost_control_by_item(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_cost_control_by_chapter(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_cost_control_project_summary(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core.fn_cost_control_top_deviations(jsonb) TO panels_core_app;