Commit d2e3731b by Vanesa Puma Yamo

Merge remote-tracking branch 'origin/master'

parents 757f524f 9a2e7ca9
# API Retención Otros Empleadores — Guía para el Frontend
Base path: `/api/v1/retencion-otros-empleadores`
Swagger UI: `/docs`
CRUD sobre `planilla.retencion_otros_empleadores`: retenciones declaradas por **otros
empleadores** distintos de Saco Oliveros, registradas manualmente mes a mes por trabajador. Clave
primaria compuesta: `documento_trabajador + anio + mes`.
---
## 1. GET `/api/v1/retencion-otros-empleadores/{documentoTrabajador}/{anio}` — Historial anual
Todos los meses registrados para un trabajador en un año.
### Path params
| Parámetro | Tipo | Notas |
|---|---|---|
| `documentoTrabajador` | String | DNI/CE del trabajador |
| `anio` | Integer | Año a consultar |
### Ejemplo
```
GET /api/v1/retencion-otros-empleadores/09742695/2026
```
### Response — `RetencionOtrosEmpleadoresListadoDTO`
Objeto con `numeroDocumento` + `personal` (nombre del trabajador) a nivel raíz, y `data` (array
ordenado por mes, **sin** documento ni nombre repetidos por fila). **No es 404 si `data` viene
vacío** — significa que el trabajador no tiene meses registrados ese año. `numeroDocumento` y
`personal` se envían **siempre**, tenga o no registros.
```json
{
"numeroDocumento": "09742695",
"personal": "PALOMINO ANICAMA YULIANA MARLENY",
"data": [
{
"anio": 2026,
"mes": 3,
"importe": 1500.00,
"retencion": 120.00,
"fechaRegistro": "2026-03-05T10:30:00"
}
]
}
```
Sin registros (mismo trabajador, año sin data):
```json
{
"numeroDocumento": "09742695",
"personal": "PALOMINO ANICAMA YULIANA MARLENY",
"data": []
}
```
| Campo | Tipo | Descripción |
|---|---|---|
| `numeroDocumento` | String | Documento consultado (eco del path param) |
| `personal` | String | Apellido + nombre desde `nuevo.personal` (join por `numero_documento`). `null` si no hay match en `personal`. Presente aunque `data` esté vacío |
| `data` | Array | Meses registrados, ordenados por mes |
#### Objeto de `data[]` — `RetencionOtrosEmpleadoresItemDTO`
| Campo | Tipo | Descripción |
|---|---|---|
| `anio` | Integer | Año del registro |
| `mes` | Integer | 1–12 |
| `importe` | BigDecimal | Importe declarado por el otro empleador |
| `retencion` | BigDecimal | Retención declarada por el otro empleador |
| `fechaRegistro` | LocalDateTime | Fecha/hora en que se creó el registro (la setea el backend) |
---
## 2. POST `/api/v1/retencion-otros-empleadores/{documentoTrabajador}/{anio}/{mes}` — Crear
### Path params
| Parámetro | Tipo | Notas |
|---|---|---|
| `documentoTrabajador` | String | |
| `anio` | Integer | |
| `mes` | Integer | 1–12 |
### Body — `RetencionOtrosEmpleadoresRequestDTO`
```json
{
"importe": 1500.00,
"retencion": 120.00
}
```
Ambos campos obligatorios.
### Ejemplo
```
POST /api/v1/retencion-otros-empleadores/09742695/2026/3
Content-Type: application/json
{ "importe": 1500.00, "retencion": 120.00 }
```
### Response
`201 Created` con el `RetencionOtrosEmpleadoresDTO` creado (`fechaRegistro` la asigna el backend).
`409 Conflict` si ya existe un registro para esa combinación `documentoTrabajador + anio + mes`
(usar PUT para modificarlo).
---
## 3. PUT `/api/v1/retencion-otros-empleadores/{documentoTrabajador}/{anio}/{mes}` — Actualizar
Mismo path y body que el POST. Reemplaza `importe` y `retencion` del registro existente
(`fechaRegistro` no cambia).
### Ejemplo
```
PUT /api/v1/retencion-otros-empleadores/09742695/2026/3
Content-Type: application/json
{ "importe": 1600.00, "retencion": 130.00 }
```
### Response
`200 OK` con el `RetencionOtrosEmpleadoresDTO` actualizado.
`404` si no existe el registro para esa clave.
---
## 4. DELETE `/api/v1/retencion-otros-empleadores/{documentoTrabajador}/{anio}/{mes}` — Eliminar
### Ejemplo
```
DELETE /api/v1/retencion-otros-empleadores/09742695/2026/3
```
### Response
`204 No Content`.
`404` si no existe el registro para esa clave.
---
## 5. Manejo de errores
Mismo shape que el resto del API, sin stack traces:
```json
{
"timestamp": "2026-09-11T15:00:00Z",
"status": 404,
"message": "descripción del error"
}
```
| Status | Causa |
|---|---|
| `404` | PUT o DELETE sobre una clave (`documentoTrabajador`+`anio`+`mes`) que no existe |
| `409` | POST sobre una clave que ya existe |
| `400` | `mes` fuera de rango (1–12) |
| `400` | Body inválido: falta `importe` o `retencion` |
| `400` | Falta un parámetro obligatorio en el path |
El frontend debe leer siempre `message` para mostrar el error al usuario.
---
## 6. `personal` / `nombreCompleto`
Se resuelve con un join a `nuevo.personal` por `numero_documento`. Esa tabla no tiene
`numero_documento` único (hay trabajadores con varios `codper` y algunos documentos placeholder
repetidos muchas veces) — se toma el registro con el `codper` más alto. Si no hay match, el campo
sale `null`.
El listado por año (`GET .../{documentoTrabajador}/{anio}`) resuelve y devuelve este nombre en
`personal` (nivel raíz de la respuesta, junto a `numeroDocumento`) **independientemente de si hay
filas en `data`**. POST y PUT siguen devolviendo el shape plano (`RetencionOtrosEmpleadoresDTO`,
con `documentoTrabajador` y `nombreCompleto` en el propio objeto) — no cambiaron.
## 7. Notas para el frontend
- No hay paginación: el listado por año trae todos los meses de una vez (máximo 12 filas).
- No hay endpoint de búsqueda por documento sin año — siempre se pide `anio` junto con
`documentoTrabajador`.
- Sin autenticación todavía (igual que el resto del API — se integrará con
Trismegisto/Saco Oliveros más adelante).
package so.planilla_be.otrosempleadores.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.http.HttpStatus;
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.PutMapping;
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.otrosempleadores.dto.RetencionOtrosEmpleadoresDTO;
import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresListadoDTO;
import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresRequestDTO;
import so.planilla_be.otrosempleadores.service.RetencionOtrosEmpleadoresService;
@Tag(name = "Retención otros empleadores", description = "CRUD de retenciones declaradas por otros empleadores")
@Validated
@RestController
@RequestMapping("/api/v1/retencion-otros-empleadores")
public class RetencionOtrosEmpleadoresController {
private final RetencionOtrosEmpleadoresService service;
public RetencionOtrosEmpleadoresController(RetencionOtrosEmpleadoresService service) {
this.service = service;
}
@Operation(summary = "Meses del año declarados para un trabajador")
@GetMapping("/{documentoTrabajador}/{anio}")
public RetencionOtrosEmpleadoresListadoDTO listarPorAnio(
@PathVariable String documentoTrabajador,
@PathVariable Integer anio) {
return service.listarPorAnio(documentoTrabajador, anio);
}
@Operation(summary = "Registrar retención de un mes")
@PostMapping("/{documentoTrabajador}/{anio}/{mes}")
public ResponseEntity<RetencionOtrosEmpleadoresDTO> crear(
@PathVariable String documentoTrabajador,
@PathVariable Integer anio,
@PathVariable @Min(1) @Max(12) Integer mes,
@RequestBody @Valid RetencionOtrosEmpleadoresRequestDTO request) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(service.crear(documentoTrabajador, anio, mes, request));
}
@Operation(summary = "Actualizar retención de un mes")
@PutMapping("/{documentoTrabajador}/{anio}/{mes}")
public RetencionOtrosEmpleadoresDTO actualizar(
@PathVariable String documentoTrabajador,
@PathVariable Integer anio,
@PathVariable @Min(1) @Max(12) Integer mes,
@RequestBody @Valid RetencionOtrosEmpleadoresRequestDTO request) {
return service.actualizar(documentoTrabajador, anio, mes, request);
}
@Operation(summary = "Eliminar retención de un mes")
@DeleteMapping("/{documentoTrabajador}/{anio}/{mes}")
public ResponseEntity<Void> eliminar(
@PathVariable String documentoTrabajador,
@PathVariable Integer anio,
@PathVariable @Min(1) @Max(12) Integer mes) {
service.eliminar(documentoTrabajador, anio, mes);
return ResponseEntity.noContent().build();
}
}
package so.planilla_be.otrosempleadores.dto;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public record RetencionOtrosEmpleadoresDTO(
String documentoTrabajador,
String nombreCompleto,
Integer anio,
Integer mes,
BigDecimal importe,
BigDecimal retencion,
LocalDateTime fechaRegistro
) {}
package so.planilla_be.otrosempleadores.dto;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public record RetencionOtrosEmpleadoresItemDTO(
Integer anio,
Integer mes,
BigDecimal importe,
BigDecimal retencion,
LocalDateTime fechaRegistro
) {}
package so.planilla_be.otrosempleadores.dto;
import java.util.List;
public record RetencionOtrosEmpleadoresListadoDTO(
String numeroDocumento,
String personal,
List<RetencionOtrosEmpleadoresItemDTO> data
) {}
package so.planilla_be.otrosempleadores.dto;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
public record RetencionOtrosEmpleadoresRequestDTO(
@NotNull BigDecimal importe,
@NotNull BigDecimal retencion
) {}
package so.planilla_be.otrosempleadores.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "personal", schema = "nuevo")
public class Personal {
@Id
@Column(name = "codper")
private Integer codper;
@Column(name = "numero_documento", length = 10)
private String numeroDocumento;
@Column(name = "apellido", length = 50)
private String apellido;
@Column(name = "nombre", length = 100)
private String nombre;
}
package so.planilla_be.otrosempleadores.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Getter
@Setter
@Entity
@Table(name = "retencion_otros_empleadores")
@IdClass(RetencionOtrosEmpleadoresId.class)
public class RetencionOtrosEmpleadores {
@Id
@Column(name = "documento_trabajador", length = 15)
private String documentoTrabajador;
@Id
@Column(name = "anio")
private Integer anio;
@Id
@Column(name = "mes")
private Integer mes;
@Column(name = "importe")
private BigDecimal importe;
@Column(name = "retencion")
private BigDecimal retencion;
@Column(name = "fecha_registro")
private LocalDateTime fechaRegistro;
}
package so.planilla_be.otrosempleadores.entity;
import java.io.Serializable;
import java.util.Objects;
public class RetencionOtrosEmpleadoresId implements Serializable {
private String documentoTrabajador;
private Integer anio;
private Integer mes;
public RetencionOtrosEmpleadoresId() {
}
public RetencionOtrosEmpleadoresId(String documentoTrabajador, Integer anio, Integer mes) {
this.documentoTrabajador = documentoTrabajador;
this.anio = anio;
this.mes = mes;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof RetencionOtrosEmpleadoresId that)) {
return false;
}
return Objects.equals(documentoTrabajador, that.documentoTrabajador)
&& Objects.equals(anio, that.anio)
&& Objects.equals(mes, that.mes);
}
@Override
public int hashCode() {
return Objects.hash(documentoTrabajador, anio, mes);
}
}
package so.planilla_be.otrosempleadores.exception;
public class RetencionOtrosEmpleadoresNoEncontradaException extends RuntimeException {
public RetencionOtrosEmpleadoresNoEncontradaException(String message) {
super(message);
}
}
package so.planilla_be.otrosempleadores.exception;
public class RetencionOtrosEmpleadoresYaExisteException extends RuntimeException {
public RetencionOtrosEmpleadoresYaExisteException(String message) {
super(message);
}
}
package so.planilla_be.otrosempleadores.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import so.planilla_be.otrosempleadores.entity.Personal;
import java.util.Optional;
public interface PersonalRepository extends JpaRepository<Personal, Integer> {
Optional<Personal> findFirstByNumeroDocumentoOrderByCodperDesc(String numeroDocumento);
}
package so.planilla_be.otrosempleadores.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import so.planilla_be.otrosempleadores.entity.RetencionOtrosEmpleadores;
import so.planilla_be.otrosempleadores.entity.RetencionOtrosEmpleadoresId;
import java.util.List;
public interface RetencionOtrosEmpleadoresRepository
extends JpaRepository<RetencionOtrosEmpleadores, RetencionOtrosEmpleadoresId> {
List<RetencionOtrosEmpleadores> findByDocumentoTrabajadorAndAnioOrderByMes(
String documentoTrabajador, Integer anio);
}
package so.planilla_be.otrosempleadores.service;
import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresDTO;
import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresListadoDTO;
import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresRequestDTO;
public interface RetencionOtrosEmpleadoresService {
RetencionOtrosEmpleadoresListadoDTO listarPorAnio(String documentoTrabajador, Integer anio);
RetencionOtrosEmpleadoresDTO crear(String documentoTrabajador, Integer anio, Integer mes,
RetencionOtrosEmpleadoresRequestDTO request);
RetencionOtrosEmpleadoresDTO actualizar(String documentoTrabajador, Integer anio, Integer mes,
RetencionOtrosEmpleadoresRequestDTO request);
void eliminar(String documentoTrabajador, Integer anio, Integer mes);
}
package so.planilla_be.otrosempleadores.service.impl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresDTO;
import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresItemDTO;
import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresListadoDTO;
import so.planilla_be.otrosempleadores.dto.RetencionOtrosEmpleadoresRequestDTO;
import so.planilla_be.otrosempleadores.entity.RetencionOtrosEmpleadores;
import so.planilla_be.otrosempleadores.entity.RetencionOtrosEmpleadoresId;
import so.planilla_be.otrosempleadores.exception.RetencionOtrosEmpleadoresNoEncontradaException;
import so.planilla_be.otrosempleadores.exception.RetencionOtrosEmpleadoresYaExisteException;
import so.planilla_be.otrosempleadores.repository.PersonalRepository;
import so.planilla_be.otrosempleadores.repository.RetencionOtrosEmpleadoresRepository;
import so.planilla_be.otrosempleadores.service.RetencionOtrosEmpleadoresService;
import java.time.LocalDateTime;
import java.util.List;
@Service
@Transactional(readOnly = true)
public class RetencionOtrosEmpleadoresServiceImpl implements RetencionOtrosEmpleadoresService {
private final RetencionOtrosEmpleadoresRepository repository;
private final PersonalRepository personalRepository;
public RetencionOtrosEmpleadoresServiceImpl(RetencionOtrosEmpleadoresRepository repository,
PersonalRepository personalRepository) {
this.repository = repository;
this.personalRepository = personalRepository;
}
@Override
public RetencionOtrosEmpleadoresListadoDTO listarPorAnio(String documentoTrabajador, Integer anio) {
String nombreCompleto = nombreCompleto(documentoTrabajador);
List<RetencionOtrosEmpleadoresItemDTO> data = repository
.findByDocumentoTrabajadorAndAnioOrderByMes(documentoTrabajador, anio).stream()
.map(this::toItemDto)
.toList();
return new RetencionOtrosEmpleadoresListadoDTO(documentoTrabajador, nombreCompleto, data);
}
private String nombreCompleto(String documentoTrabajador) {
return personalRepository.findFirstByNumeroDocumentoOrderByCodperDesc(documentoTrabajador)
.map(p -> "%s %s".formatted(
p.getApellido() == null ? "" : p.getApellido().trim(),
p.getNombre() == null ? "" : p.getNombre().trim()).trim())
.filter(nombre -> !nombre.isBlank())
.orElse(null);
}
@Override
@Transactional
public RetencionOtrosEmpleadoresDTO crear(String documentoTrabajador, Integer anio, Integer mes,
RetencionOtrosEmpleadoresRequestDTO request) {
RetencionOtrosEmpleadoresId id = new RetencionOtrosEmpleadoresId(documentoTrabajador, anio, mes);
if (repository.existsById(id)) {
throw new RetencionOtrosEmpleadoresYaExisteException(
"Ya existe retención de otros empleadores para documento %s, periodo %d-%02d"
.formatted(documentoTrabajador, anio, mes));
}
RetencionOtrosEmpleadores entidad = new RetencionOtrosEmpleadores();
entidad.setDocumentoTrabajador(documentoTrabajador);
entidad.setAnio(anio);
entidad.setMes(mes);
entidad.setImporte(request.importe());
entidad.setRetencion(request.retencion());
entidad.setFechaRegistro(LocalDateTime.now());
return toDto(repository.save(entidad), nombreCompleto(documentoTrabajador));
}
@Override
@Transactional
public RetencionOtrosEmpleadoresDTO actualizar(String documentoTrabajador, Integer anio, Integer mes,
RetencionOtrosEmpleadoresRequestDTO request) {
RetencionOtrosEmpleadores entidad = obtenerOLanzar(documentoTrabajador, anio, mes);
entidad.setImporte(request.importe());
entidad.setRetencion(request.retencion());
return toDto(repository.save(entidad), nombreCompleto(documentoTrabajador));
}
@Override
@Transactional
public void eliminar(String documentoTrabajador, Integer anio, Integer mes) {
RetencionOtrosEmpleadores entidad = obtenerOLanzar(documentoTrabajador, anio, mes);
repository.delete(entidad);
}
private RetencionOtrosEmpleadores obtenerOLanzar(String documentoTrabajador, Integer anio, Integer mes) {
return repository.findById(new RetencionOtrosEmpleadoresId(documentoTrabajador, anio, mes))
.orElseThrow(() -> new RetencionOtrosEmpleadoresNoEncontradaException(
"No existe retención de otros empleadores para documento %s, periodo %d-%02d"
.formatted(documentoTrabajador, anio, mes)));
}
private RetencionOtrosEmpleadoresItemDTO toItemDto(RetencionOtrosEmpleadores entidad) {
return new RetencionOtrosEmpleadoresItemDTO(
entidad.getAnio(),
entidad.getMes(),
entidad.getImporte(),
entidad.getRetencion(),
entidad.getFechaRegistro());
}
private RetencionOtrosEmpleadoresDTO toDto(RetencionOtrosEmpleadores entidad, String nombreCompleto) {
return new RetencionOtrosEmpleadoresDTO(
entidad.getDocumentoTrabajador(),
nombreCompleto,
entidad.getAnio(),
entidad.getMes(),
entidad.getImporte(),
entidad.getRetencion(),
entidad.getFechaRegistro());
}
}
...@@ -8,6 +8,8 @@ import org.springframework.web.bind.MissingServletRequestParameterException; ...@@ -8,6 +8,8 @@ import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; 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.RetencionOtrosEmpleadoresYaExisteException;
import java.time.Instant; import java.time.Instant;
import java.util.Map; import java.util.Map;
...@@ -21,6 +23,16 @@ public class GlobalExceptionHandler { ...@@ -21,6 +23,16 @@ public class GlobalExceptionHandler {
return build(HttpStatus.NOT_FOUND, ex.getMessage()); return build(HttpStatus.NOT_FOUND, ex.getMessage());
} }
@ExceptionHandler(RetencionOtrosEmpleadoresNoEncontradaException.class)
public ResponseEntity<Map<String, Object>> handleNotFound(RetencionOtrosEmpleadoresNoEncontradaException ex) {
return build(HttpStatus.NOT_FOUND, ex.getMessage());
}
@ExceptionHandler(RetencionOtrosEmpleadoresYaExisteException.class)
public ResponseEntity<Map<String, Object>> handleConflict(RetencionOtrosEmpleadoresYaExisteException ex) {
return build(HttpStatus.CONFLICT, ex.getMessage());
}
/** Filtros fuera de rango (p. ej. mesCorte=13). */ /** Filtros fuera de rango (p. ej. mesCorte=13). */
@ExceptionHandler(ConstraintViolationException.class) @ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, Object>> handleValidation(ConstraintViolationException ex) { public ResponseEntity<Map<String, Object>> handleValidation(ConstraintViolationException ex) {
......
...@@ -14,7 +14,6 @@ import so.planilla_be.rentaquinta.dto.RentaQuintaDetalleCompletoDTO.RegistroHist ...@@ -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.RentaQuintaDetalleCompletoDTO.TramoDTO;
import so.planilla_be.rentaquinta.dto.RentaQuintaResumenDTO; import so.planilla_be.rentaquinta.dto.RentaQuintaResumenDTO;
import so.planilla_be.rentaquinta.entity.RentaQuinta; import so.planilla_be.rentaquinta.entity.RentaQuinta;
import so.planilla_be.rentaquinta.repository.RentaQuintaResumenProjection;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
...@@ -23,20 +22,21 @@ import java.util.function.Predicate; ...@@ -23,20 +22,21 @@ import java.util.function.Predicate;
@Component @Component
public class RentaQuintaMapper { public class RentaQuintaMapper {
public RentaQuintaResumenDTO toResumen(RentaQuintaResumenProjection p, Integer periodo, public RentaQuintaResumenDTO toResumen(RentaQuinta referencia, Integer codper, Integer periodo, Integer mesCorte,
Integer mesCorte, CalculoRentaQuinta calculo) { BigDecimal historialReal, BigDecimal planillaCargada,
BigDecimal retenidoAnterior, CalculoRentaQuinta calculo) {
return new RentaQuintaResumenDTO( return new RentaQuintaResumenDTO(
p.getCodper(), codper,
periodo, periodo,
p.getNumeroDocumento(), referencia.getNumeroDocumento(),
nombreCompleto(p.getApellidoPaterno(), p.getApellidoMaterno(), p.getNombres()), nombreCompleto(referencia.getApellidoPaterno(), referencia.getApellidoMaterno(), referencia.getNombres()),
mesCorte, mesCorte,
orZero(p.getHistorialReal()), orZero(historialReal),
orZero(p.getPlanillaCargada()), orZero(planillaCargada),
calculo.proyeccionFutura(), calculo.proyeccionFutura(),
calculo.rentaBrutaAnual(), calculo.rentaBrutaAnual(),
calculo.impuestoAnual(), calculo.impuestoAnual(),
orZero(p.getRetenidoAnterior()), orZero(retenidoAnterior),
calculo.retencionDelMes()); 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> ...@@ -27,44 +27,6 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
or upper(concat(r.apellidoPaterno, ' ', r.apellidoMaterno, ' ', r.nombres)) like :texto) 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). */ /** Meses de un trabajador en un año, ordenados cronológicamente (historial anual). */
List<RentaQuinta> findByCodperAndBoletaAnioOrderByBoletaMesAsc( List<RentaQuinta> findByCodperAndBoletaAnioOrderByBoletaMesAsc(
Integer codper, Integer boletaAnio); Integer codper, Integer boletaAnio);
...@@ -74,49 +36,19 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long> ...@@ -74,49 +36,19 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
Integer codper, Integer boletaAnio, Integer boletaMes); Integer codper, Integer boletaAnio, Integer boletaMes);
/** /**
* Mes de referencia por trabajador: el último con boleta real hasta el mes de corte * Documentos que matchean los filtros de la grilla, paginados y ordenados por apellidos +
* (inclusive). Coincide con el mes de corte cuando este sí está cargado; si no, cae al mes * nombres. Solo trae la clave (numeroDocumento) — el service arma la fila agregando en Java
* real anterior y ese pasa a ser la base de la proyección (ver RentaQuintaCalculadora). * sobre {@link #historialHastaMes}, no en SQL.
*/
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).
*/ */
@Query(value = """ @Query(value = """
select max(r.codper) as codper, select distinct r.numeroDocumento as numeroDocumento,
r.numeroDocumento as numeroDocumento,
r.apellidoPaterno as apellidoPaterno, r.apellidoPaterno as apellidoPaterno,
r.apellidoMaterno as apellidoMaterno, r.apellidoMaterno as apellidoMaterno,
r.nombres as nombres, 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
from RentaQuinta r from RentaQuinta r
where r.boletaAnio = :periodo where r.boletaAnio = :periodo
and r.boletaMes <= :mesCorte and r.boletaMes <= :mesCorte
""" + FILTROS + """ """ + FILTROS + """
group by r.numeroDocumento, r.apellidoPaterno, r.apellidoMaterno, r.nombres
order by r.apellidoPaterno asc, r.apellidoMaterno asc, r.nombres asc order by r.apellidoPaterno asc, r.apellidoMaterno asc, r.nombres asc
""", """,
countQuery = """ countQuery = """
...@@ -125,7 +57,7 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long> ...@@ -125,7 +57,7 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
where r.boletaAnio = :periodo where r.boletaAnio = :periodo
and r.boletaMes <= :mesCorte and r.boletaMes <= :mesCorte
""" + FILTROS) """ + FILTROS)
Page<RentaQuintaResumenProjection> buscarResumen( Page<DocumentoResumenProjection> buscarDocumentos(
@Param("periodo") Integer periodo, @Param("periodo") Integer periodo,
@Param("mesCorte") Integer mesCorte, @Param("mesCorte") Integer mesCorte,
@Param("sede") String sede, @Param("sede") String sede,
...@@ -136,6 +68,19 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long> ...@@ -136,6 +68,19 @@ public interface RentaQuintaRepository extends JpaRepository<RentaQuinta, Long>
@Param("texto") String texto, @Param("texto") String texto,
Pageable pageable); 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). */ /** Boletas completas de un trabajador en un año, ordenadas por mes (pantalla de historial). */
@Query(""" @Query("""
select r from RentaQuinta r 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();
}
...@@ -15,6 +15,11 @@ spring: ...@@ -15,6 +15,11 @@ spring:
hibernate: hibernate:
default_schema: planilla default_schema: planilla
format_sql: true format_sql: 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
# inexistente ("Schema-validation: missing table [nuevo.personal]")
extra_physical_table_types: "FOREIGN TABLE"
threads: threads:
virtual: virtual:
enabled: true enabled: true
......
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