--liquibase formatted sql -- PANELS · core · helpers de control de costos (compartidos por gastos/almacén) --changeset panel:core-021a-cost-tenant-id splitStatements:false CREATE OR REPLACE FUNCTION core._cost_tenant_id(payload jsonb) RETURNS integer LANGUAGE sql STABLE AS $$ SELECT COALESCE( NULLIF(payload->>'tenant_id', '')::integer, NULLIF(current_setting('app.tenant_id', true), '')::integer, 0 ); $$; --changeset panel:core-021b-cost-settings splitStatements:false CREATE OR REPLACE FUNCTION core._cost_settings(p_tenant_id integer) RETURNS TABLE ( budget_warn_pct numeric, budget_critical_pct numeric, default_iva_rate numeric ) LANGUAGE plpgsql STABLE SET search_path = core AS $$ BEGIN RETURN QUERY SELECT COALESCE(s.budget_warn_pct, 85::numeric), COALESCE(s.budget_critical_pct, 100::numeric), COALESCE(s.default_iva_rate, 0.16::numeric) FROM ( SELECT budget_warn_pct, budget_critical_pct, default_iva_rate FROM tenant_cost_settings WHERE tenant_id = p_tenant_id ) s UNION ALL SELECT 85::numeric, 100::numeric, 0.16::numeric WHERE NOT EXISTS (SELECT 1 FROM tenant_cost_settings WHERE tenant_id = p_tenant_id) LIMIT 1; END; $$; --changeset panel:core-021c-cost-executed-amount splitStatements:false CREATE OR REPLACE FUNCTION core._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); $$; --changeset panel:core-021d-cost-executed-qty splitStatements:false CREATE OR REPLACE FUNCTION core._cost_executed_qty(p_budget_item_id bigint) RETURNS numeric LANGUAGE sql STABLE SET search_path = core AS $$ SELECT COALESCE( (SELECT SUM(e.quantity) 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.quantity) FROM warehouse_movements m WHERE m.budget_item_id = p_budget_item_id AND m.movement_type = 'exit'), 0 ); $$; --changeset panel:core-021e-cost-budget-warning splitStatements:false CREATE OR REPLACE FUNCTION core._cost_budget_warning( p_tenant_id integer, p_budget_item_id bigint, p_add_subtotal numeric DEFAULT 0, p_add_qty numeric DEFAULT 0 ) RETURNS jsonb LANGUAGE plpgsql STABLE SET search_path = core AS $$ DECLARE v_item record; v_warn numeric; v_critical numeric; v_exec_amt numeric; v_exec_qty numeric; v_pct numeric; v_level text; BEGIN IF p_budget_item_id IS NULL THEN RETURN NULL; END IF; SELECT id, description, amount, quantity INTO v_item FROM budget_items WHERE id = p_budget_item_id; IF NOT FOUND OR COALESCE(v_item.amount, 0) <= 0 THEN RETURN NULL; END IF; SELECT budget_warn_pct, budget_critical_pct INTO v_warn, v_critical FROM core._cost_settings(p_tenant_id); v_exec_amt := core._cost_executed_amount(p_budget_item_id) + COALESCE(p_add_subtotal, 0); v_exec_qty := core._cost_executed_qty(p_budget_item_id) + COALESCE(p_add_qty, 0); v_pct := round(v_exec_amt / v_item.amount * 100, 1); IF v_pct > v_critical THEN v_level := 'critical'; ELSIF v_pct > v_warn THEN v_level := 'warn'; ELSE RETURN NULL; END IF; RETURN jsonb_build_object( 'level', v_level, 'budget_item_id', p_budget_item_id, 'pct_after', v_pct, 'executed_amount', v_exec_amt, 'budget_amount', v_item.amount, 'executed_qty', v_exec_qty, 'budget_qty', v_item.quantity, 'message', format( 'Esta operación dejará el concepto "%s" al %s%% del presupuesto (importe)', left(v_item.description, 80), v_pct ) ); END; $$; --changeset panel:core-021f-cost-calc-iva splitStatements:false CREATE OR REPLACE FUNCTION core._cost_calc_amounts( p_subtotal numeric, p_iva_rate numeric, p_tax_included boolean, p_total numeric DEFAULT NULL ) RETURNS TABLE (subtotal numeric, iva_amount numeric, total numeric) LANGUAGE plpgsql IMMUTABLE AS $$ DECLARE v_sub numeric; v_iva numeric; v_tot numeric; BEGIN IF p_tax_included AND p_total IS NOT NULL AND p_total > 0 THEN v_tot := round(p_total, 2); v_sub := round(v_tot / (1 + COALESCE(p_iva_rate, 0.16)), 2); v_iva := round(v_tot - v_sub, 2); ELSE v_sub := round(COALESCE(p_subtotal, 0), 2); v_iva := round(v_sub * COALESCE(p_iva_rate, 0.16), 2); v_tot := round(v_sub + v_iva, 2); END IF; RETURN QUERY SELECT v_sub, v_iva, v_tot; END; $$; --changeset panel:core-021g-cost-grants endDelimiter:; splitStatements:true GRANT EXECUTE ON FUNCTION core._cost_tenant_id(jsonb) TO panels_core_app; GRANT EXECUTE ON FUNCTION core._cost_settings(integer) TO panels_core_app; GRANT EXECUTE ON FUNCTION core._cost_executed_amount(bigint) TO panels_core_app; GRANT EXECUTE ON FUNCTION core._cost_executed_qty(bigint) TO panels_core_app; GRANT EXECUTE ON FUNCTION core._cost_budget_warning(integer, bigint, numeric, numeric) TO panels_core_app; GRANT EXECUTE ON FUNCTION core._cost_calc_amounts(numeric, numeric, boolean, numeric) TO panels_core_app;