acabando presupuesto

This commit is contained in:
2025-10-16 21:46:02 +02:00
parent ff9c04afb6
commit ea8a005cde
11 changed files with 349 additions and 139 deletions

View File

@ -1,5 +1,10 @@
package com.imprimelibros.erp.pdf;
public enum DocumentType {
PRESUPUESTO, PEDIDO, FACTURA
}
PRESUPUESTO, PEDIDO, FACTURA;
@Override
public String toString() {
return name().toLowerCase();
}
}

View File

@ -1,32 +1,47 @@
package com.imprimelibros.erp.pdf;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ContentDisposition;
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; }
public PdfController(PdfService pdfService) {
this.pdfService = pdfService;
}
@PostMapping("/{type}/{templateId}")
@GetMapping(value = "/{type}/{id}", produces = "application/pdf")
public ResponseEntity<byte[]> generate(
@PathVariable("type") DocumentType type,
@PathVariable String templateId,
@RequestBody Map<String,Object> model,
@PathVariable("type") String type,
@PathVariable String id,
@RequestParam(defaultValue = "inline") String mode,
Locale locale) {
var spec = new DocumentSpec(type, templateId, locale, model);
var pdf = pdfService.generate(spec);
if (type.equals(DocumentType.PRESUPUESTO.toString()) && id == null) {
throw new IllegalArgumentException("Falta el ID del presupuesto para generar el PDF");
}
if (type.equals(DocumentType.PRESUPUESTO.toString())) {
Long presupuestoId = Long.valueOf(id);
byte[] pdf = pdfService.generaPresupuesto(presupuestoId, locale);
var headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDisposition(
("download".equals(mode)
? ContentDisposition.attachment()
: ContentDisposition.inline()).filename("presupuesto-" + id + ".pdf").build());
return new ResponseEntity<>(pdf, headers, HttpStatus.OK);
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
var fileName = type.name().toLowerCase() + "-" + templateId + ".pdf";
return ResponseEntity.ok()
.header("Content-Type", "application/pdf")
.header("Content-Disposition", "inline; filename=\"" + fileName + "\"")
.body(pdf);
}
}

View File

@ -1,20 +1,99 @@
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;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
@Service
public class PdfService {
private final TemplateRegistry registry;
private final PdfTemplateEngine engine;
private final PdfRenderer renderer;
private final PresupuestoRepository presupuestoRepository;
public PdfService(TemplateRegistry registry, PdfTemplateEngine engine, PdfRenderer renderer) {
private final Map<String, String> empresa = Map.of(
"nombre", "ImprimeLibros ERP",
"direccion", "C/ Dirección 123, 28000 Madrid",
"telefono", "+34 600 000 000",
"email", "info@imprimelibros.com",
"cif", "B-12345678",
"cp", "28000",
"poblacion", "Madrid",
"web", "www.imprimelibros.com");
private static class PrecioTirada {
private Double peso;
@JsonProperty("iva_importe_4")
private Double ivaImporte4;
@JsonProperty("total_con_iva")
private Double totalConIva;
@JsonProperty("base_imponible")
private Double baseImponible;
@JsonProperty("iva_importe_21")
private Double ivaImporte21;
@JsonProperty("precio_unitario")
private Double precioUnitario;
@JsonProperty("servicios_total")
private Double serviciosTotal;
@JsonProperty("precio_total_tirada")
private Double precioTotalTirada;
public Double getPeso() {
return peso;
}
public Double getIvaImporte4() {
return ivaImporte4;
}
public Double getTotalConIva() {
return totalConIva;
}
public Double getBaseImponible() {
return baseImponible;
}
public Double getIvaImporte21() {
return ivaImporte21;
}
public Double getPrecioUnitario() {
return precioUnitario;
}
public Double getServiciosTotal() {
return serviciosTotal;
}
public Double getPrecioTotalTirada() {
return precioTotalTirada;
}
}
public PdfService(TemplateRegistry registry, PdfTemplateEngine engine, PdfRenderer renderer,
PresupuestoRepository presupuestoRepository) {
this.registry = registry;
this.engine = engine;
this.renderer = renderer;
this.presupuestoRepository = presupuestoRepository;
}
public byte[] generate(DocumentSpec spec) {
private 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());
@ -22,4 +101,104 @@ public class PdfService {
var html = engine.render(template, spec.locale(), spec.model());
return renderer.renderHtmlToPdf(html);
}
public byte[] generaPresupuesto(Long presupuestoId, Locale locale) {
try {
Presupuesto presupuesto = presupuestoRepository.findById(presupuestoId)
.orElseThrow(() -> new IllegalArgumentException("Presupuesto no encontrado: " + presupuestoId));
Map<String, Object> model = new HashMap<>();
model.put("numero", presupuesto.getId());
model.put("fecha", presupuesto.getUpdatedAt());
model.put("empresa", empresa);
model.put("cliente", Map.of(
"nombre", presupuesto.getUser().getFullName()));
model.put("titulo", presupuesto.getTitulo());
/*
* Map<String, Object> resumen = presupuestoService.getTextosResumen(
* presupuesto, null, model, model, null)
*/
model.put("lineas", List.of(
Map.of("descripcion", "Impresión interior B/N offset 80 g",
"meta", "300 páginas · tinta negra · papel 80 g",
"uds", 1000,
"precio", 2.15,
"dto", 0,
"importe", 2150.0),
Map.of("descripcion", "Cubierta color 300 g laminado mate",
"meta", "Lomo 15 mm · 4/0 · laminado mate",
"uds", 1000,
"precio", 0.38,
"dto", 5.0,
"importe", 361.0)));
model.put("servicios", List.of(
Map.of("descripcion", "Transporte península", "unidades", 1, "precio", 90.00)));
Map<String, Object> pricing = new HashMap<>();
ObjectMapper mapper = new ObjectMapper();
// Si quieres parsear directamente a un Map:
Map<Integer, PrecioTirada> snapshot = mapper.readValue(presupuesto.getPricingSnapshotJson(),
mapper.getTypeFactory().constructMapType(Map.class, Integer.class, PrecioTirada.class));
List<Integer> tiradas = snapshot.keySet().stream().toList();
pricing.put("tiradas", tiradas);
pricing.put("impresion", snapshot.values().stream()
.map(p -> formatCurrency(p.getPrecioTotalTirada(), locale))
.toList());
pricing.put("servicios", snapshot.values().stream()
.map(p -> formatCurrency(p.getServiciosTotal(), locale))
.toList());
pricing.put("peso", snapshot.values().stream()
.map(p -> formatCurrency(p.getPeso(), locale))
.toList());
pricing.put("iva_4", snapshot.values().stream()
.map(p -> formatCurrency(p.getIvaImporte4(), locale))
.toList());
pricing.put("iva_21", snapshot.values().stream()
.map(p -> formatCurrency(p.getIvaImporte21(), locale))
.toList());
pricing.put("total", snapshot.values().stream()
.map(p -> formatCurrency(p.getTotalConIva(), locale))
.toList());
pricing.put("show_iva_4", presupuesto.getIvaImporte4().floatValue() > 0);
pricing.put("show_iva_21", presupuesto.getIvaImporte21().floatValue() > 0);
model.put("pricing", pricing);
var spec = new DocumentSpec(
DocumentType.PRESUPUESTO,
"presupuesto-a4",
Locale.forLanguageTag("es-ES"),
model);
byte[] pdf = this.generate(spec);
// HTML
// (Opcional) generar HTML de depuración con CSS incrustado
try {
String templateName = registry.resolve(DocumentType.PRESUPUESTO, "presupuesto-a4");
String html = engine.render(templateName, Locale.forLanguageTag("es-ES"), model);
String css = Files.readString(Path.of("src/main/resources/static/assets/css/presupuestopdf.css"));
String htmlWithCss = html.replaceFirst("(?i)</head>", "<style>\n" + css + "\n</style>\n</head>");
Path htmlPath = Path.of("target/presupuesto-test.html");
Files.writeString(htmlPath, htmlWithCss, StandardCharsets.UTF_8);
} catch (Exception ignore) {
/* solo para depuración */ }
return pdf;
} catch (Exception e) {
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);
}
}

View File

@ -7,11 +7,29 @@ import jakarta.persistence.*;
public class MaquetacionMatrices {
public enum Formato{
A5, _17x24_, A4
A5, _17x24_, A4;
private final String label;
Formato() {
this.label = this.name().indexOf('_') > -1 ? this.name().replace("_", "") + " mm" : this.name();
}
public String getLabel() {
return label;
}
}
public enum FontSize{
small, medium, big
small("presupuesto.maquetacion.cuerpo-texto-pequeño"),
medium("presupuesto.maquetacion.cuerpo-texto-medio"),
big("presupuesto.maquetacion.cuerpo-texto-grande");
private final String messageKey;
FontSize(String messageKey) {
this.messageKey = messageKey;
}
public String getMessageKey() {
return messageKey;
}
}
@Id

View File

@ -11,8 +11,6 @@ import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -38,12 +36,11 @@ import com.imprimelibros.erp.presupuesto.classes.PresupuestoMaquetacion;
import com.imprimelibros.erp.presupuesto.classes.PresupuestoMarcapaginas;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices;
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatricesRepository;
import com.imprimelibros.erp.presupuesto.marcapaginas.MarcapaginasRepository;
import com.imprimelibros.erp.users.UserDao;
import com.imprimelibros.erp.users.UserDetailsImpl;
import jakarta.persistence.criteria.CriteriaBuilder.In;
import jakarta.servlet.http.HttpServletRequest;
import com.imprimelibros.erp.externalApi.skApiClient;
@ -815,33 +812,59 @@ public class PresupuestoService {
servicioData.put("id", servicio.get("id"));
if (servicio.get("id").equals("marcapaginas")) {
String descripcion = servicio.get("label").toString();
/*String papel_marcapaginas = datosMarcapaginas != null
? ((Map<String, Object>) datosMarcapaginas).get("papel").toString()
: "";
if (papel_marcapaginas.equals("cartulina_grafica")) {
papel_marcapaginas = messageSource.getMessage("presupuesto.marcapaginas.papel.cartulina-grafica", null, locale);
} else if (papel_marcapaginas.equals("estucado_mate")) {
papel_marcapaginas = messageSource.getMessage("presupuesto.marcapaginas.papel.estucado-mate", null, locale);
} else {
papel_marcapaginas = "";
}*/
descripcion += "<br><ul><li>";
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) + " - " +
descripcion += "<br><ul><li>";
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 += messageSource.getMessage(
Marcapaginas.Acabado.valueOf(datosMarcapaginas.get("acabado").toString()).getMessageKey(),
null, locale);
descripcion += "</li></ul>";
servicioData.put("descripcion", descripcion);
} else if(servicio.get("id").equals("maquetacion")) {
} else if (servicio.get("id").equals("maquetacion")) {
String descripcion = servicio.get("label").toString();
descripcion += "<br><ul><li>";
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 += "</li></ul>";
servicioData.put("descripcion", descripcion);
}
else{
} else {
servicioData.put("descripcion", servicio.get("label"));
}
servicioData.put("precio", servicio.get("id").equals("marcapaginas")
? Double.parseDouble(servicio.get("price").toString())
/ Double.parseDouble(servicio.get("units").toString())
@ -881,7 +904,8 @@ public class PresupuestoService {
System.out.println("Error guardando datos adicionales: " + e.getMessage());
}
Map<String, Object> resumen = getTextosResumen(presupuesto, servicios, datosMaquetacion, datosMarcapaginas, locale);
Map<String, Object> resumen = getTextosResumen(presupuesto, servicios, datosMaquetacion, datosMarcapaginas,
locale);
if (resumen.containsKey("error"))
return resumen;
@ -1035,21 +1059,7 @@ public class PresupuestoService {
else if (s.get("id").equals("ejemplar-prueba")) {
serviciosIva4 = BigDecimal.valueOf(
s.get("price") != null ? Double.parseDouble(String.valueOf(s.get("price"))) : 0.0);
} else if (s.get("id").equals("marcapaginas")) {
PresupuestoMarcapaginas pm = presupuesto.getDatosMarcapaginasJson() != null
? new ObjectMapper().readValue(presupuesto.getDatosMarcapaginasJson(),
PresupuestoMarcapaginas.class)
: null;
Map<String, Object> precio_marcapaginas = this.getPrecioMarcapaginas(pm, locale);
s.put("price", precio_marcapaginas.getOrDefault("precio_total", 0.0));
} else if (s.get("id").equals("maquetacion")) {
PresupuestoMaquetacion pm = presupuesto.getDatosMaquetacionJson() != null
? new ObjectMapper().readValue(presupuesto.getDatosMaquetacionJson(),
PresupuestoMaquetacion.class)
: null;
Map<String, Object> precio_maquetacion = this.getPrecioMaquetacion(pm, locale);
s.put("price", precio_maquetacion.getOrDefault("precio", 0.0));
}
}
double unidades = Double.parseDouble(String.valueOf(s.getOrDefault("units", 0)));
double precio = Double.parseDouble(String.valueOf(
s.get("id").equals("marcapaginas")
@ -1146,7 +1156,8 @@ public class PresupuestoService {
datosMaquetacion != null ? new ObjectMapper().writeValueAsString(datosMaquetacion) : null);
presupuesto.setDatosMarcapaginasJson(
datosMarcapaginas != null ? new ObjectMapper().writeValueAsString(datosMarcapaginas) : null);
var resumen = this.getTextosResumen(presupuesto, serviciosList, datosMaquetacion, datosMarcapaginas, locale);
var resumen = this.getTextosResumen(presupuesto, serviciosList, datosMaquetacion, datosMarcapaginas,
locale);
Object serviciosObj = resumen.get("servicios");