Commit 814987ff by Vanesa Puma Yamo

[modified] se modifica calculo en gratificacion por conceptos correspondientes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parent 68910855
......@@ -64,6 +64,16 @@ so.planilla_be
`detallado`, `boleta`, `historial`) — no usar `periodo`/`mesCorte` en la firma pública aunque
internamente `RentaQuintaFiltroDTO` y `DatosCalculo` sigan llamándolos así.
### Mantenimiento de proyección docente por hora
`POST /api/v1/proyeccion-docente-hora` (paquete `proyecciondocentehora`): recibe una lista de filas
`{proyeccionAnio, proyeccionMes, numeroDocumento, boletaTipoPersonal (DOC|GRA), remunBrutaFutura,
gratificacion}` y las graba **solo** en `planilla.pl_proyeccion_renta_docentehora`. La tabla no tiene
unique salvo el PK identity (`id_proyeccion`, ALWAYS): la clave lógica es documento + tipo + año + mes,
y si existe se actualiza en vez de duplicar. `DELETE /api/v1/proyeccion-docente-hora/{idProyeccion}`
elimina por PK (204; 404 si no existe). `GET /api/v1/proyeccion-docente-hora/{numeroDocumento}/{anio}`
lista las filas del docente con `idProyeccion` y `nombreCompleto` (de `nuevo.personal`, temporal). Todo o nada (una transacción). Importes nulos → 0.
### Filtros de la grilla
`anio` y `mes` son **obligatorios**; el resto es opcional y, si no se envía, no filtra. Todos son
......@@ -81,10 +91,10 @@ 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 |
|---|---|---|---|---|
| `ADM` | `ADM` | — | `MONTO_FIJO` | remuneración de referencia × meses restantes |
| `DOC2` | `DOC` | `2` | `MONTO_FIJO` | igual que ADM |
| `DOC1` | `DOC` | `1` | `DOCENTE_POR_HORA` | `remun_bruta_futura` de `planilla.pl_proyeccion_renta_docentehora` del mes consultado (mismo documento, `boleta_tipo_personal='DOC'`, `proyeccion_anio` = año, `proyeccion_mes` = mes). Sin fila = 0 |
| `DOC2` | `DOC` | `2` | `MONTO_FIJO_DICIEMBRE_PROPORCIONAL` | igual que ADM, pero diciembre (sueldo bruto y gratificación) entra ÷ 30 × 16: en la proyección, en el estimado del mes de corte 12 y en la gratificación de diciembre pendiente. Una boleta real de diciembre no se toca |
| `DOC1` | `DOC` | `1` | `DOCENTE_POR_HORA` | De `planilla.pl_proyeccion_renta_docentehora` (mismo documento, `boleta_tipo_personal='DOC'`, `proyeccion_anio` = año): `remun_bruta_futura` del mes consultado = sueldo bruto de la planilla en curso (reemplaza al de la boleta); suma de `remun_bruta_futura` de los meses **posteriores** = proyección del sueldo bruto. Sin fila = 0. La gratificación de julio/diciembre pendiente es el campo `gratificacion` de las filas `boleta_tipo_personal='GRA'` en `proyeccion_mes` 7 / 12 (sin fórmula × 1.09; sin fila = 0) y va en gratificación ordinaria de la proyección |
Solo cambia la proyección del sueldo; 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
en `docs/circuito-api-renta-quinta.md`.
Los filtros de catálogo (`sede`, `areaGeneral`, `subArea`, `cargo`) llegan como
......
# API de proyección docente por hora
Mantenimiento para **grabar** filas en `planilla.pl_proyeccion_renta_docentehora`. Solo escribe en
esa tabla. La renta de quinta (DOC1) lee luego estos datos para proyectar el sueldo bruto y las
gratificaciones (ver `circuito-api-renta-quinta.md`).
Paquete: `so.planilla_be.proyecciondocentehora`.
## Endpoint
| Método | Ruta | Descripción |
|---|---|---|
| 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`).
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
{
"numeroDocumento": "12345678",
"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
`numero_documento + boleta_tipo_personal + proyeccion_anio + proyeccion_mes`.
## Request
Body: arreglo JSON, no vacío.
| Campo | Tipo | Obligatorio | Regla |
|---|---|---|---|
| `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
[
{
"proyeccionAnio": 2026,
"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
- **Insertar o actualizar:** si ya existe una fila con la clave lógica, se actualizan
`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`
Lista de las filas grabadas, en el mismo orden del request, con su `idProyeccion`.
```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
`PersonalRepository` de retención otros empleadores. Es **temporal**: los docentes vendrán de otra
tabla y solo hay que cambiar `nombreCompleto()` en `ProyeccionDocenteHoraServiceImpl`. Si el DNI no
está en personal, viene `null` y la fila se graba igual.
## Errores
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/
```
package so.planilla_be.proyecciondocentehora.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.proyecciondocentehora.dto.ProyeccionDocenteHoraDTO;
import so.planilla_be.proyecciondocentehora.dto.ProyeccionDocenteHoraListadoDTO;
import so.planilla_be.proyecciondocentehora.dto.ProyeccionDocenteHoraRequestDTO;
import so.planilla_be.proyecciondocentehora.service.ProyeccionDocenteHoraService;
import java.util.List;
@Tag(name = "Proyección docente por hora", description = "Grabado de pl_proyeccion_renta_docentehora")
@Validated
@RestController
@RequestMapping("/api/v1/proyeccion-docente-hora")
public class ProyeccionDocenteHoraController {
private final ProyeccionDocenteHoraService service;
public ProyeccionDocenteHoraController(ProyeccionDocenteHoraService service) {
this.service = service;
}
@Operation(summary = "Filas grabadas de un docente en el año, con su nombre")
@GetMapping("/{numeroDocumento}/{anio}")
public ProyeccionDocenteHoraListadoDTO listarPorAnio(
@PathVariable String numeroDocumento,
@PathVariable Integer anio) {
return service.listarPorAnio(numeroDocumento, anio);
}
@Operation(summary = "Grabar filas de proyección (inserta o actualiza por documento + tipo + año + mes)")
@PostMapping
public List<ProyeccionDocenteHoraDTO> grabar(
@RequestBody @NotEmpty List<@Valid ProyeccionDocenteHoraRequestDTO> filas) {
return service.grabar(filas);
}
@Operation(summary = "Eliminar una fila por id_proyeccion")
@DeleteMapping("/{idProyeccion}")
public ResponseEntity<Void> eliminar(@PathVariable Long idProyeccion) {
service.eliminar(idProyeccion);
return ResponseEntity.noContent().build();
}
}
package so.planilla_be.proyecciondocentehora.dto;
import java.math.BigDecimal;
public record ProyeccionDocenteHoraDTO(
Long idProyeccion,
Integer proyeccionAnio,
Integer proyeccionMes,
String numeroDocumento,
String nombreCompleto,
String boletaTipoPersonal,
BigDecimal remunBrutaFutura,
BigDecimal gratificacion
) {}
package so.planilla_be.proyecciondocentehora.dto;
import java.math.BigDecimal;
public record ProyeccionDocenteHoraItemDTO(
Long idProyeccion,
Integer proyeccionMes,
String boletaTipoPersonal,
BigDecimal remunBrutaFutura,
BigDecimal gratificacion
) {}
package so.planilla_be.proyecciondocentehora.dto;
import java.util.List;
public record ProyeccionDocenteHoraListadoDTO(
String numeroDocumento,
String nombreCompleto,
Integer proyeccionAnio,
List<ProyeccionDocenteHoraItemDTO> data
) {}
package so.planilla_be.proyecciondocentehora.dto;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
/**
* Una fila de pl_proyeccion_renta_docentehora. {@code boletaTipoPersonal}: DOC (remun_bruta_futura)
* o GRA (gratificacion, meses 7 y 12). Importes nulos se graban como 0.
*/
public record ProyeccionDocenteHoraRequestDTO(
@NotNull Integer proyeccionAnio,
@NotNull @Min(1) @Max(12) Integer proyeccionMes,
@NotBlank @Size(max = 15) String numeroDocumento,
@NotBlank @Pattern(regexp = "DOC|GRA", message = "debe ser DOC o GRA") String boletaTipoPersonal,
@PositiveOrZero @Digits(integer = 10, fraction = 2) BigDecimal remunBrutaFutura,
@PositiveOrZero @Digits(integer = 10, fraction = 2) BigDecimal gratificacion
) {}
......@@ -2,6 +2,8 @@ package so.planilla_be.proyecciondocentehora.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
......@@ -12,7 +14,8 @@ import java.math.BigDecimal;
/**
* 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
* proyección del sueldo bruto de renta de quinta. Solo lectura: la carga es externa.
* 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.
*/
@Getter
@Setter
......@@ -20,7 +23,9 @@ import java.math.BigDecimal;
@Table(name = "pl_proyeccion_renta_docentehora")
public class ProyeccionDocenteHora {
/** identity ALWAYS en BD: nunca se envía en el insert. */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id_proyeccion")
private Long idProyeccion;
......@@ -38,4 +43,8 @@ public class ProyeccionDocenteHora {
@Column(name = "remun_bruta_futura", precision = 12, scale = 2)
private BigDecimal remunBrutaFutura;
/** Gratificación proyectada: solo viene en filas boleta_tipo_personal = 'GRA' (meses 7 y 12). */
@Column(name = "gratificacion", precision = 12, scale = 2)
private BigDecimal gratificacion;
}
package so.planilla_be.proyecciondocentehora.exception;
public class ProyeccionDocenteHoraNoEncontradaException extends RuntimeException {
public ProyeccionDocenteHoraNoEncontradaException(String message) {
super(message);
}
}
......@@ -6,12 +6,22 @@ import org.springframework.data.repository.query.Param;
import so.planilla_be.proyecciondocentehora.entity.ProyeccionDocenteHora;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
public interface ProyeccionDocenteHoraRepository extends JpaRepository<ProyeccionDocenteHora, Long> {
/** Filas del docente en el año, por mes y luego por tipo (DOC antes que GRA). */
List<ProyeccionDocenteHora> findByNumeroDocumentoAndProyeccionAnioOrderByProyeccionMesAscBoletaTipoPersonalAsc(
String numeroDocumento, Integer proyeccionAnio);
/** Clave lógica (la tabla solo tiene PK sobre id_proyeccion). */
Optional<ProyeccionDocenteHora> findFirstByNumeroDocumentoAndBoletaTipoPersonalAndProyeccionAnioAndProyeccionMes(
String numeroDocumento, String boletaTipoPersonal, Integer proyeccionAnio, Integer proyeccionMes);
/**
* remun_bruta_futura del docente para el mes consultado: es directamente la proyección del
* sueldo bruto de ese mes de corte. 0 si no hay fila.
* remun_bruta_futura del docente en el mes consultado: es el sueldo bruto de la planilla en
* curso (mes de corte). 0 si no hay fila.
*/
@Query("""
select coalesce(sum(p.remunBrutaFutura), 0)
......@@ -25,4 +35,37 @@ public interface ProyeccionDocenteHoraRepository extends JpaRepository<Proyeccio
@Param("tipoPersonal") String tipoPersonal,
@Param("anio") Integer anio,
@Param("mes") int mes);
/**
* Gratificación proyectada del mes (7 o 12): campo gratificacion de las filas
* boleta_tipo_personal = 'GRA'. 0 si no hay fila.
*/
@Query("""
select coalesce(sum(p.gratificacion), 0)
from ProyeccionDocenteHora p
where p.numeroDocumento = :numeroDocumento
and trim(p.boletaTipoPersonal) = 'GRA'
and p.proyeccionAnio = :anio
and p.proyeccionMes = :mes
""")
BigDecimal gratificacionDelMes(@Param("numeroDocumento") String numeroDocumento,
@Param("anio") Integer anio,
@Param("mes") int mes);
/**
* Suma de remun_bruta_futura de los meses posteriores al consultado: es la proyección
* automatizada del sueldo bruto. 0 si no hay filas.
*/
@Query("""
select coalesce(sum(p.remunBrutaFutura), 0)
from ProyeccionDocenteHora p
where p.numeroDocumento = :numeroDocumento
and trim(p.boletaTipoPersonal) = :tipoPersonal
and p.proyeccionAnio = :anio
and p.proyeccionMes > :mes
""")
BigDecimal sumaRemunBrutaFuturaPosterior(@Param("numeroDocumento") String numeroDocumento,
@Param("tipoPersonal") String tipoPersonal,
@Param("anio") Integer anio,
@Param("mes") int mes);
}
package so.planilla_be.proyecciondocentehora.service;
import so.planilla_be.proyecciondocentehora.dto.ProyeccionDocenteHoraDTO;
import so.planilla_be.proyecciondocentehora.dto.ProyeccionDocenteHoraListadoDTO;
import so.planilla_be.proyecciondocentehora.dto.ProyeccionDocenteHoraRequestDTO;
import java.util.List;
public interface ProyeccionDocenteHoraService {
/** Filas grabadas del docente en el año, con su nombre. Sin filas: data vacía. */
ProyeccionDocenteHoraListadoDTO listarPorAnio(String numeroDocumento, Integer anio);
/**
* Graba las filas recibidas. La clave lógica es documento + boletaTipoPersonal + año + mes:
* si ya existe se actualizan los importes, si no se inserta. Todo o nada.
*/
List<ProyeccionDocenteHoraDTO> grabar(List<ProyeccionDocenteHoraRequestDTO> filas);
/** Elimina la fila por id_proyeccion. Lanza ProyeccionDocenteHoraNoEncontradaException si no existe. */
void eliminar(Long idProyeccion);
}
package so.planilla_be.proyecciondocentehora.service.impl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import so.planilla_be.otrosempleadores.repository.PersonalRepository;
import so.planilla_be.proyecciondocentehora.dto.ProyeccionDocenteHoraDTO;
import so.planilla_be.proyecciondocentehora.dto.ProyeccionDocenteHoraItemDTO;
import so.planilla_be.proyecciondocentehora.dto.ProyeccionDocenteHoraListadoDTO;
import so.planilla_be.proyecciondocentehora.dto.ProyeccionDocenteHoraRequestDTO;
import so.planilla_be.proyecciondocentehora.entity.ProyeccionDocenteHora;
import so.planilla_be.proyecciondocentehora.exception.ProyeccionDocenteHoraNoEncontradaException;
import so.planilla_be.proyecciondocentehora.repository.ProyeccionDocenteHoraRepository;
import so.planilla_be.proyecciondocentehora.service.ProyeccionDocenteHoraService;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class ProyeccionDocenteHoraServiceImpl implements ProyeccionDocenteHoraService {
private final ProyeccionDocenteHoraRepository repository;
private final PersonalRepository personalRepository;
public ProyeccionDocenteHoraServiceImpl(ProyeccionDocenteHoraRepository repository,
PersonalRepository personalRepository) {
this.repository = repository;
this.personalRepository = personalRepository;
}
@Override
@Transactional(readOnly = true)
public ProyeccionDocenteHoraListadoDTO listarPorAnio(String numeroDocumento, Integer anio) {
String documento = numeroDocumento.trim();
List<ProyeccionDocenteHoraItemDTO> data = repository
.findByNumeroDocumentoAndProyeccionAnioOrderByProyeccionMesAscBoletaTipoPersonalAsc(documento, anio)
.stream()
.map(e -> new ProyeccionDocenteHoraItemDTO(
e.getIdProyeccion(),
e.getProyeccionMes(),
e.getBoletaTipoPersonal(),
e.getRemunBrutaFutura(),
e.getGratificacion()))
.toList();
return new ProyeccionDocenteHoraListadoDTO(documento, nombreCompleto(documento), anio, data);
}
@Override
@Transactional
public List<ProyeccionDocenteHoraDTO> grabar(List<ProyeccionDocenteHoraRequestDTO> filas) {
Map<String, String> nombres = new HashMap<>();
return filas.stream()
.map(fila -> grabarFila(fila, nombres))
.toList();
}
@Override
@Transactional
public void eliminar(Long idProyeccion) {
ProyeccionDocenteHora entidad = repository.findById(idProyeccion)
.orElseThrow(() -> new ProyeccionDocenteHoraNoEncontradaException(
"No existe proyección docente por hora con id %d".formatted(idProyeccion)));
repository.delete(entidad);
}
/**
* Nombre por DNI desde la tabla de personal, reutilizando el repositorio de otros empleadores.
* Temporal: los docentes vendrán de otra tabla; cambiar solo este método. null si no se halla.
*/
private String nombreCompleto(String numeroDocumento) {
return personalRepository.findFirstByNumeroDocumentoOrderByCodperDesc(numeroDocumento)
.map(p -> "%s %s".formatted(
p.getApellido() == null ? "" : p.getApellido().trim(),
p.getNombre() == null ? "" : p.getNombre().trim()).trim())
.filter(nombre -> !nombre.isBlank())
.orElse(null);
}
private ProyeccionDocenteHoraDTO grabarFila(ProyeccionDocenteHoraRequestDTO fila,
Map<String, String> nombres) {
String documento = fila.numeroDocumento().trim();
ProyeccionDocenteHora entidad = repository
.findFirstByNumeroDocumentoAndBoletaTipoPersonalAndProyeccionAnioAndProyeccionMes(
documento, fila.boletaTipoPersonal(), fila.proyeccionAnio(), fila.proyeccionMes())
.orElseGet(ProyeccionDocenteHora::new);
entidad.setNumeroDocumento(documento);
entidad.setBoletaTipoPersonal(fila.boletaTipoPersonal());
entidad.setProyeccionAnio(fila.proyeccionAnio());
entidad.setProyeccionMes(fila.proyeccionMes());
entidad.setRemunBrutaFutura(cero(fila.remunBrutaFutura()));
entidad.setGratificacion(cero(fila.gratificacion()));
String nombre = nombres.computeIfAbsent(documento, this::nombreCompleto);
return toDto(repository.save(entidad), nombre);
}
private BigDecimal cero(BigDecimal valor) {
return valor == null ? BigDecimal.ZERO : valor;
}
private ProyeccionDocenteHoraDTO toDto(ProyeccionDocenteHora e, String nombreCompleto) {
return new ProyeccionDocenteHoraDTO(
e.getIdProyeccion(),
e.getProyeccionAnio(),
e.getProyeccionMes(),
e.getNumeroDocumento(),
nombreCompleto,
e.getBoletaTipoPersonal(),
e.getRemunBrutaFutura(),
e.getGratificacion());
}
}
......@@ -10,6 +10,8 @@ public record CalculoRentaQuinta(
BigDecimal proyeccionGratificaciones,
BigDecimal estimadoMesCorteSueldoBruto,
BigDecimal estimadoMesCorteGratificacion,
/** DOC1 con boleta real del mes de corte: remun_bruta_futura − sueldo bruto real (0 en el resto). */
BigDecimal ajusteSueldoBrutoMesCorte,
BigDecimal rentaBrutaAnual,
BigDecimal rentaBrutaAnualSinOtrosEmpleadores,
BigDecimal rentaNeta,
......
......@@ -7,7 +7,7 @@ package so.planilla_be.rentaquinta.calculo;
* <table>
* <tr><th>boleta_tipo_personal</th><th>tipo_pago</th><th>Modalidad</th></tr>
* <tr><td>ADM</td><td>(cualquiera)</td><td>{@link #MONTO_FIJO}</td></tr>
* <tr><td>DOC</td><td>2</td><td>{@link #MONTO_FIJO}</td></tr>
* <tr><td>DOC</td><td>2</td><td>{@link #MONTO_FIJO_DICIEMBRE_PROPORCIONAL}</td></tr>
* <tr><td>DOC</td><td>1</td><td>{@link #DOCENTE_POR_HORA}</td></tr>
* </table>
*
......@@ -24,12 +24,21 @@ public enum ModalidadCalculo {
* Proyección = {@code remun_bruta_futura} de pl_proyeccion_renta_docentehora del mes
* consultado. El resto del cálculo es igual que {@link #MONTO_FIJO}.
*/
DOCENTE_POR_HORA;
DOCENTE_POR_HORA,
/**
* DOC tipo_pago 2: igual que {@link #MONTO_FIJO}, pero el sueldo bruto y la
* gratificación de diciembre son proporcionales: monto ÷ 30 × 16.
*/
MONTO_FIJO_DICIEMBRE_PROPORCIONAL;
public static ModalidadCalculo de(TipoPlanilla tipoPlanilla, TipoPago tipoPago) {
return switch (tipoPlanilla) {
case MENSUAL_MONTO_FIJO -> MONTO_FIJO;
case DOCENTE -> tipoPago == TipoPago.POR_HORA ? DOCENTE_POR_HORA : MONTO_FIJO;
case DOCENTE -> tipoPago == null ? MONTO_FIJO : switch (tipoPago) {
case POR_HORA -> DOCENTE_POR_HORA;
case MONTO_FIJO -> MONTO_FIJO_DICIEMBRE_PROPORCIONAL;
};
};
}
}
......@@ -37,6 +37,10 @@ public class RentaQuintaCalculadora {
/** Bonificación extraordinaria (ex-EsSalud) que acompaña a la gratificación. */
private static final BigDecimal FACTOR_GRATIFICACION = new BigDecimal("1.09");
/** DOC2: diciembre se paga proporcional, monto ÷ 30 × 16. */
private static final BigDecimal DIAS_MES = BigDecimal.valueOf(30);
private static final BigDecimal DIAS_DICIEMBRE_PROPORCIONAL = BigDecimal.valueOf(16);
private final UitCache uit;
public RentaQuintaCalculadora(UitCache uit) {
......@@ -51,12 +55,21 @@ public class RentaQuintaCalculadora {
// sueldo bruto del mes de referencia y, si el mes de corte es julio o diciembre, su
// gratificación (109% del sueldo bruto). Este estimado reemplaza a planillaCargada (que
// queda en 0 por no ser dato real) en la renta bruta anual.
// DOC1: el sueldo bruto del mes de corte es remun_bruta_futura de ese mes, con o sin boleta.
boolean porHora = datos.modalidad() == ModalidadCalculo.DOCENTE_POR_HORA;
BigDecimal ajusteSueldoBrutoMesCorte = porHora && mesCorteReal
? orZero(datos.sueldoBrutoMesCorteDocenteHora()).subtract(orZero(datos.sueldoBrutoRealMesCorte()))
: BigDecimal.ZERO;
BigDecimal estimadoSueldoBruto = BigDecimal.ZERO;
BigDecimal estimadoGratificacion = BigDecimal.ZERO;
if (!mesCorteReal) {
estimadoSueldoBruto = orZero(datos.remuneracionReferencia());
estimadoSueldoBruto = porHora ? orZero(datos.sueldoBrutoMesCorteDocenteHora())
: montoDelMes(datos, orZero(datos.remuneracionReferencia()), datos.mesCorte());
// DOC2 en diciembre: la gratificación sale del sueldo ya proporcional (÷ 30 × 16).
if (datos.mesCorte() == 7 || datos.mesCorte() == 12) {
estimadoGratificacion = gratificacion(estimadoSueldoBruto);
estimadoGratificacion = porHora
? gratificacionDocenteHora(datos, datos.mesCorte())
: gratificacion(estimadoSueldoBruto);
}
}
BigDecimal estimadoMesCorte = estimadoSueldoBruto.add(estimadoGratificacion);
......@@ -84,6 +97,7 @@ public class RentaQuintaCalculadora {
// el total con otros empleadores incluidos (art. 40 y 53 LIR).
BigDecimal rentaBrutaAnualSinOtrosEmpleadores = orZero(datos.historialReal())
.add(orZero(datos.planillaCargada()))
.add(ajusteSueldoBrutoMesCorte)
.add(estimadoMesCorte)
.add(proyeccionFutura);
BigDecimal rentaBrutaAnual = rentaBrutaAnualSinOtrosEmpleadores
......@@ -113,6 +127,7 @@ public class RentaQuintaCalculadora {
escala(proyeccionGratificaciones),
escala(estimadoSueldoBruto),
escala(estimadoGratificacion),
escala(ajusteSueldoBrutoMesCorte),
escala(rentaBrutaAnual),
escala(rentaBrutaAnualSinOtrosEmpleadores),
escala(rentaNeta),
......@@ -138,29 +153,59 @@ public class RentaQuintaCalculadora {
* tiene registro; si no, básico + asignación familiar de pl_planilla.
*/
private BigDecimal gratificacionesPendientes(DatosCalculo datos, boolean julioCubierto, boolean diciembreCubierto) {
boolean porHora = datos.modalidad() == ModalidadCalculo.DOCENTE_POR_HORA;
BigDecimal gratificacion = gratificacion(orZero(datos.basicoReferencia()));
BigDecimal total = BigDecimal.ZERO;
if (!julioCubierto) {
total = total.add(gratificacion);
total = total.add(porHora ? gratificacionDocenteHora(datos, 7) : gratificacion);
}
if (!diciembreCubierto) {
total = total.add(gratificacion);
total = total.add(porHora ? gratificacionDocenteHora(datos, 12) : montoDelMes(datos, gratificacion, 12));
}
return total;
}
/**
* DOC2 ({@link ModalidadCalculo#MONTO_FIJO_DICIEMBRE_PROPORCIONAL}): el sueldo bruto y la
* gratificación de diciembre son proporcionales, monto ÷ 30 × 16. Cualquier otro mes o
* modalidad devuelve el monto sin cambios.
*/
private BigDecimal montoDelMes(DatosCalculo datos, BigDecimal monto, int mes) {
if (mes != 12 || datos.modalidad() != ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL) {
return monto;
}
return monto.multiply(DIAS_DICIEMBRE_PROPORCIONAL).divide(DIAS_MES, 10, RoundingMode.HALF_UP);
}
/**
* 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.
*/
private BigDecimal gratificacionDocenteHora(DatosCalculo datos, int mes) {
return orZero(mes == 7 ? datos.gratificacionJulioDocenteHora() : datos.gratificacionDiciembreDocenteHora());
}
/**
* CONDICIÓN MODALIDAD — sueldo bruto de los meses que faltan después del mes de corte:
* <ul>
* <li>ADM y DOC tipo_pago 2: remuneración del mes de referencia × meses restantes.</li>
* <li>DOC tipo_pago 1 (por hora): remun_bruta_futura del mes consultado en
* pl_proyeccion_renta_docentehora ({@code proyeccionDocenteHora}).</li>
* <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>
* <li>DOC tipo_pago 2: igual que ADM, pero diciembre entra proporcional (÷ 30 × 16).</li>
* </ul>
*/
private BigDecimal proyeccionSueldoBruto(DatosCalculo datos) {
return switch (datos.modalidad()) {
case MONTO_FIJO -> orZero(datos.remuneracionReferencia())
.multiply(BigDecimal.valueOf(12 - datos.mesCorte()));
case MONTO_FIJO_DICIEMBRE_PROPORCIONAL -> {
if (datos.mesCorte() >= 12) {
yield BigDecimal.ZERO;
}
BigDecimal remuneracion = orZero(datos.remuneracionReferencia());
yield remuneracion.multiply(BigDecimal.valueOf(11 - datos.mesCorte()))
.add(montoDelMes(datos, remuneracion, 12));
}
case DOCENTE_POR_HORA -> orZero(datos.proyeccionDocenteHora());
};
}
......@@ -245,13 +290,20 @@ public class RentaQuintaCalculadora {
* la base de la proyección cuando el mes de corte todavía no tiene boleta.
*
* <p>{@code modalidad} decide cómo se proyecta el sueldo bruto (ver {@link ModalidadCalculo});
* {@code proyeccionDocenteHora} solo se usa en {@link ModalidadCalculo#DOCENTE_POR_HORA}. La
* {@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
* {@link ModalidadCalculo#DOCENTE_POR_HORA}; {@code sueldoBrutoRealMesCorte} es el sueldo
* bruto de la boleta real del mes de corte que ese monto reemplaza. La
* UIT, las 7 UIT, los tramos y el divisor son de ley e iguales para todas las planillas.
*/
public record DatosCalculo(
Integer periodo,
ModalidadCalculo modalidad,
BigDecimal proyeccionDocenteHora,
BigDecimal sueldoBrutoMesCorteDocenteHora,
BigDecimal sueldoBrutoRealMesCorte,
BigDecimal gratificacionJulioDocenteHora,
BigDecimal gratificacionDiciembreDocenteHora,
int mesCorte,
int mesReferencia,
BigDecimal historialReal,
......
......@@ -9,7 +9,8 @@ import java.util.Arrays;
* <ul>
* <li>{@code ADM} → cálculo normal (administrativo)</li>
* <li>{@code DOC1} → docente costo por hora, proyección = remun_bruta_futura del mes consultado</li>
* <li>{@code DOC2} → docente monto fijo, mismo cálculo que ADM</li>
* <li>{@code DOC2} → docente monto fijo, mismo cálculo que ADM salvo diciembre: sueldo bruto
* y gratificación de diciembre proporcionales (÷ 30 × 16)</li>
* </ul>
*
* {@code DOC} sin dígito no se acepta. Sin {@code tipoPlanilla} se toma el {@code tipo_pago} de la
......@@ -30,6 +31,18 @@ public enum TipoPago {
return codigo;
}
/** Tipo de pago de una boleta ({@code tipo_pago} char(1), con padding). {@code null} si no aplica. */
public static TipoPago desdeCodigo(String codigo) {
if (codigo == null) {
return null;
}
String normalizado = codigo.trim();
return Arrays.stream(values())
.filter(t -> t.codigo.equals(normalizado))
.findFirst()
.orElse(null);
}
/**
* Tipo de pago contenido en el parámetro {@code tipoPlanilla} ({@code DOC1} / {@code DOC2}).
* {@code null} si no trae sufijo ({@code ADM}, {@code DOC}, vacío) o si el sufijo no es válido;
......
......@@ -35,7 +35,7 @@ public class RentaQuintaController {
/** 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 "
+ "costo por hora (proyección desde pl_proyeccion_renta_docentehora); DOC2 = docente monto fijo "
+ "(mismo cálculo que ADM). Filtra por boleta_tipo_personal y tipo_pago. Si no se envía, se "
+ "(mismo cálculo que ADM, con sueldo bruto y gratificación de diciembre ÷ 30 × 16). Filtra por boleta_tipo_personal y tipo_pago. Si no se envía, se "
+ "toma de la boleta del trabajador.";
private static final String TIPO_PLANILLA_GRABAR_DOC = "Obligatorio. ADM, DOC1 (costo por hora) o DOC2 "
......
......@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import so.planilla_be.otrosempleadores.exception.RetencionOtrosEmpleadoresNoEncontradaException;
import so.planilla_be.otrosempleadores.exception.RetencionOtrosEmpleadoresYaExisteException;
import so.planilla_be.proyecciondocentehora.exception.ProyeccionDocenteHoraNoEncontradaException;
import java.time.Instant;
import java.util.Map;
......@@ -28,6 +29,11 @@ public class GlobalExceptionHandler {
return build(HttpStatus.NOT_FOUND, ex.getMessage());
}
@ExceptionHandler(ProyeccionDocenteHoraNoEncontradaException.class)
public ResponseEntity<Map<String, Object>> handleNotFound(ProyeccionDocenteHoraNoEncontradaException ex) {
return build(HttpStatus.NOT_FOUND, ex.getMessage());
}
@ExceptionHandler(RetencionOtrosEmpleadoresYaExisteException.class)
public ResponseEntity<Map<String, Object>> handleConflict(RetencionOtrosEmpleadoresYaExisteException ex) {
return build(HttpStatus.CONFLICT, ex.getMessage());
......
......@@ -90,7 +90,10 @@ public class RentaQuintaMapper {
// Sin boleta real del mes de corte, se estima con sueldo bruto del mes de referencia
// (+ gratificación 109% si el mes de corte es julio o diciembre).
BloqueIngresosDTO actual = mesCorteReal
? conOtrosEmpleadores(bloque(historialAnio, r -> r.getBoletaMes().equals(mesCorte)), calculo.otrosEmpleadoresImporteMesActual())
? conOtrosEmpleadores(
conAjusteSueldoBruto(bloque(historialAnio, r -> r.getBoletaMes().equals(mesCorte)),
calculo.ajusteSueldoBrutoMesCorte()),
calculo.otrosEmpleadoresImporteMesActual())
: new BloqueIngresosDTO(
calculo.estimadoMesCorteSueldoBruto(),
BigDecimal.ZERO,
......@@ -174,6 +177,11 @@ public class RentaQuintaMapper {
return bloque(historial, filtro).totalBrutoRemunerativo();
}
/** Solo el componente "sueldo bruto" de {@link #totalBrutoMes}. */
public BigDecimal sueldoBrutoMes(List<RentaQuinta> historial, Predicate<RentaQuinta> filtro) {
return bloque(historial, filtro).sueldoBruto();
}
private BloqueIngresosDTO bloque(List<RentaQuinta> historial, Predicate<RentaQuinta> filtro) {
BigDecimal sueldoBruto = BigDecimal.ZERO;
BigDecimal horasExtras = BigDecimal.ZERO;
......@@ -206,21 +214,32 @@ public class RentaQuintaMapper {
.add(orZero(r.getValeConsumo()))
.add(orZero(r.getObsequios()))
.add(orZero(r.getCanastaNavidad2()))
.add(orZero(r.getValeConsumoDescuento()));
gratificaciones = gratificaciones
.add(orZero(r.getValeConsumoDescuento()))
.add(orZero(r.getCompensacionVacacional()))
.add(orZero(r.getGratificacionProporcional()))
.add(orZero(r.getVacacionesTruncas()))
.add(orZero(r.getSumaGraciosa()))
.add(orZero(r.getBonificacionExtraordinaria()))
.add(orZero(r.getBonificacionExtraordinaria2()))
.add(orZero(r.getGratificacionJulioDiciembre()));
.add(orZero(r.getBonificacionExtraordinaria()));
gratificaciones = gratificaciones
.add(orZero(r.getGratificacionJulioDiciembre()))
.add(orZero(r.getBonificacionExtraordinaria2()));
utilidades = utilidades.add(orZero(r.getUtilidades()));
}
BigDecimal total = sueldoBruto.add(horasExtras).add(gratificaciones).add(utilidades);
return new BloqueIngresosDTO(sueldoBruto, horasExtras, gratificaciones, utilidades, BigDecimal.ZERO, total);
}
/** DOC1: reemplaza el sueldo bruto real del mes de corte por remun_bruta_futura (suma el delta). */
private BloqueIngresosDTO conAjusteSueldoBruto(BloqueIngresosDTO bloque, BigDecimal ajuste) {
return new BloqueIngresosDTO(
bloque.sueldoBruto().add(ajuste),
bloque.horasExtras(),
bloque.gratificacionesOrdinarias(),
bloque.utilidades(),
bloque.otrosEmpleadores(),
bloque.totalBrutoRemunerativo().add(ajuste));
}
private BloqueIngresosDTO conHistorialRealYOtrosEmpleadores(BloqueIngresosDTO bloque, BigDecimal historialReal,
BigDecimal otrosEmpleadores) {
return new BloqueIngresosDTO(
......
......@@ -19,7 +19,7 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
String FILTROS = """
and (:sede is null or trim(r.codlocal) = :sede)
and (:tipoPlanilla is null or trim(r.boletaTipoPersonal) = :tipoPlanilla)
and (:tipoPago is null or trim(r.tipoPago) = :tipoPago)
and (:tipoPago is null or coalesce(trim(r.tipoPago), '') = :tipoPago)
and (:areaGeneral is null or r.areaGeneral = :areaGeneral)
and (:subArea is null or r.subArea = :subArea)
and (:cargo is null or r.cargo = :cargo)
......
......@@ -100,7 +100,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
filtro.mesCorte(),
codigo(filtro.sede()),
codigo(filtro.tipoPlanilla()), // CONDICIÓN TIPO PLANILLA: filtra por boleta_tipo_personal
codigo(filtro.tipoPago()), // y por tipo_pago (solo DOC)
codigoTipoPago(filtro), // y por tipo_pago (ADM vacío, DOC 1/2)
codigo(filtro.areaGeneral()),
codigo(filtro.subArea()),
codigo(filtro.cargo()),
......@@ -127,7 +127,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
filtro.tipoPago());
BigDecimal planillaCargada = (a.mesCorteReal()
? a.planillaCargadaReal()
? a.planillaCargadaReal().add(calculo.ajusteSueldoBrutoMesCorte())
: calculo.estimadoMesCorteSueldoBruto().add(calculo.estimadoMesCorteGratificacion()))
.add(calculo.otrosEmpleadoresImporteMesActual());
Integer codper = historial.stream()
......@@ -159,9 +159,15 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
return tipoPlanilla == null ? null : tipoPlanilla.getCodigo();
}
/** tipo_pago es char(1): el repository lo compara con trim() contra este código. */
private String codigo(TipoPago tipoPago) {
return tipoPago == null ? null : tipoPago.getCodigo();
/**
* 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) {
if (filtro.tipoPlanilla() == TipoPlanilla.MENSUAL_MONTO_FIJO) {
return "";
}
return filtro.tipoPago() == null ? null : filtro.tipoPago().getCodigo();
}
/**
......@@ -292,7 +298,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
do {
pagina = repository.buscarDocumentos(
filtro.periodo(), filtro.mesCorte(),
codigo(filtro.sede()), codigo(filtro.tipoPlanilla()), codigo(filtro.tipoPago()),
codigo(filtro.sede()), codigo(filtro.tipoPlanilla()), codigoTipoPago(filtro),
codigo(filtro.areaGeneral()),
codigo(filtro.subArea()), codigo(filtro.cargo()), patron(filtro.documentoOApellidos()),
PageRequest.of(page, GRABADO_MASIVO_BATCH));
......@@ -370,13 +376,30 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
OtrosEmpleadoresAgregado otrosEmpleadores = otrosEmpleadores(numeroDocumento, periodo, mesCorte);
ModalidadCalculo modalidad = modalidad(tipoPlanilla, tipoPago, historial, mesReferencia);
BigDecimal proyeccionDocenteHora = 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
// de los meses posteriores = proyección del sueldo bruto.
BigDecimal sueldoBrutoMesCorteDocenteHora = porHora
? proyeccionDocenteHoraRepository.remunBrutaFutura(
numeroDocumento, tipoPlanilla.getCodigo(), periodo, mesCorte)
: BigDecimal.ZERO;
BigDecimal proyeccionDocenteHora = porHora
? proyeccionDocenteHoraRepository.sumaRemunBrutaFuturaPosterior(
numeroDocumento, tipoPlanilla.getCodigo(), periodo, mesCorte)
: BigDecimal.ZERO;
BigDecimal gratificacionJulioDocenteHora = porHora
? proyeccionDocenteHoraRepository.gratificacionDelMes(numeroDocumento, periodo, 7)
: BigDecimal.ZERO;
BigDecimal gratificacionDiciembreDocenteHora = porHora
? proyeccionDocenteHoraRepository.gratificacionDelMes(numeroDocumento, periodo, 12)
: BigDecimal.ZERO;
BigDecimal sueldoBrutoRealMesCorte = a.mesCorteReal()
? mapper.sueldoBrutoMes(historial, esMesCorte(mesCorte))
: BigDecimal.ZERO;
return calculadora.calcular(new DatosCalculo(
periodo, modalidad, proyeccionDocenteHora, mesCorte, mesReferencia, a.historialReal(),
periodo, modalidad, proyeccionDocenteHora, sueldoBrutoMesCorteDocenteHora, sueldoBrutoRealMesCorte,
gratificacionJulioDocenteHora, gratificacionDiciembreDocenteHora, mesCorte, mesReferencia, a.historialReal(),
a.planillaCargadaReal(),
sueldoBrutoReferencia, basicoReferencia, asignacionFamiliarReferencia, a.retenidoAnterior(),
a.julioPercibida(), a.diciembrePercibida(),
......@@ -390,21 +413,22 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
* CONDICIÓN MODALIDAD — ADM siempre es monto fijo. En DOC decide el {@code tipoPago} enviado
* por el cliente; si no lo envió, el {@code tipo_pago} de las boletas DOC del mes de
* referencia (una persona puede tener filas ADM y DOC en el mismo mes): si alguna es 1 es
* docente por hora; si no, monto fijo.
* docente por hora; si alguna es 2, monto fijo con diciembre proporcional; si no, monto fijo.
*/
private ModalidadCalculo modalidad(TipoPlanilla tipoPlanilla, TipoPago solicitado, List<RentaQuinta> historial,
int mesReferencia) {
if (solicitado != null) {
return ModalidadCalculo.de(tipoPlanilla, solicitado);
}
boolean porHora = historial.stream()
List<TipoPago> tiposPago = historial.stream()
.filter(esMesCorte(mesReferencia))
.filter(r -> TipoPlanilla.deBoleta(r) == TipoPlanilla.DOCENTE)
.map(RentaQuinta::getTipoPago)
.map(r -> TipoPago.desdeCodigo(r.getTipoPago()))
.filter(Objects::nonNull)
.map(String::trim)
.anyMatch(TipoPago.POR_HORA.getCodigo()::equals);
return ModalidadCalculo.de(tipoPlanilla, porHora ? TipoPago.POR_HORA : null);
.toList();
TipoPago deBoleta = tiposPago.contains(TipoPago.POR_HORA) ? TipoPago.POR_HORA
: tiposPago.contains(TipoPago.MONTO_FIJO) ? TipoPago.MONTO_FIJO : null;
return ModalidadCalculo.de(tipoPlanilla, deBoleta);
}
/**
......
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