panels-origin/db/core/changesets/034-fn-expense-update-reclassify.sql
Cursor Agent 61e36696b9
feat: reclasificar gastos sin concepto o cambiar de partida
Si un gasto se guardó sin clasificar, ahora se edita desde la lista (clic o Reclasificar) y se le asigna o cambia el concepto del presupuesto.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:19:13 +00:00

111 lines
5.2 KiB
PL/PgSQL

--liquibase formatted sql
-- PANELS · core · reclasificar gasto (concepto, proveedor, obra)
--changeset panel:core-034a-fn-expense-update-reclassify 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;
v_line numeric;
v_supplier_id bigint;
v_supplier_name text;
v_supplier_rfc text;
v_budget_item_id bigint;
v_chapter_id bigint;
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;
IF payload ? 'supplier_id' THEN
v_supplier_id := NULLIF(payload->>'supplier_id', '')::bigint;
IF v_supplier_id IS NOT NULL THEN
SELECT name, rfc INTO v_supplier_name, v_supplier_rfc
FROM suppliers
WHERE id = v_supplier_id AND tenant_id = v_tid AND status = 'activo';
IF NOT FOUND THEN
RETURN core.rpc_err('NOT_FOUND', format('fn_expense_update: proveedor id=%s no encontrado', v_supplier_id),
jsonb_build_object('fn', 'fn_expense_update', 'supplier_id', v_supplier_id));
END IF;
ELSE
v_supplier_name := '';
v_supplier_rfc := '';
END IF;
END IF;
IF payload ? 'budget_item_id' THEN
v_budget_item_id := NULLIF(payload->>'budget_item_id', '')::bigint;
IF v_budget_item_id IS NOT NULL THEN
SELECT chapter_id INTO v_chapter_id FROM budget_items WHERE id = v_budget_item_id;
END IF;
END IF;
UPDATE expense_entries SET
expense_date = COALESCE(NULLIF(payload->>'expense_date', '')::date, expense_date),
description = COALESCE(nullif(btrim(payload->>'description'), ''), description),
project_id = CASE WHEN payload ? 'project_id' THEN NULLIF(payload->>'project_id', '')::bigint ELSE project_id END,
supplier_id = CASE WHEN payload ? 'supplier_id' THEN v_supplier_id ELSE supplier_id END,
supplier_name = CASE
WHEN payload ? 'supplier_id' THEN coalesce(v_supplier_name, '')
WHEN payload ? 'supplier_name' THEN coalesce(nullif(btrim(payload->>'supplier_name'), ''), '')
ELSE supplier_name END,
supplier_rfc = CASE
WHEN payload ? 'supplier_id' THEN coalesce(v_supplier_rfc, '')
WHEN payload ? 'supplier_rfc' THEN upper(coalesce(nullif(btrim(payload->>'supplier_rfc'), ''), ''))
ELSE supplier_rfc END,
budget_item_id = CASE WHEN payload ? 'budget_item_id' THEN v_budget_item_id ELSE budget_item_id END,
budget_chapter_id = CASE WHEN payload ? 'budget_item_id' THEN v_chapter_id ELSE budget_chapter_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,
tax_included = CASE WHEN payload ? 'tax_included' THEN COALESCE((payload->>'tax_included')::boolean, false) ELSE tax_included 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;
v_line := 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;
SELECT c.subtotal, c.iva_amount, c.total INTO v_subtotal, v_iva_amount, v_total
FROM core._cost_calc_amounts(
v_line,
v_cur.iva_rate,
v_cur.tax_included,
CASE WHEN v_cur.tax_included THEN v_line ELSE NULL END
) 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, 'budget_item_id', v_cur.budget_item_id)
);
EXCEPTION WHEN OTHERS THEN
RETURN core.rpc_from_exception('fn_expense_update', SQLSTATE, SQLERRM);
END;
$$;
--changeset panel:core-034b-fn-expense-update-grant endDelimiter:; splitStatements:true
GRANT EXECUTE ON FUNCTION core.fn_expense_update(jsonb) TO panels_core_app;