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 ...@@ -64,6 +64,16 @@ so.planilla_be
`detallado`, `boleta`, `historial`) — no usar `periodo`/`mesCorte` en la firma pública aunque `detallado`, `boleta`, `historial`) — no usar `periodo`/`mesCorte` en la firma pública aunque
internamente `RentaQuintaFiltroDTO` y `DatosCalculo` sigan llamándolos así. 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 ### Filtros de la grilla
`anio` y `mes` son **obligatorios**; el resto es opcional y, si no se envía, no filtra. Todos son `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 ...@@ -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 | | 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` | igual que ADM | | `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` | `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 | | `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`. en `docs/circuito-api-renta-quinta.md`.
Los filtros de catálogo (`sede`, `areaGeneral`, `subArea`, `cargo`) llegan como 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; ...@@ -2,6 +2,8 @@ package so.planilla_be.proyecciondocentehora.entity;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
...@@ -12,7 +14,8 @@ import java.math.BigDecimal; ...@@ -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 * 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_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 @Getter
@Setter @Setter
...@@ -20,7 +23,9 @@ import java.math.BigDecimal; ...@@ -20,7 +23,9 @@ import java.math.BigDecimal;
@Table(name = "pl_proyeccion_renta_docentehora") @Table(name = "pl_proyeccion_renta_docentehora")
public class ProyeccionDocenteHora { public class ProyeccionDocenteHora {
/** identity ALWAYS en BD: nunca se envía en el insert. */
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id_proyeccion") @Column(name = "id_proyeccion")
private Long idProyeccion; private Long idProyeccion;
...@@ -38,4 +43,8 @@ public class ProyeccionDocenteHora { ...@@ -38,4 +43,8 @@ public class ProyeccionDocenteHora {
@Column(name = "remun_bruta_futura", precision = 12, scale = 2) @Column(name = "remun_bruta_futura", precision = 12, scale = 2)
private BigDecimal remunBrutaFutura; 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; ...@@ -6,12 +6,22 @@ import org.springframework.data.repository.query.Param;
import so.planilla_be.proyecciondocentehora.entity.ProyeccionDocenteHora; import so.planilla_be.proyecciondocentehora.entity.ProyeccionDocenteHora;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List;
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). */
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 * remun_bruta_futura del docente en el mes consultado: es el sueldo bruto de la planilla en
* sueldo bruto de ese mes de corte. 0 si no hay fila. * curso (mes de corte). 0 si no hay fila.
*/ */
@Query(""" @Query("""
select coalesce(sum(p.remunBrutaFutura), 0) select coalesce(sum(p.remunBrutaFutura), 0)
...@@ -25,4 +35,37 @@ public interface ProyeccionDocenteHoraRepository extends JpaRepository<Proyeccio ...@@ -25,4 +35,37 @@ public interface ProyeccionDocenteHoraRepository extends JpaRepository<Proyeccio
@Param("tipoPersonal") String tipoPersonal, @Param("tipoPersonal") String tipoPersonal,
@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
* 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( ...@@ -10,6 +10,8 @@ public record CalculoRentaQuinta(
BigDecimal proyeccionGratificaciones, BigDecimal proyeccionGratificaciones,
BigDecimal estimadoMesCorteSueldoBruto, BigDecimal estimadoMesCorteSueldoBruto,
BigDecimal estimadoMesCorteGratificacion, 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 rentaBrutaAnual,
BigDecimal rentaBrutaAnualSinOtrosEmpleadores, BigDecimal rentaBrutaAnualSinOtrosEmpleadores,
BigDecimal rentaNeta, BigDecimal rentaNeta,
......
...@@ -7,7 +7,7 @@ package so.planilla_be.rentaquinta.calculo; ...@@ -7,7 +7,7 @@ package so.planilla_be.rentaquinta.calculo;
* <table> * <table>
* <tr><th>boleta_tipo_personal</th><th>tipo_pago</th><th>Modalidad</th></tr> * <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>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> * <tr><td>DOC</td><td>1</td><td>{@link #DOCENTE_POR_HORA}</td></tr>
* </table> * </table>
* *
...@@ -24,12 +24,21 @@ public enum ModalidadCalculo { ...@@ -24,12 +24,21 @@ public enum ModalidadCalculo {
* Proyección = {@code remun_bruta_futura} de pl_proyeccion_renta_docentehora del mes * 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}. * 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) { public static ModalidadCalculo de(TipoPlanilla tipoPlanilla, TipoPago tipoPago) {
return switch (tipoPlanilla) { return switch (tipoPlanilla) {
case MENSUAL_MONTO_FIJO -> MONTO_FIJO; 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 { ...@@ -37,6 +37,10 @@ public class RentaQuintaCalculadora {
/** Bonificación extraordinaria (ex-EsSalud) que acompaña a la gratificación. */ /** Bonificación extraordinaria (ex-EsSalud) que acompaña a la gratificación. */
private static final BigDecimal FACTOR_GRATIFICACION = new BigDecimal("1.09"); 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; private final UitCache uit;
public RentaQuintaCalculadora(UitCache uit) { public RentaQuintaCalculadora(UitCache uit) {
...@@ -51,12 +55,21 @@ public class RentaQuintaCalculadora { ...@@ -51,12 +55,21 @@ public class RentaQuintaCalculadora {
// sueldo bruto del mes de referencia y, si el mes de corte es julio o diciembre, su // 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 // 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. // 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 estimadoSueldoBruto = BigDecimal.ZERO;
BigDecimal estimadoGratificacion = BigDecimal.ZERO; BigDecimal estimadoGratificacion = BigDecimal.ZERO;
if (!mesCorteReal) { 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) { if (datos.mesCorte() == 7 || datos.mesCorte() == 12) {
estimadoGratificacion = gratificacion(estimadoSueldoBruto); estimadoGratificacion = porHora
? gratificacionDocenteHora(datos, datos.mesCorte())
: gratificacion(estimadoSueldoBruto);
} }
} }
BigDecimal estimadoMesCorte = estimadoSueldoBruto.add(estimadoGratificacion); BigDecimal estimadoMesCorte = estimadoSueldoBruto.add(estimadoGratificacion);
...@@ -84,6 +97,7 @@ public class RentaQuintaCalculadora { ...@@ -84,6 +97,7 @@ public class RentaQuintaCalculadora {
// el total con otros empleadores incluidos (art. 40 y 53 LIR). // el total con otros empleadores incluidos (art. 40 y 53 LIR).
BigDecimal rentaBrutaAnualSinOtrosEmpleadores = orZero(datos.historialReal()) BigDecimal rentaBrutaAnualSinOtrosEmpleadores = orZero(datos.historialReal())
.add(orZero(datos.planillaCargada())) .add(orZero(datos.planillaCargada()))
.add(ajusteSueldoBrutoMesCorte)
.add(estimadoMesCorte) .add(estimadoMesCorte)
.add(proyeccionFutura); .add(proyeccionFutura);
BigDecimal rentaBrutaAnual = rentaBrutaAnualSinOtrosEmpleadores BigDecimal rentaBrutaAnual = rentaBrutaAnualSinOtrosEmpleadores
...@@ -113,6 +127,7 @@ public class RentaQuintaCalculadora { ...@@ -113,6 +127,7 @@ public class RentaQuintaCalculadora {
escala(proyeccionGratificaciones), escala(proyeccionGratificaciones),
escala(estimadoSueldoBruto), escala(estimadoSueldoBruto),
escala(estimadoGratificacion), escala(estimadoGratificacion),
escala(ajusteSueldoBrutoMesCorte),
escala(rentaBrutaAnual), escala(rentaBrutaAnual),
escala(rentaBrutaAnualSinOtrosEmpleadores), escala(rentaBrutaAnualSinOtrosEmpleadores),
escala(rentaNeta), escala(rentaNeta),
...@@ -138,29 +153,59 @@ public class RentaQuintaCalculadora { ...@@ -138,29 +153,59 @@ public class RentaQuintaCalculadora {
* tiene registro; si no, básico + asignación familiar de pl_planilla. * tiene registro; si no, básico + asignación familiar de pl_planilla.
*/ */
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;
BigDecimal gratificacion = gratificacion(orZero(datos.basicoReferencia())); BigDecimal gratificacion = gratificacion(orZero(datos.basicoReferencia()));
BigDecimal total = BigDecimal.ZERO; BigDecimal total = BigDecimal.ZERO;
if (!julioCubierto) { if (!julioCubierto) {
total = total.add(gratificacion); total = total.add(porHora ? gratificacionDocenteHora(datos, 7) : gratificacion);
} }
if (!diciembreCubierto) { if (!diciembreCubierto) {
total = total.add(gratificacion); total = total.add(porHora ? gratificacionDocenteHora(datos, 12) : montoDelMes(datos, gratificacion, 12));
} }
return total; 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: * CONDICIÓN MODALIDAD — sueldo bruto de los meses que faltan después del mes de corte:
* <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): remun_bruta_futura del mes consultado en * <li>DOC tipo_pago 1 (por hora): suma de remun_bruta_futura de los meses posteriores al
* pl_proyeccion_renta_docentehora ({@code proyeccionDocenteHora}).</li> * 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> * </ul>
*/ */
private BigDecimal proyeccionSueldoBruto(DatosCalculo datos) { private BigDecimal proyeccionSueldoBruto(DatosCalculo datos) {
return switch (datos.modalidad()) { return switch (datos.modalidad()) {
case MONTO_FIJO -> orZero(datos.remuneracionReferencia()) case MONTO_FIJO -> orZero(datos.remuneracionReferencia())
.multiply(BigDecimal.valueOf(12 - datos.mesCorte())); .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()); case DOCENTE_POR_HORA -> orZero(datos.proyeccionDocenteHora());
}; };
} }
...@@ -245,13 +290,20 @@ public class RentaQuintaCalculadora { ...@@ -245,13 +290,20 @@ public class RentaQuintaCalculadora {
* la base de la proyección cuando el mes de corte todavía no tiene boleta. * 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}); * <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. * UIT, las 7 UIT, los tramos y el divisor son de ley e iguales para todas las planillas.
*/ */
public record DatosCalculo( public record DatosCalculo(
Integer periodo, Integer periodo,
ModalidadCalculo modalidad, ModalidadCalculo modalidad,
BigDecimal proyeccionDocenteHora, BigDecimal proyeccionDocenteHora,
BigDecimal sueldoBrutoMesCorteDocenteHora,
BigDecimal sueldoBrutoRealMesCorte,
BigDecimal gratificacionJulioDocenteHora,
BigDecimal gratificacionDiciembreDocenteHora,
int mesCorte, int mesCorte,
int mesReferencia, int mesReferencia,
BigDecimal historialReal, BigDecimal historialReal,
......
...@@ -9,7 +9,8 @@ import java.util.Arrays; ...@@ -9,7 +9,8 @@ import java.util.Arrays;
* <ul> * <ul>
* <li>{@code ADM} → cálculo normal (administrativo)</li> * <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 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> * </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 tipo_pago} de la
...@@ -30,6 +31,18 @@ public enum TipoPago { ...@@ -30,6 +31,18 @@ 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. */
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}). * 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; * {@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 { ...@@ -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. */ /** 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_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."; + "toma de la boleta del trabajador.";
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 "
......
...@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; ...@@ -10,6 +10,7 @@ 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.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 java.time.Instant; import java.time.Instant;
import java.util.Map; import java.util.Map;
...@@ -28,6 +29,11 @@ public class GlobalExceptionHandler { ...@@ -28,6 +29,11 @@ public class GlobalExceptionHandler {
return build(HttpStatus.NOT_FOUND, ex.getMessage()); 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) @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());
......
...@@ -90,7 +90,10 @@ public class RentaQuintaMapper { ...@@ -90,7 +90,10 @@ public class RentaQuintaMapper {
// Sin boleta real del mes de corte, se estima con sueldo bruto del mes de referencia // 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). // (+ gratificación 109% si el mes de corte es julio o diciembre).
BloqueIngresosDTO actual = mesCorteReal 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( : new BloqueIngresosDTO(
calculo.estimadoMesCorteSueldoBruto(), calculo.estimadoMesCorteSueldoBruto(),
BigDecimal.ZERO, BigDecimal.ZERO,
...@@ -174,6 +177,11 @@ public class RentaQuintaMapper { ...@@ -174,6 +177,11 @@ public class RentaQuintaMapper {
return bloque(historial, filtro).totalBrutoRemunerativo(); 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) { private BloqueIngresosDTO bloque(List<RentaQuinta> historial, Predicate<RentaQuinta> filtro) {
BigDecimal sueldoBruto = BigDecimal.ZERO; BigDecimal sueldoBruto = BigDecimal.ZERO;
BigDecimal horasExtras = BigDecimal.ZERO; BigDecimal horasExtras = BigDecimal.ZERO;
...@@ -206,21 +214,32 @@ public class RentaQuintaMapper { ...@@ -206,21 +214,32 @@ public class RentaQuintaMapper {
.add(orZero(r.getValeConsumo())) .add(orZero(r.getValeConsumo()))
.add(orZero(r.getObsequios())) .add(orZero(r.getObsequios()))
.add(orZero(r.getCanastaNavidad2())) .add(orZero(r.getCanastaNavidad2()))
.add(orZero(r.getValeConsumoDescuento())); .add(orZero(r.getValeConsumoDescuento()))
gratificaciones = gratificaciones
.add(orZero(r.getCompensacionVacacional())) .add(orZero(r.getCompensacionVacacional()))
.add(orZero(r.getGratificacionProporcional())) .add(orZero(r.getGratificacionProporcional()))
.add(orZero(r.getVacacionesTruncas())) .add(orZero(r.getVacacionesTruncas()))
.add(orZero(r.getSumaGraciosa())) .add(orZero(r.getSumaGraciosa()))
.add(orZero(r.getBonificacionExtraordinaria())) .add(orZero(r.getBonificacionExtraordinaria()));
.add(orZero(r.getBonificacionExtraordinaria2())) gratificaciones = gratificaciones
.add(orZero(r.getGratificacionJulioDiciembre())); .add(orZero(r.getGratificacionJulioDiciembre()))
.add(orZero(r.getBonificacionExtraordinaria2()));
utilidades = utilidades.add(orZero(r.getUtilidades())); utilidades = utilidades.add(orZero(r.getUtilidades()));
} }
BigDecimal total = sueldoBruto.add(horasExtras).add(gratificaciones).add(utilidades); BigDecimal total = sueldoBruto.add(horasExtras).add(gratificaciones).add(utilidades);
return new BloqueIngresosDTO(sueldoBruto, horasExtras, gratificaciones, utilidades, BigDecimal.ZERO, total); 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, private BloqueIngresosDTO conHistorialRealYOtrosEmpleadores(BloqueIngresosDTO bloque, BigDecimal historialReal,
BigDecimal otrosEmpleadores) { BigDecimal otrosEmpleadores) {
return new BloqueIngresosDTO( return new BloqueIngresosDTO(
......
...@@ -19,7 +19,7 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long> ...@@ -19,7 +19,7 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
String FILTROS = """ String FILTROS = """
and (:sede is null or trim(r.codlocal) = :sede) and (:sede is null or trim(r.codlocal) = :sede)
and (:tipoPlanilla is null or trim(r.boletaTipoPersonal) = :tipoPlanilla) and (:tipoPlanilla is null or trim(r.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 (:areaGeneral is null or r.areaGeneral = :areaGeneral)
and (:subArea is null or r.subArea = :subArea) and (:subArea is null or r.subArea = :subArea)
and (:cargo is null or r.cargo = :cargo) and (:cargo is null or r.cargo = :cargo)
......
...@@ -100,7 +100,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -100,7 +100,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
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
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.areaGeneral()),
codigo(filtro.subArea()), codigo(filtro.subArea()),
codigo(filtro.cargo()), codigo(filtro.cargo()),
...@@ -127,7 +127,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -127,7 +127,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
filtro.tipoPago()); filtro.tipoPago());
BigDecimal planillaCargada = (a.mesCorteReal() BigDecimal planillaCargada = (a.mesCorteReal()
? a.planillaCargadaReal() ? 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 = historial.stream()
...@@ -159,9 +159,15 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -159,9 +159,15 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
return tipoPlanilla == null ? null : tipoPlanilla.getCodigo(); 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) { * CONDICIÓN TIPO PAGO — tipo_pago es char(1): el repository lo compara con trim() contra este
return tipoPago == null ? null : tipoPago.getCodigo(); * 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 { ...@@ -292,7 +298,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
do { do {
pagina = repository.buscarDocumentos( pagina = repository.buscarDocumentos(
filtro.periodo(), filtro.mesCorte(), 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.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));
...@@ -370,13 +376,30 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -370,13 +376,30 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
OtrosEmpleadoresAgregado otrosEmpleadores = otrosEmpleadores(numeroDocumento, periodo, mesCorte); OtrosEmpleadoresAgregado otrosEmpleadores = otrosEmpleadores(numeroDocumento, periodo, mesCorte);
ModalidadCalculo modalidad = modalidad(tipoPlanilla, tipoPago, historial, mesReferencia); 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( ? proyeccionDocenteHoraRepository.remunBrutaFutura(
numeroDocumento, tipoPlanilla.getCodigo(), periodo, mesCorte) numeroDocumento, tipoPlanilla.getCodigo(), periodo, mesCorte)
: BigDecimal.ZERO; : 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( 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(), a.planillaCargadaReal(),
sueldoBrutoReferencia, basicoReferencia, asignacionFamiliarReferencia, a.retenidoAnterior(), sueldoBrutoReferencia, basicoReferencia, asignacionFamiliarReferencia, a.retenidoAnterior(),
a.julioPercibida(), a.diciembrePercibida(), a.julioPercibida(), a.diciembrePercibida(),
...@@ -390,21 +413,22 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -390,21 +413,22 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
* CONDICIÓN MODALIDAD — ADM siempre es monto fijo. En DOC decide el {@code tipoPago} enviado * 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 * 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 * 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, private ModalidadCalculo modalidad(TipoPlanilla tipoPlanilla, TipoPago solicitado, List<RentaQuinta> historial,
int mesReferencia) { int mesReferencia) {
if (solicitado != null) { if (solicitado != null) {
return ModalidadCalculo.de(tipoPlanilla, solicitado); return ModalidadCalculo.de(tipoPlanilla, solicitado);
} }
boolean porHora = historial.stream() 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(RentaQuinta::getTipoPago) .map(r -> TipoPago.desdeCodigo(r.getTipoPago()))
.filter(Objects::nonNull) .filter(Objects::nonNull)
.map(String::trim) .toList();
.anyMatch(TipoPago.POR_HORA.getCodigo()::equals); TipoPago deBoleta = tiposPago.contains(TipoPago.POR_HORA) ? TipoPago.POR_HORA
return ModalidadCalculo.de(tipoPlanilla, porHora ? TipoPago.POR_HORA : null); : 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