Commit 628cba00 by paolo

perf: precargar datos externos y agrupar grabado masivo de renta quinta

Elimina consultas por trabajador (otros empleadores, proyeccion docente-hora,
findById de pl_renta_quinta) en grabar-masivo, grilla y detalle; desacopla
el historial del contexto de persistencia y habilita JDBC batching.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parent d5801bec
......@@ -11,4 +11,7 @@ public interface RetencionOtrosEmpleadoresRepository
List<RetencionOtrosEmpleadores> findByDocumentoTrabajadorAndAnioOrderByMes(
String documentoTrabajador, Integer anio);
/** Declaraciones del año de varios trabajadores en una sola consulta (grilla / grabado masivo). */
List<RetencionOtrosEmpleadores> findByAnioAndDocumentoTrabajadorIn(Integer anio, List<String> documentosTrabajador);
}
......@@ -15,6 +15,10 @@ public interface ProyeccionDocenteHoraRepository extends JpaRepository<Proyeccio
List<ProyeccionDocenteHora> findByNumeroDocumentoAndProyeccionAnioOrderByProyeccionMesAscBoletaTipoPersonalAsc(
String numeroDocumento, Integer proyeccionAnio);
/** Filas del año de varios docentes en una sola consulta (grilla / grabado masivo). */
List<ProyeccionDocenteHora> findByProyeccionAnioAndNumeroDocumentoIn(Integer proyeccionAnio,
List<String> numerosDocumento);
/** Clave lógica (la tabla solo tiene PK sobre id_proyeccion). */
Optional<ProyeccionDocenteHora> findFirstByNumeroDocumentoAndBoletaTipoPersonalAndProyeccionAnioAndProyeccionMes(
String numeroDocumento, String boletaTipoPersonal, Integer proyeccionAnio, Integer proyeccionMes);
......
package so.planilla_be.rentaquinta.service.impl;
import jakarta.persistence.EntityManager;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import so.planilla_be.proyecciondocentehora.entity.ProyeccionDocenteHora;
import so.planilla_be.proyecciondocentehora.repository.ProyeccionDocenteHoraRepository;
import so.planilla_be.rentaquinta.calculo.CalculoRentaQuinta;
import so.planilla_be.rentaquinta.calculo.ModalidadCalculo;
......@@ -41,6 +43,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.IntPredicate;
import java.util.function.Predicate;
import java.util.stream.Collectors;
......@@ -56,6 +59,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
* {@code ""}, porque char(1) se lee con padding (' ') y Hibernate lo tomaría como cambio de PK.
*/
private static final String TIPO_PAGO_VACIO = " ";
private static final String TIPO_GRATIFICACION = "GRA";
private final RentaQuintaRepository repository;
private final RentaQuintaMapper mapper;
......@@ -65,6 +69,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
private final UtilidadRepository utilidadRepository;
private final RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository;
private final ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository;
private final EntityManager entityManager;
public RentaQuintaServiceImpl(RentaQuintaRepository repository, RentaQuintaMapper mapper,
RentaQuintaCalculadora calculadora,
......@@ -72,7 +77,8 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
RentaBasicoRepository rentaBasicoRepository,
UtilidadRepository utilidadRepository,
RentaQuintaGrabadaRepository rentaQuintaGrabadaRepository,
ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository) {
ProyeccionDocenteHoraRepository proyeccionDocenteHoraRepository,
EntityManager entityManager) {
this.repository = repository;
this.mapper = mapper;
this.calculadora = calculadora;
......@@ -81,6 +87,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
this.utilidadRepository = utilidadRepository;
this.rentaQuintaGrabadaRepository = rentaQuintaGrabadaRepository;
this.proyeccionDocenteHoraRepository = proyeccionDocenteHoraRepository;
this.entityManager = entityManager;
}
/**
......@@ -118,13 +125,14 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
// 1. AQUÍ OBTIENES EL MAPA CON LAS RETENCIONES GRABADAS DEL MES ANTERIOR:
Map<String, BigDecimal> retencionesAnteriores = obtenerRetencionesMesAnterior(numeros, filtro.periodo(), filtro.mesCorte());
DatosExternos externos = cargarDatosExternos(numeros, filtro.periodo());
return documentos.map(d -> {
List<RentaQuinta> historial = porDocumento.getOrDefault(d.getNumeroDocumento(), List.of());
Agregado a = agregar(historial, filtro.mesCorte());
TipoPlanilla tipoPlanilla = tipoPlanilla(filtro.tipoPlanilla(), a.referencia());
CalculoRentaQuinta calculo = calcular(a, historial, filtro.periodo(), filtro.mesCorte(), tipoPlanilla,
filtro.tipoPago());
filtro.tipoPago(), externos);
BigDecimal planillaCargada = (a.mesCorteReal()
? a.planillaCargadaReal().add(calculo.ajusteSueldoBrutoMesCorte())
......@@ -231,7 +239,8 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
"No existe boleta para documento %s, periodo %d-%02d".formatted(numeroDocumento, anio, mes));
}
TipoPlanilla tipoPlanilla = tipoPlanilla(tipoPlanillaSolicitado, a.referencia());
CalculoRentaQuinta calculo = calcular(a, historialAnio, anio, mesCorte, tipoPlanilla, tipoPago);
CalculoRentaQuinta calculo = calcular(a, historialAnio, anio, mesCorte, tipoPlanilla, tipoPago,
cargarDatosExternos(List.of(numeroDocumento), anio));
return mapper.toDetalleCompleto(a.referencia(), historialAnio, mesCorte, calculo);
}
......@@ -279,6 +288,42 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
}
/**
* Variante del grabado masivo: los registros existentes ya vienen precargados en
* {@code existentes} (clave {@link #claveGrabada(RentaQuintaGrabada)}), así que no hay un
* SELECT por trabajador; los nuevos se insertan con {@code persist} (con {@code save} la PK
* asignada dispara un merge que vuelve a consultar).
*/
private void upsertRetencion(Map<String, RentaQuintaGrabada> existentes, String numeroDocumento, Integer anio,
Integer mes, TipoPlanilla tipoPlanilla, TipoPago tipoPago,
BigDecimal retencionMesActual) {
String tipoPagoGrabado = tipoPlanilla.esDocente() && tipoPago != null ? tipoPago.getCodigo() : TIPO_PAGO_VACIO;
String clave = claveGrabada(numeroDocumento, tipoPlanilla.getCodigo(), tipoPagoGrabado);
RentaQuintaGrabada grabada = existentes.get(clave);
if (grabada != null) {
grabada.setRetencionMesActual(retencionMesActual);
return;
}
grabada = new RentaQuintaGrabada();
grabada.setNumeroDocumento(numeroDocumento);
grabada.setAnio(anio);
grabada.setMes(mes);
grabada.setBoletaTipoPersonal(tipoPlanilla.getCodigo());
grabada.setTipoPago(tipoPagoGrabado);
grabada.setRetencionMesActual(retencionMesActual);
entityManager.persist(grabada);
existentes.put(clave, grabada);
}
/** Clave de la PK de pl_renta_quinta (sin año/mes, fijos en el grabado masivo); char(n) con trim. */
private String claveGrabada(RentaQuintaGrabada g) {
return claveGrabada(g.getNumeroDocumento(), g.getBoletaTipoPersonal(), g.getTipoPago());
}
private String claveGrabada(String numeroDocumento, String boletaTipoPersonal, String tipoPago) {
return claveGrabada(numeroDocumento, boletaTipoPersonal) + "|" + (tipoPago == null ? "" : tipoPago.trim());
}
/**
* Graba (upsert) en pl_renta_quinta la retención del mes de todos los trabajadores que
* matchean los mismos filtros de la grilla ({@code buscar}), sin el tope de paginación de
* la grilla (recorre todas las páginas internamente). Usa el mismo cálculo que la grilla
......@@ -310,9 +355,16 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
return 0;
}
List<RentaQuinta> historialCompleto = repository.historialHastaMes(numeros, filtro.periodo(), filtro.mesCorte());
// Solo lectura: fuera del contexto de persistencia para que el flush no haga dirty checking
// de miles de boletas de pl_planilla.
historialCompleto.forEach(entityManager::detach);
aplicarBasicoYUtilidades(historialCompleto, filtro.periodo());
Map<String, List<RentaQuinta>> porDocumento = historialCompleto.stream()
.collect(Collectors.groupingBy(RentaQuinta::getNumeroDocumento));
DatosExternos externos = cargarDatosExternos(numeros, filtro.periodo());
Map<String, RentaQuintaGrabada> existentes = rentaQuintaGrabadaRepository
.findByAnioAndMesAndNumeroDocumentoIn(filtro.periodo(), filtro.mesCorte(), numeros).stream()
.collect(Collectors.toMap(this::claveGrabada, g -> g, (x, y) -> x));
int grabados = 0;
for (String numeroDocumento : numeros) {
......@@ -323,9 +375,9 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
}
TipoPlanilla tipoPlanilla = tipoPlanilla(filtro.tipoPlanilla(), a.referencia());
CalculoRentaQuinta calculo = calcular(a, historial, filtro.periodo(), filtro.mesCorte(), tipoPlanilla,
filtro.tipoPago());
upsertRetencion(numeroDocumento, filtro.periodo(), filtro.mesCorte(), tipoPlanilla, filtro.tipoPago(),
calculo.retencionDelMes());
filtro.tipoPago(), externos);
upsertRetencion(existentes, numeroDocumento, filtro.periodo(), filtro.mesCorte(), tipoPlanilla,
filtro.tipoPago(), calculo.retencionDelMes());
grabados++;
}
return grabados;
......@@ -363,7 +415,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
}
private CalculoRentaQuinta calcular(Agregado a, List<RentaQuinta> historial, Integer periodo, int mesCorte,
TipoPlanilla tipoPlanilla, TipoPago tipoPago) {
TipoPlanilla tipoPlanilla, TipoPago tipoPago, DatosExternos externos) {
int mesReferencia = a.referencia().getBoletaMes();
// Todas las filas del documento en el mes de referencia (una persona puede tener varios
// codper/tipo de planilla).
......@@ -373,25 +425,26 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getAsignacionFamiliar);
String numeroDocumento = a.referencia().getNumeroDocumento();
OtrosEmpleadoresAgregado otrosEmpleadores = otrosEmpleadores(numeroDocumento, periodo, mesCorte);
OtrosEmpleadoresAgregado otrosEmpleadores = otrosEmpleadores(
externos.otrosEmpleadores().getOrDefault(numeroDocumento, List.of()), mesCorte);
List<ProyeccionDocenteHora> proyeccion = externos.proyeccionDocenteHora().getOrDefault(numeroDocumento, List.of());
String tipoPersonal = tipoPlanilla.getCodigo();
ModalidadCalculo modalidad = modalidad(tipoPlanilla, tipoPago, historial, mesReferencia);
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)
? sumaProyeccion(proyeccion, tipoPersonal, m -> m == mesCorte, ProyeccionDocenteHora::getRemunBrutaFutura)
: BigDecimal.ZERO;
BigDecimal proyeccionDocenteHora = porHora
? proyeccionDocenteHoraRepository.sumaRemunBrutaFuturaPosterior(
numeroDocumento, tipoPlanilla.getCodigo(), periodo, mesCorte)
? sumaProyeccion(proyeccion, tipoPersonal, m -> m > mesCorte, ProyeccionDocenteHora::getRemunBrutaFutura)
: BigDecimal.ZERO;
BigDecimal gratificacionJulioDocenteHora = porHora
? proyeccionDocenteHoraRepository.gratificacionDelMes(numeroDocumento, periodo, 7)
? sumaProyeccion(proyeccion, TIPO_GRATIFICACION, m -> m == 7, ProyeccionDocenteHora::getGratificacion)
: BigDecimal.ZERO;
BigDecimal gratificacionDiciembreDocenteHora = porHora
? proyeccionDocenteHoraRepository.gratificacionDelMes(numeroDocumento, periodo, 12)
? sumaProyeccion(proyeccion, TIPO_GRATIFICACION, m -> m == 12, ProyeccionDocenteHora::getGratificacion)
: BigDecimal.ZERO;
BigDecimal sueldoBrutoRealMesCorte = a.mesCorteReal()
? mapper.sueldoBrutoMes(historial, esMesCorte(mesCorte))
......@@ -437,9 +490,7 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
* últimos ya pueden estar cargados de antemano por el trabajador/empleador y afectan tanto la
* renta bruta anual como la retención del mes de corte, igual que los otros dos bloques.
*/
private OtrosEmpleadoresAgregado otrosEmpleadores(String numeroDocumento, Integer periodo, int mesCorte) {
List<RetencionOtrosEmpleadores> declaraciones =
otrosEmpleadoresRepository.findByDocumentoTrabajadorAndAnioOrderByMes(numeroDocumento, periodo);
private OtrosEmpleadoresAgregado otrosEmpleadores(List<RetencionOtrosEmpleadores> declaraciones, int mesCorte) {
BigDecimal importeAnterior = BigDecimal.ZERO;
BigDecimal importeMesActual = BigDecimal.ZERO;
BigDecimal importeProyeccion = BigDecimal.ZERO;
......@@ -466,6 +517,36 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
}
/**
* Datos por documento que el cálculo necesita fuera de pl_planilla, precargados con una
* consulta por tabla para todos los documentos (en vez de varias consultas por trabajador).
*/
private DatosExternos cargarDatosExternos(List<String> documentos, Integer anio) {
if (documentos.isEmpty()) {
return new DatosExternos(Map.of(), Map.of());
}
return new DatosExternos(
otrosEmpleadoresRepository.findByAnioAndDocumentoTrabajadorIn(anio, documentos).stream()
.collect(Collectors.groupingBy(RetencionOtrosEmpleadores::getDocumentoTrabajador)),
proyeccionDocenteHoraRepository.findByProyeccionAnioAndNumeroDocumentoIn(anio, documentos).stream()
.collect(Collectors.groupingBy(ProyeccionDocenteHora::getNumeroDocumento)));
}
/**
* Equivale a las sumas {@code coalesce(sum(...), 0)} de ProyeccionDocenteHoraRepository:
* filas del tipo indicado (char(3), con trim) cuyo mes cumple {@code mes}; nulos no suman.
*/
private BigDecimal sumaProyeccion(List<ProyeccionDocenteHora> filas, String tipoPersonal,
IntPredicate mes,
Function<ProyeccionDocenteHora, BigDecimal> campo) {
return filas.stream()
.filter(p -> p.getBoletaTipoPersonal() != null && p.getBoletaTipoPersonal().trim().equals(tipoPersonal))
.filter(p -> p.getProyeccionMes() != null && mes.test(p.getProyeccionMes()))
.map(campo)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
/**
* El básico que compone el sueldo bruto (historial de ingresos, boleta, etc.) es siempre el
* de pl_planilla; pl_renta_basico no lo reemplaza. planilla.pl_utilidades pasa a ser el campo
* "utilidades" que antes vivía en la propia renta_quinta (0 si no hay declaración para ese
......@@ -575,6 +656,11 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
boolean diciembrePercibida
) {}
private record DatosExternos(
Map<String, List<RetencionOtrosEmpleadores>> otrosEmpleadores,
Map<String, List<ProyeccionDocenteHora>> proyeccionDocenteHora
) {}
private record OtrosEmpleadoresAgregado(
BigDecimal importeAnterior,
BigDecimal importeMesActual,
......
......@@ -7,6 +7,8 @@ spring:
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 10
data-source-properties:
reWriteBatchedInserts: true
jpa:
hibernate:
ddl-auto: validate # nunca 'update'/'create' sobre esta tabla
......@@ -15,6 +17,10 @@ spring:
hibernate:
default_schema: planilla
format_sql: true
jdbc:
batch_size: 200 # grabado masivo: inserts/updates de pl_renta_quinta en lote
order_inserts: true
order_updates: true
hbm2ddl:
# nuevo.personal es una foreign table (postgres_fdw): el driver la reporta como
# TABLE_TYPE "FOREIGN TABLE", no "TABLE", y sin esto ddl-auto:validate la da como
......
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