panels-origin/db/core/changesets/026-rpc-project-update-warehouse.sql
Cursor Agent ab89727522
feat: gastos, almacén central/obra y control presupuestal
Implementa el módulo integral de costos según el plan acordado:

- Esquema Liquibase: expense_entries, almacén (central + obra), IVA,
  tenant_cost_settings, permisos IAM y RLS
- Funciones RPC core: gastos, almacén, control presupuestal, sync nómina,
  cierre de almacén al concluir proyecto
- API HTTP: expenses_http, warehouse_http, cost_control_http, iam_http
- Middleware requirePermission con matriz IAM
- UI web-panel: /gastos, /almacen, /control-presupuesto, /usuarios
- Componentes BudgetItemPicker, CostSemaphore y semáforos CSS
- Smoke tests extendidos para gastos/almacén/control

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-04 04:04:17 +00:00

125 lines
4.6 KiB
PL/PgSQL

--liquibase formatted sql
-- PANELS · core · cierre de almacén al concluir proyecto
--changeset panel:core-026a-fn-project-update-warehouse splitStatements:false
CREATE OR REPLACE FUNCTION core.fn_project_update(payload jsonb)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = core
AS $$
DECLARE
v_id bigint := (payload->>'id')::bigint;
v_cur projects%ROWTYPE;
v_name text;
v_address text;
v_theme_id text;
v_status text;
v_company_id integer;
v_contract_amount numeric(14,2);
v_start_date date;
v_end_date date;
v_resident_name text;
v_siroc text;
v_payroll_tax_pct numeric(5,2);
v_close_env jsonb;
BEGIN
IF v_id IS NULL THEN
RETURN core.rpc_err(
'VALIDATION',
'fn_project_update: id es obligatorio',
jsonb_build_object('fn', 'fn_project_update', 'field', 'id')
);
END IF;
SELECT * INTO v_cur FROM projects WHERE id = v_id;
IF NOT FOUND THEN
RETURN core.rpc_err(
'NOT_FOUND',
format('fn_project_update: proyecto id=%s no encontrado', v_id),
jsonb_build_object('fn', 'fn_project_update', 'id', v_id)
);
END IF;
v_name := coalesce(nullif(btrim(payload->>'name'), ''), v_cur.name);
v_address := CASE WHEN payload ? 'address' THEN coalesce(nullif(btrim(payload->>'address'), ''), '') ELSE v_cur.address END;
v_theme_id := coalesce(nullif(btrim(payload->>'theme_id'), ''), v_cur.theme_id);
v_status := CASE
WHEN payload ? 'status' AND btrim(payload->>'status') IN ('activo', 'pausado', 'concluido', 'cancelado')
THEN btrim(payload->>'status')
ELSE v_cur.status
END;
v_company_id := coalesce((payload->>'company_id')::integer, v_cur.company_id);
v_contract_amount := CASE
WHEN payload ? 'contract_amount' THEN (payload->>'contract_amount')::numeric(14,2)
ELSE v_cur.contract_amount
END;
v_start_date := CASE
WHEN payload ? 'start_date' THEN nullif(btrim(payload->>'start_date'), '')::date
ELSE v_cur.start_date
END;
v_end_date := CASE
WHEN payload ? 'end_date' THEN nullif(btrim(payload->>'end_date'), '')::date
ELSE v_cur.end_date
END;
v_resident_name := CASE
WHEN payload ? 'resident_name' THEN coalesce(nullif(btrim(payload->>'resident_name'), ''), '')
ELSE v_cur.resident_name
END;
v_siroc := CASE
WHEN payload ? 'siroc' THEN upper(coalesce(nullif(btrim(payload->>'siroc'), ''), ''))
ELSE v_cur.siroc
END;
v_payroll_tax_pct := coalesce((payload->>'payroll_tax_pct')::numeric(5,2), v_cur.payroll_tax_pct);
IF v_company_id IS NULL THEN
RETURN core.rpc_err(
'VALIDATION',
'fn_project_update: company_id es obligatorio',
jsonb_build_object('fn', 'fn_project_update', 'field', 'company_id', 'id', v_id)
);
END IF;
IF NOT EXISTS (SELECT 1 FROM companies WHERE id = v_company_id) THEN
RETURN core.rpc_err(
'VALIDATION',
format('fn_project_update: empresa id=%s no encontrada', v_company_id),
jsonb_build_object('fn', 'fn_project_update', 'company_id', v_company_id, 'id', v_id)
);
END IF;
IF v_start_date IS NOT NULL AND v_end_date IS NOT NULL AND v_end_date < v_start_date THEN
RETURN core.rpc_err(
'VALIDATION',
'fn_project_update: la fecha de término no puede ser anterior al inicio',
jsonb_build_object('fn', 'fn_project_update', 'field', 'end_date', 'id', v_id)
);
END IF;
IF v_status IN ('concluido', 'cancelado') AND v_cur.status NOT IN ('concluido', 'cancelado') THEN
v_close_env := core.fn_warehouse_close_project(jsonb_build_object(
'project_id', v_id,
'force', COALESCE((payload->>'force_warehouse_close')::boolean, false),
'closed_by_name', coalesce(nullif(btrim(payload->>'closed_by_name'), ''), 'sistema'),
'close_notes', coalesce(nullif(btrim(payload->>'warehouse_close_notes'), ''), 'Cierre automático al concluir obra')
));
IF NOT COALESCE((v_close_env->>'ok')::boolean, false) THEN
RETURN v_close_env;
END IF;
END IF;
UPDATE projects SET
name = v_name,
address = v_address,
theme_id = v_theme_id,
status = v_status,
company_id = v_company_id,
contract_amount = v_contract_amount,
start_date = v_start_date,
end_date = v_end_date,
resident_name = v_resident_name,
siroc = v_siroc,
payroll_tax_pct = v_payroll_tax_pct
WHERE id = v_id;
RETURN core.rpc_ok(
jsonb_build_object('id', v_id, 'code', v_cur.code, 'status', v_status),
format('Proyecto %s actualizado', v_cur.code),
jsonb_build_object('fn', 'fn_project_update', 'id', v_id, 'code', v_cur.code)
);
EXCEPTION WHEN OTHERS THEN
RETURN core.rpc_from_exception('fn_project_update', SQLSTATE, SQLERRM);
END;
$$;