panels-origin/db/core/changesets/049-payroll-report.sql
Alberto Martinez 90bcd48f95 Facturas, nómina, dispersión e IMSS (#28)
<!-- 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>&nbsp;<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>&nbsp;</div>
2026-09-25 03:26:30 +00:00

81 lines
3.5 KiB
PL/PgSQL

--liquibase formatted sql
-- PANELS · core · reporte de nómina por periodo
--changeset panel:core-049a-fn-payroll-report splitStatements:false
CREATE OR REPLACE FUNCTION core.fn_payroll_report(payload jsonb)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = core
AS $$
DECLARE
v_tid integer := core._payroll_tenant_id(payload);
v_from date := NULLIF(payload->>'date_from', '')::date;
v_to date := NULLIF(payload->>'date_to', '')::date;
v_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint;
v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
v_lines jsonb;
v_adjustments jsonb;
v_loans jsonb;
v_settlements jsonb;
v_total numeric;
BEGIN
IF v_from IS NULL OR v_to IS NULL THEN
RETURN core.rpc_err('VALIDATION', 'fn_payroll_report: date_from y date_to son obligatorios',
jsonb_build_object('fn', 'fn_payroll_report'));
END IF;
SELECT COALESCE(jsonb_agg(to_jsonb(x) ORDER BY x.week_start, x.last_name_p), '[]'::jsonb),
COALESCE(SUM(x.payable_net), 0)
INTO v_lines, v_total
FROM (
SELECT l.id, l.worker_id, w.first_name, w.last_name_p, s.kind, s.project_id, p.name AS project_name,
wk.week_start, wk.week_end, l.days, l.daily_wage, l.gross, l.discounts, l.loan_discount, l.payable_net
FROM payroll_week_lines l
JOIN payroll_sheets s ON s.id = l.sheet_id
JOIN payroll_weeks wk ON wk.id = s.week_id
JOIN workers w ON w.id = l.worker_id
LEFT JOIN projects p ON p.id = COALESCE(s.project_id, l.project_id)
WHERE wk.tenant_id = v_tid
AND wk.week_start BETWEEN v_from AND v_to
AND (v_worker_id IS NULL OR l.worker_id = v_worker_id)
AND (v_project_id IS NULL OR s.project_id = v_project_id OR l.project_id = v_project_id)
) x;
SELECT COALESCE(jsonb_agg(to_jsonb(a) ORDER BY a.adjustment_date), '[]'::jsonb) INTO v_adjustments
FROM payroll_adjustments a
JOIN payroll_weeks wk ON wk.id = a.week_id
WHERE a.tenant_id = v_tid
AND wk.week_start BETWEEN v_from AND v_to
AND (v_worker_id IS NULL OR a.worker_id = v_worker_id)
AND (v_project_id IS NULL OR a.project_id = v_project_id);
SELECT COALESCE(jsonb_agg(to_jsonb(p) ORDER BY p.created_at), '[]'::jsonb) INTO v_loans
FROM (
SELECT lp.amount, lp.label, lp.loan_id, l.worker_id, wk.week_start
FROM loan_payments lp
JOIN loans l ON l.id = lp.loan_id
JOIN payroll_weeks wk ON wk.id = lp.week_id
WHERE wk.tenant_id = v_tid
AND wk.week_start BETWEEN v_from AND v_to
AND (v_worker_id IS NULL OR l.worker_id = v_worker_id)
) p;
SELECT COALESCE(jsonb_agg(to_jsonb(s) ORDER BY s.termination_date), '[]'::jsonb) INTO v_settlements
FROM settlements s
WHERE s.tenant_id = v_tid
AND s.status = 'pagado'
AND s.termination_date BETWEEN v_from AND v_to
AND (v_worker_id IS NULL OR s.worker_id = v_worker_id)
AND (v_project_id IS NULL OR s.project_id = v_project_id);
RETURN core.rpc_ok(
jsonb_build_object(
'lines', v_lines, 'adjustments', v_adjustments, 'loan_payments', v_loans,
'settlements', v_settlements, 'payable_total', core.round_money(v_total)
),
format('Reporte de nómina %s a %s', v_from, v_to),
jsonb_build_object('fn', 'fn_payroll_report', 'date_from', v_from, 'date_to', v_to)
);
EXCEPTION WHEN OTHERS THEN
RETURN core.rpc_from_exception('fn_payroll_report', SQLSTATE, SQLERRM);
END;
$$;
--changeset panel:core-049b-grants endDelimiter:; splitStatements:true
GRANT EXECUTE ON FUNCTION core.fn_payroll_report(jsonb) TO panels_core_app;