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
| `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`
# 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.
| `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.
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
consttabla=$('#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.
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).