añadidos entidades, repos y facturacionservice

This commit is contained in:
2025-12-30 19:51:04 +01:00
parent 98a5fcaa0b
commit 089641b601
18 changed files with 1200 additions and 1 deletions

View File

@ -0,0 +1,280 @@
package com.imprimelibros.erp.facturacion.service;
import com.imprimelibros.erp.facturacion.*;
import com.imprimelibros.erp.facturacion.dto.FacturaLineaUpsertDto;
import com.imprimelibros.erp.facturacion.dto.FacturaPagoUpsertDto;
import com.imprimelibros.erp.facturacion.repo.FacturaPagoRepository;
import com.imprimelibros.erp.facturacion.repo.FacturaRepository;
import com.imprimelibros.erp.facturacion.repo.SerieFacturaRepository;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
@Service
public class FacturacionService {
private final FacturaRepository facturaRepo;
private final SerieFacturaRepository serieRepo;
private final FacturaPagoRepository pagoRepo;
public FacturacionService(
FacturaRepository facturaRepo,
SerieFacturaRepository serieRepo,
FacturaPagoRepository pagoRepo
) {
this.facturaRepo = facturaRepo;
this.serieRepo = serieRepo;
this.pagoRepo = pagoRepo;
}
// -----------------------
// Estado / Numeración
// -----------------------
@Transactional
public Factura validarFactura(Long facturaId) {
Factura factura = facturaRepo.findById(facturaId)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
// Puedes permitir validar desde borrador solamente (lo normal)
if (factura.getEstado() == EstadoFactura.validada) {
return factura;
}
if (factura.getFechaEmision() == null) {
factura.setFechaEmision(LocalDateTime.now());
}
if (factura.getSerie() == null) {
throw new IllegalStateException("La factura no tiene serie asignada.");
}
// Si ya tiene numero_factura, no reservamos otro
if (factura.getNumeroFactura() == null || factura.getNumeroFactura().isBlank()) {
SerieFactura serieLocked = serieRepo.findByIdForUpdate(factura.getSerie().getId())
.orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + factura.getSerie().getId()));
long next = (serieLocked.getNumeroActual() == null) ? 1L : serieLocked.getNumeroActual();
String numeroFactura = buildNumeroFactura(serieLocked.getPrefijo(), next);
factura.setNumeroFactura(numeroFactura);
// Incrementar contador para la siguiente
serieLocked.setNumeroActual((int) (next + 1)); // si cambias numero_actual a BIGINT en entidad, quita el cast
serieRepo.save(serieLocked);
}
recalcularTotales(factura);
factura.setEstado(EstadoFactura.validada);
return facturaRepo.save(factura);
}
@Transactional
public Factura volverABorrador(Long facturaId) {
Factura factura = facturaRepo.findById(facturaId)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
factura.setEstado(EstadoFactura.borrador);
// No tocamos numero_factura (se conserva) -> evita duplicados y auditoría rara
recalcularTotales(factura);
return facturaRepo.save(factura);
}
private String buildNumeroFactura(String prefijo, long numero) {
String pref = (prefijo == null) ? "" : prefijo.trim();
String num = String.format("%07d", numero);
return pref.isBlank() ? num : (pref + "-" + num);
}
// -----------------------
// Líneas
// -----------------------
@Transactional
public Factura upsertLinea(Long facturaId, FacturaLineaUpsertDto dto) {
Factura factura = facturaRepo.findById(facturaId)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
if (factura.getEstado() != EstadoFactura.borrador) {
throw new IllegalStateException("Solo se pueden editar líneas en facturas en borrador.");
}
FacturaLinea linea;
if (dto.getId() == null) {
linea = new FacturaLinea();
linea.setFactura(factura);
factura.getLineas().add(linea);
} else {
linea = factura.getLineas().stream()
.filter(l -> dto.getId().equals(l.getId()))
.findFirst()
.orElseThrow(() -> new EntityNotFoundException("Línea no encontrada: " + dto.getId()));
}
linea.setDescripcion(dto.getDescripcion());
linea.setCantidad(dto.getCantidad());
// Base por unidad o base total? Tu migración no define precio unitario.
// Asumimos que baseLinea es TOTAL de línea (sin IVA) y cantidad informativa.
linea.setBaseLinea(scale2(dto.getBaseLinea()));
// Iva por checks: calculamos importes, no porcentajes
BigDecimal iva4 = BigDecimal.ZERO;
BigDecimal iva21 = BigDecimal.ZERO;
if (dto.isAplicaIva4() && dto.isAplicaIva21()) {
throw new IllegalArgumentException("Una línea no puede tener IVA 4% y 21% a la vez.");
}
if (dto.isAplicaIva4()) {
iva4 = scale2(linea.getBaseLinea().multiply(new BigDecimal("0.04")));
}
if (dto.isAplicaIva21()) {
iva21 = scale2(linea.getBaseLinea().multiply(new BigDecimal("0.21")));
}
linea.setIva4Linea(iva4);
linea.setIva21Linea(iva21);
linea.setTotalLinea(scale2(linea.getBaseLinea().add(iva4).add(iva21)));
recalcularTotales(factura);
return facturaRepo.save(factura);
}
@Transactional
public Factura borrarLinea(Long facturaId, Long lineaId) {
Factura factura = facturaRepo.findById(facturaId)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
if (factura.getEstado() != EstadoFactura.borrador) {
throw new IllegalStateException("Solo se pueden borrar líneas en facturas en borrador.");
}
boolean removed = factura.getLineas().removeIf(l -> lineaId.equals(l.getId()));
if (!removed) {
throw new EntityNotFoundException("Línea no encontrada: " + lineaId);
}
recalcularTotales(factura);
return facturaRepo.save(factura);
}
// -----------------------
// Pagos
// -----------------------
@Transactional
public Factura upsertPago(Long facturaId, FacturaPagoUpsertDto dto) {
Factura factura = facturaRepo.findById(facturaId)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
// Permitir añadir pagos tanto en borrador como validada (según tu regla)
FacturaPago pago;
if (dto.getId() == null) {
pago = new FacturaPago();
pago.setFactura(factura);
factura.getPagos().add(pago);
} else {
pago = factura.getPagos().stream()
.filter(p -> dto.getId().equals(p.getId()))
.findFirst()
.orElseThrow(() -> new EntityNotFoundException("Pago no encontrado: " + dto.getId()));
}
pago.setMetodoPago(dto.getMetodoPago());
pago.setCantidadPagada(scale2(dto.getCantidadPagada()));
pago.setFechaPago(dto.getFechaPago() != null ? dto.getFechaPago() : LocalDateTime.now());
pago.setNotas(dto.getNotas());
// El tipo_pago de la factura: si tiene un pago, lo reflejamos (último pago manda)
factura.setTipoPago(dto.getMetodoPago());
recalcularTotales(factura);
return facturaRepo.save(factura);
}
@Transactional
public Factura borrarPago(Long facturaId, Long pagoId) {
Factura factura = facturaRepo.findById(facturaId)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
boolean removed = factura.getPagos().removeIf(p -> pagoId.equals(p.getId()));
if (!removed) {
throw new EntityNotFoundException("Pago no encontrado: " + pagoId);
}
recalcularTotales(factura);
return facturaRepo.save(factura);
}
// -----------------------
// Recalcular totales
// -----------------------
@Transactional
public void recalcularTotales(Long facturaId) {
Factura factura = facturaRepo.findById(facturaId)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
recalcularTotales(factura);
facturaRepo.save(factura);
}
private void recalcularTotales(Factura factura) {
BigDecimal base = BigDecimal.ZERO;
BigDecimal iva4 = BigDecimal.ZERO;
BigDecimal iva21 = BigDecimal.ZERO;
BigDecimal total = BigDecimal.ZERO;
if (factura.getLineas() != null) {
for (FacturaLinea l : factura.getLineas()) {
base = base.add(nvl(l.getBaseLinea()));
iva4 = iva4.add(nvl(l.getIva4Linea()));
iva21 = iva21.add(nvl(l.getIva21Linea()));
total = total.add(nvl(l.getTotalLinea()));
}
}
factura.setBaseImponible(scale2(base));
factura.setIva4(scale2(iva4));
factura.setIva21(scale2(iva21));
factura.setTotalFactura(scale2(total));
// total_pagado
BigDecimal pagado = BigDecimal.ZERO;
if (factura.getPagos() != null) {
for (FacturaPago p : factura.getPagos()) {
pagado = pagado.add(nvl(p.getCantidadPagada()));
}
}
factura.setTotalPagado(scale2(pagado));
// estado_pago
// - cancelada: si la factura está marcada como cancelada manualmente (aquí NO lo hacemos automático)
// - pagada: si total_pagado >= total_factura y total_factura > 0
// - pendiente: resto
if (factura.getEstadoPago() == EstadoPagoFactura.cancelada) {
return;
}
BigDecimal totalFactura = nvl(factura.getTotalFactura());
if (totalFactura.compareTo(BigDecimal.ZERO) > 0 &&
factura.getTotalPagado().compareTo(totalFactura) >= 0) {
factura.setEstadoPago(EstadoPagoFactura.pagada);
} else {
factura.setEstadoPago(EstadoPagoFactura.pendiente);
}
}
private static BigDecimal nvl(BigDecimal v) {
return v == null ? BigDecimal.ZERO : v;
}
private static BigDecimal scale2(BigDecimal v) {
return (v == null ? BigDecimal.ZERO : v).setScale(2, RoundingMode.HALF_UP);
}
}