fix: planillaCargada en listado usa el mismo estimado que detallado cuando falta el mes de corte

- buscarResumen (JPQL con SUM/CASE) se reemplaza por buscarDocumentos (solo
  filtra/pagina/ordena documentos) + historialHastaMes (trae las boletas
  reales). Toda la agregacion (historialReal, planillaCargada, mes de
  referencia, gratificacion pendiente) pasa a Java, compartida con
  obtenerDetalleCompleto via el mismo helper (agregar/calcular) - antes vivia
  duplicada entre la query SQL y el service.
- Consecuencia: cuando el mes de corte no tiene boleta real, planillaCargada
  en la grilla ya no queda en 0 - muestra el mismo estimado que el bloque
  "actual" del detalle completo (sueldo bruto del mes de referencia +
  gratificacion 109% si el mes de corte es julio o diciembre).
- RentaQuintaResumenProjection reemplazado por DocumentoResumenProjection
  (solo numeroDocumento, ya no expone columnas agregadas).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AyxL2mSnUE9XyUFHw8zDmM
parent ab0780f9
......@@ -14,7 +14,6 @@ import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.RegistroHist
import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.TramoDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaResumenDTO;
import so.planilla_be.rentaquinta.entity.RentaQuinta;
import so.planilla_be.rentaquinta.repository.RentaQuintaResumenProjection;
import java.math.BigDecimal;
import java.util.List;
......@@ -23,20 +22,21 @@ import java.util.function.Predicate;
@Component
public class RentaQuintaMapper {
public RentaQuintaResumenDTO toResumen(RentaQuintaResumenProjection p, Integer periodo,
Integer mesCorte, CalculoRentaQuinta calculo) {
public RentaQuintaResumenDTO toResumen(RentaQuinta referencia, Integer codper, Integer periodo, Integer mesCorte,
BigDecimal historialReal, BigDecimal planillaCargada,
BigDecimal retenidoAnterior, CalculoRentaQuinta calculo) {
return new RentaQuintaResumenDTO(
p.getCodper(),
codper,
periodo,
p.getNumeroDocumento(),
nombreCompleto(p.getApellidoPaterno(), p.getApellidoMaterno(), p.getNombres()),
referencia.getNumeroDocumento(),
nombreCompleto(referencia.getApellidoPaterno(), referencia.getApellidoMaterno(), referencia.getNombres()),
mesCorte,
orZero(p.getHistorialReal()),
orZero(p.getPlanillaCargada()),
orZero(historialReal),
orZero(planillaCargada),
calculo.proyeccionFutura(),
calculo.rentaBrutaAnual(),
calculo.impuestoAnual(),
orZero(p.getRetenidoAnterior()),
orZero(retenidoAnterior),
calculo.retencionDelMes());
}
......
package so.planilla_be.rentaquinta.repository;
/**
* Clave de un trabajador que matchea los filtros de la grilla (paginación/orden). La fila
* completa se arma en Java (RentaQuintaServiceImpl), no en esta query.
*/
public interface DocumentoResumenProjection {
String getNumeroDocumento();
}
......@@ -27,44 +27,6 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
or upper(concat(r.apellidoPaterno, ' ', r.apellidoMaterno, ' ', r.nombres)) like :texto)
""";
/**
* Total bruto remunerativo de un mes: suma de los 4 bloques de conceptos de la matriz del
* Excel de referencia (docs/calculo-renta-quinta.md) — ingresos fijos, extras y variables,
* vacaciones y liquidación, y anuales excepcionales.
*/
String CONCEPTOS_MES = """
(coalesce(r.basico, 0)
+ coalesce(r.vacacionesGozadasRemVaca, 0)
+ coalesce(r.licenciaGoceHaber, 0)
+ coalesce(r.licenciaSaludFamiliarLey30012, 0)
+ coalesce(r.asignacionFamiliar, 0)
+ coalesce(r.movilidad, 0)
+ coalesce(r.horasExtras25, 0)
+ coalesce(r.horasExtras35, 0)
+ coalesce(r.dominical, 0)
+ coalesce(r.bonoExtraordinario, 0)
+ coalesce(r.bonoProductividad, 0)
+ coalesce(r.bonoDesempeno, 0)
+ coalesce(r.bonoAniversario, 0)
+ coalesce(r.apoyoSalud, 0)
+ coalesce(r.asignacionEstudios, 0)
+ coalesce(r.asignacionFallecimientoFamiliar, 0)
+ coalesce(r.reintegro, 0)
+ coalesce(r.canastaNavidad, 0)
+ coalesce(r.valeConsumo, 0)
+ coalesce(r.obsequios, 0)
+ coalesce(r.canastaNavidad2, 0)
+ coalesce(r.valeConsumoDescuento, 0)
+ coalesce(r.compensacionVacacional, 0)
+ coalesce(r.gratificacionProporcional, 0)
+ coalesce(r.vacacionesTruncas, 0)
+ coalesce(r.sumaGraciosa, 0)
+ coalesce(r.bonificacionExtraordinaria, 0)
+ coalesce(r.bonificacionExtraordinaria2, 0)
+ coalesce(r.gratificacionJulioDiciembre, 0)
+ coalesce(r.utilidades, 0))
""";
/** Meses de un trabajador en un año, ordenados cronológicamente (historial anual). */
List<RentaQuinta> findByCodperAndBoletaAnioOrderByBoletaMesAsc(
Integer codper, Integer boletaAnio);
......@@ -74,49 +36,19 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
Integer codper, Integer boletaAnio, Integer boletaMes);
/**
* Mes de referencia por trabajador: el último con boleta real hasta el mes de corte
* (inclusive). Coincide con el mes de corte cuando este sí está cargado; si no, cae al mes
* real anterior y ese pasa a ser la base de la proyección (ver RentaQuintaCalculadora).
*/
String MES_REFERENCIA = """
(select max(r2.boletaMes) from RentaQuinta r2
where r2.numeroDocumento = r.numeroDocumento and r2.boletaAnio = r.boletaAnio
and r2.boletaMes <= :mesCorte)
""";
/**
* Agregado anual por trabajador hasta el mes de corte: acumulado de meses previos
* (historial real / retenido), valores del propio mes de corte (planilla cargada /
* retención del mes, 0 si no está cargado) y valores del mes de referencia (base de
* proyección cuando el mes de corte no tiene boleta real).
* Documentos que matchean los filtros de la grilla, paginados y ordenados por apellidos +
* nombres. Solo trae la clave (numeroDocumento) — el service arma la fila agregando en Java
* sobre {@link #historialHastaMes}, no en SQL.
*/
@Query(value = """
select max(r.codper) as codper,
r.numeroDocumento as numeroDocumento,
select distinct r.numeroDocumento as numeroDocumento,
r.apellidoPaterno as apellidoPaterno,
r.apellidoMaterno as apellidoMaterno,
r.nombres as nombres,
sum(case when r.boletaMes < :mesCorte then """ + CONCEPTOS_MES + """
else 0 end) as historialReal,
sum(case when r.boletaMes = :mesCorte then """ + CONCEPTOS_MES + """
else 0 end) as planillaCargada,
sum(case when r.boletaMes < :mesCorte then coalesce(r.renta5taCategoria, 0) else 0 end) as retenidoAnterior,
sum(case when r.boletaMes = :mesCorte then coalesce(r.renta5taCategoria, 0) else 0 end) as retencionDelMes,
max(""" + MES_REFERENCIA + """
) as mesReferencia,
sum(case when r.boletaMes = """ + MES_REFERENCIA + """
then coalesce(r.basico, 0) else 0 end) as basicoMesCorte,
sum(case when r.boletaMes = """ + MES_REFERENCIA + """
then coalesce(r.asignacionFamiliar, 0) else 0 end) as asignacionFamiliarMesCorte,
sum(case when r.boletaMes = """ + MES_REFERENCIA + """
then coalesce(r.sueldoBruto, 0) else 0 end) as sueldoBrutoMesCorte,
max(case when r.boletaMes = 7 then 1 else 0 end) as julioCargado,
max(case when r.boletaMes = 12 then 1 else 0 end) as diciembreCargado
r.nombres as nombres
from RentaQuinta r
where r.boletaAnio = :periodo
and r.boletaMes <= :mesCorte
""" + FILTROS + """
group by r.numeroDocumento, r.apellidoPaterno, r.apellidoMaterno, r.nombres
order by r.apellidoPaterno asc, r.apellidoMaterno asc, r.nombres asc
""",
countQuery = """
......@@ -125,7 +57,7 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
where r.boletaAnio = :periodo
and r.boletaMes <= :mesCorte
""" + FILTROS)
Page<RentaQuintaResumenProjection> buscarResumen(
Page<DocumentoResumenProjection> buscarDocumentos(
@Param("periodo") Integer periodo,
@Param("mesCorte") Integer mesCorte,
@Param("sede") String sede,
......@@ -136,6 +68,19 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
@Param("texto") String texto,
Pageable pageable);
/** Boletas reales de un conjunto de documentos hasta el mes de corte (agregación en Java). */
@Query("""
select r from RentaQuinta r
where r.numeroDocumento in :documentos
and r.boletaAnio = :periodo
and r.boletaMes <= :mesCorte
order by r.numeroDocumento asc, r.boletaMes asc
""")
List<RentaQuinta> historialHastaMes(
@Param("documentos") List<String> documentos,
@Param("periodo") Integer periodo,
@Param("mesCorte") Integer mesCorte);
/** Boletas completas de un trabajador en un año, ordenadas por mes (pantalla de historial). */
@Query("""
select r from RentaQuinta r
......
package so.planilla_be.rentaquinta.repository;
import java.math.BigDecimal;
/** Agregado anual por trabajador que alimenta la grilla de retenciones. */
public interface RentaQuintaResumenProjection {
Integer getCodper();
String getNumeroDocumento();
String getApellidoPaterno();
String getApellidoMaterno();
String getNombres();
BigDecimal getHistorialReal();
BigDecimal getPlanillaCargada();
BigDecimal getRetenidoAnterior();
BigDecimal getRetencionDelMes();
/** Último mes con boleta real (<= mes de corte); base de la proyección. */
Integer getMesReferencia();
/** Básico del mes de referencia, usado para proyectar gratificaciones. */
BigDecimal getBasicoMesCorte();
BigDecimal getAsignacionFamiliarMesCorte();
/** Sueldo bruto del mes de referencia, base para proyectar los meses restantes del año. */
BigDecimal getSueldoBrutoMesCorte();
/** 1 si existe boleta real de julio (<= mes de corte), 0 si no. */
Integer getJulioCargado();
/** 1 si existe boleta real de diciembre (<= mes de corte), 0 si no. */
Integer getDiciembreCargado();
}
......@@ -16,15 +16,18 @@ import so.planilla_be.rentaquinta.dto.RentaQuintaResumenDTO;
import so.planilla_be.rentaquinta.entity.RentaQuinta;
import so.planilla_be.rentaquinta.exception.RentaQuintaNoEncontradaException;
import so.planilla_be.rentaquinta.mapper.RentaQuintaMapper;
import so.planilla_be.rentaquinta.repository.DocumentoResumenProjection;
import so.planilla_be.rentaquinta.repository.RentaQuintaRepository;
import so.planilla_be.rentaquinta.repository.RentaQuintaResumenProjection;
import so.planilla_be.rentaquinta.service.RentaQuintaService;
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@Service
@Transactional(readOnly = true)
......@@ -46,7 +49,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
@Override
public Page<RentaQuintaResumenDTO> buscar(RentaQuintaFiltroDTO filtro) {
return repository.buscarResumen(
Page<DocumentoResumenProjection> documentos = repository.buscarDocumentos(
filtro.periodo(),
filtro.mesCorte(),
codigo(filtro.sede()),
......@@ -55,23 +58,33 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
codigo(filtro.subArea()),
codigo(filtro.cargo()),
patron(filtro.documentoOApellidos()),
toPageable(filtro))
.map(p -> mapper.toResumen(p, filtro.periodo(), filtro.mesCorte(), calcular(p, filtro)));
}
toPageable(filtro));
private CalculoRentaQuinta calcular(RentaQuintaResumenProjection p, RentaQuintaFiltroDTO filtro) {
return calculadora.calcular(new DatosCalculo(
filtro.periodo(),
filtro.mesCorte(),
p.getMesReferencia(),
p.getHistorialReal(),
p.getPlanillaCargada(),
p.getSueldoBrutoMesCorte(),
p.getBasicoMesCorte(),
p.getAsignacionFamiliarMesCorte(),
p.getRetenidoAnterior(),
p.getJulioCargado() != null && p.getJulioCargado() == 1,
p.getDiciembreCargado() != null && p.getDiciembreCargado() == 1));
List<String> numeros = documentos.getContent().stream()
.map(DocumentoResumenProjection::getNumeroDocumento)
.toList();
Map<String, List<RentaQuinta>> porDocumento = numeros.isEmpty() ? Map.of()
: repository.historialHastaMes(numeros, filtro.periodo(), filtro.mesCorte()).stream()
.collect(Collectors.groupingBy(RentaQuinta::getNumeroDocumento));
return documentos.map(d -> {
List<RentaQuinta> historial = porDocumento.getOrDefault(d.getNumeroDocumento(), List.of());
Agregado a = agregar(historial, filtro.mesCorte());
CalculoRentaQuinta calculo = calcular(a, historial, filtro.periodo(), filtro.mesCorte());
// Sin boleta real del mes de corte, planillaCargada muestra el mismo estimado que el
// bloque "actual" del detalle completo (sueldo bruto del mes de referencia + grat.
// 109% si el mes de corte es julio/diciembre), no 0.
BigDecimal planillaCargada = a.mesCorteReal()
? a.planillaCargadaReal()
: calculo.estimadoMesCorteSueldoBruto().add(calculo.estimadoMesCorteGratificacion());
Integer codper = historial.stream()
.map(RentaQuinta::getCodper)
.filter(Objects::nonNull)
.max(Integer::compareTo)
.orElse(null);
return mapper.toResumen(a.referencia(), codper, filtro.periodo(), filtro.mesCorte(),
a.historialReal(), planillaCargada, a.retenidoAnterior(), calculo);
});
}
/** Los catálogos llegan como Integer pero viven en columnas TEXT: se comparan como texto. */
......@@ -113,42 +126,59 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
List<RentaQuinta> historialAnio = repository.historialAnualPorDocumento(numeroDocumento, anio);
int mesCorte = mes;
// Si el mes de corte no tiene boleta real (ej. julio sin cargar), se usa como referencia
// el mes real más reciente anterior — misma regla que la grilla (mesReferencia).
RentaQuinta boletaMesCorte = historialAnio.stream()
Agregado a = agregar(historialAnio, mesCorte);
if (a.referencia() == null) {
throw new RentaQuintaNoEncontradaException(
"No existe boleta para documento %s, periodo %d-%02d".formatted(numeroDocumento, anio, mes));
}
CalculoRentaQuinta calculo = calcular(a, historialAnio, anio, mesCorte);
return mapper.toDetalleCompleto(a.referencia(), historialAnio, mesCorte, calculo);
}
/**
* Agregado anual de un trabajador hasta el mes de corte, base de {@link DatosCalculo}: usado
* tanto por la grilla como por el detalle completo, para que ambos calculen exactamente igual.
*
* <p>{@code referencia} es la boleta del mes de corte si existe, o si no la del mes real más
* reciente anterior (null solo si el trabajador no tiene ninguna boleta hasta el mes de
* corte).
*/
private Agregado agregar(List<RentaQuinta> historial, int mesCorte) {
RentaQuinta boletaMesCorte = historial.stream()
.filter(r -> r.getBoletaMes().equals(mesCorte))
.findFirst()
.orElse(null);
RentaQuinta referencia = boletaMesCorte != null ? boletaMesCorte
: historialAnio.stream()
: historial.stream()
.filter(r -> r.getBoletaMes() < mesCorte)
.max(Comparator.comparing(RentaQuinta::getBoletaMes))
.orElseThrow(() -> new RentaQuintaNoEncontradaException(
"No existe boleta para documento %s, periodo %d-%02d".formatted(numeroDocumento, anio, mes)));
int mesReferencia = referencia.getBoletaMes();
.orElse(null);
BigDecimal historialReal = mapper.totalBrutoMes(historialAnio, r -> r.getBoletaMes() < mesCorte);
BigDecimal planillaCargada = boletaMesCorte == null ? BigDecimal.ZERO
: mapper.totalBrutoMes(historialAnio, r -> r.getBoletaMes().equals(mesCorte));
BigDecimal retenidoAnterior = sumaMesCorte(historialAnio, r -> r.getBoletaMes() < mesCorte,
BigDecimal historialReal = mapper.totalBrutoMes(historial, r -> r.getBoletaMes() < mesCorte);
BigDecimal planillaCargadaReal = boletaMesCorte == null ? BigDecimal.ZERO
: mapper.totalBrutoMes(historial, r -> r.getBoletaMes().equals(mesCorte));
BigDecimal retenidoAnterior = sumaMesCorte(historial, r -> r.getBoletaMes() < mesCorte,
RentaQuinta::getRenta5taCategoria);
boolean julioPercibida = historial.stream().anyMatch(r -> r.getBoletaMes() == 7 && r.getBoletaMes() <= mesCorte);
boolean diciembrePercibida = historial.stream().anyMatch(r -> r.getBoletaMes() == 12 && r.getBoletaMes() <= mesCorte);
// La grilla agrega todas las filas del documento en el mes de referencia (una persona
// puede tener varios codper/tipo de planilla); la base de proyección aquí hace lo mismo.
BigDecimal sueldoBrutoReferencia = sumaMesCorte(historialAnio, esMesCorte(mesReferencia), RentaQuinta::getSueldoBruto);
BigDecimal basicoReferencia = sumaMesCorte(historialAnio, esMesCorte(mesReferencia), RentaQuinta::getBasico);
BigDecimal asignacionFamiliarReferencia =
sumaMesCorte(historialAnio, esMesCorte(mesReferencia), RentaQuinta::getAsignacionFamiliar);
boolean julioPercibida = historialAnio.stream().anyMatch(r -> r.getBoletaMes() == 7 && r.getBoletaMes() <= mesCorte);
boolean diciembrePercibida = historialAnio.stream().anyMatch(r -> r.getBoletaMes() == 12 && r.getBoletaMes() <= mesCorte);
return new Agregado(referencia, boletaMesCorte != null, historialReal, planillaCargadaReal,
retenidoAnterior, julioPercibida, diciembrePercibida);
}
CalculoRentaQuinta calculo = calculadora.calcular(new DatosCalculo(
anio, mesCorte, mesReferencia, historialReal, planillaCargada,
sueldoBrutoReferencia, basicoReferencia, asignacionFamiliarReferencia, retenidoAnterior,
julioPercibida, diciembrePercibida));
private CalculoRentaQuinta calcular(Agregado a, List<RentaQuinta> historial, Integer periodo, int mesCorte) {
int mesReferencia = a.referencia().getBoletaMes();
// Todas las filas del documento en el mes de referencia (una persona puede tener varios
// codper/tipo de planilla).
BigDecimal sueldoBrutoReferencia = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getSueldoBruto);
BigDecimal basicoReferencia = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getBasico);
BigDecimal asignacionFamiliarReferencia =
sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getAsignacionFamiliar);
return mapper.toDetalleCompleto(referencia, historialAnio, mesCorte, calculo);
return calculadora.calcular(new DatosCalculo(
periodo, mesCorte, mesReferencia, a.historialReal(), a.planillaCargadaReal(),
sueldoBrutoReferencia, basicoReferencia, asignacionFamiliarReferencia, a.retenidoAnterior(),
a.julioPercibida(), a.diciembrePercibida()));
}
private Predicate<RentaQuinta> esMesCorte(int mesCorte) {
......@@ -178,4 +208,14 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
}
return historial;
}
private record Agregado(
RentaQuinta referencia,
boolean mesCorteReal,
BigDecimal historialReal,
BigDecimal planillaCargadaReal,
BigDecimal retenidoAnterior,
boolean julioPercibida,
boolean diciembrePercibida
) {}
}
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