mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 11:23:18 +00:00
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Resumen Implementa las observaciones de facturas, nómina e IMSS sobre el catálogo de proveedores y la nómina semanal que ya existían. ## Qué queda disponible - Proveedores: régimen fiscal, código postal, estatus activo/inactivo y filtro por nombre o RFC. - Facturas recibidas y emitidas: Excel, UUID, revisión si el RFC no está en el catálogo, clasificación a obra/concepto y gasto `source = invoice` solo para la recibida clasificada. - Nómina: bono, descuento con observación, horas y día festivo dentro del armado semanal. La semana pagada no acepta cambios. - Préstamos: edición (libre sin pagos; con pagos solo nota y cuota) y estado de cuenta con saldo = entregado + comisión − pagos. - Finiquitos y liquidaciones: conceptos, neto descontando descuentos vigentes y saldo de préstamo, y gasto al pagar. - Dispersión: corte semanal, corrección antes de definitiva y comprobante. - Historial de alta, baja y reingreso en la ficha, más movimientos IMSS. - Reporte por periodo en Excel y PDF. - Jornal IMSS distinto del jornal real y consulta de cuotas exportable a Excel. ## Verificación `deno check` de la API pasó. En este entorno no hay Postgres, así que `api/pendientes_test.ts` queda listo para correr cuando la base tenga las migraciones 042–050 y el permiso IAM 007. <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-5e23dc3b-921e-4091-9650-7091d3a52bbf?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-5e23dc3b-921e-4091-9650-7091d3a52bbf&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>
230 lines
9.6 KiB
PL/PgSQL
230 lines
9.6 KiB
PL/PgSQL
--liquibase formatted sql
|
|
-- PANELS · core · finiquitos y liquidaciones
|
|
|
|
--changeset panel:core-046a-tables endDelimiter:; splitStatements:true
|
|
CREATE TABLE IF NOT EXISTS core.settlements (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
tenant_id INTEGER NOT NULL,
|
|
worker_id BIGINT NOT NULL REFERENCES core.workers(id),
|
|
project_id BIGINT REFERENCES core.projects(id) ON DELETE SET NULL,
|
|
company_id BIGINT REFERENCES core.companies(id),
|
|
kind TEXT NOT NULL CHECK (kind IN ('finiquito', 'liquidacion')),
|
|
termination_date DATE NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'borrador' CHECK (status IN ('borrador', 'pagado')),
|
|
notes TEXT NOT NULL DEFAULT '',
|
|
lines_total NUMERIC(14,2) NOT NULL DEFAULT 0,
|
|
pending_discounts NUMERIC(14,2) NOT NULL DEFAULT 0,
|
|
loan_balance NUMERIC(14,2) NOT NULL DEFAULT 0,
|
|
net NUMERIC(14,2) NOT NULL DEFAULT 0,
|
|
expense_id BIGINT REFERENCES core.expense_entries(id) ON DELETE SET NULL,
|
|
paid_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_core_settlements_worker ON core.settlements (tenant_id, worker_id);
|
|
ALTER TABLE core.settlements ENABLE ROW LEVEL SECURITY;
|
|
DROP POLICY IF EXISTS tenant_isolation ON core.settlements;
|
|
CREATE POLICY tenant_isolation ON core.settlements
|
|
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer)
|
|
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer);
|
|
|
|
CREATE TABLE IF NOT EXISTS core.settlement_lines (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
settlement_id BIGINT NOT NULL REFERENCES core.settlements(id) ON DELETE CASCADE,
|
|
concept TEXT NOT NULL,
|
|
base_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
|
|
amount NUMERIC(14,2) NOT NULL DEFAULT 0,
|
|
note TEXT NOT NULL DEFAULT ''
|
|
);
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON core.settlements TO panels_core_app;
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON core.settlement_lines TO panels_core_app;
|
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA core TO panels_core_app;
|
|
|
|
--changeset panel:core-046b-fn-settlement splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core._settlement_recalc(p_id bigint)
|
|
RETURNS void
|
|
LANGUAGE plpgsql
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_s settlements%ROWTYPE;
|
|
v_lines numeric;
|
|
v_disc numeric;
|
|
v_loans numeric;
|
|
BEGIN
|
|
SELECT * INTO v_s FROM settlements WHERE id = p_id;
|
|
SELECT COALESCE(SUM(amount), 0) INTO v_lines FROM settlement_lines WHERE settlement_id = p_id;
|
|
SELECT COALESCE(SUM(a.amount), 0) INTO v_disc
|
|
FROM payroll_adjustments a
|
|
JOIN payroll_weeks w ON w.id = a.week_id
|
|
WHERE a.worker_id = v_s.worker_id
|
|
AND a.tenant_id = v_s.tenant_id
|
|
AND a.kind = 'descuento'
|
|
AND w.status <> 'paid';
|
|
SELECT COALESCE(SUM(balance), 0) INTO v_loans FROM loans WHERE worker_id = v_s.worker_id AND balance > 0;
|
|
UPDATE settlements SET
|
|
lines_total = core.round_money(v_lines),
|
|
pending_discounts = core.round_money(v_disc),
|
|
loan_balance = core.round_money(v_loans),
|
|
net = core.round_money(v_lines - v_disc - v_loans)
|
|
WHERE id = p_id;
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core.fn_settlement_save(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_tid integer := core._payroll_tenant_id(payload);
|
|
v_id bigint := NULLIF(payload->>'id', '')::bigint;
|
|
v_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint;
|
|
v_kind text := coalesce(nullif(btrim(payload->>'kind'), ''), 'finiquito');
|
|
v_date date := NULLIF(payload->>'termination_date', '')::date;
|
|
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
|
|
v_company_id bigint := NULLIF(payload->>'company_id', '')::bigint;
|
|
BEGIN
|
|
IF v_kind NOT IN ('finiquito', 'liquidacion') OR v_worker_id IS NULL OR v_date IS NULL THEN
|
|
RETURN core.rpc_err('VALIDATION', 'fn_settlement_save: persona, tipo y fecha de baja son obligatorios',
|
|
jsonb_build_object('fn', 'fn_settlement_save'));
|
|
END IF;
|
|
IF v_id IS NULL THEN
|
|
INSERT INTO settlements (tenant_id, worker_id, project_id, company_id, kind, termination_date, notes)
|
|
VALUES (v_tid, v_worker_id, v_project_id, v_company_id, v_kind, v_date, coalesce(payload->>'notes', ''))
|
|
RETURNING id INTO v_id;
|
|
ELSE
|
|
UPDATE settlements SET
|
|
project_id = v_project_id,
|
|
company_id = v_company_id,
|
|
kind = v_kind,
|
|
termination_date = v_date,
|
|
notes = coalesce(payload->>'notes', notes)
|
|
WHERE id = v_id AND tenant_id = v_tid AND status = 'borrador';
|
|
IF NOT FOUND THEN
|
|
RETURN core.rpc_err('VALIDATION', 'fn_settlement_save: el finiquito no está en borrador',
|
|
jsonb_build_object('fn', 'fn_settlement_save', 'id', v_id));
|
|
END IF;
|
|
END IF;
|
|
IF jsonb_typeof(payload->'lines') = 'array' THEN
|
|
DELETE FROM settlement_lines WHERE settlement_id = v_id;
|
|
INSERT INTO settlement_lines (settlement_id, concept, base_amount, amount, note)
|
|
SELECT v_id,
|
|
coalesce(nullif(btrim(l->>'concept'), ''), 'Concepto'),
|
|
COALESCE((l->>'base_amount')::numeric, 0),
|
|
COALESCE((l->>'amount')::numeric, 0),
|
|
coalesce(l->>'note', '')
|
|
FROM jsonb_array_elements(payload->'lines') l;
|
|
END IF;
|
|
PERFORM core._settlement_recalc(v_id);
|
|
RETURN core.rpc_ok(jsonb_build_object('id', v_id), 'Finiquito guardado',
|
|
jsonb_build_object('fn', 'fn_settlement_save', 'id', v_id));
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_settlement_save', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core.fn_settlement_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_lines jsonb;
|
|
BEGIN
|
|
PERFORM core._settlement_recalc(v_id);
|
|
SELECT to_jsonb(s) INTO v_row FROM settlements s WHERE s.id = v_id;
|
|
IF v_row IS NULL THEN
|
|
RETURN core.rpc_err('NOT_FOUND', 'Finiquito no encontrado',
|
|
jsonb_build_object('fn', 'fn_settlement_get', 'id', v_id));
|
|
END IF;
|
|
SELECT COALESCE(jsonb_agg(to_jsonb(l) ORDER BY l.id), '[]'::jsonb)
|
|
INTO v_lines FROM settlement_lines l WHERE l.settlement_id = v_id;
|
|
RETURN core.rpc_ok(jsonb_build_object('settlement', v_row, 'lines', v_lines),
|
|
'Finiquito cargado', jsonb_build_object('fn', 'fn_settlement_get', 'id', v_id));
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_settlement_get', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core.fn_settlement_list(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_tid integer := core._payroll_tenant_id(payload);
|
|
v_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint;
|
|
v_rows jsonb;
|
|
BEGIN
|
|
SELECT COALESCE(jsonb_agg(to_jsonb(x) ORDER BY x.termination_date DESC, x.id DESC), '[]'::jsonb)
|
|
INTO v_rows
|
|
FROM (
|
|
SELECT s.*, w.first_name, w.last_name_p
|
|
FROM settlements s
|
|
JOIN workers w ON w.id = s.worker_id
|
|
WHERE s.tenant_id = v_tid
|
|
AND (v_worker_id IS NULL OR s.worker_id = v_worker_id)
|
|
) x;
|
|
RETURN core.rpc_ok(jsonb_build_object('settlements', v_rows), 'Finiquitos listados',
|
|
jsonb_build_object('fn', 'fn_settlement_list'));
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_settlement_list', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION core.fn_settlement_pay(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_s settlements%ROWTYPE;
|
|
v_company_id bigint;
|
|
v_expense_id bigint;
|
|
BEGIN
|
|
SELECT * INTO v_s FROM settlements WHERE id = NULLIF(payload->>'id', '')::bigint;
|
|
IF NOT FOUND THEN
|
|
RETURN core.rpc_err('NOT_FOUND', 'Finiquito no encontrado', jsonb_build_object('fn', 'fn_settlement_pay'));
|
|
END IF;
|
|
IF v_s.status = 'pagado' THEN
|
|
RETURN core.rpc_err('VALIDATION', 'El finiquito ya está pagado', jsonb_build_object('fn', 'fn_settlement_pay', 'id', v_s.id));
|
|
END IF;
|
|
PERFORM core._settlement_recalc(v_s.id);
|
|
SELECT * INTO v_s FROM settlements WHERE id = v_s.id;
|
|
v_company_id := COALESCE(v_s.company_id, (SELECT company_id FROM projects WHERE id = v_s.project_id), (SELECT company_id FROM workers WHERE id = v_s.worker_id));
|
|
IF v_company_id IS NULL THEN
|
|
RETURN core.rpc_err('VALIDATION', 'Indique la empresa para registrar el gasto del finiquito',
|
|
jsonb_build_object('fn', 'fn_settlement_pay', 'field', 'company_id'));
|
|
END IF;
|
|
INSERT INTO expense_entries (
|
|
tenant_id, company_id, project_id, expense_date, source, source_ref_id, description,
|
|
subtotal, iva_amount, total, iva_rate, status, notes
|
|
) VALUES (
|
|
v_s.tenant_id, v_company_id, v_s.project_id, v_s.termination_date, 'settlement', v_s.id,
|
|
format('%s trabajador %s', v_s.kind, v_s.worker_id),
|
|
v_s.net, 0, v_s.net, 0, 'confirmed', 'Pago de finiquito o liquidación'
|
|
) RETURNING id INTO v_expense_id;
|
|
UPDATE settlements SET status = 'pagado', paid_at = now(), expense_id = v_expense_id, company_id = v_company_id
|
|
WHERE id = v_s.id;
|
|
RETURN core.rpc_ok(
|
|
jsonb_build_object('id', v_s.id, 'net', v_s.net, 'expense_id', v_expense_id),
|
|
format('Finiquito id=%s pagado por %s', v_s.id, v_s.net),
|
|
jsonb_build_object('fn', 'fn_settlement_pay', 'id', v_s.id)
|
|
);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_settlement_pay', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|
|
|
|
--changeset panel:core-046c-grants endDelimiter:; splitStatements:true
|
|
GRANT EXECUTE ON FUNCTION core._settlement_recalc(bigint) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_settlement_save(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_settlement_get(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_settlement_list(jsonb) TO panels_core_app;
|
|
GRANT EXECUTE ON FUNCTION core.fn_settlement_pay(jsonb) TO panels_core_app;
|