From 46715d1017e284272c32f57f6da8410a2a85a734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Jim=C3=A9nez?= Date: Fri, 17 Oct 2025 09:21:31 +0200 Subject: [PATCH] primera version final del presupuesto --- .../erp/cart/CartController.java | 3 - .../imprimelibros/erp/cart/CartService.java | 2 - .../com/imprimelibros/erp/common/Utils.java | 227 ++++++++++++++++++ .../com/imprimelibros/erp/pdf/PdfService.java | 27 ++- .../PresupuestoDatatableService.java | 14 +- src/main/resources/i18n/pdf_es.properties | 6 + .../resources/i18n/presupuesto_es.properties | 1 + .../static/assets/css/presupuestopdf.css | 29 ++- .../imprimelibros/pdf/presupuesto-a4.html | 30 ++- .../presupuestador-items/_buttons.html | 53 ++-- .../imprimelibros/erp/pdf/PdfSmokeTest.java | 4 +- 11 files changed, 323 insertions(+), 73 deletions(-) create mode 100644 src/main/java/com/imprimelibros/erp/common/Utils.java diff --git a/src/main/java/com/imprimelibros/erp/cart/CartController.java b/src/main/java/com/imprimelibros/erp/cart/CartController.java index ddbbfd0..1bd4e7a 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartController.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartController.java @@ -8,11 +8,8 @@ import com.imprimelibros.erp.users.UserDetailsImpl; import jakarta.servlet.http.HttpServletRequest; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; import com.imprimelibros.erp.users.User; -import org.springframework.boot.autoconfigure.graphql.GraphQlProperties.Http; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; diff --git a/src/main/java/com/imprimelibros/erp/cart/CartService.java b/src/main/java/com/imprimelibros/erp/cart/CartService.java index bd85a5a..afe4215 100644 --- a/src/main/java/com/imprimelibros/erp/cart/CartService.java +++ b/src/main/java/com/imprimelibros/erp/cart/CartService.java @@ -1,6 +1,5 @@ package com.imprimelibros.erp.cart; -import jakarta.mail.Message; import jakarta.transaction.Transactional; import org.springframework.context.MessageSource; @@ -21,7 +20,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter; import com.imprimelibros.erp.presupuesto.dto.Presupuesto; import com.imprimelibros.erp.presupuesto.PresupuestoRepository; -import com.imprimelibros.erp.presupuesto.service.PresupuestoService; @Service diff --git a/src/main/java/com/imprimelibros/erp/common/Utils.java b/src/main/java/com/imprimelibros/erp/common/Utils.java new file mode 100644 index 0000000..1cd7a14 --- /dev/null +++ b/src/main/java/com/imprimelibros/erp/common/Utils.java @@ -0,0 +1,227 @@ +package com.imprimelibros.erp.common; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter; +import com.imprimelibros.erp.presupuesto.dto.Presupuesto; +import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices; +import com.imprimelibros.erp.presupuesto.marcapaginas.Marcapaginas; + +@Component +public class Utils { + + private final PresupuestoFormatter presupuestoFormatter; + private final MessageSource messageSource; + + public Utils(PresupuestoFormatter presupuestoFormatter, + MessageSource messageSource) { + this.presupuestoFormatter = presupuestoFormatter; + this.messageSource = messageSource; + } + + public static String formatCurrency(BigDecimal amount, Locale locale) { + NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale); + return currencyFormatter.format(amount); + } + + public static String formatCurrency(Double amount, Locale locale) { + NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale); + return currencyFormatter.format(amount); + } + + public Map getTextoPresupuesto(Presupuesto presupuesto, Locale locale) { + + Map resumen = new HashMap<>(); + + resumen.put("titulo", presupuesto.getTitulo()); + + resumen.put("imagen", + "/assets/images/imprimelibros/presupuestador/" + presupuesto.getTipoEncuadernacion() + + ".png"); + resumen.put("imagen_alt", + messageSource.getMessage("presupuesto." + presupuesto.getTipoEncuadernacion(), null, + locale)); + + resumen.put("presupuestoId", presupuesto.getId()); + + ObjectMapper mapper = new ObjectMapper(); + List> servicios = new ArrayList<>(); + if (presupuesto.getServiciosJson() != null && !presupuesto.getServiciosJson().isBlank()) + try { + servicios = mapper.readValue(presupuesto.getServiciosJson(), new TypeReference<>() { + }); + } catch (JsonProcessingException e) { + // Manejar la excepción + } + + boolean hayDepositoLegal = servicios != null && servicios.stream() + .map(m -> java.util.Objects.toString(m.get("id"), "")) + .map(String::trim) + .anyMatch("deposito-legal"::equals); + + List> lineas = new ArrayList<>(); + HashMap linea = new HashMap<>(); + Double precio_unitario = 0.0; + Double precio_total = 0.0; + BigDecimal total = BigDecimal.ZERO; + linea.put("descripcion", presupuestoFormatter.resumen(presupuesto, servicios, locale)); + linea.put("cantidad", presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : 0); + precio_unitario = (presupuesto.getPrecioUnitario() != null + ? presupuesto.getPrecioUnitario().doubleValue() + : 0.0); + precio_total = (presupuesto.getPrecioTotalTirada() != null + ? presupuesto.getPrecioTotalTirada().doubleValue() + : 0.0); + linea.put("precio_unitario", precio_unitario); + linea.put("precio_total", BigDecimal.valueOf(precio_total).setScale(2, RoundingMode.HALF_UP)); + total = total.add(BigDecimal.valueOf(precio_total)); + lineas.add(linea); + + if (hayDepositoLegal) { + linea = new HashMap<>(); + linea.put("descripcion", + messageSource.getMessage("pdf.ejemplares-deposito-legal", new Object[]{4}, locale)); + lineas.add(linea); + } + + String serviciosExtras = ""; + if (servicios != null) { + for (Map servicio : servicios) { + serviciosExtras += messageSource.getMessage( + "presupuesto.extras-" + servicio.get("id"), null, locale) + ", "; + } + if (!serviciosExtras.isEmpty()) { + serviciosExtras = serviciosExtras.substring(0, serviciosExtras.length() - 2);; + } + if (servicios.stream().anyMatch(service -> "marcapaginas".equals(service.get("id")))) { + ObjectMapper mapperServicio = new ObjectMapper(); + Object raw = presupuesto.getDatosMarcapaginasJson(); + Map datosMarcapaginas; + String descripcion = ""; + try { + if (raw instanceof String s) { + datosMarcapaginas = mapperServicio.readValue(s, + new TypeReference>() { + }); + } else if (raw instanceof Map m) { + datosMarcapaginas = mapperServicio.convertValue(m, + new TypeReference>() { + }); + } else { + throw new IllegalArgumentException( + "Tipo no soportado para datosMarcapaginas: " + + raw); + } + } catch (JsonProcessingException e) { + throw new RuntimeException("Error parsing datosMarcapaginasJson", e); + } + descripcion += "
  • "; + descripcion += Marcapaginas.Tamanios + .valueOf(datosMarcapaginas.get("tamanio").toString()).getLabel() + + ", "; + descripcion += Marcapaginas.Caras_Impresion + .valueOf(datosMarcapaginas.get("carasImpresion").toString()) + .getMessageKey() + ", "; + descripcion += messageSource + .getMessage(Marcapaginas.Papeles + .valueOf(datosMarcapaginas.get("papel") + .toString()) + .getMessageKey(), null, locale) + + " - " + + datosMarcapaginas.get("gramaje").toString() + " gr, "; + descripcion += messageSource.getMessage( + Marcapaginas.Acabado.valueOf( + datosMarcapaginas.get("acabado").toString()) + .getMessageKey(), + null, locale); + descripcion += "
"; + resumen.put("datosMarcapaginas", descripcion); + } + if (servicios.stream().anyMatch(service -> "maquetacion".equals(service.get("id")))) { + ObjectMapper mapperServicio = new ObjectMapper(); + Object raw = presupuesto.getDatosMaquetacionJson(); + Map datosMaquetacion; + String descripcion = ""; + try { + if (raw instanceof String s) { + datosMaquetacion = mapperServicio.readValue(s, + new TypeReference>() { + }); + } else if (raw instanceof Map m) { + datosMaquetacion = mapperServicio.convertValue(m, + new TypeReference>() { + }); + } else { + throw new IllegalArgumentException( + "Tipo no soportado para datosMaquetacion: " + + raw); + } + } catch (JsonProcessingException e) { + throw new RuntimeException("Error parsing datosMaquetacionJson", e); + } + descripcion += "
  • "; + descripcion += (datosMaquetacion.get("num_caracteres") + " " + + messageSource.getMessage("presupuesto.maquetacion.caracteres", + null, locale)) + + ", "; + descripcion += MaquetacionMatrices.Formato + .valueOf(datosMaquetacion.get("formato_maquetacion").toString()) + .getLabel() + ", "; + descripcion += messageSource.getMessage(MaquetacionMatrices.FontSize + .valueOf(datosMaquetacion.get("cuerpo_texto").toString()) + .getMessageKey(), null, locale) + + ", "; + descripcion += messageSource.getMessage("presupuesto.maquetacion.num-columnas", + null, locale) + ": " + + datosMaquetacion.get("num_columnas").toString() + ", "; + descripcion += messageSource.getMessage("presupuesto.maquetacion.num-tablas", + null, locale) + ": " + + datosMaquetacion.get("num_tablas").toString() + ", "; + descripcion += messageSource.getMessage("presupuesto.maquetacion.num-fotos", + null, locale) + ": " + + datosMaquetacion.get("num_fotos").toString(); + if ((boolean) datosMaquetacion.get("correccion_ortotipografica")) { + descripcion += ", " + messageSource + .getMessage("presupuesto.maquetacion.correccion-ortotipografica", + null, locale); + } + if ((boolean) datosMaquetacion.get("texto_mecanografiado")) { + descripcion += ", " + messageSource.getMessage( + "presupuesto.maquetacion.texto-mecanografiado", + null, locale); + } + if ((boolean) datosMaquetacion.get("disenio_portada")) { + descripcion += ", " + + messageSource.getMessage( + "presupuesto.maquetacion.diseno-portada", + null, locale); + } + if ((boolean) datosMaquetacion.get("epub")) { + descripcion += ", " + messageSource.getMessage( + "presupuesto.maquetacion.epub", null, locale); + } + descripcion += "
"; + resumen.put("datosMaquetacion", descripcion); + } + } + NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(locale); + String formattedString = currencyFormat.format(total.setScale(2, RoundingMode.HALF_UP).doubleValue()); + resumen.put("total", formattedString); + resumen.put("lineas", lineas); + resumen.put("servicios", serviciosExtras); + return resumen; + } +} diff --git a/src/main/java/com/imprimelibros/erp/pdf/PdfService.java b/src/main/java/com/imprimelibros/erp/pdf/PdfService.java index 4ab1688..dd3d101 100644 --- a/src/main/java/com/imprimelibros/erp/pdf/PdfService.java +++ b/src/main/java/com/imprimelibros/erp/pdf/PdfService.java @@ -3,7 +3,6 @@ package com.imprimelibros.erp.pdf; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.text.NumberFormat; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -16,12 +15,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.imprimelibros.erp.presupuesto.PresupuestoRepository; import com.imprimelibros.erp.presupuesto.dto.Presupuesto; +import com.imprimelibros.erp.common.Utils; + @Service public class PdfService { private final TemplateRegistry registry; private final PdfTemplateEngine engine; private final PdfRenderer renderer; private final PresupuestoRepository presupuestoRepository; + private final Utils utils; private final Map empresa = Map.of( "nombre", "ImprimeLibros ERP", @@ -86,11 +88,12 @@ public class PdfService { } public PdfService(TemplateRegistry registry, PdfTemplateEngine engine, PdfRenderer renderer, - PresupuestoRepository presupuestoRepository) { + PresupuestoRepository presupuestoRepository, Utils utils) { this.registry = registry; this.engine = engine; this.renderer = renderer; this.presupuestoRepository = presupuestoRepository; + this.utils = utils; } private byte[] generate(DocumentSpec spec) { @@ -140,6 +143,9 @@ public class PdfService { model.put("servicios", List.of( Map.of("descripcion", "Transporte península", "unidades", 1, "precio", 90.00))); + Map specs = utils.getTextoPresupuesto(presupuesto, locale); + model.put("specs", specs); + Map pricing = new HashMap<>(); ObjectMapper mapper = new ObjectMapper(); @@ -150,22 +156,22 @@ public class PdfService { List tiradas = snapshot.keySet().stream().toList(); pricing.put("tiradas", tiradas); pricing.put("impresion", snapshot.values().stream() - .map(p -> formatCurrency(p.getPrecioTotalTirada(), locale)) + .map(p -> Utils.formatCurrency(p.getPrecioTotalTirada(), locale)) .toList()); pricing.put("servicios", snapshot.values().stream() - .map(p -> formatCurrency(p.getServiciosTotal(), locale)) + .map(p -> Utils.formatCurrency(p.getServiciosTotal(), locale)) .toList()); pricing.put("peso", snapshot.values().stream() - .map(p -> formatCurrency(p.getPeso(), locale)) + .map(p -> Utils.formatCurrency(p.getPeso(), locale)) .toList()); pricing.put("iva_4", snapshot.values().stream() - .map(p -> formatCurrency(p.getIvaImporte4(), locale)) + .map(p -> Utils.formatCurrency(p.getIvaImporte4(), locale)) .toList()); pricing.put("iva_21", snapshot.values().stream() - .map(p -> formatCurrency(p.getIvaImporte21(), locale)) + .map(p -> Utils.formatCurrency(p.getIvaImporte21(), locale)) .toList()); pricing.put("total", snapshot.values().stream() - .map(p -> formatCurrency(p.getTotalConIva(), locale)) + .map(p -> Utils.formatCurrency(p.getTotalConIva(), locale)) .toList()); pricing.put("show_iva_4", presupuesto.getIvaImporte4().floatValue() > 0); pricing.put("show_iva_21", presupuesto.getIvaImporte21().floatValue() > 0); @@ -196,9 +202,4 @@ public class PdfService { throw new RuntimeException("Error generando presupuesto PDF", e); } } - - private static String formatCurrency(double value, Locale locale) { - NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale); - return currencyFormatter.format(value); - } } diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java index a7690e5..0555968 100644 --- a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java +++ b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoDatatableService.java @@ -1,19 +1,15 @@ package com.imprimelibros.erp.presupuesto; +import com.imprimelibros.erp.common.Utils; import com.imprimelibros.erp.datatables.*; import com.imprimelibros.erp.presupuesto.dto.Presupuesto; import jakarta.persistence.criteria.Expression; -import jakarta.persistence.criteria.JoinType; import org.springframework.context.MessageSource; -import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import jakarta.persistence.criteria.Predicate; -import java.math.BigDecimal; -import java.text.NumberFormat; import java.time.*; import java.time.format.DateTimeFormatter; import java.util.*; @@ -71,7 +67,7 @@ public class PresupuestoDatatableService { cb.coalesce(root.get("paginasNegro"), cb.literal(0)))) .add("estado", p -> msg(p.getEstado().getMessageKey(), locale)) - .add("totalConIva", p -> formatCurrency(p.getTotalConIva(), locale)) + .add("totalConIva", p -> Utils.formatCurrency(p.getTotalConIva(), locale)) .addIf(publico, "pais", Presupuesto::getPais) .addIf(publico, "region", Presupuesto::getRegion) .addIf(publico, "ciudad", Presupuesto::getCiudad) @@ -105,12 +101,6 @@ public class PresupuestoDatatableService { return df.format(instant); } - private String formatCurrency(BigDecimal value, Locale locale) { - if (value == null) - return ""; - return NumberFormat.getCurrencyInstance(locale).format(value); - } - private String generarBotones(Presupuesto p) { boolean borrador = p.getEstado() == Presupuesto.Estado.borrador; String id = String.valueOf(p.getId()); diff --git a/src/main/resources/i18n/pdf_es.properties b/src/main/resources/i18n/pdf_es.properties index 1e1c507..e5ae686 100644 --- a/src/main/resources/i18n/pdf_es.properties +++ b/src/main/resources/i18n/pdf_es.properties @@ -14,6 +14,7 @@ pdf.presupuesto.client=CLIENTE: pdf.presupuesto.date=FECHA: pdf.presupuesto.titulo=Título: +pdf.presupuesto.descripcion=Descripción: pdf.table.tirada=TIRADA pdf.table.impresion=IMPRESIÓN @@ -22,6 +23,11 @@ pdf.table.iva-4=IVA 4% pdf.table.iva-21=IVA 21% pdf.table.precio-total=PRECIO TOTAL +pdf.servicios-adicionales=Servicios adicionales: +pdf.ejemplares-deposito-legal=Impresión de {0} ejemplares de depósito legal
+pdf.datos-maquetacion=Datos de maquetación: +pdf.datos-marcapaginas=Datos de marcapáginas: + pdf.politica-privacidad=Política de privacidad pdf.politica-privacidad.responsable=Responsable: Impresión Imprime Libros - CIF: B04998886 - Teléfono de contacto: 910052574 pdf.politica-privacidad.correo-direccion=Correo electrónico: info@imprimelibros.com - Dirección postal: Calle José Picón, Nº 28 Local A, 28028, Madrid diff --git a/src/main/resources/i18n/presupuesto_es.properties b/src/main/resources/i18n/presupuesto_es.properties index 7539dc9..5ba868c 100644 --- a/src/main/resources/i18n/presupuesto_es.properties +++ b/src/main/resources/i18n/presupuesto_es.properties @@ -187,6 +187,7 @@ presupuesto.extras=Servicios Extras presupuesto.extras-descripcion=Seleccione los servicios adicionales que desea añadir al presupuesto presupuesto.extras-retractilado=Retractilado presupuesto.extras-isbn=ISBN +presupuesto.extras-service-isbn=ISBN presupuesto.extras-deposito-legal=Depósito Legal presupuesto.extras-deposito-legal-descripcion=Se añadirán 4 ejemplares a la tirada presupuesto.extras-revision-archivos=Revisión de archivos diff --git a/src/main/resources/static/assets/css/presupuestopdf.css b/src/main/resources/static/assets/css/presupuestopdf.css index 165d183..18d7197 100644 --- a/src/main/resources/static/assets/css/presupuestopdf.css +++ b/src/main/resources/static/assets/css/presupuestopdf.css @@ -207,6 +207,7 @@ body.has-watermark { margin-left: 15mm; /* ← margen izquierdo real del A4 */ margin-right: auto; /* opcional */ color: #5c5c5c; + font-size: 9pt; } .align-with-text { @@ -231,6 +232,26 @@ body.has-watermark { padding-right: 0; } +/* Listas sin margen superior por defecto */ +ul, ol { + margin-top: 0; + margin-bottom: 0rem; /* si quieres algo abajo */ + padding-left: 1.25rem; /* sangría */ +} + +/* Párrafos con menos margen inferior */ +p { + margin: 0 0 .5rem; +} + +/* Si una lista va justo después de un texto o título, que no tenga hueco arriba */ +p + ul, p + ol, +h1 + ul, h2 + ul, h3 + ul, h4 + ul, h5 + ul, h6 + ul, +div + ul, div + ol { + margin-top: 0; +} + + .block-title { text-transform: uppercase; font-weight: 700; @@ -289,20 +310,20 @@ body.has-watermark { .prices thead th { text-align: left; - padding: 6px; + padding: 3px; border-bottom: 2px solid var(--accent); background: #eef8fe; - font-weight: 700; + font-weight: 500; } .prices tbody td { border-bottom: 1px solid var(--line); - padding: 6px; + padding: 3px; } .prices .col-tirada { width: 22%; - font-weight: 700; + font-weight: 500; } /* Footer */ diff --git a/src/main/resources/templates/imprimelibros/pdf/presupuesto-a4.html b/src/main/resources/templates/imprimelibros/pdf/presupuesto-a4.html index 36459a2..b883f3e 100644 --- a/src/main/resources/templates/imprimelibros/pdf/presupuesto-a4.html +++ b/src/main/resources/templates/imprimelibros/pdf/presupuesto-a4.html @@ -68,17 +68,37 @@ Libro de prueba - + +
+ Descripción: +
-
+
Sin especificaciones técnicas.
-
-
+
+
+ +
+
+ Servicios adicionales + +
+
+ Datos de maquetación: + +
+
+ Datos de marcapáginas: + +
+
+
+ - +
diff --git a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_buttons.html b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_buttons.html index 84a47c4..878f812 100644 --- a/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_buttons.html +++ b/src/main/resources/templates/imprimelibros/presupuestos/presupuestador-items/_buttons.html @@ -1,57 +1,46 @@ -
+
- - - - - - + + + +
-
-
-
+
\ No newline at end of file diff --git a/src/test/java/com/imprimelibros/erp/pdf/PdfSmokeTest.java b/src/test/java/com/imprimelibros/erp/pdf/PdfSmokeTest.java index 3515dc6..a52a549 100644 --- a/src/test/java/com/imprimelibros/erp/pdf/PdfSmokeTest.java +++ b/src/test/java/com/imprimelibros/erp/pdf/PdfSmokeTest.java @@ -4,7 +4,7 @@ package com.imprimelibros.erp.pdf; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; - +/* import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -15,7 +15,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.awt.Desktop; - +*/ @SpringBootTest class PdfSmokeTest {
TIRADA