diff --git a/src/main/java/com/imprimelibros/erp/common/jpa/AbstractAuditedEntitySoftTs.java b/src/main/java/com/imprimelibros/erp/common/jpa/AbstractAuditedEntitySoftTs.java new file mode 100644 index 0000000..79454ee --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/common/jpa/AbstractAuditedEntitySoftTs.java @@ -0,0 +1,67 @@ +package com.imprimelibros.erp.common.jpa; + +import com.imprimelibros.erp.users.User; +import jakarta.persistence.*; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.Instant; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class AbstractAuditedEntitySoftTs { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @CreatedDate + @Column(name = "created_at", updatable = false) + private Instant createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + private Instant updatedAt; + + @CreatedBy + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "created_by") + private User createdBy; + + @LastModifiedBy + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "updated_by") + private User updatedBy; + + @Column(name = "deleted_at") + private Instant deletedAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "deleted_by") + private User deletedBy; + + // Getters/Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public Instant getCreatedAt() { return createdAt; } + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } + + public Instant getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } + + public User getCreatedBy() { return createdBy; } + public void setCreatedBy(User createdBy) { this.createdBy = createdBy; } + + public User getUpdatedBy() { return updatedBy; } + public void setUpdatedBy(User updatedBy) { this.updatedBy = updatedBy; } + + public Instant getDeletedAt() { return deletedAt; } + public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; } + + public User getDeletedBy() { return deletedBy; } + public void setDeletedBy(User deletedBy) { this.deletedBy = deletedBy; } +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/EstadoFactura.java b/src/main/java/com/imprimelibros/erp/facturacion/EstadoFactura.java new file mode 100644 index 0000000..96e9b74 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/EstadoFactura.java @@ -0,0 +1,6 @@ +package com.imprimelibros.erp.facturacion; + +public enum EstadoFactura { + borrador, + validada +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/EstadoPagoFactura.java b/src/main/java/com/imprimelibros/erp/facturacion/EstadoPagoFactura.java new file mode 100644 index 0000000..0bd105c --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/EstadoPagoFactura.java @@ -0,0 +1,7 @@ +package com.imprimelibros.erp.facturacion; + +public enum EstadoPagoFactura { + pendiente, + pagada, + cancelada +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/Factura.java b/src/main/java/com/imprimelibros/erp/facturacion/Factura.java new file mode 100644 index 0000000..d59b264 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/Factura.java @@ -0,0 +1,156 @@ +package com.imprimelibros.erp.facturacion; + +import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs; +import com.imprimelibros.erp.users.User; +import jakarta.persistence.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table( + name = "facturas", + uniqueConstraints = { + @UniqueConstraint(name = "uq_facturas_numero_factura", columnNames = "numero_factura") + } +) +public class Factura extends AbstractAuditedEntitySoftTs { + + @Column(name = "pedido_id") + private Long pedidoId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "factura_rectificada_id") + private Factura facturaRectificada; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "factura_rectificativa_id") + private Factura facturaRectificativa; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "cliente_id") + private User cliente; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "serie_id") + private SerieFactura serie; + + @Column(name = "numero_factura", length = 50) + private String numeroFactura; + + @Enumerated(EnumType.STRING) + @Column(name = "estado", nullable = false, length = 20) + private EstadoFactura estado = EstadoFactura.borrador; + + @Enumerated(EnumType.STRING) + @Column(name = "estado_pago", nullable = false, length = 20) + private EstadoPagoFactura estadoPago = EstadoPagoFactura.pendiente; + + @Enumerated(EnumType.STRING) + @Column(name = "tipo_pago", nullable = false, length = 30) + private TipoPago tipoPago = TipoPago.otros; + + @Column(name = "fecha_emision") + private LocalDateTime fechaEmision; + + @Column(name = "base_imponible", precision = 10, scale = 2) + private BigDecimal baseImponible; + + @Column(name = "iva_4", precision = 10, scale = 2) + private BigDecimal iva4; + + @Column(name = "iva_21", precision = 10, scale = 2) + private BigDecimal iva21; + + @Column(name = "total_factura", precision = 10, scale = 2) + private BigDecimal totalFactura; + + @Column(name = "total_pagado", precision = 10, scale = 2) + private BigDecimal totalPagado = new BigDecimal("0.00"); + + @Lob + @Column(name = "notas") + private String notas; + + @OneToMany(mappedBy = "factura", cascade = CascadeType.ALL, orphanRemoval = true) + private List lineas = new ArrayList<>(); + + @OneToMany(mappedBy = "factura", cascade = CascadeType.ALL, orphanRemoval = true) + private List pagos = new ArrayList<>(); + + // Helpers + public void addLinea(FacturaLinea linea) { + linea.setFactura(this); + this.lineas.add(linea); + } + public void removeLinea(FacturaLinea linea) { + this.lineas.remove(linea); + linea.setFactura(null); + } + + public void addPago(FacturaPago pago) { + pago.setFactura(this); + this.pagos.add(pago); + } + public void removePago(FacturaPago pago) { + this.pagos.remove(pago); + pago.setFactura(null); + } + + // Getters/Setters + public Long getPedidoId() { return pedidoId; } + public void setPedidoId(Long pedidoId) { this.pedidoId = pedidoId; } + + public Factura getFacturaRectificada() { return facturaRectificada; } + public void setFacturaRectificada(Factura facturaRectificada) { this.facturaRectificada = facturaRectificada; } + + public Factura getFacturaRectificativa() { return facturaRectificativa; } + public void setFacturaRectificativa(Factura facturaRectificativa) { this.facturaRectificativa = facturaRectificativa; } + + public User getCliente() { return cliente; } + public void setCliente(User cliente) { this.cliente = cliente; } + + public SerieFactura getSerie() { return serie; } + public void setSerie(SerieFactura serie) { this.serie = serie; } + + public String getNumeroFactura() { return numeroFactura; } + public void setNumeroFactura(String numeroFactura) { this.numeroFactura = numeroFactura; } + + public EstadoFactura getEstado() { return estado; } + public void setEstado(EstadoFactura estado) { this.estado = estado; } + + public EstadoPagoFactura getEstadoPago() { return estadoPago; } + public void setEstadoPago(EstadoPagoFactura estadoPago) { this.estadoPago = estadoPago; } + + public TipoPago getTipoPago() { return tipoPago; } + public void setTipoPago(TipoPago tipoPago) { this.tipoPago = tipoPago; } + + public LocalDateTime getFechaEmision() { return fechaEmision; } + public void setFechaEmision(LocalDateTime fechaEmision) { this.fechaEmision = fechaEmision; } + + public BigDecimal getBaseImponible() { return baseImponible; } + public void setBaseImponible(BigDecimal baseImponible) { this.baseImponible = baseImponible; } + + public BigDecimal getIva4() { return iva4; } + public void setIva4(BigDecimal iva4) { this.iva4 = iva4; } + + public BigDecimal getIva21() { return iva21; } + public void setIva21(BigDecimal iva21) { this.iva21 = iva21; } + + public BigDecimal getTotalFactura() { return totalFactura; } + public void setTotalFactura(BigDecimal totalFactura) { this.totalFactura = totalFactura; } + + public BigDecimal getTotalPagado() { return totalPagado; } + public void setTotalPagado(BigDecimal totalPagado) { this.totalPagado = totalPagado; } + + public String getNotas() { return notas; } + public void setNotas(String notas) { this.notas = notas; } + + public List getLineas() { return lineas; } + public void setLineas(List lineas) { this.lineas = lineas; } + + public List getPagos() { return pagos; } + public void setPagos(List pagos) { this.pagos = pagos; } +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/FacturaLinea.java b/src/main/java/com/imprimelibros/erp/facturacion/FacturaLinea.java new file mode 100644 index 0000000..a26060b --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/FacturaLinea.java @@ -0,0 +1,56 @@ +package com.imprimelibros.erp.facturacion; + +import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs; +import jakarta.persistence.*; + +import java.math.BigDecimal; + +@Entity +@Table(name = "facturas_lineas") +public class FacturaLinea extends AbstractAuditedEntitySoftTs { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "factura_id") + private Factura factura; + + @Lob + @Column(name = "descripcion") + private String descripcion; + + @Column(name = "cantidad") + private Integer cantidad; + + @Column(name = "base_linea", precision = 10, scale = 2) + private BigDecimal baseLinea; + + @Column(name = "iva_4_linea", precision = 10, scale = 2) + private BigDecimal iva4Linea; + + @Column(name = "iva_21_linea", precision = 10, scale = 2) + private BigDecimal iva21Linea; + + @Column(name = "total_linea", precision = 10, scale = 2) + private BigDecimal totalLinea; + + // Getters/Setters + public Factura getFactura() { return factura; } + public void setFactura(Factura factura) { this.factura = factura; } + + public String getDescripcion() { return descripcion; } + public void setDescripcion(String descripcion) { this.descripcion = descripcion; } + + public Integer getCantidad() { return cantidad; } + public void setCantidad(Integer cantidad) { this.cantidad = cantidad; } + + public BigDecimal getBaseLinea() { return baseLinea; } + public void setBaseLinea(BigDecimal baseLinea) { this.baseLinea = baseLinea; } + + public BigDecimal getIva4Linea() { return iva4Linea; } + public void setIva4Linea(BigDecimal iva4Linea) { this.iva4Linea = iva4Linea; } + + public BigDecimal getIva21Linea() { return iva21Linea; } + public void setIva21Linea(BigDecimal iva21Linea) { this.iva21Linea = iva21Linea; } + + public BigDecimal getTotalLinea() { return totalLinea; } + public void setTotalLinea(BigDecimal totalLinea) { this.totalLinea = totalLinea; } +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/FacturaPago.java b/src/main/java/com/imprimelibros/erp/facturacion/FacturaPago.java new file mode 100644 index 0000000..0b1f147 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/FacturaPago.java @@ -0,0 +1,46 @@ +package com.imprimelibros.erp.facturacion; + +import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs; +import jakarta.persistence.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Table(name = "facturas_pagos") +public class FacturaPago extends AbstractAuditedEntitySoftTs { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "factura_id") + private Factura factura; + + @Enumerated(EnumType.STRING) + @Column(name = "metodo_pago", nullable = false, length = 30) + private TipoPago metodoPago = TipoPago.otros; + + @Column(name = "cantidad_pagada", precision = 10, scale = 2) + private BigDecimal cantidadPagada; + + @Column(name = "fecha_pago") + private LocalDateTime fechaPago; + + @Lob + @Column(name = "notas") + private String notas; + + // Getters/Setters + public Factura getFactura() { return factura; } + public void setFactura(Factura factura) { this.factura = factura; } + + public TipoPago getMetodoPago() { return metodoPago; } + public void setMetodoPago(TipoPago metodoPago) { this.metodoPago = metodoPago; } + + public BigDecimal getCantidadPagada() { return cantidadPagada; } + public void setCantidadPagada(BigDecimal cantidadPagada) { this.cantidadPagada = cantidadPagada; } + + public LocalDateTime getFechaPago() { return fechaPago; } + public void setFechaPago(LocalDateTime fechaPago) { this.fechaPago = fechaPago; } + + public String getNotas() { return notas; } + public void setNotas(String notas) { this.notas = notas; } +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/SerieFactura.java b/src/main/java/com/imprimelibros/erp/facturacion/SerieFactura.java new file mode 100644 index 0000000..55eb293 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/SerieFactura.java @@ -0,0 +1,34 @@ +package com.imprimelibros.erp.facturacion; + +import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs; +import jakarta.persistence.*; + +@Entity +@Table(name = "series_facturas") +public class SerieFactura extends AbstractAuditedEntitySoftTs { + + @Column(name = "nombre_serie", nullable = false, length = 100) + private String nombreSerie; + + @Column(name = "prefijo", nullable = false, length = 10) + private String prefijo; + + @Enumerated(EnumType.STRING) + @Column(name = "tipo", nullable = false, length = 50) + private TipoSerieFactura tipo = TipoSerieFactura.facturacion; + + @Column(name = "numero_actual", nullable = false) + private Integer numeroActual = 1; + + public String getNombreSerie() { return nombreSerie; } + public void setNombreSerie(String nombreSerie) { this.nombreSerie = nombreSerie; } + + public String getPrefijo() { return prefijo; } + public void setPrefijo(String prefijo) { this.prefijo = prefijo; } + + public TipoSerieFactura getTipo() { return tipo; } + public void setTipo(TipoSerieFactura tipo) { this.tipo = tipo; } + + public Integer getNumeroActual() { return numeroActual; } + public void setNumeroActual(Integer numeroActual) { this.numeroActual = numeroActual; } +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/TipoPago.java b/src/main/java/com/imprimelibros/erp/facturacion/TipoPago.java new file mode 100644 index 0000000..57de67e --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/TipoPago.java @@ -0,0 +1,8 @@ +package com.imprimelibros.erp.facturacion; + +public enum TipoPago { + tpv_tarjeta, + tpv_bizum, + transferencia, + otros +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/TipoSerieFactura.java b/src/main/java/com/imprimelibros/erp/facturacion/TipoSerieFactura.java new file mode 100644 index 0000000..c864293 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/TipoSerieFactura.java @@ -0,0 +1,5 @@ +package com.imprimelibros.erp.facturacion; + +public enum TipoSerieFactura { + facturacion +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/dto/FacturaLineaUpsertDto.java b/src/main/java/com/imprimelibros/erp/facturacion/dto/FacturaLineaUpsertDto.java new file mode 100644 index 0000000..ca3acd7 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/dto/FacturaLineaUpsertDto.java @@ -0,0 +1,41 @@ +package com.imprimelibros.erp.facturacion.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.math.BigDecimal; + +public class FacturaLineaUpsertDto { + + private Long id; // null => nueva línea + + @NotBlank + private String descripcion; + + @NotNull + private Integer cantidad; + + @NotNull + private BigDecimal baseLinea; // base imponible de la línea (sin IVA) + + private boolean aplicaIva4; + private boolean aplicaIva21; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getDescripcion() { return descripcion; } + public void setDescripcion(String descripcion) { this.descripcion = descripcion; } + + public Integer getCantidad() { return cantidad; } + public void setCantidad(Integer cantidad) { this.cantidad = cantidad; } + + public BigDecimal getBaseLinea() { return baseLinea; } + public void setBaseLinea(BigDecimal baseLinea) { this.baseLinea = baseLinea; } + + public boolean isAplicaIva4() { return aplicaIva4; } + public void setAplicaIva4(boolean aplicaIva4) { this.aplicaIva4 = aplicaIva4; } + + public boolean isAplicaIva21() { return aplicaIva21; } + public void setAplicaIva21(boolean aplicaIva21) { this.aplicaIva21 = aplicaIva21; } +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/dto/FacturaPagoUpsertDto.java b/src/main/java/com/imprimelibros/erp/facturacion/dto/FacturaPagoUpsertDto.java new file mode 100644 index 0000000..a936ad1 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/dto/FacturaPagoUpsertDto.java @@ -0,0 +1,36 @@ +package com.imprimelibros.erp.facturacion.dto; + +import com.imprimelibros.erp.facturacion.TipoPago; +import jakarta.validation.constraints.NotNull; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +public class FacturaPagoUpsertDto { + + private Long id; // null => nuevo pago + + @NotNull + private TipoPago metodoPago; + + @NotNull + private BigDecimal cantidadPagada; + + private LocalDateTime fechaPago; + private String notas; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public TipoPago getMetodoPago() { return metodoPago; } + public void setMetodoPago(TipoPago metodoPago) { this.metodoPago = metodoPago; } + + public BigDecimal getCantidadPagada() { return cantidadPagada; } + public void setCantidadPagada(BigDecimal cantidadPagada) { this.cantidadPagada = cantidadPagada; } + + public LocalDateTime getFechaPago() { return fechaPago; } + public void setFechaPago(LocalDateTime fechaPago) { this.fechaPago = fechaPago; } + + public String getNotas() { return notas; } + public void setNotas(String notas) { this.notas = notas; } +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/repo/FacturaLineaRepository.java b/src/main/java/com/imprimelibros/erp/facturacion/repo/FacturaLineaRepository.java new file mode 100644 index 0000000..115162b --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/repo/FacturaLineaRepository.java @@ -0,0 +1,10 @@ +package com.imprimelibros.erp.facturacion.repo; + +import com.imprimelibros.erp.facturacion.FacturaLinea; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface FacturaLineaRepository extends JpaRepository { + List findByFacturaId(Long facturaId); +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/repo/FacturaPagoRepository.java b/src/main/java/com/imprimelibros/erp/facturacion/repo/FacturaPagoRepository.java new file mode 100644 index 0000000..e47d8a0 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/repo/FacturaPagoRepository.java @@ -0,0 +1,10 @@ +package com.imprimelibros.erp.facturacion.repo; + +import com.imprimelibros.erp.facturacion.FacturaPago; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface FacturaPagoRepository extends JpaRepository { + List findByFacturaId(Long facturaId); +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/repo/FacturaRepository.java b/src/main/java/com/imprimelibros/erp/facturacion/repo/FacturaRepository.java new file mode 100644 index 0000000..9505952 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/repo/FacturaRepository.java @@ -0,0 +1,10 @@ +package com.imprimelibros.erp.facturacion.repo; + +import com.imprimelibros.erp.facturacion.Factura; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface FacturaRepository extends JpaRepository { + Optional findByNumeroFactura(String numeroFactura); +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/repo/SerieFacturaRepository.java b/src/main/java/com/imprimelibros/erp/facturacion/repo/SerieFacturaRepository.java new file mode 100644 index 0000000..f580c64 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/repo/SerieFacturaRepository.java @@ -0,0 +1,18 @@ +package com.imprimelibros.erp.facturacion.repo; + +import com.imprimelibros.erp.facturacion.SerieFactura; +import com.imprimelibros.erp.facturacion.TipoSerieFactura; +import org.springframework.data.jpa.repository.*; +import org.springframework.data.repository.query.Param; + +import jakarta.persistence.LockModeType; +import java.util.Optional; + +public interface SerieFacturaRepository extends JpaRepository { + + Optional findByTipo(TipoSerieFactura tipo); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select s from SerieFactura s where s.id = :id") + Optional findByIdForUpdate(@Param("id") Long id); +} diff --git a/src/main/java/com/imprimelibros/erp/facturacion/service/FacturacionService.java b/src/main/java/com/imprimelibros/erp/facturacion/service/FacturacionService.java new file mode 100644 index 0000000..95835f3 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/facturacion/service/FacturacionService.java @@ -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); + } +} diff --git a/src/main/resources/db/changelog/changesets/0023-facturacion.yml b/src/main/resources/db/changelog/changesets/0023-facturacion.yml new file mode 100644 index 0000000..706c5eb --- /dev/null +++ b/src/main/resources/db/changelog/changesets/0023-facturacion.yml @@ -0,0 +1,407 @@ +databaseChangeLog: + + - changeSet: + id: 20251230-01-pedidos-lineas-enviado + author: jjo + changes: + - modifyDataType: + tableName: pedidos_lineas + columnName: estado + newDataType: > + ENUM( + 'pendiente_pago', + 'procesando_pago', + 'denegado_pago', + 'aprobado', + 'maquetacion', + 'haciendo_ferro', + 'esperando_aceptacion_ferro', + 'produccion', + 'terminado', + 'enviado', + 'cancelado' + ) + rollback: + - modifyDataType: + tableName: pedidos_lineas + columnName: estado + newDataType: > + ENUM( + 'pendiente_pago', + 'procesando_pago', + 'denegado_pago', + 'aprobado', + 'maquetacion', + 'haciendo_ferro', + 'esperando_aceptacion_ferro', + 'produccion', + 'terminado', + 'cancelado' + ) + + # ------------------------------------------------- + + - changeSet: + id: 20251230-02-series-facturas + author: jjo + changes: + - createTable: + tableName: series_facturas + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + + - column: + name: nombre_serie + type: VARCHAR(100) + constraints: + nullable: false + + - column: + name: prefijo + type: VARCHAR(10) + constraints: + nullable: false + + - column: + name: tipo + type: ENUM('facturacion') + defaultValue: facturacion + + - column: + name: numero_actual + type: BIGINT + defaultValueNumeric: 1 + + - column: + name: created_at + type: TIMESTAMP + - column: + name: updated_at + type: TIMESTAMP + - column: + name: deleted_at + type: TIMESTAMP + + - column: + name: created_by + type: BIGINT + - column: + name: updated_by + type: BIGINT + - column: + name: deleted_by + type: BIGINT + + - addForeignKeyConstraint: + constraintName: fk_series_facturas_created_by + baseTableName: series_facturas + baseColumnNames: created_by + referencedTableName: users + referencedColumnNames: id + onDelete: SET NULL + + - addForeignKeyConstraint: + constraintName: fk_series_facturas_updated_by + baseTableName: series_facturas + baseColumnNames: updated_by + referencedTableName: users + referencedColumnNames: id + onDelete: SET NULL + + - addForeignKeyConstraint: + constraintName: fk_series_facturas_deleted_by + baseTableName: series_facturas + baseColumnNames: deleted_by + referencedTableName: users + referencedColumnNames: id + onDelete: SET NULL + + rollback: + - dropTable: + tableName: series_facturas + + # ------------------------------------------------- + + - changeSet: + id: 20251230-03-facturas + author: jjo + changes: + - createTable: + tableName: facturas + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + + - column: + name: pedido_id + type: BIGINT + + - column: + name: factura_rectificada_id + type: BIGINT + + - column: + name: factura_rectificativa_id + type: BIGINT + + - column: + name: cliente_id + type: BIGINT + + - column: + name: serie_id + type: BIGINT + + - column: + name: numero_factura + type: VARCHAR(50) + + - column: + name: estado + type: ENUM('borrador','validada') + defaultValue: borrador + + - column: + name: estado_pago + type: ENUM('pendiente','pagada','cancelada') + defaultValue: pendiente + + - column: + name: tipo_pago + type: ENUM('tpv_tarjeta','tpv_bizum','transferencia','otros') + defaultValue: otros + + - column: + name: fecha_emision + type: DATETIME + + - column: + name: base_imponible + type: DECIMAL(10,2) + - column: + name: iva_4 + type: DECIMAL(10,2) + - column: + name: iva_21 + type: DECIMAL(10,2) + - column: + name: total_factura + type: DECIMAL(10,2) + - column: + name: total_pagado + type: DECIMAL(10,2) + defaultValueNumeric: 0.00 + + - column: + name: notas + type: TEXT + + - column: + name: created_at + type: TIMESTAMP + - column: + name: updated_at + type: TIMESTAMP + - column: + name: deleted_at + type: TIMESTAMP + + - column: + name: created_by + type: BIGINT + - column: + name: updated_by + type: BIGINT + - column: + name: deleted_by + type: BIGINT + + - addUniqueConstraint: + constraintName: uq_facturas_numero_factura + tableName: facturas + columnNames: numero_factura + + - addForeignKeyConstraint: + constraintName: fk_facturas_pedido + baseTableName: facturas + baseColumnNames: pedido_id + referencedTableName: pedidos + referencedColumnNames: id + + - addForeignKeyConstraint: + constraintName: fk_facturas_cliente + baseTableName: facturas + baseColumnNames: cliente_id + referencedTableName: users + referencedColumnNames: id + + - addForeignKeyConstraint: + constraintName: fk_facturas_serie + baseTableName: facturas + baseColumnNames: serie_id + referencedTableName: series_facturas + referencedColumnNames: id + + - addForeignKeyConstraint: + constraintName: fk_facturas_rectificada + baseTableName: facturas + baseColumnNames: factura_rectificada_id + referencedTableName: facturas + referencedColumnNames: id + + - addForeignKeyConstraint: + constraintName: fk_facturas_rectificativa + baseTableName: facturas + baseColumnNames: factura_rectificativa_id + referencedTableName: facturas + referencedColumnNames: id + + rollback: + - dropTable: + tableName: facturas + + # ------------------------------------------------- + + - changeSet: + id: 20251230-04-facturas-lineas + author: jjo + changes: + - createTable: + tableName: facturas_lineas + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + + - column: + name: factura_id + type: BIGINT + - column: + name: descripcion + type: TEXT + - column: + name: cantidad + type: INT + - column: + name: base_linea + type: DECIMAL(10,2) + - column: + name: iva_4_linea + type: DECIMAL(10,2) + - column: + name: iva_21_linea + type: DECIMAL(10,2) + - column: + name: total_linea + type: DECIMAL(10,2) + + - column: + name: created_at + type: TIMESTAMP + - column: + name: updated_at + type: TIMESTAMP + - column: + name: deleted_at + type: TIMESTAMP + + - column: + name: created_by + type: BIGINT + - column: + name: updated_by + type: BIGINT + - column: + name: deleted_by + type: BIGINT + + - addForeignKeyConstraint: + constraintName: fk_facturas_lineas_factura + baseTableName: facturas_lineas + baseColumnNames: factura_id + referencedTableName: facturas + referencedColumnNames: id + + rollback: + - dropTable: + tableName: facturas_lineas + + # ------------------------------------------------- + + - changeSet: + id: 20251230-05-facturas-pagos + author: jjo + changes: + - createTable: + tableName: facturas_pagos + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + + - column: + name: factura_id + type: BIGINT + - column: + name: metodo_pago + type: ENUM('tpv_tarjeta','tpv_bizum','transferencia','otros') + defaultValue: otros + - column: + name: cantidad_pagada + type: DECIMAL(10,2) + - column: + name: fecha_pago + type: DATETIME + - column: + name: notas + type: TEXT + + - column: + name: created_at + type: TIMESTAMP + - column: + name: updated_at + type: TIMESTAMP + - column: + name: deleted_at + type: TIMESTAMP + + - column: + name: created_by + type: BIGINT + - column: + name: updated_by + type: BIGINT + - column: + name: deleted_by + type: BIGINT + + - addForeignKeyConstraint: + constraintName: fk_facturas_pagos_factura + baseTableName: facturas_pagos + baseColumnNames: factura_id + referencedTableName: facturas + referencedColumnNames: id + + rollback: + - dropTable: + tableName: facturas_pagos diff --git a/src/main/resources/db/changelog/master.yml b/src/main/resources/db/changelog/master.yml index 8b470ee..c278e7e 100644 --- a/src/main/resources/db/changelog/master.yml +++ b/src/main/resources/db/changelog/master.yml @@ -42,4 +42,6 @@ databaseChangeLog: - include: file: db/changelog/changesets/0021-add-email-and-is-palets-to-pedidos-direcciones.yml - include: - file: db/changelog/changesets/0022-add-estados-pago-to-pedidos-lineas-3.yml \ No newline at end of file + file: db/changelog/changesets/0022-add-estados-pago-to-pedidos-lineas-3.yml + - include: + file: db/changelog/changesets/0023-facturacion.yml \ No newline at end of file