mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 11:23:18 +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>
409 lines
18 KiB
PL/PgSQL
409 lines
18 KiB
PL/PgSQL
--liquibase formatted sql
|
|
-- PANELS · core · RPC gastos e IVA
|
|
|
|
--changeset panel:core-022a-fn-expense-list splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.fn_expense_list(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_tid integer := core._cost_tenant_id(payload);
|
|
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
|
v_company_id bigint := NULLIF(payload->>'company_id', '')::bigint;
|
|
v_date_from date := NULLIF(payload->>'date_from', '')::date;
|
|
v_date_to date := NULLIF(payload->>'date_to', '')::date;
|
|
v_budget_item_id bigint := NULLIF(payload->>'budget_item_id', '')::bigint;
|
|
v_source text := nullif(btrim(payload->>'source'), '');
|
|
v_rows jsonb;
|
|
BEGIN
|
|
SELECT COALESCE(jsonb_agg(to_jsonb(x) ORDER BY x.expense_date DESC, x.id DESC), '[]'::jsonb)
|
|
INTO v_rows
|
|
FROM (
|
|
SELECT e.*,
|
|
bi.description AS budget_item_description,
|
|
bi.code AS budget_item_code
|
|
FROM expense_entries e
|
|
LEFT JOIN budget_items bi ON bi.id = e.budget_item_id
|
|
WHERE e.tenant_id = v_tid
|
|
AND e.status <> 'void'
|
|
AND (v_project_id IS NULL OR e.project_id = v_project_id)
|
|
AND (v_company_id IS NULL OR e.company_id = v_company_id)
|
|
AND (v_date_from IS NULL OR e.expense_date >= v_date_from)
|
|
AND (v_date_to IS NULL OR e.expense_date <= v_date_to)
|
|
AND (v_budget_item_id IS NULL OR e.budget_item_id = v_budget_item_id)
|
|
AND (v_source IS NULL OR e.source = v_source)
|
|
) x;
|
|
RETURN core.rpc_ok(
|
|
jsonb_build_object('expenses', v_rows, 'count', jsonb_array_length(v_rows)),
|
|
format('Listado de gastos: %s registro(s) para tenant %s', jsonb_array_length(v_rows), v_tid),
|
|
jsonb_build_object('fn', 'fn_expense_list', 'tenant_id', v_tid, 'count', jsonb_array_length(v_rows))
|
|
);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_expense_list', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-022b-fn-expense-get splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.fn_expense_get(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_id bigint := NULLIF(payload->>'id', '')::bigint;
|
|
v_row jsonb;
|
|
v_attachments jsonb;
|
|
BEGIN
|
|
IF v_id IS NULL THEN
|
|
RETURN core.rpc_err('VALIDATION', 'fn_expense_get: id es obligatorio',
|
|
jsonb_build_object('fn', 'fn_expense_get', 'field', 'id'));
|
|
END IF;
|
|
SELECT to_jsonb(e) INTO v_row FROM expense_entries e WHERE id = v_id;
|
|
IF v_row IS NULL THEN
|
|
RETURN core.rpc_err('NOT_FOUND', format('fn_expense_get: gasto id=%s no encontrado', v_id),
|
|
jsonb_build_object('fn', 'fn_expense_get', 'id', v_id));
|
|
END IF;
|
|
SELECT COALESCE(jsonb_agg(to_jsonb(a) ORDER BY a.uploaded_at DESC), '[]'::jsonb)
|
|
INTO v_attachments FROM expense_attachments a WHERE a.expense_id = v_id;
|
|
RETURN core.rpc_ok(
|
|
jsonb_build_object('expense', v_row, 'attachments', v_attachments),
|
|
format('Gasto id=%s recuperado con %s comprobante(s)', v_id, jsonb_array_length(v_attachments)),
|
|
jsonb_build_object('fn', 'fn_expense_get', 'id', v_id)
|
|
);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_expense_get', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-022c-fn-expense-create splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.fn_expense_create(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_tid integer := core._cost_tenant_id(payload);
|
|
v_company_id bigint := NULLIF(payload->>'company_id', '')::bigint;
|
|
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
|
v_expense_date date := COALESCE(NULLIF(payload->>'expense_date', '')::date, CURRENT_DATE);
|
|
v_description text := coalesce(nullif(btrim(payload->>'description'), ''), '');
|
|
v_budget_item_id bigint := NULLIF(payload->>'budget_item_id', '')::bigint;
|
|
v_budget_chapter_id bigint := NULLIF(payload->>'budget_chapter_id', '')::bigint;
|
|
v_quantity numeric := COALESCE((payload->>'quantity')::numeric, 0);
|
|
v_unit text := coalesce(nullif(btrim(payload->>'unit'), ''), '');
|
|
v_unit_price numeric := COALESCE((payload->>'unit_price')::numeric, 0);
|
|
v_subtotal_in numeric := COALESCE((payload->>'subtotal')::numeric, 0);
|
|
v_iva_rate numeric;
|
|
v_tax_included boolean := COALESCE((payload->>'tax_included')::boolean, false);
|
|
v_total_in numeric := (payload->>'total')::numeric;
|
|
v_subtotal numeric; v_iva_amount numeric; v_total numeric;
|
|
v_is_deductible boolean := COALESCE((payload->>'is_deductible')::boolean, true);
|
|
v_counts_toward_budget boolean := COALESCE((payload->>'counts_toward_budget')::boolean, true);
|
|
v_supplier_name text := coalesce(nullif(btrim(payload->>'supplier_name'), ''), '');
|
|
v_supplier_rfc text := upper(coalesce(nullif(btrim(payload->>'supplier_rfc'), ''), ''));
|
|
v_notes text := coalesce(nullif(btrim(payload->>'notes'), ''), '');
|
|
v_created_by_id integer := NULLIF(payload->>'created_by_id', '')::integer;
|
|
v_created_by_name text := coalesce(nullif(btrim(payload->>'created_by_name'), ''), '');
|
|
v_id bigint;
|
|
v_warning jsonb;
|
|
v_row jsonb;
|
|
BEGIN
|
|
IF v_company_id IS NULL THEN
|
|
RETURN core.rpc_err('VALIDATION', 'fn_expense_create: company_id es obligatorio',
|
|
jsonb_build_object('fn', 'fn_expense_create', 'field', 'company_id'));
|
|
END IF;
|
|
IF v_description = '' THEN
|
|
RETURN core.rpc_err('VALIDATION', 'fn_expense_create: description es obligatoria',
|
|
jsonb_build_object('fn', 'fn_expense_create', 'field', 'description'));
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM companies WHERE id = v_company_id) THEN
|
|
RETURN core.rpc_err('VALIDATION', format('fn_expense_create: empresa id=%s no encontrada', v_company_id),
|
|
jsonb_build_object('fn', 'fn_expense_create', 'company_id', v_company_id));
|
|
END IF;
|
|
IF v_project_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM projects WHERE id = v_project_id) THEN
|
|
RETURN core.rpc_err('NOT_FOUND', format('fn_expense_create: proyecto id=%s no encontrado', v_project_id),
|
|
jsonb_build_object('fn', 'fn_expense_create', 'project_id', v_project_id));
|
|
END IF;
|
|
IF v_subtotal_in <= 0 AND v_quantity > 0 AND v_unit_price > 0 THEN
|
|
v_subtotal_in := round(v_quantity * v_unit_price, 2);
|
|
END IF;
|
|
SELECT default_iva_rate INTO v_iva_rate FROM core._cost_settings(v_tid);
|
|
v_iva_rate := COALESCE((payload->>'iva_rate')::numeric, v_iva_rate);
|
|
SELECT c.subtotal, c.iva_amount, c.total INTO v_subtotal, v_iva_amount, v_total
|
|
FROM core._cost_calc_amounts(v_subtotal_in, v_iva_rate, v_tax_included, v_total_in) c;
|
|
IF v_budget_item_id IS NOT NULL THEN
|
|
SELECT chapter_id INTO v_budget_chapter_id FROM budget_items WHERE id = v_budget_item_id;
|
|
END IF;
|
|
INSERT INTO expense_entries (
|
|
tenant_id, company_id, project_id, expense_date, source, description,
|
|
supplier_name, supplier_rfc, budget_chapter_id, budget_item_id,
|
|
quantity, unit, unit_price, subtotal, iva_rate, iva_amount, total,
|
|
tax_included, is_deductible, counts_toward_budget, status,
|
|
created_by_id, created_by_name, notes
|
|
) VALUES (
|
|
v_tid, v_company_id, v_project_id, v_expense_date, 'manual', v_description,
|
|
v_supplier_name, v_supplier_rfc, v_budget_chapter_id, v_budget_item_id,
|
|
v_quantity, v_unit, v_unit_price, v_subtotal, v_iva_rate, v_iva_amount, v_total,
|
|
v_tax_included, v_is_deductible, v_counts_toward_budget, 'confirmed',
|
|
v_created_by_id, v_created_by_name, v_notes
|
|
) RETURNING id INTO v_id;
|
|
IF v_counts_toward_budget AND v_budget_item_id IS NOT NULL THEN
|
|
v_warning := core._cost_budget_warning(v_tid, v_budget_item_id, 0, 0);
|
|
END IF;
|
|
SELECT to_jsonb(e) INTO v_row FROM expense_entries e WHERE id = v_id;
|
|
RETURN core.rpc_created(
|
|
jsonb_build_object('expense', v_row, 'budget_warning', v_warning),
|
|
format('Gasto registrado por %s (subtotal %s, IVA %s)', v_description, v_subtotal, v_iva_amount),
|
|
jsonb_build_object('fn', 'fn_expense_create', 'id', v_id)
|
|
);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_expense_create', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-022d-fn-expense-update splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.fn_expense_update(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_id bigint := NULLIF(payload->>'id', '')::bigint;
|
|
v_tid integer := core._cost_tenant_id(payload);
|
|
v_cur expense_entries%ROWTYPE;
|
|
v_warning jsonb;
|
|
v_row jsonb;
|
|
v_subtotal numeric; v_iva_amount numeric; v_total numeric;
|
|
BEGIN
|
|
IF v_id IS NULL THEN
|
|
RETURN core.rpc_err('VALIDATION', 'fn_expense_update: id es obligatorio',
|
|
jsonb_build_object('fn', 'fn_expense_update', 'field', 'id'));
|
|
END IF;
|
|
SELECT * INTO v_cur FROM expense_entries WHERE id = v_id;
|
|
IF NOT FOUND THEN
|
|
RETURN core.rpc_err('NOT_FOUND', format('fn_expense_update: gasto id=%s no encontrado', v_id),
|
|
jsonb_build_object('fn', 'fn_expense_update', 'id', v_id));
|
|
END IF;
|
|
IF v_cur.is_locked OR v_cur.source <> 'manual' THEN
|
|
RETURN core.rpc_err('VALIDATION', 'fn_expense_update: solo se editan gastos manuales no bloqueados',
|
|
jsonb_build_object('fn', 'fn_expense_update', 'id', v_id, 'source', v_cur.source));
|
|
END IF;
|
|
UPDATE expense_entries SET
|
|
expense_date = COALESCE(NULLIF(payload->>'expense_date', '')::date, expense_date),
|
|
description = COALESCE(nullif(btrim(payload->>'description'), ''), description),
|
|
supplier_name = CASE WHEN payload ? 'supplier_name' THEN coalesce(nullif(btrim(payload->>'supplier_name'), ''), '') ELSE supplier_name END,
|
|
budget_item_id = CASE WHEN payload ? 'budget_item_id' THEN NULLIF(payload->>'budget_item_id', '')::bigint ELSE budget_item_id END,
|
|
quantity = CASE WHEN payload ? 'quantity' THEN COALESCE((payload->>'quantity')::numeric, 0) ELSE quantity END,
|
|
unit = CASE WHEN payload ? 'unit' THEN coalesce(nullif(btrim(payload->>'unit'), ''), '') ELSE unit END,
|
|
unit_price = CASE WHEN payload ? 'unit_price' THEN COALESCE((payload->>'unit_price')::numeric, 0) ELSE unit_price END,
|
|
notes = CASE WHEN payload ? 'notes' THEN coalesce(nullif(btrim(payload->>'notes'), ''), '') ELSE notes END
|
|
WHERE id = v_id;
|
|
SELECT * INTO v_cur FROM expense_entries WHERE id = v_id;
|
|
SELECT c.subtotal, c.iva_amount, c.total INTO v_subtotal, v_iva_amount, v_total
|
|
FROM core._cost_calc_amounts(
|
|
CASE WHEN v_cur.quantity > 0 AND v_cur.unit_price > 0 THEN round(v_cur.quantity * v_cur.unit_price, 2) ELSE v_cur.subtotal END,
|
|
v_cur.iva_rate, v_cur.tax_included, NULL
|
|
) c;
|
|
UPDATE expense_entries SET subtotal = v_subtotal, iva_amount = v_iva_amount, total = v_total WHERE id = v_id;
|
|
IF v_cur.counts_toward_budget AND v_cur.budget_item_id IS NOT NULL THEN
|
|
v_warning := core._cost_budget_warning(v_tid, v_cur.budget_item_id, 0, 0);
|
|
END IF;
|
|
SELECT to_jsonb(e) INTO v_row FROM expense_entries e WHERE id = v_id;
|
|
RETURN core.rpc_ok(
|
|
jsonb_build_object('expense', v_row, 'budget_warning', v_warning),
|
|
format('Gasto id=%s actualizado', v_id),
|
|
jsonb_build_object('fn', 'fn_expense_update', 'id', v_id)
|
|
);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_expense_update', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-022e-fn-expense-void splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.fn_expense_void(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_id bigint := NULLIF(payload->>'id', '')::bigint;
|
|
v_cur expense_entries%ROWTYPE;
|
|
BEGIN
|
|
IF v_id IS NULL THEN
|
|
RETURN core.rpc_err('VALIDATION', 'fn_expense_void: id es obligatorio',
|
|
jsonb_build_object('fn', 'fn_expense_void', 'field', 'id'));
|
|
END IF;
|
|
SELECT * INTO v_cur FROM expense_entries WHERE id = v_id;
|
|
IF NOT FOUND THEN
|
|
RETURN core.rpc_err('NOT_FOUND', format('fn_expense_void: gasto id=%s no encontrado', v_id),
|
|
jsonb_build_object('fn', 'fn_expense_void', 'id', v_id));
|
|
END IF;
|
|
IF v_cur.is_locked THEN
|
|
RETURN core.rpc_err('VALIDATION', 'fn_expense_void: no se puede anular un gasto derivado de nómina',
|
|
jsonb_build_object('fn', 'fn_expense_void', 'id', v_id));
|
|
END IF;
|
|
UPDATE expense_entries SET status = 'void' WHERE id = v_id;
|
|
RETURN core.rpc_ok(
|
|
jsonb_build_object('id', v_id, 'status', 'void'),
|
|
format('Gasto id=%s anulado', v_id),
|
|
jsonb_build_object('fn', 'fn_expense_void', 'id', v_id)
|
|
);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_expense_void', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-022f-fn-expense-attachment-add splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.fn_expense_attachment_add(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_expense_id bigint := NULLIF(payload->>'expense_id', '')::bigint;
|
|
v_id bigint;
|
|
BEGIN
|
|
IF v_expense_id IS NULL OR NOT EXISTS (SELECT 1 FROM expense_entries WHERE id = v_expense_id) THEN
|
|
RETURN core.rpc_err('NOT_FOUND', 'fn_expense_attachment_add: gasto no encontrado',
|
|
jsonb_build_object('fn', 'fn_expense_attachment_add', 'expense_id', v_expense_id));
|
|
END IF;
|
|
INSERT INTO expense_attachments (
|
|
expense_id, original_name, mime, size_bytes, sha256, iv, storage_name,
|
|
uploaded_by_id, uploaded_by_name
|
|
) VALUES (
|
|
v_expense_id,
|
|
coalesce(nullif(btrim(payload->>'original_name'), ''), 'comprobante.pdf'),
|
|
coalesce(nullif(btrim(payload->>'mime'), ''), 'application/pdf'),
|
|
COALESCE((payload->>'size_bytes')::bigint, 0),
|
|
coalesce(nullif(btrim(payload->>'sha256'), ''), ''),
|
|
coalesce(nullif(btrim(payload->>'iv'), ''), ''),
|
|
coalesce(nullif(btrim(payload->>'storage_name'), ''), ''),
|
|
NULLIF(payload->>'uploaded_by_id', '')::integer,
|
|
coalesce(nullif(btrim(payload->>'uploaded_by_name'), ''), '')
|
|
) RETURNING id INTO v_id;
|
|
RETURN core.rpc_created(
|
|
jsonb_build_object('id', v_id, 'expense_id', v_expense_id),
|
|
format('Comprobante adjunto al gasto id=%s', v_expense_id),
|
|
jsonb_build_object('fn', 'fn_expense_attachment_add', 'id', v_id)
|
|
);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_expense_attachment_add', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-022g-fn-iva-period-summary splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.fn_iva_period_summary(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_tid integer := core._cost_tenant_id(payload);
|
|
v_company_id bigint := NULLIF(payload->>'company_id', '')::bigint;
|
|
v_year integer := COALESCE((payload->>'year')::integer, EXTRACT(YEAR FROM CURRENT_DATE)::integer);
|
|
v_month integer := COALESCE((payload->>'month')::integer, EXTRACT(MONTH FROM CURRENT_DATE)::integer);
|
|
v_acreditable numeric;
|
|
v_trasladado numeric;
|
|
BEGIN
|
|
SELECT COALESCE(SUM(e.iva_amount), 0) INTO v_acreditable
|
|
FROM expense_entries e
|
|
WHERE e.tenant_id = v_tid
|
|
AND e.status = 'confirmed'
|
|
AND e.is_deductible = true
|
|
AND EXTRACT(YEAR FROM e.expense_date) = v_year
|
|
AND EXTRACT(MONTH FROM e.expense_date) = v_month
|
|
AND (v_company_id IS NULL OR e.company_id = v_company_id);
|
|
SELECT COALESCE(iva_trasladado, 0) INTO v_trasladado
|
|
FROM iva_period_entries
|
|
WHERE tenant_id = v_tid
|
|
AND (v_company_id IS NULL OR company_id = v_company_id)
|
|
AND period_year = v_year AND period_month = v_month
|
|
LIMIT 1;
|
|
RETURN core.rpc_ok(
|
|
jsonb_build_object(
|
|
'year', v_year, 'month', v_month,
|
|
'iva_acreditable', round(COALESCE(v_acreditable, 0), 2),
|
|
'iva_trasladado', round(COALESCE(v_trasladado, 0), 2),
|
|
'iva_a_pagar', round(COALESCE(v_trasladado, 0) - COALESCE(v_acreditable, 0), 2)
|
|
),
|
|
format('Resumen IVA %s/%s: acreditable %s, trasladado %s', v_month, v_year, v_acreditable, v_trasladado),
|
|
jsonb_build_object('fn', 'fn_iva_period_summary', 'tenant_id', v_tid)
|
|
);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_iva_period_summary', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-022h-fn-cost-settings-get splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.fn_cost_settings_get(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_tid integer := core._cost_tenant_id(payload);
|
|
v_row jsonb;
|
|
BEGIN
|
|
SELECT to_jsonb(s) INTO v_row FROM tenant_cost_settings s WHERE tenant_id = v_tid;
|
|
IF v_row IS NULL THEN
|
|
v_row := jsonb_build_object('tenant_id', v_tid, 'budget_warn_pct', 85, 'budget_critical_pct', 100, 'default_iva_rate', 0.16);
|
|
END IF;
|
|
RETURN core.rpc_ok(v_row, format('Configuración de costos del tenant %s', v_tid),
|
|
jsonb_build_object('fn', 'fn_cost_settings_get', 'tenant_id', v_tid));
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_cost_settings_get', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-022i-fn-cost-settings-save splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.fn_cost_settings_save(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_tid integer := core._cost_tenant_id(payload);
|
|
BEGIN
|
|
INSERT INTO tenant_cost_settings (tenant_id, budget_warn_pct, budget_critical_pct, default_iva_rate, updated_at)
|
|
VALUES (
|
|
v_tid,
|
|
COALESCE((payload->>'budget_warn_pct')::numeric, 85),
|
|
COALESCE((payload->>'budget_critical_pct')::numeric, 100),
|
|
COALESCE((payload->>'default_iva_rate')::numeric, 0.16),
|
|
now()
|
|
)
|
|
ON CONFLICT (tenant_id) DO UPDATE SET
|
|
budget_warn_pct = EXCLUDED.budget_warn_pct,
|
|
budget_critical_pct = EXCLUDED.budget_critical_pct,
|
|
default_iva_rate = EXCLUDED.default_iva_rate,
|
|
updated_at = now();
|
|
RETURN core.rpc_ok(
|
|
(SELECT to_jsonb(s) FROM tenant_cost_settings s WHERE tenant_id = v_tid),
|
|
format('Configuración de costos guardada para tenant %s', v_tid),
|
|
jsonb_build_object('fn', 'fn_cost_settings_save', 'tenant_id', v_tid)
|
|
);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_cost_settings_save', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-022j-expense-grants endDelimiter:; splitStatements:true
|
|
GRANT EXECUTE ON FUNCTION core.fn_expense_list(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_expense_get(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_expense_create(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_expense_update(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_expense_void(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_expense_attachment_add(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_iva_period_summary(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_cost_settings_get(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_cost_settings_save(jsonb) TO panels_core_app;
|