Backup de avance: migración de identificación a codper

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parent 3514dbdb
# planilla-be # planilla-be
Backend de planilla (Saco Oliveros). Módulo inicial: **renta de quinta categoría** — consulta Backend de planilla Saco Oliveros. Java 25, Spring Boot 4.1.1, WAR, JPA/Hibernate 7 y PostgreSQL.
de retenciones mensuales y cálculo de la proyección anual por trabajador. No escribir, generar ni sugerir tests salvo pedido explícito.
## Stack ## Configuración y comandos
- Java 25 (`pom.xml`), Spring Boot 4.1.1, empaquetado **war** (`ServletInitializer`) - `src/main/resources/application.yml` no está versionado: contiene credenciales.
- Spring Data JPA (Hibernate 7) + PostgreSQL - Copiar `application.example.yml` y completar las variables DB_URL, DB_USERNAME, DB_PASSWORD.
- springdoc-openapi → Swagger UI en `/docs` - La conexión local inspeccionada usa **soporte**, esquema **planilla**.
- Lombok (`@Getter/@Setter` en la entity) - Mantener `ddl-auto: validate`. Nunca usar update/create.
- **No se escriben tests.** No sugerir ni generar tests salvo pedido explícito. - `.\mvnw.cmd spring-boot:run`; `.\mvnw.cmd package -Dmaven.test.skip=true`.
- El puerto local configurado es 8088; Swagger UI: /docs.
## Configuración
## Identificación por codper (migración 2026-10-12)
`src/main/resources/application.yml` **no está versionado** (contiene credenciales). Copiar
`application.example.yml` y completar: La identidad del trabajador es **codper**, no el documento. Las consultas y cálculos se agrupan
por codper. Datos personales se consultan desde nuevo.personal por ese código. Local y unidad organizativa
```bash se conservan en pl_planilla; si son NULL, las consultas usan el catálogo como respaldo.
cp src/main/resources/application.example.yml src/main/resources/application.yml
export DB_PASSWORD='...' - pl_planilla: PK id_pl_planilla; conserva boleta_anio, boleta_mes, boleta_tipo_personal.
``` Tiene tipopla integer (reemplaza tipo_planilla), codlocal text, id_tipo_pago integer e
id_unidad_organizativa integer. No contiene numero_documento, apellido_paterno,
Datos del entorno de pruebas: PostgreSQL, base `prueba`, usuario `soporte`, esquema **`planilla`** apellido_materno, nombres ni tipo_pago.
(no `public`). - pl_renta_quinta: PK identity id_renta_quinta; codper, anio, mes y retencion;
UNIQUE (codper, anio, mes). No depende de una boleta ni separa retenciones por modalidad.
`ddl-auto: validate` — la tabla ya existe y la carga la hace un proceso batch/ETL externo. Nunca - pl_gratificacion: PK identity id_gratificacion; FK id_pl_planilla, UNIQUE por boleta;
poner `update` ni `create`. monto_basico, mes y anio NOT NULL. El trabajador se obtiene de pl_planilla.
- pl_utilidades: PK identity id_utilidad; FK id_pl_planilla, UNIQUE por boleta; monto_utilidad.
## Comandos - pl_retencion_otros_empleadores: PK identity id_retencion; codper, anio, mes,
importe, retencion, fecha_registro; UNIQUE (codper, anio, mes).
```bash - pl_renta_basico: PK (codper, anio, mes); monto_basico.
./mvnw spring-boot:run # levanta en :8080 - pl_proyeccion_docente_horas: identity id_proyeccion; codper, anio,
./mvnw package # genera el war mes, tipo_proyeccion 1/2, remun_bruta_futura, gratificacion;
``` UNIQUE por codper + tipo + año + mes. Importes nulos en el request se graban como 0.
- pl_periodo se eliminó. /api/v1/filtros/periodos devuelve periodos distintos de nuevo.periodo_asistencia
## Estructura como {idPeriodo, mes, anio}; idPeriodo = anio * 100 + mes.
``` nuevo.personal es una FOREIGN TABLE: no se puede crear una FK convencional hacia ella.
so.planilla_be No modificar sus columnas ni los catálogos externos. El backend valida codper positivo.
├── config/OpenApiConfig DOC1/DOC2 se determinan exclusivamente por **nuevo.personal.id_tipo_pago**:
└── rentaquinta/ 1 = por hora, 2 = fijo/mensual. El parámetro tipoPlanilla filtra o valida esta modalidad;
├── entity/RentaQuinta mapeo 1:1 de las 115 columnas de planilla.renta_quinta no sobrescribe la del trabajador. ADM sigue usando boleta_tipo_personal = ADM.
├── repository/ RentaQuintaRepository + RentaQuintaResumenProjection
├── dto/ Resumen (grilla), Detalle (boleta), Historial, Filtro ### Transición de datos
├── calculo/ RentaQuintaCalculadora, UitProperties, CalculoRentaQuinta
├── mapper/RentaQuintaMapper manual (sin MapStruct) La migración db/migrar-codper.sql se aplicó en soporte dentro de una transacción.
├── service/ + service/impl/ Respaldo: respaldo_codper_20261012 (incluye los nuevos IDs y las columnas originales).
├── controller/ No volver a ejecutar la migración en esa base.
└── exception/ excepción de negocio + GlobalExceptionHandler codper y las FK pendientes quedan nullable temporalmente. No asignar códigos automáticamente.
``` Consultar db/revisar-pendientes-codper.sql para completar códigos y enlaces posteriormente.
No borrar el respaldo antes de resolverlos.
## Endpoints Las consultas por codper excluyen las boletas sin código; la consulta por ID permite leerlas.
No existe ninguna consulta del cálculo que use el respaldo como fuente.
| Método | Ruta | Descripción | El proceso batch/ETL debe cargar codper y las FK antes de normalizar estas columnas a NOT NULL.
|---|---|---|
| GET | `/api/v1/renta-quinta/listado` | Grilla paginada de retenciones, con cálculo | ## API
| 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/listado: Page, anio/mes obligatorios, filtros opcionales,
| GET | `/api/v1/renta-quinta/boleta` | Boleta por `codper` + `anio` + `mes` | page=0, size=20, tope 100. Una fila por codper.
| GET | `/api/v1/renta-quinta/historial/{codper}/{anio}` | Meses del trabajador en el periodo | - GET /api/v1/renta-quinta/listado/completo: lista JSON, mismos filtros sin page/size.
Usado por la tabla web y Excel: selección, catálogos y cálculo se cargan una sola vez,
`anio` y `mes` son los nombres de parámetro usados en **todos** los endpoints (`listado`, sin conteos ni descargas sucesivas; retención/variación se filtran sobre ese cálculo.
`detallado`, `boleta`, `historial`) — no usar `periodo`/`mesCorte` en la firma pública aunque - GET /api/v1/renta-quinta/{id}: boleta por id_pl_planilla.
internamente `RentaQuintaFiltroDTO` y `DatosCalculo` sigan llamándolos así. - GET /api/v1/renta-quinta/detallado: codper + anio + mes; tipoPlanilla opcional.
- GET /api/v1/renta-quinta/boleta: codper + anio + mes; exige una boleta única.
### Mantenimiento de proyección docente por hora - GET /api/v1/renta-quinta/historial/{codper}/{anio}.
- POST /api/v1/renta-quinta/grabar: codper + anio + mes + tipoPlanilla.
`POST /api/v1/proyeccion-docente-hora` (paquete `proyecciondocentehora`): recibe una lista de filas Guarda la retención por codper, anio y mes. Permite meses estimados sin boleta del mes;
`{proyeccionAnio, proyeccionMes, numeroDocumento, boletaTipoPersonal (DOC|GRA), remunBrutaFutura, el cálculo requiere historial de referencia hasta el corte.
gratificacion}` y las graba **solo** en `planilla.pl_proyeccion_renta_docentehora`. La tabla no tiene - POST /api/v1/renta-quinta/grabar-masivo: mismos filtros del listado, todo o nada.
unique salvo el PK identity (`id_proyeccion`, ALWAYS): la clave lógica es documento + tipo + año + mes, - GET /api/v1/proyeccion-docente-hora/{codper}/{anio}; POST lista de filas con codper;
y si existe se actualiza en vez de duplicar. `DELETE /api/v1/proyeccion-docente-hora/{idProyeccion}` DELETE /api/v1/proyeccion-docente-hora/{idProyeccion}.
elimina por PK (204; 404 si no existe). `GET /api/v1/proyeccion-docente-hora/{numeroDocumento}/{anio}` - Retenciones de otros empleadores: GET /api/v1/retencion-otros-empleadores/{codper}/{anio};
lista las filas del docente con `idProyeccion` y `nombreCompleto` (de `nuevo.personal`, temporal). Todo o nada (una transacción). Importes nulos → 0. POST, PUT, DELETE /api/v1/retencion-otros-empleadores/{codper}/{anio}/{mes}.
- Mantenimiento docente-grati-diciembre-completa permanece desactivado.
### Filtros de la grilla
Los nombres públicos son anio y mes. tipoPlanilla acepta solo ADM, DOC1, DOC2; DOC responde 400.
`anio` y `mes` son **obligatorios**; el resto es opcional y, si no se envía, no filtra. Todos son documentoOApellidos sigue siendo texto de búsqueda sobre nuevo.personal, no una clave.
`Integer` salvo `documentoOApellidos`, que es texto (busca por `numero_documento` o por apellidos Los datos personales de los DTO de consulta se conservan para mostrar información y se derivan
+ nombres, con `LIKE` en mayúsculas), y `tipoPlanilla`. del catálogo; no son columnas persistidas en planilla.
El nombre JSON idRentaQuinta de la boleta conserva compatibilidad: allí representa id_pl_planilla;
`tipoPlanilla` acepta **solo `ADM`, `DOC1`, `DOC2`** (`DOC` sin dígito responde 400). Lo reciben `listado`, `detallado`, `grabar` y `grabar-masivo`. Las letras son la PK propia de pl_renta_quinta solo identifica el registro de retención.
`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` ## Reglas del cálculo
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 La calculadora conserva las reglas anteriores; la migración solo cambia la identificación
PK). Esa columna la crea `docs/alter-pl-renta-quinta-tipo-pago.sql`: correrlo antes de desplegar o y el acceso a datos.
`ddl-auto: validate` no deja arrancar. - Historial: meses anteriores; planilla cargada: mes de corte; proyección: posteriores.
- ADM: sueldo de referencia por meses restantes; gratificaciones pendientes por contrato,
| tipoPlanilla | boleta_tipo_personal | tipo_pago | Modalidad | Proyección del sueldo bruto | básico + asignación familiar, factor 1.09; sin fecha de contrato la gratificación es 0.
|---|---|---|---|---| - DOC2: igual, pero diciembre en sueldo ÷30×16 y gratificación 5 meses +16 días.
| `ADM` | `ADM` | — | `MONTO_FIJO` | remuneración de referencia × meses restantes | Una gratificación con mes = 12 y anio del cálculo, vinculada a una boleta del codper, habilita
| `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 | diciembre completo (30 días) solo en la gratificación; monto_basico no determina la excepción.
| `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 | - Ingreso en junio después del día 1 (ADM/DOC2): julio = 0; sumar días de junio a diciembre.
- DOC1: remun_bruta_futura del mes de corte reemplaza el sueldo; sumar meses posteriores.
En DOC1 cambian la proyección del sueldo y las gratificaciones; UIT, tramos y divisor son iguales. Detalle Gratificaciones pendientes de filas tipo_proyeccion = 2 en julio/diciembre, sin factor 1.09; sin fila = 0.
en `docs/circuito-api-renta-quinta.md`. - Periodos disponibles: nuevo.periodo_asistencia. Los días naturales del mes de contrato se calculan
con YearMonth, sin consultar el catálogo por trabajador; UIT por año desde pl_uit mediante UitCache.
Los filtros de catálogo (`sede`, `areaGeneral`, `subArea`, `cargo`) llegan como - Deducción: 7 UIT; tramos 8/14/17/20/30% con límites 5/20/35/45 UIT.
número y se comparan **como texto** contra columnas TEXT: el service los convierte con - Divisor: ene-mar 12, abr 9, may-jul 8, ago 5, set-nov 4, dic 1; retención no negativa.
`codigo()`. Las columnas `char(n)` se comparan con `trim()` por el padding de PostgreSQL. - pl_renta_basico solo determina la base para gratificaciones pendientes; no sustituye el
básico de la boleta. Utilidades se aplican exclusivamente a su boleta vinculada.
> Pendiente: en los datos actuales esas columnas guardan texto (`sede='BARRANCO'`, - Separar las boletas del contexto JPA antes de ajustar importes para cálculo.
> `boleta_tipo_personal='ADM'`) y `codlocal` viene vacío, así que un filtro numérico no matchea El grabado de retenciones modifica solo pl_renta_quinta.
> nada. Cuando existan catálogos con id, o si los dropdowns pasan a mandar el texto, cambiar el - Historial y retenciones se consultan por codper; la modalidad viene del catálogo actual,
> tipo en `RentaQuintaFiltroDTO` y quitar `codigo()`. también al consultar boletas históricas, por decisión del usuario.
`page` por defecto 0, `size` 20 y **tope 100**. ## Errores y límites
## Reglas de dominio GlobalExceptionHandler devuelve {timestamp, status, message}; no exponer stack traces.
400 para codper inválido, modalidad incompatible y consulta de boleta ambigua;
### Integración de master con mzuniga 404 para boleta inexistente. Char(n) se mapea con columnDefinition = "bpchar".
No escribir boletas desde el API: carga batch/ETL.
- El cálculo de gratificación por contrato de master tiene prioridad. La matriz mensual usa Autenticación, autorización, exportación Excel y /listado/datatables siguen fuera de alcance.
`RentaQuintaCalculadora.gratificacionDelMes` con los mismos `DatosCalculo` del detalle. El listado cruza personal, área, subárea, cargo y sede mediante INNER JOIN;
- El listado agrega datos organizativos desde `nuevo.personal`, `nuevo.area`, `nuevo.subarea` el DTO conserva codper y datos personales e incluye codlocal, idSede y sede.
y `nuevo.cargo`. Los filtros `tieneVariacion` y `tieneRetencion` son Boolean opcionales y se Materializar personal con sus catálogos y sedes antes de aplicar filtros locales evita
aplican al cálculo antes de paginar; pueden requerir calcular todos los candidatos. productos cartesianos remotos y planes genéricos lentos de mysql_fdw.
- `GET /api/v1/filtros/periodos` requiere ejecutar `db/crear-pl-periodo.sql`; no se ejecuta En OrganizacionPersonalRepository, mantener los codper como selección materializada:
automáticamente. Para años nuevos, mantener el catálogo de periodos. IN/ANY parametrizado sobre la FOREIGN TABLE falla al reutilizar prepared statements.
- El mapeo de boletas usa las columnas de producción `tipo_planilla`, `neto_pagado` y La migración db/renta-quinta-por-codper-periodo.sql conserva los registros actuales,
`diferencia`. El detalle conserva el nombre JSON `tipoDocumento` por compatibilidad. sin reinsertar el respaldo histórico; aborta ante claves pendientes o duplicadas.
- Separar las entidades de boleta del contexto JPA antes de ajustar utilidades para el cálculo; ### Revisión de clasificación 2026-10-12
la escritura de retenciones solo debe afectar a `pl_renta_quinta`. ADM: pl_planilla.tipopla IN (5,6). DOC: tipopla IN (1,3), modalidad desde nuevo.personal.id_tipo_pago (1=DOC1,2=DOC2).
- La comparación con el mes anterior distingue DOC1 de DOC2 mediante `tipo_pago`. No usar boleta_tipo_personal para clasificar o filtrar el cálculo.
- `/listado/datatables` sigue pendiente: su documento describe una propuesta, no una ruta pl_gratificacion tiene codper nullable positivo: id_pl_planilla tiene prioridad para obtener codper y periodo;
publicada. `/listado` conserva la respuesta Page y los parámetros `page` y `size`. sin vínculo se usan g.codper, g.anio, g.mes. No asignar automáticamente códigos a gratificaciones.
### 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 documento tiene un registro de diciembre del año en `planilla.pl_gratificacion`
(filtrando por `mes = 12`, sin usar `monto_basico`), 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. El mantenimiento
en `/api/v1/docente-grati-diciembre-completa` (paquete `docentegratidiciembre`, ver
`docs/api-docente-grati-diciembre-completa.md`) aún usa la tabla anterior y no determina la excepción;
su adaptación está pendiente de definición.
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
# planilla-be # planilla-be
Backend de planilla (Saco Oliveros). Módulo inicial: **renta de quinta categoría** — consulta Backend de planilla Saco Oliveros. Java 25, Spring Boot 4.1.1, WAR, JPA/Hibernate 7 y PostgreSQL.
de retenciones mensuales y cálculo de la proyección anual por trabajador. No escribir, generar ni sugerir tests salvo pedido explícito.
## Stack ## Configuración y comandos
- Java 25 (`pom.xml`), Spring Boot 4.1.1, empaquetado **war** (`ServletInitializer`) - `src/main/resources/application.yml` no está versionado: contiene credenciales.
- Spring Data JPA (Hibernate 7) + PostgreSQL - Copiar `application.example.yml` y completar las variables DB_URL, DB_USERNAME, DB_PASSWORD.
- springdoc-openapi → Swagger UI en `/docs` - La conexión local inspeccionada usa **soporte**, esquema **planilla**.
- Lombok (`@Getter/@Setter` en la entity) - Mantener `ddl-auto: validate`. Nunca usar update/create.
- **No se escriben tests.** No sugerir ni generar tests salvo pedido explícito. - `.\mvnw.cmd spring-boot:run`; `.\mvnw.cmd package -Dmaven.test.skip=true`.
- El puerto local configurado es 8088; Swagger UI: /docs.
## Configuración
## Identificación por codper (migración 2026-10-12)
`src/main/resources/application.yml` **no está versionado** (contiene credenciales). Copiar
`application.example.yml` y completar: La identidad del trabajador es **codper**, no el documento. Las consultas y cálculos se agrupan
por codper. Datos personales se consultan desde nuevo.personal por ese código. Local y unidad organizativa
```bash se conservan en pl_planilla; si son NULL, las consultas usan el catálogo como respaldo.
cp src/main/resources/application.example.yml src/main/resources/application.yml
export DB_PASSWORD='...' - pl_planilla: PK id_pl_planilla; conserva boleta_anio, boleta_mes, boleta_tipo_personal.
``` Tiene tipopla integer (reemplaza tipo_planilla), codlocal text, id_tipo_pago integer e
id_unidad_organizativa integer. No contiene numero_documento, apellido_paterno,
Datos del entorno de pruebas: PostgreSQL, base `prueba`, usuario `soporte`, esquema **`planilla`** apellido_materno, nombres ni tipo_pago.
(no `public`). - pl_renta_quinta: PK identity id_renta_quinta; FK id_pl_planilla, UNIQUE por boleta;
retencion. No duplica codper, documento, año, mes, tipo personal ni tipo pago.
`ddl-auto: validate` — la tabla ya existe y la carga la hace un proceso batch/ETL externo. Nunca - pl_gratificacion: PK identity id_gratificacion; FK id_pl_planilla, UNIQUE por boleta;
poner `update` ni `create`. monto_basico, mes y anio NOT NULL. El trabajador se obtiene de pl_planilla.
- pl_utilidades: PK identity id_utilidad; FK id_pl_planilla, UNIQUE por boleta; monto_utilidad.
## Comandos - pl_retencion_otros_empleadores: PK identity id_retencion; codper, anio, mes,
importe, retencion, fecha_registro; UNIQUE (codper, anio, mes).
```bash - pl_renta_basico: PK (codper, anio, mes); monto_basico.
./mvnw spring-boot:run # levanta en :8080 - pl_proyeccion_docente_horas: identity id_proyeccion; codper, anio,
./mvnw package # genera el war mes, tipo_proyeccion 1/2, remun_bruta_futura, gratificacion;
``` UNIQUE por codper + tipo + año + mes. Importes nulos en el request se graban como 0.
- pl_periodo se eliminó. /api/v1/filtros/periodos devuelve periodos distintos de nuevo.periodo_asistencia
## Estructura como {mes, anio}, sin idPeriodo.
``` nuevo.personal es una FOREIGN TABLE: no se puede crear una FK convencional hacia ella.
so.planilla_be No modificar sus columnas ni los catálogos externos. El backend valida codper positivo.
├── config/OpenApiConfig DOC1/DOC2 se determinan exclusivamente por **nuevo.personal.id_tipo_pago**:
└── rentaquinta/ 1 = por hora, 2 = fijo/mensual. El parámetro tipoPlanilla filtra o valida esta modalidad;
├── entity/RentaQuinta mapeo 1:1 de las 115 columnas de planilla.renta_quinta no sobrescribe la del trabajador. ADM sigue usando boleta_tipo_personal = ADM.
├── repository/ RentaQuintaRepository + RentaQuintaResumenProjection
├── dto/ Resumen (grilla), Detalle (boleta), Historial, Filtro ### Transición de datos
├── calculo/ RentaQuintaCalculadora, UitProperties, CalculoRentaQuinta
├── mapper/RentaQuintaMapper manual (sin MapStruct) La migración db/migrar-codper.sql se aplicó en soporte dentro de una transacción.
├── service/ + service/impl/ Respaldo: respaldo_codper_20261012 (incluye los nuevos IDs y las columnas originales).
├── controller/ No volver a ejecutar la migración en esa base.
└── exception/ excepción de negocio + GlobalExceptionHandler codper y las FK pendientes quedan nullable temporalmente. No asignar códigos automáticamente.
``` Consultar db/revisar-pendientes-codper.sql para completar códigos y enlaces posteriormente.
No borrar el respaldo antes de resolverlos.
## Endpoints Las consultas por codper excluyen las boletas sin código; la consulta por ID permite leerlas.
No existe ninguna consulta del cálculo que use el respaldo como fuente.
| Método | Ruta | Descripción | El proceso batch/ETL debe cargar codper y las FK antes de normalizar estas columnas a NOT NULL.
|---|---|---|
| GET | `/api/v1/renta-quinta/listado` | Grilla paginada de retenciones, con cálculo | ## API
| 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/listado: Page, anio/mes obligatorios, filtros opcionales,
| GET | `/api/v1/renta-quinta/boleta` | Boleta por `codper` + `anio` + `mes` | page=0, size=20, tope 100. Una fila por codper.
| GET | `/api/v1/renta-quinta/historial/{codper}/{anio}` | Meses del trabajador en el periodo | - GET /api/v1/renta-quinta/{id}: boleta por id_pl_planilla.
- GET /api/v1/renta-quinta/detallado: codper + anio + mes; tipoPlanilla opcional.
`anio` y `mes` son los nombres de parámetro usados en **todos** los endpoints (`listado`, - GET /api/v1/renta-quinta/boleta: codper + anio + mes; exige una boleta única.
`detallado`, `boleta`, `historial`) — no usar `periodo`/`mesCorte` en la firma pública aunque - GET /api/v1/renta-quinta/historial/{codper}/{anio}.
internamente `RentaQuintaFiltroDTO` y `DatosCalculo` sigan llamándolos así. - POST /api/v1/renta-quinta/grabar: codper + anio + mes + tipoPlanilla.
Guarda la retención por FK a una boleta real y única del mes. No guardar la estimación
### Mantenimiento de proyección docente por hora de un mes sin boleta ni escoger arbitrariamente entre varias boletas.
- POST /api/v1/renta-quinta/grabar-masivo: mismos filtros del listado, todo o nada.
`POST /api/v1/proyeccion-docente-hora` (paquete `proyecciondocentehora`): recibe una lista de filas - GET /api/v1/proyeccion-docente-hora/{codper}/{anio}; POST lista de filas con codper;
`{proyeccionAnio, proyeccionMes, numeroDocumento, boletaTipoPersonal (DOC|GRA), remunBrutaFutura, DELETE /api/v1/proyeccion-docente-hora/{idProyeccion}.
gratificacion}` y las graba **solo** en `planilla.pl_proyeccion_renta_docentehora`. La tabla no tiene - Retenciones de otros empleadores: GET /api/v1/retencion-otros-empleadores/{codper}/{anio};
unique salvo el PK identity (`id_proyeccion`, ALWAYS): la clave lógica es documento + tipo + año + mes, POST, PUT, DELETE /api/v1/retencion-otros-empleadores/{codper}/{anio}/{mes}.
y si existe se actualiza en vez de duplicar. `DELETE /api/v1/proyeccion-docente-hora/{idProyeccion}` - Mantenimiento docente-grati-diciembre-completa permanece desactivado.
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. Los nombres públicos son anio y mes. tipoPlanilla acepta solo ADM, DOC1, DOC2; DOC responde 400.
documentoOApellidos sigue siendo texto de búsqueda sobre nuevo.personal, no una clave.
### Filtros de la grilla Los datos personales de los DTO de consulta se conservan para mostrar información y se derivan
del catálogo; no son columnas persistidas en planilla.
`anio` y `mes` son **obligatorios**; el resto es opcional y, si no se envía, no filtra. Todos son El nombre JSON idRentaQuinta de la boleta conserva compatibilidad: allí representa id_pl_planilla;
`Integer` salvo `documentoOApellidos`, que es texto (busca por `numero_documento` o por apellidos la PK propia de pl_renta_quinta solo identifica el registro de retención.
+ nombres, con `LIKE` en mayúsculas), y `tipoPlanilla`.
## Reglas del cálculo
`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` La calculadora conserva las reglas anteriores; la migración solo cambia la identificación
(`char(1)`, enum `calculo/TipoPago`); filtra por ambas columnas y `calculo/ModalidadCalculo` y el acceso a datos.
decide la proyección. Si no se envía, se toma de la boleta. En `pl_renta_quinta` se graba - Historial: meses anteriores; planilla cargada: mes de corte; proyección: posteriores.
`boleta_tipo_personal` (`ADM`/`DOC`) y el dígito en `tipo_pago` (`1`/`2`, vacío en ADM; es parte de la - ADM: sueldo de referencia por meses restantes; gratificaciones pendientes por contrato,
PK). Esa columna la crea `docs/alter-pl-renta-quinta-tipo-pago.sql`: correrlo antes de desplegar o básico + asignación familiar, factor 1.09; sin fecha de contrato la gratificación es 0.
`ddl-auto: validate` no deja arrancar. - DOC2: igual, pero diciembre en sueldo ÷30×16 y gratificación 5 meses +16 días.
Una gratificación con mes = 12 y anio del cálculo, vinculada a una boleta del codper, habilita
| tipoPlanilla | boleta_tipo_personal | tipo_pago | Modalidad | Proyección del sueldo bruto | diciembre completo (30 días) solo en la gratificación; monto_basico no determina la excepción.
|---|---|---|---|---| - Ingreso en junio después del día 1 (ADM/DOC2): julio = 0; sumar días de junio a diciembre.
| `ADM` | `ADM` | — | `MONTO_FIJO` | remuneración de referencia × meses restantes | - DOC1: remun_bruta_futura del mes de corte reemplaza el sueldo; sumar meses posteriores.
| `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 | Gratificaciones pendientes de filas tipo_proyeccion = 2 en julio/diciembre, sin factor 1.09; sin fila = 0.
| `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 | - Calendario: nuevo.periodo_asistencia; UIT por año desde parametro_uit mediante UitCache.
- Deducción: 7 UIT; tramos 8/14/17/20/30% con límites 5/20/35/45 UIT.
En DOC1 cambian la proyección del sueldo y las gratificaciones; UIT, tramos y divisor son iguales. Detalle - Divisor: ene-mar 12, abr 9, may-jul 8, ago 5, set-nov 4, dic 1; retención no negativa.
en `docs/circuito-api-renta-quinta.md`. - pl_renta_basico solo determina la base para gratificaciones pendientes; no sustituye el
básico de la boleta. Utilidades se aplican exclusivamente a su boleta vinculada.
Los filtros de catálogo (`sede`, `areaGeneral`, `subArea`, `cargo`) llegan como - Separar las boletas del contexto JPA antes de ajustar importes para cálculo.
número y se comparan **como texto** contra columnas TEXT: el service los convierte con El grabado de retenciones modifica solo pl_renta_quinta.
`codigo()`. Las columnas `char(n)` se comparan con `trim()` por el padding de PostgreSQL. - Historial y retenciones se consultan por codper; la modalidad viene del catálogo actual,
también al consultar boletas históricas, por decisión del usuario.
> 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 ## Errores y límites
> nada. Cuando existan catálogos con id, o si los dropdowns pasan a mandar el texto, cambiar el
> tipo en `RentaQuintaFiltroDTO` y quitar `codigo()`. GlobalExceptionHandler devuelve {timestamp, status, message}; no exponer stack traces.
400 para codper inválido, modalidad incompatible y boletas ambiguas al grabar;
`page` por defecto 0, `size` 20 y **tope 100**. 404 para boleta inexistente. Char(n) se mapea con columnDefinition = "bpchar".
No escribir boletas desde el API: carga batch/ETL.
## Reglas de dominio Autenticación, autorización, exportación Excel y /listado/datatables siguen fuera de alcance.
### 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 documento tiene un registro de diciembre del año en `planilla.pl_gratificacion`
(filtrando por `mes = 12`, sin usar `monto_basico`), 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. El mantenimiento
en `/api/v1/docente-grati-diciembre-completa` (paquete `docentegratidiciembre`, ver
`docs/api-docente-grati-diciembre-completa.md`) aún usa la tabla anterior y no determina la excepción;
su adaptación está pendiente de definición.
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
-- El vínculo a la boleta tiene prioridad; sin vínculo se usa codper + anio + mes.
SET LOCAL lock_timeout = '10s';
ALTER TABLE planilla.pl_gratificacion ADD COLUMN codper integer;
ALTER TABLE planilla.pl_gratificacion ADD CONSTRAINT ck_gratificacion_codper_positivo CHECK (codper > 0);
CREATE INDEX ix_gratificacion_codper_periodo ON planilla.pl_gratificacion (codper, anio, mes) WHERE id_pl_planilla IS NULL;
-- No se asignan códigos automáticamente: los registros existentes siguen pendientes.
SELECT count(*) total,count(codper) con_codper FROM planilla.pl_gratificacion;
-- Ejecutar completo en una transacción, después de migrar-codper.sql.
-- No completa codper ni los nuevos atributos del trabajador.
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '30s';
-- Renombrar y convertir conserva cualquier código numérico existente.
-- Un valor no numérico cancela toda la transacción, sin descartarlo silenciosamente.
ALTER TABLE planilla.pl_planilla RENAME COLUMN tipo_planilla TO tipopla;
ALTER TABLE planilla.pl_planilla
ALTER COLUMN tipopla TYPE integer USING nullif(trim(tipopla), '')::integer,
ADD COLUMN codlocal text,
ADD COLUMN id_tipo_pago integer,
ADD COLUMN id_unidad_organizativa integer;
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'planilla' AND table_name = 'pl_planilla'
AND column_name IN ('tipopla','tipo_planilla','codlocal','id_tipo_pago','id_unidad_organizativa')
ORDER BY column_name;
-- Catálogo del filtro Periodo de renta de quinta categoría. -- Script retirado tras la migración a codper del 2026-10-12.
BEGIN; -- Ver db/migrar-codper.sql y docs/migracion-codper.md.
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;
-- Nueva tabla de proyecciones DOC1. No ejecutar la migración histórica migrar-codper.sql.
-- nuevo.personal es una FOREIGN TABLE: codper se conserva sin FK convencional.
CREATE TABLE planilla.pl_proyeccion_docente_horas (
id_proyeccion bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
codper integer NOT NULL CHECK (codper > 0),
anio integer NOT NULL CHECK (anio BETWEEN 1 AND 9999),
mes integer NOT NULL CHECK (mes BETWEEN 1 AND 12),
tipo_proyeccion integer NOT NULL CHECK (tipo_proyeccion IN (1, 2)),
remun_bruta_futura numeric(12,2) NOT NULL DEFAULT 0 CHECK (remun_bruta_futura >= 0),
gratificacion numeric(12,2) NOT NULL DEFAULT 0 CHECK (gratificacion >= 0),
CONSTRAINT uq_proyeccion_docente_horas UNIQUE (codper, anio, mes, tipo_proyeccion),
CONSTRAINT ck_proyeccion_docente_horas_importes CHECK (
(tipo_proyeccion = 1 AND gratificacion = 0)
OR (tipo_proyeccion = 2 AND remun_bruta_futura = 0 AND mes IN (7, 12))
)
);
COMMENT ON TABLE planilla.pl_proyeccion_docente_horas IS
'Proyecciones por codper y periodo de docentes por hora: 1 sueldo, 2 gratificación.';
COMMENT ON COLUMN planilla.pl_proyeccion_docente_horas.tipo_proyeccion IS
'1: sueldo (remun_bruta_futura); 2: gratificación de julio o diciembre (gratificacion).';
-- Migración de la base soporte. Ejecutar el archivo completo en UNA transacción.
-- No completa codper: esa actualización se realizará después.
-- Las FK quedan nullable para conservar registros sin una boleta única.
-- El esquema de respaldo conserva documentos, periodos y nuevos IDs para resolverlos.
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '60s';
DO $$
BEGIN
IF current_database() <> 'soporte' THEN
RAISE EXCEPTION 'Esta migración fue revisada para soporte, base actual: %', current_database();
END IF;
IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'respaldo_codper_20261012') THEN
RAISE EXCEPTION 'Ya existe respaldo_codper_20261012. No repetir esta migración.';
END IF;
END $$;
LOCK TABLE planilla.pl_planilla, planilla.pl_renta_quinta, planilla.pl_gratificacion,
planilla.pl_utilidades, planilla.pl_retencion_otros_empleadores IN ACCESS EXCLUSIVE MODE;
ALTER TABLE planilla.pl_renta_quinta
ADD COLUMN id_renta_quinta bigint GENERATED ALWAYS AS IDENTITY,
ADD COLUMN id_pl_planilla bigint;
ALTER TABLE planilla.pl_gratificacion
ADD COLUMN id_gratificacion bigint GENERATED ALWAYS AS IDENTITY,
ADD COLUMN id_pl_planilla bigint;
ALTER TABLE planilla.pl_utilidades
ADD COLUMN id_utilidad bigint GENERATED ALWAYS AS IDENTITY,
ADD COLUMN id_pl_planilla bigint;
ALTER TABLE planilla.pl_retencion_otros_empleadores
ADD COLUMN id_retencion bigint GENERATED ALWAYS AS IDENTITY,
ADD COLUMN codper integer;
CREATE SCHEMA respaldo_codper_20261012;
CREATE TABLE respaldo_codper_20261012.pl_planilla AS TABLE planilla.pl_planilla;
CREATE TABLE respaldo_codper_20261012.pl_renta_quinta AS TABLE planilla.pl_renta_quinta;
CREATE TABLE respaldo_codper_20261012.pl_gratificacion AS TABLE planilla.pl_gratificacion;
CREATE TABLE respaldo_codper_20261012.pl_utilidades AS TABLE planilla.pl_utilidades;
CREATE TABLE respaldo_codper_20261012.pl_retencion_otros_empleadores AS TABLE planilla.pl_retencion_otros_empleadores;
CREATE TABLE respaldo_codper_20261012.pl_periodo AS TABLE planilla.pl_periodo;
-- Resolver solo coincidencias únicas con las columnas antiguas, antes de eliminarlas.
WITH candidatos AS (
SELECT r.id_renta_quinta, min(p.id_pl_planilla) id_planilla
FROM planilla.pl_renta_quinta r
JOIN planilla.pl_planilla p
ON trim(p.numero_documento) = trim(r.numero_documento)
AND p.boleta_anio = r.anio AND p.boleta_mes = r.mes
AND trim(p.boleta_tipo_personal) = trim(r.boleta_tipo_personal)
AND coalesce(trim(p.tipo_pago), '') = coalesce(trim(r.tipo_pago), '')
GROUP BY r.id_renta_quinta HAVING count(*) = 1
)
UPDATE planilla.pl_renta_quinta r SET id_pl_planilla = c.id_planilla
FROM candidatos c WHERE c.id_renta_quinta = r.id_renta_quinta;
WITH candidatos AS (
SELECT g.id_gratificacion, min(p.id_pl_planilla) id_planilla
FROM planilla.pl_gratificacion g
JOIN planilla.pl_planilla p ON trim(p.numero_documento) = trim(g.numero_documento)
AND p.boleta_anio = g.anio AND p.boleta_mes = g.mes
GROUP BY g.id_gratificacion HAVING count(*) = 1
)
UPDATE planilla.pl_gratificacion g SET id_pl_planilla = c.id_planilla
FROM candidatos c WHERE c.id_gratificacion = g.id_gratificacion;
WITH candidatos AS (
SELECT u.id_utilidad, min(p.id_pl_planilla) id_planilla
FROM planilla.pl_utilidades u
JOIN planilla.pl_planilla p ON trim(p.numero_documento) = trim(u.numero_documento)
AND p.boleta_anio = u.anio AND p.boleta_mes = u.mes
GROUP BY u.id_utilidad HAVING count(*) = 1
)
UPDATE planilla.pl_utilidades u SET id_pl_planilla = c.id_planilla
FROM candidatos c WHERE c.id_utilidad = u.id_utilidad;
ALTER TABLE planilla.pl_renta_quinta
DROP CONSTRAINT pl_renta_quinta_pkey,
DROP COLUMN numero_documento, DROP COLUMN anio, DROP COLUMN mes,
DROP COLUMN tipo_pago, DROP COLUMN boleta_tipo_personal,
ADD PRIMARY KEY (id_renta_quinta),
ADD CONSTRAINT fk_renta_quinta_planilla FOREIGN KEY (id_pl_planilla)
REFERENCES planilla.pl_planilla (id_pl_planilla),
ADD CONSTRAINT uq_renta_quinta_planilla UNIQUE (id_pl_planilla);
ALTER TABLE planilla.pl_gratificacion
DROP CONSTRAINT pl_gratificacion_1_pkey,
DROP COLUMN numero_documento, DROP COLUMN anio, DROP COLUMN mes, DROP COLUMN codper,
ADD PRIMARY KEY (id_gratificacion),
ADD CONSTRAINT fk_gratificacion_planilla FOREIGN KEY (id_pl_planilla)
REFERENCES planilla.pl_planilla (id_pl_planilla),
ADD CONSTRAINT uq_gratificacion_planilla UNIQUE (id_pl_planilla);
ALTER TABLE planilla.pl_utilidades
DROP CONSTRAINT pl_utilidades_pkey,
DROP COLUMN numero_documento, DROP COLUMN anio, DROP COLUMN mes, DROP COLUMN codper,
ADD PRIMARY KEY (id_utilidad),
ADD CONSTRAINT fk_utilidades_planilla FOREIGN KEY (id_pl_planilla)
REFERENCES planilla.pl_planilla (id_pl_planilla),
ADD CONSTRAINT uq_utilidades_planilla UNIQUE (id_pl_planilla);
ALTER TABLE planilla.pl_retencion_otros_empleadores
DROP CONSTRAINT retencion_otros_empleadores_pkey,
DROP COLUMN documento_trabajador,
ADD PRIMARY KEY (id_retencion),
ADD CONSTRAINT uq_otros_empleadores_codper_periodo UNIQUE (codper, anio, mes),
ADD CONSTRAINT ck_otros_empleadores_codper CHECK (codper > 0);
-- Año/mes permanecen aquí: las declaraciones y proyecciones pueden existir sin boleta.
ALTER TABLE planilla.pl_planilla
DROP COLUMN numero_documento, DROP COLUMN apellido_paterno, DROP COLUMN apellido_materno,
DROP COLUMN nombres, DROP COLUMN codlocal, DROP COLUMN unidad_organizativa, DROP COLUMN tipo_pago,
ADD CONSTRAINT ck_planilla_codper CHECK (codper > 0);
CREATE INDEX ix_planilla_codper_periodo ON planilla.pl_planilla (codper, boleta_anio, boleta_mes);
DROP TABLE planilla.pl_periodo;
-- Tablas requeridas por el backend, ausentes en la base inspeccionada.
CREATE TABLE planilla.pl_renta_basico (
codper integer NOT NULL CHECK (codper > 0),
anio integer NOT NULL,
mes integer NOT NULL CHECK (mes BETWEEN 1 AND 12),
monto_basico numeric(12,2) NOT NULL,
PRIMARY KEY (codper, anio, mes)
);
CREATE TABLE planilla.pl_proyeccion_renta_docentehora (
id_proyeccion bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
codper integer NOT NULL CHECK (codper > 0),
proyeccion_anio integer NOT NULL,
proyeccion_mes integer NOT NULL CHECK (proyeccion_mes BETWEEN 1 AND 12),
boleta_tipo_personal varchar(10) NOT NULL CHECK (boleta_tipo_personal IN ('DOC','GRA')),
remun_bruta_futura numeric(12,2),
gratificacion numeric(12,2),
CONSTRAINT uq_proyeccion_codper_tipo_periodo
UNIQUE (codper, boleta_tipo_personal, proyeccion_anio, proyeccion_mes)
);
-- Comprobar que se conservaron todas las filas de las tablas originales.
DO $$
DECLARE tabla text; originales bigint; actuales bigint;
BEGIN
FOREACH tabla IN ARRAY ARRAY['pl_planilla','pl_renta_quinta','pl_gratificacion',
'pl_utilidades','pl_retencion_otros_empleadores'] LOOP
EXECUTE format('SELECT count(*) FROM respaldo_codper_20261012.%I', tabla) INTO originales;
EXECUTE format('SELECT count(*) FROM planilla.%I', tabla) INTO actuales;
IF originales <> actuales THEN
RAISE EXCEPTION 'Conteo diferente en %: respaldo %, actual %', tabla, originales, actuales;
END IF;
END LOOP;
END $$;
SELECT 'pl_renta_quinta' tabla, count(*) total,
count(*) FILTER (WHERE id_pl_planilla IS NULL) pendientes FROM planilla.pl_renta_quinta
UNION ALL SELECT 'pl_gratificacion', count(*), count(*) FILTER (WHERE id_pl_planilla IS NULL)
FROM planilla.pl_gratificacion
UNION ALL SELECT 'pl_utilidades', count(*), count(*) FILTER (WHERE id_pl_planilla IS NULL)
FROM planilla.pl_utilidades;
-- Ejecutar manualmente en soporte. Actualiza solo boletas con codper NULL.
-- Los 8 documentos con ambos registros activos usan el codper normal confirmado.
-- Para los restantes se exige exactamente un registro con retirado = 0.
-- 40905256 queda pendiente porque ambos registros están retirados.
BEGIN;
SET LOCAL lock_timeout = '10s';
SET LOCAL statement_timeout = '60s';
CREATE TEMP TABLE personal_pendientes ON COMMIT DROP AS
SELECT codper, nullif(trim(numero_documento), '') documento,
tipopla, id_tipo_pago, id_unidad_organizativa,
nullif(trim(codlocal), '') codlocal, retirado
FROM nuevo.personal WHERE codper > 0;
WITH elegidos(documento, codper) AS (
VALUES ('09263286', 8126), ('40508088', 11251),
('41170404', 6196), ('46698954', 14258),
('48126858', 7558), ('70863318', 13832),
('72917783', 8811), ('76538696', 14186)
), unicos_activos AS (
SELECT documento, min(codper) codper
FROM personal_pendientes
WHERE retirado = 0 AND documento IS NOT NULL
AND documento NOT IN (SELECT documento FROM elegidos)
GROUP BY documento HAVING count(*) = 1
), seleccion AS (
SELECT * FROM elegidos
UNION ALL
SELECT * FROM unicos_activos
), actualizadas AS (
UPDATE planilla.pl_planilla p
SET codper = f.codper,
tipopla = coalesce(p.tipopla, f.tipopla),
id_tipo_pago = coalesce(p.id_tipo_pago, f.id_tipo_pago),
id_unidad_organizativa = coalesce(p.id_unidad_organizativa, f.id_unidad_organizativa),
codlocal = coalesce(p.codlocal, f.codlocal)
FROM respaldo_codper_20261012.pl_planilla b
JOIN seleccion s ON s.documento = nullif(trim(b.numero_documento), '')
JOIN personal_pendientes f ON f.codper = s.codper
AND f.documento = s.documento
AND f.retirado = 0
WHERE p.id_pl_planilla = b.id_pl_planilla
AND p.codper IS NULL
RETURNING p.id_pl_planilla, p.codper
)
SELECT count(*) AS boletas_actualizadas FROM actualizadas;
-- Con los datos revisados: 273 boletas actualizadas.
SELECT b.numero_documento, count(*) AS boletas_pendientes
FROM planilla.pl_planilla p
JOIN respaldo_codper_20261012.pl_planilla b USING (id_pl_planilla)
WHERE p.codper IS NULL
GROUP BY b.numero_documento ORDER BY b.numero_documento;
-- Con los datos revisados: documento 40905256, 2 boletas pendientes.
COMMIT;
SET LOCAL lock_timeout = '10s';
CREATE TEMP TABLE fuente_final AS
SELECT codper,tipopla,id_tipo_pago,id_unidad_organizativa,nullif(trim(codlocal),'') codlocal
FROM nuevo.personal
WHERE codper=169 AND trim(numero_documento)='40905256';
DO $$ BEGIN
IF (SELECT count(*) FROM fuente_final) <> 1 THEN
RAISE EXCEPTION 'No existe una fuente única para codper 169 y documento 40905256';
END IF;
END $$;
WITH actualizadas AS (
UPDATE planilla.pl_planilla p
SET codper=f.codper,tipopla=coalesce(p.tipopla,f.tipopla),
id_tipo_pago=coalesce(p.id_tipo_pago,f.id_tipo_pago),
id_unidad_organizativa=coalesce(p.id_unidad_organizativa,f.id_unidad_organizativa),
codlocal=coalesce(p.codlocal,f.codlocal)
FROM respaldo_codper_20261012.pl_planilla b CROSS JOIN fuente_final f
WHERE p.id_pl_planilla=b.id_pl_planilla AND p.codper IS NULL
AND trim(b.numero_documento)='40905256'
RETURNING p.id_pl_planilla
) SELECT count(*) boletas_actualizadas FROM actualizadas;
SELECT count(*) total,count(*) FILTER(WHERE codper IS NULL) sin_codper FROM planilla.pl_planilla;
-- Poblar únicamente coincidencias únicas; no resolver documentos ambiguos automáticamente.
-- Ejecutar dentro de una transacción. Usa el catálogo actual, no datos históricos organizativos.
SET LOCAL lock_timeout = '10s';
CREATE TEMP TABLE personal_fuente ON COMMIT DROP AS
SELECT codper, nullif(trim(numero_documento),'') documento, tipopla,
id_tipo_pago, id_unidad_organizativa, nullif(trim(codlocal),'') codlocal
FROM nuevo.personal WHERE codper > 0;
LOCK TABLE planilla.pl_planilla IN SHARE ROW EXCLUSIVE MODE;
CREATE TABLE respaldo_codper_20261012.pl_planilla_antes_poblar AS
SELECT id_pl_planilla,codper,tipopla,id_tipo_pago,id_unidad_organizativa,codlocal
FROM planilla.pl_planilla;
CREATE TEMP TABLE coincidencias_unicas ON COMMIT DROP AS
SELECT b.id_pl_planilla,min(f.codper) codper
FROM respaldo_codper_20261012.pl_planilla b
JOIN personal_fuente f ON f.documento=nullif(trim(b.numero_documento),'')
GROUP BY b.id_pl_planilla HAVING count(*)=1;
WITH actualizadas AS (
UPDATE planilla.pl_planilla p
SET codper=f.codper,tipopla=coalesce(p.tipopla,f.tipopla),
id_tipo_pago=coalesce(p.id_tipo_pago,f.id_tipo_pago),
id_unidad_organizativa=coalesce(p.id_unidad_organizativa,f.id_unidad_organizativa),
codlocal=coalesce(p.codlocal,f.codlocal)
FROM coincidencias_unicas c JOIN personal_fuente f ON f.codper=c.codper
WHERE p.id_pl_planilla=c.id_pl_planilla AND p.codper IS NULL
RETURNING p.id_pl_planilla
) SELECT count(*) boletas_actualizadas FROM actualizadas;
SELECT count(*) total,count(codper) con_codper,count(tipopla) con_tipopla,
count(id_tipo_pago) con_tipo_pago,count(id_unidad_organizativa) con_unidad,
count(codlocal) con_codlocal,count(*) FILTER(WHERE codper IS NULL) pendientes
FROM planilla.pl_planilla;
SELECT count(*) discrepancias FROM planilla.pl_planilla p
JOIN coincidencias_unicas c USING(id_pl_planilla)
JOIN personal_fuente f ON f.codper=c.codper
WHERE p.codper IS DISTINCT FROM f.codper;
-- Retención única por trabajador/año/mes, sin dependencia de una boleta.
-- Conserva IDs e importes existentes; no reinserta registros del respaldo histórico.
BEGIN;
SET LOCAL lock_timeout = '10s';
SET LOCAL statement_timeout = '60s';
LOCK TABLE planilla.pl_renta_quinta IN ACCESS EXCLUSIVE MODE;
CREATE TABLE IF NOT EXISTS respaldo_codper_20261012.pl_renta_quinta_antes_periodo AS
TABLE planilla.pl_renta_quinta;
ALTER TABLE planilla.pl_renta_quinta
ADD COLUMN IF NOT EXISTS codper integer,
ADD COLUMN IF NOT EXISTS anio integer,
ADD COLUMN IF NOT EXISTS mes integer;
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = 'planilla' AND table_name = 'pl_renta_quinta'
AND column_name = 'id_pl_planilla') THEN
EXECUTE 'UPDATE planilla.pl_renta_quinta r
SET codper = coalesce(r.codper, p.codper),
anio = coalesce(r.anio, p.boleta_anio),
mes = coalesce(r.mes, p.boleta_mes)
FROM planilla.pl_planilla p
WHERE p.id_pl_planilla = r.id_pl_planilla';
END IF;
END $$;
-- Para filas sin vínculo, solo recuperar el trabajador si el documento es inequívoco.
WITH personales AS (
SELECT nullif(trim(numero_documento), '') documento, min(codper) codper
FROM nuevo.personal WHERE codper > 0
GROUP BY nullif(trim(numero_documento), '') HAVING count(*) = 1
)
UPDATE planilla.pl_renta_quinta r
SET codper = coalesce(r.codper, p.codper),
anio = coalesce(r.anio, b.anio), mes = coalesce(r.mes, b.mes)
FROM respaldo_codper_20261012.pl_renta_quinta b
LEFT JOIN personales p ON p.documento = nullif(trim(b.numero_documento), '')
WHERE r.id_renta_quinta = b.id_renta_quinta
AND (r.codper IS NULL OR r.anio IS NULL OR r.mes IS NULL);
-- No borrar, sumar ni escoger arbitrariamente retenciones con claves repetidas.
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM planilla.pl_renta_quinta
WHERE codper IS NULL OR codper <= 0 OR anio IS NULL
OR anio NOT BETWEEN 1 AND 9999 OR mes IS NULL
OR mes NOT BETWEEN 1 AND 12 OR retencion IS NULL) THEN
RAISE EXCEPTION 'Hay retenciones con trabajador, periodo o importe pendientes; la migración se revierte';
END IF;
IF EXISTS (SELECT 1 FROM planilla.pl_renta_quinta
GROUP BY codper, anio, mes HAVING count(*) > 1) THEN
RAISE EXCEPTION 'Hay retenciones repetidas por codper/año/mes; resolverlas antes de migrar';
END IF;
END $$;
ALTER TABLE planilla.pl_renta_quinta
DROP CONSTRAINT IF EXISTS fk_renta_quinta_planilla,
DROP CONSTRAINT IF EXISTS uq_renta_quinta_planilla,
DROP COLUMN IF EXISTS id_pl_planilla,
ALTER COLUMN codper SET NOT NULL,
ALTER COLUMN anio SET NOT NULL,
ALTER COLUMN mes SET NOT NULL,
ALTER COLUMN retencion SET NOT NULL,
DROP CONSTRAINT IF EXISTS uq_renta_quinta_codper_periodo,
ADD CONSTRAINT uq_renta_quinta_codper_periodo UNIQUE (codper, anio, mes),
DROP CONSTRAINT IF EXISTS ck_renta_quinta_codper_periodo,
ADD CONSTRAINT ck_renta_quinta_codper_periodo
CHECK (codper > 0 AND anio BETWEEN 1 AND 9999 AND mes BETWEEN 1 AND 12);
COMMIT;
-- Ejecutar completo en una transacción después de migrar-codper.sql.
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '30s';
ALTER TABLE planilla.pl_gratificacion
ADD COLUMN mes integer,
ADD COLUMN anio integer;
-- Recuperar el periodo original de cada gratificación por su PK nueva.
UPDATE planilla.pl_gratificacion g
SET mes = b.mes, anio = b.anio
FROM respaldo_codper_20261012.pl_gratificacion b
WHERE b.id_gratificacion = g.id_gratificacion;
-- Para filas posteriores a la migración, usar el periodo de su boleta si existe.
UPDATE planilla.pl_gratificacion g
SET mes = coalesce(g.mes, p.boleta_mes), anio = coalesce(g.anio, p.boleta_anio)
FROM planilla.pl_planilla p
WHERE p.id_pl_planilla = g.id_pl_planilla AND (g.mes IS NULL OR g.anio IS NULL);
ALTER TABLE planilla.pl_gratificacion
ALTER COLUMN mes SET NOT NULL,
ALTER COLUMN anio SET NOT NULL,
ADD CONSTRAINT ck_gratificacion_mes CHECK (mes BETWEEN 1 AND 12);
SELECT count(*) AS total, count(*) FILTER (WHERE mes IS NULL OR anio IS NULL) AS sin_periodo
FROM planilla.pl_gratificacion;
-- Documentos y periodos conservados en el respaldo, para completar codper después.
SELECT p.id_pl_planilla, b.numero_documento, b.boleta_anio, b.boleta_mes
FROM planilla.pl_planilla p JOIN respaldo_codper_20261012.pl_planilla b USING (id_pl_planilla)
WHERE p.codper IS NULL ORDER BY p.id_pl_planilla;
-- Enlaces pendientes: elegir la boleta explícitamente cuando hay más de una candidata.
SELECT r.id_renta_quinta, b.numero_documento, b.anio, b.mes, b.boleta_tipo_personal, b.tipo_pago
FROM planilla.pl_renta_quinta r JOIN respaldo_codper_20261012.pl_renta_quinta b USING (id_renta_quinta)
WHERE r.id_pl_planilla IS NULL;
SELECT g.id_gratificacion, b.numero_documento, b.anio, b.mes
FROM planilla.pl_gratificacion g JOIN respaldo_codper_20261012.pl_gratificacion b USING (id_gratificacion)
WHERE g.id_pl_planilla IS NULL;
SELECT u.id_utilidad, b.numero_documento, b.anio, b.mes
FROM planilla.pl_utilidades u JOIN respaldo_codper_20261012.pl_utilidades b USING (id_utilidad)
WHERE u.id_pl_planilla IS NULL;
SELECT r.id_retencion, b.documento_trabajador, r.anio, r.mes
FROM planilla.pl_retencion_otros_empleadores r
JOIN respaldo_codper_20261012.pl_retencion_otros_empleadores b USING (id_retencion)
WHERE r.codper IS NULL;
-- tipo_pago en planilla.pl_renta_quinta, tal como quedó en la base de pruebas (prueba): -- Script retirado tras la migración a codper del 2026-10-12.
-- boleta_tipo_personal = 'ADM', tipo_pago = ' ' (vacío) -> administrativo -- Ver db/migrar-codper.sql y docs/migracion-codper.md.
-- boleta_tipo_personal = 'DOC', tipo_pago = '1' -> DOC1, docente costo por hora
-- boleta_tipo_personal = 'DOC', tipo_pago = '2' -> DOC2, docente monto fijo
--
-- tipo_pago es char(1), NOT NULL y parte de la PK: en ADM se graba vacío, nunca NULL.
-- Ejecutar ANTES de desplegar el backend en otro entorno: con ddl-auto: validate, la app no
-- arranca si falta la columna.
ALTER TABLE planilla.pl_renta_quinta
ADD COLUMN tipo_pago char(1) NOT NULL DEFAULT '';
ALTER TABLE planilla.pl_renta_quinta
DROP CONSTRAINT pl_renta_quinta_pkey,
ADD PRIMARY KEY (numero_documento, anio, mes, boleta_tipo_personal, tipo_pago);
COMMENT ON COLUMN planilla.pl_renta_quinta.tipo_pago IS
'Solo DOC: 1 = docente costo por hora (DOC1), 2 = docente monto fijo (DOC2). Vacío en ADM.';
> Documento anterior a la migración a codper del 2026-10-12. Sus claves, parámetros y ejemplos SQL antiguos fueron reemplazados: consultar [migracion-codper.md](migracion-codper.md).
> Módulo desactivado temporalmente: código comentado y endpoints no publicados. La tabla ya no se valida al arrancar. El contrato siguiente se conserva como referencia. > Módulo desactivado temporalmente: código comentado y endpoints no publicados. La tabla ya no se valida al arrancar. El contrato siguiente se conserva como referencia.
# API de mantenimiento de docentes # API de mantenimiento de docentes
......
# API de proyección docente por hora # Proyección docente por hora
Mantenimiento para **grabar** filas en `planilla.pl_proyeccion_renta_docentehora`. Solo escribe en El mantenimiento guarda en `planilla.pl_proyeccion_docente_horas`, identificado por `codper`.
esa tabla. La renta de quinta (DOC1) lee luego estos datos para proyectar el sueldo bruto y las No usa número de documento ni FK convencional a `nuevo.personal` (foreign table).
gratificaciones (ver `circuito-api-renta-quinta.md`). La modalidad DOC1 del cálculo sigue dependiendo de `nuevo.personal.id_tipo_pago = 1`.
Paquete: `so.planilla_be.proyecciondocentehora`. ## Tabla
## Endpoint - `id_proyeccion`: bigint identity, PK.
- `codper`: integer positivo, obligatorio.
- `anio`: integer entre 1 y 9999, obligatorio.
- `mes`: integer entre 1 y 12, obligatorio.
- `tipo_proyeccion`: integer, 1 sueldo / 2 gratificación.
- `remun_bruta_futura`, `gratificacion`: numeric(12,2), no negativos, NOT NULL DEFAULT 0.
- UNIQUE `(codper, anio, mes, tipo_proyeccion)`.
- Tipo 1: gratificacion = 0. Tipo 2: remun_bruta_futura = 0 y mes 7 o 12.
| Método | Ruta | Descripción | Script: `db/crear-pl-proyeccion-docente-horas.sql`, aplicado en soporte.
|---|---|---| La tabla anterior estaba ausente; no se migraron datos ni se borraron respaldos.
| GET | `/api/v1/proyeccion-docente-hora/{numeroDocumento}/{anio}` | Filas grabadas del docente en el año, con su nombre |
| POST | `/api/v1/proyeccion-docente-hora` | Graba una lista de filas |
| DELETE | `/api/v1/proyeccion-docente-hora/{idProyeccion}` | Elimina una fila por `id_proyeccion` |
`GET`: llena la grilla de la pantalla. Ordena por mes y luego por tipo (`DOC` antes que `GRA`). ## API
Trae el `idProyeccion` de cada fila para usarlo en el `DELETE`, y el `nombreCompleto` del DNI. Sin
filas devuelve `data: []` (200), con el nombre si el DNI está en personal.
```json Se conservan las rutas. Los campos JSON cambian de `proyeccionAnio`, `proyeccionMes`
{ y `boletaTipoPersonal` a `anio`, `mes` y `tipoProyeccion` numérico.
"numeroDocumento": "12345678", El frontend debe actualizar esos nombres; DOC/GRA ya no son valores de este módulo.
"nombreCompleto": "PEREZ GOMEZ JUAN",
"proyeccionAnio": 2026,
"data": [
{"idProyeccion": 26, "proyeccionMes": 10, "boletaTipoPersonal": "DOC",
"remunBrutaFutura": 1500.00, "gratificacion": 0.00},
{"idProyeccion": 27, "proyeccionMes": 12, "boletaTipoPersonal": "GRA",
"remunBrutaFutura": 0.00, "gratificacion": 1800.00}
]
}
```
`DELETE`: `204` sin cuerpo si se elimina; `404` si el id no existe; `400` si el id no es numérico.
Swagger: `/docs`, tag **Proyección docente por hora**.
## Estructura de la tabla
| Columna | Tipo | Nulo | Notas |
|---|---|---|---|
| `id_proyeccion` | bigint | NO | PK, **identity ALWAYS**: nunca se envía |
| `proyeccion_anio` | integer | NO | |
| `proyeccion_mes` | integer | NO | 1–12 |
| `numero_documento` | varchar(15) | NO | |
| `boleta_tipo_personal` | varchar(10) | NO | `DOC` o `GRA` |
| `remun_bruta_futura` | numeric(12,2) | SÍ | default 0.00. Se usa en filas `DOC` |
| `gratificacion` | numeric(12,2) | SÍ | default 0.00. Se usa en filas `GRA` (meses 7 y 12) |
La tabla **no tiene unique** salvo el PK. La clave lógica que usa el API es ### GET /api/v1/proyeccion-docente-hora/{codper}/{anio}
`numero_documento + boleta_tipo_personal + proyeccion_anio + proyeccion_mes`.
## Request Devuelve `{codper, nombreCompleto, anio, data}`. Cada elemento de data contiene
`idProyeccion`, `mes`, `tipoProyeccion`, `remunBrutaFutura` y `gratificacion`.
Orden: mes ascendente y tipo ascendente. Sin filas, data está vacío.
Body: arreglo JSON, no vacío. ### POST /api/v1/proyeccion-docente-hora
| Campo | Tipo | Obligatorio | Regla | Recibe una lista no vacía. Inserta o actualiza por la clave única, todo o nada.
|---|---|---|---| Importes omitidos o nulos se guardan como cero.
| `proyeccionAnio` | Integer | sí | |
| `proyeccionMes` | Integer | sí | 1 a 12 |
| `numeroDocumento` | String | sí | no vacío, máx. 15 (se hace `trim`) |
| `boletaTipoPersonal` | String | sí | solo `DOC` o `GRA` |
| `remunBrutaFutura` | BigDecimal | no | ≥ 0, hasta 2 decimales. Nulo se graba como 0 |
| `gratificacion` | BigDecimal | no | ≥ 0, hasta 2 decimales. Nulo se graba como 0 |
```json ```json
[ [
{ {"codper": 123, "anio": 2026, "mes": 10, "tipoProyeccion": 1, "remunBrutaFutura": 1500},
"proyeccionAnio": 2026, {"codper": 123, "anio": 2026, "mes": 12, "tipoProyeccion": 2, "gratificacion": 800}
"proyeccionMes": 10,
"numeroDocumento": "12345678",
"boletaTipoPersonal": "DOC",
"remunBrutaFutura": 1500.00,
"gratificacion": 0
},
{
"proyeccionAnio": 2026,
"proyeccionMes": 12,
"numeroDocumento": "12345678",
"boletaTipoPersonal": "GRA",
"remunBrutaFutura": 0,
"gratificacion": 1800.00
}
] ]
``` ```
## Comportamiento La respuesta contiene los campos grabados, idProyeccion y nombreCompleto.
El nombre se consulta por codper desde nuevo.personal; puede ser null si no se encuentra.
Se rechazan gratificaciones fuera de julio/diciembre y montos en el campo del otro tipo.
- **Insertar o actualizar:** si ya existe una fila con la clave lógica, se actualizan ### DELETE /api/v1/proyeccion-docente-hora/{idProyeccion}
`remun_bruta_futura` y `gratificacion`. Si no existe, se inserta. No se duplica.
- **Todo o nada:** una sola transacción. Si una fila falla, no se graba ninguna.
- **Solo esta tabla:** no toca `pl_renta_quinta` ni ninguna otra.
## Response `200` Elimina una fila; devuelve 204. Si no existe, devuelve 404.
Lista de las filas grabadas, en el mismo orden del request, con su `idProyeccion`. ## Cálculo
```json
[
{
"idProyeccion": 26,
"proyeccionAnio": 2026,
"proyeccionMes": 10,
"numeroDocumento": "12345678",
"nombreCompleto": "PEREZ GOMEZ JUAN",
"boletaTipoPersonal": "DOC",
"remunBrutaFutura": 1500.00,
"gratificacion": 0
}
]
```
`nombreCompleto` (apellidos + nombres) se busca por DNI en la tabla de personal, con el mismo Para DOC1, las filas tipo 1 del mes de corte reemplazan el sueldo bruto de ese mes.
`PersonalRepository` de retención otros empleadores. Es **temporal**: los docentes vendrán de otra Los meses posteriores del mismo codper y año se suman como sueldo proyectado.
tabla y solo hay que cambiar `nombreCompleto()` en `ProyeccionDocenteHoraServiceImpl`. Si el DNI no Las filas tipo 2 de julio y diciembre proporcionan las gratificaciones pendientes,
está en personal, viene `null` y la fila se graba igual. sin factor 1.09. Sin filas, los importes son cero.
Historial y boletas reales siguen consultándose desde pl_planilla.
## Errores \ No newline at end of file
Formato `{timestamp, status, message}`, igual que el resto del API.
| Status | Causa |
|---|---|
| 400 | Lista vacía, campo obligatorio ausente, `proyeccionMes` fuera de 1–12, `boletaTipoPersonal` distinto de `DOC`/`GRA`, importe negativo o con más de 2 decimales |
## Ejemplo
```bash
curl -X POST http://localhost:8080/api/v1/proyeccion-docente-hora \
-H "Content-Type: application/json" \
-d '[{"proyeccionAnio":2026,"proyeccionMes":10,"numeroDocumento":"12345678","boletaTipoPersonal":"DOC","remunBrutaFutura":1500.00}]'
```
## Archivos
```
proyecciondocentehora/
├── controller/ProyeccionDocenteHoraController
├── dto/ProyeccionDocenteHoraRequestDTO, ProyeccionDocenteHoraDTO
├── entity/ProyeccionDocenteHora @GeneratedValue(IDENTITY)
├── repository/ProyeccionDocenteHoraRepository
└── service/ + service/impl/
```
> Documento anterior a la migración a codper del 2026-10-12. Sus claves, parámetros y ejemplos SQL antiguos fueron reemplazados: consultar [migracion-codper.md](migracion-codper.md).
# Circuito del API de renta de quinta, con tipo de planilla # Circuito del API de renta de quinta, con tipo de planilla
Este documento sigue el recorrido de una petición **archivo por archivo y función por función**: Este documento sigue el recorrido de una petición **archivo por archivo y función por función**:
...@@ -28,7 +30,7 @@ combina el tipo con la columna **`tipo_pago`** (`char(1)`) de `pl_planilla`: ...@@ -28,7 +30,7 @@ combina el tipo con la columna **`tipo_pago`** (`char(1)`) de `pl_planilla`:
|---|---|---|---| |---|---|---|---|
| `ADM` | (vacío) | `MONTO_FIJO` | Remuneración de referencia × meses restantes | | `ADM` | (vacío) | `MONTO_FIJO` | Remuneración de referencia × meses restantes |
| `DOC` | `2` | `MONTO_FIJO` | Igual que ADM | | `DOC` | `2` | `MONTO_FIJO` | Igual que ADM |
| `DOC` | `1` | `DOCENTE_POR_HORA` | `remun_bruta_futura` de `planilla.pl_proyeccion_renta_docentehora` del mes consultado | | `DOC` | `1` | `DOCENTE_POR_HORA` | `remun_bruta_futura` de `planilla.pl_proyeccion_docente_horas` del mes consultado |
En DOC, el `tipo_pago` se lee de las boletas DOC del mes de referencia; vacío o desconocido = `MONTO_FIJO`. En DOC, el `tipo_pago` se lee de las boletas DOC del mes de referencia; vacío o desconocido = `MONTO_FIJO`.
...@@ -91,7 +93,7 @@ Si llega un valor inválido, por ejemplo `tipoPlanilla=XYZ`, la respuesta es: ...@@ -91,7 +93,7 @@ Si llega un valor inválido, por ejemplo `tipoPlanilla=XYZ`, la respuesta es:
|---|---|---|---|---|---| |---|---|---|---|---|---|
| **C1** | Lectura del parámetro | `controller/RentaQuintaController.java:59, 78, 106, 123` | Convierte `"ADM"/"DOC"` en `TipoPlanilla` | ✔ | ✔ | | **C1** | Lectura del parámetro | `controller/RentaQuintaController.java:59, 78, 106, 123` | Convierte `"ADM"/"DOC"` en `TipoPlanilla` | ✔ | ✔ |
| **C2** | Filtro de la grilla | `repository/RentaQuintaRepository.java:21` (lo invoca `service/impl/RentaQuintaServiceImpl.java:90`) | Solo trae trabajadores con `trim(boleta_tipo_personal) = 'ADM'/'DOC'` | ✔ | ✔ | | **C2** | Filtro de la grilla | `repository/RentaQuintaRepository.java:21` (lo invoca `service/impl/RentaQuintaServiceImpl.java:90`) | Solo trae trabajadores con `trim(boleta_tipo_personal) = 'ADM'/'DOC'` | ✔ | ✔ |
| **C3** | Modalidad del trabajador | `service/impl/RentaQuintaServiceImpl.java:367` `modalidad()` | `ModalidadCalculo.de(tipo, tipo_pago)` | `MONTO_FIJO` | tipo_pago 2 → `MONTO_FIJO`; 1 → `DOCENTE_POR_HORA` (y lee `pl_proyeccion_renta_docentehora` en `calcular()` `:332`) | | **C3** | Modalidad del trabajador | `service/impl/RentaQuintaServiceImpl.java:367` `modalidad()` | `ModalidadCalculo.de(tipo, tipo_pago)` | `MONTO_FIJO` | tipo_pago 2 → `MONTO_FIJO`; 1 → `DOCENTE_POR_HORA` (y lee `pl_proyeccion_docente_horas` en `calcular()` `:332`) |
| **C4** | Proyección del sueldo | `calculo/RentaQuintaCalculadora.java:160` `proyeccionSueldoBruto()` | Meses posteriores al de corte | Remuneración de referencia × meses que faltan | Tipo 2: igual que ADM. Tipo 1: `remun_bruta_futura` del mes consultado | | **C4** | Proyección del sueldo | `calculo/RentaQuintaCalculadora.java:160` `proyeccionSueldoBruto()` | Meses posteriores al de corte | Remuneración de referencia × meses que faltan | Tipo 2: igual que ADM. Tipo 1: `remun_bruta_futura` del mes consultado |
La base de la gratificación (`baseGratificacion()` `:451`) y su monto (`gratificacion()` `:173`, La base de la gratificación (`baseGratificacion()` `:451`) y su monto (`gratificacion()` `:173`,
...@@ -241,15 +243,15 @@ devuelven con `mapper.toDetalle` / `mapper.toHistorial`. ...@@ -241,15 +243,15 @@ devuelven con `mapper.toDetalle` / `mapper.toHistorial`.
## 7. Docente por hora (DOC, tipo_pago 1) ## 7. Docente por hora (DOC, tipo_pago 1)
Solo cambia la **proyección del sueldo bruto** [C4]. Se lee de Solo cambia la **proyección del sueldo bruto** [C4]. Se lee de
`planilla.pl_proyeccion_renta_docentehora` (📄 `proyecciondocentehora/`): `planilla.pl_proyeccion_docente_horas` (📄 `proyecciondocentehora/`):
```sql ```sql
select coalesce(sum(remun_bruta_futura), 0) select coalesce(sum(remun_bruta_futura), 0)
from planilla.pl_proyeccion_renta_docentehora from planilla.pl_proyeccion_docente_horas
where numero_documento = :doc where codper = :codper
and trim(boleta_tipo_personal) = 'DOC' and tipo_proyeccion = 1
and proyeccion_anio = :anio and anio = :anio
and proyeccion_mes = :mes -- mes consultado and mes = :mes -- mes consultado
``` ```
- Se toma la fila del **mes consultado**: su `remun_bruta_futura` es la proyección del sueldo - Se toma la fila del **mes consultado**: su `remun_bruta_futura` es la proyección del sueldo
......
> Documento anterior a la migración a codper del 2026-10-12. Sus claves, parámetros y ejemplos SQL antiguos fueron reemplazados: consultar [migracion-codper.md](migracion-codper.md).
# Otros empleadores en el punto 5 — Control de retenciones efectuadas y cálculo de cuota # Otros empleadores en el punto 5 — Control de retenciones efectuadas y cálculo de cuota
Este documento explica cómo se obtiene, para un trabajador con registros en Este documento explica cómo se obtiene, para un trabajador con registros en
......
-- OBSOLETO: el backend consulta nuevo.periodo_asistencia; no ejecutar este script.
-- Calendario mensual usado por el cálculo de la gratificación proporcional (ADM): de aquí sale -- Calendario mensual usado por el cálculo de la gratificación proporcional (ADM): de aquí sale
-- cuántos días tiene el mes de ingreso del trabajador. Una fila por año + mes. -- cuántos días tiene el mes de ingreso del trabajador. Una fila por año + mes.
-- semestre 1 = enero-junio (gratificación de julio), 2 = julio-diciembre (de diciembre) -- semestre 1 = enero-junio (gratificación de julio), 2 = julio-diciembre (de diciembre)
......
-- Mantenimiento mensual. El cálculo DOC2 consulta pl_gratificacion. -- Script retirado tras la migración a codper del 2026-10-12.
CREATE TABLE planilla.pl_docente_grati_diciembre_completa ( -- Ver db/migrar-codper.sql y docs/migracion-codper.md.
numero_documento varchar(15) NOT NULL,
anio integer NOT NULL,
mes integer NOT NULL,
estado integer NOT NULL,
PRIMARY KEY (numero_documento, anio, mes),
CONSTRAINT ck_docente_grati_mes CHECK (mes BETWEEN 1 AND 12),
CONSTRAINT ck_docente_grati_estado CHECK (estado IN (0, 1)),
CONSTRAINT ck_docente_grati_anio CHECK (anio BETWEEN 2000 AND 2100),
CONSTRAINT ck_docente_grati_documento CHECK (btrim(numero_documento) <> '')
);
COMMENT ON TABLE planilla.pl_docente_grati_diciembre_completa IS
'Mantenimiento mensual de docentes: estado 0 inactivo, 1 activo.';
> Documento anterior a la migración a codper del 2026-10-12. Sus claves, parámetros y ejemplos SQL antiguos fueron reemplazados: consultar [migracion-codper.md](migracion-codper.md).
# Origen del básico para la gratificación (`basicoGratificacion`) # Origen del básico para la gratificación (`basicoGratificacion`)
Este campo solo se usa para calcular la **gratificación pendiente** (julio/diciembre) en Este campo solo se usa para calcular la **gratificación pendiente** (julio/diciembre) en
......
> Documento anterior a la migración a codper del 2026-10-12. Sus claves, parámetros y ejemplos SQL antiguos fueron reemplazados: consultar [migracion-codper.md](migracion-codper.md).
# Endpoints — renta de quinta categoría # Endpoints — renta de quinta categoría
Base: `/api/v1/renta-quinta` Base: `/api/v1/renta-quinta`
...@@ -11,7 +13,7 @@ Solo se aceptan tres valores (no distingue mayúsculas). `DOC` sin dígito respo ...@@ -11,7 +13,7 @@ Solo se aceptan tres valores (no distingue mayúsculas). `DOC` sin dígito respo
| `tipoPlanilla` | Filtra en `pl_planilla` | Proyección del sueldo bruto | Se graba en `pl_renta_quinta` | | `tipoPlanilla` | Filtra en `pl_planilla` | Proyección del sueldo bruto | Se graba en `pl_renta_quinta` |
|---|---|---|---| |---|---|---|---|
| `ADM` | `boleta_tipo_personal = 'ADM'` | Remuneración de referencia × meses restantes | `boleta_tipo_personal = 'ADM'`, `tipo_pago` vacío (`' '`) | | `ADM` | `boleta_tipo_personal = 'ADM'` | Remuneración de referencia × meses restantes | `boleta_tipo_personal = 'ADM'`, `tipo_pago` vacío (`' '`) |
| `DOC1` | `boleta_tipo_personal = 'DOC'` y `tipo_pago = '1'` | `remun_bruta_futura` de `pl_proyeccion_renta_docentehora` del mes consultado | `boleta_tipo_personal = 'DOC'`, `tipo_pago = '1'` | | `DOC1` | `boleta_tipo_personal = 'DOC'` y `tipo_pago = '1'` | `remun_bruta_futura` de `pl_proyeccion_docente_horas` del mes consultado | `boleta_tipo_personal = 'DOC'`, `tipo_pago = '1'` |
| `DOC2` | `boleta_tipo_personal = 'DOC'` y `tipo_pago = '2'` | Igual que ADM | `boleta_tipo_personal = 'DOC'`, `tipo_pago = '2'` | | `DOC2` | `boleta_tipo_personal = 'DOC'` y `tipo_pago = '2'` | Igual que ADM | `boleta_tipo_personal = 'DOC'`, `tipo_pago = '2'` |
- `DOC1` = profesor costo por hora. `DOC2` = profesor monto fijo. - `DOC1` = profesor costo por hora. `DOC2` = profesor monto fijo.
......
-- DDL propuesto para relacionar planilla.pl_planilla con las tablas nuevas -- Script retirado tras la migración a codper del 2026-10-12.
-- planilla.pl_renta_basico y planilla.pl_utilidades. -- Ver db/migrar-codper.sql y docs/migracion-codper.md.
--
-- No se agregan columnas nuevas en pl_planilla: numero_documento, boleta_anio
-- y boleta_mes ya existen y coinciden con la primary key compuesta de ambas
-- tablas nuevas (numero_documento, anio, mes), así que las FK son compuestas
-- sobre esas mismas columnas.
--
-- Ejecutar en el esquema planilla. Requiere permisos DDL que el MCP de
-- Postgres configurado (solo lectura) no tiene.
-- 1) Opcional: alinear el tipo de numero_documento en pl_planilla (hoy text)
-- con el de las tablas nuevas (varchar(15)), si el equipo prefiere evitar
-- la comparación cross-type en la FK. No es obligatorio: Postgres admite
-- FK entre text y varchar.
-- ALTER TABLE planilla.pl_planilla
-- ALTER COLUMN numero_documento TYPE varchar(15);
-- 2) Foreign key hacia pl_renta_basico
ALTER TABLE planilla.pl_planilla
ADD CONSTRAINT fk_pl_planilla_renta_basico
FOREIGN KEY (numero_documento, boleta_anio, boleta_mes)
REFERENCES planilla.pl_renta_basico (numero_documento, anio, mes);
-- 3) Foreign key hacia pl_utilidades
ALTER TABLE planilla.pl_planilla
ADD CONSTRAINT fk_pl_planilla_utilidades
FOREIGN KEY (numero_documento, boleta_anio, boleta_mes)
REFERENCES planilla.pl_utilidades (numero_documento, anio, mes);
-- Notas:
-- * pl_renta_basico y pl_utilidades ya tienen su PK compuesta creada
-- (pl_gratificacion_pkey y pl_utilidades_pkey respectivamente), así que no
-- hace falta generarlas.
-- * Estas FK exigen que exista una fila en pl_renta_basico/pl_utilidades para
-- cada combinación (numero_documento, boleta_anio, boleta_mes) que ya tenga
-- pl_planilla. Si la carga de esas tablas es posterior o parcial, correr
-- antes un SELECT de validación (LEFT JOIN buscando NULL) para no romper el
-- ALTER TABLE con datos huérfanos.
> Documento anterior a la migración a codper del 2026-10-12. Sus claves, parámetros y ejemplos SQL antiguos fueron reemplazados: consultar [migracion-codper.md](migracion-codper.md).
# Grabado de retención en `pl_renta_quinta` — Guía para el Frontend # Grabado de retención en `pl_renta_quinta` — Guía para el Frontend
Base path: `/api/v1/renta-quinta` Base path: `/api/v1/renta-quinta`
......
> Documento anterior a la migración a codper del 2026-10-12. Sus claves, parámetros y ejemplos SQL antiguos fueron reemplazados: consultar [migracion-codper.md](migracion-codper.md).
# Circuito de grabado en `planilla.pl_renta_quinta` # Circuito de grabado en `planilla.pl_renta_quinta`
Este documento describe el flujo del endpoint `POST /api/v1/renta-quinta/grabar`, que persiste Este documento describe el flujo del endpoint `POST /api/v1/renta-quinta/grabar`, que persiste
......
# Migración a codper — 12 de octubre de 2026
## Contrato vigente de renta quinta (ajuste posterior)
- `pl_renta_quinta` guarda `codper`, `anio`, `mes` y `retencion`, con ID propio y
UNIQUE `(codper, anio, mes)`, sin FK a una boleta. El tipo de planilla filtra el cálculo,
pero no crea una segunda retención del mismo trabajador y periodo.
- Migración: `db/renta-quinta-por-codper-periodo.sql`; preserva los registros actuales,
IDs e importes, y aborta ante claves incompletas o duplicadas. No reinserta filas
retiradas del respaldo histórico.
- El grabado permite un mes estimado; requiere historial de referencia hasta el corte.
El masivo sigue siendo transaccional: un error revierte todos los cambios.
- `/filtros/periodos` devuelve `{idPeriodo, mes, anio}`, con `idPeriodo = anio * 100 + mes`.
- El listado cruza personal, área, subárea, cargo y sede mediante INNER JOIN;
incluye `codper`, `codlocal`, `idSede` y `sede`, además de los datos y montos existentes.
- La tabla de `trismegisto-asistencia` usa `GET /api/v1/renta-quinta/listado/completo`:
devuelve un array de filas con `anio`, `mes` y los mismos filtros del listado paginado,
sin `page`/`size`. La selección y el cálculo se realizan una sola vez, incluidos los
filtros de retención y variación; la tabla pagina en el navegador y Excel recibe todas
las filas. `/listado` conserva su contrato paginado para los demás clientes.
- Detalle y otros empleadores usan `codper`; el selector de personal conserva `id`
como valor y `text` solo como etiqueta.
Las secciones siguientes describen los pasos históricos anteriores a este ajuste.
Se aplicó en la base **soporte**, esquema planilla. El script es
[db/migrar-codper.sql](../db/migrar-codper.sql); debe ejecutarse completo en una transacción.
Tiene una protección para no repetirlo si ya existe el respaldo.
La aplicación mantiene ddl-auto: validate.
## Esquema resultante
| Tabla | PK | Referencia / identidad del trabajador | Periodo |
|---|---|---|---|
| pl_planilla | id_pl_planilla | codper → nuevo.personal por consulta | boleta_anio, boleta_mes |
| pl_renta_quinta | id_renta_quinta identity | id_pl_planilla FK, UNIQUE | Desde la boleta |
| pl_gratificacion | id_gratificacion identity | id_pl_planilla FK, UNIQUE | anio, mes propios |
| pl_utilidades | id_utilidad identity | id_pl_planilla FK, UNIQUE | Desde la boleta |
| pl_retencion_otros_empleadores | id_retencion identity | codper | anio, mes |
| pl_renta_basico | codper, anio, mes | codper | anio, mes |
| pl_proyeccion_docente_horas | id_proyeccion identity | codper | anio, mes |
Las tablas hijas de pl_planilla no duplican codper: se obtiene mediante la FK.
No existe FK hacia nuevo.personal porque es una tabla externa.
Se retiraron documento, apellidos, nombres, local, unidad organizativa y tipo_pago de pl_planilla.
Se retiraron documento, año y mes de las tres tablas hijas; pl_renta_quinta tampoco duplica
boleta_tipo_personal ni tipo_pago. Otros empleadores y proyecciones conservan sus periodos porque
pueden existir sin boleta.
Se eliminó pl_periodo. Los periodos se obtienen de nuevo.periodo_asistencia.
pl_renta_basico y pl_proyeccion_renta_docentehora no existían en la base inspeccionada y se crearon
vacías con codper para permitir el arranque del backend.
## Datos pendientes
No se completó ningún codper, conforme a lo solicitado. La migración conservó todas las filas
y los importes de retenciones, gratificaciones y utilidades.
| Tabla | Filas | FK vinculadas | FK pendientes |
|---|---:|---:|---:|
| pl_renta_quinta | 2.867 | 2.865 | 2 |
| pl_gratificacion | 2.234 | 0 | 2.234 |
| pl_utilidades | 3.256 | 2.553 | 703 |
Las gratificaciones no tienen boletas del mismo periodo. En utilidades hay 698 filas sin boleta
y 5 con varias candidatas; las 2 retenciones pendientes también tienen varias candidatas.
No se asignó una boleta arbitraria.
Las 20.150 boletas conservan codper NULL y las 34 declaraciones de otros empleadores quedan
con codper NULL. Las filas sin código no aparecen en consultas por trabajador.
La grilla quedará vacía hasta completar esos códigos. Las boletas siguen consultables por ID.
Respaldo de datos originales: **respaldo_codper_20261012**. Contiene documento/periodo y los
IDs nuevos de las tablas hijas para resolver los pendientes incluso después de retirar columnas.
No es una fuente de datos del cálculo ni un mecanismo automático de restauración del esquema.
Las consultas para revisar pendientes están en
[db/revisar-pendientes-codper.sql](../db/revisar-pendientes-codper.sql).
Las FK y codper quedan nullable durante la transición. Después de completar y verificar los
datos, se podrán hacer NOT NULL. No crear boletas ficticias para completar las FK.
El ETL debe cargar codper en pl_planilla y los IDs de boleta en sus tablas hijas.
Al actualizar otros empleadores deben resolverse las colisiones de UNIQUE (codper, anio, mes).
## Modalidad docente
DOC1 = nuevo.personal.id_tipo_pago = 1; DOC2 = 2. ADM se identifica por boleta_tipo_personal.
tipoPlanilla mantiene ADM/DOC1/DOC2 como filtro. En detalle/grabado, un parámetro incompatible
con el catálogo responde 400. El parámetro no reemplaza la modalidad del trabajador.
El cálculo de boletas históricas también usa el valor actual del catálogo.
La excepción de gratificación DOC2 requiere una fila de pl_gratificacion vinculada a una boleta
de diciembre del mismo codper/año. Las filas con FK NULL no habilitan la excepción.
## Cambios para el frontend
- /detallado y /grabar reciben **codper** entero, en lugar de numeroDocumento.
- Proyecciones reciben codper en el JSON y usan /{codper}/{anio} para listar.
- Otros empleadores usan /{codper}/{anio}/{mes}; sus respuestas incluyen codper.
- /filtros/periodos devuelve [{mes, anio}], sin idPeriodo.
- documentoOApellidos sigue buscando documento/nombres en nuevo.personal.
- Los campos personales de las respuestas de consulta se conservan, obtenidos del catálogo.
- El nombre JSON idRentaQuinta en la boleta/historial conserva compatibilidad y corresponde
a id_pl_planilla. No confundirlo con la nueva PK de la tabla de retenciones.
- Grabar exige una única boleta real del mes: no se puede vincular una retención calculada
de un mes estimado a una boleta de otro periodo.
- Grabado masivo es transaccional; una boleta faltante o ambigua revierte la operación completa.
## Ajuste posterior de pl_planilla
Se aplicó db/ajustar-campos-pl-planilla.sql: tipo_planilla se convirtió en tipopla integer;
se agregaron codlocal text, id_tipo_pago integer e id_unidad_organizativa integer, nullable.
Estos campos no se completaron automáticamente. La modalidad docente sigue viniendo de
nuevo.personal.id_tipo_pago. El nombre JSON tipoDocumento conserva el valor de tipopla como texto.
## Restauración de periodos de gratificación
Se aplicó db/restaurar-periodo-pl-gratificacion.sql: mes y anio son integer NOT NULL.
Se recuperaron los periodos originales de las 2.234 filas desde el respaldo por id_gratificacion.
La excepción DOC2 filtra g.mes = 12 y g.anio, y obtiene codper de la boleta vinculada.
## Clasificación y gratificaciones (actualización)
ADM se identifica por pl_planilla.tipopla 5 o 6. DOC por tipopla 1 o 3;
DOC1/DOC2 usan nuevo.personal.id_tipo_pago 1/2. boleta_tipo_personal no determina esta clasificación.
pl_gratificacion incorpora codper nullable positivo. Si id_pl_planilla existe, trabajador y periodo
se obtienen de la boleta; sin enlace se utilizan codper, anio y mes de la gratificación.
Los códigos existentes de gratificación siguen pendientes de carga.
## UIT
El backend consulta planilla.pl_uit: PK id_uit, monto uit y anio. UitCache conserva la consulta
por año y las reglas de cálculo. El calendario se consulta desde nuevo.periodo_asistencia,
sin entidad JPA para pl_calendario. La tabla de proyecciones se creó como
planilla.pl_proyeccion_docente_horas mediante db/crear-pl-proyeccion-docente-horas.sql:
codper, anio, mes y tipo_proyeccion (1 sueldo / 2 gratificación), sin documento.
El backend y el contrato JSON usan los nuevos campos; no ejecutar la migración histórica nuevamente.
> Documento anterior a la migración a codper del 2026-10-12. Sus claves, parámetros y ejemplos SQL antiguos fueron reemplazados: consultar [migracion-codper.md](migracion-codper.md).
# API Renta de Quinta Categoría — Guía para el Frontend # API Renta de Quinta Categoría — Guía para el Frontend
Base path: `/api/v1/renta-quinta` Base path: `/api/v1/renta-quinta`
......
> Documento anterior a la migración a codper del 2026-10-12. Sus claves, parámetros y ejemplos SQL antiguos fueron reemplazados: consultar [migracion-codper.md](migracion-codper.md).
# Integración de renta quinta con jQuery DataTables # Integración de renta quinta con jQuery DataTables
> Propuesta pendiente de implementación: esta rama incluye el DTO de respuesta, pero aún no > Propuesta pendiente de implementación: esta rama incluye el DTO de respuesta, pero aún no
......
> Documento anterior a la migración a codper del 2026-10-12. Sus claves, parámetros y ejemplos SQL antiguos fueron reemplazados: consultar [migracion-codper.md](migracion-codper.md).
# API Retención Otros Empleadores — Guía para el Frontend # API Retención Otros Empleadores — Guía para el Frontend
Base path: `/api/v1/retencion-otros-empleadores` Base path: `/api/v1/retencion-otros-empleadores`
......
...@@ -34,41 +34,41 @@ public class RetencionOtrosEmpleadoresController { ...@@ -34,41 +34,41 @@ public class RetencionOtrosEmpleadoresController {
} }
@Operation(summary = "Meses del año declarados para un trabajador") @Operation(summary = "Meses del año declarados para un trabajador")
@GetMapping("/{documentoTrabajador}/{anio}") @GetMapping("/{codper}/{anio}")
public RetencionOtrosEmpleadoresListadoDTO listarPorAnio( public RetencionOtrosEmpleadoresListadoDTO listarPorAnio(
@PathVariable String documentoTrabajador, @PathVariable @Min(1) Integer codper,
@PathVariable Integer anio) { @PathVariable Integer anio) {
return service.listarPorAnio(documentoTrabajador, anio); return service.listarPorAnio(codper, anio);
} }
@Operation(summary = "Registrar retención de un mes") @Operation(summary = "Registrar retención de un mes")
@PostMapping("/{documentoTrabajador}/{anio}/{mes}") @PostMapping("/{codper}/{anio}/{mes}")
public ResponseEntity<RetencionOtrosEmpleadoresDTO> crear( public ResponseEntity<RetencionOtrosEmpleadoresDTO> crear(
@PathVariable String documentoTrabajador, @PathVariable @Min(1) Integer codper,
@PathVariable Integer anio, @PathVariable Integer anio,
@PathVariable @Min(1) @Max(12) Integer mes, @PathVariable @Min(1) @Max(12) Integer mes,
@RequestBody @Valid RetencionOtrosEmpleadoresRequestDTO request) { @RequestBody @Valid RetencionOtrosEmpleadoresRequestDTO request) {
return ResponseEntity.status(HttpStatus.CREATED) return ResponseEntity.status(HttpStatus.CREATED)
.body(service.crear(documentoTrabajador, anio, mes, request)); .body(service.crear(codper, anio, mes, request));
} }
@Operation(summary = "Actualizar retención de un mes") @Operation(summary = "Actualizar retención de un mes")
@PutMapping("/{documentoTrabajador}/{anio}/{mes}") @PutMapping("/{codper}/{anio}/{mes}")
public RetencionOtrosEmpleadoresDTO actualizar( public RetencionOtrosEmpleadoresDTO actualizar(
@PathVariable String documentoTrabajador, @PathVariable @Min(1) Integer codper,
@PathVariable Integer anio, @PathVariable Integer anio,
@PathVariable @Min(1) @Max(12) Integer mes, @PathVariable @Min(1) @Max(12) Integer mes,
@RequestBody @Valid RetencionOtrosEmpleadoresRequestDTO request) { @RequestBody @Valid RetencionOtrosEmpleadoresRequestDTO request) {
return service.actualizar(documentoTrabajador, anio, mes, request); return service.actualizar(codper, anio, mes, request);
} }
@Operation(summary = "Eliminar retención de un mes") @Operation(summary = "Eliminar retención de un mes")
@DeleteMapping("/{documentoTrabajador}/{anio}/{mes}") @DeleteMapping("/{codper}/{anio}/{mes}")
public ResponseEntity<Void> eliminar( public ResponseEntity<Void> eliminar(
@PathVariable String documentoTrabajador, @PathVariable @Min(1) Integer codper,
@PathVariable Integer anio, @PathVariable Integer anio,
@PathVariable @Min(1) @Max(12) Integer mes) { @PathVariable @Min(1) @Max(12) Integer mes) {
service.eliminar(documentoTrabajador, anio, mes); service.eliminar(codper, anio, mes);
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
} }
...@@ -4,7 +4,7 @@ import java.math.BigDecimal; ...@@ -4,7 +4,7 @@ import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
public record RetencionOtrosEmpleadoresDTO( public record RetencionOtrosEmpleadoresDTO(
String documentoTrabajador, Integer codper,
String nombreCompleto, String nombreCompleto,
Integer anio, Integer anio,
Integer mes, Integer mes,
......
...@@ -3,7 +3,7 @@ package so.planilla_be.otrosempleadores.dto; ...@@ -3,7 +3,7 @@ package so.planilla_be.otrosempleadores.dto;
import java.util.List; import java.util.List;
public record RetencionOtrosEmpleadoresListadoDTO( public record RetencionOtrosEmpleadoresListadoDTO(
String numeroDocumento, Integer codper,
String personal, String personal,
List<RetencionOtrosEmpleadoresItemDTO> data List<RetencionOtrosEmpleadoresItemDTO> data
) {} ) {}
package so.planilla_be.otrosempleadores.entity; package so.planilla_be.otrosempleadores.entity;
import jakarta.persistence.Column; import jakarta.persistence.*;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@Getter @Getter
@Setter @Setter
@Entity @Entity
@Table(name = "retencion_otros_empleadores") @Table(name = "pl_retencion_otros_empleadores",
@IdClass(RetencionOtrosEmpleadoresId.class) uniqueConstraints = @UniqueConstraint(columnNames = {"codper", "anio", "mes"}))
public class RetencionOtrosEmpleadores { public class RetencionOtrosEmpleadores {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Id @Column(name = "id_retencion")
@Column(name = "documento_trabajador", length = 15) private Long idRetencion;
private String documentoTrabajador; @Column(name = "codper")
private Integer codper;
@Id
@Column(name = "anio") @Column(name = "anio")
private Integer anio; private Integer anio;
@Id
@Column(name = "mes") @Column(name = "mes")
private Integer mes; private Integer mes;
@Column(name = "importe") @Column(name = "importe")
private BigDecimal importe; private BigDecimal importe;
@Column(name = "retencion") @Column(name = "retencion")
private BigDecimal retencion; private BigDecimal retencion;
@Column(name = "fecha_registro") @Column(name = "fecha_registro")
private LocalDateTime fechaRegistro; private LocalDateTime fechaRegistro;
} }
\ No newline at end of file
package so.planilla_be.otrosempleadores.entity;
import java.io.Serializable;
import java.util.Objects;
public class RetencionOtrosEmpleadoresId implements Serializable {
private String documentoTrabajador;
private Integer anio;
private Integer mes;
public RetencionOtrosEmpleadoresId() {
}
public RetencionOtrosEmpleadoresId(String documentoTrabajador, Integer anio, Integer mes) {
this.documentoTrabajador = documentoTrabajador;
this.anio = anio;
this.mes = mes;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof RetencionOtrosEmpleadoresId that)) {
return false;
}
return Objects.equals(documentoTrabajador, that.documentoTrabajador)
&& Objects.equals(anio, that.anio)
&& Objects.equals(mes, that.mes);
}
@Override
public int hashCode() {
return Objects.hash(documentoTrabajador, anio, mes);
}
}
...@@ -3,9 +3,5 @@ package so.planilla_be.otrosempleadores.repository; ...@@ -3,9 +3,5 @@ package so.planilla_be.otrosempleadores.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import so.planilla_be.otrosempleadores.entity.Personal; import so.planilla_be.otrosempleadores.entity.Personal;
import java.util.Optional;
public interface PersonalRepository extends JpaRepository<Personal, Integer> { public interface PersonalRepository extends JpaRepository<Personal, Integer> {
Optional<Personal> findFirstByNumeroDocumentoOrderByCodperDesc(String numeroDocumento);
} }
...@@ -2,16 +2,12 @@ package so.planilla_be.otrosempleadores.repository; ...@@ -2,16 +2,12 @@ package so.planilla_be.otrosempleadores.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import so.planilla_be.otrosempleadores.entity.RetencionOtrosEmpleadores; import so.planilla_be.otrosempleadores.entity.RetencionOtrosEmpleadores;
import so.planilla_be.otrosempleadores.entity.RetencionOtrosEmpleadoresId;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface RetencionOtrosEmpleadoresRepository public interface RetencionOtrosEmpleadoresRepository extends JpaRepository<RetencionOtrosEmpleadores, Long> {
extends JpaRepository<RetencionOtrosEmpleadores, RetencionOtrosEmpleadoresId> { List<RetencionOtrosEmpleadores> findByCodperAndAnioOrderByMes(Integer codper, Integer anio);
List<RetencionOtrosEmpleadores> findByAnioAndCodperIn(Integer anio, List<Integer> codpers);
List<RetencionOtrosEmpleadores> findByDocumentoTrabajadorAndAnioOrderByMes( Optional<RetencionOtrosEmpleadores> findByCodperAndAnioAndMes(Integer codper, Integer anio, Integer mes);
String documentoTrabajador, Integer anio); boolean existsByCodperAndAnioAndMes(Integer codper, Integer anio, Integer mes);
/** Declaraciones del año de varios trabajadores en una sola consulta (grilla / grabado masivo). */
List<RetencionOtrosEmpleadores> findByAnioAndDocumentoTrabajadorIn(Integer anio, List<String> documentosTrabajador);
} }
\ No newline at end of file
...@@ -6,13 +6,13 @@ import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresRequestDTO; ...@@ -6,13 +6,13 @@ import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresRequestDTO;
public interface RetencionOtrosEmpleadoresService { public interface RetencionOtrosEmpleadoresService {
RetencionOtrosEmpleadoresListadoDTO listarPorAnio(String documentoTrabajador, Integer anio); RetencionOtrosEmpleadoresListadoDTO listarPorAnio(Integer codper, Integer anio);
RetencionOtrosEmpleadoresDTO crear(String documentoTrabajador, Integer anio, Integer mes, RetencionOtrosEmpleadoresDTO crear(Integer codper, Integer anio, Integer mes,
RetencionOtrosEmpleadoresRequestDTO request); RetencionOtrosEmpleadoresRequestDTO request);
RetencionOtrosEmpleadoresDTO actualizar(String documentoTrabajador, Integer anio, Integer mes, RetencionOtrosEmpleadoresDTO actualizar(Integer codper, Integer anio, Integer mes,
RetencionOtrosEmpleadoresRequestDTO request); RetencionOtrosEmpleadoresRequestDTO request);
void eliminar(String documentoTrabajador, Integer anio, Integer mes); void eliminar(Integer codper, Integer anio, Integer mes);
} }
...@@ -7,7 +7,7 @@ import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresItemDTO; ...@@ -7,7 +7,7 @@ import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresItemDTO;
import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresListadoDTO; import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresListadoDTO;
import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresRequestDTO; import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresRequestDTO;
import so.planilla_be.otrosempleadores.entity.RetencionOtrosEmpleadores; import so.planilla_be.otrosempleadores.entity.RetencionOtrosEmpleadores;
import so.planilla_be.otrosempleadores.entity.RetencionOtrosEmpleadoresId;
import so.planilla_be.otrosempleadores.exception.RetencionOtrosEmpleadoresNoEncontradaException; import so.planilla_be.otrosempleadores.exception.RetencionOtrosEmpleadoresNoEncontradaException;
import so.planilla_be.otrosempleadores.exception.RetencionOtrosEmpleadoresYaExisteException; import so.planilla_be.otrosempleadores.exception.RetencionOtrosEmpleadoresYaExisteException;
import so.planilla_be.otrosempleadores.repository.PersonalRepository; import so.planilla_be.otrosempleadores.repository.PersonalRepository;
...@@ -31,17 +31,17 @@ public class RetencionOtrosEmpleadoresServiceImpl implements RetencionOtrosEmple ...@@ -31,17 +31,17 @@ public class RetencionOtrosEmpleadoresServiceImpl implements RetencionOtrosEmple
} }
@Override @Override
public RetencionOtrosEmpleadoresListadoDTO listarPorAnio(String documentoTrabajador, Integer anio) { public RetencionOtrosEmpleadoresListadoDTO listarPorAnio(Integer codper, Integer anio) {
String nombreCompleto = nombreCompleto(documentoTrabajador); String nombreCompleto = nombreCompleto(codper);
List<RetencionOtrosEmpleadoresItemDTO> data = repository List<RetencionOtrosEmpleadoresItemDTO> data = repository
.findByDocumentoTrabajadorAndAnioOrderByMes(documentoTrabajador, anio).stream() .findByCodperAndAnioOrderByMes(codper, anio).stream()
.map(this::toItemDto) .map(this::toItemDto)
.toList(); .toList();
return new RetencionOtrosEmpleadoresListadoDTO(documentoTrabajador, nombreCompleto, data); return new RetencionOtrosEmpleadoresListadoDTO(codper, nombreCompleto, data);
} }
private String nombreCompleto(String documentoTrabajador) { private String nombreCompleto(Integer codper) {
return personalRepository.findFirstByNumeroDocumentoOrderByCodperDesc(documentoTrabajador) return personalRepository.findById(codper)
.map(p -> "%s %s".formatted( .map(p -> "%s %s".formatted(
p.getApellido() == null ? "" : p.getApellido().trim(), p.getApellido() == null ? "" : p.getApellido().trim(),
p.getNombre() == null ? "" : p.getNombre().trim()).trim()) p.getNombre() == null ? "" : p.getNombre().trim()).trim())
...@@ -51,46 +51,45 @@ public class RetencionOtrosEmpleadoresServiceImpl implements RetencionOtrosEmple ...@@ -51,46 +51,45 @@ public class RetencionOtrosEmpleadoresServiceImpl implements RetencionOtrosEmple
@Override @Override
@Transactional @Transactional
public RetencionOtrosEmpleadoresDTO crear(String documentoTrabajador, Integer anio, Integer mes, public RetencionOtrosEmpleadoresDTO crear(Integer codper, Integer anio, Integer mes,
RetencionOtrosEmpleadoresRequestDTO request) { RetencionOtrosEmpleadoresRequestDTO request) {
RetencionOtrosEmpleadoresId id = new RetencionOtrosEmpleadoresId(documentoTrabajador, anio, mes); if (repository.existsByCodperAndAnioAndMes(codper, anio, mes)) {
if (repository.existsById(id)) {
throw new RetencionOtrosEmpleadoresYaExisteException( throw new RetencionOtrosEmpleadoresYaExisteException(
"Ya existe retención de otros empleadores para documento %s, periodo %d-%02d" "Ya existe retención de otros empleadores para codper %d, periodo %d-%02d"
.formatted(documentoTrabajador, anio, mes)); .formatted(codper, anio, mes));
} }
RetencionOtrosEmpleadores entidad = new RetencionOtrosEmpleadores(); RetencionOtrosEmpleadores entidad = new RetencionOtrosEmpleadores();
entidad.setDocumentoTrabajador(documentoTrabajador); entidad.setCodper(codper);
entidad.setAnio(anio); entidad.setAnio(anio);
entidad.setMes(mes); entidad.setMes(mes);
entidad.setImporte(request.importe()); entidad.setImporte(request.importe());
entidad.setRetencion(request.retencion()); entidad.setRetencion(request.retencion());
entidad.setFechaRegistro(LocalDateTime.now()); entidad.setFechaRegistro(LocalDateTime.now());
return toDto(repository.save(entidad), nombreCompleto(documentoTrabajador)); return toDto(repository.save(entidad), nombreCompleto(codper));
} }
@Override @Override
@Transactional @Transactional
public RetencionOtrosEmpleadoresDTO actualizar(String documentoTrabajador, Integer anio, Integer mes, public RetencionOtrosEmpleadoresDTO actualizar(Integer codper, Integer anio, Integer mes,
RetencionOtrosEmpleadoresRequestDTO request) { RetencionOtrosEmpleadoresRequestDTO request) {
RetencionOtrosEmpleadores entidad = obtenerOLanzar(documentoTrabajador, anio, mes); RetencionOtrosEmpleadores entidad = obtenerOLanzar(codper, anio, mes);
entidad.setImporte(request.importe()); entidad.setImporte(request.importe());
entidad.setRetencion(request.retencion()); entidad.setRetencion(request.retencion());
return toDto(repository.save(entidad), nombreCompleto(documentoTrabajador)); return toDto(repository.save(entidad), nombreCompleto(codper));
} }
@Override @Override
@Transactional @Transactional
public void eliminar(String documentoTrabajador, Integer anio, Integer mes) { public void eliminar(Integer codper, Integer anio, Integer mes) {
RetencionOtrosEmpleadores entidad = obtenerOLanzar(documentoTrabajador, anio, mes); RetencionOtrosEmpleadores entidad = obtenerOLanzar(codper, anio, mes);
repository.delete(entidad); repository.delete(entidad);
} }
private RetencionOtrosEmpleadores obtenerOLanzar(String documentoTrabajador, Integer anio, Integer mes) { private RetencionOtrosEmpleadores obtenerOLanzar(Integer codper, Integer anio, Integer mes) {
return repository.findById(new RetencionOtrosEmpleadoresId(documentoTrabajador, anio, mes)) return repository.findByCodperAndAnioAndMes(codper, anio, mes)
.orElseThrow(() -> new RetencionOtrosEmpleadoresNoEncontradaException( .orElseThrow(() -> new RetencionOtrosEmpleadoresNoEncontradaException(
"No existe retención de otros empleadores para documento %s, periodo %d-%02d" "No existe retención de otros empleadores para codper %d, periodo %d-%02d"
.formatted(documentoTrabajador, anio, mes))); .formatted(codper, anio, mes)));
} }
private RetencionOtrosEmpleadoresItemDTO toItemDto(RetencionOtrosEmpleadores entidad) { private RetencionOtrosEmpleadoresItemDTO toItemDto(RetencionOtrosEmpleadores entidad) {
...@@ -104,7 +103,7 @@ public class RetencionOtrosEmpleadoresServiceImpl implements RetencionOtrosEmple ...@@ -104,7 +103,7 @@ public class RetencionOtrosEmpleadoresServiceImpl implements RetencionOtrosEmple
private RetencionOtrosEmpleadoresDTO toDto(RetencionOtrosEmpleadores entidad, String nombreCompleto) { private RetencionOtrosEmpleadoresDTO toDto(RetencionOtrosEmpleadores entidad, String nombreCompleto) {
return new RetencionOtrosEmpleadoresDTO( return new RetencionOtrosEmpleadoresDTO(
entidad.getDocumentoTrabajador(), entidad.getCodper(),
nombreCompleto, nombreCompleto,
entidad.getAnio(), entidad.getAnio(),
entidad.getMes(), entidad.getMes(),
......
...@@ -3,6 +3,7 @@ package so.planilla_be.proyecciondocentehora.controller; ...@@ -3,6 +3,7 @@ package so.planilla_be.proyecciondocentehora.controller;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
...@@ -20,7 +21,7 @@ import so.planilla_be.proyecciondocentehora.service.ProyeccionDocenteHoraService ...@@ -20,7 +21,7 @@ import so.planilla_be.proyecciondocentehora.service.ProyeccionDocenteHoraService
import java.util.List; import java.util.List;
@Tag(name = "Proyección docente por hora", description = "Grabado de pl_proyeccion_renta_docentehora") @Tag(name = "Proyección docente por hora", description = "Grabado de pl_proyeccion_docente_horas")
@Validated @Validated
@RestController @RestController
@RequestMapping("/api/v1/proyeccion-docente-hora") @RequestMapping("/api/v1/proyeccion-docente-hora")
...@@ -33,14 +34,14 @@ public class ProyeccionDocenteHoraController { ...@@ -33,14 +34,14 @@ public class ProyeccionDocenteHoraController {
} }
@Operation(summary = "Filas grabadas de un docente en el año, con su nombre") @Operation(summary = "Filas grabadas de un docente en el año, con su nombre")
@GetMapping("/{numeroDocumento}/{anio}") @GetMapping("/{codper}/{anio}")
public ProyeccionDocenteHoraListadoDTO listarPorAnio( public ProyeccionDocenteHoraListadoDTO listarPorAnio(
@PathVariable String numeroDocumento, @PathVariable @Min(1) Integer codper,
@PathVariable Integer anio) { @PathVariable Integer anio) {
return service.listarPorAnio(numeroDocumento, anio); return service.listarPorAnio(codper, anio);
} }
@Operation(summary = "Grabar filas de proyección (inserta o actualiza por documento + tipo + año + mes)") @Operation(summary = "Grabar filas de proyección (inserta o actualiza por codper + tipo + año + mes)")
@PostMapping @PostMapping
public List<ProyeccionDocenteHoraDTO> grabar( public List<ProyeccionDocenteHoraDTO> grabar(
@RequestBody @NotEmpty List<@Valid ProyeccionDocenteHoraRequestDTO> filas) { @RequestBody @NotEmpty List<@Valid ProyeccionDocenteHoraRequestDTO> filas) {
......
...@@ -4,11 +4,11 @@ import java.math.BigDecimal; ...@@ -4,11 +4,11 @@ import java.math.BigDecimal;
public record ProyeccionDocenteHoraDTO( public record ProyeccionDocenteHoraDTO(
Long idProyeccion, Long idProyeccion,
Integer proyeccionAnio, Integer anio,
Integer proyeccionMes, Integer mes,
String numeroDocumento, Integer codper,
String nombreCompleto, String nombreCompleto,
String boletaTipoPersonal, Integer tipoProyeccion,
BigDecimal remunBrutaFutura, BigDecimal remunBrutaFutura,
BigDecimal gratificacion BigDecimal gratificacion
) {} ) {}
...@@ -4,8 +4,8 @@ import java.math.BigDecimal; ...@@ -4,8 +4,8 @@ import java.math.BigDecimal;
public record ProyeccionDocenteHoraItemDTO( public record ProyeccionDocenteHoraItemDTO(
Long idProyeccion, Long idProyeccion,
Integer proyeccionMes, Integer mes,
String boletaTipoPersonal, Integer tipoProyeccion,
BigDecimal remunBrutaFutura, BigDecimal remunBrutaFutura,
BigDecimal gratificacion BigDecimal gratificacion
) {} ) {}
...@@ -3,8 +3,8 @@ package so.planilla_be.proyecciondocentehora.dto; ...@@ -3,8 +3,8 @@ package so.planilla_be.proyecciondocentehora.dto;
import java.util.List; import java.util.List;
public record ProyeccionDocenteHoraListadoDTO( public record ProyeccionDocenteHoraListadoDTO(
String numeroDocumento, Integer codper,
String nombreCompleto, String nombreCompleto,
Integer proyeccionAnio, Integer anio,
List<ProyeccionDocenteHoraItemDTO> data List<ProyeccionDocenteHoraItemDTO> data
) {} ) {}
...@@ -3,23 +3,20 @@ package so.planilla_be.proyecciondocentehora.dto; ...@@ -3,23 +3,20 @@ package so.planilla_be.proyecciondocentehora.dto;
import jakarta.validation.constraints.Digits; import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.PositiveOrZero; import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal; import java.math.BigDecimal;
/** /**
* Una fila de pl_proyeccion_renta_docentehora. {@code boletaTipoPersonal}: DOC (remun_bruta_futura) * Una fila de pl_proyeccion_docente_horas. tipoProyeccion: 1 sueldo, 2 gratificación
* o GRA (gratificacion, meses 7 y 12). Importes nulos se graban como 0. * (meses 7 y 12). Importes nulos se graban como 0.
*/ */
public record ProyeccionDocenteHoraRequestDTO( public record ProyeccionDocenteHoraRequestDTO(
@NotNull Integer proyeccionAnio, @NotNull @Min(1) @Max(9999) Integer anio,
@NotNull @Min(1) @Max(12) Integer proyeccionMes, @NotNull @Min(1) @Max(12) Integer mes,
@NotBlank @Size(max = 15) String numeroDocumento, @NotNull @Min(1) Integer codper,
@NotBlank @Pattern(regexp = "DOC|GRA", message = "debe ser DOC o GRA") String boletaTipoPersonal, @NotNull @Min(1) @Max(2) Integer tipoProyeccion,
@PositiveOrZero @Digits(integer = 10, fraction = 2) BigDecimal remunBrutaFutura, @PositiveOrZero @Digits(integer = 10, fraction = 2) BigDecimal remunBrutaFutura,
@PositiveOrZero @Digits(integer = 10, fraction = 2) BigDecimal gratificacion @PositiveOrZero @Digits(integer = 10, fraction = 2) BigDecimal gratificacion
) {} ) {}
...@@ -6,6 +6,7 @@ import jakarta.persistence.GeneratedValue; ...@@ -6,6 +6,7 @@ import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
...@@ -13,14 +14,15 @@ import java.math.BigDecimal; ...@@ -13,14 +14,15 @@ import java.math.BigDecimal;
/** /**
* Remuneración bruta proyectada por mes de un docente por hora (DOC con tipo_pago = 1), en * Remuneración bruta proyectada por mes de un docente por hora (DOC con tipo_pago = 1), en
* planilla.pl_proyeccion_renta_docentehora. Reemplaza a "remuneración × meses restantes" en la * planilla.pl_proyeccion_docente_horas. Reemplaza a "remuneración × meses restantes" en la
* proyección del sueldo bruto de renta de quinta. La renta de quinta solo la lee; se graba desde * proyección del sueldo bruto de renta de quinta. La renta de quinta solo la lee; se graba desde
* el mantenimiento de este módulo. * el mantenimiento de este módulo.
*/ */
@Getter @Getter
@Setter @Setter
@Entity @Entity
@Table(name = "pl_proyeccion_renta_docentehora") @Table(name = "pl_proyeccion_docente_horas", schema = "planilla",
uniqueConstraints = @UniqueConstraint(columnNames = {"codper", "anio", "mes", "tipo_proyeccion"}))
public class ProyeccionDocenteHora { public class ProyeccionDocenteHora {
/** identity ALWAYS en BD: nunca se envía en el insert. */ /** identity ALWAYS en BD: nunca se envía en el insert. */
...@@ -29,22 +31,22 @@ public class ProyeccionDocenteHora { ...@@ -29,22 +31,22 @@ public class ProyeccionDocenteHora {
@Column(name = "id_proyeccion") @Column(name = "id_proyeccion")
private Long idProyeccion; private Long idProyeccion;
@Column(name = "proyeccion_anio", nullable = false) @Column(name = "anio", nullable = false)
private Integer proyeccionAnio; private Integer anio;
@Column(name = "proyeccion_mes", nullable = false) @Column(name = "mes", nullable = false)
private Integer proyeccionMes; private Integer mes;
@Column(name = "numero_documento", length = 15, nullable = false) @Column(name = "codper", nullable = false)
private String numeroDocumento; private Integer codper;
@Column(name = "boleta_tipo_personal", length = 10, nullable = false) @Column(name = "tipo_proyeccion", nullable = false)
private String boletaTipoPersonal; private Integer tipoProyeccion;
@Column(name = "remun_bruta_futura", precision = 12, scale = 2) @Column(name = "remun_bruta_futura", precision = 12, scale = 2, nullable = false)
private BigDecimal remunBrutaFutura; private BigDecimal remunBrutaFutura;
/** Gratificación proyectada: solo viene en filas boleta_tipo_personal = 'GRA' (meses 7 y 12). */ /** Gratificación proyectada: tipo_proyeccion = 2, meses 7 y 12. */
@Column(name = "gratificacion", precision = 12, scale = 2) @Column(name = "gratificacion", precision = 12, scale = 2, nullable = false)
private BigDecimal gratificacion; private BigDecimal gratificacion;
} }
...@@ -11,17 +11,17 @@ import java.util.Optional; ...@@ -11,17 +11,17 @@ import java.util.Optional;
public interface ProyeccionDocenteHoraRepository extends JpaRepository<ProyeccionDocenteHora, Long> { public interface ProyeccionDocenteHoraRepository extends JpaRepository<ProyeccionDocenteHora, Long> {
/** Filas del docente en el año, por mes y luego por tipo (DOC antes que GRA). */ /** Filas del docente en el año, por mes y luego por tipo (sueldo antes que gratificación). */
List<ProyeccionDocenteHora> findByNumeroDocumentoAndProyeccionAnioOrderByProyeccionMesAscBoletaTipoPersonalAsc( List<ProyeccionDocenteHora> findByCodperAndAnioOrderByMesAscTipoProyeccionAsc(
String numeroDocumento, Integer proyeccionAnio); Integer codper, Integer anio);
/** Filas del año de varios docentes en una sola consulta (grilla / grabado masivo). */ /** Filas del año de varios docentes en una sola consulta (grilla / grabado masivo). */
List<ProyeccionDocenteHora> findByProyeccionAnioAndNumeroDocumentoIn(Integer proyeccionAnio, List<ProyeccionDocenteHora> findByAnioAndCodperIn(Integer anio,
List<String> numerosDocumento); List<Integer> codpers);
/** Clave lógica (la tabla solo tiene PK sobre id_proyeccion). */ /** Clave lógica protegida por UNIQUE (codper, anio, mes, tipo_proyeccion). */
Optional<ProyeccionDocenteHora> findFirstByNumeroDocumentoAndBoletaTipoPersonalAndProyeccionAnioAndProyeccionMes( Optional<ProyeccionDocenteHora> findFirstByCodperAndTipoProyeccionAndAnioAndMes(
String numeroDocumento, String boletaTipoPersonal, Integer proyeccionAnio, Integer proyeccionMes); Integer codper, Integer tipoProyeccion, Integer anio, Integer mes);
/** /**
* remun_bruta_futura del docente en el mes consultado: es el sueldo bruto de la planilla en * remun_bruta_futura del docente en el mes consultado: es el sueldo bruto de la planilla en
...@@ -30,29 +30,29 @@ public interface ProyeccionDocenteHoraRepository extends JpaRepository<Proyeccio ...@@ -30,29 +30,29 @@ public interface ProyeccionDocenteHoraRepository extends JpaRepository<Proyeccio
@Query(""" @Query("""
select coalesce(sum(p.remunBrutaFutura), 0) select coalesce(sum(p.remunBrutaFutura), 0)
from ProyeccionDocenteHora p from ProyeccionDocenteHora p
where p.numeroDocumento = :numeroDocumento where p.codper = :codper
and trim(p.boletaTipoPersonal) = :tipoPersonal and p.tipoProyeccion = :tipoProyeccion
and p.proyeccionAnio = :anio and p.anio = :anio
and p.proyeccionMes = :mes and p.mes = :mes
""") """)
BigDecimal remunBrutaFutura(@Param("numeroDocumento") String numeroDocumento, BigDecimal remunBrutaFutura(@Param("codper") Integer codper,
@Param("tipoPersonal") String tipoPersonal, @Param("tipoProyeccion") Integer tipoProyeccion,
@Param("anio") Integer anio, @Param("anio") Integer anio,
@Param("mes") int mes); @Param("mes") int mes);
/** /**
* Gratificación proyectada del mes (7 o 12): campo gratificacion de las filas * Gratificación proyectada del mes (7 o 12): campo gratificacion de las filas
* boleta_tipo_personal = 'GRA'. 0 si no hay fila. * tipo_proyeccion = 2. 0 si no hay fila.
*/ */
@Query(""" @Query("""
select coalesce(sum(p.gratificacion), 0) select coalesce(sum(p.gratificacion), 0)
from ProyeccionDocenteHora p from ProyeccionDocenteHora p
where p.numeroDocumento = :numeroDocumento where p.codper = :codper
and trim(p.boletaTipoPersonal) = 'GRA' and p.tipoProyeccion = 2
and p.proyeccionAnio = :anio and p.anio = :anio
and p.proyeccionMes = :mes and p.mes = :mes
""") """)
BigDecimal gratificacionDelMes(@Param("numeroDocumento") String numeroDocumento, BigDecimal gratificacionDelMes(@Param("codper") Integer codper,
@Param("anio") Integer anio, @Param("anio") Integer anio,
@Param("mes") int mes); @Param("mes") int mes);
...@@ -63,13 +63,13 @@ public interface ProyeccionDocenteHoraRepository extends JpaRepository<Proyeccio ...@@ -63,13 +63,13 @@ public interface ProyeccionDocenteHoraRepository extends JpaRepository<Proyeccio
@Query(""" @Query("""
select coalesce(sum(p.remunBrutaFutura), 0) select coalesce(sum(p.remunBrutaFutura), 0)
from ProyeccionDocenteHora p from ProyeccionDocenteHora p
where p.numeroDocumento = :numeroDocumento where p.codper = :codper
and trim(p.boletaTipoPersonal) = :tipoPersonal and p.tipoProyeccion = :tipoProyeccion
and p.proyeccionAnio = :anio and p.anio = :anio
and p.proyeccionMes > :mes and p.mes > :mes
""") """)
BigDecimal sumaRemunBrutaFuturaPosterior(@Param("numeroDocumento") String numeroDocumento, BigDecimal sumaRemunBrutaFuturaPosterior(@Param("codper") Integer codper,
@Param("tipoPersonal") String tipoPersonal, @Param("tipoProyeccion") Integer tipoProyeccion,
@Param("anio") Integer anio, @Param("anio") Integer anio,
@Param("mes") int mes); @Param("mes") int mes);
} }
...@@ -9,10 +9,10 @@ import java.util.List; ...@@ -9,10 +9,10 @@ import java.util.List;
public interface ProyeccionDocenteHoraService { public interface ProyeccionDocenteHoraService {
/** Filas grabadas del docente en el año, con su nombre. Sin filas: data vacía. */ /** Filas grabadas del docente en el año, con su nombre. Sin filas: data vacía. */
ProyeccionDocenteHoraListadoDTO listarPorAnio(String numeroDocumento, Integer anio); ProyeccionDocenteHoraListadoDTO listarPorAnio(Integer codper, Integer anio);
/** /**
* Graba las filas recibidas. La clave lógica es documento + boletaTipoPersonal + año + mes: * Graba las filas recibidas. La clave lógica es codper + tipoProyeccion + año + mes:
* si ya existe se actualizan los importes, si no se inserta. Todo o nada. * si ya existe se actualizan los importes, si no se inserta. Todo o nada.
*/ */
List<ProyeccionDocenteHoraDTO> grabar(List<ProyeccionDocenteHoraRequestDTO> filas); List<ProyeccionDocenteHoraDTO> grabar(List<ProyeccionDocenteHoraRequestDTO> filas);
......
...@@ -31,25 +31,24 @@ public class ProyeccionDocenteHoraServiceImpl implements ProyeccionDocenteHoraSe ...@@ -31,25 +31,24 @@ public class ProyeccionDocenteHoraServiceImpl implements ProyeccionDocenteHoraSe
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ProyeccionDocenteHoraListadoDTO listarPorAnio(String numeroDocumento, Integer anio) { public ProyeccionDocenteHoraListadoDTO listarPorAnio(Integer codper, Integer anio) {
String documento = numeroDocumento.trim();
List<ProyeccionDocenteHoraItemDTO> data = repository List<ProyeccionDocenteHoraItemDTO> data = repository
.findByNumeroDocumentoAndProyeccionAnioOrderByProyeccionMesAscBoletaTipoPersonalAsc(documento, anio) .findByCodperAndAnioOrderByMesAscTipoProyeccionAsc(codper, anio)
.stream() .stream()
.map(e -> new ProyeccionDocenteHoraItemDTO( .map(e -> new ProyeccionDocenteHoraItemDTO(
e.getIdProyeccion(), e.getIdProyeccion(),
e.getProyeccionMes(), e.getMes(),
e.getBoletaTipoPersonal(), e.getTipoProyeccion(),
e.getRemunBrutaFutura(), e.getRemunBrutaFutura(),
e.getGratificacion())) e.getGratificacion()))
.toList(); .toList();
return new ProyeccionDocenteHoraListadoDTO(documento, nombreCompleto(documento), anio, data); return new ProyeccionDocenteHoraListadoDTO(codper, nombreCompleto(codper), anio, data);
} }
@Override @Override
@Transactional @Transactional
public List<ProyeccionDocenteHoraDTO> grabar(List<ProyeccionDocenteHoraRequestDTO> filas) { public List<ProyeccionDocenteHoraDTO> grabar(List<ProyeccionDocenteHoraRequestDTO> filas) {
Map<String, String> nombres = new HashMap<>(); Map<Integer, String> nombres = new HashMap<>();
return filas.stream() return filas.stream()
.map(fila -> grabarFila(fila, nombres)) .map(fila -> grabarFila(fila, nombres))
.toList(); .toList();
...@@ -64,12 +63,9 @@ public class ProyeccionDocenteHoraServiceImpl implements ProyeccionDocenteHoraSe ...@@ -64,12 +63,9 @@ public class ProyeccionDocenteHoraServiceImpl implements ProyeccionDocenteHoraSe
repository.delete(entidad); repository.delete(entidad);
} }
/** /** Nombre por codper desde nuevo.personal; null si no se halla. */
* Nombre por DNI desde la tabla de personal, reutilizando el repositorio de otros empleadores. private String nombreCompleto(Integer codper) {
* Temporal: los docentes vendrán de otra tabla; cambiar solo este método. null si no se halla. return personalRepository.findById(codper)
*/
private String nombreCompleto(String numeroDocumento) {
return personalRepository.findFirstByNumeroDocumentoOrderByCodperDesc(numeroDocumento)
.map(p -> "%s %s".formatted( .map(p -> "%s %s".formatted(
p.getApellido() == null ? "" : p.getApellido().trim(), p.getApellido() == null ? "" : p.getApellido().trim(),
p.getNombre() == null ? "" : p.getNombre().trim()).trim()) p.getNombre() == null ? "" : p.getNombre().trim()).trim())
...@@ -78,19 +74,30 @@ public class ProyeccionDocenteHoraServiceImpl implements ProyeccionDocenteHoraSe ...@@ -78,19 +74,30 @@ public class ProyeccionDocenteHoraServiceImpl implements ProyeccionDocenteHoraSe
} }
private ProyeccionDocenteHoraDTO grabarFila(ProyeccionDocenteHoraRequestDTO fila, private ProyeccionDocenteHoraDTO grabarFila(ProyeccionDocenteHoraRequestDTO fila,
Map<String, String> nombres) { Map<Integer, String> nombres) {
String documento = fila.numeroDocumento().trim(); if (Integer.valueOf(1).equals(fila.tipoProyeccion()) && cero(fila.gratificacion()).signum() != 0) {
throw new IllegalArgumentException("Una proyección de sueldo no puede contener gratificacion");
}
if (Integer.valueOf(2).equals(fila.tipoProyeccion())) {
if (fila.mes() != 7 && fila.mes() != 12) {
throw new IllegalArgumentException("La gratificación solo se proyecta en julio o diciembre");
}
if (cero(fila.remunBrutaFutura()).signum() != 0) {
throw new IllegalArgumentException("Una proyección de gratificación no puede contener remunBrutaFutura");
}
}
Integer codper = fila.codper();
ProyeccionDocenteHora entidad = repository ProyeccionDocenteHora entidad = repository
.findFirstByNumeroDocumentoAndBoletaTipoPersonalAndProyeccionAnioAndProyeccionMes( .findFirstByCodperAndTipoProyeccionAndAnioAndMes(
documento, fila.boletaTipoPersonal(), fila.proyeccionAnio(), fila.proyeccionMes()) codper, fila.tipoProyeccion(), fila.anio(), fila.mes())
.orElseGet(ProyeccionDocenteHora::new); .orElseGet(ProyeccionDocenteHora::new);
entidad.setNumeroDocumento(documento); entidad.setCodper(codper);
entidad.setBoletaTipoPersonal(fila.boletaTipoPersonal()); entidad.setTipoProyeccion(fila.tipoProyeccion());
entidad.setProyeccionAnio(fila.proyeccionAnio()); entidad.setAnio(fila.anio());
entidad.setProyeccionMes(fila.proyeccionMes()); entidad.setMes(fila.mes());
entidad.setRemunBrutaFutura(cero(fila.remunBrutaFutura())); entidad.setRemunBrutaFutura(cero(fila.remunBrutaFutura()));
entidad.setGratificacion(cero(fila.gratificacion())); entidad.setGratificacion(cero(fila.gratificacion()));
String nombre = nombres.computeIfAbsent(documento, this::nombreCompleto); String nombre = nombres.computeIfAbsent(codper, this::nombreCompleto);
return toDto(repository.save(entidad), nombre); return toDto(repository.save(entidad), nombre);
} }
...@@ -101,11 +108,11 @@ public class ProyeccionDocenteHoraServiceImpl implements ProyeccionDocenteHoraSe ...@@ -101,11 +108,11 @@ public class ProyeccionDocenteHoraServiceImpl implements ProyeccionDocenteHoraSe
private ProyeccionDocenteHoraDTO toDto(ProyeccionDocenteHora e, String nombreCompleto) { private ProyeccionDocenteHoraDTO toDto(ProyeccionDocenteHora e, String nombreCompleto) {
return new ProyeccionDocenteHoraDTO( return new ProyeccionDocenteHoraDTO(
e.getIdProyeccion(), e.getIdProyeccion(),
e.getProyeccionAnio(), e.getAnio(),
e.getProyeccionMes(), e.getMes(),
e.getNumeroDocumento(), e.getCodper(),
nombreCompleto, nombreCompleto,
e.getBoletaTipoPersonal(), e.getTipoProyeccion(),
e.getRemunBrutaFutura(), e.getRemunBrutaFutura(),
e.getGratificacion()); e.getGratificacion());
} }
......
package so.planilla_be.rentabasico.entity; package so.planilla_be.rentabasico.entity;
import jakarta.persistence.Column; import jakarta.persistence.*;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.math.BigDecimal; import java.math.BigDecimal;
/**
* Básico mensual declarado en planilla.pl_renta_basico. Cuando existe un registro para
* (numeroDocumento, anio, mes) reemplaza al básico de planilla.pl_planilla en el cálculo de renta
* de quinta; si no existe, se usa el de pl_planilla tal cual.
*/
@Getter @Getter
@Setter @Setter
@Entity @Entity
@Table(name = "pl_renta_basico") @Table(name = "pl_renta_basico")
@IdClass(RentaBasicoId.class) @IdClass(RentaBasicoId.class)
public class RentaBasico { public class RentaBasico {
@Id @Column(name = "codper")
@Id private Integer codper;
@Column(name = "numero_documento", length = 15) @Id @Column(name = "anio")
private String numeroDocumento;
@Id
@Column(name = "anio")
private Integer anio; private Integer anio;
@Id @Column(name = "mes")
@Id
@Column(name = "mes")
private Integer mes; private Integer mes;
@Column(name = "monto_basico", precision = 12, scale = 2, nullable = false) @Column(name = "monto_basico", precision = 12, scale = 2, nullable = false)
private BigDecimal montoBasico; private BigDecimal montoBasico;
} }
\ No newline at end of file
package so.planilla_be.rentabasico.entity; package so.planilla_be.rentabasico.entity;
import lombok.*;
import java.io.Serializable; import java.io.Serializable;
import java.util.Objects;
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class RentaBasicoId implements Serializable { public class RentaBasicoId implements Serializable {
private Integer codper;
private String numeroDocumento;
private Integer anio; private Integer anio;
private Integer mes; private Integer mes;
public RentaBasicoId() {
}
public RentaBasicoId(String numeroDocumento, Integer anio, Integer mes) {
this.numeroDocumento = numeroDocumento;
this.anio = anio;
this.mes = mes;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof RentaBasicoId that)) {
return false;
}
return Objects.equals(numeroDocumento, that.numeroDocumento)
&& Objects.equals(anio, that.anio)
&& Objects.equals(mes, that.mes);
}
@Override
public int hashCode() {
return Objects.hash(numeroDocumento, anio, mes);
}
} }
\ No newline at end of file
...@@ -3,10 +3,8 @@ package so.planilla_be.rentabasico.repository; ...@@ -3,10 +3,8 @@ package so.planilla_be.rentabasico.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import so.planilla_be.rentabasico.entity.RentaBasico; import so.planilla_be.rentabasico.entity.RentaBasico;
import so.planilla_be.rentabasico.entity.RentaBasicoId; import so.planilla_be.rentabasico.entity.RentaBasicoId;
import java.util.List; import java.util.List;
public interface RentaBasicoRepository extends JpaRepository<RentaBasico, RentaBasicoId> { public interface RentaBasicoRepository extends JpaRepository<RentaBasico, RentaBasicoId> {
List<RentaBasico> findByAnioAndCodperInOrderByMesDesc(Integer anio, List<Integer> codpers);
List<RentaBasico> findByAnioAndNumeroDocumentoIn(Integer anio, List<String> numerosDocumento);
} }
\ No newline at end of file
...@@ -21,7 +21,7 @@ public enum ModalidadCalculo { ...@@ -21,7 +21,7 @@ public enum ModalidadCalculo {
MONTO_FIJO, MONTO_FIJO,
/** /**
* Proyección = {@code remun_bruta_futura} de pl_proyeccion_renta_docentehora del mes * Proyección = {@code remun_bruta_futura} de pl_proyeccion_docente_horas del mes
* consultado. El resto del cálculo es igual que {@link #MONTO_FIJO}. * consultado. El resto del cálculo es igual que {@link #MONTO_FIJO}.
*/ */
DOCENTE_POR_HORA, DOCENTE_POR_HORA,
......
...@@ -193,7 +193,7 @@ public class RentaQuintaCalculadora { ...@@ -193,7 +193,7 @@ public class RentaQuintaCalculadora {
* <li>Contrato posterior al semestre: 0.</li> * <li>Contrato posterior al semestre: 0.</li>
* <li>Ingreso dentro del semestre: meses = meses completos después del de ingreso hasta * <li>Ingreso dentro del semestre: meses = meses completos después del de ingreso hasta
* fin del semestre (si entra el día 1, el mes de ingreso también cuenta completo).</li> * fin del semestre (si entra el día 1, el mes de ingreso también cuenta completo).</li>
* <li>Días = días del mes en planilla.pl_calendario − día de ingreso + 1 (0 si entra el * <li>Días = días del mes en nuevo.periodo_asistencia − día de ingreso + 1 (0 si entra el
* día 1), en cualquier mes de ingreso salvo enero: ingreso en enero → solo meses * día 1), en cualquier mes de ingreso salvo enero: ingreso en enero → solo meses
* (semestre completo). Igual en ADM y DOC2.</li> * (semestre completo). Igual en ADM y DOC2.</li>
* <li>DOC2, diciembre: diciembre no es mes completo sino 16 días. Semestre completo = * <li>DOC2, diciembre: diciembre no es mes completo sino 16 días. Semestre completo =
...@@ -265,7 +265,7 @@ public class RentaQuintaCalculadora { ...@@ -265,7 +265,7 @@ public class RentaQuintaCalculadora {
/** /**
* DOC1: la gratificación de julio (7) o diciembre (12) es el campo gratificacion de * DOC1: la gratificación de julio (7) o diciembre (12) es el campo gratificacion de
* pl_proyeccion_renta_docentehora (filas boleta_tipo_personal = 'GRA'), sin fórmula × 1.09. * pl_proyeccion_docente_horas (filas tipo_proyeccion = 2), sin fórmula × 1.09.
*/ */
private BigDecimal gratificacionDocenteHora(DatosCalculo datos, int mes) { private BigDecimal gratificacionDocenteHora(DatosCalculo datos, int mes) {
return orZero(mes == 7 ? datos.gratificacionJulioDocenteHora() : datos.gratificacionDiciembreDocenteHora()); return orZero(mes == 7 ? datos.gratificacionJulioDocenteHora() : datos.gratificacionDiciembreDocenteHora());
...@@ -276,7 +276,7 @@ public class RentaQuintaCalculadora { ...@@ -276,7 +276,7 @@ public class RentaQuintaCalculadora {
* <ul> * <ul>
* <li>ADM y DOC tipo_pago 2: remuneración del mes de referencia × meses restantes.</li> * <li>ADM y DOC tipo_pago 2: remuneración del mes de referencia × meses restantes.</li>
* <li>DOC tipo_pago 1 (por hora): suma de remun_bruta_futura de los meses posteriores al * <li>DOC tipo_pago 1 (por hora): suma de remun_bruta_futura de los meses posteriores al
* consultado en pl_proyeccion_renta_docentehora ({@code proyeccionDocenteHora}).</li> * consultado en pl_proyeccion_docente_horas ({@code proyeccionDocenteHora}).</li>
* <li>DOC tipo_pago 2: igual que ADM, pero diciembre entra proporcional (÷ 30 × 16).</li> * <li>DOC tipo_pago 2: igual que ADM, pero diciembre entra proporcional (÷ 30 × 16).</li>
* </ul> * </ul>
*/ */
...@@ -382,7 +382,7 @@ public class RentaQuintaCalculadora { ...@@ -382,7 +382,7 @@ public class RentaQuintaCalculadora {
* bruto de la boleta real del mes de corte que ese monto reemplaza. * bruto de la boleta real del mes de corte que ese monto reemplaza.
* {@code gratificacionPorContrato} es true en ADM y DOC2: la gratificación se prorratea por * {@code gratificacionPorContrato} es true en ADM y DOC2: la gratificación se prorratea por
* {@code fechaInicioContrato} (sin fecha = 0); {@code diasMesInicioContrato} son los días de * {@code fechaInicioContrato} (sin fecha = 0); {@code diasMesInicioContrato} son los días de
* ese mes según planilla.pl_calendario. {@code gratificacionDiciembreCompleta} es true en DOC2 * ese mes según nuevo.periodo_asistencia. {@code gratificacionDiciembreCompleta} es true en DOC2
* cuando el documento tiene un registro de diciembre del año en planilla.pl_gratificacion. La * cuando el documento tiene un registro de diciembre del año en planilla.pl_gratificacion. La
* UIT, las 7 UIT, los tramos y el divisor son de ley e iguales para todas las planillas. * UIT, las 7 UIT, los tramos y el divisor son de ley e iguales para todas las planillas.
*/ */
......
...@@ -3,7 +3,7 @@ package so.planilla_be.rentaquinta.calculo; ...@@ -3,7 +3,7 @@ package so.planilla_be.rentaquinta.calculo;
import java.util.Arrays; import java.util.Arrays;
/** /**
* {@code tipo_pago} de pl_planilla, solo para docentes ({@link TipoPlanilla#DOCENTE}). El cliente * {@code id_tipo_pago} de nuevo.personal, solo para docentes ({@link TipoPlanilla#DOCENTE}). El cliente
* (C#) lo envía como sufijo del parámetro {@code tipoPlanilla}: * (C#) lo envía como sufijo del parámetro {@code tipoPlanilla}:
* *
* <ul> * <ul>
...@@ -13,7 +13,7 @@ import java.util.Arrays; ...@@ -13,7 +13,7 @@ import java.util.Arrays;
* y gratificación de diciembre proporcionales (÷ 30 × 16)</li> * y gratificación de diciembre proporcionales (÷ 30 × 16)</li>
* </ul> * </ul>
* *
* {@code DOC} sin dígito no se acepta. Sin {@code tipoPlanilla} se toma el {@code tipo_pago} de la * {@code DOC} sin dígito no se acepta. Sin {@code tipoPlanilla} se toma el {@code id_tipo_pago} de la
* boleta. * boleta.
*/ */
public enum TipoPago { public enum TipoPago {
...@@ -31,7 +31,7 @@ public enum TipoPago { ...@@ -31,7 +31,7 @@ public enum TipoPago {
return codigo; return codigo;
} }
/** Tipo de pago de una boleta ({@code tipo_pago} char(1), con padding). {@code null} si no aplica. */ /** Tipo de pago de una boleta ({@code id_tipo_pago} char(1), con padding). {@code null} si no aplica. */
public static TipoPago desdeCodigo(String codigo) { public static TipoPago desdeCodigo(String codigo) {
if (codigo == null) { if (codigo == null) {
return null; return null;
......
...@@ -10,11 +10,11 @@ import java.util.stream.Stream; ...@@ -10,11 +10,11 @@ import java.util.stream.Stream;
/** /**
* Tipo de planilla, identificado por {@code planilla.pl_planilla.boleta_tipo_personal}. Filtra la * Tipo de planilla, identificado por {@code planilla.pl_planilla.boleta_tipo_personal}. Filtra la
* grilla y forma parte de la clave de lo grabado en pl_renta_quinta. Las reglas del cálculo no * grilla y forma parte de la clave de lo grabado en pl_renta_quinta. Las reglas del cálculo no
* salen solo de él: junto con {@code tipo_pago} definen la {@link ModalidadCalculo}. * salen solo de él: junto con {@code id_tipo_pago} definen la {@link ModalidadCalculo}.
* *
* <p>El cliente (C#) lo envía en el parámetro {@code tipoPlanilla}: {@code ADM}, {@code DOC1} * <p>El cliente (C#) lo envía en el parámetro {@code tipoPlanilla}: {@code ADM}, {@code DOC1}
* (docente costo por hora) o {@code DOC2} (docente monto fijo); el dígito es el * (docente costo por hora) o {@code DOC2} (docente monto fijo); el dígito es el
* {@code tipo_pago} ({@link TipoPago}). Si no lo envía, se toma de la boleta del trabajador. * {@code id_tipo_pago} ({@link TipoPago}). Si no lo envía, se toma de nuevo.personal del trabajador.
*/ */
public enum TipoPlanilla { public enum TipoPlanilla {
...@@ -22,10 +22,10 @@ public enum TipoPlanilla { ...@@ -22,10 +22,10 @@ public enum TipoPlanilla {
MENSUAL_MONTO_FIJO("ADM", "Planilla mensual (monto fijo)"), MENSUAL_MONTO_FIJO("ADM", "Planilla mensual (monto fijo)"),
/** /**
* {@code DOC}: docentes. Con {@code tipo_pago = 2} calculan igual que {@link #MENSUAL_MONTO_FIJO}; * {@code DOC}: docentes. Con {@code id_tipo_pago = 2} calculan igual que {@link #MENSUAL_MONTO_FIJO};
* con {@code tipo_pago = 1} son docentes por hora (ver {@link ModalidadCalculo}). * con {@code id_tipo_pago = 1} son docentes por hora (ver {@link ModalidadCalculo}).
*/ */
DOCENTE("DOC", "Docentes (monto fijo o por hora, según tipo_pago)"); DOCENTE("DOC", "Docentes (monto fijo o por hora, según id_tipo_pago)");
private final String codigo; private final String codigo;
private final String descripcion; private final String descripcion;
...@@ -82,7 +82,7 @@ public enum TipoPlanilla { ...@@ -82,7 +82,7 @@ public enum TipoPlanilla {
/** /**
* Parámetro {@code tipoPlanilla} obligatorio (grabado en pl_renta_quinta): el tipo lo decide * Parámetro {@code tipoPlanilla} obligatorio (grabado en pl_renta_quinta): el tipo lo decide
* quien envía, no se toma de la boleta. Vacío o desconocido responde 400. * quien envía, no se toma de nuevo.personal. Vacío o desconocido responde 400.
*/ */
public static TipoPlanilla obligatorio(String codigo) { public static TipoPlanilla obligatorio(String codigo) {
TipoPlanilla tipo = desdeParametro(codigo); TipoPlanilla tipo = desdeParametro(codigo);
...@@ -99,7 +99,9 @@ public enum TipoPlanilla { ...@@ -99,7 +99,9 @@ public enum TipoPlanilla {
* cálculo que se aplicaba antes de condicionar por tipo. * cálculo que se aplicaba antes de condicionar por tipo.
*/ */
public static TipoPlanilla deBoleta(RentaQuinta boleta) { public static TipoPlanilla deBoleta(RentaQuinta boleta) {
String codigo = boleta.getBoletaTipoPersonal() == null ? "" : boleta.getBoletaTipoPersonal().trim(); Integer tipo = boleta.getTipopla();
return DOCENTE.codigo.equals(codigo) ? DOCENTE : MENSUAL_MONTO_FIJO; if (Integer.valueOf(1).equals(tipo) || Integer.valueOf(3).equals(tipo)) return DOCENTE;
if (Integer.valueOf(5).equals(tipo) || Integer.valueOf(6).equals(tipo)) return MENSUAL_MONTO_FIJO;
throw new TipoPlanillaInvalidaException("La boleta requiere tipopla 1, 3, 5 o 6");
} }
} }
...@@ -11,7 +11,7 @@ import java.util.Map; ...@@ -11,7 +11,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* Valor de la UIT por año, cargado desde {@code planilla.parametro_uit} al arrancar la * Valor de la UIT por año, cargado desde {@code planilla.pl_uit} al arrancar la
* aplicación y mantenido en memoria (bean singleton) para no consultar la base en cada cálculo. * aplicación y mantenido en memoria (bean singleton) para no consultar la base en cada cálculo.
*/ */
@Component @Component
...@@ -44,6 +44,6 @@ public class UitCache { ...@@ -44,6 +44,6 @@ public class UitCache {
.max(Integer::compareTo) .max(Integer::compareTo)
.map(valores::get) .map(valores::get)
.orElseThrow(() -> new IllegalStateException( .orElseThrow(() -> new IllegalStateException(
"No hay valores de UIT cargados en planilla.parametro_uit")); "No hay valores de UIT cargados en planilla.pl_uit"));
} }
} }
...@@ -30,7 +30,7 @@ public class FiltrosController { ...@@ -30,7 +30,7 @@ public class FiltrosController {
return service.listar(tipo); return service.listar(tipo);
} }
@Operation(summary = "Lista periodos registrados en pl_periodo, del más reciente al más antiguo") @Operation(summary = "Lista periodos registrados en nuevo.periodo_asistencia, del más reciente al más antiguo")
@GetMapping("/periodos") @GetMapping("/periodos")
public List<PeriodoFiltroDTO> listarPeriodos() { public List<PeriodoFiltroDTO> listarPeriodos() {
return service.listarPeriodos(); return service.listarPeriodos();
......
...@@ -34,13 +34,13 @@ public class RentaQuintaController { ...@@ -34,13 +34,13 @@ 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_docente_horas); DOC2 = docente monto fijo "
+ "(gratificación por fecha de contrato, diciembre de 16 días salvo excepción registrada; " + "(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 " + "sueldo bruto de diciembre ÷ 30 × 16). Filtra por tipopla y nuevo.personal.id_tipo_pago. Si no se envía, se "
+ "toma de la boleta del trabajador."; + "toma de nuevo.personal.id_tipo_pago.";
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, vacío en ADM)."; + "(monto fijo). La retención se guarda por codper, año y mes, sin requerir boleta del mes.";
private final RentaQuintaService service; private final RentaQuintaService service;
...@@ -70,29 +70,48 @@ public class RentaQuintaController { ...@@ -70,29 +70,48 @@ public class RentaQuintaController {
tieneVariacion, tieneRetencion, documentoOApellidos, page, size)); tieneVariacion, tieneRetencion, documentoOApellidos, page, size));
} }
@Operation(summary = "Detalle completo por id_renta_quinta") @Operation(summary = "Listado completo para la tabla web y Excel, calculado en una sola consulta")
@GetMapping("/listado/completo")
public List<RentaQuintaResumenDTO> listadoCompleto(
@RequestParam @NotNull Integer anio,
@RequestParam @NotNull @Min(1) @Max(12) Integer mes,
@RequestParam(required = false) Integer sede,
@Parameter(description = TIPO_PLANILLA_DOC) @RequestParam(required = false) String tipoPlanilla,
@RequestParam(required = false) Integer areaGeneral,
@RequestParam(required = false) Integer subArea,
@RequestParam(required = false) Integer cargo,
@RequestParam(required = false) Boolean tieneVariacion,
@RequestParam(required = false) Boolean tieneRetencion,
@RequestParam(required = false) String documentoOApellidos) {
TipoPlanilla tipo = TipoPlanilla.desdeParametro(tipoPlanilla);
return service.buscarCompleto(new RentaQuintaFiltroDTO(
anio, mes, sede, tipo, TipoPago.desdeParametro(tipoPlanilla), areaGeneral, subArea, cargo,
tieneVariacion, tieneRetencion, documentoOApellidos, null, null));
}
@Operation(summary = "Detalle completo por id_pl_planilla")
@GetMapping("/{id}") @GetMapping("/{id}")
public RentaQuintaDetalleDTO obtenerPorId(@PathVariable Long id) { public RentaQuintaDetalleDTO obtenerPorId(@PathVariable Long id) {
return service.obtenerPorId(id); return service.obtenerPorId(id);
} }
@Operation(summary = "Detalle completo del cálculo por número de documento + año + mes " @Operation(summary = "Detalle completo del cálculo por codper + año + mes "
+ "(boleta del mes + puntos 1 a 6: 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, control de retenciones y matriz mensual)") + "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 @Min(1) Integer codper,
@RequestParam Integer anio, @RequestParam Integer anio,
@RequestParam @Min(1) @Max(12) Integer mes, @RequestParam @Min(1) @Max(12) Integer mes,
@Parameter(description = TIPO_PLANILLA_DOC) @RequestParam(required = false) String tipoPlanilla) { @Parameter(description = TIPO_PLANILLA_DOC) @RequestParam(required = false) String tipoPlanilla) {
TipoPlanilla tipo = TipoPlanilla.desdeParametro(tipoPlanilla); TipoPlanilla tipo = TipoPlanilla.desdeParametro(tipoPlanilla);
return service.obtenerDetalleCompleto(numeroDocumento, anio, mes, tipo, TipoPago.desdeParametro(tipoPlanilla)); return service.obtenerDetalleCompleto(codper, anio, mes, tipo, TipoPago.desdeParametro(tipoPlanilla));
} }
@Operation(summary = "Detalle por codper + anio + mes") @Operation(summary = "Detalle por codper + anio + mes")
@GetMapping("/boleta") @GetMapping("/boleta")
public RentaQuintaDetalleDTO obtenerBoleta( public RentaQuintaDetalleDTO obtenerBoleta(
@RequestParam Integer codper, @RequestParam @Min(1) Integer codper,
@RequestParam Integer anio, @RequestParam Integer anio,
@RequestParam @Min(1) @Max(12) Integer mes) { @RequestParam @Min(1) @Max(12) Integer mes) {
return service.obtenerBoleta(codper, anio, mes); return service.obtenerBoleta(codper, anio, mes);
...@@ -101,21 +120,21 @@ public class RentaQuintaController { ...@@ -101,21 +120,21 @@ public class RentaQuintaController {
@Operation(summary = "Ver historial: meses del trabajador en el periodo") @Operation(summary = "Ver historial: meses del trabajador en el periodo")
@GetMapping("/historial/{codper}/{anio}") @GetMapping("/historial/{codper}/{anio}")
public List<RentaQuintaHistorialDTO> historialAnual( public List<RentaQuintaHistorialDTO> historialAnual(
@PathVariable Integer codper, @PathVariable @Min(1) Integer codper,
@PathVariable Integer anio) { @PathVariable Integer anio) {
return service.historialAnual(codper, anio); return service.historialAnual(codper, anio);
} }
@Operation(summary = "Graba (upsert) en pl_renta_quinta la retención del mes calculada para el " @Operation(summary = "Graba (upsert) en pl_renta_quinta la retención del mes calculada para el "
+ "trabajador: documento + año + mes + retención") + "trabajador por codper, año y mes, incluido un mes estimado")
@PostMapping("/grabar") @PostMapping("/grabar")
public ResponseEntity<Void> grabar( public ResponseEntity<Void> grabar(
@RequestParam String numeroDocumento, @RequestParam @Min(1) Integer codper,
@RequestParam @NotNull Integer anio, @RequestParam @NotNull Integer anio,
@RequestParam @NotNull @Min(1) @Max(12) Integer mes, @RequestParam @NotNull @Min(1) @Max(12) Integer mes,
@Parameter(description = TIPO_PLANILLA_GRABAR_DOC) @RequestParam String tipoPlanilla) { @Parameter(description = TIPO_PLANILLA_GRABAR_DOC) @RequestParam String tipoPlanilla) {
TipoPlanilla tipo = TipoPlanilla.obligatorio(tipoPlanilla); TipoPlanilla tipo = TipoPlanilla.obligatorio(tipoPlanilla);
service.grabarRetencion(numeroDocumento, anio, mes, tipo, TipoPago.desdeParametro(tipoPlanilla)); service.grabarRetencion(codper, anio, mes, tipo, TipoPago.desdeParametro(tipoPlanilla));
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
......
...@@ -9,7 +9,7 @@ import so.planilla_be.rentaquinta.calculo.TipoPlanilla; ...@@ -9,7 +9,7 @@ import so.planilla_be.rentaquinta.calculo.TipoPlanilla;
* <p>{@code documentoOApellidos} es texto libre (nro de documento o apellidos) y * <p>{@code documentoOApellidos} es texto libre (nro de documento o apellidos) y
* {@code tipoPlanilla} es el código de {@code boleta_tipo_personal} (ADM / DOC), que además de * {@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 id_tipo_pago} y decide la proyección. El resto de filtros de catálogo
* llegan como Integer. Área, subárea y cargo se resuelven desde nuevo.personal y sus catálogos * llegan como Integer. Área, subárea y cargo se resuelven desde nuevo.personal y sus catálogos
* foráneos. Los estados de variación y retención se evalúan sobre el cálculo antes de paginar. * foráneos. Los estados de variación y retención se evalúan sobre el cálculo antes de paginar.
*/ */
......
...@@ -5,10 +5,6 @@ import java.time.LocalDate; ...@@ -5,10 +5,6 @@ import java.time.LocalDate;
/** /**
* Fila de la grilla "Proyección y control de quinta categoría". * Fila de la grilla "Proyección y control de quinta categoría".
*
* <p>{@code proyeccionFutura} e {@code impuestoAnual} dependen del cálculo SUNAT (tramos,
* proyección), fuera del alcance de esta versión: se devuelven en cero / null hasta que
* exista el módulo de cálculo.
*/ */
public record RentaQuintaResumenDTO( public record RentaQuintaResumenDTO(
Integer codper, Integer codper,
...@@ -23,6 +19,9 @@ public record RentaQuintaResumenDTO( ...@@ -23,6 +19,9 @@ public record RentaQuintaResumenDTO(
String subArea, String subArea,
Integer idCargo, Integer idCargo,
String cargo, String cargo,
String codlocal,
Integer idSede,
String sede,
Integer mesProceso, Integer mesProceso,
BigDecimal historialReal, BigDecimal historialReal,
BigDecimal planillaCargada, BigDecimal planillaCargada,
......
package so.planilla_be.rentaquinta.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDate;
/**
* Mes del calendario (planilla.pl_calendario, una fila por año + mes). Lo carga
* docs/create-calendario.sql; el API solo lo lee.
*/
@Getter
@Setter
@Entity
@Table(name = "pl_calendario")
@IdClass(CalendarioId.class)
public class Calendario {
@Id
@Column(name = "anio")
private Integer anio;
@Id
@Column(name = "mes")
private Integer mes;
@Column(name = "nombre_mes")
private String nombreMes;
@Column(name = "fecha_inicio")
private LocalDate fechaInicio;
@Column(name = "fecha_fin")
private LocalDate fechaFin;
@Column(name = "dias")
private Integer dias;
/** 1 = enero-junio (gratificación de julio), 2 = julio-diciembre (de diciembre). */
@Column(name = "semestre")
private Integer semestre;
@Column(name = "mes_gratificacion")
private Integer mesGratificacion;
}
package so.planilla_be.rentaquinta.entity;
import java.io.Serializable;
public record CalendarioId(Integer anio, Integer mes) implements Serializable {}
...@@ -12,13 +12,16 @@ import java.math.BigDecimal; ...@@ -12,13 +12,16 @@ import java.math.BigDecimal;
@Getter @Getter
@Setter @Setter
@Entity @Entity
@Table(name = "parametro_uit") @Table(name = "pl_uit")
public class ParametroUit { public class ParametroUit {
@Id @Id
@Column(name = "id_uit")
private Integer idUit;
@Column(name = "anio") @Column(name = "anio")
private Integer anio; private Integer anio;
@Column(name = "monto_uit") @Column(name = "uit")
private BigDecimal montoUit; private BigDecimal montoUit;
} }
...@@ -40,22 +40,25 @@ public class RentaQuinta { ...@@ -40,22 +40,25 @@ public class RentaQuinta {
@Column(name = "codper") @Column(name = "codper")
private Integer codper; private Integer codper;
@Column(name = "codlocal", length = 4, columnDefinition = "bpchar") @Column(name = "codlocal")
private String codlocal; private String codlocal;
@Column(name = "unidad_organizativa") @Column(name = "id_unidad_organizativa")
private Integer unidadOrganizativa; private Integer unidadOrganizativa;
@Column(name = "tipo_planilla", length = 2, columnDefinition = "bpchar") @Column(name = "tipopla")
private String tipoPlanilla; private Integer tipopla;
@Column(name = "apellido_paterno") @Column(name = "id_tipo_pago")
private Integer idTipoPago;
@Transient
private String apellidoPaterno; private String apellidoPaterno;
@Column(name = "apellido_materno") @Transient
private String apellidoMaterno; private String apellidoMaterno;
@Column(name = "nombres") @Transient
private String nombres; private String nombres;
@Column(name = "documento_afp") @Column(name = "documento_afp")
...@@ -294,7 +297,7 @@ public class RentaQuinta { ...@@ -294,7 +297,7 @@ public class RentaQuinta {
@Column(name = "total_aportes", precision = 15, scale = 2) @Column(name = "total_aportes", precision = 15, scale = 2)
private BigDecimal totalAportes; private BigDecimal totalAportes;
@Column(name = "numero_documento") @Transient
private String numeroDocumento; private String numeroDocumento;
@Column(name = "cussp") @Column(name = "cussp")
...@@ -388,7 +391,7 @@ public class RentaQuinta { ...@@ -388,7 +391,7 @@ public class RentaQuinta {
private String tipoLabor; private String tipoLabor;
/** Solo en DOC: 1 = docente por hora, 2 = monto fijo (ver {@code ModalidadCalculo}). */ /** Solo en DOC: 1 = docente por hora, 2 = monto fijo (ver {@code ModalidadCalculo}). */
@Column(name = "tipo_pago", length = 1, columnDefinition = "bpchar") @Transient
private String tipoPago; private String tipoPago;
/** No existen en pl_planilla; se conservan para no romper el contrato del API. */ /** No existen en pl_planilla; se conservan para no romper el contrato del API. */
......
package so.planilla_be.rentaquinta.entity; package so.planilla_be.rentaquinta.entity;
import jakarta.persistence.Column; import jakarta.persistence.*;
import jakarta.persistence.Entity;
import jakarta.persistence.IdClass;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.math.BigDecimal; import java.math.BigDecimal;
/**
* Retención de quinta categoría grabada para un trabajador en un periodo dado, en
* planilla.pl_renta_quinta. Solo guarda lo mínimo: documento, año, mes, tipo de planilla, tipo de
* pago y la retención calculada para ese mes de corte (no es una boleta completa, esa vive en pl_planilla).
*
* <p>La PK incluye {@code boleta_tipo_personal} ({@code ADM} / {@code DOC}, ver
* {@link so.planilla_be.rentaquinta.calculo.TipoPlanilla}): un mismo documento puede tener una
* retención grabada por cada tipo de planilla en el mismo periodo.
*/
@Getter @Getter
@Setter @Setter
@Entity @Entity
@Table(name = "pl_renta_quinta") @Table(name = "pl_renta_quinta", uniqueConstraints = @UniqueConstraint(columnNames = {"codper", "anio", "mes"}))
@IdClass(RentaQuintaGrabadaId.class)
public class RentaQuintaGrabada { public class RentaQuintaGrabada {
@Id @Id
@Column(name = "numero_documento", length = 8) @GeneratedValue(strategy = GenerationType.IDENTITY)
private String numeroDocumento; @Column(name = "id_renta_quinta")
private Long idRentaQuinta;
@Id @Column(name = "codper", nullable = false)
@Column(name = "anio") private Integer codper;
@Column(name = "anio", nullable = false)
private Integer anio; private Integer anio;
@Id @Column(name = "mes", nullable = false)
@Column(name = "mes")
private Integer mes; private Integer mes;
/** char(3): {@code bpchar} para que {@code ddl-auto: validate} no falle. */ @Column(name = "retencion", nullable = false)
@Id
@Column(name = "boleta_tipo_personal", length = 3, columnDefinition = "bpchar")
private String boletaTipoPersonal;
@Column(name = "retencion_mes_actual", nullable = false)
private BigDecimal retencionMesActual; private BigDecimal retencionMesActual;
/**
* Parte de la PK, NOT NULL. Solo DOC: {@code 1} = docente costo por hora (DOC1), {@code 2} =
* monto fijo (DOC2); en ADM {@code ''} (vacío). char(1), ver
* {@link so.planilla_be.rentaquinta.calculo.TipoPago}.
*/
@Id
@Column(name = "tipo_pago", length = 1, columnDefinition = "bpchar")
private String tipoPago;
} }
package so.planilla_be.rentaquinta.entity;
import java.io.Serializable;
import java.util.Objects;
/**
* PK de planilla.pl_renta_quinta: {@code (numero_documento, anio, mes, boleta_tipo_personal,
* tipo_pago)}. {@code tipo_pago} es {@code ''} en ADM y {@code 1}/{@code 2} en DOC.
*/
public class RentaQuintaGrabadaId implements Serializable {
private String numeroDocumento;
private Integer anio;
private Integer mes;
private String boletaTipoPersonal;
private String tipoPago;
public RentaQuintaGrabadaId() {
}
public RentaQuintaGrabadaId(String numeroDocumento, Integer anio, Integer mes, String boletaTipoPersonal,
String tipoPago) {
this.numeroDocumento = numeroDocumento;
this.anio = anio;
this.mes = mes;
this.boletaTipoPersonal = boletaTipoPersonal;
this.tipoPago = tipoPago;
}
// --- GETTERS Y SETTERS OBLIGATORIOS PARA JPA ---
public String getNumeroDocumento() {
return numeroDocumento;
}
public void setNumeroDocumento(String numeroDocumento) {
this.numeroDocumento = numeroDocumento;
}
public Integer getAnio() {
return anio;
}
public void setAnio(Integer anio) {
this.anio = anio;
}
public Integer getMes() {
return mes;
}
public void setMes(Integer mes) {
this.mes = mes;
}
public String getBoletaTipoPersonal() {
return boletaTipoPersonal;
}
public void setBoletaTipoPersonal(String boletaTipoPersonal) {
this.boletaTipoPersonal = boletaTipoPersonal;
}
public String getTipoPago() {
return tipoPago;
}
public void setTipoPago(String tipoPago) {
this.tipoPago = tipoPago;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof RentaQuintaGrabadaId that)) {
return false;
}
return Objects.equals(numeroDocumento, that.numeroDocumento)
&& Objects.equals(anio, that.anio)
&& Objects.equals(mes, that.mes)
&& Objects.equals(boletaTipoPersonal, that.boletaTipoPersonal)
&& Objects.equals(tipoPago, that.tipoPago);
}
@Override
public int hashCode() {
return Objects.hash(numeroDocumento, anio, mes, boletaTipoPersonal, tipoPago);
}
}
...@@ -8,7 +8,6 @@ import org.springframework.web.bind.MissingServletRequestParameterException; ...@@ -8,7 +8,6 @@ import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
// import so.planilla_be.docentegratidiciembre.exception.DocenteGratiDiciembreCompletaNoEncontradaException;
import so.planilla_be.otrosempleadores.exception.RetencionOtrosEmpleadoresNoEncontradaException; import so.planilla_be.otrosempleadores.exception.RetencionOtrosEmpleadoresNoEncontradaException;
import so.planilla_be.otrosempleadores.exception.RetencionOtrosEmpleadoresYaExisteException; import so.planilla_be.otrosempleadores.exception.RetencionOtrosEmpleadoresYaExisteException;
import so.planilla_be.proyecciondocentehora.exception.ProyeccionDocenteHoraNoEncontradaException; import so.planilla_be.proyecciondocentehora.exception.ProyeccionDocenteHoraNoEncontradaException;
...@@ -35,10 +34,10 @@ public class GlobalExceptionHandler { ...@@ -35,10 +34,10 @@ public class GlobalExceptionHandler {
return build(HttpStatus.NOT_FOUND, ex.getMessage()); return build(HttpStatus.NOT_FOUND, ex.getMessage());
} }
// @ExceptionHandler(DocenteGratiDiciembreCompletaNoEncontradaException.class) @ExceptionHandler(IllegalArgumentException.class)
// public ResponseEntity<Map<String, Object>> handleNotFound(DocenteGratiDiciembreCompletaNoEncontradaException ex) { public ResponseEntity<Map<String, Object>> handleArgument(IllegalArgumentException ex) {
// return build(HttpStatus.NOT_FOUND, ex.getMessage()); return build(HttpStatus.BAD_REQUEST, ex.getMessage());
// } }
@ExceptionHandler(RetencionOtrosEmpleadoresYaExisteException.class) @ExceptionHandler(RetencionOtrosEmpleadoresYaExisteException.class)
public ResponseEntity<Map<String, Object>> handleConflict(RetencionOtrosEmpleadoresYaExisteException ex) { public ResponseEntity<Map<String, Object>> handleConflict(RetencionOtrosEmpleadoresYaExisteException ex) {
......
...@@ -43,6 +43,9 @@ public class RentaQuintaMapper { ...@@ -43,6 +43,9 @@ public class RentaQuintaMapper {
organizacion == null ? null : organizacion.subArea(), organizacion == null ? null : organizacion.subArea(),
organizacion == null ? null : organizacion.idCargo(), organizacion == null ? null : organizacion.idCargo(),
organizacion == null ? null : organizacion.cargo(), organizacion == null ? null : organizacion.cargo(),
organizacion == null ? null : organizacion.codlocal(),
organizacion == null ? null : organizacion.idSede(),
organizacion == null ? null : organizacion.sede(),
mesCorte, mesCorte,
orZero(historialReal), orZero(historialReal),
orZero(planillaCargada), orZero(planillaCargada),
...@@ -334,7 +337,7 @@ public class RentaQuintaMapper { ...@@ -334,7 +337,7 @@ public class RentaQuintaMapper {
e.getCodper(), e.getCodper(),
e.getCodlocal(), e.getCodlocal(),
e.getUnidadOrganizativa(), e.getUnidadOrganizativa(),
e.getTipoPlanilla(), e.getTipopla() == null ? null : e.getTipopla().toString(),
e.getApellidoPaterno(), e.getApellidoPaterno(),
e.getApellidoMaterno(), e.getApellidoMaterno(),
e.getNombres(), e.getNombres(),
......
package so.planilla_be.rentaquinta.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import so.planilla_be.rentaquinta.entity.Calendario;
import so.planilla_be.rentaquinta.entity.CalendarioId;
public interface CalendarioRepository extends JpaRepository<Calendario, CalendarioId> {
}
package so.planilla_be.rentaquinta.repository;
/**
* Clave de un trabajador que matchea los filtros de la grilla (paginación/orden). La fila
* completa se arma en Java (RentaQuintaServiceImpl), no en esta query.
*/
public interface DocumentoResumenProjection {
String getNumeroDocumento();
}
...@@ -2,25 +2,24 @@ package so.planilla_be.rentaquinta.repository; ...@@ -2,25 +2,24 @@ package so.planilla_be.rentaquinta.repository;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.Map; import java.util.Map;
/** Consulta de registros anuales que habilitan la gratificación DOC2 de diciembre completo. */
@Repository @Repository
public class GratificacionRepository { public class GratificacionRepository {
private final NamedParameterJdbcTemplate jdbc; private final NamedParameterJdbcTemplate jdbc;
public GratificacionRepository(NamedParameterJdbcTemplate jdbc) { this.jdbc = jdbc; }
public GratificacionRepository(NamedParameterJdbcTemplate jdbc) { /** La presencia en diciembre habilita la excepción DOC2, sin usar monto_basico. */
this.jdbc = jdbc; public boolean existeEnDiciembre(Integer codper, Integer anio) {
}
/** Solo diciembre del año consultado habilita la excepción; monto_basico no interviene. */
public boolean existeEnDiciembre(String numeroDocumento, Integer anio) {
return Boolean.TRUE.equals(jdbc.queryForObject(""" return Boolean.TRUE.equals(jdbc.queryForObject("""
select exists ( select exists (
select 1 from planilla.pl_gratificacion select 1 from planilla.pl_gratificacion g
where trim(numero_documento) = :numeroDocumento and anio = :anio and mes = 12 left join planilla.pl_planilla p on p.id_pl_planilla = g.id_pl_planilla
where (g.id_pl_planilla is not null and p.codper = :codper
and p.boleta_anio = :anio and p.boleta_mes = 12)
or (g.id_pl_planilla is null and g.codper = :codper
and g.anio = :anio and g.mes = 12)
) )
""", Map.of("numeroDocumento", numeroDocumento.trim(), "anio", anio), Boolean.class)); """, Map.of("codper", codper, "anio", anio), Boolean.class));
} }
} }
...@@ -2,43 +2,47 @@ package so.planilla_be.rentaquinta.repository; ...@@ -2,43 +2,47 @@ package so.planilla_be.rentaquinta.repository;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** Consulta FDW en lote: las unidades organizativas de pl_planilla vienen vacías. */
@Repository @Repository
public class OrganizacionPersonalRepository { public class OrganizacionPersonalRepository {
private final NamedParameterJdbcTemplate jdbc; private final NamedParameterJdbcTemplate jdbc;
public OrganizacionPersonalRepository(NamedParameterJdbcTemplate jdbc) { this.jdbc = jdbc; }
public OrganizacionPersonalRepository(NamedParameterJdbcTemplate jdbc) { public Map<Integer, Organizacion> buscar(List<Integer> codpers) {
this.jdbc = jdbc; if (codpers.isEmpty()) return Map.of();
} // Materializar los códigos evita enviar IN/ANY parametrizados a mysql_fdw
// cuando PostgreSQL reutiliza un plan genérico del prepared statement.
public Map<String, Organizacion> buscar(List<String> documentos) {
if (documentos.isEmpty()) return Map.of();
return jdbc.query(""" return jdbc.query("""
select distinct on (trim(p.numero_documento)) with seleccion as materialized (
trim(p.numero_documento) as documento, p.id_unidad_organizativa, select unnest(cast(array[:codpers] as integer[])) as codper
p.codarea, a.desarea, p.id_subarea, s.nombre, p.codcargo, c.descargo )
from nuevo.personal p select p.codper, p.numero_documento, p."apellidoPaterno", p."apellidoMaterno",
p.nombre as nombres, p.codlocal, p.id_tipo_pago, p.id_unidad_organizativa,
p.codarea, a.desarea, p.id_subarea, s.nombre as subarea, p.codcargo, c.descargo,
l.id_local, l.deslocal as sede
from seleccion x
inner join nuevo.personal p on p.codper = x.codper
left join nuevo.area a on a.codarea = p.codarea 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.subarea s on s.id_subarea = p.id_subarea
left join nuevo.cargo c on c.codcargo = p.codcargo left join nuevo.cargo c on c.codcargo = p.codcargo
where trim(p.numero_documento) in (:documentos) left join nuevo.locales l on trim(l.codlocal) = trim(p.codlocal)
order by trim(p.numero_documento), p.codper desc """, Map.of("codpers", codpers), (rs, row) -> new Organizacion(
""", Map.of("documentos", documentos.stream().map(String::trim).distinct().toList()), rs.getObject("codper", Integer.class), rs.getString("numero_documento"),
(rs, row) -> new Organizacion(rs.getString("documento"), rs.getString("apellidoPaterno"), rs.getString("apellidoMaterno"), rs.getString("nombres"),
rs.getObject("id_unidad_organizativa", Integer.class), rs.getString("codlocal"), rs.getObject("id_tipo_pago", Integer.class),
rs.getObject("codarea", Integer.class), rs.getString("desarea"), rs.getObject("id_unidad_organizativa", Integer.class), rs.getObject("codarea", Integer.class),
rs.getObject("id_subarea", Integer.class), rs.getString("nombre"), rs.getString("desarea"), rs.getObject("id_subarea", Integer.class), rs.getString("subarea"),
rs.getObject("codcargo", Integer.class), rs.getString("descargo"))) rs.getObject("codcargo", Integer.class), rs.getString("descargo"),
.stream().collect(Collectors.toMap(Organizacion::documento, Function.identity())); rs.getObject("id_local", Integer.class), rs.getString("sede")))
.stream().collect(Collectors.toMap(Organizacion::codper, Function.identity()));
} }
public record Organizacion(String documento, Integer idUnidadOrganizativa, public record Organizacion(Integer codper, String numeroDocumento, String apellidoPaterno,
Integer idArea, String area, Integer idSubArea, String subArea, String apellidoMaterno, String nombres, String codlocal, Integer idTipoPago,
Integer idCargo, String cargo) {} Integer idUnidadOrganizativa, Integer idArea, String area, Integer idSubArea,
String subArea, Integer idCargo, String cargo, Integer idSede, String sede) {}
} }
package so.planilla_be.rentaquinta.repository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.time.YearMonth;
import java.util.Optional;
@Repository
public class PeriodoAsistenciaRepository {
private final JdbcTemplate jdbcTemplate;
public PeriodoAsistenciaRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/** Las fechas de corte de asistencia no representan los días naturales del mes. */
public Optional<Integer> diasDelMes(int anio, int mes) {
return jdbcTemplate.query(
"select distinct anio, mes from nuevo.periodo_asistencia where anio = ? and mes = ?",
(rs, rowNum) -> YearMonth.of(rs.getInt("anio"), rs.getInt("mes")).lengthOfMonth(),
anio, mes).stream().findFirst();
}
}
package so.planilla_be.rentaquinta.repository;
public interface PersonalResumenProjection {
Integer getCodper();
}
\ No newline at end of file
package so.planilla_be.rentaquinta.repository; package so.planilla_be.rentaquinta.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import so.planilla_be.rentaquinta.entity.RentaQuintaGrabada;
import so.planilla_be.rentaquinta.entity.RentaQuintaGrabadaId;
import java.util.List;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import so.planilla_be.rentaquinta.entity.RentaQuintaGrabada;
import java.util.List;
import java.util.Optional;
public interface RentaQuintaGrabadaRepository extends JpaRepository<RentaQuintaGrabada, Long> {
Optional<RentaQuintaGrabada> findByCodperAndAnioAndMes(Integer codper, Integer anio, Integer mes);
List<RentaQuintaGrabada> findByCodperAndAnioOrderByMes(Integer codper, Integer anio);
public interface RentaQuintaGrabadaRepository extends JpaRepository<RentaQuintaGrabada, RentaQuintaGrabadaId> {
@Query(""" @Query("""
select r from RentaQuintaGrabada r select r from RentaQuintaGrabada r
where r.anio = :anio and r.numeroDocumento = :numeroDocumento where r.anio = :anio and r.mes = :mes and r.codper in :codpers
and trim(r.boletaTipoPersonal) = :tipoPersonal
and coalesce(trim(r.tipoPago), '') = :tipoPago
""") """)
List<RentaQuintaGrabada> retencionesAnuales( List<RentaQuintaGrabada> buscarMes(@Param("anio") Integer anio,
@Param("anio") Integer anio, @Param("mes") Integer mes, @Param("codpers") List<Integer> codpers);
@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")
List<RentaQuintaGrabada> findByAnioAndMesAndNumeroDocumentoIn(
@Param("anio") Integer anio,
@Param("mes") Integer mes,
@Param("numerosDocumento") List<String> numerosDocumento
);
} }
...@@ -2,112 +2,79 @@ package so.planilla_be.rentaquinta.repository; ...@@ -2,112 +2,79 @@ package so.planilla_be.rentaquinta.repository;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.*;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import so.planilla_be.rentaquinta.entity.RentaQuinta; import so.planilla_be.rentaquinta.entity.RentaQuinta;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long> { public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long> {
// Separar catálogos y filtros locales evita productos cartesianos y planes genéricos
/** // costosos en mysql_fdw; se conservan los INNER JOIN y la normalización de codlocal.
* Filtros opcionales de la grilla (catálogo + búsqueda por documento/apellidos), compartidos String CATALOGOS_MATERIALIZADOS = """
* entre la query de datos y su countQuery. with sedes as materialized (
*/ select id_local, codlocal from nuevo.locales
), personales as materialized (
select p.codper, p.codlocal, p.numero_documento,
p."apellidoPaterno", p."apellidoMaterno", p.nombre,
p.id_tipo_pago, p.codarea, p.id_subarea, p.codcargo
from nuevo.personal p
inner join nuevo.area a on a.codarea = p.codarea
inner join nuevo.subarea s on s.id_subarea = p.id_subarea
inner join nuevo.cargo c on c.codcargo = p.codcargo
)
""";
String CATALOGOS = """
from planilla.pl_planilla r
inner join personales p on p.codper = r.codper
inner join sedes l on trim(l.codlocal) = trim(p.codlocal)
""";
String FILTROS = """ String FILTROS = """
and (:sede is null or trim(r.codlocal) = :sede) and r.codper > 0
and (:tipoPlanilla is null or trim(r.boleta_tipo_personal) = :tipoPlanilla) and (:sede is null or cast(l.id_local as text) = :sede)
and (:tipoPago is null or coalesce(trim(r.tipo_pago), '') = :tipoPago) and (:tipoPlanilla is null
and ((:areaGeneral is null and :subArea is null and :cargo is null) or trim(r.numero_documento) in ( or (:tipoPlanilla = 'ADM' and r.tipopla in (5, 6))
select p.documento from ( or (:tipoPlanilla = 'DOC' and r.tipopla in (1, 3)))
select distinct on (trim(numero_documento)) trim(numero_documento) as documento, and (:tipoPago is null or cast(p.id_tipo_pago as text) = :tipoPago)
codarea, id_subarea, codcargo and (:areaGeneral is null or cast(p.codarea as text) = :areaGeneral)
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 (:subArea is null or cast(p.id_subarea as text) = :subArea)
and (:cargo is null or cast(p.codcargo as text) = :cargo) and (:cargo is null or cast(p.codcargo as text) = :cargo)
)) and (:texto is null or upper(p.numero_documento) like :texto
and (:texto is null or upper(concat(p."apellidoPaterno", ' ', p."apellidoMaterno", ' ', p.nombre)) like :texto)
or upper(r.numero_documento) 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). */ @Query(value = CATALOGOS_MATERIALIZADOS + """
List<RentaQuinta> findByCodperAndBoletaAnioOrderByBoletaMesAsc( select r.codper as codper
Integer codper, Integer boletaAnio); """ + CATALOGOS + """
where r.boleta_anio = :periodo and r.boleta_mes <= :mesCorte
/** Boleta de un trabajador en un mes puntual. */
Optional<RentaQuinta> findByCodperAndBoletaAnioAndBoletaMes(
Integer codper, Integer boletaAnio, Integer boletaMes);
/**
* 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
* sobre {@link #historialHastaMes}, no en SQL.
*/
@Query(value = """
select r.numero_documento as "numeroDocumento",
min(r.apellido_paterno) as "apellidoPaterno",
min(r.apellido_materno) as "apellidoMaterno",
min(r.nombres) as nombres
from planilla.pl_planilla r
where r.boleta_anio = :periodo
and r.boleta_mes <= :mesCorte
""" + FILTROS + """ """ + FILTROS + """
group by r.numero_documento group by r.codper
order by "apellidoPaterno" asc, "apellidoMaterno" asc, nombres asc, r.numero_documento asc order by min(p."apellidoPaterno"), min(p."apellidoMaterno"), min(p.nombre), r.codper
""", """, countQuery = CATALOGOS_MATERIALIZADOS + """
countQuery = """ select count(distinct r.codper)
select count(distinct r.numero_documento) """ + CATALOGOS + """
from planilla.pl_planilla r where r.boleta_anio = :periodo and r.boleta_mes <= :mesCorte
where r.boleta_anio = :periodo
and r.boleta_mes <= :mesCorte
""" + FILTROS, nativeQuery = true) """ + FILTROS, nativeQuery = true)
Page<DocumentoResumenProjection> buscarDocumentos( Page<PersonalResumenProjection> buscarPersonal(
@Param("periodo") Integer periodo, @Param("periodo") Integer periodo, @Param("mesCorte") Integer mesCorte,
@Param("mesCorte") Integer mesCorte, @Param("sede") String sede, @Param("tipoPlanilla") String tipoPlanilla,
@Param("sede") String sede, @Param("tipoPago") String tipoPago, @Param("areaGeneral") String areaGeneral,
@Param("tipoPlanilla") String tipoPlanilla, @Param("subArea") String subArea, @Param("cargo") String cargo,
@Param("tipoPago") String tipoPago, @Param("texto") String texto, Pageable pageable);
@Param("areaGeneral") String areaGeneral,
@Param("subArea") String subArea,
@Param("cargo") String cargo,
@Param("texto") String texto,
Pageable pageable);
/** Boletas reales de un conjunto de documentos hasta el mes de corte (agregación en Java). */
@Query(""" @Query("""
select r from RentaQuinta r select r from RentaQuinta r where r.codper in :codpers
where r.numeroDocumento in :documentos and r.boletaAnio = :periodo and r.boletaMes <= :mesCorte
and r.boletaAnio = :periodo order by r.codper, r.boletaMes, r.idRentaQuinta
and r.boletaMes <= :mesCorte
order by r.numeroDocumento asc, r.boletaMes asc
""") """)
List<RentaQuinta> historialHastaMes( List<RentaQuinta> historialHastaMes(@Param("codpers") List<Integer> codpers,
@Param("documentos") List<String> documentos, @Param("periodo") Integer periodo, @Param("mesCorte") Integer mesCorte);
@Param("periodo") Integer periodo,
@Param("mesCorte") Integer mesCorte);
/** Boletas completas de un trabajador en un año, ordenadas por mes (pantalla de historial). */
@Query(""" @Query("""
select r from RentaQuinta r select r from RentaQuinta r where r.codper = :codper and r.boletaAnio = :anio
where r.codper = :codper and r.boletaAnio = :anio order by r.boletaMes, r.idRentaQuinta
order by r.boletaMes asc
""") """)
List<RentaQuinta> historialAnual( List<RentaQuinta> historialAnual(@Param("codper") Integer codper, @Param("anio") Integer anio);
@Param("codper") Integer codper,
@Param("anio") Integer anio);
@Query(""" List<RentaQuinta> findByCodperAndBoletaAnioAndBoletaMesOrderByIdRentaQuintaAsc(
select r from RentaQuinta r Integer codper, Integer boletaAnio, Integer boletaMes);
where r.numeroDocumento = :numeroDocumento and r.boletaAnio = :anio
order by r.boletaMes asc
""")
List<RentaQuinta> historialAnualPorDocumento(
@Param("numeroDocumento") String numeroDocumento,
@Param("anio") Integer anio);
} }
...@@ -15,16 +15,18 @@ public interface RentaQuintaService { ...@@ -15,16 +15,18 @@ public interface RentaQuintaService {
Page<RentaQuintaResumenDTO> buscar(RentaQuintaFiltroDTO filtro); Page<RentaQuintaResumenDTO> buscar(RentaQuintaFiltroDTO filtro);
List<RentaQuintaResumenDTO> buscarCompleto(RentaQuintaFiltroDTO filtro);
RentaQuintaDetalleDTO obtenerPorId(Long id); RentaQuintaDetalleDTO obtenerPorId(Long id);
RentaQuintaDetalleCompletoDTO obtenerDetalleCompleto(String numeroDocumento, Integer anio, Integer mes, RentaQuintaDetalleCompletoDTO obtenerDetalleCompleto(Integer codper, Integer anio, Integer mes,
TipoPlanilla tipoPlanilla, TipoPago tipoPago); TipoPlanilla tipoPlanilla, TipoPago tipoPago);
RentaQuintaDetalleDTO obtenerBoleta(Integer codper, Integer anio, Integer mes); RentaQuintaDetalleDTO obtenerBoleta(Integer codper, Integer anio, Integer mes);
List<RentaQuintaHistorialDTO> historialAnual(Integer codper, Integer anio); List<RentaQuintaHistorialDTO> historialAnual(Integer codper, Integer anio);
void grabarRetencion(String numeroDocumento, Integer anio, Integer mes, TipoPlanilla tipoPlanilla, void grabarRetencion(Integer codper, Integer anio, Integer mes, TipoPlanilla tipoPlanilla,
TipoPago tipoPago); TipoPago tipoPago);
int grabarMasivo(RentaQuintaFiltroDTO filtro); int grabarMasivo(RentaQuintaFiltroDTO filtro);
......
...@@ -25,8 +25,9 @@ public class FiltrosServiceImpl implements FiltrosService { ...@@ -25,8 +25,9 @@ public class FiltrosServiceImpl implements FiltrosService {
@Override @Override
public List<PeriodoFiltroDTO> listarPeriodos() { public List<PeriodoFiltroDTO> listarPeriodos() {
return jdbcTemplate.query("select id_periodo, mes, anio from planilla.pl_periodo order by anio desc, mes desc", return jdbcTemplate.query("select distinct mes, anio from nuevo.periodo_asistencia order by anio desc, mes desc",
(rs, rowNum) -> new PeriodoFiltroDTO(rs.getInt("id_periodo"), rs.getInt("mes"), rs.getInt("anio"))); (rs, rowNum) -> new PeriodoFiltroDTO(rs.getInt("anio") * 100 + rs.getInt("mes"),
rs.getInt("mes"), rs.getInt("anio")));
} }
@Override @Override
......
...@@ -24,13 +24,12 @@ import so.planilla_be.rentaquinta.dto.RentaQuintaHistorialDTO; ...@@ -24,13 +24,12 @@ import so.planilla_be.rentaquinta.dto.RentaQuintaHistorialDTO;
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.entity.RentaQuintaGrabada; import so.planilla_be.rentaquinta.entity.RentaQuintaGrabada;
import so.planilla_be.rentaquinta.entity.RentaQuintaGrabadaId;
import so.planilla_be.rentaquinta.exception.RentaQuintaNoEncontradaException; import so.planilla_be.rentaquinta.exception.RentaQuintaNoEncontradaException;
import so.planilla_be.rentaquinta.mapper.RentaQuintaMapper; import so.planilla_be.rentaquinta.mapper.RentaQuintaMapper;
import so.planilla_be.rentaquinta.entity.Calendario;
import so.planilla_be.rentaquinta.entity.CalendarioId;
import so.planilla_be.rentaquinta.repository.CalendarioRepository; import so.planilla_be.rentaquinta.repository.PersonalResumenProjection;
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.repository.OrganizacionPersonalRepository;
...@@ -63,12 +62,9 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -63,12 +62,9 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
private static final int SIZE_DEFAULT = 20; private static final int SIZE_DEFAULT = 20;
private static final int SIZE_MAX = 100; private static final int SIZE_MAX = 100;
private static final int GRABADO_MASIVO_BATCH = 200; private static final int GRABADO_MASIVO_BATCH = 200;
/**
* tipo_pago de pl_renta_quinta en ADM: la columna es NOT NULL (parte de la PK). Un espacio y no private static final int TIPO_SUELDO = 1;
* {@code ""}, porque char(1) se lee con padding (' ') y Hibernate lo tomaría como cambio de PK. private static final int TIPO_GRATIFICACION = 2;
*/
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;
...@@ -78,7 +74,6 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -78,7 +74,6 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
private final UtilidadRepository utilidadRepository; private final UtilidadRepository utilidadRepository;
private final RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository; private final RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository;
private final ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository; private final ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository;
private final CalendarioRepository calendarioRepository;
private final GratificacionRepository gratificacionRepository; private final GratificacionRepository gratificacionRepository;
private final EntityManager entityManager; private final EntityManager entityManager;
private final OrganizacionPersonalRepository organizacionRepository; private final OrganizacionPersonalRepository organizacionRepository;
...@@ -90,7 +85,6 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -90,7 +85,6 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
UtilidadRepository utilidadRepository, UtilidadRepository utilidadRepository,
RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository, RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository,
ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository, ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository,
CalendarioRepository calendarioRepository,
GratificacionRepository gratificacionRepository, GratificacionRepository gratificacionRepository,
EntityManager entityManager, EntityManager entityManager,
OrganizacionPersonalRepository organizacionRepository) { OrganizacionPersonalRepository organizacionRepository) {
...@@ -102,7 +96,6 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -102,7 +96,6 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
this.utilidadRepository = utilidadRepository; this.utilidadRepository = utilidadRepository;
this.rentaQuintaGrabadaRepository = rentaQuintaGrabadaRepository; this.rentaQuintaGrabadaRepository = rentaQuintaGrabadaRepository;
this.proyeccionDocenteHoraRepository = proyeccionDocenteHoraRepository; this.proyeccionDocenteHoraRepository = proyeccionDocenteHoraRepository;
this.calendarioRepository = calendarioRepository;
this.gratificacionRepository = gratificacionRepository; this.gratificacionRepository = gratificacionRepository;
this.entityManager = entityManager; this.entityManager = entityManager;
this.organizacionRepository = organizacionRepository; this.organizacionRepository = organizacionRepository;
...@@ -129,6 +122,13 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -129,6 +122,13 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
return new PageImpl<>(filas.subList(desde, hasta), pageable, filas.size()); return new PageImpl<>(filas.subList(desde, hasta), pageable, filas.size());
} }
/** La tabla web requiere todas las filas: seleccionarlas y calcularlas una sola vez. */
@Override
public List<RentaQuintaResumenDTO> buscarCompleto(RentaQuintaFiltroDTO filtro) {
return buscarPagina(filtro, Pageable.unpaged()).getContent().stream()
.filter(r -> cumpleEstados(r, filtro)).toList();
}
private boolean cumpleEstados(RentaQuintaResumenDTO fila, RentaQuintaFiltroDTO filtro) { private boolean cumpleEstados(RentaQuintaResumenDTO fila, RentaQuintaFiltroDTO filtro) {
boolean variacion = orZero(fila.diferenciaRetencion()).abs().compareTo(new BigDecimal("0.001")) > 0; boolean variacion = orZero(fila.diferenciaRetencion()).abs().compareTo(new BigDecimal("0.001")) > 0;
boolean retencion = orZero(fila.retencionDelMes()).signum() > 0; boolean retencion = orZero(fila.retencionDelMes()).signum() > 0;
...@@ -137,34 +137,34 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -137,34 +137,34 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
} }
private Page<RentaQuintaResumenDTO> buscarPagina(RentaQuintaFiltroDTO filtro, Pageable pageable) { private Page<RentaQuintaResumenDTO> buscarPagina(RentaQuintaFiltroDTO filtro, Pageable pageable) {
Page<DocumentoResumenProjection> documentos = repository.buscarDocumentos( Page<PersonalResumenProjection> documentos = repository.buscarPersonal(
filtro.periodo(), filtro.periodo(),
filtro.mesCorte(), filtro.mesCorte(),
codigo(filtro.sede()), codigo(filtro.sede()),
codigo(filtro.tipoPlanilla()), // CONDICIÓN TIPO PLANILLA: filtra por boleta_tipo_personal codigo(filtro.tipoPlanilla()), // CONDICIÓN TIPO PLANILLA: filtra por boleta_tipo_personal
codigoTipoPago(filtro), // y por tipo_pago (ADM vacío, DOC 1/2) codigoTipoPago(filtro), // DOC1/DOC2: nuevo.personal.id_tipo_pago
codigo(filtro.areaGeneral()), codigo(filtro.areaGeneral()),
codigo(filtro.subArea()), codigo(filtro.subArea()),
codigo(filtro.cargo()), codigo(filtro.cargo()),
patron(filtro.documentoOApellidos()), patron(filtro.documentoOApellidos()),
pageable); pageable);
List<String> numeros = documentos.getContent().stream() List<Integer> numeros = documentos.getContent().stream()
.map(DocumentoResumenProjection::getNumeroDocumento) .map(PersonalResumenProjection::getCodper)
.toList(); .toList();
List<RentaQuinta> historialCompleto = numeros.isEmpty() ? List.of() List<RentaQuinta> historialCompleto = numeros.isEmpty() ? List.of()
: repository.historialHastaMes(numeros, filtro.periodo(), filtro.mesCorte()); : repository.historialHastaMes(numeros, filtro.periodo(), filtro.mesCorte());
aplicarBasicoYUtilidades(historialCompleto, filtro.periodo()); historialCompleto = seleccionarHistorial(historialCompleto, filtro.tipoPlanilla());
Map<String, List<RentaQuinta>> porDocumento = historialCompleto.stream() var organizaciones = aplicarBasicoYUtilidades(historialCompleto, filtro.periodo());
.collect(Collectors.groupingBy(RentaQuinta::getNumeroDocumento)); Map<Integer, List<RentaQuinta>> porCodper = historialCompleto.stream()
.collect(Collectors.groupingBy(RentaQuinta::getCodper));
// 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<Integer, 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 = porCodper.getOrDefault(d.getCodper(), 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,
...@@ -174,25 +174,19 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -174,25 +174,19 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
? a.planillaCargadaReal().add(calculo.ajusteSueldoBrutoMesCorte()) ? a.planillaCargadaReal().add(calculo.ajusteSueldoBrutoMesCorte())
: calculo.estimadoMesCorteSueldoBruto().add(calculo.estimadoMesCorteGratificacion())) : calculo.estimadoMesCorteSueldoBruto().add(calculo.estimadoMesCorteGratificacion()))
.add(calculo.otrosEmpleadoresImporteMesActual()); .add(calculo.otrosEmpleadoresImporteMesActual());
Integer codper = historial.stream() Integer codper = d.getCodper();
.map(RentaQuinta::getCodper)
.filter(Objects::nonNull)
.max(Integer::compareTo)
.orElse(null);
BigDecimal historialReal = a.historialReal().add(calculo.otrosEmpleadoresImporteAnterior()); BigDecimal historialReal = a.historialReal().add(calculo.otrosEmpleadoresImporteAnterior());
// Comparar con la misma modalidad del mes anterior (DOC1 y DOC2 tienen PK distintas). // Comparar la retención del mes anterior del mismo trabajador.
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(), tipoPagoAnterior), BigDecimal.ZERO); codper, 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())); organizaciones.get(d.getCodper()));
}); });
} }
...@@ -206,13 +200,10 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -206,13 +200,10 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
return tipoPlanilla == null ? null : tipoPlanilla.getCodigo(); return tipoPlanilla == null ? null : tipoPlanilla.getCodigo();
} }
/** /** DOC1/DOC2 se filtran por id_tipo_pago; ADM no aplica ese filtro. */
* CONDICIÓN TIPO PAGO — tipo_pago es char(1): el repository lo compara con trim() contra este
* código. ADM exige tipo_pago vacío; DOC1 / DOC2 exigen 1 / 2; sin tipoPlanilla no filtra.
*/
private String codigoTipoPago(RentaQuintaFiltroDTO filtro) { private String codigoTipoPago(RentaQuintaFiltroDTO filtro) {
if (filtro.tipoPlanilla() == TipoPlanilla.MENSUAL_MONTO_FIJO) { if (filtro.tipoPlanilla() == TipoPlanilla.MENSUAL_MONTO_FIJO) {
return ""; return null;
} }
return filtro.tipoPago() == null ? null : filtro.tipoPago().getCodigo(); return filtro.tipoPago() == null ? null : filtro.tipoPago().getCodigo();
} }
...@@ -249,9 +240,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -249,9 +240,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
@Override @Override
public RentaQuintaDetalleDTO obtenerBoleta(Integer codper, Integer anio, Integer mes) { public RentaQuintaDetalleDTO obtenerBoleta(Integer codper, Integer anio, Integer mes) {
RentaQuinta boleta = repository.findByCodperAndBoletaAnioAndBoletaMes(codper, anio, mes) RentaQuinta boleta = boletaUnica(repository.findByCodperAndBoletaAnioAndBoletaMesOrderByIdRentaQuintaAsc(codper, anio, mes), codper, anio, mes);
.orElseThrow(() -> new RentaQuintaNoEncontradaException(
"No existe boleta para codper %d, periodo %d-%02d".formatted(codper, anio, mes)));
aplicarBasicoYUtilidades(List.of(boleta), anio); aplicarBasicoYUtilidades(List.of(boleta), anio);
return mapper.toDetalle(boleta); return mapper.toDetalle(boleta);
} }
...@@ -259,20 +248,21 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -259,20 +248,21 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
/** Consulta el cálculo y la matriz; la escritura se realiza desde grabarRetencion. */ /** Consulta el cálculo y la matriz; la escritura se realiza desde grabarRetencion. */
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public RentaQuintaDetalleCompletoDTO obtenerDetalleCompleto(String numeroDocumento, Integer anio, Integer mes, public RentaQuintaDetalleCompletoDTO obtenerDetalleCompleto(Integer codper, Integer anio, Integer mes,
TipoPlanilla tipoPlanillaSolicitado, TipoPlanilla tipoPlanillaSolicitado,
TipoPago tipoPago) { TipoPago tipoPago) {
List<RentaQuinta> historialAnio = repository.historialAnualPorDocumento(numeroDocumento, anio); List<RentaQuinta> historialAnio = repository.historialAnual(codper, anio);
historialAnio = seleccionarHistorial(historialAnio, tipoPlanillaSolicitado);
aplicarBasicoYUtilidades(historialAnio, anio); aplicarBasicoYUtilidades(historialAnio, anio);
int mesCorte = mes; int mesCorte = mes;
Agregado a = agregar(historialAnio, mesCorte); Agregado a = agregar(historialAnio, mesCorte);
if (a.referencia() == null) { if (a.referencia() == null) {
throw new RentaQuintaNoEncontradaException( throw new RentaQuintaNoEncontradaException(
"No existe boleta para documento %s, periodo %d-%02d".formatted(numeroDocumento, anio, mes)); "No existe boleta para codper %d, periodo %d-%02d".formatted(codper, anio, mes));
} }
TipoPlanilla tipoPlanilla = tipoPlanilla(tipoPlanillaSolicitado, a.referencia()); TipoPlanilla tipoPlanilla = tipoPlanilla(tipoPlanillaSolicitado, a.referencia());
DatosExternos externos = cargarDatosExternos(List.of(numeroDocumento), anio); DatosExternos externos = cargarDatosExternos(List.of(codper), anio);
DatosCalculo datos = datosCalculo(a, historialAnio, anio, mesCorte, tipoPlanilla, tipoPago, externos); DatosCalculo datos = datosCalculo(a, historialAnio, anio, mesCorte, tipoPlanilla, tipoPago, externos);
CalculoRentaQuinta calculo = calculadora.calcular(datos); CalculoRentaQuinta calculo = calculadora.calcular(datos);
return mapper.toDetalleCompleto(a.referencia(), historialAnio, mesCorte, calculo, return mapper.toDetalleCompleto(a.referencia(), historialAnio, mesCorte, calculo,
...@@ -282,17 +272,16 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -282,17 +272,16 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
private List<RentaQuintaDetalleCompletoDTO.MesIngresosDTO> matrizMensual( private List<RentaQuintaDetalleCompletoDTO.MesIngresosDTO> matrizMensual(
Agregado a, List<RentaQuinta> historial, int mesCorte, TipoPlanilla tipoPlanilla, TipoPago tipoPago, Agregado a, List<RentaQuinta> historial, int mesCorte, TipoPlanilla tipoPlanilla, TipoPago tipoPago,
DatosExternos externos, DatosCalculo datos, CalculoRentaQuinta calculo) { DatosExternos externos, DatosCalculo datos, CalculoRentaQuinta calculo) {
String documento = a.referencia().getNumeroDocumento(); Integer codper = a.referencia().getCodper();
int mesReferencia = a.referencia().getBoletaMes(); int mesReferencia = a.referencia().getBoletaMes();
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;
String tipoPagoRetencion = tipoPagoRetencion(tipoPlanilla, tipoPago, modalidad);
Map<Integer, BigDecimal> retencionesGrabadas = rentaQuintaGrabadaRepository Map<Integer, BigDecimal> retencionesGrabadas = rentaQuintaGrabadaRepository
.retencionesAnuales(a.referencia().getBoletaAnio(), documento, tipoPlanilla.getCodigo(), tipoPagoRetencion) .findByCodperAndAnioOrderByMes(codper, a.referencia().getBoletaAnio())
.stream().filter(r -> r.getRetencionMesActual() != null) .stream().filter(r -> r.getRetencionMesActual() != null)
.collect(Collectors.toMap(RentaQuintaGrabada::getMes, RentaQuintaGrabada::getRetencionMesActual)); .collect(Collectors.toMap(RentaQuintaGrabada::getMes, RentaQuintaGrabada::getRetencionMesActual));
List<ProyeccionDocenteHora> docente = externos.proyeccionDocenteHora().getOrDefault(documento, List.of()); List<ProyeccionDocenteHora> docente = externos.proyeccionDocenteHora().getOrDefault(codper, List.of());
List<RetencionOtrosEmpleadores> otros = externos.otrosEmpleadores().getOrDefault(documento, List.of()); List<RetencionOtrosEmpleadores> otros = externos.otrosEmpleadores().getOrDefault(codper, List.of());
BigDecimal sueldo = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getSueldoBruto); BigDecimal sueldo = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getSueldoBruto);
BigDecimal familiar = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getAsignacionFamiliar); BigDecimal familiar = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getAsignacionFamiliar);
BigDecimal movilidad = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getMovilidad); BigDecimal movilidad = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getMovilidad);
...@@ -310,7 +299,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -310,7 +299,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
BigDecimal factor = mes == 12 && modalidad == ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL BigDecimal factor = mes == 12 && modalidad == ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL
? BigDecimal.valueOf(16).divide(BigDecimal.valueOf(30), 10, RoundingMode.HALF_UP) : BigDecimal.ONE; ? BigDecimal.valueOf(16).divide(BigDecimal.valueOf(30), 10, RoundingMode.HALF_UP) : BigDecimal.ONE;
BigDecimal sueldoMes = mes == mesCorte ? calculo.estimadoMesCorteSueldoBruto() BigDecimal sueldoMes = mes == mesCorte ? calculo.estimadoMesCorteSueldoBruto()
: porHora ? sumaProyeccion(docente, tipoPlanilla.getCodigo(), m -> m == mesActual, ProyeccionDocenteHora::getRemunBrutaFutura) : porHora ? sumaProyeccion(docente, TIPO_SUELDO, m -> m == mesActual, ProyeccionDocenteHora::getRemunBrutaFutura)
: sueldo.multiply(factor).setScale(2, RoundingMode.HALF_UP); : sueldo.multiply(factor).setScale(2, RoundingMode.HALF_UP);
if (mes > mesCorte) { if (mes > mesCorte) {
if (mes == 12) sueldoMes = saldoSueldo; if (mes == 12) sueldoMes = saldoSueldo;
...@@ -344,93 +333,43 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -344,93 +333,43 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
return matriz; return matriz;
} }
private String tipoPagoRetencion(TipoPlanilla tipoPlanilla, TipoPago tipoPago, ModalidadCalculo modalidad) { /** Guarda el cálculo por trabajador y periodo, aunque el mes de corte sea estimado. */
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 -> "";
};
}
/**
* Graba (upsert) en planilla.pl_renta_quinta la retención del mes calculada para el
* trabajador: documento + año + mes + tipo de planilla + retención, no una boleta completa
* (esa vive en pl_planilla). Reutiliza el mismo cálculo que {@link #obtenerDetalleCompleto}.
* El tipo grabado es siempre el enviado por el cliente: nunca se toma de la boleta.
*/
@Override @Override
@Transactional @Transactional
public void grabarRetencion(String numeroDocumento, Integer anio, Integer mes, TipoPlanilla tipoPlanilla, public void grabarRetencion(Integer codper, Integer anio, Integer mes, TipoPlanilla tipoPlanilla,
TipoPago tipoPago) { TipoPago tipoPago) {
exigirTipoPlanilla(tipoPlanilla); exigirTipoPlanilla(tipoPlanilla);
RentaQuintaDetalleCompletoDTO detalle = obtenerDetalleCompleto(numeroDocumento, anio, mes, tipoPlanilla, RentaQuintaDetalleCompletoDTO detalle = obtenerDetalleCompleto(codper, anio, mes, tipoPlanilla, tipoPago);
tipoPago); upsertRetencion(codper, anio, mes, detalle.controlRetenciones().retencionMesActual());
upsertRetencion(numeroDocumento, anio, mes, tipoPlanilla, tipoPago,
detalle.controlRetenciones().retencionMesActual());
} }
/** El grabado en pl_renta_quinta requiere el tipo enviado (ADM / DOC); no hay valor por defecto. */
private void exigirTipoPlanilla(TipoPlanilla tipoPlanilla) { private void exigirTipoPlanilla(TipoPlanilla tipoPlanilla) {
if (tipoPlanilla == null) { if (tipoPlanilla == null) {
throw new TipoPlanillaInvalidaException("tipoPlanilla es obligatorio para grabar (ADM / DOC1 / DOC2)"); throw new TipoPlanillaInvalidaException("tipoPlanilla es obligatorio para grabar (ADM / DOC1 / DOC2)");
} }
} }
/** private RentaQuinta boletaUnica(List<RentaQuinta> boletas, Integer codper, Integer anio, Integer mes) {
* {@code tipo_pago} se graba tal como lo envió el cliente (DOC1 → 1, DOC2 → 2); en ADM va if (boletas.isEmpty()) {
* vacío: la columna es parte de la PK y NOT NULL, no admite null. throw new RentaQuintaNoEncontradaException(
*/ "No existe boleta para codper %d, periodo %d-%02d".formatted(codper, anio, mes));
private void upsertRetencion(String numeroDocumento, Integer anio, Integer mes, TipoPlanilla tipoPlanilla,
TipoPago tipoPago, BigDecimal retencionMesActual) {
String tipoPagoGrabado = tipoPlanilla.esDocente() && tipoPago != null ? tipoPago.getCodigo() : TIPO_PAGO_VACIO;
RentaQuintaGrabadaId id = new RentaQuintaGrabadaId(numeroDocumento, anio, mes, tipoPlanilla.getCodigo(),
tipoPagoGrabado);
RentaQuintaGrabada grabada = rentaQuintaGrabadaRepository.findById(id).orElseGet(RentaQuintaGrabada::new);
grabada.setNumeroDocumento(numeroDocumento);
grabada.setAnio(anio);
grabada.setMes(mes);
grabada.setBoletaTipoPersonal(tipoPlanilla.getCodigo());
grabada.setTipoPago(tipoPagoGrabado);
grabada.setRetencionMesActual(retencionMesActual);
rentaQuintaGrabadaRepository.save(grabada);
} }
if (boletas.size() != 1) {
/** throw new IllegalArgumentException(
* Variante del grabado masivo: los registros existentes ya vienen precargados en "Existe más de una boleta para codper %d, periodo %d-%02d; consultar la boleta por ID"
* {@code existentes} (clave {@link #claveGrabada(RentaQuintaGrabada)}), así que no hay un .formatted(codper, anio, mes));
* 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);
} }
return boletas.getFirst();
/** 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) { private void upsertRetencion(Integer codper, Integer anio, Integer mes, BigDecimal retencion) {
return claveGrabada(numeroDocumento, boletaTipoPersonal) + "|" + (tipoPago == null ? "" : tipoPago.trim()); RentaQuintaGrabada grabada = rentaQuintaGrabadaRepository
.findByCodperAndAnioAndMes(codper, anio, mes).orElseGet(RentaQuintaGrabada::new);
grabada.setCodper(codper);
grabada.setAnio(anio);
grabada.setMes(mes);
grabada.setRetencionMesActual(retencion);
rentaQuintaGrabadaRepository.save(grabada);
} }
/** /**
...@@ -447,23 +386,23 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -447,23 +386,23 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
} }
private int grabarSegunFiltro(RentaQuintaFiltroDTO filtro) { private int grabarSegunFiltro(RentaQuintaFiltroDTO filtro) {
List<String> numeros = new ArrayList<>(); List<Integer> numeros = new ArrayList<>();
int page = 0; int page = 0;
Page<?> pagina; Page<?> pagina;
do { do {
if (filtro.tieneVariacion() != null || filtro.tieneRetencion() != null) { if (filtro.tieneVariacion() != null || filtro.tieneRetencion() != null) {
Page<RentaQuintaResumenDTO> calculados = buscarPagina(filtro, PageRequest.of(page, GRABADO_MASIVO_BATCH)); Page<RentaQuintaResumenDTO> calculados = buscarPagina(filtro, PageRequest.of(page, GRABADO_MASIVO_BATCH));
calculados.getContent().stream().filter(r -> cumpleEstados(r, filtro)) calculados.getContent().stream().filter(r -> cumpleEstados(r, filtro))
.map(RentaQuintaResumenDTO::numeroDocumento).forEach(numeros::add); .map(RentaQuintaResumenDTO::codper).forEach(numeros::add);
pagina = calculados; pagina = calculados;
} else { } else {
Page<DocumentoResumenProjection> documentos = repository.buscarDocumentos( Page<PersonalResumenProjection> documentos = repository.buscarPersonal(
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(documentos.getContent().stream().map(DocumentoResumenProjection::getNumeroDocumento).toList()); numeros.addAll(documentos.getContent().stream().map(PersonalResumenProjection::getCodper).toList());
pagina = documentos; pagina = documentos;
} }
page++; page++;
...@@ -473,17 +412,16 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -473,17 +412,16 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
return 0; return 0;
} }
List<RentaQuinta> historialCompleto = repository.historialHastaMes(numeros, filtro.periodo(), filtro.mesCorte()); List<RentaQuinta> historialCompleto = repository.historialHastaMes(numeros, filtro.periodo(), filtro.mesCorte());
historialCompleto = seleccionarHistorial(historialCompleto, filtro.tipoPlanilla());
aplicarBasicoYUtilidades(historialCompleto, filtro.periodo()); aplicarBasicoYUtilidades(historialCompleto, filtro.periodo());
Map<String, List<RentaQuinta>> porDocumento = historialCompleto.stream() Map<Integer, List<RentaQuinta>> porCodper = historialCompleto.stream()
.collect(Collectors.groupingBy(RentaQuinta::getNumeroDocumento)); .collect(Collectors.groupingBy(RentaQuinta::getCodper));
DatosExternos externos = cargarDatosExternos(numeros, filtro.periodo()); 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 (Integer codper : numeros) {
List<RentaQuinta> historial = porDocumento.getOrDefault(numeroDocumento, List.of()); List<RentaQuinta> historial = porCodper.getOrDefault(codper, List.of());
Agregado a = agregar(historial, filtro.mesCorte()); Agregado a = agregar(historial, filtro.mesCorte());
if (a.referencia() == null) { if (a.referencia() == null) {
continue; continue;
...@@ -491,8 +429,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -491,8 +429,7 @@ 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(), externos); filtro.tipoPago(), externos);
upsertRetencion(existentes, numeroDocumento, filtro.periodo(), filtro.mesCorte(), tipoPlanilla, upsertRetencion(codper, filtro.periodo(), filtro.mesCorte(), calculo.retencionDelMes());
filtro.tipoPago(), calculo.retencionDelMes());
grabados++; grabados++;
} }
return grabados; return grabados;
...@@ -537,28 +474,28 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -537,28 +474,28 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
private DatosCalculo datosCalculo(Agregado a, List<RentaQuinta> historial, Integer periodo, int mesCorte, private DatosCalculo datosCalculo(Agregado a, List<RentaQuinta> historial, Integer periodo, int mesCorte,
TipoPlanilla tipoPlanilla, TipoPago tipoPago, DatosExternos externos) { 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 boletas del codper en el mes de referencia (puede haber varios
// codper/tipo de planilla). // tipos de planilla).
BigDecimal sueldoBrutoReferencia = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getSueldoBruto); BigDecimal sueldoBrutoReferencia = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getSueldoBruto);
BigDecimal basicoReferencia = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getBasicoGratificacion); BigDecimal basicoReferencia = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getBasicoGratificacion);
BigDecimal asignacionFamiliarReferencia = BigDecimal asignacionFamiliarReferencia =
sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getAsignacionFamiliar); sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getAsignacionFamiliar);
String numeroDocumento = a.referencia().getNumeroDocumento(); Integer codper = a.referencia().getCodper();
OtrosEmpleadoresAgregado otrosEmpleadores = otrosEmpleadores( OtrosEmpleadoresAgregado otrosEmpleadores = otrosEmpleadores(
externos.otrosEmpleadores().getOrDefault(numeroDocumento, List.of()), mesCorte); externos.otrosEmpleadores().getOrDefault(codper, List.of()), mesCorte);
List<ProyeccionDocenteHora> proyeccion = externos.proyeccionDocenteHora().getOrDefault(numeroDocumento, List.of()); List<ProyeccionDocenteHora> proyeccion = externos.proyeccionDocenteHora().getOrDefault(codper, 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
? sumaProyeccion(proyeccion, tipoPersonal, m -> m == mesCorte, ProyeccionDocenteHora::getRemunBrutaFutura) ? sumaProyeccion(proyeccion, TIPO_SUELDO, m -> m == mesCorte, ProyeccionDocenteHora::getRemunBrutaFutura)
: BigDecimal.ZERO; : BigDecimal.ZERO;
BigDecimal proyeccionDocenteHora = porHora BigDecimal proyeccionDocenteHora = porHora
? sumaProyeccion(proyeccion, tipoPersonal, m -> m > mesCorte, ProyeccionDocenteHora::getRemunBrutaFutura) ? sumaProyeccion(proyeccion, TIPO_SUELDO, m -> m > mesCorte, ProyeccionDocenteHora::getRemunBrutaFutura)
: BigDecimal.ZERO; : BigDecimal.ZERO;
BigDecimal gratificacionJulioDocenteHora = porHora BigDecimal gratificacionJulioDocenteHora = porHora
? sumaProyeccion(proyeccion, TIPO_GRATIFICACION, m -> m == 7, ProyeccionDocenteHora::getGratificacion) ? sumaProyeccion(proyeccion, TIPO_GRATIFICACION, m -> m == 7, ProyeccionDocenteHora::getGratificacion)
...@@ -582,10 +519,10 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -582,10 +519,10 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
.findFirst() .findFirst()
.orElse(null) .orElse(null)
: null; : null;
// DOC2: un registro del documento en pl_gratificacion para diciembre del año habilita // DOC2: una gratificación vinculada a la boleta de diciembre habilita la excepción.
// diciembre completo (30 días). monto_basico no modifica esta regla. // diciembre completo (30 días). monto_basico no modifica esta regla.
boolean gratificacionDiciembreCompleta = modalidad == ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL boolean gratificacionDiciembreCompleta = modalidad == ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL
&& gratificacionRepository.existeEnDiciembre(numeroDocumento, periodo); && gratificacionRepository.existeEnDiciembre(codper, periodo);
return new DatosCalculo( return new DatosCalculo(
periodo, modalidad, proyeccionDocenteHora, sueldoBrutoMesCorteDocenteHora, sueldoBrutoRealMesCorte, periodo, modalidad, proyeccionDocenteHora, sueldoBrutoMesCorteDocenteHora, sueldoBrutoRealMesCorte,
...@@ -601,39 +538,30 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -601,39 +538,30 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
gratificacionDiciembreCompleta); gratificacionDiciembreCompleta);
} }
/** /** Los días naturales no dependen del catálogo de asistencia ni de sus fechas de corte. */
* Días del mes de la fecha según planilla.pl_calendario; si ese año aún no está cargado en el
* calendario, los del mes real (java.time).
*/
private int diasDelMes(LocalDate fecha) { private int diasDelMes(LocalDate fecha) {
if (fecha == null) { if (fecha == null) {
return 0; return 0;
} }
return calendarioRepository.findById(new CalendarioId(fecha.getYear(), fecha.getMonthValue())) return YearMonth.from(fecha).lengthOfMonth();
.map(Calendario::getDias)
.orElseGet(() -> YearMonth.from(fecha).lengthOfMonth());
} }
/** /** La modalidad docente proviene exclusivamente de nuevo.personal.id_tipo_pago. */
* CONDICIÓN MODALIDAD — ADM siempre es monto fijo. En DOC decide el {@code tipoPago} enviado private ModalidadCalculo modalidad(TipoPlanilla tipoPlanilla, TipoPago solicitado,
* por el cliente; si no lo envió, el {@code tipo_pago} de las boletas DOC del mes de List<RentaQuinta> historial, int mesReferencia) {
* referencia (una persona puede tener filas ADM y DOC en el mismo mes): si alguna es 1 es if (!tipoPlanilla.esDocente()) return ModalidadCalculo.MONTO_FIJO;
* docente por hora; si alguna es 2, monto fijo con diciembre proporcional; si no, monto fijo. TipoPago personal = historial.stream()
*/
private ModalidadCalculo modalidad(TipoPlanilla tipoPlanilla, TipoPago solicitado, List<RentaQuinta> historial,
int mesReferencia) {
if (solicitado != null) {
return ModalidadCalculo.de(tipoPlanilla, solicitado);
}
List<TipoPago> tiposPago = historial.stream()
.filter(esMesCorte(mesReferencia)) .filter(esMesCorte(mesReferencia))
.filter(r -> TipoPlanilla.deBoleta(r) == TipoPlanilla.DOCENTE) .filter(r -> TipoPlanilla.deBoleta(r) == TipoPlanilla.DOCENTE)
.map(r -> TipoPago.desdeCodigo(r.getTipoPago())) .map(r -> TipoPago.desdeCodigo(r.getTipoPago()))
.filter(Objects::nonNull) .filter(Objects::nonNull).findFirst().orElse(null);
.toList(); if (personal == null) {
TipoPago deBoleta = tiposPago.contains(TipoPago.POR_HORA) ? TipoPago.POR_HORA throw new TipoPlanillaInvalidaException("El docente requiere id_tipo_pago 1 o 2 en nuevo.personal");
: tiposPago.contains(TipoPago.MONTO_FIJO) ? TipoPago.MONTO_FIJO : null; }
return ModalidadCalculo.de(tipoPlanilla, deBoleta); if (solicitado != null && solicitado != personal) {
throw new TipoPlanillaInvalidaException("tipoPlanilla no coincide con id_tipo_pago de nuevo.personal");
}
return ModalidadCalculo.de(tipoPlanilla, personal);
} }
/** /**
...@@ -669,30 +597,30 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -669,30 +597,30 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
} }
/** /**
* Datos por documento que el cálculo necesita fuera de pl_planilla, precargados con una * Datos por codper 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). * consulta por tabla para todos los trabajadores (en vez de varias consultas por trabajador).
*/ */
private DatosExternos cargarDatosExternos(List<String> documentos, Integer anio) { private DatosExternos cargarDatosExternos(List<Integer> codpers, Integer anio) {
if (documentos.isEmpty()) { if (codpers.isEmpty()) {
return new DatosExternos(Map.of(), Map.of()); return new DatosExternos(Map.of(), Map.of());
} }
return new DatosExternos( return new DatosExternos(
otrosEmpleadoresRepository.findByAnioAndDocumentoTrabajadorIn(anio, documentos).stream() otrosEmpleadoresRepository.findByAnioAndCodperIn(anio, codpers).stream()
.collect(Collectors.groupingBy(RetencionOtrosEmpleadores::getDocumentoTrabajador)), .collect(Collectors.groupingBy(RetencionOtrosEmpleadores::getCodper)),
proyeccionDocenteHoraRepository.findByProyeccionAnioAndNumeroDocumentoIn(anio, documentos).stream() proyeccionDocenteHoraRepository.findByAnioAndCodperIn(anio, codpers).stream()
.collect(Collectors.groupingBy(ProyeccionDocenteHora::getNumeroDocumento))); .collect(Collectors.groupingBy(ProyeccionDocenteHora::getCodper)));
} }
/** /**
* Equivale a las sumas {@code coalesce(sum(...), 0)} de ProyeccionDocenteHoraRepository: * 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. * filas del tipo indicado (1 sueldo, 2 gratificación) cuyo mes cumple {@code mes}; nulos no suman.
*/ */
private BigDecimal sumaProyeccion(List<ProyeccionDocenteHora> filas, String tipoPersonal, private BigDecimal sumaProyeccion(List<ProyeccionDocenteHora> filas, Integer tipoProyeccion,
IntPredicate mes, IntPredicate mes,
Function<ProyeccionDocenteHora, BigDecimal> campo) { Function<ProyeccionDocenteHora, BigDecimal> campo) {
return filas.stream() return filas.stream()
.filter(p -> p.getBoletaTipoPersonal() != null && p.getBoletaTipoPersonal().trim().equals(tipoPersonal)) .filter(p -> Objects.equals(p.getTipoProyeccion(), tipoProyeccion))
.filter(p -> p.getProyeccionMes() != null && mes.test(p.getProyeccionMes())) .filter(p -> p.getMes() != null && mes.test(p.getMes()))
.map(campo) .map(campo)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add); .reduce(BigDecimal.ZERO, BigDecimal::add);
...@@ -709,30 +637,40 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -709,30 +637,40 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
* filtrar por mes); si existe, se usa solo ese básico; si no, básico + asignación familiar de * filtrar por mes); si existe, se usa solo ese básico; si no, básico + asignación familiar de
* pl_planilla de ese mes. * pl_planilla de ese mes.
*/ */
private void aplicarBasicoYUtilidades(List<RentaQuinta> historial, Integer anio) { private Map<Integer, OrganizacionPersonalRepository.Organizacion> aplicarBasicoYUtilidades(
if (historial.isEmpty()) { List<RentaQuinta> historial, Integer anio) {
return; if (historial.isEmpty()) return Map.of();
} List<Integer> codpers = historial.stream().map(RentaQuinta::getCodper)
List<String> documentos = historial.stream() .filter(Objects::nonNull).distinct().toList();
.map(RentaQuinta::getNumeroDocumento) var personales = organizacionRepository.buscar(codpers);
.distinct() Map<Integer, BigDecimal> basicos = codpers.isEmpty() ? Map.of() : rentaBasicoRepository
.toList(); .findByAnioAndCodperInOrderByMesDesc(anio, codpers).stream()
Map<String, BigDecimal> basicoPorDocumento = rentaBasicoRepository .collect(Collectors.toMap(RentaBasico::getCodper, RentaBasico::getMontoBasico, (a, b) -> a));
.findByAnioAndNumeroDocumentoIn(anio, documentos).stream() Map<Long, BigDecimal> utilidades = utilidadRepository
.collect(Collectors.toMap(RentaBasico::getNumeroDocumento, RentaBasico::getMontoBasico, (a, b) -> a)); .buscarPorPlanillas(historial.stream().map(RentaQuinta::getIdRentaQuinta).toList()).stream()
Map<String, Map<Integer, BigDecimal>> utilidadPorDocumento = utilidadRepository .collect(Collectors.toMap(u -> u.getPlanilla().getIdRentaQuinta(),
.findByAnioAndNumeroDocumentoIn(anio, documentos).stream() Utilidad::getMontoUtilidad, BigDecimal::add));
.collect(Collectors.groupingBy(Utilidad::getNumeroDocumento,
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, if (entityManager.contains(r)) entityManager.detach(r);
// incluso cuando la selección de estados se ejecuta dentro del grabado masivo. var p = r.getCodper() == null ? null : personales.get(r.getCodper());
if (entityManager.contains(r)) { if (p != null) {
entityManager.detach(r); r.setNumeroDocumento(p.numeroDocumento());
} r.setApellidoPaterno(p.apellidoPaterno());
r.setBasicoGratificacion(baseGratificacion(r, basicoPorDocumento.get(r.getNumeroDocumento()))); r.setApellidoMaterno(p.apellidoMaterno());
r.setUtilidades(orZero(valorDelMes(utilidadPorDocumento, r.getNumeroDocumento(), r.getBoletaMes()))); r.setNombres(p.nombres());
} if (r.getCodlocal() == null) r.setCodlocal(p.codlocal());
if (r.getUnidadOrganizativa() == null) r.setUnidadOrganizativa(p.idUnidadOrganizativa());
r.setTipoPago(p.idTipoPago() == null ? null : p.idTipoPago().toString());
r.setAreaGeneral(p.area());
r.setSubArea(p.subArea());
r.setCargo(p.cargo());
r.setSede(p.sede());
}
r.setBasicoGratificacion(baseGratificacion(r,
r.getCodper() == null ? null : basicos.get(r.getCodper())));
r.setUtilidades(orZero(utilidades.get(r.getIdRentaQuinta())));
}
return personales;
} }
/** /**
...@@ -746,31 +684,22 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -746,31 +684,22 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
} }
/** /**
* Retenciones del mes anterior con clave documento + tipo de personal + tipo de pago. * Retenciones del mes anterior por trabajador, independientes de las boletas.
*/ */
private Map<String, BigDecimal> obtenerRetencionesMesAnterior(List<String> documentos, Integer anio, int mesCorte) { private Map<Integer, BigDecimal> obtenerRetencionesMesAnterior(List<Integer> codpers, Integer anio, int mesCorte) {
if (mesCorte <= 1 || documentos.isEmpty()) { if (mesCorte <= 1 || codpers.isEmpty()) {
return Map.of(); return Map.of();
} }
int mesAnterior = mesCorte - 1; int mesAnterior = mesCorte - 1;
return rentaQuintaGrabadaRepository.findByAnioAndMesAndNumeroDocumentoIn(anio, mesAnterior, documentos) return rentaQuintaGrabadaRepository.buscarMes(anio, mesAnterior, codpers)
.stream() .stream()
.collect(Collectors.toMap( .collect(Collectors.toMap(
this::claveGrabada, RentaQuintaGrabada::getCodper,
RentaQuintaGrabada::getRetencionMesActual, RentaQuintaGrabada::getRetencionMesActual,
(a, b) -> a BigDecimal::add
)); ));
} }
/** boleta_tipo_personal es char(3): trim por el padding de PostgreSQL. */
private String claveGrabada(String numeroDocumento, String boletaTipoPersonal) {
return numeroDocumento + "|" + (boletaTipoPersonal == null ? "" : boletaTipoPersonal.trim());
}
private BigDecimal valorDelMes(Map<String, Map<Integer, BigDecimal>> porDocumento, String numeroDocumento, Integer mes) {
Map<Integer, BigDecimal> porMes = porDocumento.get(numeroDocumento);
return porMes == null ? null : porMes.get(mes);
}
private Predicate<RentaQuinta> esMesCorte(int mesCorte) { private Predicate<RentaQuinta> esMesCorte(int mesCorte) {
return r -> r.getBoletaMes().equals(mesCorte); return r -> r.getBoletaMes().equals(mesCorte);
} }
...@@ -801,6 +730,11 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -801,6 +730,11 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
return historial; return historial;
} }
private List<RentaQuinta> seleccionarHistorial(List<RentaQuinta> historial, TipoPlanilla tipo) {
return tipo == null ? historial : historial.stream()
.filter(r -> TipoPlanilla.deBoleta(r) == tipo).toList();
}
private record Agregado( private record Agregado(
RentaQuinta referencia, RentaQuinta referencia,
boolean mesCorteReal, boolean mesCorteReal,
...@@ -812,8 +746,8 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -812,8 +746,8 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
) {} ) {}
private record DatosExternos( private record DatosExternos(
Map<String, List<RetencionOtrosEmpleadores>> otrosEmpleadores, Map<Integer, List<RetencionOtrosEmpleadores>> otrosEmpleadores,
Map<String, List<ProyeccionDocenteHora>> proyeccionDocenteHora Map<Integer, List<ProyeccionDocenteHora>> proyeccionDocenteHora
) {} ) {}
private record OtrosEmpleadoresAgregado( private record OtrosEmpleadoresAgregado(
......
package so.planilla_be.utilidades.entity; package so.planilla_be.utilidades.entity;
import jakarta.persistence.Column; import jakarta.persistence.*;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import so.planilla_be.rentaquinta.entity.RentaQuinta;
import java.math.BigDecimal; import java.math.BigDecimal;
/**
* Utilidades mensuales declaradas en planilla.pl_utilidades. Reemplaza a la antigua columna
* "utilidades" de renta_quinta en el cálculo de renta de quinta.
*/
@Getter @Getter
@Setter @Setter
@Entity @Entity
@Table(name = "pl_utilidades") @Table(name = "pl_utilidades")
@IdClass(UtilidadId.class)
public class Utilidad { public class Utilidad {
@Id @Id
@Column(name = "numero_documento", length = 15) @GeneratedValue(strategy = GenerationType.IDENTITY)
private String numeroDocumento; @Column(name = "id_utilidad")
private Long idUtilidad;
@Id @ManyToOne(fetch = FetchType.LAZY)
@Column(name = "anio") @JoinColumn(name = "id_pl_planilla")
private Integer anio; private RentaQuinta planilla;
@Id
@Column(name = "mes")
private Integer mes;
@Column(name = "monto_utilidad", precision = 12, scale = 2, nullable = false) @Column(name = "monto_utilidad", precision = 12, scale = 2, nullable = false)
private BigDecimal montoUtilidad; private BigDecimal montoUtilidad;
......
package so.planilla_be.utilidades.entity;
import java.io.Serializable;
import java.util.Objects;
public class UtilidadId implements Serializable {
private String numeroDocumento;
private Integer anio;
private Integer mes;
public UtilidadId() {
}
public UtilidadId(String numeroDocumento, Integer anio, Integer mes) {
this.numeroDocumento = numeroDocumento;
this.anio = anio;
this.mes = mes;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof UtilidadId that)) {
return false;
}
return Objects.equals(numeroDocumento, that.numeroDocumento)
&& Objects.equals(anio, that.anio)
&& Objects.equals(mes, that.mes);
}
@Override
public int hashCode() {
return Objects.hash(numeroDocumento, anio, mes);
}
}
package so.planilla_be.utilidades.repository; package so.planilla_be.utilidades.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import so.planilla_be.utilidades.entity.Utilidad; import so.planilla_be.utilidades.entity.Utilidad;
import so.planilla_be.utilidades.entity.UtilidadId;
import java.util.List; import java.util.List;
public interface UtilidadRepository extends JpaRepository<Utilidad, UtilidadId> { public interface UtilidadRepository extends JpaRepository<Utilidad, Long> {
@Query("select u from Utilidad u join fetch u.planilla p where p.idRentaQuinta in :ids")
List<Utilidad> findByAnioAndNumeroDocumentoIn(Integer anio, List<String> numerosDocumento); List<Utilidad> buscarPorPlanillas(List<Long> ids);
} }
\ No newline at end of file
package so.planilla_be;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import so.planilla_be.rentaquinta.calculo.*;
import so.planilla_be.rentaquinta.dto.*;
import so.planilla_be.rentaquinta.service.RentaQuintaService;
import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresRequestDTO;
import so.planilla_be.otrosempleadores.service.RetencionOtrosEmpleadoresService;
import so.planilla_be.proyecciondocentehora.dto.ProyeccionDocenteHoraRequestDTO;
import so.planilla_be.proyecciondocentehora.service.ProyeccionDocenteHoraService;
import tools.jackson.databind.json.JsonMapper;
/** Independent before-migration write oracle. Every transaction is explicitly rolled back. */
public class MigrationWriteReference {
public static void main(String[] args) throws Exception {
String testUrl=System.getProperty("spring.datasource.url","");
if(!testUrl.matches("jdbc:postgresql://(127\\.0\\.0\\.1|localhost):15432/planilla_migration_test"))
throw new IllegalArgumentException("Write reference requires the isolated localhost:15432/planilla_migration_test database");
SpringApplication app=new SpringApplication(PlanillaBeApplication.class);
app.setWebApplicationType(WebApplicationType.NONE);
app.setDefaultProperties(Map.of("spring.devtools.restart.enabled","false","spring.jpa.show-sql","false"));
try(ConfigurableApplicationContext context=app.run()) {
JdbcTemplate jdbc=context.getBean(JdbcTemplate.class);
TransactionTemplate transaction=new TransactionTemplate(context.getBean(PlatformTransactionManager.class));
RentaQuintaService renta=context.getBean(RentaQuintaService.class);
RetencionOtrosEmpleadoresService otros=context.getBean(RetencionOtrosEmpleadoresService.class);
ProyeccionDocenteHoraService proyecciones=context.getBean(ProyeccionDocenteHoraService.class);
List<Object> results=new ArrayList<>();
Map<String,Object> before=fingerprint(jdbc);
for(String tipo:List.of("ADM","DOC1","DOC2")) {
long start=System.nanoTime();
Map<String,Object> result=transaction.execute(status -> {
try {
int count=renta.grabarMasivo(new RentaQuintaFiltroDTO(2026,10,null,TipoPlanilla.obligatorio(tipo),
TipoPago.desdeParametro(tipo),null,null,null,null,null,null,null,null));
org.springframework.orm.jpa.EntityManagerFactoryUtils.getTransactionalEntityManager(
context.getBean(jakarta.persistence.EntityManagerFactory.class)).flush();
List<Map<String,Object>> rows=jdbc.queryForList("select codper,anio,mes,retencion from planilla.pl_renta_quinta where anio=2026 and mes=10 order by codper");
return new LinkedHashMap<>(Map.of("tipo",tipo,"count",count,"rows",rows));
} finally { status.setRollbackOnly(); }
});
result.put("elapsedMs",(System.nanoTime()-start)/1_000_000.0);
results.add(result);
System.out.println("WRITE REFERENCE "+tipo+": "+result.get("count")+" workers");
}
Integer codper=jdbc.queryForObject("select min(codper) from planilla.pl_planilla where codper>0",Integer.class);
results.add(transaction.execute(status -> {
try {
otros.crear(codper,9998,1,new RetencionOtrosEmpleadoresRequestDTO(new BigDecimal("1234.56"),new BigDecimal("78.90")));
Object created=stableOtros(otros.listarPorAnio(codper,9998));
otros.actualizar(codper,9998,1,new RetencionOtrosEmpleadoresRequestDTO(new BigDecimal("2222.22"),BigDecimal.ZERO));
Object updated=stableOtros(otros.listarPorAnio(codper,9998));
otros.eliminar(codper,9998,1);
Object deleted=stableOtros(otros.listarPorAnio(codper,9998));
return Map.of("case","otros-crud","created",created,"updated",updated,"deleted",deleted);
} finally { status.setRollbackOnly(); }
}));
results.add(transaction.execute(status -> {
try {
var created=proyecciones.grabar(List.of(new ProyeccionDocenteHoraRequestDTO(9998,7,codper,1,new BigDecimal("3333.33"),null),
new ProyeccionDocenteHoraRequestDTO(9998,7,codper,2,null,new BigDecimal("1500.00"))));
Object initial=stableProyecciones(proyecciones.listarPorAnio(codper,9998));
proyecciones.grabar(List.of(new ProyeccionDocenteHoraRequestDTO(9998,7,codper,1,new BigDecimal("4444.44"),null)));
Object updated=stableProyecciones(proyecciones.listarPorAnio(codper,9998));
for(var row:created) proyecciones.eliminar(row.idProyeccion());
Object deleted=stableProyecciones(proyecciones.listarPorAnio(codper,9998));
return Map.of("case","proyecciones-crud","created",initial,"updated",updated,"deleted",deleted);
} finally { status.setRollbackOnly(); }
}));
Map<String,Object> after=fingerprint(jdbc);
if(!before.equals(after)) throw new AssertionError("Write oracle did not restore table data after rollback");
JsonMapper mapper=JsonMapper.builder().findAndAddModules().build();
Files.writeString(Path.of(args[0]),mapper.writerWithDefaultPrettyPrinter().writeValueAsString(
Map.of("codper",codper,"results",results,"rollbackVerified",true,"fingerprint",before)));
}
}
static Map<String,Object> fingerprint(JdbcTemplate jdbc) {
Map<String,Object> values=new LinkedHashMap<>();
for(String table:List.of("pl_planilla","pl_renta_quinta","pl_retencion_otros_empleadores","pl_proyeccion_docente_horas"))
values.put(table,jdbc.queryForMap("select count(*) as count, md5(coalesce(string_agg(row_to_json(t)::text,',' order by row_to_json(t)::text),'')) as checksum from planilla."+table+" t"));
return values;
}
static Object stableOtros(so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresListadoDTO data) {
Map<String,Object> result=new LinkedHashMap<>(); result.put("codper",data.codper()); result.put("personal",data.personal());
result.put("data",data.data().stream().map(r->Map.of("anio",r.anio(),"mes",r.mes(),"importe",r.importe(),"retencion",r.retencion())).toList()); return result;
}
static Object stableProyecciones(so.planilla_be.proyecciondocentehora.dto.ProyeccionDocenteHoraListadoDTO data) {
Map<String,Object> result=new LinkedHashMap<>(); result.put("codper",data.codper()); result.put("nombreCompleto",data.nombreCompleto()); result.put("anio",data.anio());
result.put("data",data.data().stream().map(r->Map.of("mes",r.mes(),"tipoProyeccion",r.tipoProyeccion(),"remunBrutaFutura",r.remunBrutaFutura(),"gratificacion",r.gratificacion())).toList()); return result;
}
}
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