--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;