feat: agregar filtros de retencion y datos organizativos en renta quinta

parent 628cba00
# 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 diciembre (sueldo bruto y gratificación) entra ÷ 30 × 16: en la proyección, en el estimado del mes de corte 12 y en la gratificación de diciembre pendiente. 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
### 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`
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
# Integración de renta quinta con jQuery 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).
...@@ -58,13 +58,15 @@ public class RentaQuintaController { ...@@ -58,13 +58,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")
...@@ -127,11 +129,13 @@ public class RentaQuintaController { ...@@ -127,11 +129,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) {}
...@@ -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
......
...@@ -14,6 +14,13 @@ public record RentaQuintaResumenDTO( ...@@ -14,6 +14,13 @@ public record RentaQuintaResumenDTO(
Integer periodo, Integer periodo,
String numeroDocumento, String numeroDocumento,
String nombreCompleto, String nombreCompleto,
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,
......
...@@ -14,6 +14,7 @@ import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.RegistroHist ...@@ -14,6 +14,7 @@ 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;
...@@ -24,12 +25,20 @@ public class RentaQuintaMapper { ...@@ -24,12 +25,20 @@ 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()),
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),
......
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) {}
}
...@@ -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,6 +2,7 @@ package so.planilla_be.rentaquinta.service.impl; ...@@ -2,6 +2,7 @@ package so.planilla_be.rentaquinta.service.impl;
import jakarta.persistence.EntityManager; 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;
...@@ -28,6 +29,7 @@ import so.planilla_be.rentaquinta.mapper.RentaQuintaMapper; ...@@ -28,6 +29,7 @@ import so.planilla_be.rentaquinta.mapper.RentaQuintaMapper;
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;
...@@ -70,6 +72,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -70,6 +72,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
private final RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository; private final RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository;
private final ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository; private final ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository;
private final EntityManager entityManager; private final EntityManager entityManager;
private final OrganizacionPersonalRepository organizacionRepository;
public RentaQuintaServiceImpl(RentaQuintaRepository repository, RentaQuintaMapper mapper, public RentaQuintaServiceImpl(RentaQuintaRepository repository, RentaQuintaMapper mapper,
RentaQuintaCalculadora calculadora, RentaQuintaCalculadora calculadora,
...@@ -78,7 +81,8 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -78,7 +81,8 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
UtilidadRepository utilidadRepository, UtilidadRepository utilidadRepository,
RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository, RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository,
ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository, ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository,
EntityManager entityManager) { EntityManager entityManager,
OrganizacionPersonalRepository organizacionRepository) {
this.repository = repository; this.repository = repository;
this.mapper = mapper; this.mapper = mapper;
this.calculadora = calculadora; this.calculadora = calculadora;
...@@ -88,6 +92,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -88,6 +92,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
this.rentaQuintaGrabadaRepository = rentaQuintaGrabadaRepository; this.rentaQuintaGrabadaRepository = rentaQuintaGrabadaRepository;
this.proyeccionDocenteHoraRepository = proyeccionDocenteHoraRepository; this.proyeccionDocenteHoraRepository = proyeccionDocenteHoraRepository;
this.entityManager = entityManager; this.entityManager = entityManager;
this.organizacionRepository = organizacionRepository;
} }
/** /**
...@@ -99,8 +104,33 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -99,8 +104,33 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
* {@code grabarMasivo}. * {@code grabarMasivo}.
*/ */
@Override @Override
@Transactional @Transactional(readOnly = true)
public Page<RentaQuintaResumenDTO> buscar(RentaQuintaFiltroDTO filtro) { public Page<RentaQuintaResumenDTO> buscar(RentaQuintaFiltroDTO 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) {
//grabarSegunFiltro(filtro); //grabarSegunFiltro(filtro);
Page<DocumentoResumenProjection> documentos = repository.buscarDocumentos( Page<DocumentoResumenProjection> documentos = repository.buscarDocumentos(
filtro.periodo(), filtro.periodo(),
...@@ -112,7 +142,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -112,7 +142,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)
...@@ -126,6 +156,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -126,6 +156,7 @@ 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()); 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());
...@@ -153,7 +184,8 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -153,7 +184,8 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
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()));
}); });
} }
...@@ -339,15 +371,23 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -339,15 +371,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());
......
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