package com.imprimelibros.erp.facturacion.service; import com.imprimelibros.erp.common.Utils; import com.imprimelibros.erp.configurationERP.VariableService; import com.imprimelibros.erp.facturacion.*; import com.imprimelibros.erp.facturacion.dto.FacturaGuardarDto; import com.imprimelibros.erp.facturacion.dto.FacturaLineaUpsertDto; import com.imprimelibros.erp.facturacion.dto.FacturaPagoUpsertDto; import com.imprimelibros.erp.facturacion.repo.FacturaLineaRepository; import com.imprimelibros.erp.facturacion.repo.FacturaPagoRepository; import com.imprimelibros.erp.facturacion.repo.FacturaRepository; import com.imprimelibros.erp.facturacion.repo.SerieFacturaRepository; import com.imprimelibros.erp.pedidos.Pedido; import com.imprimelibros.erp.pedidos.PedidoLinea; import com.imprimelibros.erp.pedidos.PedidoLineaRepository; import com.imprimelibros.erp.pedidos.PedidoService; import com.imprimelibros.erp.presupuesto.dto.Presupuesto; import com.imprimelibros.erp.users.User; import com.imprimelibros.erp.users.UserService; import jakarta.persistence.EntityNotFoundException; import org.springframework.context.MessageSource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Locale; import java.math.BigDecimal; import java.math.RoundingMode; import java.security.Principal; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; @Service public class FacturacionService { private final FacturaRepository facturaRepo; private final SerieFacturaRepository serieRepo; private final FacturaPagoRepository pagoRepo; private final FacturaLineaRepository lineaFacturaRepository; private final PedidoLineaRepository pedidoLineaRepo; private final UserService userService; private final Utils utils; private final MessageSource messageSource; private final PedidoService pedidoService; private final VariableService variableService; public FacturacionService( FacturaRepository facturaRepo, FacturaLineaRepository lineaFacturaRepository, SerieFacturaRepository serieRepo, FacturaPagoRepository pagoRepo, PedidoLineaRepository pedidoLineaRepo, UserService userService, Utils utils, MessageSource messageSource, PedidoService pedidoService, VariableService variableService) { this.facturaRepo = facturaRepo; this.lineaFacturaRepository = lineaFacturaRepository; this.serieRepo = serieRepo; this.pagoRepo = pagoRepo; this.pedidoLineaRepo = pedidoLineaRepo; this.userService = userService; this.utils = utils; this.messageSource = messageSource; this.pedidoService = pedidoService; this.variableService = variableService; } public SerieFactura getDefaultSerieFactura() { Long defaultSerieId = variableService.getValorEntero("serie_facturacion_default").longValue(); SerieFactura serie = serieRepo.findById(defaultSerieId).orElse(null); if (serie == null) { throw new IllegalStateException("No hay ninguna serie de facturación configurada."); } return serie; } public Factura getFactura(Long facturaId) { return facturaRepo.findById(facturaId) .orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId)); } // ----------------------- // Nueva factura // ----------------------- @Transactional public Factura crearNuevaFacturaAuto(Pedido pedido, SerieFactura serie, TipoPago tipoPago, Locale locale) { Factura factura = new Factura(); factura.setCliente(pedido.getCreatedBy()); factura.setCreatedAt(Instant.now()); factura.setUpdatedAt(Instant.now()); Boolean pedidoPendientePago = false; List lineasPedido = pedidoLineaRepo.findByPedidoId(pedido.getId()); for (PedidoLinea lineaPedido : lineasPedido) { if (lineaPedido.getEstado() == PedidoLinea.Estado.pendiente_pago) { pedidoPendientePago = true; break; } } factura.setEstado(pedidoPendientePago ? EstadoFactura.borrador : EstadoFactura.validada); factura.setEstadoPago(pedidoPendientePago ? EstadoPagoFactura.pendiente : EstadoPagoFactura.pagada); factura.setTipoPago(pedidoPendientePago ? TipoPago.otros : tipoPago); factura.setPedidoId(pedido.getId()); factura.setSerie(serie); factura.setNumeroFactura(this.getNumberFactura(serie)); factura.setFechaEmision(LocalDateTime.now()); factura.setBaseImponible(BigDecimal.valueOf(pedido.getBase()).setScale(2, RoundingMode.HALF_UP)); factura.setIva4(BigDecimal.valueOf(pedido.getIva4()).setScale(2, RoundingMode.HALF_UP)); factura.setIva21(BigDecimal.valueOf(pedido.getIva21()).setScale(2, RoundingMode.HALF_UP)); factura.setTotalFactura(BigDecimal.valueOf(pedido.getTotal()).setScale(2, RoundingMode.HALF_UP)); factura.setTotalPagado(BigDecimal.valueOf(pedido.getTotal()).setScale(2, RoundingMode.HALF_UP)); // rellenar lineas List lineasFactura = new ArrayList<>(); for (PedidoLinea lineaPedido : lineasPedido) { Presupuesto p = lineaPedido.getPresupuesto(); FacturaLinea lineaFactura = new FacturaLinea(); lineaFactura.setDescripcion(this.obtenerLineaFactura(lineaPedido, locale)); lineaFactura.setCantidad(p.getSelectedTirada()); lineaFactura.setBaseLinea(p.getBaseImponible()); lineaFactura.setIva4Linea(p.getIvaImporte4()); lineaFactura.setIva21Linea(p.getIvaImporte21()); lineaFactura.setTotalLinea(p.getTotalConIva()); lineaFactura.setCreatedBy(p.getUser()); lineaFactura.setFactura(factura); lineasFactura.add(lineaFactura); } if(pedido.getEnvio() > 0){ FacturaLinea lineaEnvio = new FacturaLinea(); lineaEnvio.setDescripcion(messageSource.getMessage("facturas.lineas.gastos-envio", null, "Gastos de envío", locale)); lineaEnvio.setCantidad(1); BigDecimal baseEnvio = BigDecimal.valueOf(pedido.getEnvio()).setScale(2, RoundingMode.HALF_UP); lineaEnvio.setBaseLinea(baseEnvio); BigDecimal iva21Envio = baseEnvio.multiply(BigDecimal.valueOf(0.21)).setScale(2, RoundingMode.HALF_UP); lineaEnvio.setIva21Linea(iva21Envio); lineaEnvio.setIva4Linea(BigDecimal.ZERO); lineaEnvio.setTotalLinea(baseEnvio.add(iva21Envio)); lineaEnvio.setCreatedBy(pedido.getCreatedBy()); lineaEnvio.setCreatedAt(Instant.now()); lineaEnvio.setFactura(factura); lineasFactura.add(lineaEnvio); } factura.setLineas(lineasFactura); factura = facturaRepo.save(factura); if (pedidoPendientePago) { return factura; } FacturaPago pago = new FacturaPago(); pago.setMetodoPago(tipoPago); pago.setCantidadPagada(factura.getTotalFactura()); pago.setFechaPago(LocalDateTime.now()); pago.setFactura(factura); pago.setCreatedBy(pedido.getCreatedBy()); pago.setCreatedAt(Instant.now()); pagoRepo.save(pago); return factura; } // ----------------------- // Estado / Numeración // ----------------------- @Transactional public String getNumberFactura(SerieFactura serie) { try { long next = (serie.getNumeroActual() == null) ? 1L : serie.getNumeroActual(); String numeroFactura = buildNumeroFactura(serie.getPrefijo(), next); // Incrementar contador para la siguiente serie.setNumeroActual(next + 1); serieRepo.save(serie); return numeroFactura; } catch (Exception e) { return null; } } @Transactional public void guardarCabeceraYDireccionFacturacion(Long facturaId, FacturaGuardarDto dto) { Factura factura = getFactura(facturaId); // ✅ Solo editable si borrador (tu regla actual para cabecera/dirección) if (factura.getEstado() != EstadoFactura.borrador) { throw new IllegalStateException("Solo se puede guardar cabecera/dirección en borrador."); } // 1) Cabecera if (dto.getCabecera() != null) { var c = dto.getCabecera(); if (c.getSerieId() != null) { SerieFactura serie = serieRepo.findById(c.getSerieId()) .orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + c.getSerieId())); factura.setSerie(serie); } if (c.getClienteId() != null) { User cliente = userService.findById(c.getClienteId()); if(cliente == null){ throw new EntityNotFoundException("Cliente no encontrado: " + c.getClienteId()); } factura.setCliente(cliente); } if (c.getFechaEmision() != null) { factura.setFechaEmision(c.getFechaEmision()); } } // 2) Dirección de facturación del pedido asociado Long pedidoId = factura.getPedidoId(); if (pedidoId != null && dto.getDireccionFacturacion() != null) { pedidoService.upsertDireccionFacturacion(pedidoId, dto.getDireccionFacturacion()); } facturaRepo.save(factura); } @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(next + 1); 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("%05d", numero); return pref.isBlank() ? num : (pref + " " + num + "/" + LocalDate.now().getYear()); } // ----------------------- // Líneas // ----------------------- @Transactional public void createLinea(Long facturaId, FacturaLineaUpsertDto req) { Factura factura = this.getFactura(facturaId); FacturaLinea lf = new FacturaLinea(); lf.setFactura(factura); lf.setCantidad(1); applyRequest(lf, req); lineaFacturaRepository.save(lf); this.recalcularTotales(factura); } @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.setBaseLinea(scale2(dto.getBase())); linea.setIva4Linea(dto.getIva4()); linea.setIva21Linea(dto.getIva21()); linea.setTotalLinea(scale2(linea.getBaseLinea() .add(nvl(linea.getIva4Linea())) .add(nvl(linea.getIva21Linea())))); 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, Principal principal) { 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); pago.setCreatedBy(Utils.currentUser(principal)); pago.setCreatedAt(Instant.now()); 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()); pago.setUpdatedAt(Instant.now()); pago.setUpdatedBy(Utils.currentUser(principal)); // 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, Principal principal) { Factura factura = facturaRepo.findById(facturaId) .orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId)); FacturaPago pago = factura.getPagos().stream() .filter(p -> pagoId.equals(p.getId())) .findFirst() .orElseThrow(() -> new EntityNotFoundException("Pago no encontrado: " + pagoId)); // soft delete pago.setDeletedAt(Instant.now()); pago.setDeletedBy(Utils.currentUser(principal)); 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()) { if (p.getDeletedAt() != null) continue; 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); } private String obtenerLineaFactura(PedidoLinea lineaPedido, Locale locale) { Map specs = utils.getTextoPresupuesto(lineaPedido.getPresupuesto(), locale); StringBuilder html = new StringBuilder(); html.append("
") .append("
"); if (specs == null) { return "
"; } // 1) Líneas del presupuesto (HTML) Object lineasObj = specs.get("lineas"); if (lineasObj instanceof List lineasList) { for (Object o : lineasList) { if (!(o instanceof Map m)) continue; Object descObj = m.get("descripcion"); String descripcionHtml = descObj != null ? descObj.toString() : ""; if (descripcionHtml.isBlank()) continue; html.append("
") .append("") .append(descripcionHtml) // OJO: esto es HTML (como th:utext) .append("") .append("
"); } } // 2) Servicios adicionales (texto) Object serviciosObj = specs.get("servicios"); String servicios = (serviciosObj != null) ? serviciosObj.toString().trim() : ""; if (!servicios.isBlank()) { String label = messageSource.getMessage("pdf.servicios-adicionales", null, "Servicios adicionales", locale); html.append("
") .append("").append(escapeHtml(label)).append("") .append("").append(escapeHtml(servicios)).append("") .append("
"); } // 3) Datos de maquetación (HTML) Object datosMaqObj = specs.get("datosMaquetacion"); if (datosMaqObj != null && !datosMaqObj.toString().isBlank()) { String label = messageSource.getMessage("pdf.datos-maquetacion", null, "Datos de maquetación:", locale); html.append("
") .append("").append(escapeHtml(label)).append("") .append("") .append(datosMaqObj) // HTML (como th:utext) .append("") .append("
"); } // 4) Datos de marcapáginas (HTML) Object datosMarcaObj = specs.get("datosMarcapaginas"); if (datosMarcaObj != null && !datosMarcaObj.toString().isBlank()) { String label = messageSource.getMessage("pdf.datos-marcapaginas", null, "Datos de marcapáginas:", locale); html.append("
") .append("").append(escapeHtml(label)).append("") .append("") .append(datosMarcaObj) // HTML (como th:utext) .append("") .append("
"); } html.append("
"); return html.toString(); } /** * Escape mínimo para texto plano (equivalente a th:text). * No lo uses para fragmentos que ya son HTML (th:utext). */ private static String escapeHtml(String s) { if (s == null) return ""; return s.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); } private void applyRequest(FacturaLinea lf, FacturaLineaUpsertDto req) { // HTML lf.setDescripcion(req.getDescripcion() == null ? "" : req.getDescripcion()); BigDecimal base = nvl(req.getBase()); BigDecimal iva4 = nvl(req.getIva4()); BigDecimal iva21 = nvl(req.getIva21()); lf.setBaseLinea(base); lf.setIva4Linea(iva4); lf.setIva21Linea(iva21); // total de línea (por ahora) lf.setTotalLinea(base.add(iva4).add(iva21)); } }