mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 16:13:17 +00:00
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Validación de endpoints y SPs Se probaron todos los endpoints nuevos (gastos, almacén, control presupuestal, IAM) y sus RPCs asociados. ### Bugs corregidos en migraciones/SQL 1. **IAM grants en schema core** — `iam-004e` intentaba `GRANT` sobre `core` con rol `iam_owner` (sin permiso). Los grants cruzados se movieron a `core-027-iam-rpc-cross-grants.sql`. 2. **`_cost_settings` ambiguo** — columnas `budget_warn_pct` etc. colisionaban con `RETURNS TABLE` en PL/pgSQL, rompiendo `fn_expense_create` y control presupuestal. Corregido en `core-028-fix-cost-settings-ambiguous.sql`. ### Tests añadidos - `api/cost_modules_test.ts` — CRUD RPC gastos, flujo almacén completo, control presupuestal, IAM permisos - `scripts/crud-smoke-test.sh` — smoke HTTP de todos los endpoints nuevos ### Resultados - `deno test cost_modules_test.ts` — 5/5 OK - `./scripts/crud-smoke-test.sh` — todos los checks OK (gastos CRUD, IVA, cost-settings, almacén, transferencias, cost-control, IAM) <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-eeec3b5c-f789-43e8-a353-0755e4706377?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-eeec3b5c-f789-43e8-a353-0755e4706377&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>
180 lines
5.3 KiB
PL/PgSQL
180 lines
5.3 KiB
PL/PgSQL
--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;
|