diff --git a/pom.xml b/pom.xml
index 578047b..b058b23 100644
--- a/pom.xml
+++ b/pom.xml
@@ -139,6 +139,18 @@
test
+
+
+ com.openhtmltopdf
+ openhtmltopdf-pdfbox
+ 1.0.10
+
+
+ com.openhtmltopdf
+ openhtmltopdf-slf4j
+ 1.0.10
+
+
diff --git a/src/main/java/com/imprimelibros/erp/datatables/DataTablesSpecification.java b/src/main/java/com/imprimelibros/erp/datatables/DataTablesSpecification.java
index 1b479d3..298b08d 100644
--- a/src/main/java/com/imprimelibros/erp/datatables/DataTablesSpecification.java
+++ b/src/main/java/com/imprimelibros/erp/datatables/DataTablesSpecification.java
@@ -31,7 +31,7 @@ public class DataTablesSpecification {
ands.add(like(cb, path, col.search.value));
} catch (IllegalArgumentException ex) {
// columna no mapeada o relación: la ignoramos
- System.out.println("[DT] columna no mapeada o relación: " + col.name);
+ //System.out.println("[DT] columna no mapeada o relación: " + col.name);
}
}
}
diff --git a/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java b/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java
index 37f8089..1dc4c20 100644
--- a/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java
+++ b/src/main/java/com/imprimelibros/erp/externalApi/skApiClient.java
@@ -90,7 +90,7 @@ public class skApiClient {
margen.getMargenMax(),
margen.getMargenMin());
double nuevoPrecio = precios.get(i) * (1 + margenValue / 100.0);
- precios.set(i, nuevoPrecio);
+ precios.set(i, Math.round(nuevoPrecio * 10000.0) / 10000.0); // redondear a 2 decimales
} else {
System.out.println("No se encontró margen para tirada " + tirada);
}
diff --git a/src/main/java/com/imprimelibros/erp/pdf/DocumentSpec.java b/src/main/java/com/imprimelibros/erp/pdf/DocumentSpec.java
new file mode 100644
index 0000000..9638eea
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/pdf/DocumentSpec.java
@@ -0,0 +1,11 @@
+package com.imprimelibros.erp.pdf;
+
+import java.util.Locale;
+import java.util.Map;
+
+public record DocumentSpec(
+ DocumentType type,
+ String templateId, // p.ej. "presupuesto-a4"
+ Locale locale,
+ Map model // data del documento
+) {}
diff --git a/src/main/java/com/imprimelibros/erp/pdf/DocumentType.java b/src/main/java/com/imprimelibros/erp/pdf/DocumentType.java
new file mode 100644
index 0000000..4f27aef
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/pdf/DocumentType.java
@@ -0,0 +1,5 @@
+package com.imprimelibros.erp.pdf;
+
+public enum DocumentType {
+ PRESUPUESTO, PEDIDO, FACTURA
+}
diff --git a/src/main/java/com/imprimelibros/erp/pdf/PdfController.java b/src/main/java/com/imprimelibros/erp/pdf/PdfController.java
new file mode 100644
index 0000000..6c4f5c6
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/pdf/PdfController.java
@@ -0,0 +1,32 @@
+package com.imprimelibros.erp.pdf;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Locale;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/pdf")
+public class PdfController {
+ private final PdfService pdfService;
+
+ public PdfController(PdfService pdfService) { this.pdfService = pdfService; }
+
+ @PostMapping("/{type}/{templateId}")
+ public ResponseEntity generate(
+ @PathVariable("type") DocumentType type,
+ @PathVariable String templateId,
+ @RequestBody Map model,
+ Locale locale) {
+
+ var spec = new DocumentSpec(type, templateId, locale, model);
+ var pdf = pdfService.generate(spec);
+
+ var fileName = type.name().toLowerCase() + "-" + templateId + ".pdf";
+ return ResponseEntity.ok()
+ .header("Content-Type", "application/pdf")
+ .header("Content-Disposition", "inline; filename=\"" + fileName + "\"")
+ .body(pdf);
+ }
+}
diff --git a/src/main/java/com/imprimelibros/erp/pdf/PdfModuleConfig.java b/src/main/java/com/imprimelibros/erp/pdf/PdfModuleConfig.java
new file mode 100644
index 0000000..244f915
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/pdf/PdfModuleConfig.java
@@ -0,0 +1,18 @@
+package com.imprimelibros.erp.pdf;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import java.util.Map;
+
+@Configuration
+@ConfigurationProperties(prefix = "imprimelibros.pdf")
+public class PdfModuleConfig {
+ /**
+ * Mapa: "TYPE:templateId" -> "ruta thymeleaf" (sin extensión).
+ * Ej: "PRESUPUESTO:presupuesto-a4" -> "pdf/presupuesto-a4"
+ */
+ private Map templates;
+
+ public Map getTemplates() { return templates; }
+ public void setTemplates(Map templates) { this.templates = templates; }
+}
diff --git a/src/main/java/com/imprimelibros/erp/pdf/PdfRenderer.java b/src/main/java/com/imprimelibros/erp/pdf/PdfRenderer.java
new file mode 100644
index 0000000..3502808
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/pdf/PdfRenderer.java
@@ -0,0 +1,44 @@
+package com.imprimelibros.erp.pdf;
+
+import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
+import org.springframework.core.io.Resource;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.io.ByteArrayOutputStream;
+
+@Service
+public class PdfRenderer {
+
+ @Value("classpath:/static/")
+ private Resource staticRoot;
+
+ public byte[] renderHtmlToPdf(String html) {
+ try (var baos = new ByteArrayOutputStream()) {
+ var builder = new PdfRendererBuilder();
+
+ builder.useFont(() -> getClass().getResourceAsStream("/static/assets/fonts/OpenSans-Regular.ttf"), "Open Sans",
+ 400, com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder.FontStyle.NORMAL, true);
+ builder.useFont(() -> getClass().getResourceAsStream("/static/assets/fonts/OpenSans-SemiBold.ttf"), "Open Sans",
+ 600, com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder.FontStyle.NORMAL, true);
+ builder.useFont(() -> getClass().getResourceAsStream("/static/assets/fonts/OpenSans-Bold.ttf"), "Open Sans", 700,
+ com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder.FontStyle.NORMAL, true);
+
+ builder.useFastMode();
+ builder.withHtmlContent(html, baseUrl());
+ builder.toStream(baos);
+ builder.run();
+ return baos.toByteArray();
+ } catch (Exception e) {
+ throw new IllegalStateException("Error generando PDF", e);
+ }
+ }
+
+ private String baseUrl() {
+ try {
+ return staticRoot.getURL().toString();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/imprimelibros/erp/pdf/PdfService.java b/src/main/java/com/imprimelibros/erp/pdf/PdfService.java
new file mode 100644
index 0000000..246d0f5
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/pdf/PdfService.java
@@ -0,0 +1,25 @@
+package com.imprimelibros.erp.pdf;
+
+import org.springframework.stereotype.Service;
+
+@Service
+public class PdfService {
+ private final TemplateRegistry registry;
+ private final PdfTemplateEngine engine;
+ private final PdfRenderer renderer;
+
+ public PdfService(TemplateRegistry registry, PdfTemplateEngine engine, PdfRenderer renderer) {
+ this.registry = registry;
+ this.engine = engine;
+ this.renderer = renderer;
+ }
+
+ public byte[] generate(DocumentSpec spec) {
+ var template = registry.resolve(spec.type(), spec.templateId());
+ if (template == null) {
+ throw new IllegalArgumentException("Plantilla no registrada: " + spec.type() + ":" + spec.templateId());
+ }
+ var html = engine.render(template, spec.locale(), spec.model());
+ return renderer.renderHtmlToPdf(html);
+ }
+}
diff --git a/src/main/java/com/imprimelibros/erp/pdf/PdfTemplateEngine.java b/src/main/java/com/imprimelibros/erp/pdf/PdfTemplateEngine.java
new file mode 100644
index 0000000..cc588b6
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/pdf/PdfTemplateEngine.java
@@ -0,0 +1,22 @@
+package com.imprimelibros.erp.pdf;
+
+import org.thymeleaf.context.Context;
+import org.thymeleaf.spring6.SpringTemplateEngine;
+import org.springframework.stereotype.Service;
+import java.util.Locale;
+import java.util.Map;
+
+@Service
+public class PdfTemplateEngine {
+ private final SpringTemplateEngine thymeleaf;
+
+ public PdfTemplateEngine(SpringTemplateEngine thymeleaf) {
+ this.thymeleaf = thymeleaf;
+ }
+
+ public String render(String templateName, Locale locale, Map model) {
+ Context ctx = new Context(locale);
+ if (model != null) model.forEach(ctx::setVariable);
+ return thymeleaf.process(templateName, ctx);
+ }
+}
diff --git a/src/main/java/com/imprimelibros/erp/pdf/TemplateRegistry.java b/src/main/java/com/imprimelibros/erp/pdf/TemplateRegistry.java
new file mode 100644
index 0000000..153d88f
--- /dev/null
+++ b/src/main/java/com/imprimelibros/erp/pdf/TemplateRegistry.java
@@ -0,0 +1,18 @@
+// com.imprimelibros.erp.pdf.TemplateRegistry.java
+package com.imprimelibros.erp.pdf;
+
+import org.springframework.stereotype.Service;
+
+@Service
+public class TemplateRegistry {
+ private final PdfModuleConfig config;
+
+ public TemplateRegistry(PdfModuleConfig config) {
+ this.config = config;
+ }
+
+ public String resolve(DocumentType type, String templateId) {
+ if (config.getTemplates() == null) return null;
+ return config.getTemplates().get(type.name() + ":" + templateId);
+ }
+}
diff --git a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java
index e44bf70..36e65fa 100644
--- a/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java
+++ b/src/main/java/com/imprimelibros/erp/presupuesto/PresupuestoController.java
@@ -531,7 +531,9 @@ public class PresupuestoController {
"presupuesto.plantilla-cubierta",
"presupuesto.plantilla-cubierta-text",
"presupuesto.impresion-cubierta",
- "presupuesto.impresion-cubierta-help");
+ "presupuesto.impresion-cubierta-help",
+ "presupuesto.exito.guardado",
+ "presupuesto.add.error.save.title");
Map translations = translationService.getTranslations(locale, keys);
model.addAttribute("languageBundle", translations);
@@ -558,12 +560,14 @@ public class PresupuestoController {
return "redirect:/presupuesto";
}
+ model.addAttribute("presupuesto_id", presupuestoOpt.get().getId());
String path = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest().getRequestURI();
String mode = path.contains("/view/") ? "view" : "edit";
if (mode.equals("view")) {
model.addAttribute("appMode", "view");
} else {
+ model.addAttribute("cliente_id", presupuestoOpt.get().getUser().getId());
model.addAttribute("appMode", "edit");
}
model.addAttribute("id", presupuestoOpt.get().getId());
@@ -594,7 +598,7 @@ public class PresupuestoController {
model.addAttribute("ancho_alto_max", variableService.getValorEntero("ancho_alto_max"));
model.addAttribute("appMode", "add");
-
+
if (!mode.equals("public")) {
model.addAttribute("cliente_id", clienteId);
}
@@ -603,24 +607,6 @@ public class PresupuestoController {
return "imprimelibros/presupuestos/presupuesto-form";
}
- @GetMapping(value = "/api/get", produces = "application/json")
- public ResponseEntity getPresupuesto(
- @RequestParam("id") Long id, Authentication authentication) {
-
- Optional presupuestoOpt = presupuestoRepository.findById(id);
-
- if (!presupuestoService.canAccessPresupuesto(presupuestoOpt.get(), authentication)) {
- return ResponseEntity.status(403).build();
- }
-
- if (presupuestoOpt.isPresent()) {
- PresupuestoFormDataDto vm = formDataMapper.toFormData(presupuestoOpt.get());
- return ResponseEntity.ok(vm);
- } else {
- return ResponseEntity.notFound().build();
- }
- }
-
@GetMapping(value = "/datatable/{tipo}", produces = "application/json")
@ResponseBody
public DataTablesResponse