Commit 68910855 by Vanesa Puma Yamo

se agregamo modifiaciones

parent 4bf1eb8a
...@@ -68,9 +68,26 @@ internamente `RentaQuintaFiltroDTO` y `DatosCalculo` sigan llamándolos así. ...@@ -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 `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 `Integer` salvo `documentoOApellidos`, que es texto (busca por `numero_documento` o por apellidos
+ nombres, con `LIKE` en mayúsculas). + nombres, con `LIKE` en mayúsculas), y `tipoPlanilla`.
Los filtros de catálogo (`sede`, `tipoPlanilla`, `areaGeneral`, `subArea`, `cargo`) llegan como `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 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. `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.';
# 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 # Endpoints — renta de quinta categoría
Solo documenta parámetros de entrada (query/path). Las respuestas no cambiaron de estructura.
Base: `/api/v1/renta-quinta` Base: `/api/v1/renta-quinta`
`anio` y `mes` son los nombres usados en **todos** los endpoints — antes `/listado` usaba `anio` y `mes` son los nombres usados en **todos** los endpoints.
`periodo`/`mesCorte`, ahora unificado.
## 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` ## GET `/listado`
...@@ -15,8 +29,8 @@ Grilla paginada de retenciones, con cálculo. ...@@ -15,8 +29,8 @@ Grilla paginada de retenciones, con cálculo.
|---|---|---|---| |---|---|---|---|
| `anio` | Integer | sí | Periodo (año) de la grilla | | `anio` | Integer | sí | Periodo (año) de la grilla |
| `mes` | Integer (1-12) | sí | Mes de corte | | `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) | | `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) | | `areaGeneral` | Integer | no | Filtro de catálogo (comparado como texto) |
| `subArea` | 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) | | `cargo` | Integer | no | Filtro de catálogo (comparado como texto) |
...@@ -24,24 +38,75 @@ Grilla paginada de retenciones, con cálculo. ...@@ -24,24 +38,75 @@ Grilla paginada de retenciones, con cálculo.
| `page` | Integer | no (default 0) | Página | | `page` | Integer | no (default 0) | Página |
| `size` | Integer | no (default 20, tope 100) | Tamaño de 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 | | 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, ## POST `/grabar`
liquidación anual, escala por tramos y control de retenciones).
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 | | Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---| |---|---|---|---|
| `numeroDocumento` | String | sí | Documento del trabajador | | `numeroDocumento` | String | sí | Documento del trabajador |
| `anio` | Integer | sí | Periodo (año) | | `anio` | Integer | sí | Periodo (año) |
| `mes` | Integer (1-12) | sí | Mes de corte | | `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` ## GET `/boleta`
...@@ -61,3 +126,16 @@ Meses del trabajador en el periodo. ...@@ -61,3 +126,16 @@ Meses del trabajador en el periodo.
|---|---|---|---| |---|---|---|---|
| `codper` (path) | Integer | sí | Código de personal | | `codper` (path) | Integer | sí | Código de personal |
| `anio` (path) | Integer | sí | Periodo (año) | | `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 ...@@ -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 | | `periodo` | Integer | **Sí** | — | Año a consultar |
| `mesCorte` | Integer | **Sí** | — | 1–12. Mes de corte de la proyección | | `mesCorte` | Integer | **Sí** | — | 1–12. Mes de corte de la proyección |
| `sede` | Integer | No | — | Filtro de catálogo (ver nota abajo) | | `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 | | `areaGeneral` | Integer | No | — | Filtro de catálogo |
| `subArea` | Integer | No | — | Filtro de catálogo | | `subArea` | Integer | No | — | Filtro de catálogo |
| `cargo` | 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 { ...@@ -56,15 +56,14 @@ public class RentaQuintaCalculadora {
if (!mesCorteReal) { if (!mesCorteReal) {
estimadoSueldoBruto = orZero(datos.remuneracionReferencia()); estimadoSueldoBruto = orZero(datos.remuneracionReferencia());
if (datos.mesCorte() == 7 || datos.mesCorte() == 12) { if (datos.mesCorte() == 7 || datos.mesCorte() == 12) {
estimadoGratificacion = estimadoSueldoBruto.multiply(FACTOR_GRATIFICACION); estimadoGratificacion = gratificacion(estimadoSueldoBruto);
} }
} }
BigDecimal estimadoMesCorte = estimadoSueldoBruto.add(estimadoGratificacion); BigDecimal estimadoMesCorte = estimadoSueldoBruto.add(estimadoGratificacion);
// Meses después del mes de corte: el propio mes de corte ya está cubierto por // Meses después del mes de corte: el propio mes de corte ya está cubierto por
// planillaCargada (real) o por el estimado de arriba. // planillaCargada (real) o por el estimado de arriba.
BigDecimal proyeccionSueldoBruto = orZero(datos.remuneracionReferencia()) BigDecimal proyeccionSueldoBruto = proyeccionSueldoBruto(datos);
.multiply(BigDecimal.valueOf(12 - datos.mesCorte()));
boolean julioCubierto = datos.julioPercibida() || (datos.mesCorte() == 7 && !mesCorteReal); boolean julioCubierto = datos.julioPercibida() || (datos.mesCorte() == 7 && !mesCorteReal);
boolean diciembreCubierto = datos.diciembrePercibida() || (datos.mesCorte() == 12 && !mesCorteReal); boolean diciembreCubierto = datos.diciembrePercibida() || (datos.mesCorte() == 12 && !mesCorteReal);
BigDecimal proyeccionGratificaciones = gratificacionesPendientes(datos, julioCubierto, diciembreCubierto); BigDecimal proyeccionGratificaciones = gratificacionesPendientes(datos, julioCubierto, diciembreCubierto);
...@@ -134,10 +133,12 @@ public class RentaQuintaCalculadora { ...@@ -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 * 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). * 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) { private BigDecimal gratificacionesPendientes(DatosCalculo datos, boolean julioCubierto, boolean diciembreCubierto) {
BigDecimal gratificacion = orZero(datos.basicoReferencia().add(datos.asignacionFamiliarReferencia())) BigDecimal gratificacion = gratificacion(orZero(datos.basicoReferencia()));
.multiply(FACTOR_GRATIFICACION);
BigDecimal total = BigDecimal.ZERO; BigDecimal total = BigDecimal.ZERO;
if (!julioCubierto) { if (!julioCubierto) {
total = total.add(gratificacion); total = total.add(gratificacion);
...@@ -148,6 +149,31 @@ public class RentaQuintaCalculadora { ...@@ -148,6 +149,31 @@ public class RentaQuintaCalculadora {
return total; 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) { private BigDecimal impuestoSobre(BigDecimal rentaNeta, BigDecimal valorUit, List<TramoCalculado> tramos) {
BigDecimal impuesto = BigDecimal.ZERO; BigDecimal impuesto = BigDecimal.ZERO;
BigDecimal pisoTramo = BigDecimal.ZERO; BigDecimal pisoTramo = BigDecimal.ZERO;
...@@ -217,9 +243,15 @@ public class RentaQuintaCalculadora { ...@@ -217,9 +243,15 @@ public class RentaQuintaCalculadora {
* {@code mesCorte} cuando ese mes sí está cargado. {@code remuneracionReferencia}, * {@code mesCorte} cuando ese mes sí está cargado. {@code remuneracionReferencia},
* {@code basicoReferencia} y {@code asignacionFamiliarReferencia} vienen de ese mes, y son * {@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. * 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( public record DatosCalculo(
Integer periodo, Integer periodo,
ModalidadCalculo modalidad,
BigDecimal proyeccionDocenteHora,
int mesCorte, int mesCorte,
int mesReferencia, int mesReferencia,
BigDecimal historialReal, 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; package so.planilla_be.rentaquinta.controller;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; 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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; 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.RentaQuintaDetalleCompletoDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleDTO; import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaFiltroDTO; import so.planilla_be.rentaquinta.dto.RentaQuintaFiltroDTO;
...@@ -27,6 +32,16 @@ import java.util.List; ...@@ -27,6 +32,16 @@ import java.util.List;
@RequestMapping("/api/v1/renta-quinta") @RequestMapping("/api/v1/renta-quinta")
public class RentaQuintaController { 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; private final RentaQuintaService service;
public RentaQuintaController(RentaQuintaService service) { public RentaQuintaController(RentaQuintaService service) {
...@@ -39,15 +54,16 @@ public class RentaQuintaController { ...@@ -39,15 +54,16 @@ public class RentaQuintaController {
@RequestParam @NotNull Integer anio, @RequestParam @NotNull Integer anio,
@RequestParam @NotNull @Min(1) @Max(12) Integer mes, @RequestParam @NotNull @Min(1) @Max(12) Integer mes,
@RequestParam(required = false) Integer sede, @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 areaGeneral,
@RequestParam(required = false) Integer subArea, @RequestParam(required = false) Integer subArea,
@RequestParam(required = false) Integer cargo, @RequestParam(required = false) Integer cargo,
@RequestParam(required = false) String documentoOApellidos, @RequestParam(required = false) String documentoOApellidos,
@RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "0") Integer page,
@RequestParam(defaultValue = "20") Integer size) { @RequestParam(defaultValue = "20") Integer size) {
TipoPlanilla tipo = TipoPlanilla.desdeParametro(tipoPlanilla);
return service.buscar(new RentaQuintaFiltroDTO( 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)); documentoOApellidos, page, size));
} }
...@@ -64,8 +80,10 @@ public class RentaQuintaController { ...@@ -64,8 +80,10 @@ public class RentaQuintaController {
public RentaQuintaDetalleCompletoDTO detallado( public RentaQuintaDetalleCompletoDTO detallado(
@RequestParam String numeroDocumento, @RequestParam String numeroDocumento,
@RequestParam Integer anio, @RequestParam Integer anio,
@RequestParam @Min(1) @Max(12) Integer mes) { @RequestParam @Min(1) @Max(12) Integer mes,
return service.obtenerDetalleCompleto(numeroDocumento, anio, 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") @Operation(summary = "Detalle por codper + anio + mes")
...@@ -84,4 +102,38 @@ public class RentaQuintaController { ...@@ -84,4 +102,38 @@ public class RentaQuintaController {
@PathVariable Integer anio) { @PathVariable Integer anio) {
return service.historialAnual(codper, 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; 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. * Filtros de la grilla de retenciones mensuales.
* *
* <p>Todos los filtros son numéricos salvo {@code documentoOApellidos}, que es texto libre * <p>{@code documentoOApellidos} es texto libre (nro de documento o apellidos) y
* (nro de documento o apellidos). Los filtros de catálogo (sede, tipo de planilla, área, * {@code tipoPlanilla} es el código de {@code boleta_tipo_personal} (ADM / DOC), que además de
* subárea, cargo) llegan como Integer y se comparan contra columnas TEXT convirtiendo el * filtrar decide las reglas del cálculo; {@code tipoPago} (sufijo de DOC1 / DOC2: 1 = por hora,
* número a su representación en texto. * 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( public record RentaQuintaFiltroDTO(
Integer periodo, Integer periodo,
Integer mesCorte, Integer mesCorte,
Integer sede, Integer sede,
Integer tipoPlanilla, TipoPlanilla tipoPlanilla,
TipoPago tipoPago,
Integer areaGeneral, Integer areaGeneral,
Integer subArea, Integer subArea,
Integer cargo, Integer cargo,
......
...@@ -21,5 +21,7 @@ public record RentaQuintaResumenDTO( ...@@ -21,5 +21,7 @@ public record RentaQuintaResumenDTO(
BigDecimal rentaBrutaAnual, BigDecimal rentaBrutaAnual,
BigDecimal impuestoAnual, BigDecimal impuestoAnual,
BigDecimal retenidoAnterior, BigDecimal retenidoAnterior,
BigDecimal retencionDelMes BigDecimal retencionDelMes,
BigDecimal retencionAnteriorGrabada, // <--- NUEVO
BigDecimal diferenciaRetencion // <--- NUEVO
) {} ) {}
...@@ -168,6 +168,15 @@ public class RentaQuinta { ...@@ -168,6 +168,15 @@ public class RentaQuinta {
@Transient @Transient
private BigDecimal utilidades; 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 */ /* VACACIONES Y LIQUIDACIÓN */
@Column(name = "cts", precision = 15, scale = 2) @Column(name = "cts", precision = 15, scale = 2)
...@@ -378,6 +387,10 @@ public class RentaQuinta { ...@@ -378,6 +387,10 @@ public class RentaQuinta {
@Column(name = "tipo_labor") @Column(name = "tipo_labor")
private String tipoLabor; 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. */ /** No existen en pl_planilla; se conservan para no romper el contrato del API. */
@Transient @Transient
private Integer campo1; 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 { ...@@ -33,6 +33,12 @@ public class GlobalExceptionHandler {
return build(HttpStatus.CONFLICT, ex.getMessage()); 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). */ /** Filtros fuera de rango (p. ej. mesCorte=13). */
@ExceptionHandler(ConstraintViolationException.class) @ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, Object>> handleValidation(ConstraintViolationException ex) { 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 { ...@@ -24,7 +24,7 @@ public class RentaQuintaMapper {
public RentaQuintaResumenDTO toResumen(RentaQuinta referencia, Integer codper, Integer periodo, Integer mesCorte, public RentaQuintaResumenDTO toResumen(RentaQuinta referencia, Integer codper, Integer periodo, Integer mesCorte,
BigDecimal historialReal, BigDecimal planillaCargada, BigDecimal historialReal, BigDecimal planillaCargada,
BigDecimal retenidoAnterior, CalculoRentaQuinta calculo) { BigDecimal retenidoAnterior, CalculoRentaQuinta calculo,BigDecimal retencionAnteriorGrabada, BigDecimal diferencia) {
return new RentaQuintaResumenDTO( return new RentaQuintaResumenDTO(
codper, codper,
periodo, periodo,
...@@ -33,11 +33,13 @@ public class RentaQuintaMapper { ...@@ -33,11 +33,13 @@ public class RentaQuintaMapper {
mesCorte, mesCorte,
orZero(historialReal), orZero(historialReal),
orZero(planillaCargada), orZero(planillaCargada),
calculo.proyeccionFutura(), calculo.proyeccionFutura().add(calculo.otrosEmpleadoresImporteProyeccion()),
calculo.rentaBrutaAnualSinOtrosEmpleadores(), calculo.rentaBrutaAnualSinOtrosEmpleadores(),
calculo.impuestoAnual(), calculo.impuestoAnual(),
orZero(retenidoAnterior), orZero(retenidoAnterior),
calculo.retencionDelMes()); calculo.retencionDelMes(),
orZero(retencionAnteriorGrabada),
diferencia);
} }
public RentaQuintaHistorialDTO toHistorial(RentaQuinta e) { 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> ...@@ -19,6 +19,7 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
String FILTROS = """ String FILTROS = """
and (:sede is null or trim(r.codlocal) = :sede) and (:sede is null or trim(r.codlocal) = :sede)
and (:tipoPlanilla is null or trim(r.boletaTipoPersonal) = :tipoPlanilla) 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 (:areaGeneral is null or r.areaGeneral = :areaGeneral)
and (:subArea is null or r.subArea = :subArea) and (:subArea is null or r.subArea = :subArea)
and (:cargo is null or r.cargo = :cargo) and (:cargo is null or r.cargo = :cargo)
...@@ -62,6 +63,7 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long> ...@@ -62,6 +63,7 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
@Param("mesCorte") Integer mesCorte, @Param("mesCorte") Integer mesCorte,
@Param("sede") String sede, @Param("sede") String sede,
@Param("tipoPlanilla") String tipoPlanilla, @Param("tipoPlanilla") String tipoPlanilla,
@Param("tipoPago") String tipoPago,
@Param("areaGeneral") String areaGeneral, @Param("areaGeneral") String areaGeneral,
@Param("subArea") String subArea, @Param("subArea") String subArea,
@Param("cargo") String cargo, @Param("cargo") String cargo,
......
package so.planilla_be.rentaquinta.service; package so.planilla_be.rentaquinta.service;
import org.springframework.data.domain.Page; 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.RentaQuintaDetalleCompletoDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleDTO; import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaFiltroDTO; import so.planilla_be.rentaquinta.dto.RentaQuintaFiltroDTO;
...@@ -15,9 +17,15 @@ public interface RentaQuintaService { ...@@ -15,9 +17,15 @@ public interface RentaQuintaService {
RentaQuintaDetalleDTO obtenerPorId(Long id); 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); RentaQuintaDetalleDTO obtenerBoleta(Integer codper, Integer anio, Integer mes);
List<RentaQuintaHistorialDTO> historialAnual(Integer codper, Integer anio); List<RentaQuintaHistorialDTO> historialAnual(Integer codper, Integer anio);
void grabarRetencion(String numeroDocumento, Integer anio, Integer mes, TipoPlanilla tipoPlanilla,
TipoPago tipoPago);
int grabarMasivo(RentaQuintaFiltroDTO filtro);
} }
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