preparando el imprimir

This commit is contained in:
2025-10-12 21:42:04 +02:00
parent 6641c1f077
commit 26c2ca543a
41 changed files with 1325 additions and 208 deletions

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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<String, Object> model // data del documento
) {}

View File

@ -0,0 +1,5 @@
package com.imprimelibros.erp.pdf;
public enum DocumentType {
PRESUPUESTO, PEDIDO, FACTURA
}

View File

@ -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<byte[]> generate(
@PathVariable("type") DocumentType type,
@PathVariable String templateId,
@RequestBody Map<String,Object> 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);
}
}

View File

@ -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<String, String> templates;
public Map<String, String> getTemplates() { return templates; }
public void setTemplates(Map<String, String> templates) { this.templates = templates; }
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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<String,Object> model) {
Context ctx = new Context(locale);
if (model != null) model.forEach(ctx::setVariable);
return thymeleaf.process(templateName, ctx);
}
}

View File

@ -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);
}
}

View File

@ -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<String, String> 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<PresupuestoFormDataDto> getPresupuesto(
@RequestParam("id") Long id, Authentication authentication) {
Optional<Presupuesto> 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<Map<String, Object>> datatable(
@ -638,7 +624,6 @@ public class PresupuestoController {
}
}
@DeleteMapping("/{id}")
@Transactional
public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth, Locale locale) {
@ -697,7 +682,25 @@ public class PresupuestoController {
}
}
@PostMapping(path="/save")
@GetMapping(value = "/api/get", produces = "application/json")
public ResponseEntity<PresupuestoFormDataDto> getPresupuesto(
@RequestParam("id") Long id, Authentication authentication) {
Optional<Presupuesto> 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();
}
}
@PostMapping(path = "api/save")
public ResponseEntity<?> save(
@RequestBody Map<String, Object> body,
Locale locale, HttpServletRequest request) {
@ -707,6 +710,11 @@ public class PresupuestoController {
String mode = objectMapper.convertValue(body.get("mode"), String.class);
@SuppressWarnings("unchecked")
List<Map<String, Object>> serviciosList = (List<Map<String, Object>>) body.getOrDefault("servicios", List.of());
Long cliente_id = objectMapper.convertValue(body.get("cliente_id"), Long.class);
Map<String, Object> datosMaquetacion = (Map<String, Object>) objectMapper
.convertValue(body.get("datosMaquetacion"), Map.class);
Map<String, Object> datosMarcapaginas = (Map<String, Object>) objectMapper
.convertValue(body.get("datosMarcapaginas"), Map.class);
Set<ConstraintViolation<Presupuesto>> violations = validator.validate(presupuesto,
PresupuestoValidationGroups.All.class);
@ -715,34 +723,26 @@ public class PresupuestoController {
Map<String, String> errores = new HashMap<>();
for (ConstraintViolation<Presupuesto> v : violations) {
String campo = v.getPropertyPath().toString();
String mensaje = messageSource.getMessage(v.getMessage().replace("{", "").replace("}", ""), null, locale);
String mensaje = messageSource.getMessage(v.getMessage().replace("{", "").replace("}", ""), null,
locale);
errores.put(campo, mensaje);
}
return ResponseEntity.badRequest().body(errores);
}
try {
var resumen = presupuestoService.getTextosResumen(presupuesto, serviciosList, locale);
Long cliente_id = objectMapper.convertValue(body.get("cliente_id"), Long.class);
if(id == null && cliente_id != null && !mode.equals("public")) {
Map<String, Object> saveResult = presupuestoService.guardarPresupuesto(
presupuesto,
serviciosList,
datosMaquetacion,
datosMarcapaginas,
mode,
cliente_id,
id,
request,
locale);
presupuesto.setUser(userRepo.findById(cliente_id).orElse(null));
presupuesto.setOrigen(Presupuesto.Origen.privado);
}
if (mode.equals("public")) {
presupuesto.setOrigen(Presupuesto.Origen.publico);
String sessionId = request.getSession(true).getId();
String ip = request.getRemoteAddr();
presupuesto = presupuestoService.getDatosLocalizacion(presupuesto, sessionId, ip);
if (id != null) {
presupuesto.setId(id); // para que actualice, no cree uno nuevo
}
}
presupuesto = presupuestoService.generateTotalizadores(presupuesto, serviciosList, resumen, locale);
Map<String, Object> saveResult = presupuestoService.guardarPresupuesto(presupuesto);
return ResponseEntity.ok(Map.of("id", saveResult.get("presupuesto_id"),
"message", messageSource.getMessage("presupuesto.exito.guardado", null, locale)));
} catch (Exception ex) {

View File

@ -3,6 +3,9 @@ package com.imprimelibros.erp.presupuesto.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import jakarta.persistence.criteria.CriteriaBuilder.In;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@ -84,10 +87,17 @@ public class PresupuestoFormDataMapper {
// ===== Servicios / Extras =====
public static class Servicios {
public List<String> servicios = List.of();
public List<DatosServicios> servicios = new ArrayList<DatosServicios>();
public DatosMarcapaginas datosMarcapaginas = new DatosMarcapaginas();
public DatosMaquetacion datosMaquetacion = new DatosMaquetacion();
public static class DatosServicios {
public String id;
public String label;
public Integer units;
public Double price;
}
public static class DatosMarcapaginas {
public Integer marcapaginas_tirada = 100;
public String tamanio_marcapaginas = "_50x140_";
@ -184,8 +194,11 @@ public class PresupuestoFormDataMapper {
vm.selectedTirada = p.getSelectedTirada();
// ===== Servicios desde JSONs
// servicios_json: acepta ["maquetacion","marcapaginas"] o [{id:...}, ...]
vm.servicios.servicios = parseServiciosIds(p.getServiciosJson());
vm.servicios.servicios = parse(p.getServiciosJson(),
new TypeReference<List<PresupuestoFormDataDto.Servicios.DatosServicios>>() {
});
if (vm.servicios.servicios == null)
vm.servicios.servicios = new ArrayList<>();
// datos_maquetacion_json
PresupuestoFormDataDto.Servicios.DatosMaquetacion maq = parse(p.getDatosMaquetacionJson(),
@ -230,31 +243,13 @@ public class PresupuestoFormDataMapper {
}
}
private List<String> parseServiciosIds(String json) {
if (json == null || json.isBlank())
return new ArrayList<>();
private <T> T parse(String json, TypeReference<T> typeRef) {
try {
// 1) intentar como lista de strings
List<String> ids = om.readValue(json, new TypeReference<List<String>>() {
});
return ids != null ? ids : new ArrayList<>();
} catch (Exception ignore) {
}
try {
// 2) intentar como lista de objetos con 'id'
List<Map<String, Object>> list = om.readValue(json, new TypeReference<>() {
});
List<String> ids = new ArrayList<>();
for (Map<String, Object> it : list) {
Object id = it.get("id");
if (id != null)
ids.add(String.valueOf(id));
}
return ids;
} catch (
Exception e) {
return new ArrayList<>();
if (json == null || json.isBlank())
return null;
return om.readValue(json, typeRef);
} catch (Exception e) {
return null;
}
}

View File

@ -41,6 +41,10 @@ import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatricesReposito
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;
@Service
@ -448,6 +452,16 @@ public class PresupuestoService {
: "0.00";
}
private String obtenerPrecioRetractilado(Integer tirada) {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("tirada",tirada != null ? tirada : 0);
Double precio_retractilado = apiClient.getRetractilado(requestBody);
return precio_retractilado != null
? String.valueOf(Math.round(precio_retractilado * 100.0) / 100.0)
: "0.00";
}
public Map<String, Object> obtenerServiciosExtras(Presupuesto presupuesto, Locale locale) {
List<Object> opciones = new ArrayList<>();
@ -747,13 +761,20 @@ public class PresupuestoService {
if (hayDepositoLegal) {
pressupuestoTemp.setSelectedTirada(
presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() + 4 : 4);
for (Integer i = 0; i < pressupuestoTemp.getTiradas().length; i++) {
Integer tirada = pressupuestoTemp.getTiradas()[i];
if (tirada != null && tirada >= 4) {
tirada = tirada + 4;
}
pressupuestoTemp.getTiradas()[i] = tirada;
}
}
HashMap<String, Object> precios = this.calcularPresupuesto(pressupuestoTemp, locale);
if (precios.containsKey("error")) {
resumen.put("error", precios.get("error"));
return resumen;
}
resumen.put("precios", precios);
HashMap<String, Object> linea = new HashMap<>();
Double precio_unitario = 0.0;
@ -824,7 +845,7 @@ public class PresupuestoService {
if (mode.equals("public")) {
presupuesto = getDatosLocalizacion(presupuesto, sessionId, ip);
} else
presupuesto.setOrigen(Presupuesto.Origen.privado);
@ -847,7 +868,7 @@ public class PresupuestoService {
if (save != null && save) {
// Si NO es para guardar (solo calcular resumen), devolver sin persistir
this.guardarPresupuesto(presupuesto);
presupuestoRepository.saveAndFlush(presupuesto);
}
// Opcional: devolver el id guardado al frontend para que lo envíe en llamadas
@ -865,24 +886,24 @@ public class PresupuestoService {
}
public Presupuesto getDatosLocalizacion(Presupuesto presupuesto, String sessionId, String ip) {
presupuesto.setOrigen(Presupuesto.Origen.publico);
presupuesto.setSessionId(sessionId);
// IP: guarda hash y trunc (si tienes campos). Si no, guarda tal cual en
// ip_trunc/ip_hash según tu modelo.
String ipTrunc = anonymizeIp(ip);
presupuesto.setIpTrunc(ipTrunc);
presupuesto.setIpHash(Integer.toHexString(ip.hashCode()));
// ubicación (si tienes un servicio GeoIP disponible; si no, omite estas tres
// líneas)
try {
GeoIpService.GeoData geo = geoIpService.lookup(ip).orElse(null);
presupuesto.setPais(geo.getPais());
presupuesto.setRegion(geo.getRegion());
presupuesto.setCiudad(geo.getCiudad());
} catch (Exception ignore) {
}
presupuesto.setOrigen(Presupuesto.Origen.publico);
presupuesto.setSessionId(sessionId);
// IP: guarda hash y trunc (si tienes campos). Si no, guarda tal cual en
// ip_trunc/ip_hash según tu modelo.
String ipTrunc = anonymizeIp(ip);
presupuesto.setIpTrunc(ipTrunc);
presupuesto.setIpHash(Integer.toHexString(ip.hashCode()));
// ubicación (si tienes un servicio GeoIP disponible; si no, omite estas tres
// líneas)
try {
GeoIpService.GeoData geo = geoIpService.lookup(ip).orElse(null);
presupuesto.setPais(geo.getPais());
presupuesto.setRegion(geo.getRegion());
presupuesto.setCiudad(geo.getCiudad());
} catch (Exception ignore) {
}
return presupuesto;
}
@ -892,84 +913,185 @@ public class PresupuestoService {
Map<String, Object> resumen,
Locale locale) {
// Genera los totalizadores (precio unitario, total tirada, etc.) sin guardar
double precioUnit = 0.0;
int cantidad = presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : 0;
try {
@SuppressWarnings("unchecked")
List<Double> precios = (List<Double>) ((Map<String, Object>) resumen.getOrDefault("precios", Map.of()))
.getOrDefault("precios", List.of());
if (precios.isEmpty()) {
// si no venía en "resumen", recalcúlalo directamente
var preciosCalc = this.calcularPresupuesto(presupuesto, locale);
precios = (List<Double>) ((Map<String, Object>) preciosCalc.get("data")).get("precios");
}
precioUnit = precios.get(0);
// guarda el snapshot completo de precios para auditoría
presupuesto.setPreciosPorTiradaJson(new ObjectMapper().writeValueAsString(precios));
} catch (Exception ignore) {
Map<Integer, Map<String, Object>> pricing_snapshot = new HashMap<>();
@SuppressWarnings("unchecked")
Map<String, Object> preciosNode = (Map<String, Object>) resumen.getOrDefault("precios", Map.of());
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) preciosNode.getOrDefault("data", Map.of());
@SuppressWarnings("unchecked")
List<Integer> tiradas = (List<Integer>) data.getOrDefault("tiradas", List.of());
@SuppressWarnings("unchecked")
List<Double> precios = (List<Double>) data.getOrDefault("precios", List.of());
@SuppressWarnings("unchecked")
List<Double> pesos = (List<Double>) data.getOrDefault("peso", List.of());
if (precios.isEmpty()) {
var preciosCalc = this.calcularPresupuesto(presupuesto, locale);
precios = (List<Double>) ((Map<String, Object>) preciosCalc.get("data")).getOrDefault("precios", List.of());
}
BigDecimal precioTotalTirada = BigDecimal.valueOf(precioUnit)
.multiply(BigDecimal.valueOf(cantidad))
.setScale(2, RoundingMode.HALF_UP);
// iterate getTiradas with a foreach with not null
for (Integer tirada : presupuesto.getTiradas()) {
if (tirada == null) {
continue;
}
// servicios_total
BigDecimal serviciosTotal = BigDecimal.ZERO;
if (servicios != null) {
for (Map<String, Object> s : servicios) {
// Genera los totalizadores (precio unitario, total tirada, etc.) sin guardar
double precioUnit = 0.0;
int cantidad = tirada != null ? tirada : 0;
int index = tiradas.indexOf(tirada);
try {
if (index >= 0 && index < precios.size()) {
precioUnit = precios.get(index);
} else if (!precios.isEmpty()) {
precioUnit = precios.get(0); // fallback al primero
}
// guarda el snapshot completo de precios para auditoría
presupuesto.setPreciosPorTiradaJson(new ObjectMapper().writeValueAsString(precios));
} catch (Exception ignore) {
precioUnit = 0.0;
}
BigDecimal precioTotalTirada = BigDecimal.valueOf(precioUnit)
.multiply(BigDecimal.valueOf(cantidad))
.setScale(2, RoundingMode.HALF_UP);
// servicios_total
BigDecimal serviciosTotal = BigDecimal.ZERO;
if (servicios != null) {
for (Map<String, Object> s : servicios) {
try {
// retractilado o ejemplar-prueba: recalcular precio
if (s.get("id").equals("retractilado")) {
double precio_retractilado = obtenerPrecioRetractilado(cantidad) != null
? Double.parseDouble(obtenerPrecioRetractilado(cantidad))
: 0.0;
s.put("price", precio_retractilado);
}
double unidades = Double.parseDouble(String.valueOf(s.getOrDefault("units", 0)));
double precio = Double.parseDouble(String.valueOf(
s.get("id").equals("marcapaginas")
? (Double.parseDouble(String.valueOf(s.get("price"))) / unidades) // unidad
: s.getOrDefault("price", 0)));
serviciosTotal = serviciosTotal.add(
BigDecimal.valueOf(precio).multiply(BigDecimal.valueOf(unidades)));
} catch (Exception ignore) {
}
}
try {
double unidades = Double.parseDouble(String.valueOf(s.getOrDefault("units", 0)));
double precio = Double.parseDouble(String.valueOf(
s.get("id").equals("marcapaginas")
? (Double.parseDouble(String.valueOf(s.get("price"))) / unidades) // unidad
: s.getOrDefault("price", 0)));
serviciosTotal = serviciosTotal.add(
BigDecimal.valueOf(precio).multiply(BigDecimal.valueOf(unidades)));
presupuesto.setServiciosJson(new ObjectMapper().writeValueAsString(servicios));
} catch (Exception ignore) {
}
}
// base imponible, IVA y total (si tienes IVA configurable, úsalo; si no, 0)
BigDecimal baseImponible = precioTotalTirada.add(serviciosTotal);
BigDecimal ivaTipo = BigDecimal.ZERO;
try {
presupuesto.setServiciosJson(new ObjectMapper().writeValueAsString(servicios));
double iva = 4.0; // 0..100
ivaTipo = BigDecimal.valueOf(iva);
} catch (Exception ignore) {
}
BigDecimal ivaImporte = baseImponible.multiply(ivaTipo).divide(BigDecimal.valueOf(100), 2,
RoundingMode.HALF_UP);
BigDecimal totalConIva = baseImponible.add(ivaImporte);
// precios y totales
if (tirada == (presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : 0)) {
presupuesto.setPrecioUnitario(BigDecimal.valueOf(precioUnit).setScale(6, RoundingMode.HALF_UP));
presupuesto.setPrecioTotalTirada(precioTotalTirada);
presupuesto.setServiciosTotal(serviciosTotal);
presupuesto.setBaseImponible(baseImponible);
presupuesto.setIvaTipo(ivaTipo);
presupuesto.setIvaImporte(ivaImporte);
presupuesto.setTotalConIva(totalConIva);
}
Map<String, Object> snap = new HashMap<>();
snap.put("precio_unitario", BigDecimal.valueOf(precioUnit).setScale(6, RoundingMode.HALF_UP));
snap.put("precio_total_tirada", precioTotalTirada);
snap.put("servicios_total", serviciosTotal);
snap.put("base_imponible", baseImponible);
snap.put("iva_tipo", ivaTipo);
snap.put("iva_importe", ivaImporte);
snap.put("total_con_iva", totalConIva);
snap.put("peso", (index >= 0 && index < pesos.size()) ? pesos.get(index) : 0.0);
pricing_snapshot.put(tirada, snap);
}
// base imponible, IVA y total (si tienes IVA configurable, úsalo; si no, 0)
BigDecimal baseImponible = precioTotalTirada.add(serviciosTotal);
BigDecimal ivaTipo = BigDecimal.ZERO;
try {
double iva = 4.0; // 0..100
ivaTipo = BigDecimal.valueOf(iva);
String json = new ObjectMapper()
.writer()
.withDefaultPrettyPrinter() // opcional
.writeValueAsString(pricing_snapshot);
presupuesto.setPricingSnapshotJson(pricing_snapshot.isEmpty() ? null : json);
} catch (Exception ignore) {
}
BigDecimal ivaImporte = baseImponible.multiply(ivaTipo).divide(BigDecimal.valueOf(100), 2,
RoundingMode.HALF_UP);
BigDecimal totalConIva = baseImponible.add(ivaImporte);
// precios y totales
presupuesto.setPrecioUnitario(BigDecimal.valueOf(precioUnit).setScale(6, RoundingMode.HALF_UP));
presupuesto.setPrecioTotalTirada(precioTotalTirada);
presupuesto.setServiciosTotal(serviciosTotal);
presupuesto.setBaseImponible(baseImponible);
presupuesto.setIvaTipo(ivaTipo);
presupuesto.setIvaImporte(ivaImporte);
presupuesto.setTotalConIva(totalConIva);
return presupuesto;
}
@Transactional
public HashMap<String, Object> guardarPresupuesto(Presupuesto presupuesto) {
public HashMap<String, Object> guardarPresupuesto(
Presupuesto presupuesto,
List<Map<String, Object>> serviciosList,
Map<String, Object> datosMaquetacion,
Map<String, Object> datosMarcapaginas,
String mode,
Long cliente_id,
Long id,
HttpServletRequest request,
Locale locale) {
HashMap<String, Object> result = new HashMap<>();
try {
Presupuesto p = presupuestoRepository.saveAndFlush(presupuesto);
presupuesto.setDatosMaquetacionJson(
datosMaquetacion != null ? new ObjectMapper().writeValueAsString(datosMaquetacion) : null);
presupuesto.setDatosMarcapaginasJson(
datosMarcapaginas != null ? new ObjectMapper().writeValueAsString(datosMarcapaginas) : null);
var resumen = this.getTextosResumen(presupuesto, serviciosList, locale);
Object serviciosObj = resumen.get("servicios");
if (serviciosObj instanceof List<?> servicios && !servicios.isEmpty()) {
// serializa a JSON válido
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(servicios);
presupuesto.setServiciosJson(json);
} else {
// decide tu política: null o "[]"
presupuesto.setServiciosJson(null); // o presupuesto.setServiciosJson("[]");
}
if (cliente_id != null && !mode.equals("public")) {
presupuesto.setUser(userRepo.findById(cliente_id).orElse(null));
presupuesto.setOrigen(Presupuesto.Origen.privado);
if (id != null) {
presupuesto.setId(id);
}
}
if (mode.equals("public")) {
presupuesto.setOrigen(Presupuesto.Origen.publico);
String sessionId = request.getSession(true).getId();
String ip = request.getRemoteAddr();
presupuesto = this.getDatosLocalizacion(presupuesto, sessionId, ip);
if (id != null) {
presupuesto.setId(id); // para que actualice, no cree uno nuevo
}
}
presupuesto = this.generateTotalizadores(presupuesto, serviciosList, resumen, locale);
presupuestoRepository.saveAndFlush(presupuesto);
result.put("success", true);
result.put("presupuesto_id", p.getId());
result.put("presupuesto_id", presupuesto.getId());
return result;
} catch (Exception e) {
@ -1007,7 +1129,7 @@ public class PresupuestoService {
if (isUser) {
// Si es usuario, solo puede ver sus propios presupuestos
String username = authentication.getName();
if (!presupuesto.getUser().getUserName().equals(username)) {
if (presupuesto.getUser() == null || !presupuesto.getUser().getUserName().equals(username)) {
return false;
}
}