From 61e36696b9764ed360ba07e01a3e0ee8e978418a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Sep 2026 20:19:13 +0000 Subject: [PATCH] feat: reclasificar gastos sin concepto o cambiar de partida MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- db/core/changelog-master.xml | 1 + .../034-fn-expense-update-reclassify.sql | 111 +++++++++++++ web-panel/assets/css/desktop-v2.css | 3 +- web-panel/pages/gastos.vue | 153 +++++++++++++++--- 4 files changed, 243 insertions(+), 25 deletions(-) create mode 100644 db/core/changesets/034-fn-expense-update-reclassify.sql diff --git a/db/core/changelog-master.xml b/db/core/changelog-master.xml index acf04c1..68c42c2 100644 --- a/db/core/changelog-master.xml +++ b/db/core/changelog-master.xml @@ -39,5 +39,6 @@ + diff --git a/db/core/changesets/034-fn-expense-update-reclassify.sql b/db/core/changesets/034-fn-expense-update-reclassify.sql new file mode 100644 index 0000000..15e1c3b --- /dev/null +++ b/db/core/changesets/034-fn-expense-update-reclassify.sql @@ -0,0 +1,111 @@ +--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; diff --git a/web-panel/assets/css/desktop-v2.css b/web-panel/assets/css/desktop-v2.css index 3ea0639..aec53a5 100644 --- a/web-panel/assets/css/desktop-v2.css +++ b/web-panel/assets/css/desktop-v2.css @@ -2286,7 +2286,8 @@ html.dark .p-dialog .p-dialog-header-icon { line-height: 1.35; overflow-wrap: anywhere; } -html.dark .budget-item-option-desc { +html.dark .budget-item-option-desc, +html.dark .budget-item-picker .budget-item-value-desc { color: #94a3b8; } diff --git a/web-panel/pages/gastos.vue b/web-panel/pages/gastos.vue index deb25e3..168cc69 100644 --- a/web-panel/pages/gastos.vue +++ b/web-panel/pages/gastos.vue @@ -3,7 +3,9 @@ + + @@ -11,20 +13,28 @@ - - + + @@ -38,7 +48,7 @@ - +