Commit 41b66e29 by Vanesa Puma Yamo

feat: gratificacion por fecha de contrato y excepcion DOC2 diciembre

- ADM y DOC2: gratificacion prorrateada por fecha_inicio_contrato con
  dias del mes segun planilla.pl_calendario (docs/create-calendario.sql).
- Ingreso en junio despues del dia 1: sin gratificacion de julio; los
  dias de junio se suman a la gratificacion de diciembre.
- DOC2: si el DNI esta en planilla.pl_docente_grati_diciembre_completa
  para el anio, diciembre va completo (30 dias) en vez de 16.
- Mantenimiento /api/v1/docente-grati-diciembre-completa (GET, POST
  upsert por dni + anio, DELETE). Tabla en
  docs/create-docente-grati-diciembre-completa.sql.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parent 1e9d8757
...@@ -91,7 +91,7 @@ PK). Esa columna la crea `docs/alter-pl-renta-quinta-tipo-pago.sql`: correrlo an ...@@ -91,7 +91,7 @@ PK). Esa columna la crea `docs/alter-pl-renta-quinta-tipo-pago.sql`: correrlo an
| tipoPlanilla | boleta_tipo_personal | tipo_pago | Modalidad | Proyección del sueldo bruto | | tipoPlanilla | boleta_tipo_personal | tipo_pago | Modalidad | Proyección del sueldo bruto |
|---|---|---|---|---| |---|---|---|---|---|
| `ADM` | `ADM` | — | `MONTO_FIJO` | remuneración de referencia × meses restantes | | `ADM` | `ADM` | — | `MONTO_FIJO` | remuneración de referencia × meses restantes |
| `DOC2` | `DOC` | `2` | `MONTO_FIJO_DICIEMBRE_PROPORCIONAL` | igual que ADM, pero diciembre (sueldo bruto y gratificación) entra ÷ 30 × 16: en la proyección, en el estimado del mes de corte 12 y en la gratificación de diciembre pendiente. Una boleta real de diciembre no se toca | | `DOC2` | `DOC` | `2` | `MONTO_FIJO_DICIEMBRE_PROPORCIONAL` | igual que ADM, pero el sueldo bruto de diciembre entra ÷ 30 × 16 (proyección y estimado del mes de corte 12) y en su gratificación diciembre vale 16 días (5 meses + 16 días, ver reglas de dominio). Una boleta real de diciembre no se toca |
| `DOC1` | `DOC` | `1` | `DOCENTE_POR_HORA` | De `planilla.pl_proyeccion_renta_docentehora` (mismo documento, `boleta_tipo_personal='DOC'`, `proyeccion_anio` = año): `remun_bruta_futura` del mes consultado = sueldo bruto de la planilla en curso (reemplaza al de la boleta); suma de `remun_bruta_futura` de los meses **posteriores** = proyección del sueldo bruto. Sin fila = 0. La gratificación de julio/diciembre pendiente es el campo `gratificacion` de las filas `boleta_tipo_personal='GRA'` en `proyeccion_mes` 7 / 12 (sin fórmula × 1.09; sin fila = 0) y va en gratificación ordinaria de la proyección | | `DOC1` | `DOC` | `1` | `DOCENTE_POR_HORA` | De `planilla.pl_proyeccion_renta_docentehora` (mismo documento, `boleta_tipo_personal='DOC'`, `proyeccion_anio` = año): `remun_bruta_futura` del mes consultado = sueldo bruto de la planilla en curso (reemplaza al de la boleta); suma de `remun_bruta_futura` de los meses **posteriores** = proyección del sueldo bruto. Sin fila = 0. La gratificación de julio/diciembre pendiente es el campo `gratificacion` de las filas `boleta_tipo_personal='GRA'` en `proyeccion_mes` 7 / 12 (sin fórmula × 1.09; sin fila = 0) y va en gratificación ordinaria de la proyección |
En DOC1 cambian la proyección del sueldo y las gratificaciones; UIT, tramos y divisor son iguales. Detalle En DOC1 cambian la proyección del sueldo y las gratificaciones; UIT, tramos y divisor son iguales. Detalle
...@@ -125,6 +125,23 @@ que la duplicaba en la grilla. ...@@ -125,6 +125,23 @@ que la duplicaba en la grilla.
1. Proyección = remuneración del mes de corte × meses restantes + gratificaciones de julio y 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` diciembre aún no percibidas, cada una `(básico + asignación familiar) × 1.09`
- **ADM**: la gratificación se prorratea por `fecha_inicio_contrato` (boleta ADM del mes de
referencia, **obligatoria**: sin fecha la gratificación es 0):
`((base/6 × meses) + (base/6/30 × días)) × 1.09`. Contrato anterior al semestre
o ingreso en enero → semestre completo (6 meses); posterior → 0. Ingreso en otro mes del
semestre: meses completos después del mes de ingreso (día 1 = mes completo) + días del mes
de ingreso (días según `planilla.pl_calendario` − día + 1). Sin corte de 16 días. DOC1 no cambia.
Ingreso en junio después del día 1 (ADM y DOC2): gratificación de julio = 0 y los días de
junio se suman a la de diciembre (semestre completo + días de junio).
- **DOC2**: igual que ADM (fecha de su boleta DOC), pero en la gratificación de diciembre,
diciembre cuenta 16 días: semestre completo = `((base/6 × 5) + (base/6/30 × 16)) × 1.09`. La tabla la crea
`docs/create-calendario.sql` (2026): correrlo antes de desplegar o `validate` no arranca.
Excepción: si el DNI está en `planilla.pl_docente_grati_diciembre_completa` para el año
(PK `dni + anio`), su gratificación de diciembre DOC2 va con diciembre completo (30 días,
igual que ADM). Solo la gratificación: el sueldo bruto de diciembre sigue ÷ 30 × 16. La crea
`docs/create-docente-grati-diciembre-completa.sql` (correr antes de desplegar). Mantenimiento
en `/api/v1/docente-grati-diciembre-completa` (paquete `docentegratidiciembre`, ver
`docs/api-docente-grati-diciembre-completa.md`).
2. Renta bruta anual = historial + planilla cargada + proyección 2. Renta bruta anual = historial + planilla cargada + proyección
3. Renta neta = bruta − **7 UIT** 3. Renta neta = bruta − **7 UIT**
4. Impuesto anual = tramos progresivos 8% / 14% / 17% / 20% / 30% (5, 20, 35 y 45 UIT) 4. Impuesto anual = tramos progresivos 8% / 14% / 17% / 20% / 30% (5, 20, 35 y 45 UIT)
......
# API de docentes con gratificación de diciembre completa (DOC2)
Mantenimiento para **grabar** docentes en `planilla.pl_docente_grati_diciembre_completa`. Solo
escribe en esa tabla. La renta de quinta la lee para decidir cómo calcula la gratificación de
diciembre en la planilla **DOC2**.
Paquete: `so.planilla_be.docentegratidiciembre`.
## Para qué sirve
En DOC2 la gratificación de diciembre se calcula con **diciembre a 16 días**: el semestre completo
es 5 meses (julio a noviembre) + 16 días. Si el DNI del trabajador está en esta tabla **para el año
consultado**, diciembre va **completo (30 días)** y la gratificación se calcula igual que ADM:
semestre completo = 6 meses.
| | No está en la tabla | Está en la tabla (mismo año) |
|---|---|---|
| Semestre completo | `((base/6 × 5) + (base/6/30 × 16)) × 1.09` | `(base/6 × 6) × 1.09` = `base × 1.09` |
| Ingreso en junio después del día 1 | 5 meses + días de junio + 16 días | 6 meses + días de junio |
| Ingreso en diciembre | 16 − día + 1 días | 30 días del mes − día + 1 (si entra el día 1, mes completo) |
- **Solo DOC2.** ADM y DOC1 no miran esta tabla.
- **Solo la gratificación.** El sueldo bruto de diciembre en DOC2 sigue entrando ÷ 30 × 16.
- **Por año.** Un DNI registrado en 2026 no aplica a 2027; hay que registrarlo en cada año.
- `seleccion` no interviene en el cálculo: basta que exista la fila DNI + año.
Ejemplo (DNI 73180595, ingreso 02/06/2026, base 3,852.18): sin registro la gratificación de
diciembre es **4,548.78**; registrado en 2026 es (3,852.18 + 29 × 21.401) × 1.09 = **4,875.36**.
## Endpoints
| Método | Ruta | Descripción |
|---|---|---|
| GET | `/api/v1/docente-grati-diciembre-completa/{anio}` | Docentes registrados en el año |
| POST | `/api/v1/docente-grati-diciembre-completa` | Graba una lista de docentes |
| DELETE | `/api/v1/docente-grati-diciembre-completa/{dni}/{anio}` | Elimina un docente del año |
Swagger: `/docs`, tag **Docente gratificación diciembre completa**.
## Estructura de la tabla
| Columna | Tipo | Nulo | Notas |
|---|---|---|---|
| `dni` | varchar(15) | NO | PK (con `anio`) |
| `anio` | integer | NO | PK (con `dni`). Año de la planilla al que aplica |
| `apellido_paterno` | varchar(100) | SÍ | |
| `apellido_materno` | varchar(100) | SÍ | |
| `nombres` | varchar(150) | SÍ | |
| `seleccion` | integer | SÍ | Campo libre del mantenimiento |
La crea `docs/create-docente-grati-diciembre-completa.sql`. **Correrlo antes de desplegar**: con
`ddl-auto: validate` la app no arranca si falta la tabla.
## GET `/{anio}`
Llena la grilla de la pantalla. Ordena por apellido paterno, materno y nombres. Sin filas devuelve
`[]` (200).
```json
[
{
"anio": 2026,
"dni": "73180595",
"apellidoPaterno": "ACEVEDO",
"apellidoMaterno": "CUITO",
"nombres": "MAURICIO JUNIO",
"seleccion": 1
}
]
```
## POST
Body: arreglo JSON, no vacío.
| Campo | Tipo | Obligatorio | Regla |
|---|---|---|---|
| `anio` | Integer | sí | 2000 a 2100 |
| `dni` | String | sí | no vacío, máx. 15 (se hace `trim`) |
| `apellidoPaterno` | String | no | máx. 100 |
| `apellidoMaterno` | String | no | máx. 100 |
| `nombres` | String | no | máx. 150 |
| `seleccion` | Integer | no | |
Apellidos y nombres se graban con `trim` y en **mayúsculas**; vacíos se graban como `null`.
```json
[
{
"anio": 2026,
"dni": "73180595",
"apellidoPaterno": "Acevedo",
"apellidoMaterno": "Cuito",
"nombres": "Mauricio Junio",
"seleccion": 1
}
]
```
### Comportamiento
- **Insertar o actualizar:** si ya existe la fila `dni + anio`, se actualizan apellidos, nombres y
`seleccion`. 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. La retención ya grabada en
`pl_renta_quinta` no se recalcula sola: hay que volver a consultar/grabar la renta de quinta.
### Response `200`
Lista de las filas grabadas, en el mismo orden del request (mismo formato que el GET).
## DELETE `/{dni}/{anio}`
`204` sin cuerpo si se elimina; `404` si no existe la fila; `400` si `anio` no es numérico.
## Errores
Formato `{timestamp, status, message}`, igual que el resto del API.
| Status | Causa |
|---|---|
| 400 | Lista vacía, `dni` o `anio` ausente, `anio` fuera de 2000–2100, texto más largo que la columna, `anio` no numérico en la ruta |
| 404 | `DELETE` de un DNI + año que no existe |
## Ejemplo
```bash
curl -X POST http://localhost:8080/api/v1/docente-grati-diciembre-completa \
-H "Content-Type: application/json" \
-d '[{"anio":2026,"dni":"73180595","apellidoPaterno":"ACEVEDO","apellidoMaterno":"CUITO","nombres":"MAURICIO JUNIO","seleccion":1}]'
curl http://localhost:8080/api/v1/docente-grati-diciembre-completa/2026
curl -X DELETE http://localhost:8080/api/v1/docente-grati-diciembre-completa/73180595/2026
```
## Archivos
```
docentegratidiciembre/
├── controller/DocenteGratiDiciembreCompletaController
├── dto/DocenteGratiDiciembreCompletaRequestDTO, DocenteGratiDiciembreCompletaDTO
├── entity/DocenteGratiDiciembreCompleta + DocenteGratiDiciembreCompletaId PK dni + anio
├── exception/DocenteGratiDiciembreCompletaNoEncontradaException
├── repository/DocenteGratiDiciembreCompletaRepository existsByDniAndAnio (lo usa renta de quinta)
└── service/ + service/impl/
```
En renta de quinta: `RentaQuintaServiceImpl#calcular` arma `gratificacionDiciembreCompleta` (DOC2 +
fila DNI + año) y `RentaQuintaCalculadora#gratificacionProporcional` deja de aplicar los 16 días.
-- 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.
-- semestre 1 = enero-junio (gratificación de julio), 2 = julio-diciembre (de diciembre)
-- mes_gratificacion mes en que se paga la gratificación de ese semestre (7 o 12)
--
-- Ejecutar ANTES de desplegar el backend: con ddl-auto: validate, la app no arranca si falta la
-- tabla. Para un año nuevo, repetir el INSERT cambiando el año en generate_series.
CREATE TABLE planilla.pl_calendario (
anio integer NOT NULL,
mes integer NOT NULL CHECK (mes BETWEEN 1 AND 12),
nombre_mes varchar(20) NOT NULL,
fecha_inicio date NOT NULL,
fecha_fin date NOT NULL,
dias integer NOT NULL,
semestre integer NOT NULL CHECK (semestre IN (1, 2)),
mes_gratificacion integer NOT NULL CHECK (mes_gratificacion IN (7, 12)),
PRIMARY KEY (anio, mes)
);
INSERT INTO planilla.pl_calendario (anio, mes, nombre_mes, fecha_inicio, fecha_fin, dias, semestre, mes_gratificacion)
SELECT extract(year FROM d)::int,
extract(month FROM d)::int,
(ARRAY['ENERO', 'FEBRERO', 'MARZO', 'ABRIL', 'MAYO', 'JUNIO', 'JULIO', 'AGOSTO',
'SETIEMBRE', 'OCTUBRE', 'NOVIEMBRE', 'DICIEMBRE'])[extract(month FROM d)::int],
d::date,
(d + interval '1 month' - interval '1 day')::date,
extract(day FROM d + interval '1 month' - interval '1 day')::int,
CASE WHEN extract(month FROM d) <= 6 THEN 1 ELSE 2 END,
CASE WHEN extract(month FROM d) <= 6 THEN 7 ELSE 12 END
FROM generate_series(DATE '2026-01-01', DATE '2026-12-01', interval '1 month') AS d;
COMMENT ON TABLE planilla.pl_calendario IS
'Meses del año con sus días y semestre de gratificación. Lo usa el cálculo de renta de quinta.';
-- Docentes DOC2 (DOC + tipo_pago 2) cuya gratificación de diciembre del año se calcula con
-- diciembre completo (30 días) en vez de 16 días. Si el DNI está en esta tabla para el año, la
-- gratificación de diciembre de su planilla DOC2 se prorratea igual que ADM (semestre completo =
-- 6 meses). No cambia el sueldo bruto de diciembre (sigue ÷ 30 × 16) ni afecta a ADM ni a DOC1.
-- anio año de la planilla al que aplica (una fila por DNI + año)
-- seleccion campo numérico libre para el mantenimiento; el cálculo solo mira si existe la fila.
--
-- Ejecutar ANTES de desplegar el backend: con ddl-auto: validate, la app no arranca si falta la
-- tabla. Se graba desde POST /api/v1/docente-grati-diciembre-completa.
CREATE TABLE planilla.pl_docente_grati_diciembre_completa (
dni varchar(15) NOT NULL,
anio integer NOT NULL,
apellido_paterno varchar(100),
apellido_materno varchar(100),
nombres varchar(150),
seleccion integer,
PRIMARY KEY (dni, anio)
);
COMMENT ON TABLE planilla.pl_docente_grati_diciembre_completa IS
'Docentes DOC2 con gratificación de diciembre a 30 días (no 16) por año. Lo usa el cálculo de renta de quinta.';
-- Ejemplo:
-- INSERT INTO planilla.pl_docente_grati_diciembre_completa (dni, anio, apellido_paterno, apellido_materno, nombres, seleccion)
-- VALUES ('73180595', 2026, 'ACEVEDO', 'CUITO', 'MAURICIO JUNIO', 1);
package so.planilla_be.docentegratidiciembre.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import so.planilla_be.docentegratidiciembre.dto.DocenteGratiDiciembreCompletaDTO;
import so.planilla_be.docentegratidiciembre.dto.DocenteGratiDiciembreCompletaRequestDTO;
import so.planilla_be.docentegratidiciembre.service.DocenteGratiDiciembreCompletaService;
import java.util.List;
@Tag(name = "Docente gratificación diciembre completa",
description = "Grabado de pl_docente_grati_diciembre_completa (DOC2 con diciembre a 30 días)")
@Validated
@RestController
@RequestMapping("/api/v1/docente-grati-diciembre-completa")
public class DocenteGratiDiciembreCompletaController {
private final DocenteGratiDiciembreCompletaService service;
public DocenteGratiDiciembreCompletaController(DocenteGratiDiciembreCompletaService service) {
this.service = service;
}
@Operation(summary = "Docentes registrados en el año")
@GetMapping("/{anio}")
public List<DocenteGratiDiciembreCompletaDTO> listarPorAnio(@PathVariable Integer anio) {
return service.listarPorAnio(anio);
}
@Operation(summary = "Grabar docentes (inserta o actualiza por DNI + año)")
@PostMapping
public List<DocenteGratiDiciembreCompletaDTO> grabar(
@RequestBody @NotEmpty List<@Valid DocenteGratiDiciembreCompletaRequestDTO> filas) {
return service.grabar(filas);
}
@Operation(summary = "Eliminar un docente del año")
@DeleteMapping("/{dni}/{anio}")
public ResponseEntity<Void> eliminar(@PathVariable String dni, @PathVariable Integer anio) {
service.eliminar(dni, anio);
return ResponseEntity.noContent().build();
}
}
package so.planilla_be.docentegratidiciembre.dto;
public record DocenteGratiDiciembreCompletaDTO(
Integer anio,
String dni,
String apellidoPaterno,
String apellidoMaterno,
String nombres,
Integer seleccion
) {}
package so.planilla_be.docentegratidiciembre.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
/**
* Una fila de pl_docente_grati_diciembre_completa. La clave es dni + anio: si ya existe se
* actualizan apellidos, nombres y seleccion.
*/
public record DocenteGratiDiciembreCompletaRequestDTO(
@NotNull @Min(2000) @Max(2100) Integer anio,
@NotBlank @Size(max = 15) String dni,
@Size(max = 100) String apellidoPaterno,
@Size(max = 100) String apellidoMaterno,
@Size(max = 150) String nombres,
Integer seleccion
) {}
package so.planilla_be.docentegratidiciembre.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;
/**
* Docente DOC2 cuya gratificación de diciembre del año va con diciembre completo (30 días) y no a
* 16 días (planilla.pl_docente_grati_diciembre_completa, una fila por DNI + año). La crea
* docs/create-docente-grati-diciembre-completa.sql; se graba desde el mantenimiento de este módulo
* y la renta de quinta solo la lee.
*/
@Getter
@Setter
@Entity
@Table(name = "pl_docente_grati_diciembre_completa")
@IdClass(DocenteGratiDiciembreCompletaId.class)
public class DocenteGratiDiciembreCompleta {
@Id
@Column(name = "dni")
private String dni;
@Id
@Column(name = "anio")
private Integer anio;
@Column(name = "apellido_paterno")
private String apellidoPaterno;
@Column(name = "apellido_materno")
private String apellidoMaterno;
@Column(name = "nombres")
private String nombres;
/** Campo libre del mantenimiento; el cálculo solo mira si existe la fila DNI + año. */
@Column(name = "seleccion")
private Integer seleccion;
}
package so.planilla_be.docentegratidiciembre.entity;
import java.io.Serializable;
public record DocenteGratiDiciembreCompletaId(String dni, Integer anio) implements Serializable {}
package so.planilla_be.docentegratidiciembre.exception;
public class DocenteGratiDiciembreCompletaNoEncontradaException extends RuntimeException {
public DocenteGratiDiciembreCompletaNoEncontradaException(String message) {
super(message);
}
}
package so.planilla_be.docentegratidiciembre.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import so.planilla_be.docentegratidiciembre.entity.DocenteGratiDiciembreCompleta;
import so.planilla_be.docentegratidiciembre.entity.DocenteGratiDiciembreCompletaId;
import java.util.List;
public interface DocenteGratiDiciembreCompletaRepository
extends JpaRepository<DocenteGratiDiciembreCompleta, DocenteGratiDiciembreCompletaId> {
/** Docentes registrados en el año, por apellidos y nombres. */
List<DocenteGratiDiciembreCompleta> findByAnioOrderByApellidoPaternoAscApellidoMaternoAscNombresAsc(Integer anio);
/** Renta de quinta DOC2: true si el DNI tiene diciembre completo (30 días) en el año. */
boolean existsByDniAndAnio(String dni, Integer anio);
}
package so.planilla_be.docentegratidiciembre.service;
import so.planilla_be.docentegratidiciembre.dto.DocenteGratiDiciembreCompletaDTO;
import so.planilla_be.docentegratidiciembre.dto.DocenteGratiDiciembreCompletaRequestDTO;
import java.util.List;
public interface DocenteGratiDiciembreCompletaService {
/** Docentes registrados en el año. Sin filas: lista vacía. */
List<DocenteGratiDiciembreCompletaDTO> listarPorAnio(Integer anio);
/**
* Graba las filas recibidas. La clave es dni + anio: si ya existe se actualizan los datos, si
* no se inserta. Todo o nada.
*/
List<DocenteGratiDiciembreCompletaDTO> grabar(List<DocenteGratiDiciembreCompletaRequestDTO> filas);
/** Elimina la fila dni + anio. Lanza DocenteGratiDiciembreCompletaNoEncontradaException si no existe. */
void eliminar(String dni, Integer anio);
}
package so.planilla_be.docentegratidiciembre.service.impl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import so.planilla_be.docentegratidiciembre.dto.DocenteGratiDiciembreCompletaDTO;
import so.planilla_be.docentegratidiciembre.dto.DocenteGratiDiciembreCompletaRequestDTO;
import so.planilla_be.docentegratidiciembre.entity.DocenteGratiDiciembreCompleta;
import so.planilla_be.docentegratidiciembre.entity.DocenteGratiDiciembreCompletaId;
import so.planilla_be.docentegratidiciembre.exception.DocenteGratiDiciembreCompletaNoEncontradaException;
import so.planilla_be.docentegratidiciembre.repository.DocenteGratiDiciembreCompletaRepository;
import so.planilla_be.docentegratidiciembre.service.DocenteGratiDiciembreCompletaService;
import java.util.List;
@Service
public class DocenteGratiDiciembreCompletaServiceImpl implements DocenteGratiDiciembreCompletaService {
private final DocenteGratiDiciembreCompletaRepository repository;
public DocenteGratiDiciembreCompletaServiceImpl(DocenteGratiDiciembreCompletaRepository repository) {
this.repository = repository;
}
@Override
@Transactional(readOnly = true)
public List<DocenteGratiDiciembreCompletaDTO> listarPorAnio(Integer anio) {
return repository.findByAnioOrderByApellidoPaternoAscApellidoMaternoAscNombresAsc(anio).stream()
.map(this::toDto)
.toList();
}
@Override
@Transactional
public List<DocenteGratiDiciembreCompletaDTO> grabar(List<DocenteGratiDiciembreCompletaRequestDTO> filas) {
return filas.stream()
.map(this::grabarFila)
.toList();
}
@Override
@Transactional
public void eliminar(String dni, Integer anio) {
DocenteGratiDiciembreCompleta entidad = repository
.findById(new DocenteGratiDiciembreCompletaId(dni.trim(), anio))
.orElseThrow(() -> new DocenteGratiDiciembreCompletaNoEncontradaException(
"No existe docente con gratificación de diciembre completa para DNI %s, año %d"
.formatted(dni.trim(), anio)));
repository.delete(entidad);
}
private DocenteGratiDiciembreCompletaDTO grabarFila(DocenteGratiDiciembreCompletaRequestDTO fila) {
String dni = fila.dni().trim();
DocenteGratiDiciembreCompleta entidad = repository
.findById(new DocenteGratiDiciembreCompletaId(dni, fila.anio()))
.orElseGet(DocenteGratiDiciembreCompleta::new);
entidad.setDni(dni);
entidad.setAnio(fila.anio());
entidad.setApellidoPaterno(texto(fila.apellidoPaterno()));
entidad.setApellidoMaterno(texto(fila.apellidoMaterno()));
entidad.setNombres(texto(fila.nombres()));
entidad.setSeleccion(fila.seleccion());
return toDto(repository.save(entidad));
}
/** Apellidos y nombres en mayúsculas y sin espacios sobrantes; vacío se graba como null. */
private String texto(String valor) {
if (valor == null || valor.isBlank()) {
return null;
}
return valor.trim().toUpperCase();
}
private DocenteGratiDiciembreCompletaDTO toDto(DocenteGratiDiciembreCompleta e) {
return new DocenteGratiDiciembreCompletaDTO(
e.getAnio(),
e.getDni(),
e.getApellidoPaterno(),
e.getApellidoMaterno(),
e.getNombres(),
e.getSeleccion());
}
}
...@@ -5,6 +5,7 @@ import so.planilla_be.rentaquinta.calculo.CalculoRentaQuinta.TramoCalculado; ...@@ -5,6 +5,7 @@ import so.planilla_be.rentaquinta.calculo.CalculoRentaQuinta.TramoCalculado;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
...@@ -41,6 +42,9 @@ public class RentaQuintaCalculadora { ...@@ -41,6 +42,9 @@ public class RentaQuintaCalculadora {
private static final BigDecimal DIAS_MES = BigDecimal.valueOf(30); private static final BigDecimal DIAS_MES = BigDecimal.valueOf(30);
private static final BigDecimal DIAS_DICIEMBRE_PROPORCIONAL = BigDecimal.valueOf(16); private static final BigDecimal DIAS_DICIEMBRE_PROPORCIONAL = BigDecimal.valueOf(16);
/** Gratificación proporcional: meses de un semestre. */
private static final BigDecimal MESES_SEMESTRE = BigDecimal.valueOf(6);
private final UitCache uit; private final UitCache uit;
public RentaQuintaCalculadora(UitCache uit) { public RentaQuintaCalculadora(UitCache uit) {
...@@ -65,11 +69,13 @@ public class RentaQuintaCalculadora { ...@@ -65,11 +69,13 @@ public class RentaQuintaCalculadora {
if (!mesCorteReal) { if (!mesCorteReal) {
estimadoSueldoBruto = porHora ? orZero(datos.sueldoBrutoMesCorteDocenteHora()) estimadoSueldoBruto = porHora ? orZero(datos.sueldoBrutoMesCorteDocenteHora())
: montoDelMes(datos, orZero(datos.remuneracionReferencia()), datos.mesCorte()); : montoDelMes(datos, orZero(datos.remuneracionReferencia()), datos.mesCorte());
// DOC2 en diciembre: la gratificación sale del sueldo ya proporcional (÷ 30 × 16). // Misma base que las gratificaciones pendientes (basicoReferencia), para que el mes de
// corte 7/12 dé lo mismo que se proyectaba antes. En DOC2 los 16 días de diciembre ya
// los aplica gratificacionProporcional.
if (datos.mesCorte() == 7 || datos.mesCorte() == 12) { if (datos.mesCorte() == 7 || datos.mesCorte() == 12) {
estimadoGratificacion = porHora estimadoGratificacion = porHora
? gratificacionDocenteHora(datos, datos.mesCorte()) ? gratificacionDocenteHora(datos, datos.mesCorte())
: gratificacion(estimadoSueldoBruto); : gratificacionProporcional(datos, orZero(datos.basicoReferencia()), datos.mesCorte());
} }
} }
BigDecimal estimadoMesCorte = estimadoSueldoBruto.add(estimadoGratificacion); BigDecimal estimadoMesCorte = estimadoSueldoBruto.add(estimadoGratificacion);
...@@ -150,25 +156,95 @@ public class RentaQuintaCalculadora { ...@@ -150,25 +156,95 @@ public class RentaQuintaCalculadora {
* boleta real ni el estimado del propio mes de corte cuando este es julio/diciembre). * boleta real ni el estimado del propio mes de corte cuando este es julio/diciembre).
* *
* <p>Base = {@code basicoReferencia} × 1.09: monto_basico de pl_renta_basico si el trabajador * <p>Base = {@code basicoReferencia} × 1.09: monto_basico de pl_renta_basico si el trabajador
* tiene registro; si no, básico + asignación familiar de pl_planilla. * tiene registro; si no, básico + asignación familiar de pl_planilla. En ADM se prorratea
* según la fecha de inicio de contrato (ver {@link #gratificacionProporcional}).
*/ */
private BigDecimal gratificacionesPendientes(DatosCalculo datos, boolean julioCubierto, boolean diciembreCubierto) { private BigDecimal gratificacionesPendientes(DatosCalculo datos, boolean julioCubierto, boolean diciembreCubierto) {
boolean porHora = datos.modalidad() == ModalidadCalculo.DOCENTE_POR_HORA; boolean porHora = datos.modalidad() == ModalidadCalculo.DOCENTE_POR_HORA;
BigDecimal gratificacion = gratificacion(orZero(datos.basicoReferencia())); BigDecimal base = orZero(datos.basicoReferencia());
BigDecimal total = BigDecimal.ZERO; BigDecimal total = BigDecimal.ZERO;
if (!julioCubierto) { if (!julioCubierto) {
total = total.add(porHora ? gratificacionDocenteHora(datos, 7) : gratificacion); total = total.add(porHora ? gratificacionDocenteHora(datos, 7) : gratificacionProporcional(datos, base, 7));
} }
if (!diciembreCubierto) { if (!diciembreCubierto) {
total = total.add(porHora ? gratificacionDocenteHora(datos, 12) : montoDelMes(datos, gratificacion, 12)); total = total.add(porHora ? gratificacionDocenteHora(datos, 12) : gratificacionProporcional(datos, base, 12));
} }
return total; return total;
} }
/** /**
* DOC2 ({@link ModalidadCalculo#MONTO_FIJO_DICIEMBRE_PROPORCIONAL}): el sueldo bruto y la * CONDICIÓN FECHA DE CONTRATO (ADM y DOC2) — gratificación de julio (semestre enero-junio) o
* gratificación de diciembre son proporcionales, monto ÷ 30 × 16. Cualquier otro mes o * de diciembre (julio-diciembre) según cuánto del semestre cubre el contrato:
* modalidad devuelve el monto sin cambios. * <pre>((base / 6 × meses) + (base / 6 / 30 × días)) × 1.09</pre>
* <ul>
* <li>DOC1 / DOC sin tipo de pago: sin prorrateo → base × 1.09.</li>
* <li>Sin fecha de inicio de contrato: 0 (la fecha es obligatoria para calcularla).</li>
* <li>Contrato anterior al semestre: semestre completo (6 meses, 0 días → base × 1.09).</li>
* <li>Contrato posterior al semestre: 0.</li>
* <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>
* <li>Días = días del mes en planilla.pl_calendario − día de ingreso + 1 (0 si entra el
* día 1), en cualquier mes de ingreso salvo enero: ingreso en enero → solo meses
* (semestre completo). Igual en ADM y DOC2.</li>
* <li>DOC2, diciembre: diciembre no es mes completo sino 16 días. Semestre completo =
* 5 meses (julio-noviembre) + 16 días; ingreso en diciembre → 16 − día + 1 días.
* Excepción: si el DNI está en planilla.pl_docente_grati_diciembre_completa, diciembre
* va completo (30 días) y se calcula igual que ADM.</li>
* <li>Ingreso en junio después del día 1 (ADM y DOC2): no hay gratificación de julio; los
* días de junio (días del mes − día + 1) se suman a la de diciembre, que además toma su
* semestre completo (6 meses en ADM; 5 meses + 16 días en DOC2).</li>
* </ul>
*/
private BigDecimal gratificacionProporcional(DatosCalculo datos, BigDecimal base, int mesGratificacion) {
if (!datos.gratificacionPorContrato()) {
return gratificacion(base);
}
LocalDate inicio = datos.fechaInicioContrato();
if (inicio == null) {
return BigDecimal.ZERO;
}
boolean docenteMontoFijo = datos.modalidad() == ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL;
boolean diciembreProporcional = docenteMontoFijo && mesGratificacion == 12
&& !datos.gratificacionDiciembreCompleta();
int primerMes = mesGratificacion == 7 ? 1 : 7;
int ultimoMes = mesGratificacion == 7 ? 6 : 12;
int ultimoMesCompleto = diciembreProporcional ? 11 : ultimoMes;
int diasFinales = diciembreProporcional ? DIAS_DICIEMBRE_PROPORCIONAL.intValue() : 0;
int anio = datos.periodo();
int mesIngreso = inicio.getMonthValue();
boolean ingresoJunioPosteriorAlDia1 = inicio.getYear() == anio && mesIngreso == 6 && inicio.getDayOfMonth() > 1;
int meses;
int dias;
if (ingresoJunioPosteriorAlDia1) {
if (mesGratificacion == 7) {
return BigDecimal.ZERO;
}
meses = ultimoMesCompleto - primerMes + 1;
dias = datos.diasMesInicioContrato() - inicio.getDayOfMonth() + 1 + diasFinales;
} else if (inicio.getYear() < anio || mesIngreso < primerMes || mesIngreso == 1) {
meses = ultimoMesCompleto - primerMes + 1;
dias = diasFinales;
} else if (inicio.getYear() > anio || mesIngreso > ultimoMes) {
return BigDecimal.ZERO;
} else if (diciembreProporcional && mesIngreso == 12) {
meses = 0;
dias = Math.max(0, diasFinales - inicio.getDayOfMonth() + 1);
} else {
boolean mesCompleto = inicio.getDayOfMonth() == 1;
meses = ultimoMesCompleto - mesIngreso + (mesCompleto ? 1 : 0);
dias = (mesCompleto ? 0 : datos.diasMesInicioContrato() - inicio.getDayOfMonth() + 1) + diasFinales;
}
BigDecimal porMes = base.divide(MESES_SEMESTRE, 10, RoundingMode.HALF_UP);
BigDecimal porDia = porMes.divide(DIAS_MES, 10, RoundingMode.HALF_UP);
return gratificacion(porMes.multiply(BigDecimal.valueOf(meses)).add(porDia.multiply(BigDecimal.valueOf(dias))));
}
/**
* DOC2 ({@link ModalidadCalculo#MONTO_FIJO_DICIEMBRE_PROPORCIONAL}): el sueldo bruto de
* diciembre es proporcional, monto ÷ 30 × 16 (la gratificación de diciembre aplica los 16 días
* en {@link #gratificacionProporcional}). Cualquier otro mes o modalidad devuelve el monto sin
* cambios.
*/ */
private BigDecimal montoDelMes(DatosCalculo datos, BigDecimal monto, int mes) { private BigDecimal montoDelMes(DatosCalculo datos, BigDecimal monto, int mes) {
if (mes != 12 || datos.modalidad() != ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL) { if (mes != 12 || datos.modalidad() != ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL) {
...@@ -293,7 +369,11 @@ public class RentaQuintaCalculadora { ...@@ -293,7 +369,11 @@ public class RentaQuintaCalculadora {
* {@code proyeccionDocenteHora} (suma de remun_bruta_futura de los meses posteriores) y * {@code proyeccionDocenteHora} (suma de remun_bruta_futura de los meses posteriores) y
* {@code sueldoBrutoMesCorteDocenteHora} (remun_bruta_futura del mes de corte) solo se usan en * {@code sueldoBrutoMesCorteDocenteHora} (remun_bruta_futura del mes de corte) solo se usan en
* {@link ModalidadCalculo#DOCENTE_POR_HORA}; {@code sueldoBrutoRealMesCorte} es el sueldo * {@link ModalidadCalculo#DOCENTE_POR_HORA}; {@code sueldoBrutoRealMesCorte} es el sueldo
* bruto de la boleta real del mes de corte que ese monto reemplaza. La * 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 fechaInicioContrato} (sin fecha = 0); {@code diasMesInicioContrato} son los días de
* ese mes según planilla.pl_calendario. {@code gratificacionDiciembreCompleta} es true en DOC2
* cuando el DNI está en planilla.pl_docente_grati_diciembre_completa. 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.
*/ */
public record DatosCalculo( public record DatosCalculo(
...@@ -319,6 +399,10 @@ public class RentaQuintaCalculadora { ...@@ -319,6 +399,10 @@ public class RentaQuintaCalculadora {
BigDecimal otrosEmpleadoresImporteProyeccion, BigDecimal otrosEmpleadoresImporteProyeccion,
BigDecimal otrosEmpleadoresRetencionAnterior, BigDecimal otrosEmpleadoresRetencionAnterior,
BigDecimal otrosEmpleadoresRetencionMesActual, BigDecimal otrosEmpleadoresRetencionMesActual,
BigDecimal otrosEmpleadoresRetencionProyeccion BigDecimal otrosEmpleadoresRetencionProyeccion,
boolean gratificacionPorContrato,
LocalDate fechaInicioContrato,
int diasMesInicioContrato,
boolean gratificacionDiciembreCompleta
) {} ) {}
} }
package so.planilla_be.rentaquinta.dto; package so.planilla_be.rentaquinta.dto;
import java.math.BigDecimal; import java.math.BigDecimal;
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".
...@@ -14,6 +15,7 @@ public record RentaQuintaResumenDTO( ...@@ -14,6 +15,7 @@ public record RentaQuintaResumenDTO(
Integer periodo, Integer periodo,
String numeroDocumento, String numeroDocumento,
String nombreCompleto, String nombreCompleto,
LocalDate fechaInicioContrato,
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 {}
...@@ -8,6 +8,7 @@ import org.springframework.web.bind.MissingServletRequestParameterException; ...@@ -8,6 +8,7 @@ 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;
...@@ -34,6 +35,11 @@ public class GlobalExceptionHandler { ...@@ -34,6 +35,11 @@ public class GlobalExceptionHandler {
return build(HttpStatus.NOT_FOUND, ex.getMessage()); return build(HttpStatus.NOT_FOUND, ex.getMessage());
} }
@ExceptionHandler(DocenteGratiDiciembreCompletaNoEncontradaException.class)
public ResponseEntity<Map<String, Object>> handleNotFound(DocenteGratiDiciembreCompletaNoEncontradaException ex) {
return build(HttpStatus.NOT_FOUND, ex.getMessage());
}
@ExceptionHandler(RetencionOtrosEmpleadoresYaExisteException.class) @ExceptionHandler(RetencionOtrosEmpleadoresYaExisteException.class)
public ResponseEntity<Map<String, Object>> handleConflict(RetencionOtrosEmpleadoresYaExisteException ex) { public ResponseEntity<Map<String, Object>> handleConflict(RetencionOtrosEmpleadoresYaExisteException ex) {
return build(HttpStatus.CONFLICT, ex.getMessage()); return build(HttpStatus.CONFLICT, ex.getMessage());
......
...@@ -30,6 +30,7 @@ public class RentaQuintaMapper { ...@@ -30,6 +30,7 @@ public class RentaQuintaMapper {
periodo, periodo,
referencia.getNumeroDocumento(), referencia.getNumeroDocumento(),
nombreCompleto(referencia.getApellidoPaterno(), referencia.getApellidoMaterno(), referencia.getNombres()), nombreCompleto(referencia.getApellidoPaterno(), referencia.getApellidoMaterno(), referencia.getNombres()),
referencia.getFechaInicioContrato(),
mesCorte, mesCorte,
orZero(historialReal), orZero(historialReal),
orZero(planillaCargada), orZero(planillaCargada),
......
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> {
}
...@@ -5,6 +5,7 @@ import org.springframework.data.domain.PageRequest; ...@@ -5,6 +5,7 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import so.planilla_be.docentegratidiciembre.repository.DocenteGratiDiciembreCompletaRepository;
import so.planilla_be.proyecciondocentehora.repository.ProyeccionDocenteHoraRepository; import so.planilla_be.proyecciondocentehora.repository.ProyeccionDocenteHoraRepository;
import so.planilla_be.rentaquinta.calculo.CalculoRentaQuinta; import so.planilla_be.rentaquinta.calculo.CalculoRentaQuinta;
import so.planilla_be.rentaquinta.calculo.ModalidadCalculo; import so.planilla_be.rentaquinta.calculo.ModalidadCalculo;
...@@ -23,6 +24,9 @@ import so.planilla_be.rentaquinta.entity.RentaQuintaGrabada; ...@@ -23,6 +24,9 @@ import so.planilla_be.rentaquinta.entity.RentaQuintaGrabada;
import so.planilla_be.rentaquinta.entity.RentaQuintaGrabadaId; 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.DocumentoResumenProjection; import so.planilla_be.rentaquinta.repository.DocumentoResumenProjection;
import so.planilla_be.rentaquinta.repository.RentaQuintaGrabadaRepository; import so.planilla_be.rentaquinta.repository.RentaQuintaGrabadaRepository;
import so.planilla_be.rentaquinta.repository.RentaQuintaRepository; import so.planilla_be.rentaquinta.repository.RentaQuintaRepository;
...@@ -35,6 +39,8 @@ import so.planilla_be.utilidades.entity.Utilidad; ...@@ -35,6 +39,8 @@ import so.planilla_be.utilidades.entity.Utilidad;
import so.planilla_be.utilidades.repository.UtilidadRepository; import so.planilla_be.utilidades.repository.UtilidadRepository;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
...@@ -65,6 +71,8 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -65,6 +71,8 @@ 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 DocenteGratiDiciembreCompletaRepository docenteGratiDiciembreCompletaRepository;
public RentaQuintaServiceImpl(RentaQuintaRepository repository, RentaQuintaMapper mapper, public RentaQuintaServiceImpl(RentaQuintaRepository repository, RentaQuintaMapper mapper,
RentaQuintaCalculadora calculadora, RentaQuintaCalculadora calculadora,
...@@ -72,7 +80,9 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -72,7 +80,9 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
RentaBasicoRepository rentaBasicoRepository, RentaBasicoRepository rentaBasicoRepository,
UtilidadRepository utilidadRepository, UtilidadRepository utilidadRepository,
RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository, RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository,
ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository) { ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository,
CalendarioRepository calendarioRepository,
DocenteGratiDiciembreCompletaRepository docenteGratiDiciembreCompletaRepository) {
this.repository = repository; this.repository = repository;
this.mapper = mapper; this.mapper = mapper;
this.calculadora = calculadora; this.calculadora = calculadora;
...@@ -81,6 +91,8 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -81,6 +91,8 @@ 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.docenteGratiDiciembreCompletaRepository = docenteGratiDiciembreCompletaRepository;
} }
/** /**
...@@ -396,6 +408,23 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -396,6 +408,23 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
BigDecimal sueldoBrutoRealMesCorte = a.mesCorteReal() BigDecimal sueldoBrutoRealMesCorte = a.mesCorteReal()
? mapper.sueldoBrutoMes(historial, esMesCorte(mesCorte)) ? mapper.sueldoBrutoMes(historial, esMesCorte(mesCorte))
: BigDecimal.ZERO; : BigDecimal.ZERO;
// Gratificación proporcional por fecha de contrato: ADM y DOC2, y obligatoria (sin fecha la
// gratificación es 0). Se toma de la boleta del mismo tipo (ADM / DOC) del mes de referencia.
boolean gratificacionPorContrato = tipoPlanilla == TipoPlanilla.MENSUAL_MONTO_FIJO
|| modalidad == ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL;
LocalDate fechaInicioContrato = gratificacionPorContrato
? historial.stream()
.filter(esMesCorte(mesReferencia))
.filter(r -> TipoPlanilla.deBoleta(r) == tipoPlanilla)
.map(RentaQuinta::getFechaInicioContrato)
.filter(Objects::nonNull)
.findFirst()
.orElse(null)
: null;
// DOC2: si el DNI está en pl_docente_grati_diciembre_completa para el año, la gratificación
// de diciembre va con diciembre completo (30 días) en vez de 16.
boolean gratificacionDiciembreCompleta = modalidad == ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL
&& docenteGratiDiciembreCompletaRepository.existsByDniAndAnio(numeroDocumento.trim(), periodo);
return calculadora.calcular(new DatosCalculo( return calculadora.calcular(new DatosCalculo(
periodo, modalidad, proyeccionDocenteHora, sueldoBrutoMesCorteDocenteHora, sueldoBrutoRealMesCorte, periodo, modalidad, proyeccionDocenteHora, sueldoBrutoMesCorteDocenteHora, sueldoBrutoRealMesCorte,
...@@ -406,7 +435,22 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -406,7 +435,22 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
otrosEmpleadores.importeAnterior(), otrosEmpleadores.importeMesActual(), otrosEmpleadores.importeAnterior(), otrosEmpleadores.importeMesActual(),
otrosEmpleadores.importeProyeccion(), otrosEmpleadores.importeProyeccion(),
otrosEmpleadores.retencionAnterior(), otrosEmpleadores.retencionMesActual(), otrosEmpleadores.retencionAnterior(), otrosEmpleadores.retencionMesActual(),
otrosEmpleadores.retencionProyeccion())); otrosEmpleadores.retencionProyeccion(),
gratificacionPorContrato, fechaInicioContrato, diasDelMes(fechaInicioContrato),
gratificacionDiciembreCompleta));
}
/**
* 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) {
if (fecha == null) {
return 0;
}
return calendarioRepository.findById(new CalendarioId(fecha.getYear(), fecha.getMonthValue()))
.map(Calendario::getDias)
.orElseGet(() -> YearMonth.from(fecha).lengthOfMonth());
} }
/** /**
......
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