Commit 68910855 by Vanesa Puma Yamo

se agregamo modifiaciones

parent 4bf1eb8a
......@@ -68,9 +68,26 @@ internamente `RentaQuintaFiltroDTO` y `DatosCalculo` sigan llamándolos así.
`anio` y `mes` son **obligatorios**; el resto es opcional y, si no se envía, no filtra. Todos son
`Integer` salvo `documentoOApellidos`, que es texto (busca por `numero_documento` o por apellidos
+ nombres, con `LIKE` en mayúsculas).
Los filtros de catálogo (`sede`, `tipoPlanilla`, `areaGeneral`, `subArea`, `cargo`) llegan como
+ nombres, con `LIKE` en mayúsculas), y `tipoPlanilla`.
`tipoPlanilla` acepta **solo `ADM`, `DOC1`, `DOC2`** (`DOC` sin dígito responde 400). Lo reciben `listado`, `detallado`, `grabar` y `grabar-masivo`. Las letras son
`boleta_tipo_personal` (enum `calculo/TipoPlanilla`) y el dígito es `pl_planilla.tipo_pago`
(`char(1)`, enum `calculo/TipoPago`); filtra por ambas columnas y `calculo/ModalidadCalculo`
decide la proyección. Si no se envía, se toma de la boleta. En `pl_renta_quinta` se graba
`boleta_tipo_personal` (`ADM`/`DOC`) y el dígito en `tipo_pago` (`1`/`2`, vacío en ADM; es parte de la
PK). Esa columna la crea `docs/alter-pl-renta-quinta-tipo-pago.sql`: correrlo antes de desplegar o
`ddl-auto: validate` no deja arrancar.
| tipoPlanilla | boleta_tipo_personal | tipo_pago | Modalidad | Proyección del sueldo bruto |
|---|---|---|---|---|
| `ADM` | `ADM` | — | `MONTO_FIJO` | remuneración de referencia × meses restantes |
| `DOC2` | `DOC` | `2` | `MONTO_FIJO` | igual que ADM |
| `DOC1` | `DOC` | `1` | `DOCENTE_POR_HORA` | `remun_bruta_futura` de `planilla.pl_proyeccion_renta_docentehora` del mes consultado (mismo documento, `boleta_tipo_personal='DOC'`, `proyeccion_anio` = año, `proyeccion_mes` = mes). Sin fila = 0 |
Solo cambia la proyección del sueldo; gratificaciones, UIT, tramos y divisor son iguales. Detalle
en `docs/circuito-api-renta-quinta.md`.
Los filtros de catálogo (`sede`, `areaGeneral`, `subArea`, `cargo`) llegan como
número y se comparan **como texto** contra columnas TEXT: el service los convierte con
`codigo()`. Las columnas `char(n)` se comparan con `trim()` por el padding de PostgreSQL.
......
-- tipo_pago en planilla.pl_renta_quinta, tal como quedó en la base de pruebas (prueba):
-- boleta_tipo_personal = 'ADM', tipo_pago = ' ' (vacío) -> administrativo
-- boleta_tipo_personal = 'DOC', tipo_pago = '1' -> DOC1, docente costo por hora
-- boleta_tipo_personal = 'DOC', tipo_pago = '2' -> DOC2, docente monto fijo
--
-- tipo_pago es char(1), NOT NULL y parte de la PK: en ADM se graba vacío, nunca NULL.
-- Ejecutar ANTES de desplegar el backend en otro entorno: con ddl-auto: validate, la app no
-- arranca si falta la columna.
ALTER TABLE planilla.pl_renta_quinta
ADD COLUMN tipo_pago char(1) NOT NULL DEFAULT '';
ALTER TABLE planilla.pl_renta_quinta
DROP CONSTRAINT pl_renta_quinta_pkey,
ADD PRIMARY KEY (numero_documento, anio, mes, boleta_tipo_personal, tipo_pago);
COMMENT ON COLUMN planilla.pl_renta_quinta.tipo_pago IS
'Solo DOC: 1 = docente costo por hora (DOC1), 2 = docente monto fijo (DOC2). Vacío en ADM.';
# Circuito del API de renta de quinta, con tipo de planilla
Este documento sigue el recorrido de una petición **archivo por archivo y función por función**:
desde que el cliente (C#) llama al API hasta que recibe el JSON. También marca en qué puntos el
código cambia según el **tipo de planilla**.
> Todas las rutas empiezan en `src/main/java/so/planilla_be/`. El formato `archivo:línea` lleva
> directo a la función.
---
## 1. Tipos de planilla
El tipo de planilla sale de la columna **`boleta_tipo_personal`** de `planilla.pl_planilla`.
| Código en BD | Constante en Java | Planillas que cubre |
|---|---|---|
| `ADM` | `TipoPlanilla.MENSUAL_MONTO_FIJO` | Planilla mensual (monto fijo) |
| `DOC` | `TipoPlanilla.DOCENTE` | Docentes: monto fijo o por hora, según `tipo_pago` |
📄 Definición: `rentaquinta/calculo/TipoPlanilla.java`
El tipo de planilla filtra la grilla y es parte de la clave de `pl_renta_quinta`. La regla del
cálculo la decide **`ModalidadCalculo`** (📄 `rentaquinta/calculo/ModalidadCalculo.java`), que
combina el tipo con la columna **`tipo_pago`** (`char(1)`) de `pl_planilla`:
| boleta_tipo_personal | tipo_pago | `ModalidadCalculo` | Proyección del sueldo bruto |
|---|---|---|---|
| `ADM` | (vacío) | `MONTO_FIJO` | Remuneración de referencia × meses restantes |
| `DOC` | `2` | `MONTO_FIJO` | Igual que ADM |
| `DOC` | `1` | `DOCENTE_POR_HORA` | `remun_bruta_futura` de `planilla.pl_proyeccion_renta_docentehora` del mes consultado |
En DOC, el `tipo_pago` se lee de las boletas DOC del mes de referencia; vacío o desconocido = `MONTO_FIJO`.
| Función / variable | Para qué sirve |
|---|---|
| `MENSUAL_MONTO_FIJO` / `DOCENTE` | Tipos de planilla. En el código: `switch (tipoPlanilla) { case MENSUAL_MONTO_FIJO -> …; case DOCENTE -> …; }` |
| `getCodigo()` | Devuelve `"ADM"` o `"DOC"` |
| `esMensualOMontoFijo()` / `esDocente()` | Para escribir condiciones del tipo `if (tipo.esDocente())` |
| `TipoPago.desdeParametro("DOC1")` | Extrae el dígito de `DOC1`/`DOC2` → `POR_HORA` / `MONTO_FIJO` |
| `ModalidadCalculo.de(tipo, tipoPago)` | Devuelve `MONTO_FIJO` o `DOCENTE_POR_HORA` |
| `desdeParametro(String)` `:56` | Convierte el parámetro que manda C# (`"ADM"`, `"doc"`, `" DOC "`…) en el enum. Vacío = no se envió. Un código desconocido responde **400** |
| `deBoleta(RentaQuinta)` `:74` | Lee el tipo desde la boleta (hace `trim` porque la columna es `char(3)`). Si viene vacío o desconocido, usa `MENSUAL_MONTO_FIJO` |
¿Por qué `switch` y no `if`? Con `switch` sobre un enum, **Java no compila si falta un caso**.
Si mañana se agrega un tercer tipo de planilla, el compilador obliga a decidir qué hace en cada
punto condicionado, y así ninguno se olvida.
---
## 2. Cómo manda C# el tipo de planilla
Es el parámetro de query **`tipoPlanilla`** (opcional en listado/detallado, obligatorio al grabar):
| Valor | Qué calcula | Filtra por |
|---|---|---|
| `ADM` | Administrativo, cálculo normal | `boleta_tipo_personal='ADM'` |
| `DOC1` | Docente costo por hora: proyección = `remun_bruta_futura` del mes consultado | `'DOC'` + `tipo_pago='1'` |
| `DOC2` | Docente monto fijo: igual que ADM | `'DOC'` + `tipo_pago='2'` |
Las letras son `boleta_tipo_personal` (`TipoPlanilla`) y el dígito es `tipo_pago` (`TipoPago`).
No distingue mayúsculas (`doc1` vale). `DOC` sin dígito responde 400. En `pl_renta_quinta` se graba `boleta_tipo_personal` (`ADM`/`DOC`) y `tipo_pago` (`1`/`2`, vacío en ADM).
| Endpoint | Ejemplo |
|---|---|
| `GET /api/v1/renta-quinta/listado` | `?anio=2026&mes=8&tipoPlanilla=ADM` |
| `GET /api/v1/renta-quinta/detallado` | `?numeroDocumento=12345678&anio=2026&mes=8&tipoPlanilla=DOC1` |
| `POST /api/v1/renta-quinta/grabar` | `?numeroDocumento=12345678&anio=2026&mes=8&tipoPlanilla=DOC2` |
| `POST /api/v1/renta-quinta/grabar-masivo` | `?anio=2026&mes=8&tipoPlanilla=DOC1` |
**Regla de resolución** (función `tipoPlanilla(...)` en `RentaQuintaServiceImpl.java:151`):
```
¿C# envió tipoPlanilla?
├─ SÍ → se usa ese tipo (y en listado / grabar-masivo además FILTRA a los trabajadores de ese tipo)
└─ NO → se usa el boleta_tipo_personal de la boleta de referencia del trabajador
```
Si llega un valor inválido, por ejemplo `tipoPlanilla=XYZ`, la respuesta es:
```json
{ "timestamp": "...", "status": 400,
"message": "tipoPlanilla 'XYZ' no válido. Valores permitidos: ADM, DOC1, DOC2" }
```
---
## 3. Mapa de los puntos condicionados
| # | Punto | Archivo:línea | Qué decide | ADM | DOC |
|---|---|---|---|---|---|
| **C1** | Lectura del parámetro | `controller/RentaQuintaController.java:59, 78, 106, 123` | Convierte `"ADM"/"DOC"` en `TipoPlanilla` | ✔ | ✔ |
| **C2** | Filtro de la grilla | `repository/RentaQuintaRepository.java:21` (lo invoca `service/impl/RentaQuintaServiceImpl.java:90`) | Solo trae trabajadores con `trim(boleta_tipo_personal) = 'ADM'/'DOC'` | ✔ | ✔ |
| **C3** | Modalidad del trabajador | `service/impl/RentaQuintaServiceImpl.java:367` `modalidad()` | `ModalidadCalculo.de(tipo, tipo_pago)` | `MONTO_FIJO` | tipo_pago 2 → `MONTO_FIJO`; 1 → `DOCENTE_POR_HORA` (y lee `pl_proyeccion_renta_docentehora` en `calcular()` `:332`) |
| **C4** | Proyección del sueldo | `calculo/RentaQuintaCalculadora.java:160` `proyeccionSueldoBruto()` | Meses posteriores al de corte | Remuneración de referencia × meses que faltan | Tipo 2: igual que ADM. Tipo 1: `remun_bruta_futura` del mes consultado |
La base de la gratificación (`baseGratificacion()` `:451`) y su monto (`gratificacion()` `:173`,
base × 1.09) son **iguales** para ADM y DOC. Para encontrar los puntos, busca
`CONDICIÓN MODALIDAD` en el código.
### Qué NO se condiciona, y por qué
| Regla | Dónde | Motivo |
|---|---|---|
| UIT del año | `UitCache.valorPara` | Parámetro SUNAT, igual para todos |
| Deducción de 7 UIT | `RentaQuintaCalculadora.java:94` | Ley (art. 46 LIR) |
| Tramos 8/14/17/20/30 % | `impuestoSobre()` `:179` | Ley (art. 53 LIR) |
| Divisor del mes | `divisorDelMes()` `:221` | Reglamento (art. 40) |
| Retenciones previas y otros empleadores | `calcular()` `:101`, `otrosEmpleadores()` | Se descuentan igual para todo trabajador |
| Conceptos que suman como ingreso | `mapper/RentaQuintaMapper.java:177` `bloque()` | Salen de las mismas columnas de `pl_planilla` en ambos tipos. **Si el docente por hora necesita otros conceptos (p. ej. `horas_adicionales`), este sería el sexto punto a condicionar** |
---
## 4. Circuito detallado: `GET /listado`
Ejemplo: C# llama a `GET /api/v1/renta-quinta/listado?anio=2026&mes=8&tipoPlanilla=ADM`
### Paso 1 · Controller: recibe la petición
📄 `rentaquinta/controller/RentaQuintaController.java:47`, función `listado(...)`
1. Spring valida `anio` (obligatorio) y `mes` (1..12). Si falla → `exception/GlobalExceptionHandler.java` → 400.
2. **[C1]** `TipoPlanilla.desdeParametro("ADM")` → `MENSUAL_MONTO_FIJO`.
3. Arma el `RentaQuintaFiltroDTO` (📄 `dto/RentaQuintaFiltroDTO.java`), que ahora trae `TipoPlanilla tipoPlanilla`.
4. Llama a `service.buscar(filtro)`.
> `service` es la interfaz `service/RentaQuintaService.java`. Spring inyecta la clase real,
> `service/impl/RentaQuintaServiceImpl.java`.
### Paso 2 · Service: busca a los trabajadores
📄 `service/impl/RentaQuintaServiceImpl.java:84`, función `buscar(filtro)`
| Orden | Llamada | Archivo:línea | Qué hace |
|---|---|---|---|
| 2.1 | `codigo(filtro.tipoPlanilla())` | ServiceImpl `:143` | `MENSUAL_MONTO_FIJO` → `"ADM"` |
| 2.2 | `patron()`, `toPageable()` | ServiceImpl | Texto → `%TEXTO%`; página y tamaño (máx. 100) |
| 2.3 | **[C2]** `repository.buscarDocumentos(...)` | Repository `:60` (filtro en `:21`) | SQL: DNI + nombres de `pl_planilla` con `boleta_anio = 2026`, `boleta_mes <= 8`, `trim(boleta_tipo_personal) = 'ADM'` y el resto de filtros. Paginado |
| 2.4 | `repository.historialHastaMes(...)` | Repository `:79` | SQL: todas las boletas de esos DNI, de enero al mes 8 |
| 2.5 | `aplicarBasicoYUtilidades(...)` | ServiceImpl `:381` | Lee `pl_renta_basico` y `pl_utilidades`. Por cada boleta llama a **[C3]** `baseGratificacion()` `:407`, que según el `boleta_tipo_personal` **de esa boleta** fija `basicoGratificacion` |
| 2.6 | `obtenerRetencionesMesAnterior(...)` | ServiceImpl `:417` | Retención grabada del mes 7 en `pl_renta_quinta` |
### Paso 3 · Service: por cada trabajador de la página
Dentro de `documentos.map(...)`, en `RentaQuintaServiceImpl.java:109-134`:
| Orden | Llamada | Archivo:línea | Qué hace |
|---|---|---|---|
| 3.1 | `agregar(historial, mes)` | ServiceImpl `:291` | Boleta de referencia, `historialReal` (meses < 8), `planillaCargadaReal` (mes 8), `retenidoAnterior`, ¿ya cobró julio/diciembre? Para sumar los ingresos usa `mapper.totalBrutoMes()` → `bloque()` (Mapper `:173`, `:177`) |
| 3.2 | `tipoPlanilla(filtro.tipoPlanilla(), a.referencia())` | ServiceImpl `:112` → `:151` | **Decide el tipo del trabajador**: el enviado por C#, o el de su boleta |
| 3.3 | `calcular(a, historial, anio, mes, tipoPlanilla)` | ServiceImpl `:314` | Suma sueldo, `basicoGratificacion` y asignación del mes de referencia, y llama a `otrosEmpleadores()` `:342` (1 SQL por trabajador). Arma `DatosCalculo` **con el `tipoPlanilla`** y llama a la calculadora |
### Paso 4 · Calculadora: impuesto y retención
📄 `calculo/RentaQuintaCalculadora.java:46`, función `calcular(datos)`
```
calcular(datos) datos.tipoPlanilla() = MENSUAL_MONTO_FIJO
│
├─ UIT del año ............................ UitCache.valorPara(2026)
│
├─ ¿El mes 8 tiene boleta real?
│ NO → estimado = remuneración de referencia
│ si el mes es 7 o 12 → + gratificacion(estimado) = × 1.09 :173
│
├─ [C4] proyeccionSueldoBruto(datos) .................................... :160
│ switch (datos.modalidad())
│ MONTO_FIJO → remuneración × (12 − 8) (ADM y DOC tipo_pago 2)
│ DOCENTE_POR_HORA → remun_bruta_futura del mes 8 (DOC tipo_pago 1)
│
├─ gratificacionesPendientes(...) ....................................... :140
│ por cada julio / diciembre no cobrado:
│ gratificacion(basicoReferencia) = base × 1.09 (igual para todos)
│
├─ Renta bruta anual = historial + mes actual + proyección (+ otros empleadores)
├─ Renta neta = bruta − 7 UIT ................................... :94 (ley, no condicionado)
├─ Impuesto anual = impuestoSobre() tramos ........................... :179 (ley, no condicionado)
├─ Saldo = impuesto − retenido − otros empleadores ......... :101
└─ Retención del mes = saldo ÷ divisorDelMes(8) ......................... :221 (ley, no condicionado)
→ CalculoRentaQuinta (calculo/CalculoRentaQuinta.java)
```
### Paso 5 · Mapper: arma la fila
📄 `mapper/RentaQuintaMapper.java:25`, función `toResumen(...)` → `RentaQuintaResumenDTO`
(📄 `dto/RentaQuintaResumenDTO.java`). Spring lo convierte en JSON y lo devuelve a C#.
### Resumen visual
```
C# ── GET /listado?anio=2026&mes=8&tipoPlanilla=ADM ──►
RentaQuintaController.listado [C1] desdeParametro("ADM")
└► RentaQuintaServiceImpl.buscar
├► RentaQuintaRepository.buscarDocumentos [C2] trim(boleta_tipo_personal)='ADM'
├► RentaQuintaRepository.historialHastaMes
├► aplicarBasicoYUtilidades
│ └► baseGratificacion (por boleta)
├► obtenerRetencionesMesAnterior
└► por cada trabajador:
├► agregar → mapper.totalBrutoMes → bloque
├► tipoPlanilla(solicitado, referencia) ← decide el tipo del trabajador
├► calcular → otrosEmpleadores
│ ├► modalidad(tipo, tipo_pago) [C3]
│ ├► (DOC por hora) ProyeccionDocenteHoraRepository.remunBrutaFutura
│ └► RentaQuintaCalculadora.calcular
│ ├► proyeccionSueldoBruto [C4]
│ ├► gratificacion (estimado)
│ ├► gratificacionesPendientes → gratificacion
│ ├► impuestoSobre (ley)
│ └► divisorDelMes (ley)
└► mapper.toResumen
◄── JSON Page<RentaQuintaResumenDTO> ── C#
```
---
## 5. Circuito de `GET /detallado`
Ejemplo: `?numeroDocumento=12345678&anio=2026&mes=8&tipoPlanilla=DOC`
| Orden | Llamada | Archivo:línea |
|---|---|---|
| 1 | `detallado(...)` + **[C1]** `desdeParametro("DOC")` → `DOCENTE` | Controller `:73`, `:78` |
| 2 | `obtenerDetalleCompleto(doc, anio, mes, tipo)` | ServiceImpl `:195` |
| 3 | `repository.historialAnualPorDocumento` (todo el año del DNI) | ServiceImpl `:197` |
| 4 | `aplicarBasicoYUtilidades` → `baseGratificacion` | ServiceImpl `:451` |
| 5 | `agregar` → si no hay boleta de referencia, 404 | ServiceImpl `:291` |
| 6 | `tipoPlanilla(tipoSolicitado, referencia)` | ServiceImpl `:206` |
| 7 | `calcular(..., tipo)` → **[C3]** `modalidad` → calculadora **[C4]** | ServiceImpl `:332` |
| 8 | `mapper.toDetalleCompleto` (bloques pasado / actual / proyección, liquidación, tramos, control) | ServiceImpl `:208` → Mapper `:82` |
`POST /grabar` pasa por la misma cadena (`grabarRetencion` → `obtenerDetalleCompleto`).
⚠️ Hoy el `upsertRetencion` está **comentado**: calcula, pero no graba.
`POST /grabar-masivo` → `grabarSegunFiltro` (ServiceImpl `:246`) repite el circuito del listado
en lotes de 200, con **[C2]** filtrando y `tipoPlanilla(...)` en `:275`, y graba con `upsertRetencion`.
## 6. Endpoints sin cálculo
`GET /{id}`, `GET /boleta` y `GET /historial/{codper}/{anio}` no reciben `tipoPlanilla`: solo
leen la boleta. Pasan por `aplicarBasicoYUtilidades` y la
devuelven con `mapper.toDetalle` / `mapper.toHistorial`.
---
## 7. Docente por hora (DOC, tipo_pago 1)
Solo cambia la **proyección del sueldo bruto** [C4]. Se lee de
`planilla.pl_proyeccion_renta_docentehora` (📄 `proyecciondocentehora/`):
```sql
select coalesce(sum(remun_bruta_futura), 0)
from planilla.pl_proyeccion_renta_docentehora
where numero_documento = :doc
and trim(boleta_tipo_personal) = 'DOC'
and proyeccion_anio = :anio
and proyeccion_mes = :mes -- mes consultado
```
- Se toma la fila del **mes consultado**: su `remun_bruta_futura` es la proyección del sueldo
bruto de ese mes de corte, sin multiplicar ni sumar otros meses.
- Sin fila para el docente → proyección 0 (no cae a remuneración × meses).
- DOC2 (`tipo_pago = 2`) sigue el flujo de siempre: remuneración de referencia × meses restantes.
- Historial, mes actual, estimado del mes de corte sin boleta, gratificaciones, UIT, tramos y
divisor: igual que ADM.
## 8. Decisiones a confirmar
1. **Trabajador con boletas ADM y DOC en el mismo año**: hoy el ingreso **suma todas sus
boletas** (mismo empleador; la ley calcula sobre el total). El tipo solo define **qué reglas**
se aplican: el enviado por C#, o el de la boleta de referencia.
2. **Código vacío o desconocido en `boleta_tipo_personal`**: se trata como `ADM`.
3. **`tipoPlanilla` es opcional**: si C# no lo envía, el tipo se toma de la boleta. Si debe ser
obligatorio, basta con poner `@RequestParam` sin `required = false` en el controller.
# Origen del básico para la gratificación (`basicoGratificacion`)
Este campo solo se usa para calcular la **gratificación pendiente** (julio/diciembre) en
`RentaQuintaCalculadora.gratificacionesPendientes`. Se llena en
`RentaQuintaServiceImpl.aplicarBasicoYUtilidades` (líneas 361-382):
```java
Map<String, BigDecimal> basicoPorDocumento = rentaBasicoRepository
.findByAnioAndNumeroDocumentoIn(anio, documentos).stream()
.collect(Collectors.toMap(RentaBasico::getNumeroDocumento, RentaBasico::getMontoBasico, (a, b) -> a));
for (RentaQuinta r : historial) {
BigDecimal basicoGratificacion = basicoPorDocumento.get(r.getNumeroDocumento());
r.setBasicoGratificacion(basicoGratificacion != null ? basicoGratificacion : r.getBasico());
...
}
```
La regla es simple: **¿existe algún registro en `pl_renta_basico` para ese trabajador en ese
año?** (no filtra por mes, así que si hay varios se queda con uno arbitrario, por el
`(a, b) -> a` del `toMap`).
- Si existe → se usa `montoBasico` de `pl_renta_basico`.
- Si no existe → se usa `basico` de la propia boleta (`pl_planilla`) del mes de referencia.
---
## Caso 1 — usa `pl_renta_basico`
Trabajador `12345678`, periodo 2026, mes de corte julio.
**`pl_renta_basico`** tiene un registro para este trabajador en 2026:
| numero_documento | anio | mes | monto_basico |
|---|---|---|---|
| 12345678 | 2026 | 3 | 2,000.00 |
**`pl_planilla`** (boleta de junio, mes de referencia porque julio aún no está cargado):
| numero_documento | boleta_anio | boleta_mes | basico |
|---|---|---|---|
| 12345678 | 2026 | 6 | 1,800.00 |
`basicoPorDocumento.get("12345678")` devuelve `2000.00` (no es `null`) →
```java
r.setBasicoGratificacion(2000.00); // gana pl_renta_basico
```
Gratificación pendiente = `2000.00 × 1.09 = 2,180.00` (por julio, y otra vez si diciembre
tampoco fue percibida).
---
## Caso 2 — usa `pl_planilla`
Mismo escenario, pero el trabajador `87654321` **no tiene ningún registro** en
`pl_renta_basico` para 2026.
**`pl_renta_basico`**: sin filas para `87654321` en 2026 (o directamente la tabla no tiene
datos suyos).
**`pl_planilla`** (boleta de junio, mes de referencia):
| numero_documento | boleta_anio | boleta_mes | basico |
|---|---|---|---|
| 87654321 | 2026 | 6 | 1,800.00 |
`basicoPorDocumento.get("87654321")` devuelve `null` →
```java
r.setBasicoGratificacion(r.getBasico()); // cae al básico de pl_planilla: 1,800.00
```
Gratificación pendiente = `1,800.00 × 1.09 = 1,962.00`.
---
## Nota
El resto de la boleta (sueldo bruto, asignación familiar, etc.) siempre sale de `pl_planilla`;
`pl_renta_basico` **solo** puede pisar el básico usado específicamente para proyectar la
gratificación de julio/diciembre — nunca reemplaza el `basico` que se muestra en la boleta ni
afecta el sueldo bruto del mes.
# Parámetros de los endpoints — renta de quinta categoría
Solo documenta parámetros de entrada (query/path). Las respuestas no cambiaron de estructura.
# Endpoints — renta de quinta categoría
Base: `/api/v1/renta-quinta`
`anio` y `mes` son los nombres usados en **todos** los endpoints — antes `/listado` usaba
`periodo`/`mesCorte`, ahora unificado.
`anio` y `mes` son los nombres usados en **todos** los endpoints.
## Tipo de planilla (`tipoPlanilla`)
Solo se aceptan tres valores (no distingue mayúsculas). `DOC` sin dígito responde 400.
| `tipoPlanilla` | Filtra en `pl_planilla` | Proyección del sueldo bruto | Se graba en `pl_renta_quinta` |
|---|---|---|---|
| `ADM` | `boleta_tipo_personal = 'ADM'` | Remuneración de referencia × meses restantes | `boleta_tipo_personal = 'ADM'`, `tipo_pago` vacío (`' '`) |
| `DOC1` | `boleta_tipo_personal = 'DOC'` y `tipo_pago = '1'` | `remun_bruta_futura` de `pl_proyeccion_renta_docentehora` del mes consultado | `boleta_tipo_personal = 'DOC'`, `tipo_pago = '1'` |
| `DOC2` | `boleta_tipo_personal = 'DOC'` y `tipo_pago = '2'` | Igual que ADM | `boleta_tipo_personal = 'DOC'`, `tipo_pago = '2'` |
- `DOC1` = profesor costo por hora. `DOC2` = profesor monto fijo.
- Solo cambia la proyección del sueldo bruto; gratificaciones, UIT, tramos y divisor son iguales.
- En `listado` y `detallado` es opcional: si no se envía, no filtra y el tipo se toma de la boleta
del trabajador. En `grabar` y `grabar-masivo` es **obligatorio**.
---
## GET `/listado`
......@@ -15,8 +29,8 @@ Grilla paginada de retenciones, con cálculo.
|---|---|---|---|
| `anio` | Integer | sí | Periodo (año) de la grilla |
| `mes` | Integer (1-12) | sí | Mes de corte |
| `tipoPlanilla` | String (`ADM` / `DOC1` / `DOC2`) | no | Filtra y decide el cálculo (ver tabla arriba) |
| `sede` | Integer | no | Filtro de catálogo (comparado como texto) |
| `tipoPlanilla` | Integer | no | Filtro de catálogo (comparado como texto) |
| `areaGeneral` | Integer | no | Filtro de catálogo (comparado como texto) |
| `subArea` | Integer | no | Filtro de catálogo (comparado como texto) |
| `cargo` | Integer | no | Filtro de catálogo (comparado como texto) |
......@@ -24,24 +38,75 @@ Grilla paginada de retenciones, con cálculo.
| `page` | Integer | no (default 0) | Página |
| `size` | Integer | no (default 20, tope 100) | Tamaño de página |
## GET `/{id}`
```
GET /api/v1/renta-quinta/listado?anio=2026&mes=8&tipoPlanilla=DOC1&page=0&size=20
```
Boleta completa por `id_renta_quinta`.
## GET `/detallado`
Detalle completo del cálculo (boleta del mes + historial de ingresos, parámetros SUNAT,
liquidación anual, escala por tramos y control de retenciones).
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` (path) | Long | sí | `id_renta_quinta` |
| `numeroDocumento` | String | sí | Documento del trabajador |
| `anio` | Integer | sí | Periodo (año) |
| `mes` | Integer (1-12) | sí | Mes de corte |
| `tipoPlanilla` | String (`ADM` / `DOC1` / `DOC2`) | no | Reglas del cálculo; si no se envía, se toma de la boleta |
## GET `/detallado`
```
GET /api/v1/renta-quinta/detallado?numeroDocumento=42564692&anio=2026&mes=8&tipoPlanilla=DOC1
```
Detalle completo del cálculo (boleta del mes + historial de ingresos, parámetros SUNAT,
liquidación anual, escala por tramos y control de retenciones).
## POST `/grabar`
Calcula y graba (upsert) la retención del mes de un trabajador en `pl_renta_quinta`. Responde
`200` sin cuerpo.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `numeroDocumento` | String | sí | Documento del trabajador |
| `anio` | Integer | sí | Periodo (año) |
| `mes` | Integer (1-12) | sí | Mes de corte |
| `tipoPlanilla` | String (`ADM` / `DOC1` / `DOC2`) | **sí** | Decide el cálculo y lo que se graba en `boleta_tipo_personal` + `tipo_pago` |
```
POST /api/v1/renta-quinta/grabar?numeroDocumento=42564692&anio=2026&mes=8&tipoPlanilla=DOC2
```
## POST `/grabar-masivo`
Graba (upsert) la retención del mes de **todos** los trabajadores que cumplen los filtros (mismos
que `listado`, sin paginar).
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `anio` | Integer | sí | Periodo (año) |
| `mes` | Integer (1-12) | sí | Mes de corte |
| `tipoPlanilla` | String (`ADM` / `DOC1` / `DOC2`) | **sí** | Filtra, decide el cálculo y lo que se graba |
| `sede` | 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 | `numero_documento` o apellidos + nombres |
```
POST /api/v1/renta-quinta/grabar-masivo?anio=2026&mes=8&tipoPlanilla=ADM
```
Respuesta:
```json
{ "trabajadoresGrabados": 3 }
```
## GET `/{id}`
Boleta completa por `id_renta_quinta`.
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `id` (path) | Long | sí | `id_renta_quinta` |
## GET `/boleta`
......@@ -61,3 +126,16 @@ Meses del trabajador en el periodo.
|---|---|---|---|
| `codper` (path) | Integer | sí | Código de personal |
| `anio` (path) | Integer | sí | Periodo (año) |
---
## Errores
Formato: `{ "timestamp", "status", "message" }`.
| Caso | Status | `message` |
|---|---|---|
| `tipoPlanilla` inválido (p. ej. `DOC`, `DOC3`) | 400 | `tipoPlanilla 'DOC' no válido. Valores permitidos: ADM, DOC1, DOC2` |
| Falta `tipoPlanilla` en `grabar` / `grabar-masivo` | 400 | `Falta el parámetro obligatorio 'tipoPlanilla'` |
| `mes` fuera de 1-12 u otro parámetro obligatorio ausente | 400 | Detalle del parámetro |
| Trabajador sin boleta en el periodo (`detallado`, `grabar`) | 404 | `No existe boleta para documento ..., periodo ...` |
# Grabado de retención en `pl_renta_quinta` — Guía para el Frontend
Base path: `/api/v1/renta-quinta`
Swagger UI: `/docs`
`GET /detallado` sigue siendo **solo consulta**: calcula y devuelve el reporte, no graba nada.
El grabado en `pl_renta_quinta` es una acción **aparte**, disparada por un botón — no ocurre
automáticamente al consultar el detallado.
---
## Flujo recomendado en pantalla
1. El usuario consulta el reporte:
`GET /detallado?numeroDocumento={doc}&anio={anio}&mes={mes}`
→ se muestra el detalle completo, incluida la fila **"RETENCIÓN DEL MES ACTUAL"**
(`controlRetenciones.retencionMesActual`).
2. El usuario aprieta el botón **"Grabar"** (o "Guardar retención") de esa pantalla.
3. El frontend llama a `POST /grabar` con los **mismos** `numeroDocumento`, `anio` y `mes` que
ya está mostrando en pantalla — no hace falta volver a calcular nada en el front, el backend
recalcula internamente con la misma lógica.
```
POST /api/v1/renta-quinta/grabar?numeroDocumento={doc}&anio={anio}&mes={mes}
```
Respuesta: `200 OK`, sin body.
El valor que queda grabado en `pl_renta_quinta.retencion_mes_actual` es exactamente
`controlRetenciones.retencionMesActual` del `/detallado` — el mismo número que el usuario vio en
pantalla antes de apretar el botón.
## Grabado masivo (grilla)
Para un botón "Grabar todo" desde la grilla (`/listado`), usar el mismo filtro que la grilla,
sin el tope de página:
```
POST /api/v1/renta-quinta/grabar-masivo?anio={anio}&mes={mes}&sede=...&tipoPlanilla=...&areaGeneral=...&subArea=...&cargo=...&documentoOApellidos=...
```
Todos los filtros salvo `anio` y `mes` son opcionales, igual que en `/listado`. Respuesta:
```json
{ "trabajadoresGrabados": 3 }
```
## `mes` = mes de corte / proyección, no "último mes con boleta real"
`mes` es el mes que el usuario está consultando/proyectando — puede ser un mes sin boleta real
todavía (el backend lo estima con el sueldo del mes de referencia). Ejemplo: si los datos reales
llegan hasta julio y el usuario quiere ver/grabar la proyección de agosto, el frontend manda
`mes=8`, no `mes=7`.
## Cada mes es una fila independiente (no pisa las anteriores)
La PK de `pl_renta_quinta` es `(numero_documento, anio, mes, boleta_tipo_personal)`. Grabar `mes=9` no borra ni
modifica la fila de `mes=8` del mismo trabajador — cada mes consultado queda como su propia fila
(upsert por esa combinación). Si el usuario recalcula el mismo `mes` dos veces, se actualiza esa
misma fila, no se duplica.
## Errores
Si `numeroDocumento` + `anio` + `mes` no tiene ningún dato (ni boleta real ni mes anterior de
referencia), el backend responde `404` con el body estándar `{timestamp, status, message}` de
`GlobalExceptionHandler` — no graba nada.
# Circuito de grabado en `planilla.pl_renta_quinta`
Este documento describe el flujo del endpoint `POST /api/v1/renta-quinta/grabar`, que persiste
la retención de quinta categoría calculada para un trabajador en un mes puntual.
## Tabla destino
`planilla.pl_renta_quinta` — **no** es una boleta completa (eso sigue viviendo en
`pl_planilla`). Solo guarda:
| Columna | Tipo | Notas |
|---|---|---|
| `numero_documento` | `varchar(8)` | PK (compuesta) |
| `anio` | `integer` | PK (compuesta) |
| `mes` | `integer` | PK (compuesta) |
| `boleta_tipo_personal` | `char(3)` | PK (compuesta): `ADM` / `DOC` (`calculo/TipoPlanilla`) |
| `retencion_mes_actual` | `numeric` | resultado del cálculo, NOT NULL |
| `tipo_pago` | `char(1)` | Solo DOC: `1` = DOC1 (costo por hora), `2` = DOC2 (monto fijo). Vacío (`' '`) en ADM. PK (compuesta), NOT NULL (`calculo/TipoPago`) |
| `tipoPlanilla` enviado | `boleta_tipo_personal` | `tipo_pago` |
|---|---|---|
| `ADM` | `ADM` | vacío (`' '`) |
| `DOC1` | `DOC` | `1` |
| `DOC2` | `DOC` | `2` |
PK compuesta `(numero_documento, anio, mes, boleta_tipo_personal, tipo_pago)`: una fila por trabajador,
periodo y tipo de planilla; `tipo_pago` es parte de la PK y NOT NULL, por eso ADM graba un espacio y no NULL. El tipo grabado es **siempre** el
`tipoPlanilla` enviado por el cliente (obligatorio en `/grabar` y `/grabar-masivo`; sin él o
vacío responde 400). No se toma de la boleta ni se rellena por defecto. `ddl-auto: validate`,
así que el mapeo tiene que calzar exacto con esas 6 columnas (`boleta_tipo_personal` y
`tipo_pago` con `columnDefinition = "bpchar"`). La columna `tipo_pago` se crea con
`docs/alter-pl-renta-quinta-tipo-pago.sql`, **antes** de desplegar.
## Endpoint
```
POST /api/v1/renta-quinta/grabar?numeroDocumento={doc}&anio={anio}&mes={mes}&tipoPlanilla={ADM|DOC1|DOC2}
```
`RentaQuintaController.grabar` (`controller/RentaQuintaController.java`) valida `anio` obligatorio
y `mes` entre 1 y 12, y delega en el service. Responde `200 OK` sin body.
## Circuito paso a paso
```
POST /grabar
│
▼
RentaQuintaController.grabar(numeroDocumento, anio, mes)
│
▼
RentaQuintaServiceImpl.grabarRetencion(numeroDocumento, anio, mes) [@Transactional]
│
├─► obtenerDetalleCompleto(numeroDocumento, anio, mes)
│ │ (el MISMO cálculo que usa GET /detallado — no hay una fórmula aparte)
│ ├─ repository.historialAnualPorDocumento(numeroDocumento, anio)
│ ├─ aplicarBasicoYUtilidades(historial, anio) → básico de pl_planilla / pl_renta_basico,
│ │ básicoGratificacion, utilidades de pl_utilidades
│ ├─ agregar(historial, mesCorte) → historialReal, planillaCargada, retenidoAnterior
│ ├─ calcular(...) → RentaQuintaCalculadora.calcular(DatosCalculo)
│ │ → renta bruta anual, renta neta, impuesto anual,
│ │ retencionDelMes (impuesto anual − retenido previo) ÷ divisor del mes
│ └─ mapper.toDetalleCompleto(...) → RentaQuintaDetalleCompletoDTO
│ (control.retencionMesActual = retencionDelMes)
│
├─► detalle.controlRetenciones().retencionMesActual() ← el número que se graba
│
├─► rentaQuintaGrabadaRepository.findById(numeroDocumento, anio, mes)
│ existe fila → la reutiliza (UPDATE)
│ no existe → RentaQuintaGrabada nueva (INSERT)
│
└─► rentaQuintaGrabadaRepository.save(...) → UPSERT en pl_renta_quinta
```
## Piezas involucradas
| Capa | Clase |
|---|---|
| Controller | `RentaQuintaController.grabar` |
| Service (interfaz) | `RentaQuintaService.grabarRetencion` |
| Service (impl) | `RentaQuintaServiceImpl.grabarRetencion` |
| Entity destino | `entity.RentaQuintaGrabada` (`@Table(name = "pl_renta_quinta")`) |
| PK compuesta | `entity.RentaQuintaGrabadaId` |
| Repositorio | `repository.RentaQuintaGrabadaRepository` |
## Por qué reusa `obtenerDetalleCompleto`
Para que la retención grabada sea siempre idéntica a la que se muestra en
`GET /detallado` (misma boleta, mismos otros empleadores, mismo básico de gratificación, etc.),
no se repite el cálculo: se llama al mismo método que arma el DTO completo y se toma solo el
campo `controlRetenciones.retencionMesActual` para persistirlo.
## Idempotencia
El `findById` + `save` sobre la PK compuesta hace que grabar dos veces el mismo
`numeroDocumento` + `anio` + `mes` + tipo de planilla actualice la misma fila (UPSERT), no duplique registros.
......@@ -19,7 +19,7 @@ Grilla "Proyección y control de quinta categoría". Cada fila es **un trabajado
| `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 |
| `tipoPlanilla` | String | No | — | `ADM` (administrativo), `DOC1` (docente costo por hora, proyección = `remun_bruta_futura` del mes consultado), o `DOC2` (docente monto fijo, cálculo como ADM). `DOC` sin dígito → 400 |
| `areaGeneral` | Integer | No | — | Filtro de catálogo |
| `subArea` | Integer | No | — | Filtro de catálogo |
| `cargo` | Integer | No | — | Filtro de catálogo |
......
package so.planilla_be.proyecciondocentehora.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;
/**
* Remuneración bruta proyectada por mes de un docente por hora (DOC con tipo_pago = 1), en
* planilla.pl_proyeccion_renta_docentehora. Reemplaza a "remuneración × meses restantes" en la
* proyección del sueldo bruto de renta de quinta. Solo lectura: la carga es externa.
*/
@Getter
@Setter
@Entity
@Table(name = "pl_proyeccion_renta_docentehora")
public class ProyeccionDocenteHora {
@Id
@Column(name = "id_proyeccion")
private Long idProyeccion;
@Column(name = "proyeccion_anio", nullable = false)
private Integer proyeccionAnio;
@Column(name = "proyeccion_mes", nullable = false)
private Integer proyeccionMes;
@Column(name = "numero_documento", length = 15, nullable = false)
private String numeroDocumento;
@Column(name = "boleta_tipo_personal", length = 10, nullable = false)
private String boletaTipoPersonal;
@Column(name = "remun_bruta_futura", precision = 12, scale = 2)
private BigDecimal remunBrutaFutura;
}
package so.planilla_be.proyecciondocentehora.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import so.planilla_be.proyecciondocentehora.entity.ProyeccionDocenteHora;
import java.math.BigDecimal;
public interface ProyeccionDocenteHoraRepository extends JpaRepository<ProyeccionDocenteHora, Long> {
/**
* remun_bruta_futura del docente para el mes consultado: es directamente la proyección del
* sueldo bruto de ese mes de corte. 0 si no hay fila.
*/
@Query("""
select coalesce(sum(p.remunBrutaFutura), 0)
from ProyeccionDocenteHora p
where p.numeroDocumento = :numeroDocumento
and trim(p.boletaTipoPersonal) = :tipoPersonal
and p.proyeccionAnio = :anio
and p.proyeccionMes = :mes
""")
BigDecimal remunBrutaFutura(@Param("numeroDocumento") String numeroDocumento,
@Param("tipoPersonal") String tipoPersonal,
@Param("anio") Integer anio,
@Param("mes") int mes);
}
package so.planilla_be.rentaquinta.calculo;
/**
* Regla con la que se proyecta el sueldo bruto de un trabajador, a partir de su
* {@link TipoPlanilla} ({@code boleta_tipo_personal}) y del {@code tipo_pago} de pl_planilla:
*
* <table>
* <tr><th>boleta_tipo_personal</th><th>tipo_pago</th><th>Modalidad</th></tr>
* <tr><td>ADM</td><td>(cualquiera)</td><td>{@link #MONTO_FIJO}</td></tr>
* <tr><td>DOC</td><td>2</td><td>{@link #MONTO_FIJO}</td></tr>
* <tr><td>DOC</td><td>1</td><td>{@link #DOCENTE_POR_HORA}</td></tr>
* </table>
*
* <p>El tipo de pago lo envía el cliente como sufijo de {@code tipoPlanilla} ({@code DOC1} /
* {@code DOC2}, ver {@link TipoPago}); si no lo envía,
* se lee de la boleta. Un DOC sin tipo de pago se trata como {@link #MONTO_FIJO}.
*/
public enum ModalidadCalculo {
/** Proyección = remuneración del mes de referencia × meses restantes. */
MONTO_FIJO,
/**
* Proyección = {@code remun_bruta_futura} de pl_proyeccion_renta_docentehora del mes
* consultado. El resto del cálculo es igual que {@link #MONTO_FIJO}.
*/
DOCENTE_POR_HORA;
public static ModalidadCalculo de(TipoPlanilla tipoPlanilla, TipoPago tipoPago) {
return switch (tipoPlanilla) {
case MENSUAL_MONTO_FIJO -> MONTO_FIJO;
case DOCENTE -> tipoPago == TipoPago.POR_HORA ? DOCENTE_POR_HORA : MONTO_FIJO;
};
}
}
......@@ -56,15 +56,14 @@ public class RentaQuintaCalculadora {
if (!mesCorteReal) {
estimadoSueldoBruto = orZero(datos.remuneracionReferencia());
if (datos.mesCorte() == 7 || datos.mesCorte() == 12) {
estimadoGratificacion = estimadoSueldoBruto.multiply(FACTOR_GRATIFICACION);
estimadoGratificacion = gratificacion(estimadoSueldoBruto);
}
}
BigDecimal estimadoMesCorte = estimadoSueldoBruto.add(estimadoGratificacion);
// Meses después del mes de corte: el propio mes de corte ya está cubierto por
// planillaCargada (real) o por el estimado de arriba.
BigDecimal proyeccionSueldoBruto = orZero(datos.remuneracionReferencia())
.multiply(BigDecimal.valueOf(12 - datos.mesCorte()));
BigDecimal proyeccionSueldoBruto = proyeccionSueldoBruto(datos);
boolean julioCubierto = datos.julioPercibida() || (datos.mesCorte() == 7 && !mesCorteReal);
boolean diciembreCubierto = datos.diciembrePercibida() || (datos.mesCorte() == 12 && !mesCorteReal);
BigDecimal proyeccionGratificaciones = gratificacionesPendientes(datos, julioCubierto, diciembreCubierto);
......@@ -134,10 +133,12 @@ public class RentaQuintaCalculadora {
/**
* Gratificaciones de julio y/o diciembre que aún no se han percibido por ningún medio (ni
* boleta real ni el estimado del propio mes de corte cuando este es julio/diciembre).
*
* <p>Base = {@code basicoReferencia} × 1.09: monto_basico de pl_renta_basico si el trabajador
* tiene registro; si no, básico + asignación familiar de pl_planilla.
*/
private BigDecimal gratificacionesPendientes(DatosCalculo datos, boolean julioCubierto, boolean diciembreCubierto) {
BigDecimal gratificacion = orZero(datos.basicoReferencia().add(datos.asignacionFamiliarReferencia()))
.multiply(FACTOR_GRATIFICACION);
BigDecimal gratificacion = gratificacion(orZero(datos.basicoReferencia()));
BigDecimal total = BigDecimal.ZERO;
if (!julioCubierto) {
total = total.add(gratificacion);
......@@ -148,6 +149,31 @@ public class RentaQuintaCalculadora {
return total;
}
/**
* CONDICIÓN MODALIDAD — sueldo bruto de los meses que faltan después del mes de corte:
* <ul>
* <li>ADM y DOC tipo_pago 2: remuneración del mes de referencia × meses restantes.</li>
* <li>DOC tipo_pago 1 (por hora): remun_bruta_futura del mes consultado en
* pl_proyeccion_renta_docentehora ({@code proyeccionDocenteHora}).</li>
* </ul>
*/
private BigDecimal proyeccionSueldoBruto(DatosCalculo datos) {
return switch (datos.modalidad()) {
case MONTO_FIJO -> orZero(datos.remuneracionReferencia())
.multiply(BigDecimal.valueOf(12 - datos.mesCorte()));
case DOCENTE_POR_HORA -> orZero(datos.proyeccionDocenteHora());
};
}
/**
* Monto de una gratificación (julio o diciembre) a partir de su base: base × 1.09
* (gratificación + bonificación extraordinaria). Se usa tanto para el estimado del mes de
* corte como para las gratificaciones pendientes. Igual para todas las modalidades.
*/
private BigDecimal gratificacion(BigDecimal base) {
return base.multiply(FACTOR_GRATIFICACION);
}
private BigDecimal impuestoSobre(BigDecimal rentaNeta, BigDecimal valorUit, List<TramoCalculado> tramos) {
BigDecimal impuesto = BigDecimal.ZERO;
BigDecimal pisoTramo = BigDecimal.ZERO;
......@@ -217,9 +243,15 @@ public class RentaQuintaCalculadora {
* {@code mesCorte} cuando ese mes sí está cargado. {@code remuneracionReferencia},
* {@code basicoReferencia} y {@code asignacionFamiliarReferencia} vienen de ese mes, y son
* la base de la proyección cuando el mes de corte todavía no tiene boleta.
*
* <p>{@code modalidad} decide cómo se proyecta el sueldo bruto (ver {@link ModalidadCalculo});
* {@code proyeccionDocenteHora} solo se usa en {@link ModalidadCalculo#DOCENTE_POR_HORA}. La
* UIT, las 7 UIT, los tramos y el divisor son de ley e iguales para todas las planillas.
*/
public record DatosCalculo(
Integer periodo,
ModalidadCalculo modalidad,
BigDecimal proyeccionDocenteHora,
int mesCorte,
int mesReferencia,
BigDecimal historialReal,
......
package so.planilla_be.rentaquinta.calculo;
import java.util.Arrays;
/**
* {@code tipo_pago} de pl_planilla, solo para docentes ({@link TipoPlanilla#DOCENTE}). El cliente
* (C#) lo envía como sufijo del parámetro {@code tipoPlanilla}:
*
* <ul>
* <li>{@code ADM} → cálculo normal (administrativo)</li>
* <li>{@code DOC1} → docente costo por hora, proyección = remun_bruta_futura del mes consultado</li>
* <li>{@code DOC2} → docente monto fijo, mismo cálculo que ADM</li>
* </ul>
*
* {@code DOC} sin dígito no se acepta. Sin {@code tipoPlanilla} se toma el {@code tipo_pago} de la
* boleta.
*/
public enum TipoPago {
POR_HORA("1"),
MONTO_FIJO("2");
private final String codigo;
TipoPago(String codigo) {
this.codigo = codigo;
}
public String getCodigo() {
return codigo;
}
/**
* Tipo de pago contenido en el parámetro {@code tipoPlanilla} ({@code DOC1} / {@code DOC2}).
* {@code null} si no trae sufijo ({@code ADM}, {@code DOC}, vacío) o si el sufijo no es válido;
* la validación del parámetro la hace {@link TipoPlanilla#desdeParametro}.
*/
public static TipoPago desdeParametro(String tipoPlanilla) {
if (tipoPlanilla == null) {
return null;
}
String normalizado = tipoPlanilla.trim().toUpperCase();
String prefijo = TipoPlanilla.DOCENTE.getCodigo();
if (!normalizado.startsWith(prefijo) || normalizado.length() == prefijo.length()) {
return null;
}
String sufijo = normalizado.substring(prefijo.length());
return Arrays.stream(values())
.filter(t -> t.codigo.equals(sufijo))
.findFirst()
.orElse(null);
}
}
package so.planilla_be.rentaquinta.calculo;
import so.planilla_be.rentaquinta.entity.RentaQuinta;
import so.planilla_be.rentaquinta.exception.TipoPlanillaInvalidaException;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Tipo de planilla, identificado por {@code planilla.pl_planilla.boleta_tipo_personal}. Filtra la
* grilla y forma parte de la clave de lo grabado en pl_renta_quinta. Las reglas del cálculo no
* salen solo de él: junto con {@code tipo_pago} definen la {@link ModalidadCalculo}.
*
* <p>El cliente (C#) lo envía en el parámetro {@code tipoPlanilla}: {@code ADM}, {@code DOC1}
* (docente costo por hora) o {@code DOC2} (docente monto fijo); el dígito es el
* {@code tipo_pago} ({@link TipoPago}). Si no lo envía, se toma de la boleta del trabajador.
*/
public enum TipoPlanilla {
/** {@code ADM}: planilla mensual (monto fijo). */
MENSUAL_MONTO_FIJO("ADM", "Planilla mensual (monto fijo)"),
/**
* {@code DOC}: docentes. Con {@code tipo_pago = 2} calculan igual que {@link #MENSUAL_MONTO_FIJO};
* con {@code tipo_pago = 1} son docentes por hora (ver {@link ModalidadCalculo}).
*/
DOCENTE("DOC", "Docentes (monto fijo o por hora, según tipo_pago)");
private final String codigo;
private final String descripcion;
TipoPlanilla(String codigo, String descripcion) {
this.codigo = codigo;
this.descripcion = descripcion;
}
public String getCodigo() {
return codigo;
}
public String getDescripcion() {
return descripcion;
}
public boolean esMensualOMontoFijo() {
return this == MENSUAL_MONTO_FIJO;
}
public boolean esDocente() {
return this == DOCENTE;
}
/**
* Valores aceptados en el parámetro {@code tipoPlanilla}: {@code ADM}, {@code DOC1} (docente
* costo por hora) y {@code DOC2} (docente monto fijo). {@code DOC} sin dígito no se acepta. El
* sufijo de DOC lo resuelve {@link TipoPago#desdeParametro}.
*/
public static final String VALORES_PARAMETRO = Stream.concat(
Stream.of(MENSUAL_MONTO_FIJO.codigo),
Arrays.stream(TipoPago.values()).map(p -> DOCENTE.codigo + p.getCodigo()))
.collect(Collectors.joining(", "));
/**
* Parámetro {@code tipoPlanilla} recibido del cliente ({@link #VALORES_PARAMETRO}).
* {@code null} o vacío = no se envió; un código desconocido responde 400.
*/
public static TipoPlanilla desdeParametro(String codigo) {
if (codigo == null || codigo.isBlank()) {
return null;
}
String normalizado = codigo.trim().toUpperCase();
if (normalizado.equals(MENSUAL_MONTO_FIJO.codigo)) {
return MENSUAL_MONTO_FIJO;
}
if (TipoPago.desdeParametro(normalizado) != null) {
return DOCENTE;
}
throw new TipoPlanillaInvalidaException(
"tipoPlanilla '%s' no válido. Valores permitidos: %s".formatted(codigo, VALORES_PARAMETRO));
}
/**
* Parámetro {@code tipoPlanilla} obligatorio (grabado en pl_renta_quinta): el tipo lo decide
* quien envía, no se toma de la boleta. Vacío o desconocido responde 400.
*/
public static TipoPlanilla obligatorio(String codigo) {
TipoPlanilla tipo = desdeParametro(codigo);
if (tipo == null) {
throw new TipoPlanillaInvalidaException(
"tipoPlanilla es obligatorio. Valores permitidos: %s".formatted(VALORES_PARAMETRO));
}
return tipo;
}
/**
* Tipo de planilla de una boleta. {@code boleta_tipo_personal} es {@code char(3)}, por eso el
* trim. Un código vacío o desconocido se trata como {@link #MENSUAL_MONTO_FIJO}, que es el
* cálculo que se aplicaba antes de condicionar por tipo.
*/
public static TipoPlanilla deBoleta(RentaQuinta boleta) {
String codigo = boleta.getBoletaTipoPersonal() == null ? "" : boleta.getBoletaTipoPersonal().trim();
return DOCENTE.codigo.equals(codigo) ? DOCENTE : MENSUAL_MONTO_FIJO;
}
}
package so.planilla_be.rentaquinta.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
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.calculo.TipoPago;
import so.planilla_be.rentaquinta.calculo.TipoPlanilla;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaFiltroDTO;
......@@ -27,6 +32,16 @@ import java.util.List;
@RequestMapping("/api/v1/renta-quinta")
public class RentaQuintaController {
/** Parámetro que envía el cliente (C#) para elegir las reglas del cálculo por tipo de planilla. */
private static final String TIPO_PLANILLA_DOC = "ADM = administrativo (cálculo normal); DOC1 = docente "
+ "costo por hora (proyección desde pl_proyeccion_renta_docentehora); DOC2 = docente monto fijo "
+ "(mismo cálculo que ADM). Filtra por boleta_tipo_personal y tipo_pago. Si no se envía, se "
+ "toma de la boleta del trabajador.";
private static final String TIPO_PLANILLA_GRABAR_DOC = "Obligatorio. ADM, DOC1 (costo por hora) o DOC2 "
+ "(monto fijo). En pl_renta_quinta se graba boleta_tipo_personal (ADM/DOC) y tipo_pago (1/2, null en ADM).";
private final RentaQuintaService service;
public RentaQuintaController(RentaQuintaService service) {
......@@ -39,15 +54,16 @@ public class RentaQuintaController {
@RequestParam @NotNull Integer anio,
@RequestParam @NotNull @Min(1) @Max(12) Integer mes,
@RequestParam(required = false) Integer sede,
@RequestParam(required = false) Integer tipoPlanilla,
@Parameter(description = TIPO_PLANILLA_DOC) @RequestParam(required = false) String tipoPlanilla,
@RequestParam(required = false) Integer areaGeneral,
@RequestParam(required = false) Integer subArea,
@RequestParam(required = false) Integer cargo,
@RequestParam(required = false) String documentoOApellidos,
@RequestParam(defaultValue = "0") Integer page,
@RequestParam(defaultValue = "20") Integer size) {
TipoPlanilla tipo = TipoPlanilla.desdeParametro(tipoPlanilla);
return service.buscar(new RentaQuintaFiltroDTO(
anio, mes, sede, tipoPlanilla, areaGeneral, subArea, cargo,
anio, mes, sede, tipo, TipoPago.desdeParametro(tipoPlanilla), areaGeneral, subArea, cargo,
documentoOApellidos, page, size));
}
......@@ -64,8 +80,10 @@ public class RentaQuintaController {
public RentaQuintaDetalleCompletoDTO detallado(
@RequestParam String numeroDocumento,
@RequestParam Integer anio,
@RequestParam @Min(1) @Max(12) Integer mes) {
return service.obtenerDetalleCompleto(numeroDocumento, anio, mes);
@RequestParam @Min(1) @Max(12) Integer mes,
@Parameter(description = TIPO_PLANILLA_DOC) @RequestParam(required = false) String tipoPlanilla) {
TipoPlanilla tipo = TipoPlanilla.desdeParametro(tipoPlanilla);
return service.obtenerDetalleCompleto(numeroDocumento, anio, mes, tipo, TipoPago.desdeParametro(tipoPlanilla));
}
@Operation(summary = "Detalle por codper + anio + mes")
......@@ -84,4 +102,38 @@ public class RentaQuintaController {
@PathVariable Integer anio) {
return service.historialAnual(codper, anio);
}
@Operation(summary = "Graba (upsert) en pl_renta_quinta la retención del mes calculada para el "
+ "trabajador: documento + año + mes + retención")
@PostMapping("/grabar")
public ResponseEntity<Void> grabar(
@RequestParam String numeroDocumento,
@RequestParam @NotNull Integer anio,
@RequestParam @NotNull @Min(1) @Max(12) Integer mes,
@Parameter(description = TIPO_PLANILLA_GRABAR_DOC) @RequestParam String tipoPlanilla) {
TipoPlanilla tipo = TipoPlanilla.obligatorio(tipoPlanilla);
service.grabarRetencion(numeroDocumento, anio, mes, tipo, TipoPago.desdeParametro(tipoPlanilla));
return ResponseEntity.ok().build();
}
@Operation(summary = "Graba (upsert) en pl_renta_quinta la retención del mes de todos los "
+ "trabajadores que matchean los mismos filtros del listado (sin límite de página)")
@PostMapping("/grabar-masivo")
public GrabadoMasivoResultado grabarMasivo(
@RequestParam @NotNull Integer anio,
@RequestParam @NotNull @Min(1) @Max(12) Integer mes,
@RequestParam(required = false) Integer sede,
@Parameter(description = TIPO_PLANILLA_GRABAR_DOC) @RequestParam String tipoPlanilla,
@RequestParam(required = false) Integer areaGeneral,
@RequestParam(required = false) Integer subArea,
@RequestParam(required = false) Integer cargo,
@RequestParam(required = false) String documentoOApellidos) {
TipoPlanilla tipo = TipoPlanilla.obligatorio(tipoPlanilla);
int grabados = service.grabarMasivo(new RentaQuintaFiltroDTO(
anio, mes, sede, tipo, TipoPago.desdeParametro(tipoPlanilla), areaGeneral, subArea, cargo,
documentoOApellidos, null, null));
return new GrabadoMasivoResultado(grabados);
}
public record GrabadoMasivoResultado(int trabajadoresGrabados) {}
}
package so.planilla_be.rentaquinta.dto;
import so.planilla_be.rentaquinta.calculo.TipoPago;
import so.planilla_be.rentaquinta.calculo.TipoPlanilla;
/**
* Filtros de la grilla de retenciones mensuales.
*
* <p>Todos los filtros son numéricos salvo {@code documentoOApellidos}, que es texto libre
* (nro de documento o apellidos). Los filtros de catálogo (sede, tipo de planilla, área,
* subárea, cargo) llegan como Integer y se comparan contra columnas TEXT convirtiendo el
* número a su representación en texto.
* <p>{@code documentoOApellidos} es texto libre (nro de documento o apellidos) y
* {@code tipoPlanilla} es el código de {@code boleta_tipo_personal} (ADM / DOC), que además de
* filtrar decide las reglas del cálculo; {@code tipoPago} (sufijo de DOC1 / DOC2: 1 = por hora,
* 2 = monto fijo) filtra por {@code tipo_pago} y decide la proyección. El resto de filtros de catálogo
* (sede, área, subárea, cargo) llegan como Integer y se comparan contra columnas TEXT convirtiendo
* el número a su
* representación en texto.
*/
public record RentaQuintaFiltroDTO(
Integer periodo,
Integer mesCorte,
Integer sede,
Integer tipoPlanilla,
TipoPlanilla tipoPlanilla,
TipoPago tipoPago,
Integer areaGeneral,
Integer subArea,
Integer cargo,
......
......@@ -21,5 +21,7 @@ public record RentaQuintaResumenDTO(
BigDecimal rentaBrutaAnual,
BigDecimal impuestoAnual,
BigDecimal retenidoAnterior,
BigDecimal retencionDelMes
BigDecimal retencionDelMes,
BigDecimal retencionAnteriorGrabada, // <--- NUEVO
BigDecimal diferenciaRetencion // <--- NUEVO
) {}
......@@ -168,6 +168,15 @@ public class RentaQuinta {
@Transient
private BigDecimal utilidades;
/**
* Básico usado para calcular la gratificación pendiente (julio/diciembre): se toma de
* planilla.pl_renta_basico del mes anterior al de referencia; si no hay registro ahí, el
* básico + asignación familiar de pl_planilla del mes de referencia. Ver
* RentaQuintaServiceImpl#aplicarBasicoYUtilidades.
*/
@Transient
private BigDecimal basicoGratificacion;
/* VACACIONES Y LIQUIDACIÓN */
@Column(name = "cts", precision = 15, scale = 2)
......@@ -378,6 +387,10 @@ public class RentaQuinta {
@Column(name = "tipo_labor")
private String tipoLabor;
/** Solo en DOC: 1 = docente por hora, 2 = monto fijo (ver {@code ModalidadCalculo}). */
@Column(name = "tipo_pago", length = 1, columnDefinition = "bpchar")
private String tipoPago;
/** No existen en pl_planilla; se conservan para no romper el contrato del API. */
@Transient
private Integer campo1;
......
package so.planilla_be.rentaquinta.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.IdClass;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
/**
* Retención de quinta categoría grabada para un trabajador en un periodo dado, en
* planilla.pl_renta_quinta. Solo guarda lo mínimo: documento, año, mes, tipo de planilla, tipo de
* pago y la retención calculada para ese mes de corte (no es una boleta completa, esa vive en pl_planilla).
*
* <p>La PK incluye {@code boleta_tipo_personal} ({@code ADM} / {@code DOC}, ver
* {@link so.planilla_be.rentaquinta.calculo.TipoPlanilla}): un mismo documento puede tener una
* retención grabada por cada tipo de planilla en el mismo periodo.
*/
@Getter
@Setter
@Entity
@Table(name = "pl_renta_quinta")
@IdClass(RentaQuintaGrabadaId.class)
public class RentaQuintaGrabada {
@Id
@Column(name = "numero_documento", length = 8)
private String numeroDocumento;
@Id
@Column(name = "anio")
private Integer anio;
@Id
@Column(name = "mes")
private Integer mes;
/** char(3): {@code bpchar} para que {@code ddl-auto: validate} no falle. */
@Id
@Column(name = "boleta_tipo_personal", length = 3, columnDefinition = "bpchar")
private String boletaTipoPersonal;
@Column(name = "retencion_mes_actual", nullable = false)
private BigDecimal retencionMesActual;
/**
* Parte de la PK, NOT NULL. Solo DOC: {@code 1} = docente costo por hora (DOC1), {@code 2} =
* monto fijo (DOC2); en ADM {@code ''} (vacío). char(1), ver
* {@link so.planilla_be.rentaquinta.calculo.TipoPago}.
*/
@Id
@Column(name = "tipo_pago", length = 1, columnDefinition = "bpchar")
private String tipoPago;
}
package so.planilla_be.rentaquinta.entity;
import java.io.Serializable;
import java.util.Objects;
/**
* PK de planilla.pl_renta_quinta: {@code (numero_documento, anio, mes, boleta_tipo_personal,
* tipo_pago)}. {@code tipo_pago} es {@code ''} en ADM y {@code 1}/{@code 2} en DOC.
*/
public class RentaQuintaGrabadaId implements Serializable {
private String numeroDocumento;
private Integer anio;
private Integer mes;
private String boletaTipoPersonal;
private String tipoPago;
public RentaQuintaGrabadaId() {
}
public RentaQuintaGrabadaId(String numeroDocumento, Integer anio, Integer mes, String boletaTipoPersonal,
String tipoPago) {
this.numeroDocumento = numeroDocumento;
this.anio = anio;
this.mes = mes;
this.boletaTipoPersonal = boletaTipoPersonal;
this.tipoPago = tipoPago;
}
// --- GETTERS Y SETTERS OBLIGATORIOS PARA JPA ---
public String getNumeroDocumento() {
return numeroDocumento;
}
public void setNumeroDocumento(String numeroDocumento) {
this.numeroDocumento = numeroDocumento;
}
public Integer getAnio() {
return anio;
}
public void setAnio(Integer anio) {
this.anio = anio;
}
public Integer getMes() {
return mes;
}
public void setMes(Integer mes) {
this.mes = mes;
}
public String getBoletaTipoPersonal() {
return boletaTipoPersonal;
}
public void setBoletaTipoPersonal(String boletaTipoPersonal) {
this.boletaTipoPersonal = boletaTipoPersonal;
}
public String getTipoPago() {
return tipoPago;
}
public void setTipoPago(String tipoPago) {
this.tipoPago = tipoPago;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof RentaQuintaGrabadaId that)) {
return false;
}
return Objects.equals(numeroDocumento, that.numeroDocumento)
&& Objects.equals(anio, that.anio)
&& Objects.equals(mes, that.mes)
&& Objects.equals(boletaTipoPersonal, that.boletaTipoPersonal)
&& Objects.equals(tipoPago, that.tipoPago);
}
@Override
public int hashCode() {
return Objects.hash(numeroDocumento, anio, mes, boletaTipoPersonal, tipoPago);
}
}
......@@ -33,6 +33,12 @@ public class GlobalExceptionHandler {
return build(HttpStatus.CONFLICT, ex.getMessage());
}
/** tipoPlanilla con un código distinto de ADM / DOC. */
@ExceptionHandler(TipoPlanillaInvalidaException.class)
public ResponseEntity<Map<String, Object>> handleTipoPlanilla(TipoPlanillaInvalidaException ex) {
return build(HttpStatus.BAD_REQUEST, ex.getMessage());
}
/** Filtros fuera de rango (p. ej. mesCorte=13). */
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, Object>> handleValidation(ConstraintViolationException ex) {
......
package so.planilla_be.rentaquinta.exception;
public class TipoPlanillaInvalidaException extends RuntimeException {
public TipoPlanillaInvalidaException(String message) {
super(message);
}
}
......@@ -24,7 +24,7 @@ public class RentaQuintaMapper {
public RentaQuintaResumenDTO toResumen(RentaQuinta referencia, Integer codper, Integer periodo, Integer mesCorte,
BigDecimal historialReal, BigDecimal planillaCargada,
BigDecimal retenidoAnterior, CalculoRentaQuinta calculo) {
BigDecimal retenidoAnterior, CalculoRentaQuinta calculo,BigDecimal retencionAnteriorGrabada, BigDecimal diferencia) {
return new RentaQuintaResumenDTO(
codper,
periodo,
......@@ -33,11 +33,13 @@ public class RentaQuintaMapper {
mesCorte,
orZero(historialReal),
orZero(planillaCargada),
calculo.proyeccionFutura(),
calculo.proyeccionFutura().add(calculo.otrosEmpleadoresImporteProyeccion()),
calculo.rentaBrutaAnualSinOtrosEmpleadores(),
calculo.impuestoAnual(),
orZero(retenidoAnterior),
calculo.retencionDelMes());
calculo.retencionDelMes(),
orZero(retencionAnteriorGrabada),
diferencia);
}
public RentaQuintaHistorialDTO toHistorial(RentaQuinta e) {
......
package so.planilla_be.rentaquinta.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import so.planilla_be.rentaquinta.entity.RentaQuintaGrabada;
import so.planilla_be.rentaquinta.entity.RentaQuintaGrabadaId;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface RentaQuintaGrabadaRepository extends JpaRepository<RentaQuintaGrabada, RentaQuintaGrabadaId> {
@Query("select r from RentaQuintaGrabada r where r.anio = :anio and r.mes = :mes and r.numeroDocumento in :numerosDocumento")
List<RentaQuintaGrabada> findByAnioAndMesAndNumeroDocumentoIn(
@Param("anio") Integer anio,
@Param("mes") Integer mes,
@Param("numerosDocumento") List<String> numerosDocumento
);
}
......@@ -19,6 +19,7 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
String FILTROS = """
and (:sede is null or trim(r.codlocal) = :sede)
and (:tipoPlanilla is null or trim(r.boletaTipoPersonal) = :tipoPlanilla)
and (:tipoPago is null or trim(r.tipoPago) = :tipoPago)
and (:areaGeneral is null or r.areaGeneral = :areaGeneral)
and (:subArea is null or r.subArea = :subArea)
and (:cargo is null or r.cargo = :cargo)
......@@ -62,6 +63,7 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
@Param("mesCorte") Integer mesCorte,
@Param("sede") String sede,
@Param("tipoPlanilla") String tipoPlanilla,
@Param("tipoPago") String tipoPago,
@Param("areaGeneral") String areaGeneral,
@Param("subArea") String subArea,
@Param("cargo") String cargo,
......
package so.planilla_be.rentaquinta.service;
import org.springframework.data.domain.Page;
import so.planilla_be.rentaquinta.calculo.TipoPago;
import so.planilla_be.rentaquinta.calculo.TipoPlanilla;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaFiltroDTO;
......@@ -15,9 +17,15 @@ public interface RentaQuintaService {
RentaQuintaDetalleDTO obtenerPorId(Long id);
RentaQuintaDetalleCompletoDTO obtenerDetalleCompleto(String numeroDocumento, Integer anio, Integer mes);
RentaQuintaDetalleCompletoDTO obtenerDetalleCompleto(String numeroDocumento, Integer anio, Integer mes,
TipoPlanilla tipoPlanilla, TipoPago tipoPago);
RentaQuintaDetalleDTO obtenerBoleta(Integer codper, Integer anio, Integer mes);
List<RentaQuintaHistorialDTO> historialAnual(Integer codper, Integer anio);
void grabarRetencion(String numeroDocumento, Integer anio, Integer mes, TipoPlanilla tipoPlanilla,
TipoPago tipoPago);
int grabarMasivo(RentaQuintaFiltroDTO filtro);
}
......@@ -5,18 +5,26 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import so.planilla_be.proyecciondocentehora.repository.ProyeccionDocenteHoraRepository;
import so.planilla_be.rentaquinta.calculo.CalculoRentaQuinta;
import so.planilla_be.rentaquinta.calculo.ModalidadCalculo;
import so.planilla_be.rentaquinta.calculo.RentaQuintaCalculadora;
import so.planilla_be.rentaquinta.calculo.RentaQuintaCalculadora.DatosCalculo;
import so.planilla_be.rentaquinta.calculo.TipoPago;
import so.planilla_be.rentaquinta.calculo.TipoPlanilla;
import so.planilla_be.rentaquinta.exception.TipoPlanillaInvalidaException;
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.entity.RentaQuintaGrabada;
import so.planilla_be.rentaquinta.entity.RentaQuintaGrabadaId;
import so.planilla_be.rentaquinta.exception.RentaQuintaNoEncontradaException;
import so.planilla_be.rentaquinta.mapper.RentaQuintaMapper;
import so.planilla_be.rentaquinta.repository.DocumentoResumenProjection;
import so.planilla_be.rentaquinta.repository.RentaQuintaGrabadaRepository;
import so.planilla_be.rentaquinta.repository.RentaQuintaRepository;
import so.planilla_be.rentaquinta.service.RentaQuintaService;
import so.planilla_be.otrosempleadores.entity.RetencionOtrosEmpleadores;
......@@ -27,6 +35,7 @@ import so.planilla_be.utilidades.entity.Utilidad;
import so.planilla_be.utilidades.repository.UtilidadRepository;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
......@@ -41,6 +50,12 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
private static final int SIZE_DEFAULT = 20;
private static final int SIZE_MAX = 100;
private static final int GRABADO_MASIVO_BATCH = 200;
/**
* tipo_pago de pl_renta_quinta en ADM: la columna es NOT NULL (parte de la PK). Un espacio y no
* {@code ""}, porque char(1) se lee con padding (' ') y Hibernate lo tomaría como cambio de PK.
*/
private static final String TIPO_PAGO_VACIO = " ";
private final RentaQuintaRepository repository;
private final RentaQuintaMapper mapper;
......@@ -48,27 +63,44 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
private final RetencionOtrosEmpleadoresRepository otrosEmpleadoresRepository;
private final RentaBasicoRepository rentaBasicoRepository;
private final UtilidadRepository utilidadRepository;
private final RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository;
private final ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository;
public RentaQuintaServiceImpl(RentaQuintaRepository repository, RentaQuintaMapper mapper,
RentaQuintaCalculadora calculadora,
RetencionOtrosEmpleadoresRepository otrosEmpleadoresRepository,
RentaBasicoRepository rentaBasicoRepository,
UtilidadRepository utilidadRepository) {
UtilidadRepository utilidadRepository,
RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository,
ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository) {
this.repository = repository;
this.mapper = mapper;
this.calculadora = calculadora;
this.otrosEmpleadoresRepository = otrosEmpleadoresRepository;
this.rentaBasicoRepository = rentaBasicoRepository;
this.utilidadRepository = utilidadRepository;
this.rentaQuintaGrabadaRepository = rentaQuintaGrabadaRepository;
this.proyeccionDocenteHoraRepository = proyeccionDocenteHoraRepository;
}
/**
* TODO: provisorio mientras el frontend no tiene un botón de grabado masivo dedicado (ver
* docs/grabar-renta-quinta-frontend.md). Hasta entonces, cada consulta de la grilla graba de
* una vez en pl_renta_quinta la retención de TODOS los trabajadores que matchean el filtro
* (no solo los de la página actual) — igual que dispararía {@link #grabarMasivo}. Cuando el
* botón esté, sacar el {@code grabarSegunFiltro} de acá y dejar que solo lo dispare
* {@code grabarMasivo}.
*/
@Override
@Transactional
public Page<RentaQuintaResumenDTO> buscar(RentaQuintaFiltroDTO filtro) {
//grabarSegunFiltro(filtro);
Page<DocumentoResumenProjection> documentos = repository.buscarDocumentos(
filtro.periodo(),
filtro.mesCorte(),
codigo(filtro.sede()),
codigo(filtro.tipoPlanilla()),
codigo(filtro.tipoPlanilla()), // CONDICIÓN TIPO PLANILLA: filtra por boleta_tipo_personal
codigo(filtro.tipoPago()), // y por tipo_pago (solo DOC)
codigo(filtro.areaGeneral()),
codigo(filtro.subArea()),
codigo(filtro.cargo()),
......@@ -84,26 +116,36 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
Map<String, List<RentaQuinta>> porDocumento = historialCompleto.stream()
.collect(Collectors.groupingBy(RentaQuinta::getNumeroDocumento));
// 1. AQUÍ OBTIENES EL MAPA CON LAS RETENCIONES GRABADAS DEL MES ANTERIOR:
Map<String, BigDecimal> retencionesAnteriores = obtenerRetencionesMesAnterior(numeros, filtro.periodo(), filtro.mesCorte());
return documentos.map(d -> {
List<RentaQuinta> historial = porDocumento.getOrDefault(d.getNumeroDocumento(), List.of());
Agregado a = agregar(historial, filtro.mesCorte());
CalculoRentaQuinta calculo = calcular(a, historial, filtro.periodo(), filtro.mesCorte());
// Sin boleta real del mes de corte, planillaCargada muestra el mismo estimado que el
// bloque "actual" del detalle completo (sueldo bruto del mes de referencia + grat.
// 109% si el mes de corte es julio/diciembre), no 0.
BigDecimal planillaCargada = a.mesCorteReal()
TipoPlanilla tipoPlanilla = tipoPlanilla(filtro.tipoPlanilla(), a.referencia());
CalculoRentaQuinta calculo = calcular(a, historial, filtro.periodo(), filtro.mesCorte(), tipoPlanilla,
filtro.tipoPago());
BigDecimal planillaCargada = (a.mesCorteReal()
? a.planillaCargadaReal()
: calculo.estimadoMesCorteSueldoBruto().add(calculo.estimadoMesCorteGratificacion());
: calculo.estimadoMesCorteSueldoBruto().add(calculo.estimadoMesCorteGratificacion()))
.add(calculo.otrosEmpleadoresImporteMesActual());
Integer codper = historial.stream()
.map(RentaQuinta::getCodper)
.filter(Objects::nonNull)
.max(Integer::compareTo)
.orElse(null);
// Mismo total que el bloque "pasado real realizado" del detalle completo: planilla
// propia + lo declarado por otros empleadores en meses anteriores al de corte.
BigDecimal historialReal = a.historialReal().add(calculo.otrosEmpleadoresImporteAnterior());
// Extraer la retención grabada en base de datos del mes anterior (206.24) y restar con la calculada del mes (247.97)
BigDecimal retencionAnteriorGrabada = retencionesAnteriores.getOrDefault(
claveGrabada(d.getNumeroDocumento(), tipoPlanilla.getCodigo()), BigDecimal.ZERO);
BigDecimal retencionActualCalculada = calculo.retencionDelMes();
BigDecimal diferencia = retencionActualCalculada.subtract(retencionAnteriorGrabada);
// 3. PASAS LOS VALORES A TU MAPPER:
return mapper.toResumen(a.referencia(), codper, filtro.periodo(), filtro.mesCorte(),
historialReal, planillaCargada, a.retenidoAnterior(), calculo);
historialReal, planillaCargada, a.retenidoAnterior(), calculo,retencionAnteriorGrabada, diferencia);
});
}
......@@ -112,6 +154,24 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
return valor == null ? null : valor.toString();
}
/** boleta_tipo_personal es char(3): el repository lo compara con trim() contra este código. */
private String codigo(TipoPlanilla tipoPlanilla) {
return tipoPlanilla == null ? null : tipoPlanilla.getCodigo();
}
/** tipo_pago es char(1): el repository lo compara con trim() contra este código. */
private String codigo(TipoPago tipoPago) {
return tipoPago == null ? null : tipoPago.getCodigo();
}
/**
* Tipo de planilla con el que se calcula a un trabajador: el que envía el cliente (C#) en
* {@code tipoPlanilla}; si no lo envía, el de su boleta de referencia.
*/
private TipoPlanilla tipoPlanilla(TipoPlanilla solicitado, RentaQuinta referencia) {
return solicitado != null ? solicitado : TipoPlanilla.deBoleta(referencia);
}
private String patron(String texto) {
if (texto == null || texto.isBlank()) {
return null;
......@@ -143,8 +203,18 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
return mapper.toDetalle(boleta);
}
/**
* TODO: provisorio mientras el frontend no tiene el botón "Grabar" (ver
* docs/grabar-renta-quinta-frontend.md). Hasta que exista ese botón dedicado,
* cada consulta del detallado graba de una vez la retención en pl_renta_quinta, para no
* depender de un segundo request manual. Cuando el botón esté, sacar el {@code upsertRetencion}
* de acá y dejar que solo lo dispare {@link #grabarRetencion}.
*/
@Override
public RentaQuintaDetalleCompletoDTO obtenerDetalleCompleto(String numeroDocumento, Integer anio, Integer mes) {
@Transactional
public RentaQuintaDetalleCompletoDTO obtenerDetalleCompleto(String numeroDocumento, Integer anio, Integer mes,
TipoPlanilla tipoPlanillaSolicitado,
TipoPago tipoPago) {
List<RentaQuinta> historialAnio = repository.historialAnualPorDocumento(numeroDocumento, anio);
aplicarBasicoYUtilidades(historialAnio, anio);
int mesCorte = mes;
......@@ -154,11 +224,108 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
throw new RentaQuintaNoEncontradaException(
"No existe boleta para documento %s, periodo %d-%02d".formatted(numeroDocumento, anio, mes));
}
CalculoRentaQuinta calculo = calcular(a, historialAnio, anio, mesCorte);
TipoPlanilla tipoPlanilla = tipoPlanilla(tipoPlanillaSolicitado, a.referencia());
CalculoRentaQuinta calculo = calcular(a, historialAnio, anio, mesCorte, tipoPlanilla, tipoPago);
return mapper.toDetalleCompleto(a.referencia(), historialAnio, mesCorte, calculo);
}
/**
* Graba (upsert) en planilla.pl_renta_quinta la retención del mes calculada para el
* trabajador: documento + año + mes + tipo de planilla + retención, no una boleta completa
* (esa vive en pl_planilla). Reutiliza el mismo cálculo que {@link #obtenerDetalleCompleto}.
* El tipo grabado es siempre el enviado por el cliente: nunca se toma de la boleta.
*/
@Override
@Transactional
public void grabarRetencion(String numeroDocumento, Integer anio, Integer mes, TipoPlanilla tipoPlanilla,
TipoPago tipoPago) {
exigirTipoPlanilla(tipoPlanilla);
RentaQuintaDetalleCompletoDTO detalle = obtenerDetalleCompleto(numeroDocumento, anio, mes, tipoPlanilla,
tipoPago);
upsertRetencion(numeroDocumento, anio, mes, tipoPlanilla, tipoPago,
detalle.controlRetenciones().retencionMesActual());
}
/** El grabado en pl_renta_quinta requiere el tipo enviado (ADM / DOC); no hay valor por defecto. */
private void exigirTipoPlanilla(TipoPlanilla tipoPlanilla) {
if (tipoPlanilla == null) {
throw new TipoPlanillaInvalidaException("tipoPlanilla es obligatorio para grabar (ADM / DOC1 / DOC2)");
}
}
/**
* {@code tipo_pago} se graba tal como lo envió el cliente (DOC1 → 1, DOC2 → 2); en ADM va
* vacío: la columna es parte de la PK y NOT NULL, no admite null.
*/
private void upsertRetencion(String numeroDocumento, Integer anio, Integer mes, TipoPlanilla tipoPlanilla,
TipoPago tipoPago, BigDecimal retencionMesActual) {
String tipoPagoGrabado = tipoPlanilla.esDocente() && tipoPago != null ? tipoPago.getCodigo() : TIPO_PAGO_VACIO;
RentaQuintaGrabadaId id = new RentaQuintaGrabadaId(numeroDocumento, anio, mes, tipoPlanilla.getCodigo(),
tipoPagoGrabado);
RentaQuintaGrabada grabada = rentaQuintaGrabadaRepository.findById(id).orElseGet(RentaQuintaGrabada::new);
grabada.setNumeroDocumento(numeroDocumento);
grabada.setAnio(anio);
grabada.setMes(mes);
grabada.setBoletaTipoPersonal(tipoPlanilla.getCodigo());
grabada.setTipoPago(tipoPagoGrabado);
grabada.setRetencionMesActual(retencionMesActual);
rentaQuintaGrabadaRepository.save(grabada);
}
/**
* Graba (upsert) en pl_renta_quinta la retención del mes de todos los trabajadores que
* matchean los mismos filtros de la grilla ({@code buscar}), sin el tope de paginación de
* la grilla (recorre todas las páginas internamente). Usa el mismo cálculo que la grilla
* ({@code calculo.retencionDelMes()}), consistente con lo que muestra el resumen.
*/
@Override
@Transactional
public int grabarMasivo(RentaQuintaFiltroDTO filtro) {
exigirTipoPlanilla(filtro.tipoPlanilla());
return grabarSegunFiltro(filtro);
}
private int grabarSegunFiltro(RentaQuintaFiltroDTO filtro) {
List<String> numeros = new ArrayList<>();
int page = 0;
Page<DocumentoResumenProjection> pagina;
do {
pagina = repository.buscarDocumentos(
filtro.periodo(), filtro.mesCorte(),
codigo(filtro.sede()), codigo(filtro.tipoPlanilla()), codigo(filtro.tipoPago()),
codigo(filtro.areaGeneral()),
codigo(filtro.subArea()), codigo(filtro.cargo()), patron(filtro.documentoOApellidos()),
PageRequest.of(page, GRABADO_MASIVO_BATCH));
numeros.addAll(pagina.getContent().stream().map(DocumentoResumenProjection::getNumeroDocumento).toList());
page++;
} while (pagina.hasNext());
if (numeros.isEmpty()) {
return 0;
}
List<RentaQuinta> historialCompleto = repository.historialHastaMes(numeros, filtro.periodo(), filtro.mesCorte());
aplicarBasicoYUtilidades(historialCompleto, filtro.periodo());
Map<String, List<RentaQuinta>> porDocumento = historialCompleto.stream()
.collect(Collectors.groupingBy(RentaQuinta::getNumeroDocumento));
int grabados = 0;
for (String numeroDocumento : numeros) {
List<RentaQuinta> historial = porDocumento.getOrDefault(numeroDocumento, List.of());
Agregado a = agregar(historial, filtro.mesCorte());
if (a.referencia() == null) {
continue;
}
TipoPlanilla tipoPlanilla = tipoPlanilla(filtro.tipoPlanilla(), a.referencia());
CalculoRentaQuinta calculo = calcular(a, historial, filtro.periodo(), filtro.mesCorte(), tipoPlanilla,
filtro.tipoPago());
upsertRetencion(numeroDocumento, filtro.periodo(), filtro.mesCorte(), tipoPlanilla, filtro.tipoPago(),
calculo.retencionDelMes());
grabados++;
}
return grabados;
}
/**
* Agregado anual de un trabajador hasta el mes de corte, base de {@link DatosCalculo}: usado
* tanto por la grilla como por el detalle completo, para que ambos calculen exactamente igual.
*
......@@ -189,19 +356,28 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
retenidoAnterior, julioPercibida, diciembrePercibida);
}
private CalculoRentaQuinta calcular(Agregado a, List<RentaQuinta> historial, Integer periodo, int mesCorte) {
private CalculoRentaQuinta calcular(Agregado a, List<RentaQuinta> historial, Integer periodo, int mesCorte,
TipoPlanilla tipoPlanilla, TipoPago tipoPago) {
int mesReferencia = a.referencia().getBoletaMes();
// Todas las filas del documento en el mes de referencia (una persona puede tener varios
// codper/tipo de planilla).
BigDecimal sueldoBrutoReferencia = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getSueldoBruto);
BigDecimal basicoReferencia = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getBasico);
BigDecimal basicoReferencia = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getBasicoGratificacion);
BigDecimal asignacionFamiliarReferencia =
sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getAsignacionFamiliar);
OtrosEmpleadoresAgregado otrosEmpleadores = otrosEmpleadores(a.referencia().getNumeroDocumento(), periodo, mesCorte);
String numeroDocumento = a.referencia().getNumeroDocumento();
OtrosEmpleadoresAgregado otrosEmpleadores = otrosEmpleadores(numeroDocumento, periodo, mesCorte);
ModalidadCalculo modalidad = modalidad(tipoPlanilla, tipoPago, historial, mesReferencia);
BigDecimal proyeccionDocenteHora = modalidad == ModalidadCalculo.DOCENTE_POR_HORA
? proyeccionDocenteHoraRepository.remunBrutaFutura(
numeroDocumento, tipoPlanilla.getCodigo(), periodo, mesCorte)
: BigDecimal.ZERO;
return calculadora.calcular(new DatosCalculo(
periodo, mesCorte, mesReferencia, a.historialReal(), a.planillaCargadaReal(),
periodo, modalidad, proyeccionDocenteHora, mesCorte, mesReferencia, a.historialReal(),
a.planillaCargadaReal(),
sueldoBrutoReferencia, basicoReferencia, asignacionFamiliarReferencia, a.retenidoAnterior(),
a.julioPercibida(), a.diciembrePercibida(),
otrosEmpleadores.importeAnterior(), otrosEmpleadores.importeMesActual(),
......@@ -211,6 +387,27 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
}
/**
* CONDICIÓN MODALIDAD — ADM siempre es monto fijo. En DOC decide el {@code tipoPago} enviado
* por el cliente; si no lo envió, el {@code tipo_pago} de las boletas DOC del mes de
* referencia (una persona puede tener filas ADM y DOC en el mismo mes): si alguna es 1 es
* docente por hora; si no, monto fijo.
*/
private ModalidadCalculo modalidad(TipoPlanilla tipoPlanilla, TipoPago solicitado, List<RentaQuinta> historial,
int mesReferencia) {
if (solicitado != null) {
return ModalidadCalculo.de(tipoPlanilla, solicitado);
}
boolean porHora = historial.stream()
.filter(esMesCorte(mesReferencia))
.filter(r -> TipoPlanilla.deBoleta(r) == TipoPlanilla.DOCENTE)
.map(RentaQuinta::getTipoPago)
.filter(Objects::nonNull)
.map(String::trim)
.anyMatch(TipoPago.POR_HORA.getCodigo()::equals);
return ModalidadCalculo.de(tipoPlanilla, porHora ? TipoPago.POR_HORA : null);
}
/**
* Importe y retención declarados por otros empleadores, separados en tres columnas: meses
* anteriores al de corte, el propio mes de corte, y meses posteriores (proyección) — estos
* últimos ya pueden estar cargados de antemano por el trabajador/empleador y afectan tanto la
......@@ -245,10 +442,15 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
}
/**
* planilla.pl_renta_basico reemplaza el básico de pl_planilla cuando existe un registro para
* (numeroDocumento, anio, mes); si no, se conserva el de pl_planilla. planilla.pl_utilidades
* pasa a ser el campo "utilidades" que antes vivía en la propia renta_quinta (0 si no hay
* declaración para ese mes).
* El básico que compone el sueldo bruto (historial de ingresos, boleta, etc.) es siempre el
* de pl_planilla; pl_renta_basico no lo reemplaza. planilla.pl_utilidades pasa a ser el campo
* "utilidades" que antes vivía en la propia renta_quinta (0 si no hay declaración para ese
* mes).
*
* <p>Además calcula {@code basicoGratificacion}: solo para la gratificación pendiente
* (julio/diciembre) se busca en pl_renta_basico si existe algún registro del trabajador (sin
* filtrar por mes); si existe, se usa solo ese básico; si no, básico + asignación familiar de
* pl_planilla de ese mes.
*/
private void aplicarBasicoYUtilidades(List<RentaQuinta> historial, Integer anio) {
if (historial.isEmpty()) {
......@@ -258,23 +460,52 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
.map(RentaQuinta::getNumeroDocumento)
.distinct()
.toList();
Map<String, Map<Integer, BigDecimal>> basicoPorDocumento = rentaBasicoRepository
Map<String, BigDecimal> basicoPorDocumento = rentaBasicoRepository
.findByAnioAndNumeroDocumentoIn(anio, documentos).stream()
.collect(Collectors.groupingBy(RentaBasico::getNumeroDocumento,
Collectors.toMap(RentaBasico::getMes, RentaBasico::getMontoBasico)));
.collect(Collectors.toMap(RentaBasico::getNumeroDocumento, RentaBasico::getMontoBasico, (a, b) -> a));
Map<String, Map<Integer, BigDecimal>> utilidadPorDocumento = utilidadRepository
.findByAnioAndNumeroDocumentoIn(anio, documentos).stream()
.collect(Collectors.groupingBy(Utilidad::getNumeroDocumento,
Collectors.toMap(Utilidad::getMes, Utilidad::getMontoUtilidad)));
for (RentaQuinta r : historial) {
BigDecimal basico = valorDelMes(basicoPorDocumento, r.getNumeroDocumento(), r.getBoletaMes());
if (basico != null) {
r.setBasico(basico);
}
r.setBasicoGratificacion(baseGratificacion(r, basicoPorDocumento.get(r.getNumeroDocumento())));
r.setUtilidades(orZero(valorDelMes(utilidadPorDocumento, r.getNumeroDocumento(), r.getBoletaMes())));
}
}
/**
* Base de la gratificación pendiente (julio/diciembre) de una boleta, igual para ADM y DOC
* (monto fijo o por hora). {@code basicoRentaBasico} es el monto de pl_renta_basico del
* trabajador (null si no tiene registro).
*/
private BigDecimal baseGratificacion(RentaQuinta boleta, BigDecimal basicoRentaBasico) {
return basicoRentaBasico != null ? basicoRentaBasico
: orZero(boleta.getBasico()).add(orZero(boleta.getAsignacionFamiliar()));
}
/**
* Retenciones grabadas del mes anterior, con clave documento + tipo de planilla
* ({@link #claveGrabada}): la PK de pl_renta_quinta incluye boleta_tipo_personal, así que un
* documento puede tener una fila ADM y otra DOC en el mismo mes.
*/
private Map<String, BigDecimal> obtenerRetencionesMesAnterior(List<String> documentos, Integer anio, int mesCorte) {
if (mesCorte <= 1 || documentos.isEmpty()) {
return Map.of();
}
int mesAnterior = mesCorte - 1;
return rentaQuintaGrabadaRepository.findByAnioAndMesAndNumeroDocumentoIn(anio, mesAnterior, documentos)
.stream()
.collect(Collectors.toMap(
g -> claveGrabada(g.getNumeroDocumento(), g.getBoletaTipoPersonal()),
RentaQuintaGrabada::getRetencionMesActual,
(a, b) -> a
));
}
/** boleta_tipo_personal es char(3): trim por el padding de PostgreSQL. */
private String claveGrabada(String numeroDocumento, String boletaTipoPersonal) {
return numeroDocumento + "|" + (boletaTipoPersonal == null ? "" : boletaTipoPersonal.trim());
}
private BigDecimal valorDelMes(Map<String, Map<Integer, BigDecimal>> porDocumento, String numeroDocumento, Integer mes) {
Map<Integer, BigDecimal> porMes = porDocumento.get(numeroDocumento);
return porMes == null ? null : porMes.get(mes);
......@@ -328,4 +559,5 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
BigDecimal retencionMesActual,
BigDecimal retencionProyeccion
) {}
}
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