feat: agregar periodos y matriz mensual de ingresos de renta quinta

parent 26550ad7
-- Catálogo del filtro Periodo de renta de quinta categoría.
BEGIN;
CREATE TABLE IF NOT EXISTS planilla.pl_periodo (
id_periodo INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
mes INTEGER NOT NULL,
anio INTEGER NOT NULL,
CONSTRAINT ck_pl_periodo_mes CHECK (mes BETWEEN 1 AND 12),
CONSTRAINT ck_pl_periodo_anio CHECK (anio BETWEEN 1000 AND 9999),
CONSTRAINT uq_pl_periodo_mes_anio UNIQUE (mes, anio)
);
-- Incluir los doce meses permite consultar también los meses proyectados.
INSERT INTO planilla.pl_periodo (mes, anio)
SELECT meses.mes, periodos.anio
FROM (
SELECT DISTINCT boleta_anio AS anio
FROM planilla.pl_planilla
WHERE boleta_anio BETWEEN 1000 AND 9999
) periodos
CROSS JOIN generate_series(1, 12) AS meses(mes)
ORDER BY periodos.anio, meses.mes
ON CONFLICT (mes, anio) DO NOTHING;
COMMIT;
...@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.RequestParam; ...@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import so.planilla_be.rentaquinta.dto.FiltroItemDTO; import so.planilla_be.rentaquinta.dto.FiltroItemDTO;
import so.planilla_be.rentaquinta.dto.TipoFiltro; import so.planilla_be.rentaquinta.dto.TipoFiltro;
import so.planilla_be.rentaquinta.dto.PeriodoFiltroDTO;
import so.planilla_be.rentaquinta.service.FiltrosService; import so.planilla_be.rentaquinta.service.FiltrosService;
import java.util.List; import java.util.List;
...@@ -28,4 +29,10 @@ public class FiltrosController { ...@@ -28,4 +29,10 @@ public class FiltrosController {
public List<FiltroItemDTO> listar(@RequestParam TipoFiltro tipo) { public List<FiltroItemDTO> listar(@RequestParam TipoFiltro tipo) {
return service.listar(tipo); return service.listar(tipo);
} }
@Operation(summary = "Lista periodos registrados en pl_periodo, del más reciente al más antiguo")
@GetMapping("/periodos")
public List<PeriodoFiltroDTO> listarPeriodos() {
return service.listarPeriodos();
}
} }
package so.planilla_be.rentaquinta.dto;
public record PeriodoFiltroDTO(Integer idPeriodo, Integer mes, Integer anio) {}
...@@ -2,12 +2,13 @@ package so.planilla_be.rentaquinta.dto; ...@@ -2,12 +2,13 @@ package so.planilla_be.rentaquinta.dto;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* Detalle completo de renta de quinta de un trabajador en un mes (puntos 1 a 5 del Excel de * Detalle completo de renta de quinta de un trabajador en un mes (puntos 1 a 6 del Excel de
* referencia "Ver Historial - Cálculo"), en un solo JSON: la boleta del mes, historial de * referencia "Ver Historial - Cálculo"), en un solo JSON: la boleta del mes, historial de
* ingresos, parámetros SUNAT, liquidación del impuesto anual proyectado, escala por tramos y * ingresos, parámetros SUNAT, liquidación del impuesto anual proyectado, escala por tramos y
* control de retenciones. * control de retenciones y matriz mensual de ingresos.
*/ */
public record RentaQuintaDetalleCompletoDTO( public record RentaQuintaDetalleCompletoDTO(
RentaQuintaDetalleDTO boleta, RentaQuintaDetalleDTO boleta,
...@@ -15,9 +16,14 @@ public record RentaQuintaDetalleCompletoDTO( ...@@ -15,9 +16,14 @@ public record RentaQuintaDetalleCompletoDTO(
ParametrosSistemaDTO parametrosSistema, ParametrosSistemaDTO parametrosSistema,
LiquidacionImpuestoAnualDTO liquidacionImpuestoAnual, LiquidacionImpuestoAnualDTO liquidacionImpuestoAnual,
EscalaImpositivaDTO escalaImpositiva, EscalaImpositivaDTO escalaImpositiva,
ControlRetencionesDTO controlRetenciones ControlRetencionesDTO controlRetenciones,
List<MesIngresosDTO> matrizMensualIngresos
) { ) {
/** Punto 6: importes por concepto y mes; retención grabada o, en su ausencia, de la boleta. */
public record MesIngresosDTO(int mes, boolean proyectado, Map<String, BigDecimal> conceptos,
BigDecimal totalBruto, BigDecimal retencionEfectuada) {}
/** Punto 1: registro e historial de ingresos consolidados, en 3 columnas de periodo. */ /** Punto 1: registro e historial de ingresos consolidados, en 3 columnas de periodo. */
public record RegistroHistorialIngresosDTO( public record RegistroHistorialIngresosDTO(
BloqueIngresosDTO pasadoRealRealizado, BloqueIngresosDTO pasadoRealRealizado,
......
...@@ -18,6 +18,9 @@ import so.planilla_be.rentaquinta.repository.OrganizacionPersonalRepository.Orga ...@@ -18,6 +18,9 @@ import so.planilla_be.rentaquinta.repository.OrganizacionPersonalRepository.Orga
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
@Component @Component
...@@ -84,11 +87,12 @@ public class RentaQuintaMapper { ...@@ -84,11 +87,12 @@ public class RentaQuintaMapper {
} }
/** /**
* Imagen detallada de la boleta (puntos 1 a 5 del Excel "Ver Historial - Cálculo"): historial * Imagen detallada de la boleta (puntos 1 a 6 del Excel "Ver Historial - Cálculo"): historial
* de ingresos por columna de periodo, parámetros SUNAT, liquidación anual, escala por tramos * de ingresos por columna de periodo, parámetros SUNAT, liquidación anual, escala por tramos
* y control de retenciones. * y control de retenciones.
*/ */
public RentaQuintaDetalleCompletoDTO toDetalleCompleto(RentaQuinta boleta, List<RentaQuinta> historialAnio, int mesCorte, CalculoRentaQuinta calculo) { public RentaQuintaDetalleCompletoDTO toDetalleCompleto(RentaQuinta boleta, List<RentaQuinta> historialAnio, int mesCorte, CalculoRentaQuinta calculo,
List<RentaQuintaDetalleCompletoDTO.MesIngresosDTO> matrizMensual) {
// El total del bloque "pasado" usa el mismo historialReal que ya entra a la base // El total del bloque "pasado" usa el mismo historialReal que ya entra a la base
// imponible en RentaQuintaCalculadora, en vez de recalcularlo aparte: así el punto 1 // imponible en RentaQuintaCalculadora, en vez de recalcularlo aparte: así el punto 1
// siempre queda consistente con lo que efectivamente se usó para el impuesto anual. // siempre queda consistente con lo que efectivamente se usó para el impuesto anual.
...@@ -173,7 +177,42 @@ public class RentaQuintaMapper { ...@@ -173,7 +177,42 @@ public class RentaQuintaMapper {
retencionOtrosEmpleadoresTotal, retencionOtrosEmpleadoresTotal,
saldoPendiente, calculo.divisorMes(), calculo.retencionDelMes()); saldoPendiente, calculo.divisorMes(), calculo.retencionDelMes());
return new RentaQuintaDetalleCompletoDTO(toDetalle(boleta), registro, parametros, liquidacion, escala, control); return new RentaQuintaDetalleCompletoDTO(toDetalle(boleta), registro, parametros, liquidacion, escala, control, matrizMensual);
}
public Map<String, BigDecimal> conceptosMensuales(List<RentaQuinta> boletas) {
Map<String, BigDecimal> conceptos = new LinkedHashMap<>();
conceptos.put("basico", sumar(boletas, RentaQuinta::getBasico));
conceptos.put("asignacionFamiliar", sumar(boletas, RentaQuinta::getAsignacionFamiliar));
conceptos.put("movilidad", sumar(boletas, RentaQuinta::getMovilidad));
conceptos.put("vacacionesGozadas", sumar(boletas, RentaQuinta::getVacacionesGozadasRemVaca));
conceptos.put("licencias", sumar(boletas, RentaQuinta::getLicenciaGoceHaber, RentaQuinta::getLicenciaSaludFamiliarLey30012));
conceptos.put("horasExtras", sumar(boletas, RentaQuinta::getHorasExtras25, RentaQuinta::getHorasExtras35));
conceptos.put("dominical", sumar(boletas, RentaQuinta::getDominical));
conceptos.put("bonos", sumar(boletas, RentaQuinta::getBonoExtraordinario, RentaQuinta::getBonoProductividad,
RentaQuinta::getBonoDesempeno, RentaQuinta::getBonoAniversario));
conceptos.put("apoyoAsignaciones", sumar(boletas, RentaQuinta::getApoyoSalud, RentaQuinta::getAsignacionEstudios,
RentaQuinta::getAsignacionFallecimientoFamiliar));
conceptos.put("reintegro", sumar(boletas, RentaQuinta::getReintegro));
conceptos.put("canastaObsequiosVales", sumar(boletas, RentaQuinta::getCanastaNavidad, RentaQuinta::getValeConsumo,
RentaQuinta::getObsequios, RentaQuinta::getCanastaNavidad2, RentaQuinta::getValeConsumoDescuento));
conceptos.put("compraVacaciones", sumar(boletas, RentaQuinta::getCompensacionVacacional));
conceptos.put("vacacionesTruncas", sumar(boletas, RentaQuinta::getVacacionesTruncas));
conceptos.put("gratificacionesTruncas", sumar(boletas, RentaQuinta::getGratificacionProporcional));
conceptos.put("bonificacionTrunca", sumar(boletas, RentaQuinta::getBonificacionExtraordinaria));
conceptos.put("sumaGraciosa", sumar(boletas, RentaQuinta::getSumaGraciosa));
conceptos.put("gratificacion", sumar(boletas, RentaQuinta::getGratificacionJulioDiciembre, RentaQuinta::getBonificacionExtraordinaria2));
conceptos.put("utilidades", sumar(boletas, RentaQuinta::getUtilidades));
return conceptos;
}
@SafeVarargs
private final BigDecimal sumar(List<RentaQuinta> boletas, Function<RentaQuinta, BigDecimal>... campos) {
BigDecimal total = BigDecimal.ZERO;
for (RentaQuinta boleta : boletas) {
for (Function<RentaQuinta, BigDecimal> campo : campos) total = total.add(orZero(campo.apply(boleta)));
}
return total;
} }
/** /**
......
...@@ -9,6 +9,18 @@ import org.springframework.data.jpa.repository.Query; ...@@ -9,6 +9,18 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
public interface RentaQuintaGrabadaRepository extends JpaRepository<RentaQuintaGrabada, RentaQuintaGrabadaId> { public interface RentaQuintaGrabadaRepository extends JpaRepository<RentaQuintaGrabada, RentaQuintaGrabadaId> {
@Query("""
select r from RentaQuintaGrabada r
where r.anio = :anio and r.numeroDocumento = :numeroDocumento
and trim(r.boletaTipoPersonal) = :tipoPersonal
and coalesce(trim(r.tipoPago), '') = :tipoPago
""")
List<RentaQuintaGrabada> retencionesAnuales(
@Param("anio") Integer anio,
@Param("numeroDocumento") String numeroDocumento,
@Param("tipoPersonal") String tipoPersonal,
@Param("tipoPago") String tipoPago);
@Query("select r from RentaQuintaGrabada r where r.anio = :anio and r.mes = :mes and r.numeroDocumento in :numerosDocumento") @Query("select r from RentaQuintaGrabada r where r.anio = :anio and r.mes = :mes and r.numeroDocumento in :numerosDocumento")
List<RentaQuintaGrabada> findByAnioAndMesAndNumeroDocumentoIn( List<RentaQuintaGrabada> findByAnioAndMesAndNumeroDocumentoIn(
@Param("anio") Integer anio, @Param("anio") Integer anio,
......
...@@ -2,10 +2,12 @@ package so.planilla_be.rentaquinta.service; ...@@ -2,10 +2,12 @@ package so.planilla_be.rentaquinta.service;
import so.planilla_be.rentaquinta.dto.FiltroItemDTO; import so.planilla_be.rentaquinta.dto.FiltroItemDTO;
import so.planilla_be.rentaquinta.dto.TipoFiltro; import so.planilla_be.rentaquinta.dto.TipoFiltro;
import so.planilla_be.rentaquinta.dto.PeriodoFiltroDTO;
import java.util.List; import java.util.List;
public interface FiltrosService { public interface FiltrosService {
List<FiltroItemDTO> listar(TipoFiltro tipo); List<FiltroItemDTO> listar(TipoFiltro tipo);
List<PeriodoFiltroDTO> listarPeriodos();
} }
...@@ -4,6 +4,7 @@ import org.springframework.jdbc.core.JdbcTemplate; ...@@ -4,6 +4,7 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import so.planilla_be.rentaquinta.dto.FiltroItemDTO; import so.planilla_be.rentaquinta.dto.FiltroItemDTO;
import so.planilla_be.rentaquinta.dto.TipoFiltro; import so.planilla_be.rentaquinta.dto.TipoFiltro;
import so.planilla_be.rentaquinta.dto.PeriodoFiltroDTO;
import so.planilla_be.rentaquinta.service.FiltrosService; import so.planilla_be.rentaquinta.service.FiltrosService;
import java.util.List; import java.util.List;
...@@ -23,6 +24,12 @@ public class FiltrosServiceImpl implements FiltrosService { ...@@ -23,6 +24,12 @@ public class FiltrosServiceImpl implements FiltrosService {
} }
@Override @Override
public List<PeriodoFiltroDTO> listarPeriodos() {
return jdbcTemplate.query("select id_periodo, mes, anio from planilla.pl_periodo order by anio desc, mes desc",
(rs, rowNum) -> new PeriodoFiltroDTO(rs.getInt("id_periodo"), rs.getInt("mes"), rs.getInt("anio")));
}
@Override
public List<FiltroItemDTO> listar(TipoFiltro tipo) { public List<FiltroItemDTO> listar(TipoFiltro tipo) {
String sql = switch (tipo) { String sql = switch (tipo) {
case AREA -> "select codarea as id, desarea as text from nuevo.area order by desarea"; case AREA -> "select codarea as id, desarea as text from nuevo.area order by desarea";
......
...@@ -39,6 +39,7 @@ import so.planilla_be.utilidades.entity.Utilidad; ...@@ -39,6 +39,7 @@ import so.planilla_be.utilidades.entity.Utilidad;
import so.planilla_be.utilidades.repository.UtilidadRepository; import so.planilla_be.utilidades.repository.UtilidadRepository;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
...@@ -271,9 +272,80 @@ public class RentaQuintaServiceImpl implements RentaQuintaService { ...@@ -271,9 +272,80 @@ public class RentaQuintaServiceImpl implements RentaQuintaService {
"No existe boleta para documento %s, periodo %d-%02d".formatted(numeroDocumento, anio, mes)); "No existe boleta para documento %s, periodo %d-%02d".formatted(numeroDocumento, anio, mes));
} }
TipoPlanilla tipoPlanilla = tipoPlanilla(tipoPlanillaSolicitado, a.referencia()); TipoPlanilla tipoPlanilla = tipoPlanilla(tipoPlanillaSolicitado, a.referencia());
CalculoRentaQuinta calculo = calcular(a, historialAnio, anio, mesCorte, tipoPlanilla, tipoPago, DatosExternos externos = cargarDatosExternos(List.of(numeroDocumento), anio);
cargarDatosExternos(List.of(numeroDocumento), anio)); CalculoRentaQuinta calculo = calcular(a, historialAnio, anio, mesCorte, tipoPlanilla, tipoPago, externos);
return mapper.toDetalleCompleto(a.referencia(), historialAnio, mesCorte, calculo); return mapper.toDetalleCompleto(a.referencia(), historialAnio, mesCorte, calculo,
matrizMensual(a, historialAnio, mesCorte, tipoPlanilla, tipoPago, externos, calculo));
}
private List<RentaQuintaDetalleCompletoDTO.MesIngresosDTO> matrizMensual(
Agregado a, List<RentaQuinta> historial, int mesCorte, TipoPlanilla tipoPlanilla, TipoPago tipoPago,
DatosExternos externos, CalculoRentaQuinta calculo) {
String documento = a.referencia().getNumeroDocumento();
int mesReferencia = a.referencia().getBoletaMes();
ModalidadCalculo modalidad = modalidad(tipoPlanilla, tipoPago, historial, mesReferencia);
boolean porHora = modalidad == ModalidadCalculo.DOCENTE_POR_HORA;
String tipoPagoRetencion = !tipoPlanilla.esDocente() ? ""
: tipoPago != null ? tipoPago.getCodigo()
: porHora ? TipoPago.POR_HORA.getCodigo()
: modalidad == ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL ? TipoPago.MONTO_FIJO.getCodigo() : "";
Map<Integer, BigDecimal> retencionesGrabadas = rentaQuintaGrabadaRepository
.retencionesAnuales(a.referencia().getBoletaAnio(), documento, tipoPlanilla.getCodigo(), tipoPagoRetencion)
.stream().filter(r -> r.getRetencionMesActual() != null)
.collect(Collectors.toMap(RentaQuintaGrabada::getMes, RentaQuintaGrabada::getRetencionMesActual));
List<ProyeccionDocenteHora> docente = externos.proyeccionDocenteHora().getOrDefault(documento, List.of());
List<RetencionOtrosEmpleadores> otros = externos.otrosEmpleadores().getOrDefault(documento, List.of());
BigDecimal sueldo = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getSueldoBruto);
BigDecimal baseGratificacion = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getBasicoGratificacion);
BigDecimal familiar = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getAsignacionFamiliar);
BigDecimal movilidad = sumaMesCorte(historial, esMesCorte(mesReferencia), RentaQuinta::getMovilidad);
List<RentaQuintaDetalleCompletoDTO.MesIngresosDTO> matriz = new ArrayList<>();
BigDecimal saldoSueldo = calculo.proyeccionSueldoBruto();
BigDecimal saldoGratificacion = calculo.proyeccionGratificaciones();
for (int mes = 1; mes <= 12; mes++) {
final int mesActual = mes;
// Las boletas posteriores al corte no sustituyen la proyección usada en los puntos 1 a 5.
List<RentaQuinta> reales = mes <= mesCorte
? historial.stream().filter(esMesCorte(mes)).toList() : List.of();
boolean proyectado = mes > mesCorte || (mes == mesCorte && reales.isEmpty());
Map<String, BigDecimal> conceptos = mapper.conceptosMensuales(reales);
if (proyectado) {
BigDecimal factor = mes == 12 && modalidad == ModalidadCalculo.MONTO_FIJO_DICIEMBRE_PROPORCIONAL
? BigDecimal.valueOf(16).divide(BigDecimal.valueOf(30), 10, RoundingMode.HALF_UP) : BigDecimal.ONE;
BigDecimal sueldoMes = mes == mesCorte ? calculo.estimadoMesCorteSueldoBruto()
: porHora ? sumaProyeccion(docente, tipoPlanilla.getCodigo(), m -> m == mesActual, ProyeccionDocenteHora::getRemunBrutaFutura)
: sueldo.multiply(factor).setScale(2, RoundingMode.HALF_UP);
if (mes > mesCorte) {
if (mes == 12) sueldoMes = saldoSueldo;
saldoSueldo = saldoSueldo.subtract(sueldoMes);
}
BigDecimal familiarMes = porHora ? BigDecimal.ZERO : familiar.multiply(factor).setScale(2, RoundingMode.HALF_UP);
BigDecimal movilidadMes = porHora ? BigDecimal.ZERO : movilidad.multiply(factor).setScale(2, RoundingMode.HALF_UP);
conceptos.put("basico", sueldoMes.subtract(familiarMes).subtract(movilidadMes));
conceptos.put("asignacionFamiliar", familiarMes);
conceptos.put("movilidad", movilidadMes);
BigDecimal gratificacion = BigDecimal.ZERO;
if (mes == mesCorte) gratificacion = calculo.estimadoMesCorteGratificacion();
else if (mes == 7 || mes == 12) {
gratificacion = porHora ? sumaProyeccion(docente, TIPO_GRATIFICACION, m -> m == mesActual, ProyeccionDocenteHora::getGratificacion)
: baseGratificacion.multiply(new BigDecimal("1.09")).multiply(factor).setScale(2, RoundingMode.HALF_UP);
if (mes == 12) gratificacion = saldoGratificacion;
saldoGratificacion = saldoGratificacion.subtract(gratificacion);
}
conceptos.put("gratificacion", gratificacion);
} else if (mes == mesCorte) {
conceptos.merge("basico", calculo.ajusteSueldoBrutoMesCorte(), BigDecimal::add);
}
conceptos.put("otrosEmpleadores", otros.stream().filter(r -> r.getMes() == mesActual)
.map(r -> orZero(r.getImporte())).reduce(BigDecimal.ZERO, BigDecimal::add));
BigDecimal total = conceptos.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
// Una retención grabada de cero también tiene prioridad sobre la boleta.
BigDecimal retenido = retencionesGrabadas.get(mes);
if (retenido == null) retenido = sumaMesCorte(historial, esMesCorte(mes), RentaQuinta::getRenta5taCategoria);
matriz.add(new RentaQuintaDetalleCompletoDTO.MesIngresosDTO(mes,
proyectado || (porHora && mes == mesCorte), conceptos, total, retenido.negate()));
}
return matriz;
} }
/** /**
......
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