Commit bdbbb023 by Vanesa Puma Yamo

feat: UIT desde tabla planilla.parametro_uit y detalle completo de boleta

Reemplaza UitProperties (config estática por año) por UitCache respaldado
en la entidad ParametroUit, y agrega RentaQuintaDetalleCompletoDTO para la
boleta con el detalle completo del cálculo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FmJuJaDLW5s4jZqdPjcF9
parent 6706f579
# Cálculo de renta de quinta categoría — referencia (Excel "Ver Historial - Cálculo")
Este documento resume la lógica del Excel de referencia (`Ver Historial - Cálculo.xlsx`,
hoja única) que sirvió de base para `RentaQuintaCalculadora`. Sirve para validar que el cálculo
del API reproduce el mismo criterio que usaba el área antes de tener el módulo.
## 1. Matriz mensualizada (filas 41-69 del Excel)
Los conceptos de `planilla.renta_quinta` se agrupan en 4 bloques, cada uno con un subtotal
mensual (columnas Ene..Dic + Total Anual):
| Bloque | Conceptos (columnas de `renta_quinta`) |
|---|---|
| **1. Ingresos fijos** | `basico`, `asignacion_familiar`, `movilidad` |
| **2. Extras y variables** | `horas_extras_25` + `horas_extras_35`, `apoyo_salud` + `asignacion_estudios` + `bono_desempeno` (apoyo y asignaciones), `dominical`, `canasta_navidad`+`vale_consumo`+`obsequios`, `bono_extraordinario`+`bono_productividad`+`bono_aniversario`+`reintegro` (bonos), `suma_graciosa` |
| **3. Vacaciones y liquidación** | `vacaciones_truncas` (compra de vacaciones), `vacaciones_gozadas_rem_vaca` (vacaciones truncas), `gratificacion_proporcional` (gratificación trunca), `bonificacion_extraordinaria` (bonificación extraordinaria trunca) |
| **4. Anuales excepcionales** | `gratificacion_julio_diciembre` (gratificación), utilidades (fila "UTILIDADES", sin columna dedicada aún en la tabla) |
Cada bloque tiene una fila de subtotal (`Subtotal Ingresos Fijos`, `Subtotal Extras y
Variables`, `Subtotal Vacaciones y Liquidación`, `Subtotal Anuales Excepcionales`). La suma de
los 4 subtotales por mes da el **TOTAL BRUTO MENSUAL** (fila 68), y la resta contra la
retención de boleta del mes da la fila de **RETENCIÓN EFECTUADA**.
> Nota: `sueldo_bruto` (columna de la tabla) no es un concepto sumado en la matriz — es el
> total ya calculado por planilla para ese mes; el Excel usa la matriz de conceptos para
> reconstruirlo/validarlo columna por columna.
## 2. División en 3 tramos (esto es el "dividir los montos totales")
El **TOTAL BRUTO REMUNERATIVO** (bloque 1 del Excel, fila 8) no es un solo número: se arma
sumando el total anual de cada bloque partido en **3 columnas de periodo**, que se corresponden
1:1 con las 3 fuentes de dato que ya usa `RentaQuintaCalculadora`:
| Columna Excel | Meses | Equivalente en el dominio (`CLAUDE.md`) |
|---|---|---|
| **Pasado real realizado** | Ene - Mar (meses ya cerrados, anteriores al de corte) | `historialReal` / `retenidoAnterior` — suma de los meses anteriores al de corte |
| **Planilla en curso** | Abril (mes de corte) | `planillaCargada` — retención/remuneración registrada en el mes de corte |
| **Proyección automatizada** | May - Dic (meses restantes del año) | Proyección: remuneración del mes de corte × meses restantes + gratificaciones aún no percibidas |
Fórmulas del Excel (fila 3, ejemplo con "Sueldo Bruto"):
```
Pasado real = Subtotal_bloque(Ene) + Subtotal_bloque(Feb) + Subtotal_bloque(Mar)
Planilla curso = Subtotal_bloque(Abril) -- mes de corte
Proyección = Subtotal_bloque(May) + ... + Subtotal_bloque(Dic)
Total bruto remunerativo (fila) = Pasado real + Planilla en curso + Proyección
```
Y el **TOTAL BRUTO REMUNERATIVO** general (E8) es la suma de esa columna para los 4 bloques —
exactamente `historialReal + planillaCargada + proyección` que ya calcula
`RentaQuintaCalculadora`, solo que el Excel lo arma sumando conceptos mes a mes en vez de partir
de un monto ya consolidado.
## 3. Parámetros SUNAT y escala (filas 11-31)
Igual que en `RentaQuintaCalculadora` / art. 40 y 53 LIR:
- `Deducción de ley = UIT × 7`
- Tramo 1 tope = `UIT × 5`
- Renta neta imponible = `MAX(0, TOTAL BRUTO REMUNERATIVO − 7 UIT)` (incluye ingresos de otros
empleadores si los hay)
- Escala progresiva sobre la renta neta:
- Hasta 5 UIT → 8%
- Más de 5 hasta 20 UIT → 14%
- Más de 20 hasta 35 UIT → 17%
- Más de 35 hasta 45 UIT → 20%
- Más de 45 UIT → 30%
- Impuesto anual = suma de los 5 tramos
## 4. Retención del mes (filas 33-38)
```
Saldo pendiente = Impuesto anual − Retenciones acumuladas anteriores (Ene..mes anterior al de corte)
Retención del mes de corte = Saldo pendiente ÷ Factor divisor legal del mes
```
Los factores divisores (Ene-Mar 12, Abr 9, May-Jul 8, Ago 5, Set-Nov 4, Dic 1) coinciden con los
documentados en `CLAUDE.md` para `RentaQuintaCalculadora`. La retención nunca es negativa
(`MAX(0, ...)` implícito en el Excel al no permitir saldo negativo dividido).
## Fuente
`C:\Users\sistem20user\Downloads\Ver Historial - Cálculo.xlsx` (hoja única, sin nombre).
# API Renta de Quinta Categoría — Guía para el Frontend
Base path: `/api/v1/renta-quinta`
Swagger UI: `/docs`
API de **solo lectura**. No hay endpoints de creación/edición/borrado (la carga de datos es batch/ETL).
---
## 1. GET `/api/v1/renta-quinta` — Grilla paginada
Grilla "Proyección y control de quinta categoría". Cada fila es **un trabajador** (agrupado por
`numero_documento` + nombres), no un registro mensual individual.
### Query params
| Parámetro | Tipo | Obligatorio | Default | Notas |
|---|---|---|---|---|
| `periodo` | Integer | **Sí** | — | Año a consultar |
| `mesCorte` | Integer | **Sí** | — | 1–12. Mes de corte de la proyección |
| `sede` | Integer | No | — | Filtro de catálogo (ver nota abajo) |
| `tipoPlanilla` | Integer | No | — | Filtro de catálogo |
| `areaGeneral` | Integer | No | — | Filtro de catálogo |
| `subArea` | Integer | No | — | Filtro de catálogo |
| `cargo` | Integer | No | — | Filtro de catálogo |
| `documentoOApellidos` | String | No | — | Texto libre: busca por número de documento **o** por apellidos + nombres (case-insensitive, `LIKE`) |
| `page` | Integer | No | `0` | Página (0-indexed) |
| `size` | Integer | No | `20` | Tamaño de página, **tope 100** |
⚠️ **Nota importante para el frontend**: `sede`, `tipoPlanilla`, `areaGeneral`, `subArea` y `cargo`
se declaran como `Integer` en la API, pero hoy en los datos reales esas columnas guardan **texto**
(ej. `sede='BARRANCO'`) y `codlocal` viene vacío. Mientras no exista un catálogo con IDs numéricos,
enviar estos filtros como número **no va a matchear ningún resultado**. Consultar con backend antes
de conectar dropdowns a estos filtros — es un tema pendiente de resolver (ver `CLAUDE.md` del
backend).
### Ejemplo de request
```
GET /api/v1/renta-quinta?periodo=2026&mesCorte=8&page=0&size=20
```
### Response — `Page<RentaQuintaResumenDTO>`
Estructura estándar de Spring Data `Page`:
```json
{
"content": [ /* array de RentaQuintaResumenDTO, ver abajo */ ],
"totalElements": 123,
"totalPages": 7,
"number": 0,
"size": 20,
"first": true,
"last": false,
"numberOfElements": 20,
"empty": false
}
```
#### `RentaQuintaResumenDTO`
| Campo | Tipo | Descripción |
|---|---|---|
| `codper` | Integer | Código de personal |
| `periodo` | Integer | Año consultado |
| `numeroDocumento` | String | DNI / documento del trabajador |
| `nombreCompleto` | String | Apellidos y nombres |
| `mesProceso` | Integer | Mes de corte usado en el cálculo |
| `historialReal` | BigDecimal | Suma de retenciones de los meses **anteriores** al de corte |
| `planillaCargada` | BigDecimal | Retención registrada en el **mes de corte** |
| `proyeccionFutura` | BigDecimal | Remuneración proyectada para los meses restantes del año + gratificaciones pendientes |
| `rentaBrutaAnual` | BigDecimal | `historialReal + planillaCargada + proyeccionFutura` |
| `impuestoAnual` | BigDecimal | Impuesto anual según tramos progresivos sobre la renta neta |
| `retenidoAnterior` | BigDecimal | Igual a `historialReal` (retenido antes del mes de corte) |
| `retencionDelMes` | BigDecimal | Monto a retener en el mes de corte (nunca negativo) |
---
## 2. GET `/api/v1/renta-quinta/{id}` — Boleta por ID
Trae el detalle completo de un registro puntual de `planilla.renta_quinta`.
### Path param
| Parámetro | Tipo | Notas |
|---|---|---|
| `id` | Long | `id_renta_quinta` |
### Ejemplo
```
GET /api/v1/renta-quinta/4521
```
### Response — `RentaQuintaDetalleDTO`
Mapeo 1:1 de las ~115 columnas de la tabla (boleta completa: datos personales, conceptos
remunerativos, descuentos, aportes, fechas de contrato/vacaciones, etc.). Ver el record completo en
`src/main/java/so/planilla_be/rentaquinta/dto/RentaQuintaDetalleDTO.java` para la lista exacta de
campos y tipos — no se resume aquí porque el frontend probablemente solo necesite un subconjunto
para renderizar la boleta.
Si no existe el `id`: **404** con el cuerpo de error estándar (ver sección 5).
---
## 2.1. GET `/api/v1/renta-quinta/detalle-completo` — Detalle completo por documento + año + mes
Devuelve, en **un solo JSON**, la boleta del mes junto con los puntos 1 a 5 del cálculo del Excel
de referencia "Ver Historial - Cálculo" (ver `docs/calculo-renta-quinta.md`): historial de
ingresos por columna de periodo, parámetros SUNAT, liquidación del impuesto anual, escala por
tramos y control de retenciones. El `mes` recibido se usa como **mes de corte**; se agregan el
resto de meses del año de ese mismo `numero_documento` (agrupación por trabajador, igual criterio
que la grilla — ver `CLAUDE.md`).
### Query params
| Parámetro | Tipo | Obligatorio | Notas |
|---|---|---|---|
| `numeroDocumento` | String | **Sí** | DNI/CE del trabajador |
| `anio` | Integer | **Sí** | Año de la boleta |
| `mes` | Integer | **Sí** | 1–12, mes de corte |
### Ejemplo
```
GET /api/v1/renta-quinta/detalle-completo?numeroDocumento=09742695&anio=2026&mes=8
```
### Response — `RentaQuintaDetalleCompletoDTO`
```json
{
"boleta": { /* RentaQuintaDetalleDTO completo de la boleta del mes consultado */ },
"registroHistorialIngresos": {
"pasadoRealRealizado": {
"sueldoBruto": 9600.00, "horasExtras": 0.00, "gratificacionesOrdinarias": 0.00,
"utilidadesDistribuidas": 0.00, "otrosEmpleadores": 0.00, "totalBrutoRemunerativo": 9600.00
},
"planillaEnCurso": { /* mismo shape, mes de corte */ },
"proyeccionAutomatizada": { /* mismo shape, meses restantes + gratificaciones pendientes */ },
"ingresosAnualesTotales": 51550.00
},
"parametrosSistema": {
"valorUit": 5350.00, "deduccionLey7Uit": 37450.00,
"montoTopeTramo1_5Uit": 26750.00, "factorDivisorMes": 5
},
"liquidacionImpuestoAnual": {
"rentaBrutaAnual": 51550.00, "otrosEmpleadores": 0.00, "deduccionLey7Uit": 37450.00,
"rentaNetaImponible": 14100.00, "impuestoAnualCalculado": 1128.00
},
"escalaImpositiva": {
"tramos": [
{ "tramo": "Hasta 5 UIT", "baseImponible": 14100.00, "tasa": 0.08, "impuesto": 1128.00 },
{ "tramo": "Más de 5 UIT hasta 20 UIT", "baseImponible": 0.00, "tasa": 0.14, "impuesto": 0.00 }
/* ... resto de tramos, solo hasta donde llega la renta neta */
],
"totalImpuestoAnualDeterminado": 1128.00
},
"controlRetenciones": {
"impuestoAnualReferencia": 1128.00, "retencionesAcumuladasAnteriores": 152.97,
"saldoPendienteRetencion": 975.03, "factorDivisorMes": 5, "retencionMesActual": 195.01
}
}
```
⚠️ **Pendiente / limitación conocida**: en el Excel de referencia, "Utilidades Distribuidas" y
"Otros Empleadores" son datos que no existen en `planilla.renta_quinta` (el segundo, además, se
edita manualmente en el Excel). Mientras no se defina una fuente de datos para ellos, el API
**siempre devuelve 0.00** en esos campos.
Si no existe boleta para ese `numeroDocumento` + `anio` + `mes`: **404** con el cuerpo de error
estándar (ver sección 5).
---
## 3. GET `/api/v1/renta-quinta/boleta` — Boleta por codper + año + mes
Igual que el endpoint anterior pero buscando por la combinación natural del registro.
### Query params
| Parámetro | Tipo | Obligatorio | Notas |
|---|---|---|---|
| `codper` | Integer | **Sí** | Código de personal |
| `anio` | Integer | **Sí** | Año |
| `mes` | Integer | **Sí** | 1–12 |
### Ejemplo
```
GET /api/v1/renta-quinta/boleta?codper=8801&anio=2026&mes=8
```
### Response
Igual que en el endpoint 2: `RentaQuintaDetalleDTO`. 404 si no existe el registro.
---
## 4. GET `/api/v1/renta-quinta/historial/{codper}/{anio}` — Historial anual
Pantalla "Ver historial": todos los meses cargados de un trabajador en un año dado.
### Path params
| Parámetro | Tipo |
|---|---|
| `codper` | Integer |
| `anio` | Integer |
### Ejemplo
```
GET /api/v1/renta-quinta/historial/8801/2026
```
### Response — `List<RentaQuintaHistorialDTO>`
Array (puede venir vacío, no es 404) con un elemento por mes cargado:
| Campo | Tipo |
|---|---|
| `idRentaQuinta` | Long |
| `codper` | Integer |
| `boletaAnio` | Integer |
| `boletaMes` | Integer |
| `boletaTipoPersonal` | String |
| `sede` | String |
| `areaGeneral` | String |
| `subArea` | String |
| `cargo` | String |
| `sueldoBruto` | BigDecimal |
| `basico` | BigDecimal |
| `asignacionFamiliar` | BigDecimal |
| `horasExtras25` | BigDecimal |
| `horasExtras35` | BigDecimal |
| `gratificacionJulioDiciembre` | BigDecimal |
| `gratificacionProporcional` | BigDecimal |
| `bonificacionExtraordinaria` | BigDecimal |
| `vacacionesGozadasRemVaca` | BigDecimal |
| `reintegro` | BigDecimal |
| `remBruta` | BigDecimal |
| `remuneracionImponibleSnpAfp` | BigDecimal |
| `totalAfp` | BigDecimal |
| `snp` | BigDecimal |
| `renta5taCategoria` | BigDecimal |
| `totalDscto` | BigDecimal |
| `netoPorPagar` | BigDecimal |
| `diasLaborados` | Integer |
| `diasNoLaborados` | Integer |
---
## 5. Manejo de errores
Todas las respuestas de error usan el mismo shape, sin stack traces:
```json
{
"timestamp": "2026-08-31T15:00:00Z",
"status": 404,
"message": "descripción del error"
}
```
| Status | Causa |
|---|---|
| `404` | Boleta no encontrada (`/{id}` o `/boleta` sin match) |
| `400` | `mesCorte` o `mes` fuera de rango (1–12) |
| `400` | Falta un parámetro obligatorio (`periodo`, `mesCorte`, `codper`, `anio`, `mes`) — mensaje: `"Falta el parámetro obligatorio 'x'"` |
| `400` | Un filtro numérico recibió un valor no numérico — mensaje: `"El parámetro 'x' debe ser numérico"` |
El frontend debe leer siempre `message` para mostrar el error al usuario; no depender del texto
exacto en `timestamp`.
---
## 6. Pendientes que afectan al frontend
- **Filtros de catálogo** (`sede`, `tipoPlanilla`, `areaGeneral`, `subArea`, `cargo`): no usar
todavía con IDs numéricos reales hasta que el backend confirme el catálogo — hoy no matchean.
- **UIT 2026 no cargada**: mientras no se configure, el cálculo usa un valor "por defecto" y todos
los importes (`proyeccionFutura`, `rentaBrutaAnual`, `impuestoAnual`, `retencionDelMes`) del
periodo 2026 pueden estar desactualizados. Si el frontend muestra estos campos, considerar un
aviso o validar con backend antes de pasar a producción.
- **Autenticación**: aún no implementada; no enviar tokens todavía (se integrará con
Trismegisto/Saco Oliveros más adelante).
- **Exportar a Excel / "calcular masivo"**: fuera de alcance del API actual, no hay endpoint.
-- Table: planilla.renta_quinta
-- DROP TABLE IF EXISTS planilla.renta_quinta;
CREATE TABLE IF NOT EXISTS planilla.renta_quinta
(
id_renta_quinta bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
boleta_anio integer,
boleta_mes integer,
boleta_tipo_personal character(3) COLLATE pg_catalog."default",
fecha_registro timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
codper integer,
codlocal character(4) COLLATE pg_catalog."default",
unidad_organizativa integer,
tipo_documento character(2) COLLATE pg_catalog."default",
apellido_paterno text COLLATE pg_catalog."default",
apellido_materno text COLLATE pg_catalog."default",
nombres text COLLATE pg_catalog."default",
documento_afp text COLLATE pg_catalog."default",
sueldo_bruto numeric(15,2),
rem_contrato numeric(15,2),
-- INGRESOS FIJOS
basico numeric(15,2),
asignacion_familiar numeric(15,2),
movilidad numeric(15,2),
-- EXTRAS Y VARIABLES
horas_extras_25 numeric(15,2),
horas_extras_35 numeric(15,2),
apoyo_salud numeric(15,2),
compensacion_vacacional numeric(15,2),
asignacion_estudios numeric(15,2),
bono_extraordinario numeric(15,2),
bono_productividad numeric(15,2),
reintegro numeric(15,2),
asignacion_fallecimiento_familiar numeric(15,2),
suma_graciosa numeric(15,2),
bono_desempeno numeric(15,2),
bono_aniversario numeric(15,2),
canasta_navidad numeric(15,2),
vale_consumo numeric(15,2),
obsequios numeric(15,2),
obsequios_descuento numeric(15,2),
canasta_navidad_2 numeric(15,2),
vale_consumo_descuento numeric(15,2),
dominical numeric(15,2),
bonificacion_extraordinaria numeric(15,2),
bonificacion_extraordinaria_2 numeric(15,2),
--VACACIONES Y LIQUIDACIONES
vacaciones_truncas numeric(15,2),
vacaciones_gozadas_rem_vaca numeric(15,2),
cts numeric(15,2),
gratificacion_proporcional numeric(15,2),
gratificacion_julio_diciembre numeric(15,2),
subsidio_enfermedad numeric(15,2),
licencia_goce_haber numeric(15,2),
subsidio_maternidad numeric(15,2),
licencia_salud_familiar_ley_30012 numeric(15,2),
rem_bruta numeric(15,2),
remuneracion_imponible_snp_afp numeric(15,2),
snp numeric(15,2),
afp_aportacion_obligatoria numeric(15,2),
prima_seguro numeric(15,2),
tipo_comision text COLLATE pg_catalog."default",
comision_afp_porcentual numeric(10,4),
total_afp numeric(15,2),
renta_5ta_categoria numeric(15,2),
adelanto numeric(15,2),
adelanto_vacaciones numeric(15,2),
retencion_judicial numeric(15,2),
tardanzas numeric(15,2),
inasistencias numeric(15,2),
pension numeric(15,2),
prestamos numeric(15,2),
uniformes_pavitos_navidad_otros numeric(15,2),
asistencia_tard_fsa_salida_anticipada numeric(15,2),
uniforme numeric(15,2),
pavos numeric(15,2),
colaboracion numeric(15,2),
seguro_accidente numeric(15,2),
rifa numeric(15,2),
otros numeric(15,2),
dcto_eps numeric(15,2),
total_dscto numeric(15,2),
neto_por_pagar numeric(15,2),
aportes numeric(15,2),
aporte_essalud_eps numeric(15,2),
essalud_vida numeric(15,2),
sctr_salud numeric(15,2),
sctr_pension numeric(15,2),
poliza_seguro numeric(15,2),
total_aportes numeric(15,2),
numero_documento text COLLATE pg_catalog."default",
cussp text COLLATE pg_catalog."default",
fecha_nacimiento date,
fecha_ingreso date,
fecha_cese date,
fondo_pension text COLLATE pg_catalog."default",
tipo_contrato text COLLATE pg_catalog."default",
fecha_inicio_contrato date,
fecha_termino_contrato date,
dias_laborados integer,
dias_no_laborados integer,
dias_subsidiados integer,
descanso_medico integer,
horas_normales integer,
horas_adicionales integer,
area_general text COLLATE pg_catalog."default",
sub_area text COLLATE pg_catalog."default",
cargo text COLLATE pg_catalog."default",
curso text COLLATE pg_catalog."default",
observacion text COLLATE pg_catalog."default",
sede text COLLATE pg_catalog."default",
genero text COLLATE pg_catalog."default",
inicio_vacaciones date,
termino_vacaciones date,
lsgh integer,
lcgh integer,
lcgh_fallecimiento integer,
faltas integer,
pago numeric(15,2),
pendiente numeric(15,2),
tipo_labor text COLLATE pg_catalog."default",
campo_1 integer,
campo_2 integer,
CONSTRAINT renta_quinta_pkey PRIMARY KEY (id_renta_quinta)
)
TABLESPACE pg_default;
ALTER TABLE IF EXISTS planilla.renta_quinta
OWNER to soporte;
package so.planilla_be.rentaquinta.calculo;
import java.math.BigDecimal;
import java.util.List;
/** Resultado del cálculo de quinta categoría para un trabajador en un mes de corte. */
public record CalculoRentaQuinta(
BigDecimal proyeccionFutura,
BigDecimal proyeccionSueldoBruto,
BigDecimal proyeccionGratificaciones,
BigDecimal rentaBrutaAnual,
BigDecimal rentaNeta,
BigDecimal impuestoAnual,
BigDecimal retencionDelMes
) {}
BigDecimal retencionDelMes,
BigDecimal valorUit,
BigDecimal divisorMes,
List<TramoCalculado> tramos
) {
/** Un tramo de la escala progresiva ya aplicado sobre la renta neta del trabajador. */
public record TramoCalculado(
String descripcion,
BigDecimal baseImponible,
BigDecimal tasa,
BigDecimal impuesto
) {}
}
package so.planilla_be.rentaquinta.calculo;
import org.springframework.stereotype.Component;
import so.planilla_be.rentaquinta.calculo.CalculoRentaQuinta.TramoCalculado;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
/**
......@@ -27,72 +29,85 @@ public class RentaQuintaCalculadora {
/** Tramos del art. 53: límite superior en UIT (null = sin tope) y tasa. */
private static final List<Tramo> TRAMOS = List.of(
new Tramo(BigDecimal.valueOf(5), new BigDecimal("0.08")),
new Tramo(BigDecimal.valueOf(20), new BigDecimal("0.14")),
new Tramo(BigDecimal.valueOf(35), new BigDecimal("0.17")),
new Tramo(BigDecimal.valueOf(45), new BigDecimal("0.20")),
new Tramo(null, new BigDecimal("0.30")));
new Tramo("Hasta 5 UIT", BigDecimal.valueOf(5), new BigDecimal("0.08")),
new Tramo("Más de 5 UIT hasta 20 UIT", BigDecimal.valueOf(20), new BigDecimal("0.14")),
new Tramo("Más de 20 UIT hasta 35 UIT", BigDecimal.valueOf(35), new BigDecimal("0.17")),
new Tramo("Más de 35 UIT hasta 45 UIT", BigDecimal.valueOf(45), new BigDecimal("0.20")),
new Tramo("Más de 45 UIT", null, new BigDecimal("0.30")));
/** Bonificación extraordinaria (ex-EsSalud) que acompaña a la gratificación. */
private static final BigDecimal FACTOR_GRATIFICACION = new BigDecimal("1.09");
private final UitProperties uit;
private final UitCache uit;
public RentaQuintaCalculadora(UitProperties uit) {
public RentaQuintaCalculadora(UitCache uit) {
this.uit = uit;
}
public CalculoRentaQuinta calcular(DatosCalculo datos) {
BigDecimal valorUit = uit.valorPara(datos.periodo());
BigDecimal proyeccionFutura = proyectarMesesFuturos(datos);
BigDecimal proyeccionSueldoBruto = orZero(datos.remuneracionMesCorte())
.multiply(BigDecimal.valueOf(12 - datos.mesCorte()));
BigDecimal proyeccionGratificaciones = gratificacionesPendientes(datos);
BigDecimal proyeccionFutura = proyeccionSueldoBruto.add(proyeccionGratificaciones);
BigDecimal rentaBrutaAnual = orZero(datos.historialReal())
.add(orZero(datos.planillaCargada()))
.add(proyeccionFutura);
BigDecimal rentaNeta = rentaBrutaAnual.subtract(DEDUCCION_UIT.multiply(valorUit));
BigDecimal impuestoAnual = impuestoSobre(rentaNeta.max(BigDecimal.ZERO), valorUit);
List<TramoCalculado> tramos = new ArrayList<>();
BigDecimal impuestoAnual = impuestoSobre(rentaNeta.max(BigDecimal.ZERO), valorUit, tramos);
BigDecimal saldo = impuestoAnual.subtract(orZero(datos.retenidoAnterior()));
BigDecimal divisorMes = divisorDelMes(datos.mesCorte());
BigDecimal retencionDelMes = saldo.max(BigDecimal.ZERO)
.divide(divisorDelMes(datos.mesCorte()), ESCALA, RoundingMode.HALF_UP);
.divide(divisorMes, ESCALA, RoundingMode.HALF_UP);
return new CalculoRentaQuinta(
escala(proyeccionFutura), escala(rentaBrutaAnual), escala(impuestoAnual), retencionDelMes);
escala(proyeccionFutura),
escala(proyeccionSueldoBruto),
escala(proyeccionGratificaciones),
escala(rentaBrutaAnual),
escala(rentaNeta),
escala(impuestoAnual),
retencionDelMes,
valorUit,
divisorMes,
tramos);
}
/**
* Los meses posteriores al de corte se proyectan con la remuneración del mes de corte, más las
* gratificaciones de julio y diciembre que todavía no se han percibido.
*/
private BigDecimal proyectarMesesFuturos(DatosCalculo datos) {
int mesesRestantes = 12 - datos.mesCorte();
BigDecimal proyeccion = orZero(datos.remuneracionMesCorte())
.multiply(BigDecimal.valueOf(mesesRestantes));
/** Gratificaciones de julio y/o diciembre que aún no se han percibido al mes de corte. */
private BigDecimal gratificacionesPendientes(DatosCalculo datos) {
BigDecimal gratificacion = orZero(datos.basico())
.add(orZero(datos.asignacionFamiliar()))
.multiply(FACTOR_GRATIFICACION);
BigDecimal total = BigDecimal.ZERO;
if (datos.mesCorte() < 7) {
proyeccion = proyeccion.add(gratificacion);
total = total.add(gratificacion);
}
if (datos.mesCorte() < 12) {
proyeccion = proyeccion.add(gratificacion);
total = total.add(gratificacion);
}
return proyeccion;
return total;
}
private BigDecimal impuestoSobre(BigDecimal rentaNeta, BigDecimal valorUit) {
private BigDecimal impuestoSobre(BigDecimal rentaNeta, BigDecimal valorUit, List<TramoCalculado> tramos) {
BigDecimal impuesto = BigDecimal.ZERO;
BigDecimal pisoTramo = BigDecimal.ZERO;
for (Tramo tramo : TRAMOS) {
if (tramo.limiteUit() == null) {
impuesto = impuesto.add(rentaNeta.subtract(pisoTramo).max(BigDecimal.ZERO)
.multiply(tramo.tasa()));
BigDecimal gravado = rentaNeta.subtract(pisoTramo).max(BigDecimal.ZERO);
BigDecimal impuestoTramo = gravado.multiply(tramo.tasa());
impuesto = impuesto.add(impuestoTramo);
tramos.add(new TramoCalculado(tramo.descripcion(), escala(gravado), tramo.tasa(), escala(impuestoTramo)));
break;
}
BigDecimal techoTramo = tramo.limiteUit().multiply(valorUit);
BigDecimal gravadoEnTramo = rentaNeta.min(techoTramo).subtract(pisoTramo).max(BigDecimal.ZERO);
impuesto = impuesto.add(gravadoEnTramo.multiply(tramo.tasa()));
BigDecimal impuestoTramo = gravadoEnTramo.multiply(tramo.tasa());
impuesto = impuesto.add(impuestoTramo);
tramos.add(new TramoCalculado(tramo.descripcion(), escala(gravadoEnTramo), tramo.tasa(), escala(impuestoTramo)));
if (rentaNeta.compareTo(techoTramo) <= 0) {
break;
}
......@@ -122,7 +137,7 @@ public class RentaQuintaCalculadora {
return v == null ? BigDecimal.ZERO : v;
}
private record Tramo(BigDecimal limiteUit, BigDecimal tasa) {}
private record Tramo(String descripcion, BigDecimal limiteUit, BigDecimal tasa) {}
/** Insumos del cálculo, tomados del acumulado anual del trabajador. */
public record DatosCalculo(
......
package so.planilla_be.rentaquinta.calculo;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import so.planilla_be.rentaquinta.entity.ParametroUit;
import so.planilla_be.rentaquinta.repository.ParametroUitRepository;
import java.math.BigDecimal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Valor de la UIT por año, cargado desde {@code planilla.parametro_uit} al arrancar la
* aplicación y mantenido en memoria (bean singleton) para no consultar la base en cada cálculo.
*/
@Component
public class UitCache {
private final ParametroUitRepository repository;
private final Map<Integer, BigDecimal> valores = new ConcurrentHashMap<>();
public UitCache(ParametroUitRepository repository) {
this.repository = repository;
}
@EventListener(ApplicationReadyEvent.class)
public void cargar() {
valores.clear();
for (ParametroUit p : repository.findAll()) {
valores.put(p.getAnio(), p.getMontoUit());
}
}
/**
* Valor de la UIT para el año dado. Si no hay un valor cargado para ese año exacto, se usa
* el del año más reciente disponible en la tabla.
*/
public BigDecimal valorPara(Integer anio) {
if (anio != null && valores.containsKey(anio)) {
return valores.get(anio);
}
return valores.keySet().stream()
.max(Integer::compareTo)
.map(valores::get)
.orElseThrow(() -> new IllegalStateException(
"No hay valores de UIT cargados en planilla.parametro_uit"));
}
}
package so.planilla_be.rentaquinta.calculo;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.math.BigDecimal;
import java.util.Map;
/**
* Valor de la UIT por periodo. Se configura en {@code renta-quinta.uit.valores}
* para no tener que recompilar cuando SUNAT publica el valor del año siguiente.
*/
@ConfigurationProperties(prefix = "renta-quinta.uit")
public record UitProperties(Map<Integer, BigDecimal> valores, BigDecimal porDefecto) {
public BigDecimal valorPara(Integer periodo) {
if (valores != null && periodo != null && valores.containsKey(periodo)) {
return valores.get(periodo);
}
return porDefecto;
}
}
......@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaFiltroDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaHistorialDTO;
......@@ -56,6 +57,17 @@ public class RentaQuintaController {
return service.obtenerPorId(id);
}
@Operation(summary = "Detalle completo del cálculo por número de documento + año + mes "
+ "(boleta del mes + puntos 1 a 5: historial de ingresos, parámetros SUNAT, "
+ "liquidación anual, escala por tramos y control de retenciones)")
@GetMapping("/detalle-completo")
public RentaQuintaDetalleCompletoDTO obtenerDetalleCompleto(
@RequestParam String numeroDocumento,
@RequestParam Integer anio,
@RequestParam @Min(1) @Max(12) Integer mes) {
return service.obtenerDetalleCompleto(numeroDocumento, anio, mes);
}
@Operation(summary = "Detalle por codper + anio + mes")
@GetMapping("/boleta")
public RentaQuintaDetalleDTO obtenerBoleta(
......
package so.planilla_be.rentaquinta.dto;
import java.math.BigDecimal;
import java.util.List;
/**
* Detalle completo de renta de quinta de un trabajador en un mes (puntos 1 a 5 del Excel de
* referencia "Ver Historial - Cálculo"), en un solo JSON: la boleta del mes, historial de
* ingresos, parámetros SUNAT, liquidación del impuesto anual proyectado, escala por tramos y
* control de retenciones.
*/
public record RentaQuintaDetalleCompletoDTO(
RentaQuintaDetalleDTO boleta,
RegistroHistorialIngresosDTO registroHistorialIngresos,
ParametrosSistemaDTO parametrosSistema,
LiquidacionImpuestoAnualDTO liquidacionImpuestoAnual,
EscalaImpositivaDTO escalaImpositiva,
ControlRetencionesDTO controlRetenciones
) {
/** Punto 1: registro e historial de ingresos consolidados, en 3 columnas de periodo. */
public record RegistroHistorialIngresosDTO(
BloqueIngresosDTO pasadoRealRealizado,
BloqueIngresosDTO planillaEnCurso,
BloqueIngresosDTO proyeccionAutomatizada,
BigDecimal ingresosAnualesTotales
) {}
public record BloqueIngresosDTO(
BigDecimal sueldoBruto,
BigDecimal horasExtras,
BigDecimal gratificacionesOrdinarias,
BigDecimal utilidadesDistribuidas,
BigDecimal otrosEmpleadores,
BigDecimal totalBrutoRemunerativo
) {}
/** Punto 2: parámetros del sistema (SUNAT) vigentes para el periodo. */
public record ParametrosSistemaDTO(
BigDecimal valorUit,
BigDecimal deduccionLey7Uit,
BigDecimal montoTopeTramo1_5Uit,
BigDecimal factorDivisorMes
) {}
/** Punto 3: liquidación del impuesto anual proyectado. */
public record LiquidacionImpuestoAnualDTO(
BigDecimal rentaBrutaAnual,
BigDecimal otrosEmpleadores,
BigDecimal deduccionLey7Uit,
BigDecimal rentaNetaImponible,
BigDecimal impuestoAnualCalculado
) {}
/** Punto 4: aplicación de la escala impositiva por tramos. */
public record EscalaImpositivaDTO(
List<TramoDTO> tramos,
BigDecimal totalImpuestoAnualDeterminado
) {}
public record TramoDTO(
String tramo,
BigDecimal baseImponible,
BigDecimal tasa,
BigDecimal impuesto
) {}
/** Punto 5: control de retenciones efectuadas y cálculo de la cuota del mes. */
public record ControlRetencionesDTO(
BigDecimal impuestoAnualReferencia,
BigDecimal retencionesAcumuladasAnteriores,
BigDecimal saldoPendienteRetencion,
BigDecimal factorDivisorMes,
BigDecimal retencionMesActual
) {}
}
package so.planilla_be.rentaquinta.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
@Entity
@Table(name = "parametro_uit")
public class ParametroUit {
@Id
@Column(name = "anio")
private Integer anio;
@Column(name = "monto_uit")
private BigDecimal montoUit;
}
......@@ -4,11 +4,21 @@ import org.springframework.stereotype.Component;
import so.planilla_be.rentaquinta.calculo.CalculoRentaQuinta;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaHistorialDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.BloqueIngresosDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.ControlRetencionesDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.EscalaImpositivaDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.LiquidacionImpuestoAnualDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.ParametrosSistemaDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.RegistroHistorialIngresosDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.TramoDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaResumenDTO;
import so.planilla_be.rentaquinta.entity.RentaQuinta;
import so.planilla_be.rentaquinta.repository.RentaQuintaResumenProjection;
import java.math.BigDecimal;
import java.util.List;
import java.util.function.Predicate;
@Component
public class RentaQuintaMapper {
......@@ -62,6 +72,78 @@ public class RentaQuintaMapper {
e.getDiasNoLaborados());
}
/**
* Imagen detallada de la boleta (puntos 1 a 5 del Excel "Ver Historial - Cálculo"): historial
* de ingresos por columna de periodo, parámetros SUNAT, liquidación anual, escala por tramos
* y control de retenciones.
*/
public RentaQuintaDetalleCompletoDTO toDetalleCompleto(RentaQuinta boleta, List<RentaQuinta> historialAnio,
int mesCorte, CalculoRentaQuinta calculo) {
BloqueIngresosDTO pasado = bloque(historialAnio, r -> r.getBoletaMes() < mesCorte);
BloqueIngresosDTO actual = bloque(historialAnio, r -> r.getBoletaMes().equals(mesCorte));
BloqueIngresosDTO proyeccion = new BloqueIngresosDTO(
calculo.proyeccionSueldoBruto(),
BigDecimal.ZERO,
calculo.proyeccionGratificaciones(),
BigDecimal.ZERO,
BigDecimal.ZERO,
calculo.proyeccionFutura());
BigDecimal ingresosAnuales = pasado.totalBrutoRemunerativo()
.add(actual.totalBrutoRemunerativo())
.add(proyeccion.totalBrutoRemunerativo());
RegistroHistorialIngresosDTO registro = new RegistroHistorialIngresosDTO(
pasado, actual, proyeccion, ingresosAnuales);
BigDecimal deduccionLey = calculo.valorUit().multiply(BigDecimal.valueOf(7));
ParametrosSistemaDTO parametros = new ParametrosSistemaDTO(
calculo.valorUit(),
deduccionLey,
calculo.valorUit().multiply(BigDecimal.valueOf(5)),
calculo.divisorMes());
LiquidacionImpuestoAnualDTO liquidacion = new LiquidacionImpuestoAnualDTO(
calculo.rentaBrutaAnual(),
BigDecimal.ZERO,
deduccionLey,
calculo.rentaNeta(),
calculo.impuestoAnual());
List<TramoDTO> tramos = calculo.tramos().stream()
.map(t -> new TramoDTO(t.descripcion(), t.baseImponible(), t.tasa(), t.impuesto()))
.toList();
EscalaImpositivaDTO escala = new EscalaImpositivaDTO(tramos, calculo.impuestoAnual());
BigDecimal retenidoAnterior = historialAnio.stream()
.filter(r -> r.getBoletaMes() < mesCorte)
.map(r -> orZero(r.getRenta5taCategoria()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal saldoPendiente = calculo.impuestoAnual().subtract(retenidoAnterior).max(BigDecimal.ZERO);
ControlRetencionesDTO control = new ControlRetencionesDTO(
calculo.impuestoAnual(), retenidoAnterior, saldoPendiente,
calculo.divisorMes(), calculo.retencionDelMes());
return new RentaQuintaDetalleCompletoDTO(toDetalle(boleta), registro, parametros, liquidacion, escala, control);
}
private BloqueIngresosDTO bloque(List<RentaQuinta> historial, Predicate<RentaQuinta> filtro) {
BigDecimal sueldoBruto = BigDecimal.ZERO;
BigDecimal horasExtras = BigDecimal.ZERO;
BigDecimal gratificaciones = BigDecimal.ZERO;
for (RentaQuinta r : historial) {
if (!filtro.test(r)) {
continue;
}
sueldoBruto = sueldoBruto.add(orZero(r.getSueldoBruto()));
horasExtras = horasExtras.add(orZero(r.getHorasExtras25())).add(orZero(r.getHorasExtras35()));
gratificaciones = gratificaciones
.add(orZero(r.getGratificacionProporcional()))
.add(orZero(r.getGratificacionJulioDiciembre()));
}
BigDecimal total = sueldoBruto.add(horasExtras).add(gratificaciones);
return new BloqueIngresosDTO(sueldoBruto, horasExtras, gratificaciones, BigDecimal.ZERO, BigDecimal.ZERO, total);
}
private BigDecimal orZero(BigDecimal v) {
return v == null ? BigDecimal.ZERO : v;
}
......
package so.planilla_be.rentaquinta.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import so.planilla_be.rentaquinta.entity.ParametroUit;
public interface ParametroUitRepository extends JpaRepository<ParametroUit, Integer> {
}
......@@ -59,6 +59,9 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
Optional<RentaQuinta> findByCodperAndBoletaAnioAndBoletaMes(
Integer codper, Integer boletaAnio, Integer boletaMes);
Optional<RentaQuinta> findByNumeroDocumentoAndBoletaAnioAndBoletaMes(
String numeroDocumento, Integer boletaAnio, Integer boletaMes);
/**
* Agregado anual por trabajador hasta el mes de corte: acumulado de meses previos
* (historial real / retenido) y valores del propio mes de corte (planilla cargada /
......@@ -110,4 +113,13 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
List<RentaQuinta> historialAnual(
@Param("codper") Integer codper,
@Param("anio") Integer anio);
@Query("""
select r from RentaQuinta r
where r.numeroDocumento = :numeroDocumento and r.boletaAnio = :anio
order by r.boletaMes asc
""")
List<RentaQuinta> historialAnualPorDocumento(
@Param("numeroDocumento") String numeroDocumento,
@Param("anio") Integer anio);
}
package so.planilla_be.rentaquinta.service;
import org.springframework.data.domain.Page;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaFiltroDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaHistorialDTO;
......@@ -14,6 +15,8 @@ public interface RentaQuintaService {
RentaQuintaDetalleDTO obtenerPorId(Long id);
RentaQuintaDetalleCompletoDTO obtenerDetalleCompleto(String numeroDocumento, Integer anio, Integer mes);
RentaQuintaDetalleDTO obtenerBoleta(Integer codper, Integer anio, Integer mes);
List<RentaQuintaHistorialDTO> historialAnual(Integer codper, Integer anio);
......
......@@ -8,16 +8,19 @@ import org.springframework.transaction.annotation.Transactional;
import so.planilla_be.rentaquinta.calculo.CalculoRentaQuinta;
import so.planilla_be.rentaquinta.calculo.RentaQuintaCalculadora;
import so.planilla_be.rentaquinta.calculo.RentaQuintaCalculadora.DatosCalculo;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaFiltroDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaHistorialDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaResumenDTO;
import so.planilla_be.rentaquinta.entity.RentaQuinta;
import so.planilla_be.rentaquinta.exception.RentaQuintaNoEncontradaException;
import so.planilla_be.rentaquinta.mapper.RentaQuintaMapper;
import so.planilla_be.rentaquinta.repository.RentaQuintaRepository;
import so.planilla_be.rentaquinta.repository.RentaQuintaResumenProjection;
import so.planilla_be.rentaquinta.service.RentaQuintaService;
import java.math.BigDecimal;
import java.util.List;
@Service
......@@ -100,6 +103,64 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
}
@Override
public RentaQuintaDetalleCompletoDTO obtenerDetalleCompleto(String numeroDocumento, Integer anio, Integer mes) {
RentaQuinta boleta = repository.findByNumeroDocumentoAndBoletaAnioAndBoletaMes(numeroDocumento, anio, mes)
.orElseThrow(() -> new RentaQuintaNoEncontradaException(
"No existe boleta para documento %s, periodo %d-%02d".formatted(numeroDocumento, anio, mes)));
List<RentaQuinta> historialAnio = repository.historialAnualPorDocumento(numeroDocumento, anio);
int mesCorte = boleta.getBoletaMes();
BigDecimal historialReal = sumaConceptos(historialAnio, r -> r.getBoletaMes() < mesCorte);
BigDecimal planillaCargada = sumaConceptos(historialAnio, r -> r.getBoletaMes().equals(mesCorte));
BigDecimal retenidoAnterior = historialAnio.stream()
.filter(r -> r.getBoletaMes() < mesCorte)
.map(r -> orZero(r.getRenta5taCategoria()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
CalculoRentaQuinta calculo = calculadora.calcular(new DatosCalculo(
boleta.getBoletaAnio(), mesCorte, historialReal, planillaCargada,
boleta.getSueldoBruto(), boleta.getBasico(), boleta.getAsignacionFamiliar(), retenidoAnterior));
return mapper.toDetalleCompleto(boleta, historialAnio, mesCorte, calculo);
}
/**
* Total bruto remunerativo de un mes: mismo criterio que {@code RentaQuintaRepository.CONCEPTOS_MES},
* aplicado en memoria sobre el historial anual ya cargado del trabajador.
*/
private BigDecimal sumaConceptos(List<RentaQuinta> historial, java.util.function.Predicate<RentaQuinta> filtro) {
return historial.stream()
.filter(filtro)
.map(r -> orZero(r.getBasico())
.add(orZero(r.getAsignacionFamiliar()))
.add(orZero(r.getMovilidad()))
.add(orZero(r.getHorasExtras25()))
.add(orZero(r.getHorasExtras35()))
.add(orZero(r.getApoyoSalud()))
.add(orZero(r.getAsignacionEstudios()))
.add(orZero(r.getBonoDesempeno()))
.add(orZero(r.getDominical()))
.add(orZero(r.getCanastaNavidad()))
.add(orZero(r.getValeConsumo()))
.add(orZero(r.getObsequios()))
.add(orZero(r.getBonoExtraordinario()))
.add(orZero(r.getBonoProductividad()))
.add(orZero(r.getBonoAniversario()))
.add(orZero(r.getReintegro()))
.add(orZero(r.getSumaGraciosa()))
.add(orZero(r.getVacacionesTruncas()))
.add(orZero(r.getVacacionesGozadasRemVaca()))
.add(orZero(r.getGratificacionProporcional()))
.add(orZero(r.getBonificacionExtraordinaria()))
.add(orZero(r.getGratificacionJulioDiciembre())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private BigDecimal orZero(BigDecimal v) {
return v == null ? BigDecimal.ZERO : v;
}
@Override
public List<RentaQuintaHistorialDTO> historialAnual(Integer codper, Integer anio) {
List<RentaQuintaHistorialDTO> historial = repository.historialAnual(codper, anio).stream()
.map(mapper::toHistorial)
......
......@@ -25,13 +25,3 @@ server:
springdoc:
swagger-ui:
path: /docs
renta-quinta:
uit:
# Valor de la UIT por periodo (SUNAT). Actualizar cada año.
valores:
2023: 4950
2024: 5150
2025: 5350
# 2026: pendiente de confirmar el valor publicado por SUNAT
por-defecto: 5350
package so.planilla_be.rentaquinta.calculo;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import so.planilla_be.rentaquinta.calculo.RentaQuintaCalculadora.DatosCalculo;
import java.math.BigDecimal;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
......@@ -12,8 +12,13 @@ class RentaQuintaCalculadoraTest {
private static final BigDecimal UIT_2024 = new BigDecimal("5150");
private final RentaQuintaCalculadora calculadora =
new RentaQuintaCalculadora(new UitProperties(Map.of(2024, UIT_2024), UIT_2024));
private final UitCache uitCache = Mockito.mock(UitCache.class);
private final RentaQuintaCalculadora calculadora;
{
Mockito.when(uitCache.valorPara(2024)).thenReturn(UIT_2024);
calculadora = new RentaQuintaCalculadora(uitCache);
}
private DatosCalculo enDiciembre(String historial, String mesActual, String retenidoAnterior) {
return new DatosCalculo(2024, 12, new BigDecimal(historial), new BigDecimal(mesActual),
......
......@@ -120,3 +120,9 @@ create table renta_quinta
create index if not exists idx_renta_quinta_codper_anio_mes
on renta_quinta (codper, boleta_anio, boleta_mes);
create table parametro_uit
(
anio integer primary key,
monto_uit numeric(10, 2) not null
);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment