Commit b7ffff5a by Mauro Paolo Josue Zuñiga Mallqui

merge: integrar mzuniga preservando gratificacion por contrato

Conservar reglas de master y fechaInicioContrato, usar el mismo calculo en la matriz mensual y mantener tipoDocumento en el JSON. Separar boletas antes de ajustar utilidades y distinguir DOC1/DOC2 en la retencion anterior.
parents 41b66e29 f34d5c7b
# planilla-be
Backend de planilla (Saco Oliveros). Módulo inicial: **renta de quinta categoría** — consulta
de retenciones mensuales y cálculo de la proyección anual por trabajador.
## Stack
- Java 25 (`pom.xml`), Spring Boot 4.1.1, empaquetado **war** (`ServletInitializer`)
- Spring Data JPA (Hibernate 7) + PostgreSQL
- springdoc-openapi → Swagger UI en `/docs`
- Lombok (`@Getter/@Setter` en la entity)
- **No se escriben tests.** No sugerir ni generar tests salvo pedido explícito.
## Configuración
`src/main/resources/application.yml` **no está versionado** (contiene credenciales). Copiar
`application.example.yml` y completar:
```bash
cp src/main/resources/application.example.yml src/main/resources/application.yml
export DB_PASSWORD='...'
```
Datos del entorno de pruebas: PostgreSQL, base `prueba`, usuario `soporte`, esquema **`planilla`**
(no `public`).
`ddl-auto: validate` — la tabla ya existe y la carga la hace un proceso batch/ETL externo. Nunca
poner `update` ni `create`.
## Comandos
```bash
./mvnw spring-boot:run # levanta en :8080
./mvnw package # genera el war
```
## Estructura
```
so.planilla_be
├── config/OpenApiConfig
└── rentaquinta/
├── entity/RentaQuinta mapeo 1:1 de las 115 columnas de planilla.renta_quinta
├── repository/ RentaQuintaRepository + RentaQuintaResumenProjection
├── dto/ Resumen (grilla), Detalle (boleta), Historial, Filtro
├── calculo/ RentaQuintaCalculadora, UitProperties, CalculoRentaQuinta
├── mapper/RentaQuintaMapper manual (sin MapStruct)
├── service/ + service/impl/
├── controller/
└── exception/ excepción de negocio + GlobalExceptionHandler
```
## Endpoints
| Método | Ruta | Descripción |
|---|---|---|
| GET | `/api/v1/renta-quinta/listado` | Grilla paginada de retenciones, con cálculo |
| GET | `/api/v1/renta-quinta/{id}` | Boleta completa por `id_renta_quinta` |
| GET | `/api/v1/renta-quinta/detallado` | Detalle completo por `numeroDocumento` + `anio` + `mes` |
| GET | `/api/v1/renta-quinta/boleta` | Boleta por `codper` + `anio` + `mes` |
| GET | `/api/v1/renta-quinta/historial/{codper}/{anio}` | Meses del trabajador en el periodo |
`anio` y `mes` son los nombres de parámetro usados en **todos** los endpoints (`listado`,
`detallado`, `boleta`, `historial`) — no usar `periodo`/`mesCorte` en la firma pública aunque
internamente `RentaQuintaFiltroDTO` y `DatosCalculo` sigan llamándolos así.
### Mantenimiento de proyección docente por hora
`POST /api/v1/proyeccion-docente-hora` (paquete `proyecciondocentehora`): recibe una lista de filas
`{proyeccionAnio, proyeccionMes, numeroDocumento, boletaTipoPersonal (DOC|GRA), remunBrutaFutura,
gratificacion}` y las graba **solo** en `planilla.pl_proyeccion_renta_docentehora`. La tabla no tiene
unique salvo el PK identity (`id_proyeccion`, ALWAYS): la clave lógica es documento + tipo + año + mes,
y si existe se actualiza en vez de duplicar. `DELETE /api/v1/proyeccion-docente-hora/{idProyeccion}`
elimina por PK (204; 404 si no existe). `GET /api/v1/proyeccion-docente-hora/{numeroDocumento}/{anio}`
lista las filas del docente con `idProyeccion` y `nombreCompleto` (de `nuevo.personal`, temporal). Todo o nada (una transacción). Importes nulos → 0.
### Filtros de la grilla
`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), 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_DICIEMBRE_PROPORCIONAL` | igual que ADM, pero el sueldo bruto de diciembre entra ÷ 30 × 16 (proyección y estimado del mes de corte 12) y en su gratificación diciembre vale 16 días (5 meses + 16 días, ver reglas de dominio). Una boleta real de diciembre no se toca |
| `DOC1` | `DOC` | `1` | `DOCENTE_POR_HORA` | De `planilla.pl_proyeccion_renta_docentehora` (mismo documento, `boleta_tipo_personal='DOC'`, `proyeccion_anio` = año): `remun_bruta_futura` del mes consultado = sueldo bruto de la planilla en curso (reemplaza al de la boleta); suma de `remun_bruta_futura` de los meses **posteriores** = proyección del sueldo bruto. Sin fila = 0. La gratificación de julio/diciembre pendiente es el campo `gratificacion` de las filas `boleta_tipo_personal='GRA'` en `proyeccion_mes` 7 / 12 (sin fórmula × 1.09; sin fila = 0) y va en gratificación ordinaria de la proyección |
En DOC1 cambian la proyección del sueldo y las 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.
> Pendiente: en los datos actuales esas columnas guardan texto (`sede='BARRANCO'`,
> `boleta_tipo_personal='ADM'`) y `codlocal` viene vacío, así que un filtro numérico no matchea
> nada. Cuando existan catálogos con id, o si los dropdowns pasan a mandar el texto, cambiar el
> tipo en `RentaQuintaFiltroDTO` y quitar `codigo()`.
`page` por defecto 0, `size` 20 y **tope 100**.
## Reglas de dominio
### Integración de master con mzuniga
- El cálculo de gratificación por contrato de master tiene prioridad. La matriz mensual usa
`RentaQuintaCalculadora.gratificacionDelMes` con los mismos `DatosCalculo` del detalle.
- El listado agrega datos organizativos desde `nuevo.personal`, `nuevo.area`, `nuevo.subarea`
y `nuevo.cargo`. Los filtros `tieneVariacion` y `tieneRetencion` son Boolean opcionales y se
aplican al cálculo antes de paginar; pueden requerir calcular todos los candidatos.
- `GET /api/v1/filtros/periodos` requiere ejecutar `db/crear-pl-periodo.sql`; no se ejecuta
automáticamente. Para años nuevos, mantener el catálogo de periodos.
- El mapeo de boletas usa las columnas de producción `tipo_planilla`, `neto_pagado` y
`diferencia`. El detalle conserva el nombre JSON `tipoDocumento` por compatibilidad.
- Separar las entidades de boleta del contexto JPA antes de ajustar utilidades para el cálculo;
la escritura de retenciones solo debe afectar a `pl_renta_quinta`.
- La comparación con el mes anterior distingue DOC1 de DOC2 mediante `tipo_pago`.
- `/listado/datatables` sigue pendiente: su documento describe una propuesta, no una ruta
publicada. `/listado` conserva la respuesta Page y los parámetros `page` y `size`.
### Agregación de la grilla
Una fila = un trabajador (no un mes). Se agrupa por `numero_documento` + nombres, **no por
`codper`**: en los datos reales `codper` viene 0/1/2 y una misma persona puede tener varios, lo
que la duplicaba en la grilla.
- `historialReal` / `retenidoAnterior`: suma de los meses **anteriores** al de corte
- `planillaCargada` / retención registrada: el **mes de corte**
### Cálculo de quinta categoría
`RentaQuintaCalculadora` (art. 40 y 53 de la LIR; art. 40 del Reglamento):
1. Proyección = remuneración del mes de corte × meses restantes + gratificaciones de julio y
diciembre aún no percibidas, cada una `(básico + asignación familiar) × 1.09`
- **ADM**: la gratificación se prorratea por `fecha_inicio_contrato` (boleta ADM del mes de
referencia, **obligatoria**: sin fecha la gratificación es 0):
`((base/6 × meses) + (base/6/30 × días)) × 1.09`. Contrato anterior al semestre
o ingreso en enero → semestre completo (6 meses); posterior → 0. Ingreso en otro mes del
semestre: meses completos después del mes de ingreso (día 1 = mes completo) + días del mes
de ingreso (días según `planilla.pl_calendario` − día + 1). Sin corte de 16 días. DOC1 no cambia.
Ingreso en junio después del día 1 (ADM y DOC2): gratificación de julio = 0 y los días de
junio se suman a la de diciembre (semestre completo + días de junio).
- **DOC2**: igual que ADM (fecha de su boleta DOC), pero en la gratificación de diciembre,
diciembre cuenta 16 días: semestre completo = `((base/6 × 5) + (base/6/30 × 16)) × 1.09`. La tabla la crea
`docs/create-calendario.sql` (2026): correrlo antes de desplegar o `validate` no arranca.
Excepción: si el DNI está en `planilla.pl_docente_grati_diciembre_completa` para el año
(PK `dni + anio`), su gratificación de diciembre DOC2 va con diciembre completo (30 días,
igual que ADM). Solo la gratificación: el sueldo bruto de diciembre sigue ÷ 30 × 16. La crea
`docs/create-docente-grati-diciembre-completa.sql` (correr antes de desplegar). Mantenimiento
en `/api/v1/docente-grati-diciembre-completa` (paquete `docentegratidiciembre`, ver
`docs/api-docente-grati-diciembre-completa.md`).
2. Renta bruta anual = historial + planilla cargada + proyección
3. Renta neta = bruta − **7 UIT**
4. Impuesto anual = tramos progresivos 8% / 14% / 17% / 20% / 30% (5, 20, 35 y 45 UIT)
5. Retención del mes = (impuesto anual − retenido previo) ÷ divisor del mes
(ene-mar 12, abr 9, may-jul 8, ago 5, set-nov 4, dic 1); **nunca negativa**
La **UIT es configurable por año** en `renta-quinta.uit.valores` — se actualiza cada año, no
hardcodear. **Falta cargar el valor 2026**: hoy cae en `por-defecto`, y de ese número dependen
todos los importes del periodo.
## Notas de la tabla `planilla.renta_quinta`
- Snapshot mensual por trabajador; el API es **solo lectura**
- Las columnas `char(n)` necesitan `columnDefinition = "bpchar"` o `ddl-auto: validate` falla
(`found [bpchar], but expecting [varchar]`)
- No hay índice único confirmado sobre `codper + anio + mes + tipo_personal`; validar con el
equipo antes de crearlo en producción
## Manejo de errores
`GlobalExceptionHandler` devuelve `{timestamp, status, message}` para 404 de negocio, filtros
fuera de rango, parámetros obligatorios ausentes y filtros numéricos con valor no numérico. No
exponer stack traces en las respuestas.
## Fuera de alcance por ahora
- Escritura de boletas desde el API (la carga es batch/ETL)
- Autenticación/autorización (se integrará con el esquema de Trismegisto/Saco Oliveros)
- Exportación a Excel y "calcular masivo" de la pantalla
-- Catálogo del filtro Periodo de renta de quinta categoría.
BEGIN;
CREATE TABLE IF NOT EXISTS planilla.pl_periodo (
id_periodo INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
mes INTEGER NOT NULL,
anio INTEGER NOT NULL,
CONSTRAINT ck_pl_periodo_mes CHECK (mes BETWEEN 1 AND 12),
CONSTRAINT ck_pl_periodo_anio CHECK (anio BETWEEN 1000 AND 9999),
CONSTRAINT uq_pl_periodo_mes_anio UNIQUE (mes, anio)
);
-- Incluir los doce meses permite consultar también los meses proyectados.
INSERT INTO planilla.pl_periodo (mes, anio)
SELECT meses.mes, periodos.anio
FROM (
SELECT DISTINCT boleta_anio AS anio
FROM planilla.pl_planilla
WHERE boleta_anio BETWEEN 1000 AND 9999
) periodos
CROSS JOIN generate_series(1, 12) AS meses(mes)
ORDER BY periodos.anio, meses.mes
ON CONFLICT (mes, anio) DO NOTHING;
COMMIT;
# Integración de renta quinta con jQuery DataTables
> Propuesta pendiente de implementación: esta rama incluye el DTO de respuesta, pero aún no
> publica `/api/v1/renta-quinta/listado/datatables`. La ruta disponible es `/listado`, que
> recibe `page` y `size` y devuelve una página Spring. El ejemplo siguiente requiere implementar
> primero el adaptador DataTables.
## Endpoint
`GET /api/v1/renta-quinta/listado/datatables`
Usar `serverSide: true`: cada cambio de página consulta al backend y solo calcula las filas solicitadas. El endpoint anterior `/listado` sigue devolviendo una página Spring y conserva su contrato.
## Parámetros GET
| Parámetro | Descripción |
|---|---|
| `anio` | Obligatorio, por ejemplo `2026` |
| `mes` | Obligatorio, de 1 a 12 |
| `tipoPlanilla` | Opcional: `ADM`, `DOC1`, `DOC2` |
| `draw` | Contador de solicitud generado por DataTables, entero >= 0 |
| `start` | Posición inicial desde 0; debe ser múltiplo de `length` |
| `length` | Tamaño de página, de 1 a 100; por defecto 20 |
| `search[value]` | Búsqueda por documento o apellidos y nombres |
| `sede`, `areaGeneral`, `subArea`, `cargo` | Filtros numéricos opcionales; omitir cuando no se seleccionen |
No enviar `page` ni `size` a esta ruta. No ofrecer la opción «Todos» (`length=-1`). No se admite ordenamiento dinámico ni búsqueda por columna: configurar `ordering: false`. El orden es por apellido paterno, materno y nombres.
Primera página:
```text
/api/v1/renta-quinta/listado/datatables?anio=2026&mes=8&tipoPlanilla=ADM&draw=1&start=0&length=20
```
Segunda página: `start=20&length=20`. La siguiente solicitud usa el `draw` que genera DataTables.
## Respuesta
```json
{
"draw": 1,
"recordsTotal": 350,
"recordsFiltered": 12,
"data": []
}
```
Los números son ilustrativos. `data` contiene las filas de la página con los campos de `RentaQuintaResumenDTO`. `recordsTotal` cuenta los trabajadores dentro del contexto seleccionado (año, mes, tipo de planilla y filtros de catálogo), antes de aplicar la búsqueda de texto. `recordsFiltered` cuenta todos los resultados de esa búsqueda, no solo los de la página. Sin búsqueda ambos totales coinciden. Solo se carga el historial y se calcula la página; la búsqueda SQL y los conteos todavía pueden necesitar recorrer más registros.
## JavaScript
El ejemplo supone que existe una tabla `#tablaRenta` con ocho encabezados y controles `#anio`, `#mes`, `#tipoPlanilla`, `#sede`, `#areaGeneral`, `#subArea`, `#cargo`. Adaptar los selectores a la pantalla. Si frontend y backend tienen distinta dirección, cambiar `url` y configurar el acceso correspondiente.
```javascript
const tabla = $('#tablaRenta').DataTable({
processing: true,
serverSide: true,
ordering: false,
pageLength: 20,
lengthMenu: [10, 20, 50, 100],
searchDelay: 500,
ajax: {
url: '/api/v1/renta-quinta/listado/datatables',
type: 'GET',
data: function (d) {
// Conservar draw, start, length y search que genera DataTables.
d.anio = $('#anio').val();
d.mes = $('#mes').val();
for (const campo of ['tipoPlanilla', 'sede', 'areaGeneral', 'subArea', 'cargo']) {
const valor = $('#' + campo).val();
if (valor !== null && valor !== undefined && valor !== '') {
d[campo] = valor;
} else {
delete d[campo];
}
}
},
dataSrc: 'data'
},
columns: [
{ data: 'numeroDocumento', render: $.fn.dataTable.render.text() },
{ data: 'nombreCompleto', render: $.fn.dataTable.render.text() },
{ data: 'historialReal' },
{ data: 'planillaCargada' },
{ data: 'proyeccionFutura' },
{ data: 'rentaBrutaAnual' },
{ data: 'impuestoAnual' },
{ data: 'retencionDelMes' }
]
});
$('#anio, #mes, #tipoPlanilla, #sede, #areaGeneral, #subArea, #cargo')
.on('change', function () {
tabla.ajax.reload(null, true); // Volver a la primera página al cambiar filtros.
});
```
La búsqueda global de DataTables envía `search[value]` automáticamente. No usar `tabla.clear().rows.add(...)` para cargar todo el listado ni reemplazar el objeto `d` por uno que pierda los parámetros de DataTables. Los valores inválidos de paginación responden 400.
## Validación manual
1. Abrir la pantalla: verificar en Network `start=0`, `length=20` y hasta 20 filas en `data`.
2. Ir a la segunda página: verificar `start=20` y una nueva solicitud.
3. Buscar un documento: verificar `search[value]`, las filas y `recordsFiltered`.
4. Cambiar mes o tipo: verificar que vuelva a `start=0`.
5. Revisar el log `Listado renta quinta` para comparar tiempos por etapa. Una página menor reduce la carga del historial y del cálculo, pero no garantiza que los conteos sean más rápidos.
Referencia del protocolo: [DataTables: Server-side processing](https://datatables.net/manual/server-side).
...@@ -11,4 +11,7 @@ public interface RetencionOtrosEmpleadoresRepository ...@@ -11,4 +11,7 @@ public interface RetencionOtrosEmpleadoresRepository
List<RetencionOtrosEmpleadores> findByDocumentoTrabajadorAndAnioOrderByMes( List<RetencionOtrosEmpleadores> findByDocumentoTrabajadorAndAnioOrderByMes(
String documentoTrabajador, Integer anio); String documentoTrabajador, Integer anio);
/** Declaraciones del año de varios trabajadores en una sola consulta (grilla / grabado masivo). */
List<RetencionOtrosEmpleadores> findByAnioAndDocumentoTrabajadorIn(Integer anio, List<String> documentosTrabajador);
} }
...@@ -15,6 +15,10 @@ public interface ProyeccionDocenteHoraRepository extends JpaRepository<Proyeccio ...@@ -15,6 +15,10 @@ public interface ProyeccionDocenteHoraRepository extends JpaRepository<Proyeccio
List<ProyeccionDocenteHora> findByNumeroDocumentoAndProyeccionAnioOrderByProyeccionMesAscBoletaTipoPersonalAsc( List<ProyeccionDocenteHora> findByNumeroDocumentoAndProyeccionAnioOrderByProyeccionMesAscBoletaTipoPersonalAsc(
String numeroDocumento, Integer proyeccionAnio); String numeroDocumento, Integer proyeccionAnio);
/** Filas del año de varios docentes en una sola consulta (grilla / grabado masivo). */
List<ProyeccionDocenteHora> findByProyeccionAnioAndNumeroDocumentoIn(Integer proyeccionAnio,
List<String> numerosDocumento);
/** Clave lógica (la tabla solo tiene PK sobre id_proyeccion). */ /** Clave lógica (la tabla solo tiene PK sobre id_proyeccion). */
Optional<ProyeccionDocenteHora> findFirstByNumeroDocumentoAndBoletaTipoPersonalAndProyeccionAnioAndProyeccionMes( Optional<ProyeccionDocenteHora> findFirstByNumeroDocumentoAndBoletaTipoPersonalAndProyeccionAnioAndProyeccionMes(
String numeroDocumento, String boletaTipoPersonal, Integer proyeccionAnio, Integer proyeccionMes); String numeroDocumento, String boletaTipoPersonal, Integer proyeccionAnio, Integer proyeccionMes);
......
...@@ -172,6 +172,16 @@ public class RentaQuintaCalculadora { ...@@ -172,6 +172,16 @@ public class RentaQuintaCalculadora {
return total; return total;
} }
/** Importe por mes para la matriz, con las mismas reglas de contrato y modalidad del cálculo. */
public BigDecimal gratificacionDelMes(DatosCalculo datos, int mes) {
if (mes != 7 && mes != 12) {
return BigDecimal.ZERO;
}
return escala(datos.modalidad() == ModalidadCalculo.DOCENTE_POR_HORA
? gratificacionDocenteHora(datos, mes)
: gratificacionProporcional(datos, orZero(datos.basicoReferencia()), mes));
}
/** /**
* CONDICIÓN FECHA DE CONTRATO (ADM y DOC2) — gratificación de julio (semestre enero-junio) o * CONDICIÓN FECHA DE CONTRATO (ADM y DOC2) — gratificación de julio (semestre enero-junio) o
* de diciembre (julio-diciembre) según cuánto del semestre cubre el contrato: * de diciembre (julio-diciembre) según cuánto del semestre cubre el contrato:
......
...@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.RequestParam; ...@@ -8,6 +8,7 @@ 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.dto.FiltroItemDTO; import so.planilla_be.rentaquinta.dto.FiltroItemDTO;
import so.planilla_be.rentaquinta.dto.TipoFiltro; import so.planilla_be.rentaquinta.dto.TipoFiltro;
import so.planilla_be.rentaquinta.dto.PeriodoFiltroDTO;
import so.planilla_be.rentaquinta.service.FiltrosService; import so.planilla_be.rentaquinta.service.FiltrosService;
import java.util.List; import java.util.List;
...@@ -28,4 +29,10 @@ public class FiltrosController { ...@@ -28,4 +29,10 @@ public class FiltrosController {
public List<FiltroItemDTO> listar(@RequestParam TipoFiltro tipo) { public List<FiltroItemDTO> listar(@RequestParam TipoFiltro tipo) {
return service.listar(tipo); return service.listar(tipo);
} }
@Operation(summary = "Lista periodos registrados en pl_periodo, del más reciente al más antiguo")
@GetMapping("/periodos")
public List<PeriodoFiltroDTO> listarPeriodos() {
return service.listarPeriodos();
}
} }
...@@ -35,11 +35,12 @@ public class RentaQuintaController { ...@@ -35,11 +35,12 @@ public class RentaQuintaController {
/** Parámetro que envía el cliente (C#) para elegir las reglas del cálculo por tipo de planilla. */ /** 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 " 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 " + "costo por hora (proyección desde pl_proyeccion_renta_docentehora); DOC2 = docente monto fijo "
+ "(mismo cálculo que ADM, con sueldo bruto y gratificación de diciembre ÷ 30 × 16). Filtra por boleta_tipo_personal y tipo_pago. Si no se envía, se " + "(gratificación por fecha de contrato, diciembre de 16 días salvo excepción registrada; "
+ "sueldo bruto de diciembre ÷ 30 × 16). Filtra por boleta_tipo_personal y tipo_pago. Si no se envía, se "
+ "toma de la boleta del trabajador."; + "toma de la boleta del trabajador.";
private static final String TIPO_PLANILLA_GRABAR_DOC = "Obligatorio. ADM, DOC1 (costo por hora) o DOC2 " 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)."; + "(monto fijo). En pl_renta_quinta se graba boleta_tipo_personal (ADM/DOC) y tipo_pago (1/2, vacío en ADM).";
private final RentaQuintaService service; private final RentaQuintaService service;
...@@ -58,13 +59,15 @@ public class RentaQuintaController { ...@@ -58,13 +59,15 @@ public class RentaQuintaController {
@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) Boolean tieneVariacion,
@RequestParam(required = false) Boolean tieneRetencion,
@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); TipoPlanilla tipo = TipoPlanilla.desdeParametro(tipoPlanilla);
return service.buscar(new RentaQuintaFiltroDTO( return service.buscar(new RentaQuintaFiltroDTO(
anio, mes, sede, tipo, TipoPago.desdeParametro(tipoPlanilla), areaGeneral, subArea, cargo, anio, mes, sede, tipo, TipoPago.desdeParametro(tipoPlanilla), areaGeneral, subArea, cargo,
documentoOApellidos, page, size)); tieneVariacion, tieneRetencion, documentoOApellidos, page, size));
} }
@Operation(summary = "Detalle completo por id_renta_quinta") @Operation(summary = "Detalle completo por id_renta_quinta")
...@@ -74,8 +77,8 @@ public class RentaQuintaController { ...@@ -74,8 +77,8 @@ public class RentaQuintaController {
} }
@Operation(summary = "Detalle completo del cálculo por número de documento + año + mes " @Operation(summary = "Detalle completo del cálculo por número de documento + año + mes "
+ "(boleta del mes + puntos 1 a 5: historial de ingresos, parámetros SUNAT, " + "(boleta del mes + puntos 1 a 6: historial de ingresos, parámetros SUNAT, "
+ "liquidación anual, escala por tramos y control de retenciones)") + "liquidación anual, escala por tramos, control de retenciones y matriz mensual)")
@GetMapping("/detallado") @GetMapping("/detallado")
public RentaQuintaDetalleCompletoDTO detallado( public RentaQuintaDetalleCompletoDTO detallado(
@RequestParam String numeroDocumento, @RequestParam String numeroDocumento,
...@@ -127,11 +130,13 @@ public class RentaQuintaController { ...@@ -127,11 +130,13 @@ public class RentaQuintaController {
@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) Boolean tieneVariacion,
@RequestParam(required = false) Boolean tieneRetencion,
@RequestParam(required = false) String documentoOApellidos) { @RequestParam(required = false) String documentoOApellidos) {
TipoPlanilla tipo = TipoPlanilla.obligatorio(tipoPlanilla); TipoPlanilla tipo = TipoPlanilla.obligatorio(tipoPlanilla);
int grabados = service.grabarMasivo(new RentaQuintaFiltroDTO( int grabados = service.grabarMasivo(new RentaQuintaFiltroDTO(
anio, mes, sede, tipo, TipoPago.desdeParametro(tipoPlanilla), areaGeneral, subArea, cargo, anio, mes, sede, tipo, TipoPago.desdeParametro(tipoPlanilla), areaGeneral, subArea, cargo,
documentoOApellidos, null, null)); tieneVariacion, tieneRetencion, documentoOApellidos, null, null));
return new GrabadoMasivoResultado(grabados); return new GrabadoMasivoResultado(grabados);
} }
......
package so.planilla_be.rentaquinta.dto;
import java.util.List;
public record DataTableResponse<T>(int draw, long recordsTotal, long recordsFiltered, List<T> data) {}
package so.planilla_be.rentaquinta.dto;
public record PeriodoFiltroDTO(Integer idPeriodo, Integer mes, Integer anio) {}
...@@ -2,12 +2,13 @@ package so.planilla_be.rentaquinta.dto; ...@@ -2,12 +2,13 @@ package so.planilla_be.rentaquinta.dto;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* Detalle completo de renta de quinta de un trabajador en un mes (puntos 1 a 5 del Excel de * Detalle completo de renta de quinta de un trabajador en un mes (puntos 1 a 6 del Excel de
* referencia "Ver Historial - Cálculo"), en un solo JSON: la boleta del mes, historial de * referencia "Ver Historial - Cálculo"), en un solo JSON: la boleta del mes, historial de
* ingresos, parámetros SUNAT, liquidación del impuesto anual proyectado, escala por tramos y * ingresos, parámetros SUNAT, liquidación del impuesto anual proyectado, escala por tramos y
* control de retenciones. * control de retenciones y matriz mensual de ingresos.
*/ */
public record RentaQuintaDetalleCompletoDTO( public record RentaQuintaDetalleCompletoDTO(
RentaQuintaDetalleDTO boleta, RentaQuintaDetalleDTO boleta,
...@@ -15,9 +16,14 @@ public record RentaQuintaDetalleCompletoDTO( ...@@ -15,9 +16,14 @@ public record RentaQuintaDetalleCompletoDTO(
ParametrosSistemaDTO parametrosSistema, ParametrosSistemaDTO parametrosSistema,
LiquidacionImpuestoAnualDTO liquidacionImpuestoAnual, LiquidacionImpuestoAnualDTO liquidacionImpuestoAnual,
EscalaImpositivaDTO escalaImpositiva, EscalaImpositivaDTO escalaImpositiva,
ControlRetencionesDTO controlRetenciones ControlRetencionesDTO controlRetenciones,
List<MesIngresosDTO> matrizMensualIngresos
) { ) {
/** Punto 6: importes por concepto y mes; retención grabada o, en su ausencia, de la boleta. */
public record MesIngresosDTO(int mes, boolean proyectado, Map<String, BigDecimal> conceptos,
BigDecimal totalBruto, BigDecimal retencionEfectuada) {}
/** Punto 1: registro e historial de ingresos consolidados, en 3 columnas de periodo. */ /** Punto 1: registro e historial de ingresos consolidados, en 3 columnas de periodo. */
public record RegistroHistorialIngresosDTO( public record RegistroHistorialIngresosDTO(
BloqueIngresosDTO pasadoRealRealizado, BloqueIngresosDTO pasadoRealRealizado,
......
...@@ -10,9 +10,8 @@ import so.planilla_be.rentaquinta.calculo.TipoPlanilla; ...@@ -10,9 +10,8 @@ import so.planilla_be.rentaquinta.calculo.TipoPlanilla;
* {@code tipoPlanilla} es el código de {@code boleta_tipo_personal} (ADM / DOC), que además de * {@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, * 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 * 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 * llegan como Integer. Área, subárea y cargo se resuelven desde nuevo.personal y sus catálogos
* el número a su * foráneos. Los estados de variación y retención se evalúan sobre el cálculo antes de paginar.
* representación en texto.
*/ */
public record RentaQuintaFiltroDTO( public record RentaQuintaFiltroDTO(
Integer periodo, Integer periodo,
...@@ -23,6 +22,8 @@ public record RentaQuintaFiltroDTO( ...@@ -23,6 +22,8 @@ public record RentaQuintaFiltroDTO(
Integer areaGeneral, Integer areaGeneral,
Integer subArea, Integer subArea,
Integer cargo, Integer cargo,
Boolean tieneVariacion,
Boolean tieneRetencion,
String documentoOApellidos, String documentoOApellidos,
Integer page, Integer page,
Integer size Integer size
......
...@@ -16,6 +16,13 @@ public record RentaQuintaResumenDTO( ...@@ -16,6 +16,13 @@ public record RentaQuintaResumenDTO(
String numeroDocumento, String numeroDocumento,
String nombreCompleto, String nombreCompleto,
LocalDate fechaInicioContrato, LocalDate fechaInicioContrato,
Integer idUnidadOrganizativa,
Integer idArea,
String area,
Integer idSubArea,
String subArea,
Integer idCargo,
String cargo,
Integer mesProceso, Integer mesProceso,
BigDecimal historialReal, BigDecimal historialReal,
BigDecimal planillaCargada, BigDecimal planillaCargada,
......
...@@ -46,8 +46,8 @@ public class RentaQuinta { ...@@ -46,8 +46,8 @@ public class RentaQuinta {
@Column(name = "unidad_organizativa") @Column(name = "unidad_organizativa")
private Integer unidadOrganizativa; private Integer unidadOrganizativa;
@Column(name = "tipo_documento", length = 2, columnDefinition = "bpchar") @Column(name = "tipo_planilla", length = 2, columnDefinition = "bpchar")
private String tipoDocumento; private String tipoPlanilla;
@Column(name = "apellido_paterno") @Column(name = "apellido_paterno")
private String apellidoPaterno; private String apellidoPaterno;
...@@ -378,10 +378,10 @@ public class RentaQuinta { ...@@ -378,10 +378,10 @@ public class RentaQuinta {
@Column(name = "faltas") @Column(name = "faltas")
private Integer faltas; private Integer faltas;
@Column(name = "pago", precision = 15, scale = 2) @Column(name = "neto_pagado", precision = 15, scale = 2)
private BigDecimal pago; private BigDecimal pago;
@Column(name = "pendiente", precision = 15, scale = 2) @Column(name = "diferencia", precision = 15, scale = 2)
private BigDecimal pendiente; private BigDecimal pendiente;
@Column(name = "tipo_labor") @Column(name = "tipo_labor")
......
...@@ -14,9 +14,13 @@ import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.RegistroHist ...@@ -14,9 +14,13 @@ import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.RegistroHist
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.TramoDTO; import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.TramoDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaResumenDTO; import so.planilla_be.rentaquinta.dto.RentaQuintaResumenDTO;
import so.planilla_be.rentaquinta.entity.RentaQuinta; import so.planilla_be.rentaquinta.entity.RentaQuinta;
import so.planilla_be.rentaquinta.repository.OrganizacionPersonalRepository.Organizacion;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
@Component @Component
...@@ -24,13 +28,21 @@ public class RentaQuintaMapper { ...@@ -24,13 +28,21 @@ 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 retencionAnteriorGrabada, BigDecimal diferencia) { BigDecimal retenidoAnterior, CalculoRentaQuinta calculo,BigDecimal retencionAnteriorGrabada, BigDecimal diferencia,
Organizacion organizacion) {
return new RentaQuintaResumenDTO( return new RentaQuintaResumenDTO(
codper, codper,
periodo, periodo,
referencia.getNumeroDocumento(), referencia.getNumeroDocumento(),
nombreCompleto(referencia.getApellidoPaterno(), referencia.getApellidoMaterno(), referencia.getNombres()), nombreCompleto(referencia.getApellidoPaterno(), referencia.getApellidoMaterno(), referencia.getNombres()),
referencia.getFechaInicioContrato(), referencia.getFechaInicioContrato(),
organizacion == null ? null : organizacion.idUnidadOrganizativa(),
organizacion == null ? null : organizacion.idArea(),
organizacion == null ? null : organizacion.area(),
organizacion == null ? null : organizacion.idSubArea(),
organizacion == null ? null : organizacion.subArea(),
organizacion == null ? null : organizacion.idCargo(),
organizacion == null ? null : organizacion.cargo(),
mesCorte, mesCorte,
orZero(historialReal), orZero(historialReal),
orZero(planillaCargada), orZero(planillaCargada),
...@@ -76,11 +88,12 @@ public class RentaQuintaMapper { ...@@ -76,11 +88,12 @@ public class RentaQuintaMapper {
} }
/** /**
* Imagen detallada de la boleta (puntos 1 a 5 del Excel "Ver Historial - Cálculo"): historial * Imagen detallada de la boleta (puntos 1 a 6 del Excel "Ver Historial - Cálculo"): historial
* de ingresos por columna de periodo, parámetros SUNAT, liquidación anual, escala por tramos * de ingresos por columna de periodo, parámetros SUNAT, liquidación anual, escala por tramos
* y control de retenciones. * y control de retenciones.
*/ */
public RentaQuintaDetalleCompletoDTO toDetalleCompleto(RentaQuinta boleta, List<RentaQuinta> historialAnio, int mesCorte, CalculoRentaQuinta calculo) { public RentaQuintaDetalleCompletoDTO toDetalleCompleto(RentaQuinta boleta, List<RentaQuinta> historialAnio, int mesCorte, CalculoRentaQuinta calculo,
List<RentaQuintaDetalleCompletoDTO.MesIngresosDTO> matrizMensual) {
// El total del bloque "pasado" usa el mismo historialReal que ya entra a la base // El total del bloque "pasado" usa el mismo historialReal que ya entra a la base
// imponible en RentaQuintaCalculadora, en vez de recalcularlo aparte: así el punto 1 // imponible en RentaQuintaCalculadora, en vez de recalcularlo aparte: así el punto 1
// siempre queda consistente con lo que efectivamente se usó para el impuesto anual. // siempre queda consistente con lo que efectivamente se usó para el impuesto anual.
...@@ -165,7 +178,42 @@ public class RentaQuintaMapper { ...@@ -165,7 +178,42 @@ public class RentaQuintaMapper {
retencionOtrosEmpleadoresTotal, retencionOtrosEmpleadoresTotal,
saldoPendiente, calculo.divisorMes(), calculo.retencionDelMes()); saldoPendiente, calculo.divisorMes(), calculo.retencionDelMes());
return new RentaQuintaDetalleCompletoDTO(toDetalle(boleta), registro, parametros, liquidacion, escala, control); return new RentaQuintaDetalleCompletoDTO(toDetalle(boleta), registro, parametros, liquidacion, escala, control, matrizMensual);
}
public Map<String, BigDecimal> conceptosMensuales(List<RentaQuinta> boletas) {
Map<String, BigDecimal> conceptos = new LinkedHashMap<>();
conceptos.put("basico", sumar(boletas, RentaQuinta::getBasico));
conceptos.put("asignacionFamiliar", sumar(boletas, RentaQuinta::getAsignacionFamiliar));
conceptos.put("movilidad", sumar(boletas, RentaQuinta::getMovilidad));
conceptos.put("vacacionesGozadas", sumar(boletas, RentaQuinta::getVacacionesGozadasRemVaca));
conceptos.put("licencias", sumar(boletas, RentaQuinta::getLicenciaGoceHaber, RentaQuinta::getLicenciaSaludFamiliarLey30012));
conceptos.put("horasExtras", sumar(boletas, RentaQuinta::getHorasExtras25, RentaQuinta::getHorasExtras35));
conceptos.put("dominical", sumar(boletas, RentaQuinta::getDominical));
conceptos.put("bonos", sumar(boletas, RentaQuinta::getBonoExtraordinario, RentaQuinta::getBonoProductividad,
RentaQuinta::getBonoDesempeno, RentaQuinta::getBonoAniversario));
conceptos.put("apoyoAsignaciones", sumar(boletas, RentaQuinta::getApoyoSalud, RentaQuinta::getAsignacionEstudios,
RentaQuinta::getAsignacionFallecimientoFamiliar));
conceptos.put("reintegro", sumar(boletas, RentaQuinta::getReintegro));
conceptos.put("canastaObsequiosVales", sumar(boletas, RentaQuinta::getCanastaNavidad, RentaQuinta::getValeConsumo,
RentaQuinta::getObsequios, RentaQuinta::getCanastaNavidad2, RentaQuinta::getValeConsumoDescuento));
conceptos.put("compraVacaciones", sumar(boletas, RentaQuinta::getCompensacionVacacional));
conceptos.put("vacacionesTruncas", sumar(boletas, RentaQuinta::getVacacionesTruncas));
conceptos.put("gratificacionesTruncas", sumar(boletas, RentaQuinta::getGratificacionProporcional));
conceptos.put("bonificacionTrunca", sumar(boletas, RentaQuinta::getBonificacionExtraordinaria));
conceptos.put("sumaGraciosa", sumar(boletas, RentaQuinta::getSumaGraciosa));
conceptos.put("gratificacion", sumar(boletas, RentaQuinta::getGratificacionJulioDiciembre, RentaQuinta::getBonificacionExtraordinaria2));
conceptos.put("utilidades", sumar(boletas, RentaQuinta::getUtilidades));
return conceptos;
}
@SafeVarargs
private final BigDecimal sumar(List<RentaQuinta> boletas, Function<RentaQuinta, BigDecimal>... campos) {
BigDecimal total = BigDecimal.ZERO;
for (RentaQuinta boleta : boletas) {
for (Function<RentaQuinta, BigDecimal> campo : campos) total = total.add(orZero(campo.apply(boleta)));
}
return total;
} }
/** /**
...@@ -286,7 +334,7 @@ public class RentaQuintaMapper { ...@@ -286,7 +334,7 @@ public class RentaQuintaMapper {
e.getCodper(), e.getCodper(),
e.getCodlocal(), e.getCodlocal(),
e.getUnidadOrganizativa(), e.getUnidadOrganizativa(),
e.getTipoDocumento(), e.getTipoPlanilla(),
e.getApellidoPaterno(), e.getApellidoPaterno(),
e.getApellidoMaterno(), e.getApellidoMaterno(),
e.getNombres(), e.getNombres(),
......
package so.planilla_be.rentaquinta.repository;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/** Consulta FDW en lote: las unidades organizativas de pl_planilla vienen vacías. */
@Repository
public class OrganizacionPersonalRepository {
private final NamedParameterJdbcTemplate jdbc;
public OrganizacionPersonalRepository(NamedParameterJdbcTemplate jdbc) {
this.jdbc = jdbc;
}
public Map<String, Organizacion> buscar(List<String> documentos) {
if (documentos.isEmpty()) return Map.of();
return jdbc.query("""
select distinct on (trim(p.numero_documento))
trim(p.numero_documento) as documento, p.id_unidad_organizativa,
p.codarea, a.desarea, p.id_subarea, s.nombre, p.codcargo, c.descargo
from nuevo.personal p
left join nuevo.area a on a.codarea = p.codarea
left join nuevo.subarea s on s.id_subarea = p.id_subarea
left join nuevo.cargo c on c.codcargo = p.codcargo
where trim(p.numero_documento) in (:documentos)
order by trim(p.numero_documento), p.codper desc
""", Map.of("documentos", documentos.stream().map(String::trim).distinct().toList()),
(rs, row) -> new Organizacion(rs.getString("documento"),
rs.getObject("id_unidad_organizativa", Integer.class),
rs.getObject("codarea", Integer.class), rs.getString("desarea"),
rs.getObject("id_subarea", Integer.class), rs.getString("nombre"),
rs.getObject("codcargo", Integer.class), rs.getString("descargo")))
.stream().collect(Collectors.toMap(Organizacion::documento, Function.identity()));
}
public record Organizacion(String documento, Integer idUnidadOrganizativa,
Integer idArea, String area, Integer idSubArea, String subArea,
Integer idCargo, String cargo) {}
}
...@@ -9,6 +9,18 @@ import org.springframework.data.jpa.repository.Query; ...@@ -9,6 +9,18 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
public interface RentaQuintaGrabadaRepository extends JpaRepository<RentaQuintaGrabada, RentaQuintaGrabadaId> { public interface RentaQuintaGrabadaRepository extends JpaRepository<RentaQuintaGrabada, RentaQuintaGrabadaId> {
@Query("""
select r from RentaQuintaGrabada r
where r.anio = :anio and r.numeroDocumento = :numeroDocumento
and trim(r.boletaTipoPersonal) = :tipoPersonal
and coalesce(trim(r.tipoPago), '') = :tipoPago
""")
List<RentaQuintaGrabada> retencionesAnuales(
@Param("anio") Integer anio,
@Param("numeroDocumento") String numeroDocumento,
@Param("tipoPersonal") String tipoPersonal,
@Param("tipoPago") String tipoPago);
@Query("select r from RentaQuintaGrabada r where r.anio = :anio and r.mes = :mes and r.numeroDocumento in :numerosDocumento") @Query("select r from RentaQuintaGrabada r where r.anio = :anio and r.mes = :mes and r.numeroDocumento in :numerosDocumento")
List<RentaQuintaGrabada> findByAnioAndMesAndNumeroDocumentoIn( List<RentaQuintaGrabada> findByAnioAndMesAndNumeroDocumentoIn(
@Param("anio") Integer anio, @Param("anio") Integer anio,
......
...@@ -18,14 +18,22 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long> ...@@ -18,14 +18,22 @@ 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.boleta_tipo_personal) = :tipoPlanilla)
and (:tipoPago is null or coalesce(trim(r.tipoPago), '') = :tipoPago) and (:tipoPago is null or coalesce(trim(r.tipo_pago), '') = :tipoPago)
and (:areaGeneral is null or r.areaGeneral = :areaGeneral) and ((:areaGeneral is null and :subArea is null and :cargo is null) or trim(r.numero_documento) in (
and (:subArea is null or r.subArea = :subArea) select p.documento from (
and (:cargo is null or r.cargo = :cargo) select distinct on (trim(numero_documento)) trim(numero_documento) as documento,
codarea, id_subarea, codcargo
from nuevo.personal
order by trim(numero_documento), codper desc
) p
where (:areaGeneral is null or cast(p.codarea as text) = :areaGeneral)
and (:subArea is null or cast(p.id_subarea as text) = :subArea)
and (:cargo is null or cast(p.codcargo as text) = :cargo)
))
and (:texto is null and (:texto is null
or upper(r.numeroDocumento) like :texto or upper(r.numero_documento) like :texto
or upper(concat(r.apellidoPaterno, ' ', r.apellidoMaterno, ' ', r.nombres)) like :texto) or upper(concat(r.apellido_paterno, ' ', r.apellido_materno, ' ', r.nombres)) like :texto)
"""; """;
/** Meses de un trabajador en un año, ordenados cronológicamente (historial anual). */ /** Meses de un trabajador en un año, ordenados cronológicamente (historial anual). */
...@@ -37,27 +45,28 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long> ...@@ -37,27 +45,28 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
Integer codper, Integer boletaAnio, Integer boletaMes); Integer codper, Integer boletaAnio, Integer boletaMes);
/** /**
* Documentos que matchean los filtros de la grilla, paginados y ordenados por apellidos + * Documentos únicos que matchean los filtros de la grilla, paginados y ordenados por apellidos +
* nombres. Solo trae la clave (numeroDocumento) — el service arma la fila agregando en Java * nombres. Solo trae la clave (numeroDocumento) — el service arma la fila agregando en Java
* sobre {@link #historialHastaMes}, no en SQL. * sobre {@link #historialHastaMes}, no en SQL.
*/ */
@Query(value = """ @Query(value = """
select distinct r.numeroDocumento as numeroDocumento, select r.numero_documento as "numeroDocumento",
r.apellidoPaterno as apellidoPaterno, min(r.apellido_paterno) as "apellidoPaterno",
r.apellidoMaterno as apellidoMaterno, min(r.apellido_materno) as "apellidoMaterno",
r.nombres as nombres min(r.nombres) as nombres
from RentaQuinta r from planilla.pl_planilla r
where r.boletaAnio = :periodo where r.boleta_anio = :periodo
and r.boletaMes <= :mesCorte and r.boleta_mes <= :mesCorte
""" + FILTROS + """ """ + FILTROS + """
order by r.apellidoPaterno asc, r.apellidoMaterno asc, r.nombres asc group by r.numero_documento
order by "apellidoPaterno" asc, "apellidoMaterno" asc, nombres asc, r.numero_documento asc
""", """,
countQuery = """ countQuery = """
select count(distinct r.numeroDocumento) select count(distinct r.numero_documento)
from RentaQuinta r from planilla.pl_planilla r
where r.boletaAnio = :periodo where r.boleta_anio = :periodo
and r.boletaMes <= :mesCorte and r.boleta_mes <= :mesCorte
""" + FILTROS) """ + FILTROS, nativeQuery = true)
Page<DocumentoResumenProjection> buscarDocumentos( Page<DocumentoResumenProjection> buscarDocumentos(
@Param("periodo") Integer periodo, @Param("periodo") Integer periodo,
@Param("mesCorte") Integer mesCorte, @Param("mesCorte") Integer mesCorte,
......
...@@ -2,10 +2,12 @@ package so.planilla_be.rentaquinta.service; ...@@ -2,10 +2,12 @@ package so.planilla_be.rentaquinta.service;
import so.planilla_be.rentaquinta.dto.FiltroItemDTO; import so.planilla_be.rentaquinta.dto.FiltroItemDTO;
import so.planilla_be.rentaquinta.dto.TipoFiltro; import so.planilla_be.rentaquinta.dto.TipoFiltro;
import so.planilla_be.rentaquinta.dto.PeriodoFiltroDTO;
import java.util.List; import java.util.List;
public interface FiltrosService { public interface FiltrosService {
List<FiltroItemDTO> listar(TipoFiltro tipo); List<FiltroItemDTO> listar(TipoFiltro tipo);
List<PeriodoFiltroDTO> listarPeriodos();
} }
...@@ -4,6 +4,7 @@ import org.springframework.jdbc.core.JdbcTemplate; ...@@ -4,6 +4,7 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import so.planilla_be.rentaquinta.dto.FiltroItemDTO; import so.planilla_be.rentaquinta.dto.FiltroItemDTO;
import so.planilla_be.rentaquinta.dto.TipoFiltro; import so.planilla_be.rentaquinta.dto.TipoFiltro;
import so.planilla_be.rentaquinta.dto.PeriodoFiltroDTO;
import so.planilla_be.rentaquinta.service.FiltrosService; import so.planilla_be.rentaquinta.service.FiltrosService;
import java.util.List; import java.util.List;
...@@ -23,6 +24,12 @@ public class FiltrosServiceImpl implements FiltrosService { ...@@ -23,6 +24,12 @@ public class FiltrosServiceImpl implements FiltrosService {
} }
@Override @Override
public List<PeriodoFiltroDTO> listarPeriodos() {
return jdbcTemplate.query("select id_periodo, mes, anio from planilla.pl_periodo order by anio desc, mes desc",
(rs, rowNum) -> new PeriodoFiltroDTO(rs.getInt("id_periodo"), rs.getInt("mes"), rs.getInt("anio")));
}
@Override
public List<FiltroItemDTO> listar(TipoFiltro tipo) { public List<FiltroItemDTO> listar(TipoFiltro tipo) {
String sql = switch (tipo) { String sql = switch (tipo) {
case AREA -> "select codarea as id, desarea as text from nuevo.area order by desarea"; case AREA -> "select codarea as id, desarea as text from nuevo.area order by desarea";
......
package so.planilla_be.rentaquinta.service.impl; package so.planilla_be.rentaquinta.service.impl;
import jakarta.persistence.EntityManager;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import so.planilla_be.docentegratidiciembre.repository.DocenteGratiDiciembreCompletaRepository; import so.planilla_be.docentegratidiciembre.repository.DocenteGratiDiciembreCompletaRepository;
import so.planilla_be.proyecciondocentehora.entity.ProyeccionDocenteHora;
import so.planilla_be.proyecciondocentehora.repository.ProyeccionDocenteHoraRepository; import so.planilla_be.proyecciondocentehora.repository.ProyeccionDocenteHoraRepository;
import so.planilla_be.rentaquinta.calculo.CalculoRentaQuinta; import so.planilla_be.rentaquinta.calculo.CalculoRentaQuinta;
import so.planilla_be.rentaquinta.calculo.ModalidadCalculo; import so.planilla_be.rentaquinta.calculo.ModalidadCalculo;
...@@ -30,6 +33,7 @@ import so.planilla_be.rentaquinta.repository.CalendarioRepository; ...@@ -30,6 +33,7 @@ import so.planilla_be.rentaquinta.repository.CalendarioRepository;
import so.planilla_be.rentaquinta.repository.DocumentoResumenProjection; import so.planilla_be.rentaquinta.repository.DocumentoResumenProjection;
import so.planilla_be.rentaquinta.repository.RentaQuintaGrabadaRepository; import so.planilla_be.rentaquinta.repository.RentaQuintaGrabadaRepository;
import so.planilla_be.rentaquinta.repository.RentaQuintaRepository; import so.planilla_be.rentaquinta.repository.RentaQuintaRepository;
import so.planilla_be.rentaquinta.repository.OrganizacionPersonalRepository;
import so.planilla_be.rentaquinta.service.RentaQuintaService; import so.planilla_be.rentaquinta.service.RentaQuintaService;
import so.planilla_be.otrosempleadores.entity.RetencionOtrosEmpleadores; import so.planilla_be.otrosempleadores.entity.RetencionOtrosEmpleadores;
import so.planilla_be.otrosempleadores.repository.RetencionOtrosEmpleadoresRepository; import so.planilla_be.otrosempleadores.repository.RetencionOtrosEmpleadoresRepository;
...@@ -41,12 +45,14 @@ import so.planilla_be.utilidades.repository.UtilidadRepository; ...@@ -41,12 +45,14 @@ import so.planilla_be.utilidades.repository.UtilidadRepository;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.YearMonth; import java.time.YearMonth;
import java.math.RoundingMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.IntPredicate;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
...@@ -62,6 +68,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -62,6 +68,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
* {@code ""}, porque char(1) se lee con padding (' ') y Hibernate lo tomaría como cambio de PK. * {@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 static final String TIPO_PAGO_VACIO = " ";
private static final String TIPO_GRATIFICACION = "GRA";
private final RentaQuintaRepository repository; private final RentaQuintaRepository repository;
private final RentaQuintaMapper mapper; private final RentaQuintaMapper mapper;
...@@ -73,6 +80,8 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -73,6 +80,8 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
private final ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository; private final ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository;
private final CalendarioRepository calendarioRepository; private final CalendarioRepository calendarioRepository;
private final DocenteGratiDiciembreCompletaRepository docenteGratiDiciembreCompletaRepository; private final DocenteGratiDiciembreCompletaRepository docenteGratiDiciembreCompletaRepository;
private final EntityManager entityManager;
private final OrganizacionPersonalRepository organizacionRepository;
public RentaQuintaServiceImpl(RentaQuintaRepository repository, RentaQuintaMapper mapper, public RentaQuintaServiceImpl(RentaQuintaRepository repository, RentaQuintaMapper mapper,
RentaQuintaCalculadora calculadora, RentaQuintaCalculadora calculadora,
...@@ -82,7 +91,9 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -82,7 +91,9 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository, RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository,
ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository, ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository,
CalendarioRepository calendarioRepository, CalendarioRepository calendarioRepository,
DocenteGratiDiciembreCompletaRepository docenteGratiDiciembreCompletaRepository) { DocenteGratiDiciembreCompletaRepository docenteGratiDiciembreCompletaRepository,
EntityManager entityManager,
OrganizacionPersonalRepository organizacionRepository) {
this.repository = repository; this.repository = repository;
this.mapper = mapper; this.mapper = mapper;
this.calculadora = calculadora; this.calculadora = calculadora;
...@@ -93,20 +104,39 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -93,20 +104,39 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
this.proyeccionDocenteHoraRepository = proyeccionDocenteHoraRepository; this.proyeccionDocenteHoraRepository = proyeccionDocenteHoraRepository;
this.calendarioRepository = calendarioRepository; this.calendarioRepository = calendarioRepository;
this.docenteGratiDiciembreCompletaRepository = docenteGratiDiciembreCompletaRepository; this.docenteGratiDiciembreCompletaRepository = docenteGratiDiciembreCompletaRepository;
this.entityManager = entityManager;
this.organizacionRepository = organizacionRepository;
} }
/** /** Consulta sin escritura; los estados calculados se filtran antes de paginar. */
* 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 @Override
@Transactional @Transactional(readOnly = true)
public Page<RentaQuintaResumenDTO> buscar(RentaQuintaFiltroDTO filtro) { public Page<RentaQuintaResumenDTO> buscar(RentaQuintaFiltroDTO filtro) {
//grabarSegunFiltro(filtro); Pageable pageable = toPageable(filtro);
if (filtro.tieneVariacion() == null && filtro.tieneRetencion() == null) {
return buscarPagina(filtro, pageable);
}
// Los estados dependen del cálculo: filtrarlos antes de paginar la respuesta.
List<RentaQuintaResumenDTO> filas = new ArrayList<>();
int page = 0;
Page<RentaQuintaResumenDTO> pagina;
do {
pagina = buscarPagina(filtro, PageRequest.of(page++, GRABADO_MASIVO_BATCH));
pagina.getContent().stream().filter(r -> cumpleEstados(r, filtro)).forEach(filas::add);
} while (pagina.hasNext());
int desde = (int) Math.min(pageable.getOffset(), filas.size());
int hasta = Math.min(desde + pageable.getPageSize(), filas.size());
return new PageImpl<>(filas.subList(desde, hasta), pageable, filas.size());
}
private boolean cumpleEstados(RentaQuintaResumenDTO fila, RentaQuintaFiltroDTO filtro) {
boolean variacion = orZero(fila.diferenciaRetencion()).abs().compareTo(new BigDecimal("0.001")) > 0;
boolean retencion = orZero(fila.retencionDelMes()).signum() > 0;
return (filtro.tieneVariacion() == null || filtro.tieneVariacion() == variacion)
&& (filtro.tieneRetencion() == null || filtro.tieneRetencion() == retencion);
}
private Page<RentaQuintaResumenDTO> buscarPagina(RentaQuintaFiltroDTO filtro, Pageable pageable) {
Page<DocumentoResumenProjection> documentos = repository.buscarDocumentos( Page<DocumentoResumenProjection> documentos = repository.buscarDocumentos(
filtro.periodo(), filtro.periodo(),
filtro.mesCorte(), filtro.mesCorte(),
...@@ -117,7 +147,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -117,7 +147,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
codigo(filtro.subArea()), codigo(filtro.subArea()),
codigo(filtro.cargo()), codigo(filtro.cargo()),
patron(filtro.documentoOApellidos()), patron(filtro.documentoOApellidos()),
toPageable(filtro)); pageable);
List<String> numeros = documentos.getContent().stream() List<String> numeros = documentos.getContent().stream()
.map(DocumentoResumenProjection::getNumeroDocumento) .map(DocumentoResumenProjection::getNumeroDocumento)
...@@ -130,13 +160,15 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -130,13 +160,15 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
// 1. AQUÍ OBTIENES EL MAPA CON LAS RETENCIONES GRABADAS DEL MES ANTERIOR: // 1. AQUÍ OBTIENES EL MAPA CON LAS RETENCIONES GRABADAS DEL MES ANTERIOR:
Map<String, BigDecimal> retencionesAnteriores = obtenerRetencionesMesAnterior(numeros, filtro.periodo(), filtro.mesCorte()); Map<String, BigDecimal> retencionesAnteriores = obtenerRetencionesMesAnterior(numeros, filtro.periodo(), filtro.mesCorte());
DatosExternos externos = cargarDatosExternos(numeros, filtro.periodo());
var organizaciones = organizacionRepository.buscar(numeros);
return documentos.map(d -> { return documentos.map(d -> {
List<RentaQuinta> historial = porDocumento.getOrDefault(d.getNumeroDocumento(), List.of()); List<RentaQuinta> historial = porDocumento.getOrDefault(d.getNumeroDocumento(), List.of());
Agregado a = agregar(historial, filtro.mesCorte()); Agregado a = agregar(historial, filtro.mesCorte());
TipoPlanilla tipoPlanilla = tipoPlanilla(filtro.tipoPlanilla(), a.referencia()); TipoPlanilla tipoPlanilla = tipoPlanilla(filtro.tipoPlanilla(), a.referencia());
CalculoRentaQuinta calculo = calcular(a, historial, filtro.periodo(), filtro.mesCorte(), tipoPlanilla, CalculoRentaQuinta calculo = calcular(a, historial, filtro.periodo(), filtro.mesCorte(), tipoPlanilla,
filtro.tipoPago()); filtro.tipoPago(), externos);
BigDecimal planillaCargada = (a.mesCorteReal() BigDecimal planillaCargada = (a.mesCorteReal()
? a.planillaCargadaReal().add(calculo.ajusteSueldoBrutoMesCorte()) ? a.planillaCargadaReal().add(calculo.ajusteSueldoBrutoMesCorte())
...@@ -150,14 +182,17 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -150,14 +182,17 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
BigDecimal historialReal = a.historialReal().add(calculo.otrosEmpleadoresImporteAnterior()); 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) // Comparar con la misma modalidad del mes anterior (DOC1 y DOC2 tienen PK distintas).
ModalidadCalculo modalidad = modalidad(tipoPlanilla, filtro.tipoPago(), historial, a.referencia().getBoletaMes());
String tipoPagoAnterior = tipoPagoRetencion(tipoPlanilla, filtro.tipoPago(), modalidad);
BigDecimal retencionAnteriorGrabada = retencionesAnteriores.getOrDefault( BigDecimal retencionAnteriorGrabada = retencionesAnteriores.getOrDefault(
claveGrabada(d.getNumeroDocumento(), tipoPlanilla.getCodigo()), BigDecimal.ZERO); claveGrabada(d.getNumeroDocumento(), tipoPlanilla.getCodigo(), tipoPagoAnterior), BigDecimal.ZERO);
BigDecimal retencionActualCalculada = calculo.retencionDelMes(); BigDecimal retencionActualCalculada = calculo.retencionDelMes();
BigDecimal diferencia = retencionActualCalculada.subtract(retencionAnteriorGrabada); BigDecimal diferencia = retencionActualCalculada.subtract(retencionAnteriorGrabada);
// 3. PASAS LOS VALORES A TU MAPPER: // 3. PASAS LOS VALORES A TU MAPPER:
return mapper.toResumen(a.referencia(), codper, filtro.periodo(), filtro.mesCorte(), return mapper.toResumen(a.referencia(), codper, filtro.periodo(), filtro.mesCorte(),
historialReal, planillaCargada, a.retenidoAnterior(), calculo,retencionAnteriorGrabada, diferencia); historialReal, planillaCargada, a.retenidoAnterior(), calculo,retencionAnteriorGrabada, diferencia,
organizaciones.get(d.getNumeroDocumento().trim()));
}); });
} }
...@@ -221,15 +256,9 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -221,15 +256,9 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
return mapper.toDetalle(boleta); return mapper.toDetalle(boleta);
} }
/** /** Consulta el cálculo y la matriz; la escritura se realiza desde grabarRetencion. */
* 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 @Override
@Transactional @Transactional(readOnly = true)
public RentaQuintaDetalleCompletoDTO obtenerDetalleCompleto(String numeroDocumento, Integer anio, Integer mes, public RentaQuintaDetalleCompletoDTO obtenerDetalleCompleto(String numeroDocumento, Integer anio, Integer mes,
TipoPlanilla tipoPlanillaSolicitado, TipoPlanilla tipoPlanillaSolicitado,
TipoPago tipoPago) { TipoPago tipoPago) {
...@@ -243,8 +272,86 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -243,8 +272,86 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
"No existe boleta para documento %s, periodo %d-%02d".formatted(numeroDocumento, anio, mes)); "No existe boleta para documento %s, periodo %d-%02d".formatted(numeroDocumento, anio, mes));
} }
TipoPlanilla tipoPlanilla = tipoPlanilla(tipoPlanillaSolicitado, a.referencia()); TipoPlanilla tipoPlanilla = tipoPlanilla(tipoPlanillaSolicitado, a.referencia());
CalculoRentaQuinta calculo = calcular(a, historialAnio, anio, mesCorte, tipoPlanilla, tipoPago); DatosExternos externos = cargarDatosExternos(List.of(numeroDocumento), anio);
return mapper.toDetalleCompleto(a.referencia(), historialAnio, mesCorte, calculo); DatosCalculo datos = datosCalculo(a, historialAnio, anio, mesCorte, tipoPlanilla, tipoPago, externos);
CalculoRentaQuinta calculo = calculadora.calcular(datos);
return mapper.toDetalleCompleto(a.referencia(), historialAnio, mesCorte, calculo,
matrizMensual(a, historialAnio, mesCorte, tipoPlanilla, tipoPago, externos, datos, calculo));
}
private List<RentaQuintaDetalleCompletoDTO.MesIngresosDTO> matrizMensual(
Agregado a, List<RentaQuinta> historial, int mesCorte, TipoPlanilla tipoPlanilla, TipoPago tipoPago,
DatosExternos externos, DatosCalculo datos, CalculoRentaQuinta calculo) {
String documento = a.referencia().getNumeroDocumento();
int mesReferencia = a.referencia().getBoletaMes();
ModalidadCalculo modalidad = modalidad(tipoPlanilla, tipoPago, historial, mesReferencia);
boolean porHora = modalidad == ModalidadCalculo.DOCENTE_POR_HORA;
String tipoPagoRetencion = tipoPagoRetencion(tipoPlanilla, tipoPago, modalidad);
Map<Integer, BigDecimal> retencionesGrabadas = rentaQuintaGrabadaRepository
.retencionesAnuales(a.referencia().getBoletaAnio(), documento, tipoPlanilla.getCodigo(), tipoPagoRetencion)
.stream().filter(r -> r.getRetencionMesActual() != null)
.collect(Collectors.toMap(RentaQuintaGrabada::getMes, RentaQuintaGrabada::getRetencionMesActual));
List<ProyeccionDocenteHora> docente = externos.proyeccionDocenteHora().getOrDefault(documento, List.of());
List<RetencionOtrosEmpleadores> otros = externos.otrosEmpleadores().getOrDefault(documento, List.of());
BigDecimal sueldo = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getSueldoBruto);
BigDecimal familiar = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getAsignacionFamiliar);
BigDecimal movilidad = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getMovilidad);
List<RentaQuintaDetalleCompletoDTO.MesIngresosDTO> matriz = new ArrayList<>();
BigDecimal saldoSueldo = calculo.proyeccionSueldoBruto();
BigDecimal saldoGratificacion = calculo.proyeccionGratificaciones();
for (int mes = 1; mes <= 12; mes++) {
final int mesActual = mes;
// Las boletas posteriores al corte no sustituyen la proyección usada en los puntos 1 a 5.
List<RentaQuinta> reales = mes <= mesCorte
? historial.stream().filter(esMesCorte(mes)).toList() : List.of();
boolean proyectado = mes > mesCorte || (mes == mesCorte && reales.isEmpty());
Map<String, BigDecimal> conceptos = mapper.conceptosMensuales(reales);
if (proyectado) {
BigDecimal factor = mes == 12 && modalidad == ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL
? BigDecimal.valueOf(16).divide(BigDecimal.valueOf(30), 10, RoundingMode.HALF_UP) : BigDecimal.ONE;
BigDecimal sueldoMes = mes == mesCorte ? calculo.estimadoMesCorteSueldoBruto()
: porHora ? sumaProyeccion(docente, tipoPlanilla.getCodigo(), m -> m == mesActual, ProyeccionDocenteHora::getRemunBrutaFutura)
: sueldo.multiply(factor).setScale(2, RoundingMode.HALF_UP);
if (mes > mesCorte) {
if (mes == 12) sueldoMes = saldoSueldo;
saldoSueldo = saldoSueldo.subtract(sueldoMes);
}
BigDecimal familiarMes = porHora ? BigDecimal.ZERO : familiar.multiply(factor).setScale(2, RoundingMode.HALF_UP);
BigDecimal movilidadMes = porHora ? BigDecimal.ZERO : movilidad.multiply(factor).setScale(2, RoundingMode.HALF_UP);
conceptos.put("basico", sueldoMes.subtract(familiarMes).subtract(movilidadMes));
conceptos.put("asignacionFamiliar", familiarMes);
conceptos.put("movilidad", movilidadMes);
BigDecimal gratificacion = BigDecimal.ZERO;
if (mes == mesCorte) gratificacion = calculo.estimadoMesCorteGratificacion();
else if (mes == 7 || mes == 12) {
gratificacion = calculadora.gratificacionDelMes(datos, mes);
if (mes == 12) gratificacion = saldoGratificacion;
saldoGratificacion = saldoGratificacion.subtract(gratificacion);
}
conceptos.put("gratificacion", gratificacion);
} else if (mes == mesCorte) {
conceptos.merge("basico", calculo.ajusteSueldoBrutoMesCorte(), BigDecimal::add);
}
conceptos.put("otrosEmpleadores", otros.stream().filter(r -> r.getMes() == mesActual)
.map(r -> orZero(r.getImporte())).reduce(BigDecimal.ZERO, BigDecimal::add));
BigDecimal total = conceptos.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
// Una retención grabada de cero también tiene prioridad sobre la boleta.
BigDecimal retenido = retencionesGrabadas.get(mes);
if (retenido == null) retenido = sumaMesCorte(historial, esMesCorte(mes), RentaQuinta::getRenta5taCategoria);
matriz.add(new RentaQuintaDetalleCompletoDTO.MesIngresosDTO(mes,
proyectado || (porHora && mes == mesCorte), conceptos, total, retenido.negate()));
}
return matriz;
}
private String tipoPagoRetencion(TipoPlanilla tipoPlanilla, TipoPago tipoPago, ModalidadCalculo modalidad) {
if (!tipoPlanilla.esDocente()) return "";
if (tipoPago != null) return tipoPago.getCodigo();
return switch (modalidad) {
case DOCENTE_POR_HORA -> TipoPago.POR_HORA.getCodigo();
case MONTO_FIJO_DICIEMBRE_PROPORCIONAL -> TipoPago.MONTO_FIJO.getCodigo();
case MONTO_FIJO -> "";
};
} }
/** /**
...@@ -291,6 +398,42 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -291,6 +398,42 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
} }
/** /**
* Variante del grabado masivo: los registros existentes ya vienen precargados en
* {@code existentes} (clave {@link #claveGrabada(RentaQuintaGrabada)}), así que no hay un
* SELECT por trabajador; los nuevos se insertan con {@code persist} (con {@code save} la PK
* asignada dispara un merge que vuelve a consultar).
*/
private void upsertRetencion(Map<String, RentaQuintaGrabada> existentes, String numeroDocumento, Integer anio,
Integer mes, TipoPlanilla tipoPlanilla, TipoPago tipoPago,
BigDecimal retencionMesActual) {
String tipoPagoGrabado = tipoPlanilla.esDocente() && tipoPago != null ? tipoPago.getCodigo() : TIPO_PAGO_VACIO;
String clave = claveGrabada(numeroDocumento, tipoPlanilla.getCodigo(), tipoPagoGrabado);
RentaQuintaGrabada grabada = existentes.get(clave);
if (grabada != null) {
grabada.setRetencionMesActual(retencionMesActual);
return;
}
grabada = new RentaQuintaGrabada();
grabada.setNumeroDocumento(numeroDocumento);
grabada.setAnio(anio);
grabada.setMes(mes);
grabada.setBoletaTipoPersonal(tipoPlanilla.getCodigo());
grabada.setTipoPago(tipoPagoGrabado);
grabada.setRetencionMesActual(retencionMesActual);
entityManager.persist(grabada);
existentes.put(clave, grabada);
}
/** Clave de la PK de pl_renta_quinta (sin año/mes, fijos en el grabado masivo); char(n) con trim. */
private String claveGrabada(RentaQuintaGrabada g) {
return claveGrabada(g.getNumeroDocumento(), g.getBoletaTipoPersonal(), g.getTipoPago());
}
private String claveGrabada(String numeroDocumento, String boletaTipoPersonal, String tipoPago) {
return claveGrabada(numeroDocumento, boletaTipoPersonal) + "|" + (tipoPago == null ? "" : tipoPago.trim());
}
/**
* Graba (upsert) en pl_renta_quinta la retención del mes de todos los trabajadores que * 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 * 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 * la grilla (recorre todas las páginas internamente). Usa el mismo cálculo que la grilla
...@@ -306,15 +449,23 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -306,15 +449,23 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
private int grabarSegunFiltro(RentaQuintaFiltroDTO filtro) { private int grabarSegunFiltro(RentaQuintaFiltroDTO filtro) {
List<String> numeros = new ArrayList<>(); List<String> numeros = new ArrayList<>();
int page = 0; int page = 0;
Page<DocumentoResumenProjection> pagina; Page<?> pagina;
do { do {
pagina = repository.buscarDocumentos( if (filtro.tieneVariacion() != null || filtro.tieneRetencion() != null) {
Page<RentaQuintaResumenDTO> calculados = buscarPagina(filtro, PageRequest.of(page, GRABADO_MASIVO_BATCH));
calculados.getContent().stream().filter(r -> cumpleEstados(r, filtro))
.map(RentaQuintaResumenDTO::numeroDocumento).forEach(numeros::add);
pagina = calculados;
} else {
Page<DocumentoResumenProjection> documentos = repository.buscarDocumentos(
filtro.periodo(), filtro.mesCorte(), filtro.periodo(), filtro.mesCorte(),
codigo(filtro.sede()), codigo(filtro.tipoPlanilla()), codigoTipoPago(filtro), codigo(filtro.sede()), codigo(filtro.tipoPlanilla()), codigoTipoPago(filtro),
codigo(filtro.areaGeneral()), codigo(filtro.areaGeneral()),
codigo(filtro.subArea()), codigo(filtro.cargo()), patron(filtro.documentoOApellidos()), codigo(filtro.subArea()), codigo(filtro.cargo()), patron(filtro.documentoOApellidos()),
PageRequest.of(page, GRABADO_MASIVO_BATCH)); PageRequest.of(page, GRABADO_MASIVO_BATCH));
numeros.addAll(pagina.getContent().stream().map(DocumentoResumenProjection::getNumeroDocumento).toList()); numeros.addAll(documentos.getContent().stream().map(DocumentoResumenProjection::getNumeroDocumento).toList());
pagina = documentos;
}
page++; page++;
} while (pagina.hasNext()); } while (pagina.hasNext());
...@@ -325,6 +476,10 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -325,6 +476,10 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
aplicarBasicoYUtilidades(historialCompleto, filtro.periodo()); aplicarBasicoYUtilidades(historialCompleto, filtro.periodo());
Map<String, List<RentaQuinta>> porDocumento = historialCompleto.stream() Map<String, List<RentaQuinta>> porDocumento = historialCompleto.stream()
.collect(Collectors.groupingBy(RentaQuinta::getNumeroDocumento)); .collect(Collectors.groupingBy(RentaQuinta::getNumeroDocumento));
DatosExternos externos = cargarDatosExternos(numeros, filtro.periodo());
Map<String, RentaQuintaGrabada> existentes = rentaQuintaGrabadaRepository
.findByAnioAndMesAndNumeroDocumentoIn(filtro.periodo(), filtro.mesCorte(), numeros).stream()
.collect(Collectors.toMap(this::claveGrabada, g -> g, (x, y) -> x));
int grabados = 0; int grabados = 0;
for (String numeroDocumento : numeros) { for (String numeroDocumento : numeros) {
...@@ -335,9 +490,9 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -335,9 +490,9 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
} }
TipoPlanilla tipoPlanilla = tipoPlanilla(filtro.tipoPlanilla(), a.referencia()); TipoPlanilla tipoPlanilla = tipoPlanilla(filtro.tipoPlanilla(), a.referencia());
CalculoRentaQuinta calculo = calcular(a, historial, filtro.periodo(), filtro.mesCorte(), tipoPlanilla, CalculoRentaQuinta calculo = calcular(a, historial, filtro.periodo(), filtro.mesCorte(), tipoPlanilla,
filtro.tipoPago()); filtro.tipoPago(), externos);
upsertRetencion(numeroDocumento, filtro.periodo(), filtro.mesCorte(), tipoPlanilla, filtro.tipoPago(), upsertRetencion(existentes, numeroDocumento, filtro.periodo(), filtro.mesCorte(), tipoPlanilla,
calculo.retencionDelMes()); filtro.tipoPago(), calculo.retencionDelMes());
grabados++; grabados++;
} }
return grabados; return grabados;
...@@ -375,7 +530,12 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -375,7 +530,12 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
} }
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) { TipoPlanilla tipoPlanilla, TipoPago tipoPago, DatosExternos externos) {
return calculadora.calcular(datosCalculo(a, historial, periodo, mesCorte, tipoPlanilla, tipoPago, externos));
}
private DatosCalculo datosCalculo(Agregado a, List<RentaQuinta> historial, Integer periodo, int mesCorte,
TipoPlanilla tipoPlanilla, TipoPago tipoPago, DatosExternos externos) {
int mesReferencia = a.referencia().getBoletaMes(); int mesReferencia = a.referencia().getBoletaMes();
// Todas las filas del documento en el mes de referencia (una persona puede tener varios // Todas las filas del documento en el mes de referencia (una persona puede tener varios
// codper/tipo de planilla). // codper/tipo de planilla).
...@@ -385,25 +545,26 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -385,25 +545,26 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getAsignacionFamiliar); sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getAsignacionFamiliar);
String numeroDocumento = a.referencia().getNumeroDocumento(); String numeroDocumento = a.referencia().getNumeroDocumento();
OtrosEmpleadoresAgregado otrosEmpleadores = otrosEmpleadores(numeroDocumento, periodo, mesCorte); OtrosEmpleadoresAgregado otrosEmpleadores = otrosEmpleadores(
externos.otrosEmpleadores().getOrDefault(numeroDocumento, List.of()), mesCorte);
List<ProyeccionDocenteHora> proyeccion = externos.proyeccionDocenteHora().getOrDefault(numeroDocumento, List.of());
String tipoPersonal = tipoPlanilla.getCodigo();
ModalidadCalculo modalidad = modalidad(tipoPlanilla, tipoPago, historial, mesReferencia); ModalidadCalculo modalidad = modalidad(tipoPlanilla, tipoPago, historial, mesReferencia);
boolean porHora = modalidad == ModalidadCalculo.DOCENTE_POR_HORA; boolean porHora = modalidad == ModalidadCalculo.DOCENTE_POR_HORA;
// DOC1: remun_bruta_futura del mes de corte = sueldo bruto de la planilla en curso; la suma // DOC1: remun_bruta_futura del mes de corte = sueldo bruto de la planilla en curso; la suma
// de los meses posteriores = proyección del sueldo bruto. // de los meses posteriores = proyección del sueldo bruto.
BigDecimal sueldoBrutoMesCorteDocenteHora = porHora BigDecimal sueldoBrutoMesCorteDocenteHora = porHora
? proyeccionDocenteHoraRepository.remunBrutaFutura( ? sumaProyeccion(proyeccion, tipoPersonal, m -> m == mesCorte, ProyeccionDocenteHora::getRemunBrutaFutura)
numeroDocumento, tipoPlanilla.getCodigo(), periodo, mesCorte)
: BigDecimal.ZERO; : BigDecimal.ZERO;
BigDecimal proyeccionDocenteHora = porHora BigDecimal proyeccionDocenteHora = porHora
? proyeccionDocenteHoraRepository.sumaRemunBrutaFuturaPosterior( ? sumaProyeccion(proyeccion, tipoPersonal, m -> m > mesCorte, ProyeccionDocenteHora::getRemunBrutaFutura)
numeroDocumento, tipoPlanilla.getCodigo(), periodo, mesCorte)
: BigDecimal.ZERO; : BigDecimal.ZERO;
BigDecimal gratificacionJulioDocenteHora = porHora BigDecimal gratificacionJulioDocenteHora = porHora
? proyeccionDocenteHoraRepository.gratificacionDelMes(numeroDocumento, periodo, 7) ? sumaProyeccion(proyeccion, TIPO_GRATIFICACION, m -> m == 7, ProyeccionDocenteHora::getGratificacion)
: BigDecimal.ZERO; : BigDecimal.ZERO;
BigDecimal gratificacionDiciembreDocenteHora = porHora BigDecimal gratificacionDiciembreDocenteHora = porHora
? proyeccionDocenteHoraRepository.gratificacionDelMes(numeroDocumento, periodo, 12) ? sumaProyeccion(proyeccion, TIPO_GRATIFICACION, m -> m == 12, ProyeccionDocenteHora::getGratificacion)
: BigDecimal.ZERO; : BigDecimal.ZERO;
BigDecimal sueldoBrutoRealMesCorte = a.mesCorteReal() BigDecimal sueldoBrutoRealMesCorte = a.mesCorteReal()
? mapper.sueldoBrutoMes(historial, esMesCorte(mesCorte)) ? mapper.sueldoBrutoMes(historial, esMesCorte(mesCorte))
...@@ -426,7 +587,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -426,7 +587,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
boolean gratificacionDiciembreCompleta = modalidad == ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL boolean gratificacionDiciembreCompleta = modalidad == ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL
&& docenteGratiDiciembreCompletaRepository.existsByDniAndAnio(numeroDocumento.trim(), periodo); && docenteGratiDiciembreCompletaRepository.existsByDniAndAnio(numeroDocumento.trim(), periodo);
return calculadora.calcular(new DatosCalculo( return new DatosCalculo(
periodo, modalidad, proyeccionDocenteHora, sueldoBrutoMesCorteDocenteHora, sueldoBrutoRealMesCorte, periodo, modalidad, proyeccionDocenteHora, sueldoBrutoMesCorteDocenteHora, sueldoBrutoRealMesCorte,
gratificacionJulioDocenteHora, gratificacionDiciembreDocenteHora, mesCorte, mesReferencia, a.historialReal(), gratificacionJulioDocenteHora, gratificacionDiciembreDocenteHora, mesCorte, mesReferencia, a.historialReal(),
a.planillaCargadaReal(), a.planillaCargadaReal(),
...@@ -437,7 +598,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -437,7 +598,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
otrosEmpleadores.retencionAnterior(), otrosEmpleadores.retencionMesActual(), otrosEmpleadores.retencionAnterior(), otrosEmpleadores.retencionMesActual(),
otrosEmpleadores.retencionProyeccion(), otrosEmpleadores.retencionProyeccion(),
gratificacionPorContrato, fechaInicioContrato, diasDelMes(fechaInicioContrato), gratificacionPorContrato, fechaInicioContrato, diasDelMes(fechaInicioContrato),
gratificacionDiciembreCompleta)); gratificacionDiciembreCompleta);
} }
/** /**
...@@ -481,9 +642,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -481,9 +642,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
* últimos ya pueden estar cargados de antemano por el trabajador/empleador y afectan tanto la * últimos ya pueden estar cargados de antemano por el trabajador/empleador y afectan tanto la
* renta bruta anual como la retención del mes de corte, igual que los otros dos bloques. * renta bruta anual como la retención del mes de corte, igual que los otros dos bloques.
*/ */
private OtrosEmpleadoresAgregado otrosEmpleadores(String numeroDocumento, Integer periodo, int mesCorte) { private OtrosEmpleadoresAgregado otrosEmpleadores(List<RetencionOtrosEmpleadores> declaraciones, int mesCorte) {
List<RetencionOtrosEmpleadores> declaraciones =
otrosEmpleadoresRepository.findByDocumentoTrabajadorAndAnioOrderByMes(numeroDocumento, periodo);
BigDecimal importeAnterior = BigDecimal.ZERO; BigDecimal importeAnterior = BigDecimal.ZERO;
BigDecimal importeMesActual = BigDecimal.ZERO; BigDecimal importeMesActual = BigDecimal.ZERO;
BigDecimal importeProyeccion = BigDecimal.ZERO; BigDecimal importeProyeccion = BigDecimal.ZERO;
...@@ -510,6 +669,36 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -510,6 +669,36 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
} }
/** /**
* Datos por documento que el cálculo necesita fuera de pl_planilla, precargados con una
* consulta por tabla para todos los documentos (en vez de varias consultas por trabajador).
*/
private DatosExternos cargarDatosExternos(List<String> documentos, Integer anio) {
if (documentos.isEmpty()) {
return new DatosExternos(Map.of(), Map.of());
}
return new DatosExternos(
otrosEmpleadoresRepository.findByAnioAndDocumentoTrabajadorIn(anio, documentos).stream()
.collect(Collectors.groupingBy(RetencionOtrosEmpleadores::getDocumentoTrabajador)),
proyeccionDocenteHoraRepository.findByProyeccionAnioAndNumeroDocumentoIn(anio, documentos).stream()
.collect(Collectors.groupingBy(ProyeccionDocenteHora::getNumeroDocumento)));
}
/**
* Equivale a las sumas {@code coalesce(sum(...), 0)} de ProyeccionDocenteHoraRepository:
* filas del tipo indicado (char(3), con trim) cuyo mes cumple {@code mes}; nulos no suman.
*/
private BigDecimal sumaProyeccion(List<ProyeccionDocenteHora> filas, String tipoPersonal,
IntPredicate mes,
Function<ProyeccionDocenteHora, BigDecimal> campo) {
return filas.stream()
.filter(p -> p.getBoletaTipoPersonal() != null && p.getBoletaTipoPersonal().trim().equals(tipoPersonal))
.filter(p -> p.getProyeccionMes() != null && mes.test(p.getProyeccionMes()))
.map(campo)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
/**
* El básico que compone el sueldo bruto (historial de ingresos, boleta, etc.) es siempre el * 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 * 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 * "utilidades" que antes vivía en la propia renta_quinta (0 si no hay declaración para ese
...@@ -536,6 +725,11 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -536,6 +725,11 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
.collect(Collectors.groupingBy(Utilidad::getNumeroDocumento, .collect(Collectors.groupingBy(Utilidad::getNumeroDocumento,
Collectors.toMap(Utilidad::getMes, Utilidad::getMontoUtilidad))); Collectors.toMap(Utilidad::getMes, Utilidad::getMontoUtilidad)));
for (RentaQuinta r : historial) { for (RentaQuinta r : historial) {
// Estos ajustes son para el cálculo; nunca deben actualizar la boleta de origen,
// incluso cuando la selección de estados se ejecuta dentro del grabado masivo.
if (entityManager.contains(r)) {
entityManager.detach(r);
}
r.setBasicoGratificacion(baseGratificacion(r, basicoPorDocumento.get(r.getNumeroDocumento()))); r.setBasicoGratificacion(baseGratificacion(r, basicoPorDocumento.get(r.getNumeroDocumento())));
r.setUtilidades(orZero(valorDelMes(utilidadPorDocumento, r.getNumeroDocumento(), r.getBoletaMes()))); r.setUtilidades(orZero(valorDelMes(utilidadPorDocumento, r.getNumeroDocumento(), r.getBoletaMes())));
} }
...@@ -552,9 +746,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -552,9 +746,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
} }
/** /**
* Retenciones grabadas del mes anterior, con clave documento + tipo de planilla * Retenciones del mes anterior con clave documento + tipo de personal + tipo de pago.
* ({@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) { private Map<String, BigDecimal> obtenerRetencionesMesAnterior(List<String> documentos, Integer anio, int mesCorte) {
if (mesCorte <= 1 || documentos.isEmpty()) { if (mesCorte <= 1 || documentos.isEmpty()) {
...@@ -564,7 +756,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -564,7 +756,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
return rentaQuintaGrabadaRepository.findByAnioAndMesAndNumeroDocumentoIn(anio, mesAnterior, documentos) return rentaQuintaGrabadaRepository.findByAnioAndMesAndNumeroDocumentoIn(anio, mesAnterior, documentos)
.stream() .stream()
.collect(Collectors.toMap( .collect(Collectors.toMap(
g -> claveGrabada(g.getNumeroDocumento(), g.getBoletaTipoPersonal()), this::claveGrabada,
RentaQuintaGrabada::getRetencionMesActual, RentaQuintaGrabada::getRetencionMesActual,
(a, b) -> a (a, b) -> a
)); ));
...@@ -619,6 +811,11 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -619,6 +811,11 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
boolean diciembrePercibida boolean diciembrePercibida
) {} ) {}
private record DatosExternos(
Map<String, List<RetencionOtrosEmpleadores>> otrosEmpleadores,
Map<String, List<ProyeccionDocenteHora>> proyeccionDocenteHora
) {}
private record OtrosEmpleadoresAgregado( private record OtrosEmpleadoresAgregado(
BigDecimal importeAnterior, BigDecimal importeAnterior,
BigDecimal importeMesActual, BigDecimal importeMesActual,
......
...@@ -7,6 +7,8 @@ spring: ...@@ -7,6 +7,8 @@ spring:
password: ${DB_PASSWORD} password: ${DB_PASSWORD}
hikari: hikari:
maximum-pool-size: 10 maximum-pool-size: 10
data-source-properties:
reWriteBatchedInserts: true
jpa: jpa:
hibernate: hibernate:
ddl-auto: validate # nunca 'update'/'create' sobre esta tabla ddl-auto: validate # nunca 'update'/'create' sobre esta tabla
...@@ -15,6 +17,10 @@ spring: ...@@ -15,6 +17,10 @@ spring:
hibernate: hibernate:
default_schema: planilla default_schema: planilla
format_sql: true format_sql: true
jdbc:
batch_size: 200 # grabado masivo: inserts/updates de pl_renta_quinta en lote
order_inserts: true
order_updates: true
hbm2ddl: hbm2ddl:
# nuevo.personal es una foreign table (postgres_fdw): el driver la reporta como # nuevo.personal es una foreign table (postgres_fdw): el driver la reporta como
# TABLE_TYPE "FOREIGN TABLE", no "TABLE", y sin esto ddl-auto:validate la da como # TABLE_TYPE "FOREIGN TABLE", no "TABLE", y sin esto ddl-auto:validate la da como
......
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