mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-12 16:38:48 +00:00
terminando pdf de facturas
This commit is contained in:
11121
logs/erp.log
11121
logs/erp.log
File diff suppressed because it is too large
Load Diff
@ -101,6 +101,24 @@ public class Utils {
|
|||||||
throw new IllegalStateException("No se pudo obtener el ID del usuario actual");
|
throw new IllegalStateException("No se pudo obtener el ID del usuario actual");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static User currentUser(Principal principal) {
|
||||||
|
|
||||||
|
if (principal == null) {
|
||||||
|
throw new IllegalStateException("Usuario no autenticado");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (principal instanceof Authentication auth) {
|
||||||
|
Object principalObj = auth.getPrincipal();
|
||||||
|
|
||||||
|
if (principalObj instanceof UserDetailsImpl udi) {
|
||||||
|
return udi.getUser();
|
||||||
|
} else if (principalObj instanceof User u && u.getId() != null) {
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("No se pudo obtener el ID del usuario actual");
|
||||||
|
}
|
||||||
|
|
||||||
public static String formatCurrency(BigDecimal amount, Locale locale) {
|
public static String formatCurrency(BigDecimal amount, Locale locale) {
|
||||||
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale);
|
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale);
|
||||||
return currencyFormatter.format(amount);
|
return currencyFormatter.format(amount);
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.imprimelibros.erp.common.web;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Entities;
|
||||||
|
|
||||||
|
public class HtmlToXhtml {
|
||||||
|
|
||||||
|
public static String toXhtml(String html) {
|
||||||
|
if (html == null || html.isBlank()) return "";
|
||||||
|
|
||||||
|
Document doc = Jsoup.parseBodyFragment(html);
|
||||||
|
|
||||||
|
doc.outputSettings()
|
||||||
|
.syntax(Document.OutputSettings.Syntax.xml) // => <br/>
|
||||||
|
.escapeMode(Entities.EscapeMode.xhtml) // entidades XHTML
|
||||||
|
.prettyPrint(false); // no metas saltos raros
|
||||||
|
|
||||||
|
// devolvemos sólo el contenido del body (sin <html><head>…)
|
||||||
|
return doc.body().html();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,13 +6,18 @@ import com.imprimelibros.erp.datatables.DataTablesRequest;
|
|||||||
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||||
import com.imprimelibros.erp.facturacion.EstadoFactura;
|
import com.imprimelibros.erp.facturacion.EstadoFactura;
|
||||||
import com.imprimelibros.erp.facturacion.Factura;
|
import com.imprimelibros.erp.facturacion.Factura;
|
||||||
|
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.FacturaRepository;
|
import com.imprimelibros.erp.facturacion.repo.FacturaRepository;
|
||||||
|
import com.imprimelibros.erp.facturacion.service.FacturacionService;
|
||||||
import com.imprimelibros.erp.i18n.TranslationService;
|
import com.imprimelibros.erp.i18n.TranslationService;
|
||||||
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||||
|
|
||||||
import jakarta.persistence.EntityNotFoundException;
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
@ -22,20 +27,20 @@ import org.springframework.stereotype.Controller;
|
|||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
|
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/facturas")
|
@RequestMapping("/facturas")
|
||||||
@PreAuthorize("hasRole('SUPERADMIN') || hasRole('ADMIN')")
|
@PreAuthorize("hasRole('SUPERADMIN') || hasRole('ADMIN')")
|
||||||
public class FacturasController {
|
public class FacturasController {
|
||||||
|
|
||||||
|
private final FacturacionService facturacionService;
|
||||||
|
|
||||||
private final FacturaRepository repo;
|
private final FacturaRepository repo;
|
||||||
private final TranslationService translationService;
|
private final TranslationService translationService;
|
||||||
private final MessageSource messageSource;
|
private final MessageSource messageSource;
|
||||||
@ -45,11 +50,12 @@ public class FacturasController {
|
|||||||
FacturaRepository repo,
|
FacturaRepository repo,
|
||||||
TranslationService translationService,
|
TranslationService translationService,
|
||||||
MessageSource messageSource,
|
MessageSource messageSource,
|
||||||
PedidoService pedidoService) {
|
PedidoService pedidoService, FacturacionService facturacionService) {
|
||||||
this.repo = repo;
|
this.repo = repo;
|
||||||
this.translationService = translationService;
|
this.translationService = translationService;
|
||||||
this.messageSource = messageSource;
|
this.messageSource = messageSource;
|
||||||
this.pedidoService = pedidoService;
|
this.pedidoService = pedidoService;
|
||||||
|
this.facturacionService = facturacionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@ -75,12 +81,36 @@ public class FacturasController {
|
|||||||
PedidoDireccion direccionFacturacion = pedidoService
|
PedidoDireccion direccionFacturacion = pedidoService
|
||||||
.getPedidoDireccionFacturacionByPedidoId(factura.getPedidoId());
|
.getPedidoDireccionFacturacionByPedidoId(factura.getPedidoId());
|
||||||
|
|
||||||
|
List<String> keys = List.of(
|
||||||
|
"facturas.lineas.error.base",
|
||||||
|
"facturas.lineas.delete.title",
|
||||||
|
"facturas.lineas.delete.text",
|
||||||
|
|
||||||
|
"facturas.pagos.delete.title",
|
||||||
|
"facturas.pagos.delete.text",
|
||||||
|
"facturas.pagos.error.cantidad",
|
||||||
|
"facturas.pagos.error.fecha",
|
||||||
|
|
||||||
|
"app.eliminar",
|
||||||
|
"app.cancelar");
|
||||||
|
|
||||||
|
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||||
|
model.addAttribute("languageBundle", translations);
|
||||||
|
|
||||||
model.addAttribute("direccionFacturacion", direccionFacturacion);
|
model.addAttribute("direccionFacturacion", direccionFacturacion);
|
||||||
model.addAttribute("factura", factura);
|
model.addAttribute("factura", factura);
|
||||||
|
|
||||||
return "imprimelibros/facturas/facturas-form";
|
return "imprimelibros/facturas/facturas-form";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/guardar")
|
||||||
|
public ResponseEntity<?> guardarFacturaCabeceraYDireccion(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestBody @Valid FacturaGuardarDto payload) {
|
||||||
|
facturacionService.guardarCabeceraYDireccionFacturacion(id, payload);
|
||||||
|
return ResponseEntity.ok(Map.of("ok", true));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/container")
|
@GetMapping("/{id}/container")
|
||||||
public String facturaContainer(@PathVariable Long id, Model model, Locale locale) {
|
public String facturaContainer(@PathVariable Long id, Model model, Locale locale) {
|
||||||
Factura factura = repo.findById(id)
|
Factura factura = repo.findById(id)
|
||||||
@ -116,7 +146,8 @@ public class FacturasController {
|
|||||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||||
|
|
||||||
if (factura.getEstado() != EstadoFactura.validada) {
|
if (factura.getEstado() != EstadoFactura.validada) {
|
||||||
return ResponseEntity.badRequest().body("Solo se pueden marcar como borrador facturas en estado 'validada'.");
|
return ResponseEntity.badRequest()
|
||||||
|
.body("Solo se pueden marcar como borrador facturas en estado 'validada'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
factura.setEstado(EstadoFactura.borrador);
|
factura.setEstado(EstadoFactura.borrador);
|
||||||
@ -125,22 +156,75 @@ public class FacturasController {
|
|||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{facturaId}/lineas")
|
||||||
|
public ResponseEntity<?> createLinea(@PathVariable Long facturaId,
|
||||||
|
@Valid @RequestBody FacturaLineaUpsertDto req) {
|
||||||
|
facturacionService.createLinea(facturaId, req);
|
||||||
|
return ResponseEntity.ok(Map.of("ok", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{facturaId}/lineas/{lineaId}")
|
||||||
|
public ResponseEntity<?> updateLinea(@PathVariable Long facturaId,
|
||||||
|
@PathVariable Long lineaId,
|
||||||
|
@Valid @RequestBody FacturaLineaUpsertDto req) {
|
||||||
|
facturacionService.upsertLinea(facturaId, req);
|
||||||
|
return ResponseEntity.ok(Map.of("ok", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{facturaId}/lineas/{lineaId}/delete")
|
||||||
|
public ResponseEntity<?> deleteLinea(@PathVariable Long facturaId,
|
||||||
|
@PathVariable Long lineaId) {
|
||||||
|
facturacionService.borrarLinea(facturaId, lineaId);
|
||||||
|
return ResponseEntity.ok(Map.of("ok", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -----------------------------
|
||||||
|
* Pagos
|
||||||
|
* --------------------------------
|
||||||
|
*/
|
||||||
|
@PostMapping("/{facturaId}/pagos")
|
||||||
|
public ResponseEntity<?> createPago(
|
||||||
|
@PathVariable Long facturaId,
|
||||||
|
@Valid @RequestBody FacturaPagoUpsertDto req, Principal principal) {
|
||||||
|
facturacionService.upsertPago(facturaId, req, principal);
|
||||||
|
return ResponseEntity.ok(Map.of("ok", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{facturaId}/pagos/{pagoId}")
|
||||||
|
public ResponseEntity<?> updatePago(
|
||||||
|
@PathVariable Long facturaId,
|
||||||
|
@PathVariable Long pagoId,
|
||||||
|
@Valid @RequestBody FacturaPagoUpsertDto req,
|
||||||
|
Principal principal) {
|
||||||
|
// opcional: fuerza consistencia
|
||||||
|
req.setId(pagoId);
|
||||||
|
facturacionService.upsertPago(facturaId, req, principal);
|
||||||
|
return ResponseEntity.ok(Map.of("ok", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{facturaId}/pagos/{pagoId}/delete")
|
||||||
|
public ResponseEntity<?> deletePago(
|
||||||
|
@PathVariable Long facturaId,
|
||||||
|
@PathVariable Long pagoId, Principal principal) {
|
||||||
|
facturacionService.borrarPago(facturaId, pagoId, principal);
|
||||||
|
return ResponseEntity.ok(Map.of("ok", true));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/notas")
|
@PostMapping("/{id}/notas")
|
||||||
public ResponseEntity<?> setNotas(
|
public ResponseEntity<?> setNotas(
|
||||||
@PathVariable Long id,
|
@PathVariable Long id,
|
||||||
@RequestBody Map<String, String> payload,
|
@RequestBody Map<String, String> payload,
|
||||||
Model model,
|
Model model,
|
||||||
Locale locale
|
Locale locale) {
|
||||||
) {
|
|
||||||
Factura factura = repo.findById(id)
|
Factura factura = repo.findById(id)
|
||||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||||
String notas = payload.get("notas");
|
String notas = payload.get("notas");
|
||||||
factura.setNotas(notas);
|
factura.setNotas(notas);
|
||||||
repo.save(factura);
|
repo.save(factura);
|
||||||
|
|
||||||
return ResponseEntity.ok().build();
|
return ResponseEntity.ok().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
// API: DataTables (server-side)
|
// API: DataTables (server-side)
|
||||||
|
|||||||
@ -0,0 +1,110 @@
|
|||||||
|
package com.imprimelibros.erp.facturacion.dto;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||||
|
|
||||||
|
public class DireccionFacturacionDto {
|
||||||
|
private String razonSocial;
|
||||||
|
private String identificacionFiscal;
|
||||||
|
private String direccion;
|
||||||
|
private String cp;
|
||||||
|
private String ciudad;
|
||||||
|
private String provincia;
|
||||||
|
private String paisKeyword;
|
||||||
|
private String telefono;
|
||||||
|
|
||||||
|
public String getRazonSocial() {
|
||||||
|
return razonSocial;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRazonSocial(String razonSocial) {
|
||||||
|
this.razonSocial = razonSocial;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIdentificacionFiscal() {
|
||||||
|
return identificacionFiscal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIdentificacionFiscal(String identificacionFiscal) {
|
||||||
|
this.identificacionFiscal = identificacionFiscal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDireccion() {
|
||||||
|
return direccion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDireccion(String direccion) {
|
||||||
|
this.direccion = direccion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCp() {
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCp(String cp) {
|
||||||
|
this.cp = cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCiudad() {
|
||||||
|
return ciudad;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCiudad(String ciudad) {
|
||||||
|
this.ciudad = ciudad;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProvincia() {
|
||||||
|
return provincia;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProvincia(String provincia) {
|
||||||
|
this.provincia = provincia;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPaisKeyword() {
|
||||||
|
return paisKeyword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPaisKeyword(String paisKeyword) {
|
||||||
|
this.paisKeyword = paisKeyword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTelefono() {
|
||||||
|
return telefono;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTelefono(String telefono) {
|
||||||
|
this.telefono = telefono;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PedidoDireccion toPedidoDireccion() {
|
||||||
|
PedidoDireccion pd = new PedidoDireccion();
|
||||||
|
applyTo(pd);
|
||||||
|
pd.setFacturacion(true);
|
||||||
|
return pd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyTo(PedidoDireccion pd) {
|
||||||
|
pd.setRazonSocial(this.razonSocial);
|
||||||
|
pd.setIdentificacionFiscal(this.identificacionFiscal);
|
||||||
|
pd.setDireccion(this.direccion);
|
||||||
|
|
||||||
|
// CP robusto
|
||||||
|
Integer cpInt = null;
|
||||||
|
if (this.cp != null && !this.cp.isBlank()) {
|
||||||
|
try {
|
||||||
|
cpInt = Integer.valueOf(this.cp.trim());
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// si quieres, lanza IllegalArgumentException para validarlo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pd.setCp(cpInt);
|
||||||
|
|
||||||
|
pd.setCiudad(this.ciudad);
|
||||||
|
pd.setProvincia(this.provincia);
|
||||||
|
|
||||||
|
pd.setPaisCode3(this.paisKeyword);
|
||||||
|
|
||||||
|
pd.setTelefono(this.telefono);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.imprimelibros.erp.facturacion.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class FacturaCabeceraDto {
|
||||||
|
private Long serieId;
|
||||||
|
private Long clienteId;
|
||||||
|
private LocalDateTime fechaEmision;
|
||||||
|
|
||||||
|
public Long getSerieId() {
|
||||||
|
return serieId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSerieId(Long serieId) {
|
||||||
|
this.serieId = serieId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getClienteId() {
|
||||||
|
return clienteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClienteId(Long clienteId) {
|
||||||
|
this.clienteId = clienteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getFechaEmision() {
|
||||||
|
return fechaEmision;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFechaEmision(LocalDateTime fechaEmision) {
|
||||||
|
this.fechaEmision = fechaEmision;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.imprimelibros.erp.facturacion.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
|
public class FacturaGuardarDto {
|
||||||
|
@Valid private FacturaCabeceraDto cabecera;
|
||||||
|
@Valid private DireccionFacturacionDto direccionFacturacion;
|
||||||
|
|
||||||
|
// getters/setters
|
||||||
|
public FacturaCabeceraDto getCabecera() {
|
||||||
|
return cabecera;
|
||||||
|
}
|
||||||
|
public void setCabecera(FacturaCabeceraDto cabecera) {
|
||||||
|
this.cabecera = cabecera;
|
||||||
|
}
|
||||||
|
public DireccionFacturacionDto getDireccionFacturacion() {
|
||||||
|
return direccionFacturacion;
|
||||||
|
}
|
||||||
|
public void setDireccionFacturacion(DireccionFacturacionDto direccionFacturacion) {
|
||||||
|
this.direccionFacturacion = direccionFacturacion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1,25 +1,21 @@
|
|||||||
package com.imprimelibros.erp.facturacion.dto;
|
package com.imprimelibros.erp.facturacion.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
public class FacturaLineaUpsertDto {
|
public class FacturaLineaUpsertDto {
|
||||||
|
|
||||||
private Long id; // null => nueva línea
|
// Para update puedes mandarlo, pero realmente lo sacamos del path
|
||||||
|
private Long id;
|
||||||
@NotBlank
|
|
||||||
private String descripcion;
|
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private Integer cantidad;
|
private String descripcion; // HTML
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private BigDecimal baseLinea; // base imponible de la línea (sin IVA)
|
private BigDecimal base;
|
||||||
|
|
||||||
private boolean aplicaIva4;
|
private BigDecimal iva4;
|
||||||
private boolean aplicaIva21;
|
private BigDecimal iva21;
|
||||||
|
|
||||||
public Long getId() { return id; }
|
public Long getId() { return id; }
|
||||||
public void setId(Long id) { this.id = id; }
|
public void setId(Long id) { this.id = id; }
|
||||||
@ -27,15 +23,12 @@ public class FacturaLineaUpsertDto {
|
|||||||
public String getDescripcion() { return descripcion; }
|
public String getDescripcion() { return descripcion; }
|
||||||
public void setDescripcion(String descripcion) { this.descripcion = descripcion; }
|
public void setDescripcion(String descripcion) { this.descripcion = descripcion; }
|
||||||
|
|
||||||
public Integer getCantidad() { return cantidad; }
|
public BigDecimal getBase() { return base; }
|
||||||
public void setCantidad(Integer cantidad) { this.cantidad = cantidad; }
|
public void setBase(BigDecimal base) { this.base = base; }
|
||||||
|
|
||||||
public BigDecimal getBaseLinea() { return baseLinea; }
|
public BigDecimal getIva4() { return iva4; }
|
||||||
public void setBaseLinea(BigDecimal baseLinea) { this.baseLinea = baseLinea; }
|
public void setIva4(BigDecimal iva4) { this.iva4 = iva4; }
|
||||||
|
|
||||||
public boolean isAplicaIva4() { return aplicaIva4; }
|
public BigDecimal getIva21() { return iva21; }
|
||||||
public void setAplicaIva4(boolean aplicaIva4) { this.aplicaIva4 = aplicaIva4; }
|
public void setIva21(BigDecimal iva21) { this.iva21 = iva21; }
|
||||||
|
|
||||||
public boolean isAplicaIva21() { return aplicaIva21; }
|
|
||||||
public void setAplicaIva21(boolean aplicaIva21) { this.aplicaIva21 = aplicaIva21; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import com.imprimelibros.erp.facturacion.FacturaLinea;
|
|||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface FacturaLineaRepository extends JpaRepository<FacturaLinea, Long> {
|
public interface FacturaLineaRepository extends JpaRepository<FacturaLinea, Long> {
|
||||||
List<FacturaLinea> findByFacturaId(Long facturaId);
|
List<FacturaLinea> findByFacturaId(Long facturaId);
|
||||||
|
Optional<FacturaLinea> findByIdAndFacturaId(Long id, Long facturaId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,10 @@ import com.imprimelibros.erp.facturacion.FacturaPago;
|
|||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface FacturaPagoRepository extends JpaRepository<FacturaPago, Long> {
|
public interface FacturaPagoRepository extends JpaRepository<FacturaPago, Long> {
|
||||||
List<FacturaPago> findByFacturaId(Long facturaId);
|
List<FacturaPago> findByFacturaIdAndDeletedAtIsNullOrderByFechaPagoDescIdDesc(Long facturaId);
|
||||||
|
Optional<FacturaPago> findByIdAndFacturaIdAndDeletedAtIsNull(Long id, Long facturaId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,15 +2,20 @@ package com.imprimelibros.erp.facturacion.service;
|
|||||||
|
|
||||||
import com.imprimelibros.erp.common.Utils;
|
import com.imprimelibros.erp.common.Utils;
|
||||||
import com.imprimelibros.erp.facturacion.*;
|
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.FacturaLineaUpsertDto;
|
||||||
import com.imprimelibros.erp.facturacion.dto.FacturaPagoUpsertDto;
|
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.FacturaPagoRepository;
|
||||||
import com.imprimelibros.erp.facturacion.repo.FacturaRepository;
|
import com.imprimelibros.erp.facturacion.repo.FacturaRepository;
|
||||||
import com.imprimelibros.erp.facturacion.repo.SerieFacturaRepository;
|
import com.imprimelibros.erp.facturacion.repo.SerieFacturaRepository;
|
||||||
import com.imprimelibros.erp.pedidos.Pedido;
|
import com.imprimelibros.erp.pedidos.Pedido;
|
||||||
import com.imprimelibros.erp.pedidos.PedidoLinea;
|
import com.imprimelibros.erp.pedidos.PedidoLinea;
|
||||||
import com.imprimelibros.erp.pedidos.PedidoLineaRepository;
|
import com.imprimelibros.erp.pedidos.PedidoLineaRepository;
|
||||||
|
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||||
|
import com.imprimelibros.erp.users.User;
|
||||||
|
import com.imprimelibros.erp.users.UserService;
|
||||||
|
|
||||||
import jakarta.persistence.EntityNotFoundException;
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
|
||||||
@ -24,6 +29,7 @@ import java.util.Map;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
|
import java.security.Principal;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@ -34,26 +40,34 @@ public class FacturacionService {
|
|||||||
private final FacturaRepository facturaRepo;
|
private final FacturaRepository facturaRepo;
|
||||||
private final SerieFacturaRepository serieRepo;
|
private final SerieFacturaRepository serieRepo;
|
||||||
private final FacturaPagoRepository pagoRepo;
|
private final FacturaPagoRepository pagoRepo;
|
||||||
|
private final FacturaLineaRepository lineaFacturaRepository;
|
||||||
private final PedidoLineaRepository pedidoLineaRepo;
|
private final PedidoLineaRepository pedidoLineaRepo;
|
||||||
|
private final UserService userService;
|
||||||
private final Utils utils;
|
private final Utils utils;
|
||||||
private final MessageSource messageSource;
|
private final MessageSource messageSource;
|
||||||
|
private final PedidoService pedidoService;
|
||||||
|
|
||||||
public FacturacionService(
|
public FacturacionService(
|
||||||
FacturaRepository facturaRepo,
|
FacturaRepository facturaRepo,
|
||||||
|
FacturaLineaRepository lineaFacturaRepository,
|
||||||
SerieFacturaRepository serieRepo,
|
SerieFacturaRepository serieRepo,
|
||||||
FacturaPagoRepository pagoRepo,
|
FacturaPagoRepository pagoRepo,
|
||||||
PedidoLineaRepository pedidoLineaRepo,
|
PedidoLineaRepository pedidoLineaRepo,
|
||||||
|
UserService userService,
|
||||||
Utils utils,
|
Utils utils,
|
||||||
MessageSource messageSource) {
|
MessageSource messageSource,
|
||||||
|
PedidoService pedidoService) {
|
||||||
this.facturaRepo = facturaRepo;
|
this.facturaRepo = facturaRepo;
|
||||||
|
this.lineaFacturaRepository = lineaFacturaRepository;
|
||||||
this.serieRepo = serieRepo;
|
this.serieRepo = serieRepo;
|
||||||
this.pagoRepo = pagoRepo;
|
this.pagoRepo = pagoRepo;
|
||||||
this.pedidoLineaRepo = pedidoLineaRepo;
|
this.pedidoLineaRepo = pedidoLineaRepo;
|
||||||
|
this.userService = userService;
|
||||||
this.utils = utils;
|
this.utils = utils;
|
||||||
this.messageSource = messageSource;
|
this.messageSource = messageSource;
|
||||||
|
this.pedidoService = pedidoService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public SerieFactura getDefaultSerieFactura() {
|
public SerieFactura getDefaultSerieFactura() {
|
||||||
List<SerieFactura> series = serieRepo.findAll();
|
List<SerieFactura> series = serieRepo.findAll();
|
||||||
if (series.isEmpty()) {
|
if (series.isEmpty()) {
|
||||||
@ -64,6 +78,11 @@ public class FacturacionService {
|
|||||||
return series.get(0);
|
return series.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Factura getFactura(Long facturaId) {
|
||||||
|
return facturaRepo.findById(facturaId)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------
|
// -----------------------
|
||||||
// Nueva factura
|
// Nueva factura
|
||||||
// -----------------------
|
// -----------------------
|
||||||
@ -113,7 +132,7 @@ public class FacturacionService {
|
|||||||
|
|
||||||
factura = facturaRepo.save(factura);
|
factura = facturaRepo.save(factura);
|
||||||
|
|
||||||
if(pedidoPendientePago) {
|
if (pedidoPendientePago) {
|
||||||
return factura;
|
return factura;
|
||||||
}
|
}
|
||||||
FacturaPago pago = new FacturaPago();
|
FacturaPago pago = new FacturaPago();
|
||||||
@ -149,6 +168,48 @@ public class FacturacionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@Transactional
|
||||||
public Factura validarFactura(Long facturaId) {
|
public Factura validarFactura(Long facturaId) {
|
||||||
Factura factura = facturaRepo.findById(facturaId)
|
Factura factura = facturaRepo.findById(facturaId)
|
||||||
@ -210,6 +271,20 @@ public class FacturacionService {
|
|||||||
// -----------------------
|
// -----------------------
|
||||||
// Líneas
|
// 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
|
@Transactional
|
||||||
public Factura upsertLinea(Long facturaId, FacturaLineaUpsertDto dto) {
|
public Factura upsertLinea(Long facturaId, FacturaLineaUpsertDto dto) {
|
||||||
@ -233,29 +308,15 @@ public class FacturacionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
linea.setDescripcion(dto.getDescripcion());
|
linea.setDescripcion(dto.getDescripcion());
|
||||||
linea.setCantidad(dto.getCantidad());
|
|
||||||
|
|
||||||
// Base por unidad o base total? Tu migración no define precio unitario.
|
linea.setBaseLinea(scale2(dto.getBase()));
|
||||||
// Asumimos que baseLinea es TOTAL de línea (sin IVA) y cantidad informativa.
|
|
||||||
linea.setBaseLinea(scale2(dto.getBaseLinea()));
|
|
||||||
|
|
||||||
// Iva por checks: calculamos importes, no porcentajes
|
linea.setIva4Linea(dto.getIva4());
|
||||||
BigDecimal iva4 = BigDecimal.ZERO;
|
linea.setIva21Linea(dto.getIva21());
|
||||||
BigDecimal iva21 = BigDecimal.ZERO;
|
|
||||||
|
|
||||||
if (dto.isAplicaIva4() && dto.isAplicaIva21()) {
|
linea.setTotalLinea(scale2(linea.getBaseLinea()
|
||||||
throw new IllegalArgumentException("Una línea no puede tener IVA 4% y 21% a la vez.");
|
.add(nvl(linea.getIva4Linea()))
|
||||||
}
|
.add(nvl(linea.getIva21Linea()))));
|
||||||
if (dto.isAplicaIva4()) {
|
|
||||||
iva4 = scale2(linea.getBaseLinea().multiply(new BigDecimal("0.04")));
|
|
||||||
}
|
|
||||||
if (dto.isAplicaIva21()) {
|
|
||||||
iva21 = scale2(linea.getBaseLinea().multiply(new BigDecimal("0.21")));
|
|
||||||
}
|
|
||||||
|
|
||||||
linea.setIva4Linea(iva4);
|
|
||||||
linea.setIva21Linea(iva21);
|
|
||||||
linea.setTotalLinea(scale2(linea.getBaseLinea().add(iva4).add(iva21)));
|
|
||||||
|
|
||||||
recalcularTotales(factura);
|
recalcularTotales(factura);
|
||||||
return facturaRepo.save(factura);
|
return facturaRepo.save(factura);
|
||||||
@ -284,7 +345,7 @@ public class FacturacionService {
|
|||||||
// -----------------------
|
// -----------------------
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Factura upsertPago(Long facturaId, FacturaPagoUpsertDto dto) {
|
public Factura upsertPago(Long facturaId, FacturaPagoUpsertDto dto, Principal principal) {
|
||||||
Factura factura = facturaRepo.findById(facturaId)
|
Factura factura = facturaRepo.findById(facturaId)
|
||||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||||
|
|
||||||
@ -293,6 +354,8 @@ public class FacturacionService {
|
|||||||
if (dto.getId() == null) {
|
if (dto.getId() == null) {
|
||||||
pago = new FacturaPago();
|
pago = new FacturaPago();
|
||||||
pago.setFactura(factura);
|
pago.setFactura(factura);
|
||||||
|
pago.setCreatedBy(Utils.currentUser(principal));
|
||||||
|
pago.setCreatedAt(Instant.now());
|
||||||
factura.getPagos().add(pago);
|
factura.getPagos().add(pago);
|
||||||
} else {
|
} else {
|
||||||
pago = factura.getPagos().stream()
|
pago = factura.getPagos().stream()
|
||||||
@ -305,7 +368,8 @@ public class FacturacionService {
|
|||||||
pago.setCantidadPagada(scale2(dto.getCantidadPagada()));
|
pago.setCantidadPagada(scale2(dto.getCantidadPagada()));
|
||||||
pago.setFechaPago(dto.getFechaPago() != null ? dto.getFechaPago() : LocalDateTime.now());
|
pago.setFechaPago(dto.getFechaPago() != null ? dto.getFechaPago() : LocalDateTime.now());
|
||||||
pago.setNotas(dto.getNotas());
|
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
|
// El tipo_pago de la factura: si tiene un pago, lo reflejamos (último pago
|
||||||
// manda)
|
// manda)
|
||||||
factura.setTipoPago(dto.getMetodoPago());
|
factura.setTipoPago(dto.getMetodoPago());
|
||||||
@ -315,14 +379,18 @@ public class FacturacionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Factura borrarPago(Long facturaId, Long pagoId) {
|
public Factura borrarPago(Long facturaId, Long pagoId, Principal principal) {
|
||||||
Factura factura = facturaRepo.findById(facturaId)
|
Factura factura = facturaRepo.findById(facturaId)
|
||||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||||
|
|
||||||
boolean removed = factura.getPagos().removeIf(p -> pagoId.equals(p.getId()));
|
FacturaPago pago = factura.getPagos().stream()
|
||||||
if (!removed) {
|
.filter(p -> pagoId.equals(p.getId()))
|
||||||
throw new EntityNotFoundException("Pago no encontrado: " + pagoId);
|
.findFirst()
|
||||||
}
|
.orElseThrow(() -> new EntityNotFoundException("Pago no encontrado: " + pagoId));
|
||||||
|
|
||||||
|
// soft delete
|
||||||
|
pago.setDeletedAt(Instant.now());
|
||||||
|
pago.setDeletedBy(Utils.currentUser(principal));
|
||||||
|
|
||||||
recalcularTotales(factura);
|
recalcularTotales(factura);
|
||||||
return facturaRepo.save(factura);
|
return facturaRepo.save(factura);
|
||||||
@ -364,6 +432,8 @@ public class FacturacionService {
|
|||||||
BigDecimal pagado = BigDecimal.ZERO;
|
BigDecimal pagado = BigDecimal.ZERO;
|
||||||
if (factura.getPagos() != null) {
|
if (factura.getPagos() != null) {
|
||||||
for (FacturaPago p : factura.getPagos()) {
|
for (FacturaPago p : factura.getPagos()) {
|
||||||
|
if (p.getDeletedAt() != null)
|
||||||
|
continue;
|
||||||
pagado = pagado.add(nvl(p.getCantidadPagada()));
|
pagado = pagado.add(nvl(p.getCantidadPagada()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -480,4 +550,20 @@ public class FacturacionService {
|
|||||||
.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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,8 +25,8 @@ public class PdfController {
|
|||||||
@RequestParam(defaultValue = "inline") String mode,
|
@RequestParam(defaultValue = "inline") String mode,
|
||||||
Locale locale) {
|
Locale locale) {
|
||||||
|
|
||||||
if (type.equals(DocumentType.PRESUPUESTO.toString()) && id == null) {
|
if (id == null) {
|
||||||
throw new IllegalArgumentException("Falta el ID del presupuesto para generar el PDF");
|
throw new IllegalArgumentException("Falta el ID para generar el PDF");
|
||||||
}
|
}
|
||||||
if (type.equals(DocumentType.PRESUPUESTO.toString())) {
|
if (type.equals(DocumentType.PRESUPUESTO.toString())) {
|
||||||
Long presupuestoId = Long.valueOf(id);
|
Long presupuestoId = Long.valueOf(id);
|
||||||
@ -39,7 +39,22 @@ public class PdfController {
|
|||||||
: ContentDisposition.inline()).filename("presupuesto-" + id + ".pdf").build());
|
: ContentDisposition.inline()).filename("presupuesto-" + id + ".pdf").build());
|
||||||
|
|
||||||
return new ResponseEntity<>(pdf, headers, HttpStatus.OK);
|
return new ResponseEntity<>(pdf, headers, HttpStatus.OK);
|
||||||
} else {
|
}/*else if(type.equals(DocumentType.PEDIDO.toString())) {
|
||||||
|
|
||||||
|
} */else if (type.equals(DocumentType.FACTURA.toString())) {
|
||||||
|
Long facturaId = Long.valueOf(id);
|
||||||
|
byte[] pdf = pdfService.generaFactura(facturaId, locale);
|
||||||
|
var headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_PDF);
|
||||||
|
headers.setContentDisposition(
|
||||||
|
("download".equals(mode)
|
||||||
|
? ContentDisposition.attachment()
|
||||||
|
: ContentDisposition.inline()).filename("factura-" + id + ".pdf").build());
|
||||||
|
|
||||||
|
return new ResponseEntity<>(pdf, headers, HttpStatus.OK);
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,11 @@ import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
|
|||||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||||
|
|
||||||
import com.imprimelibros.erp.common.Utils;
|
import com.imprimelibros.erp.common.Utils;
|
||||||
|
import com.imprimelibros.erp.common.web.HtmlToXhtml;
|
||||||
|
import com.imprimelibros.erp.facturacion.Factura;
|
||||||
|
import com.imprimelibros.erp.facturacion.service.FacturacionService;
|
||||||
|
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||||
|
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class PdfService {
|
public class PdfService {
|
||||||
@ -24,6 +29,8 @@ public class PdfService {
|
|||||||
private final PdfRenderer renderer;
|
private final PdfRenderer renderer;
|
||||||
private final PresupuestoRepository presupuestoRepository;
|
private final PresupuestoRepository presupuestoRepository;
|
||||||
private final Utils utils;
|
private final Utils utils;
|
||||||
|
private final FacturacionService facturacionService;
|
||||||
|
private final PedidoService pedidoService;
|
||||||
|
|
||||||
private final Map<String, String> empresa = Map.of(
|
private final Map<String, String> empresa = Map.of(
|
||||||
"nombre", "ImprimeLibros ERP",
|
"nombre", "ImprimeLibros ERP",
|
||||||
@ -35,7 +42,6 @@ public class PdfService {
|
|||||||
"poblacion", "Madrid",
|
"poblacion", "Madrid",
|
||||||
"web", "www.imprimelibros.com");
|
"web", "www.imprimelibros.com");
|
||||||
|
|
||||||
|
|
||||||
private static class PrecioTirada {
|
private static class PrecioTirada {
|
||||||
private Double peso;
|
private Double peso;
|
||||||
@JsonProperty("iva_importe_4")
|
@JsonProperty("iva_importe_4")
|
||||||
@ -88,12 +94,15 @@ public class PdfService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public PdfService(TemplateRegistry registry, PdfTemplateEngine engine, PdfRenderer renderer,
|
public PdfService(TemplateRegistry registry, PdfTemplateEngine engine, PdfRenderer renderer,
|
||||||
PresupuestoRepository presupuestoRepository, Utils utils) {
|
PresupuestoRepository presupuestoRepository, Utils utils, FacturacionService facturacionService,
|
||||||
|
PedidoService pedidoService) {
|
||||||
this.registry = registry;
|
this.registry = registry;
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
this.renderer = renderer;
|
this.renderer = renderer;
|
||||||
this.presupuestoRepository = presupuestoRepository;
|
this.presupuestoRepository = presupuestoRepository;
|
||||||
this.utils = utils;
|
this.utils = utils;
|
||||||
|
this.pedidoService = pedidoService;
|
||||||
|
this.facturacionService = facturacionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] generate(DocumentSpec spec) {
|
private byte[] generate(DocumentSpec spec) {
|
||||||
@ -181,4 +190,54 @@ public class PdfService {
|
|||||||
throw new RuntimeException("Error generando presupuesto PDF", e);
|
throw new RuntimeException("Error generando presupuesto PDF", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] generaFactura(Long facturaId, Locale locale) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
Factura factura = facturacionService.getFactura(facturaId);
|
||||||
|
if (factura == null) {
|
||||||
|
throw new IllegalArgumentException("Factura no encontrada: " + facturaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
factura.getLineas().forEach(l -> l.setDescripcion(HtmlToXhtml.toXhtml(l.getDescripcion())));
|
||||||
|
|
||||||
|
PedidoDireccion direccionFacturacion = pedidoService
|
||||||
|
.getPedidoDireccionFacturacionByPedidoId(factura.getPedidoId());
|
||||||
|
if (direccionFacturacion == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Dirección de facturación no encontrada para el pedido: " + factura.getPedidoId());
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> model = new HashMap<>();
|
||||||
|
model.put("factura", factura);
|
||||||
|
model.put("direccionFacturacion", direccionFacturacion);
|
||||||
|
|
||||||
|
var spec = new DocumentSpec(
|
||||||
|
DocumentType.FACTURA,
|
||||||
|
"factura-a4",
|
||||||
|
locale,
|
||||||
|
model);
|
||||||
|
|
||||||
|
byte[] pdf = this.generate(spec);
|
||||||
|
|
||||||
|
// HTML
|
||||||
|
// (Opcional) generar HTML de depuración con CSS incrustado
|
||||||
|
try {
|
||||||
|
String templateName = registry.resolve(DocumentType.FACTURA, "factura-a4");
|
||||||
|
String html = engine.render(templateName, locale, model);
|
||||||
|
String css = Files.readString(Path.of("src/main/resources/static/assets/css/facturapdf.css"));
|
||||||
|
String htmlWithCss = html.replaceFirst("(?i)</head>", "<style>\n" + css + "\n</style>\n</head>");
|
||||||
|
Path htmlPath = Path.of("target/factura-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 factura PDF", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
|
|||||||
import com.imprimelibros.erp.users.UserService;
|
import com.imprimelibros.erp.users.UserService;
|
||||||
import com.imprimelibros.erp.direcciones.DireccionService;
|
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||||
import com.imprimelibros.erp.externalApi.skApiClient;
|
import com.imprimelibros.erp.externalApi.skApiClient;
|
||||||
|
import com.imprimelibros.erp.facturacion.dto.DireccionFacturacionDto;
|
||||||
import com.imprimelibros.erp.pedidos.PedidoLinea.Estado;
|
import com.imprimelibros.erp.pedidos.PedidoLinea.Estado;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@ -59,11 +60,10 @@ public class PedidoService {
|
|||||||
this.messageSource = messageSource;
|
this.messageSource = messageSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Pedido getPedidoById(Long pedidoId) {
|
public Pedido getPedidoById(Long pedidoId) {
|
||||||
return pedidoRepository.findById(pedidoId).orElse(null);
|
return pedidoRepository.findById(pedidoId).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PedidoDireccion getPedidoDireccionFacturacionByPedidoId(Long pedidoId) {
|
public PedidoDireccion getPedidoDireccionFacturacionByPedidoId(Long pedidoId) {
|
||||||
return pedidoDireccionRepository.findByPedidoIdAndFacturacionTrue(pedidoId);
|
return pedidoDireccionRepository.findByPedidoIdAndFacturacionTrue(pedidoId);
|
||||||
}
|
}
|
||||||
@ -95,10 +95,11 @@ public class PedidoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auditoría mínima
|
// Auditoría mínima
|
||||||
/*Long userId = cart.getUserId();
|
/*
|
||||||
pedido.setCreatedBy(userService.findById(userId));
|
* Long userId = cart.getUserId();
|
||||||
pedido.setUpdatedBy(userService.findById(userId));
|
* pedido.setCreatedBy(userService.findById(userId));
|
||||||
*/
|
* pedido.setUpdatedBy(userService.findById(userId));
|
||||||
|
*/
|
||||||
// Se obtiene el usuario del primer presupuesto del carrito
|
// Se obtiene el usuario del primer presupuesto del carrito
|
||||||
Long userId = null;
|
Long userId = null;
|
||||||
List<CartItem> cartItems = cart.getItems();
|
List<CartItem> cartItems = cart.getItems();
|
||||||
@ -108,7 +109,7 @@ public class PedidoService {
|
|||||||
userId = firstPresupuesto.getUser().getId();
|
userId = firstPresupuesto.getUser().getId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(userId == null){
|
if (userId == null) {
|
||||||
userId = cart.getUserId();
|
userId = cart.getUserId();
|
||||||
}
|
}
|
||||||
pedido.setCreatedBy(userService.findById(userId));
|
pedido.setCreatedBy(userService.findById(userId));
|
||||||
@ -116,7 +117,6 @@ public class PedidoService {
|
|||||||
pedido.setCreatedAt(Instant.now());
|
pedido.setCreatedAt(Instant.now());
|
||||||
pedido.setDeleted(false);
|
pedido.setDeleted(false);
|
||||||
pedido.setUpdatedAt(Instant.now());
|
pedido.setUpdatedAt(Instant.now());
|
||||||
|
|
||||||
|
|
||||||
// Guardamos el pedido
|
// Guardamos el pedido
|
||||||
Pedido pedidoGuardado = pedidoRepository.save(pedido);
|
Pedido pedidoGuardado = pedidoRepository.save(pedido);
|
||||||
@ -186,6 +186,36 @@ public class PedidoService {
|
|||||||
return pedidoRepository.findById(pedidoId).orElse(null);
|
return pedidoRepository.findById(pedidoId).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Boolean upsertDireccionFacturacion(Long pedidoId, DireccionFacturacionDto direccionData) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
||||||
|
if (pedido == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PedidoDireccion direccionPedido = pedidoDireccionRepository.findByPedidoIdAndFacturacionTrue(pedidoId);
|
||||||
|
|
||||||
|
if (direccionPedido == null) {
|
||||||
|
// crear
|
||||||
|
direccionPedido = direccionData.toPedidoDireccion();
|
||||||
|
direccionPedido.setPedido(pedido);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// actualizar en la existente (NO crees una nueva, para conservar ID)
|
||||||
|
direccionData.applyTo(direccionPedido); // si implementas applyTo()
|
||||||
|
direccionPedido.setFacturacion(true); // por si acaso
|
||||||
|
}
|
||||||
|
|
||||||
|
pedidoDireccionRepository.save(direccionPedido);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Lista de los items del pedido preparados para la vista */
|
/** Lista de los items del pedido preparados para la vista */
|
||||||
@Transactional
|
@Transactional
|
||||||
public List<Map<String, Object>> getLineas(Long pedidoId, Locale locale) {
|
public List<Map<String, Object>> getLineas(Long pedidoId, Locale locale) {
|
||||||
@ -334,7 +364,6 @@ public class PedidoService {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public byte[] getFerroFileContent(Long pedidoLineaId, Locale locale) {
|
public byte[] getFerroFileContent(Long pedidoLineaId, Locale locale) {
|
||||||
return downloadFile(pedidoLineaId, "ferro", locale);
|
return downloadFile(pedidoLineaId, "ferro", locale);
|
||||||
}
|
}
|
||||||
@ -365,7 +394,6 @@ public class PedidoService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Boolean cancelarPedido(Long pedidoId) {
|
public Boolean cancelarPedido(Long pedidoId) {
|
||||||
|
|
||||||
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
||||||
@ -387,8 +415,6 @@ public class PedidoService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************
|
/***************************
|
||||||
* MÉTODOS PRIVADOS
|
* MÉTODOS PRIVADOS
|
||||||
***************************/
|
***************************/
|
||||||
|
|||||||
@ -38,7 +38,12 @@ facturas.lineas.base=Base Imponible
|
|||||||
facturas.lineas.iva_4=I.V.A. 4%
|
facturas.lineas.iva_4=I.V.A. 4%
|
||||||
facturas.lineas.iva_21=I.V.A. 21%
|
facturas.lineas.iva_21=I.V.A. 21%
|
||||||
facturas.lineas.total=Total
|
facturas.lineas.total=Total
|
||||||
|
facturas.lineas.titulo=Líneas de la Factura
|
||||||
|
facturas.lineas.iva_4.help=Introduce el importe del I.V.A. (no el %).
|
||||||
|
facturas.lineas.iva_21.help=Introduce el importe del I.V.A. (no el %).
|
||||||
|
facturas.lineas.delete.title=¿Eliminar línea de factura?
|
||||||
|
facturas.lineas.delete.text=Esta acción no se puede deshacer.
|
||||||
|
facturas.lineas.error.base=La base imponible no es válida.
|
||||||
|
|
||||||
facturas.direccion.titulo=Dirección de Facturación
|
facturas.direccion.titulo=Dirección de Facturación
|
||||||
facturas.direccion.razon-social=Razón Social
|
facturas.direccion.razon-social=Razón Social
|
||||||
@ -51,6 +56,29 @@ facturas.direccion.pais=País
|
|||||||
facturas.direccion.telefono=Teléfono
|
facturas.direccion.telefono=Teléfono
|
||||||
|
|
||||||
|
|
||||||
|
facturas.pagos.titulo=Pago de factura
|
||||||
|
facturas.pagos.acciones=Acciones
|
||||||
|
facturas.pagos.acciones.agregar=Agregar pago
|
||||||
|
facturas.pagos.acciones.editar=Editar
|
||||||
|
facturas.pagos.acciones.eliminar=Eliminar
|
||||||
|
facturas.pagos.metodo=Método de pago
|
||||||
|
facturas.pagos.notas=Notas
|
||||||
|
facturas.pagos.cantidad=Cantidad pagada
|
||||||
|
facturas.pagos.fecha=Fecha de pago
|
||||||
|
facturas.pagos.tipo=Tipo de pago
|
||||||
|
facturas.pagos.tipo.tpv_tarjeta=TPV/Tarjeta
|
||||||
|
facturas.pagos.tipo.tpv_bizum=TPV/Bizum
|
||||||
|
facturas.pagos.tipo.transferencia=Transferencia
|
||||||
|
facturas.pagos.tipo.otros=Otros
|
||||||
|
facturas.pagos.total_pagado=Total pagado
|
||||||
|
|
||||||
|
|
||||||
|
facturas.pagos.delete.title=Eliminar pago
|
||||||
|
facturas.pagos.delete.text=Esta acción no se puede deshacer.
|
||||||
|
facturas.pagos.error.cantidad=La cantidad no es válida.
|
||||||
|
facturas.pagos.error.fecha=La fecha no es válida.
|
||||||
|
|
||||||
|
|
||||||
facturas.delete.title=¿Estás seguro de que deseas eliminar esta factura?
|
facturas.delete.title=¿Estás seguro de que deseas eliminar esta factura?
|
||||||
facturas.delete.text=Esta acción no se puede deshacer.
|
facturas.delete.text=Esta acción no se puede deshacer.
|
||||||
facturas.delete.ok.title=Factura eliminada
|
facturas.delete.ok.title=Factura eliminada
|
||||||
|
|||||||
@ -4,6 +4,8 @@ pdf.company.postalcode=28028
|
|||||||
pdf.company.city=Madrid
|
pdf.company.city=Madrid
|
||||||
pdf.company.phone=+34 910052574
|
pdf.company.phone=+34 910052574
|
||||||
|
|
||||||
|
pdf.page=Página
|
||||||
|
|
||||||
pdf.presupuesto=PRESUPUESTO
|
pdf.presupuesto=PRESUPUESTO
|
||||||
pdf.factura=FACTURA
|
pdf.factura=FACTURA
|
||||||
pdf.pedido=PEDIDO
|
pdf.pedido=PEDIDO
|
||||||
@ -29,6 +31,26 @@ pdf.datos-marcapaginas=Datos de marcapáginas:
|
|||||||
pdf.incluye-envio=El presupuesto incluye el envío a una dirección de la península.
|
pdf.incluye-envio=El presupuesto incluye el envío a una dirección de la península.
|
||||||
pdf.presupuesto-validez=Validez del presupuesto: 30 días desde la fecha de emisión.
|
pdf.presupuesto-validez=Validez del presupuesto: 30 días desde la fecha de emisión.
|
||||||
|
|
||||||
|
# Factura
|
||||||
|
pdf.factura.number=FACTURA Nº:
|
||||||
|
pdf.factura.razon-social=RAZÓN SOCIAL:
|
||||||
|
pdf.factura.identificacion-fiscal=IDENTIFICACIÓN FISCAL:
|
||||||
|
pdf.factura.direccion=DIRECCIÓN:
|
||||||
|
pdf.factura.codigo-postal=CÓDIGO POSTAL:
|
||||||
|
pdf.factura.ciudad=CIUDAD:
|
||||||
|
pdf.factura.provincia=PROVINCIA:
|
||||||
|
pdf.factura.pais=PAÍS:
|
||||||
|
|
||||||
|
pdf.factura.lineas.descripcion=DESCRIPCIÓN
|
||||||
|
pdf.factura.lineas.base=BASE IMPONIBLE
|
||||||
|
pdf.factura.lineas.iva_4=IVA 4%
|
||||||
|
pdf.factura.lineas.iva_21=IVA 21%
|
||||||
|
pdf.factura.lineas.total=TOTAL
|
||||||
|
pdf.factura.total-base=TOTAL BASE IMPONIBLE
|
||||||
|
pdf.factura.total-iva_4=TOTAL IVA 4%
|
||||||
|
pdf.factura.total-iva_21=TOTAL IVA 21%
|
||||||
|
pdf.factura.total-general=TOTAL GENERAL
|
||||||
|
|
||||||
pdf.politica-privacidad=Política de privacidad
|
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.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
|
pdf.politica-privacidad.correo-direccion=Correo electrónico: info@imprimelibros.com - Dirección postal: Calle José Picón, Nº 28 Local A, 28028, Madrid
|
||||||
|
|||||||
@ -11998,19 +11998,19 @@ div.dtr-modal div.dtr-modal-close:hover {
|
|||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
}
|
}
|
||||||
.flatpickr-calendar.arrowTop::before {
|
.flatpickr-calendar.arrowTop::before {
|
||||||
border-bottom-color: #687cfe;
|
border-bottom-color: #92b2a7;
|
||||||
}
|
}
|
||||||
.flatpickr-calendar.arrowTop::after {
|
.flatpickr-calendar.arrowTop::after {
|
||||||
border-bottom-color: #687cfe;
|
border-bottom-color: #92b2a7;
|
||||||
}
|
}
|
||||||
.flatpickr-calendar.arrowBottom::before, .flatpickr-calendar.arrowBottom::after {
|
.flatpickr-calendar.arrowBottom::before, .flatpickr-calendar.arrowBottom::after {
|
||||||
top: 100%;
|
top: 100%;
|
||||||
}
|
}
|
||||||
.flatpickr-calendar.arrowBottom::before {
|
.flatpickr-calendar.arrowBottom::before {
|
||||||
border-top-color: #687cfe;
|
border-top-color: #92b2a7;
|
||||||
}
|
}
|
||||||
.flatpickr-calendar.arrowBottom::after {
|
.flatpickr-calendar.arrowBottom::after {
|
||||||
border-top-color: #687cfe;
|
border-top-color: #92b2a7;
|
||||||
}
|
}
|
||||||
.flatpickr-calendar:focus {
|
.flatpickr-calendar:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
@ -12025,7 +12025,7 @@ div.dtr-modal div.dtr-modal-close:hover {
|
|||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -ms-flexbox;
|
display: -ms-flexbox;
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: #687cfe;
|
background-color: #92b2a7;
|
||||||
border-radius: 5px 5px 0px 0px;
|
border-radius: 5px 5px 0px 0px;
|
||||||
}
|
}
|
||||||
.flatpickr-months .flatpickr-month {
|
.flatpickr-months .flatpickr-month {
|
||||||
@ -12297,7 +12297,7 @@ div.dtr-modal div.dtr-modal-close:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.flatpickr-weekdays {
|
.flatpickr-weekdays {
|
||||||
background-color: #687cfe;
|
background-color: #92b2a7;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -12322,7 +12322,7 @@ div.dtr-modal div.dtr-modal-close:hover {
|
|||||||
span.flatpickr-weekday {
|
span.flatpickr-weekday {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
background: #687cfe;
|
background: #92b2a7;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -12424,11 +12424,11 @@ span.flatpickr-weekday {
|
|||||||
color: var(--vz-dark);
|
color: var(--vz-dark);
|
||||||
}
|
}
|
||||||
.flatpickr-day.selected, .flatpickr-day.startRange, .flatpickr-day.endRange, .flatpickr-day.selected.inRange, .flatpickr-day.startRange.inRange, .flatpickr-day.endRange.inRange, .flatpickr-day.selected:focus, .flatpickr-day.startRange:focus, .flatpickr-day.endRange:focus, .flatpickr-day.selected:hover, .flatpickr-day.startRange:hover, .flatpickr-day.endRange:hover, .flatpickr-day.selected.prevMonthDay, .flatpickr-day.startRange.prevMonthDay, .flatpickr-day.endRange.prevMonthDay, .flatpickr-day.selected.nextMonthDay, .flatpickr-day.startRange.nextMonthDay, .flatpickr-day.endRange.nextMonthDay {
|
.flatpickr-day.selected, .flatpickr-day.startRange, .flatpickr-day.endRange, .flatpickr-day.selected.inRange, .flatpickr-day.startRange.inRange, .flatpickr-day.endRange.inRange, .flatpickr-day.selected:focus, .flatpickr-day.startRange:focus, .flatpickr-day.endRange:focus, .flatpickr-day.selected:hover, .flatpickr-day.startRange:hover, .flatpickr-day.endRange:hover, .flatpickr-day.selected.prevMonthDay, .flatpickr-day.startRange.prevMonthDay, .flatpickr-day.endRange.prevMonthDay, .flatpickr-day.selected.nextMonthDay, .flatpickr-day.startRange.nextMonthDay, .flatpickr-day.endRange.nextMonthDay {
|
||||||
background: #687cfe;
|
background: #92b2a7;
|
||||||
-webkit-box-shadow: none;
|
-webkit-box-shadow: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: #687cfe;
|
border-color: #92b2a7;
|
||||||
}
|
}
|
||||||
.flatpickr-day.selected.startRange, .flatpickr-day.startRange.startRange, .flatpickr-day.endRange.startRange {
|
.flatpickr-day.selected.startRange, .flatpickr-day.startRange.startRange, .flatpickr-day.endRange.startRange {
|
||||||
border-radius: 50px 0 0 50px;
|
border-radius: 50px 0 0 50px;
|
||||||
|
|||||||
426
src/main/resources/static/assets/css/facturapdf.css
Normal file
426
src/main/resources/static/assets/css/facturapdf.css
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
:root {
|
||||||
|
--verde: #92b2a7;
|
||||||
|
--letterspace: 8px;
|
||||||
|
/* ← puedes ajustar este valor en el root */
|
||||||
|
-ink: #1b1e28;
|
||||||
|
--muted: #5b6472;
|
||||||
|
--accent: #0ea5e9;
|
||||||
|
/* azul tira a cyan */
|
||||||
|
--line: #e6e8ef;
|
||||||
|
--bg-tag: #f4f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Open Sans (rutas relativas desde css → fonts) */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Open Sans";
|
||||||
|
src: url("../fonts/OpenSans-Regular.ttf") format("truetype");
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Open Sans";
|
||||||
|
src: url("../fonts/OpenSans-SemiBold.ttf") format("truetype");
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Open Sans";
|
||||||
|
src: url("../fonts/OpenSans-Bold.ttf") format("truetype");
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
|
||||||
|
/* Estos márgenes sustituyen a tu padding grande en .page-content */
|
||||||
|
margin: 15mm 14mm 50mm 14mm; /* bottom grande para el footer */
|
||||||
|
|
||||||
|
@bottom-center {
|
||||||
|
content: element(pdfFooter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
font-family: "Open Sans" !important;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 11pt;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
body.has-watermark {
|
||||||
|
background-image: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ====== HEADER (tabla) ====== */
|
||||||
|
.il-header {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 0 0 8mm 0;
|
||||||
|
/* ↓ espacio bajo el header */
|
||||||
|
}
|
||||||
|
|
||||||
|
.il-left,
|
||||||
|
.il-right {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.il-left {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.il-right {
|
||||||
|
width: 50%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.il-logo {
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ← tamaño logo */
|
||||||
|
|
||||||
|
/* Caja superior derecha con esquinas */
|
||||||
|
.il-company-box {
|
||||||
|
display: inline-block;
|
||||||
|
align-items: end;
|
||||||
|
/* para alinear a la derecha sin ocupar todo */
|
||||||
|
position: relative;
|
||||||
|
padding: 4mm 4mm;
|
||||||
|
/* ← espacio texto ↔ esquinas */
|
||||||
|
color: #000;
|
||||||
|
font-size: 10.5pt;
|
||||||
|
/* ← tamaño de letra */
|
||||||
|
line-height: 1;
|
||||||
|
/* ← separación entre líneas */
|
||||||
|
max-width: 75mm;
|
||||||
|
/* ← ancho máximo de la caja */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Esquinas */
|
||||||
|
.il-company-box .corner {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
/* ← anchura esquina */
|
||||||
|
height: 20px;
|
||||||
|
/* ← altura esquina */
|
||||||
|
border-color: #92b2a7;
|
||||||
|
/* ← color esquina */
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner.tl {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border-top: 2px solid #92b2a7;
|
||||||
|
border-left: 2px solid #92b2a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner.tr {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
border-top: 2px solid #92b2a7;
|
||||||
|
border-right: 2px solid #92b2a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner.bl {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
border-bottom: 2px solid #92b2a7;
|
||||||
|
border-left: 2px solid #92b2a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner.br {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
border-bottom: 2px solid #92b2a7;
|
||||||
|
border-right: 2px solid #92b2a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.company-line {
|
||||||
|
margin: 1.5mm 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Nueva banda verde PRESUPUESTO */
|
||||||
|
.doc-banner {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #92b2a7 !important; /* ← tu verde corporativo */
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2mm 0;
|
||||||
|
margin-bottom: 4mm;
|
||||||
|
display: block; /* evita conflictos */
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-text {
|
||||||
|
font-family: "Open Sans", Arial, sans-serif !important;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 20pt;
|
||||||
|
letter-spacing: 8px; /* ← configurable */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* ficha superior */
|
||||||
|
.sheet-info {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 4mm 0 6mm 0;
|
||||||
|
font-size: 10.5pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-info td {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-info .lbl {
|
||||||
|
color: var(--muted);
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*.sheet-info .val {
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/* Línea título libro */
|
||||||
|
.line-title {
|
||||||
|
font-family: "Open Sans", Arial, sans-serif !important;
|
||||||
|
margin: 3mm 0 5mm 0;
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 10.5pt;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #5c5c5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-title .lbl {
|
||||||
|
margin-right: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specs 2 columnas */
|
||||||
|
.specs-wrapper {
|
||||||
|
width: 180mm;
|
||||||
|
margin-left: 15mm; /* ← margen izquierdo real del A4 */
|
||||||
|
margin-right: auto; /* opcional */
|
||||||
|
color: #5c5c5c;
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-with-text {
|
||||||
|
margin-left: 1mm;
|
||||||
|
margin-right: 0;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.specs {
|
||||||
|
display: table;
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
margin-bottom: 6mm;
|
||||||
|
}
|
||||||
|
.specs .col {
|
||||||
|
display: table-cell;
|
||||||
|
width: 50%;
|
||||||
|
padding-right: 6mm;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.specs .col:last-child {
|
||||||
|
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;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 8pt;
|
||||||
|
margin: 2mm 0 1mm 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kv {
|
||||||
|
margin: 1mm 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kv span {
|
||||||
|
color: var(--muted);
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 55%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kv b {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subblock {
|
||||||
|
margin-top: 3mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services li {
|
||||||
|
margin: 1mm 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bloque marcapáginas */
|
||||||
|
.bookmark {
|
||||||
|
margin-top: 4mm;
|
||||||
|
border: 1px dashed var(--line);
|
||||||
|
padding: 3mm;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark .bk-title {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 2mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabla de precios (tiradas) */
|
||||||
|
.prices {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 6mm;
|
||||||
|
font-size: 10.5pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prices thead th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 3px;
|
||||||
|
border-bottom: 2px solid var(--accent);
|
||||||
|
background: #eef8fe;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prices tbody td {
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prices .col-tirada {
|
||||||
|
width: 22%;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
|
||||||
|
|
||||||
|
.footer .address {
|
||||||
|
display: table-cell;
|
||||||
|
width: 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer .privacy {
|
||||||
|
display: table-cell;
|
||||||
|
width: 55%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-title {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1mm;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pv-text {
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Caja a página completa SIN vw/vh y SIN z-index negativo */
|
||||||
|
.watermark {
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0; /* ocupa toda la HOJA */
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0; /* debajo del contenido */
|
||||||
|
}
|
||||||
|
|
||||||
|
.watermark img {
|
||||||
|
position: absolute;
|
||||||
|
top: 245mm; /* baja/sube (70–85%) */
|
||||||
|
left: 155mm; /* desplaza a la derecha si quieres */
|
||||||
|
transform: translate(-50%, -50%) rotate(-15deg);
|
||||||
|
width: 60%; /* tamaño grande, ya no hay recorte por márgenes */
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.items-table {
|
||||||
|
width: 100%;
|
||||||
|
border-color: #92b2a7 ;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
}
|
||||||
|
.items-table thead th {
|
||||||
|
background-color: #f3f6f9;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
.items-table tbody td {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Saca el footer fuente fuera del papel (pero sigue existiendo para capturarlo) */
|
||||||
|
.pdf-footer-source {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: -200mm; /* cualquier valor grande negativo */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* El footer que se captura */
|
||||||
|
#pdf-footer {
|
||||||
|
position: running(pdfFooter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo del footer ya dentro del margin-box */
|
||||||
|
.footer {
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
padding-top: 4mm;
|
||||||
|
padding-bottom: 6mm; /* aire para que no quede pegado abajo */
|
||||||
|
font-size: 7.5pt;
|
||||||
|
color: var(--muted);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Numeración */
|
||||||
|
#pdf-footer .page-number {
|
||||||
|
margin-top: 2mm;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pdf-footer .page-number .pn::before {
|
||||||
|
content: " " counter(page) "/" counter(pages);
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ $(() => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const $container = $('#factura-container');
|
const $container = $('#factura-container');
|
||||||
|
|
||||||
const MIN_LOADER_TIME = 500; // ms (ajusta a gusto)
|
const MIN_LOADER_TIME = 500; // ms (ajusta a gusto)
|
||||||
@ -36,6 +37,13 @@ $(() => {
|
|||||||
return $container.data('factura-id');
|
return $container.data('factura-id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFlatpickrLocale() {
|
||||||
|
const lang = (document.documentElement.lang || 'es').toLowerCase().split('-')[0]; // es-ES -> es
|
||||||
|
const l10ns = window.flatpickr?.l10ns;
|
||||||
|
return (l10ns && l10ns[lang]) ? l10ns[lang] : (l10ns?.default || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function reloadFacturaContainer() {
|
function reloadFacturaContainer() {
|
||||||
const id = getFacturaId();
|
const id = getFacturaId();
|
||||||
if (!id) return $.Deferred().reject('No factura id').promise();
|
if (!id) return $.Deferred().reject('No factura id').promise();
|
||||||
@ -88,6 +96,18 @@ $(() => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$container.on('click', '#btn-imprimir-factura', function () {
|
||||||
|
const id = getFacturaId();
|
||||||
|
const url = `/api/pdf/factura/${id}?mode=download`;
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.target = '_self'; // descarga en la misma pestaña
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
});
|
||||||
|
|
||||||
// Delegación (funciona aunque reemplacemos el contenido interno)
|
// Delegación (funciona aunque reemplacemos el contenido interno)
|
||||||
$container.on('click', '#btn-validar-factura', function () {
|
$container.on('click', '#btn-validar-factura', function () {
|
||||||
const id = getFacturaId();
|
const id = getFacturaId();
|
||||||
@ -114,8 +134,33 @@ $(() => {
|
|||||||
|
|
||||||
|
|
||||||
$container.on('click', '#btn-guardar-factura', function () {
|
$container.on('click', '#btn-guardar-factura', function () {
|
||||||
|
const facturaId = getFacturaId();
|
||||||
|
|
||||||
|
const fechaEmisionStr = $('#facturaFechaEmision').val();
|
||||||
|
const fechaEmision = parseEsDateToIsoLocal(fechaEmisionStr);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
cabecera: {
|
||||||
|
serieId: $('#facturaSerieId').val() || null,
|
||||||
|
clienteId: $('#facturaClienteId').val() || null,
|
||||||
|
fechaEmision: fechaEmision // ISO LocalDateTime (00:00:00)
|
||||||
|
},
|
||||||
|
direccionFacturacion: {
|
||||||
|
razonSocial: $('#dirRazonSocial').val() || '',
|
||||||
|
identificacionFiscal: $('#dirIdentificacionFiscal').val() || '',
|
||||||
|
direccion: $('#dirDireccion').val() || '',
|
||||||
|
cp: $('#dirCp').val() || '',
|
||||||
|
ciudad: $('#dirCiudad').val() || '',
|
||||||
|
provincia: $('#dirProvincia').val() || '',
|
||||||
|
paisKeyword: $('#dirPais').val() || '', // lo que tú guardas como keyword
|
||||||
|
telefono: $('#dirTelefono').val() || ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
postAndReload(`/facturas/${facturaId}/guardar`, payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function destroySelect2($root) {
|
function destroySelect2($root) {
|
||||||
$root.find('.js-select2-factura').each(function () {
|
$root.find('.js-select2-factura').each(function () {
|
||||||
const $el = $(this);
|
const $el = $(this);
|
||||||
@ -163,21 +208,561 @@ $(() => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// MODIFICACIÓN: Modal + Quill para líneas de factura
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
let lineaModalInstance = null;
|
||||||
|
let lineaQuill = null;
|
||||||
|
|
||||||
|
function getLineaModal() {
|
||||||
|
const el = document.getElementById('lineaFacturaModal');
|
||||||
|
if (!el) return null;
|
||||||
|
lineaModalInstance = bootstrap.Modal.getOrCreateInstance(el);
|
||||||
|
return lineaModalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLineaModal() {
|
||||||
|
const m = getLineaModal();
|
||||||
|
if (m) m.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLineaModal() {
|
||||||
|
const m = getLineaModal();
|
||||||
|
if (m) m.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLineaModalError(msg) {
|
||||||
|
const $err = $('#lineaFacturaModalError');
|
||||||
|
if (!msg) {
|
||||||
|
$err.addClass('d-none').text('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$err.removeClass('d-none').text(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quill config (igual que en presupuestos, pero solo para este editor del modal)
|
||||||
|
function buildSnowConfig() {
|
||||||
|
return {
|
||||||
|
theme: 'snow',
|
||||||
|
modules: {
|
||||||
|
toolbar: [
|
||||||
|
[{ 'font': [] }, { 'size': [] }],
|
||||||
|
['bold', 'italic', 'underline', 'strike'],
|
||||||
|
[{ 'color': [] }, { 'background': [] }],
|
||||||
|
[{ 'script': 'super' }, { 'script': 'sub' }],
|
||||||
|
[{ 'header': [false, 1, 2, 3, 4, 5, 6] }, 'blockquote', 'code-block'],
|
||||||
|
[{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'indent': '-1' }, { 'indent': '+1' }],
|
||||||
|
['direction', { 'align': [] }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLineaQuill() {
|
||||||
|
const el = document.getElementById('lineaFacturaDescripcionEditor');
|
||||||
|
if (!el) return null;
|
||||||
|
|
||||||
|
// Evita doble init y evita apuntar a DOM viejo tras reload
|
||||||
|
if (!lineaQuill) {
|
||||||
|
lineaQuill = new Quill(el, buildSnowConfig());
|
||||||
|
}
|
||||||
|
return lineaQuill;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLineaDescripcionHtml(html) {
|
||||||
|
const q = getLineaQuill();
|
||||||
|
if (!q) return;
|
||||||
|
q.clipboard.dangerouslyPasteHTML(html ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLineaDescripcionHtml() {
|
||||||
|
const q = getLineaQuill();
|
||||||
|
if (!q) return '';
|
||||||
|
return q.root.innerHTML ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLineaDescripcion() {
|
||||||
|
const q = getLineaQuill();
|
||||||
|
if (!q) return;
|
||||||
|
q.setText('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formato ES: coma decimal
|
||||||
|
const nfEs = new Intl.NumberFormat('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
|
|
||||||
|
function parseEsNumber(str) {
|
||||||
|
if (str == null) return null;
|
||||||
|
const s = String(str).trim();
|
||||||
|
if (!s) return null;
|
||||||
|
const normalized = s.replace(/\./g, '').replace(',', '.');
|
||||||
|
const n = Number(normalized);
|
||||||
|
return Number.isFinite(n) ? n : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEsNumber(n) {
|
||||||
|
const num = Number(n);
|
||||||
|
if (!Number.isFinite(num)) return '';
|
||||||
|
return nfEs.format(num);
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachEsDecimalHandlers(selector) {
|
||||||
|
// input: permitir dígitos y separador decimal, normalizar '.' a ','
|
||||||
|
$container.on('input', selector, function () {
|
||||||
|
let v = $(this).val() ?? '';
|
||||||
|
v = String(v).replace(/[^\d\.,]/g, '');
|
||||||
|
|
||||||
|
const parts = v.split(/[.,]/);
|
||||||
|
if (parts.length > 1) {
|
||||||
|
v = parts[0] + ',' + parts.slice(1).join('');
|
||||||
|
}
|
||||||
|
$(this).val(v);
|
||||||
|
});
|
||||||
|
|
||||||
|
// blur: formatear a 2 decimales
|
||||||
|
$container.on('blur', selector, function () {
|
||||||
|
const n = parseEsNumber($(this).val());
|
||||||
|
$(this).val(formatEsNumber(n ?? 0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// (handlers una sola vez; usan delegación)
|
||||||
|
attachEsDecimalHandlers('#lineaFacturaBase');
|
||||||
|
attachEsDecimalHandlers('#lineaFacturaIva4');
|
||||||
|
attachEsDecimalHandlers('#lineaFacturaIva21');
|
||||||
|
|
||||||
|
|
||||||
|
function resetLineaModal() {
|
||||||
|
$('#lineaFacturaModalTitle').text('Nueva línea');
|
||||||
|
$('#lineaFacturaId').val('');
|
||||||
|
|
||||||
|
clearLineaDescripcion();
|
||||||
|
|
||||||
|
$('#lineaFacturaBase').val(formatEsNumber(0));
|
||||||
|
$('#lineaFacturaIva4').val(formatEsNumber(0));
|
||||||
|
$('#lineaFacturaIva21').val(formatEsNumber(0));
|
||||||
|
showLineaModalError(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillLineaModalForEdit({ id, descripcionHtml, base, iva4, iva21 }) {
|
||||||
|
$('#lineaFacturaModalTitle').text('Editar línea');
|
||||||
|
$('#lineaFacturaId').val(id ?? '');
|
||||||
|
|
||||||
|
setLineaDescripcionHtml(descripcionHtml ?? '');
|
||||||
|
|
||||||
|
$('#lineaFacturaBase').val(formatEsNumber(Number(base) ?? 0));
|
||||||
|
$('#lineaFacturaIva4').val(formatEsNumber(Number(iva4) ?? 0));
|
||||||
|
$('#lineaFacturaIva21').val(formatEsNumber(Number(iva21) ?? 0));
|
||||||
|
|
||||||
|
|
||||||
|
showLineaModalError(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abrir modal: crear
|
||||||
|
$container.on('click', '#btn-add-linea-factura', function () {
|
||||||
|
getLineaQuill(); // asegura init sobre DOM actual
|
||||||
|
resetLineaModal();
|
||||||
|
showLineaModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Abrir modal: editar
|
||||||
|
$container.on('click', '.btn-edit-linea-factura', function () {
|
||||||
|
getLineaQuill(); // asegura init sobre DOM actual
|
||||||
|
|
||||||
|
const $btn = $(this);
|
||||||
|
const id = $btn.data('linea-id');
|
||||||
|
|
||||||
|
// Leemos HTML guardado en textarea hidden (por seguridad)
|
||||||
|
const descripcionHtml = $(`#linea-desc-${id}`).val() ?? '';
|
||||||
|
|
||||||
|
fillLineaModalForEdit({
|
||||||
|
id,
|
||||||
|
descripcionHtml,
|
||||||
|
base: $btn.data('base'),
|
||||||
|
iva4: $btn.data('iva4'),
|
||||||
|
iva21: $btn.data('iva21')
|
||||||
|
});
|
||||||
|
|
||||||
|
showLineaModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Borrar línea
|
||||||
|
$container.on('click', '.btn-delete-linea-factura', function () {
|
||||||
|
const $btn = $(this);
|
||||||
|
const id = $btn.data('linea-id');
|
||||||
|
const facturaId = getFacturaId();
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: window.languageBundle.get(['facturas.lineas.delete.title']) || 'Eliminar línea',
|
||||||
|
html: window.languageBundle.get(['facturas.lineas.delete.text']) || 'Esta acción no se puede deshacer.',
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-danger w-xs mt-2',
|
||||||
|
cancelButton: 'btn btn-light w-xs mt-2'
|
||||||
|
},
|
||||||
|
confirmButtonText: window.languageBundle.get(['app.eliminar']) || 'Eliminar',
|
||||||
|
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
|
||||||
|
}).then((result) => {
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
postAndReload(`/facturas/${facturaId}/lineas/${id}/delete`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Guardar (todavía sin endpoint; deja el payload preparado)
|
||||||
|
$container.on('click', '#btnGuardarLineaFactura', function () {
|
||||||
|
showLineaModalError(null);
|
||||||
|
|
||||||
|
const idLinea = $('#lineaFacturaId').val() || null;
|
||||||
|
|
||||||
|
const descripcionHtml = getLineaDescripcionHtml();
|
||||||
|
const base = parseEsNumber($('#lineaFacturaBase').val());
|
||||||
|
const iva4 = parseEsNumber($('#lineaFacturaIva4').val()) ?? 0;
|
||||||
|
const iva21 = parseEsNumber($('#lineaFacturaIva21').val()) ?? 0;
|
||||||
|
|
||||||
|
if (base == null) {
|
||||||
|
showLineaModalError(window.languageBundle['facturas.lineas.error.base'] || 'La base no es válida.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vacío real de Quill
|
||||||
|
const descTrim = (descripcionHtml ?? '').trim();
|
||||||
|
const isEmptyQuill = (descTrim === '' || descTrim === '<p><br></p>');
|
||||||
|
const descripcion = isEmptyQuill ? '' : descripcionHtml;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
id: idLinea,
|
||||||
|
descripcion, // HTML
|
||||||
|
base,
|
||||||
|
iva4,
|
||||||
|
iva21
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Aquí conectaremos endpoints:
|
||||||
|
const facturaId = getFacturaId();
|
||||||
|
const url = idLinea
|
||||||
|
? `/facturas/${facturaId}/lineas/${idLinea}`
|
||||||
|
: `/facturas/${facturaId}/lineas`;
|
||||||
|
postAndReload(url, payload).done(() => hideLineaModal());
|
||||||
|
|
||||||
|
hideLineaModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// FIN MODAL
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// MODAL + Quill + Flatpickr para pagos de factura
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
let pagoModalInstance = null;
|
||||||
|
let pagoQuill = null;
|
||||||
|
let pagoFlatpickr = null;
|
||||||
|
|
||||||
|
function getPagoModal() {
|
||||||
|
const el = document.getElementById('pagoFacturaModal');
|
||||||
|
if (!el) return null;
|
||||||
|
pagoModalInstance = bootstrap.Modal.getOrCreateInstance(el);
|
||||||
|
return pagoModalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPagoModal() {
|
||||||
|
const m = getPagoModal();
|
||||||
|
if (m) m.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePagoModal() {
|
||||||
|
const m = getPagoModal();
|
||||||
|
if (m) m.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPagoModalError(msg) {
|
||||||
|
const $err = $('#pagoFacturaModalError');
|
||||||
|
if (!msg) {
|
||||||
|
$err.addClass('d-none').text('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$err.removeClass('d-none').text(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPagoQuill() {
|
||||||
|
const el = document.getElementById('pagoFacturaNotasEditor');
|
||||||
|
if (!el) return null;
|
||||||
|
|
||||||
|
// Evita doble init y evita apuntar a DOM viejo tras reload
|
||||||
|
if (!pagoQuill) {
|
||||||
|
pagoQuill = new Quill(el, buildSnowConfig());
|
||||||
|
}
|
||||||
|
return pagoQuill;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPagoNotasHtml(html) {
|
||||||
|
const q = getPagoQuill();
|
||||||
|
if (!q) return;
|
||||||
|
q.clipboard.dangerouslyPasteHTML(html ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPagoNotasHtml() {
|
||||||
|
const q = getPagoQuill();
|
||||||
|
if (!q) return '';
|
||||||
|
return q.root.innerHTML ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPagoNotas() {
|
||||||
|
const q = getPagoQuill();
|
||||||
|
if (!q) return;
|
||||||
|
q.setText('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPagoFlatpickr() {
|
||||||
|
const input = document.getElementById('pagoFacturaFecha');
|
||||||
|
if (!input) return null;
|
||||||
|
|
||||||
|
if (!pagoFlatpickr) {
|
||||||
|
pagoFlatpickr = flatpickr(input, {
|
||||||
|
enableTime: false,
|
||||||
|
dateFormat: "d/m/Y",
|
||||||
|
placeholder: "",
|
||||||
|
locale: getFlatpickrLocale()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return pagoFlatpickr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convierte "dd/MM/yyyy HH:mm" => "yyyy-MM-ddTHH:mm:00" (LocalDateTime)
|
||||||
|
function parseEsDateTimeToIsoLocal(str) {
|
||||||
|
if (!str) return null;
|
||||||
|
const s = String(str).trim();
|
||||||
|
if (!s) return null;
|
||||||
|
|
||||||
|
const parts = s.split(' ');
|
||||||
|
if (parts.length < 2) return null;
|
||||||
|
|
||||||
|
const [dmy, hm] = parts;
|
||||||
|
const [dd, mm, yyyy] = dmy.split('/').map(n => Number(n));
|
||||||
|
const [HH, MM] = hm.split(':').map(n => Number(n));
|
||||||
|
|
||||||
|
if (!dd || !mm || !yyyy || Number.isNaN(HH) || Number.isNaN(MM)) return null;
|
||||||
|
|
||||||
|
const pad2 = (n) => String(n).padStart(2, '0');
|
||||||
|
return `${String(yyyy).padStart(4, '0')}-${pad2(mm)}-${pad2(dd)}T${pad2(HH)}:${pad2(MM)}:00`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEsDateToIsoLocal(str) {
|
||||||
|
if (!str) return null;
|
||||||
|
const s = String(str).trim();
|
||||||
|
if (!s) return null;
|
||||||
|
|
||||||
|
const [dd, mm, yyyy] = s.split('/').map(n => Number(n));
|
||||||
|
if (!dd || !mm || !yyyy) return null;
|
||||||
|
|
||||||
|
const pad2 = (n) => String(n).padStart(2, '0');
|
||||||
|
return `${String(yyyy).padStart(4, '0')}-${pad2(mm)}-${pad2(dd)}T00:00:00`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function setPagoFechaFromDataAttr(fechaRaw) {
|
||||||
|
const fp = getPagoFlatpickr();
|
||||||
|
if (!fp) return;
|
||||||
|
|
||||||
|
if (!fechaRaw) { fp.clear(); return; }
|
||||||
|
|
||||||
|
// fechaRaw: "yyyy-MM-dd"
|
||||||
|
const [Y, M, D] = String(fechaRaw).split('-').map(Number);
|
||||||
|
if (!Y || !M || !D) return;
|
||||||
|
|
||||||
|
fp.setDate(new Date(Y, M - 1, D), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function resetPagoModal() {
|
||||||
|
$('#pagoFacturaModalTitle').text('Nuevo pago');
|
||||||
|
$('#pagoFacturaId').val('');
|
||||||
|
|
||||||
|
$('#pagoFacturaMetodo').val('tpv_tarjeta');
|
||||||
|
$('#pagoFacturaCantidad').val(formatEsNumber(0));
|
||||||
|
|
||||||
|
const fp = getPagoFlatpickr();
|
||||||
|
if (fp) fp.clear();
|
||||||
|
|
||||||
|
clearPagoNotas();
|
||||||
|
showPagoModalError(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillPagoModalForEdit({ id, metodo, cantidad, fechaRaw, notasHtml }) {
|
||||||
|
$('#pagoFacturaModalTitle').text('Editar pago');
|
||||||
|
$('#pagoFacturaId').val(id ?? '');
|
||||||
|
|
||||||
|
$('#pagoFacturaMetodo').val(metodo ?? 'tpv_tarjeta');
|
||||||
|
$('#pagoFacturaCantidad').val(formatEsNumber(Number(cantidad) ?? 0));
|
||||||
|
|
||||||
|
setPagoFechaFromDataAttr(fechaRaw);
|
||||||
|
setPagoNotasHtml(notasHtml ?? '');
|
||||||
|
|
||||||
|
showPagoModalError(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formato ES para cantidad (mismo handler que líneas)
|
||||||
|
attachEsDecimalHandlers('#pagoFacturaCantidad');
|
||||||
|
|
||||||
|
// Abrir modal: crear
|
||||||
|
$container.on('click', '#btn-add-pago-factura', function () {
|
||||||
|
getPagoQuill(); // init sobre DOM actual
|
||||||
|
getPagoFlatpickr(); // init sobre DOM actual
|
||||||
|
resetPagoModal();
|
||||||
|
showPagoModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Abrir modal: editar
|
||||||
|
$container.on('click', '.btn-edit-pago-factura', function () {
|
||||||
|
getPagoQuill();
|
||||||
|
getPagoFlatpickr();
|
||||||
|
|
||||||
|
const $btn = $(this);
|
||||||
|
const id = $btn.data('pago-id');
|
||||||
|
const notasHtml = $(`#pago-notas-${id}`).val() ?? '';
|
||||||
|
|
||||||
|
fillPagoModalForEdit({
|
||||||
|
id,
|
||||||
|
metodo: $btn.data('metodo'),
|
||||||
|
cantidad: $btn.data('cantidad'),
|
||||||
|
fechaRaw: $btn.data('fecha'), // "yyyy-MM-dd HH:mm" desde Thymeleaf
|
||||||
|
notasHtml
|
||||||
|
});
|
||||||
|
|
||||||
|
showPagoModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Borrar pago (Swal igual que líneas)
|
||||||
|
$container.on('click', '.btn-delete-pago-factura', function () {
|
||||||
|
const pagoId = $(this).data('pago-id');
|
||||||
|
const facturaId = getFacturaId();
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: window.languageBundle.get(['facturas.pagos.delete.title']) || 'Eliminar pago',
|
||||||
|
html: window.languageBundle.get(['facturas.pagos.delete.text']) || 'Esta acción no se puede deshacer.',
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-danger w-xs mt-2',
|
||||||
|
cancelButton: 'btn btn-light w-xs mt-2'
|
||||||
|
},
|
||||||
|
confirmButtonText: window.languageBundle.get(['app.eliminar']) || 'Eliminar',
|
||||||
|
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
|
||||||
|
}).then((result) => {
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
postAndReload(`/facturas/${facturaId}/pagos/${pagoId}/delete`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Guardar pago
|
||||||
|
$container.on('click', '#btnGuardarPagoFactura', function () {
|
||||||
|
showPagoModalError(null);
|
||||||
|
|
||||||
|
const facturaId = getFacturaId();
|
||||||
|
const pagoId = $('#pagoFacturaId').val() || null;
|
||||||
|
|
||||||
|
const metodoPago = $('#pagoFacturaMetodo').val();
|
||||||
|
const cantidad = parseEsNumber($('#pagoFacturaCantidad').val());
|
||||||
|
const fechaStr = $('#pagoFacturaFecha').val();
|
||||||
|
|
||||||
|
if (cantidad == null || cantidad <= 0) {
|
||||||
|
showPagoModalError(window.languageBundle.get(['facturas.pagos.error.cantidad']) || 'La cantidad no es válida.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fechaPago = parseEsDateToIsoLocal($('#pagoFacturaFecha').val());
|
||||||
|
|
||||||
|
if (!fechaPago) {
|
||||||
|
showPagoModalError(window.languageBundle.get(['facturas.pagos.error.fecha']) || 'La fecha no es válida.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notasHtml = getPagoNotasHtml();
|
||||||
|
const notasTrim = (notasHtml ?? '').trim();
|
||||||
|
const isEmptyQuill = (notasTrim === '' || notasTrim === '<p><br></p>');
|
||||||
|
const notas = isEmptyQuill ? '' : notasHtml;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
id: pagoId,
|
||||||
|
metodoPago,
|
||||||
|
cantidadPagada: cantidad,
|
||||||
|
fechaPago,
|
||||||
|
notas
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = pagoId
|
||||||
|
? `/facturas/${facturaId}/pagos/${pagoId}`
|
||||||
|
: `/facturas/${facturaId}/pagos`;
|
||||||
|
|
||||||
|
postAndReload(url, payload).done(() => hidePagoModal());
|
||||||
|
});
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
|
||||||
|
let facturaFechaEmisionFp = null;
|
||||||
|
|
||||||
|
function getFacturaFechaEmisionFlatpickr() {
|
||||||
|
const input = document.getElementById('facturaFechaEmision');
|
||||||
|
if (!input) return null;
|
||||||
|
|
||||||
|
if (!facturaFechaEmisionFp) {
|
||||||
|
facturaFechaEmisionFp = flatpickr(input, {
|
||||||
|
enableTime: false,
|
||||||
|
dateFormat: "d/m/Y",
|
||||||
|
locale: getFlatpickrLocale()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return facturaFechaEmisionFp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// dd/MM/yyyy -> yyyy-MM-ddT00:00:00
|
||||||
|
function parseEsDateToIsoLocal(str) {
|
||||||
|
if (!str) return null;
|
||||||
|
const s = String(str).trim();
|
||||||
|
if (!s) return null;
|
||||||
|
const [dd, mm, yyyy] = s.split('/').map(n => Number(n));
|
||||||
|
if (!dd || !mm || !yyyy) return null;
|
||||||
|
|
||||||
|
const pad2 = (n) => String(n).padStart(2, '0');
|
||||||
|
return `${String(yyyy).padStart(4, '0')}-${pad2(mm)}-${pad2(dd)}T00:00:00`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function afterFacturaRender() {
|
function afterFacturaRender() {
|
||||||
const $root = $('#factura-container');
|
const $root = $('#factura-container');
|
||||||
|
|
||||||
// el data-factura-estado viene del fragmento factura-container
|
|
||||||
const estado = $root.find('[data-factura-estado]').first().data('factura-estado');
|
const estado = $root.find('[data-factura-estado]').first().data('factura-estado');
|
||||||
|
|
||||||
// siempre limpia antes para evitar restos si vienes de borrador->validada
|
|
||||||
destroySelect2($root);
|
destroySelect2($root);
|
||||||
|
|
||||||
if (estado === 'borrador') {
|
if (estado === 'borrador') {
|
||||||
initSelect2ForDraft($root);
|
initSelect2ForDraft($root);
|
||||||
|
|
||||||
|
// ✅ Fecha emisión editable con flatpickr solo en borrador
|
||||||
|
facturaFechaEmisionFp = null; // reset por si cambió el DOM
|
||||||
|
getFacturaFechaEmisionFlatpickr(); // init sobre DOM actual
|
||||||
|
} else {
|
||||||
|
facturaFechaEmisionFp = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resets que ya tenías
|
||||||
|
lineaQuill = null;
|
||||||
|
lineaModalInstance = null;
|
||||||
|
|
||||||
|
// si tienes pagos: resetea también...
|
||||||
|
pagoQuill = null;
|
||||||
|
pagoModalInstance = null;
|
||||||
|
pagoFlatpickr = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
afterFacturaRender();
|
afterFacturaRender();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
const scripts = [
|
const scripts = [
|
||||||
"/assets/libs/toastify-js/src/toastify.js",
|
"/assets/libs/toastify-js/src/toastify.js",
|
||||||
"/assets/libs/choices.js/public/assets/scripts/choices.min.js",
|
"/assets/libs/choices.js/public/assets/scripts/choices.min.js",
|
||||||
"/assets/libs/flatpickr/flatpickr.min.js",
|
|
||||||
"/assets/libs/feather-icons/feather.min.js" // <- AÑADIMOS feather aquí
|
"/assets/libs/feather-icons/feather.min.js" // <- AÑADIMOS feather aquí
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,8 @@
|
|||||||
<link th:href="@{/assets/css/presupuestador.css}" rel="stylesheet"
|
<link th:href="@{/assets/css/presupuestador.css}" rel="stylesheet"
|
||||||
th:unless="${#authorization.expression('isAuthenticated()')}" />
|
th:unless="${#authorization.expression('isAuthenticated()')}" />
|
||||||
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
|
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
|
||||||
|
<link sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')"
|
||||||
|
th:href="@{/assets/libs/quill/quill.snow.css}" rel="stylesheet" type="text/css" />
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
@ -68,6 +70,9 @@
|
|||||||
<script th:src="@{/assets/libs/datatables/buttons.print.min.js}"></script>
|
<script th:src="@{/assets/libs/datatables/buttons.print.min.js}"></script>
|
||||||
<script th:src="@{/assets/libs/datatables/buttons.colVis.min.js}"></script>
|
<script th:src="@{/assets/libs/datatables/buttons.colVis.min.js}"></script>
|
||||||
|
|
||||||
|
<script sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')"
|
||||||
|
th:src="@{/assets/libs/quill/quill.min.js}"></script>
|
||||||
|
|
||||||
<script type="module" th:src="@{/assets/js/pages/imprimelibros/facturas/view.js}"></script>
|
<script type="module" th:src="@{/assets/js/pages/imprimelibros/facturas/view.js}"></script>
|
||||||
|
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|||||||
@ -15,17 +15,17 @@
|
|||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
|
|
||||||
<!-- Número -->
|
<!-- Número (solo lectura siempre, normalmente) -->
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label" th:text="#{facturas.form.numero-factura}">Número</label>
|
<label class="form-label" th:text="#{facturas.form.numero-factura}">Número de factura</label>
|
||||||
<input type="text" class="form-control" th:value="${factura.numeroFactura}"
|
<input id="facturaNumero" type="text" class="form-control" th:value="${factura.numeroFactura}" readonly>
|
||||||
th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Serie -->
|
<!-- Serie -->
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label" th:text="#{facturas.form.serie}">Serie facturación</label>
|
<label class="form-label" th:text="#{facturas.form.serie}">Serie</label>
|
||||||
<select class="form-control js-select2-factura" data-url="/configuracion/series-facturacion/api/get-series" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
<select id="facturaSerieId" class="form-control js-select2-factura"
|
||||||
|
data-url="/configuracion/series-facturacion/api/get-series" th:attr="disabled=${isReadonly}">
|
||||||
<option th:value="${factura.serie != null ? factura.serie.id : ''}"
|
<option th:value="${factura.serie != null ? factura.serie.id : ''}"
|
||||||
th:text="${factura.serie != null ? factura.serie.nombreSerie : ''}" selected>
|
th:text="${factura.serie != null ? factura.serie.nombreSerie : ''}" selected>
|
||||||
</option>
|
</option>
|
||||||
@ -35,19 +35,20 @@
|
|||||||
<!-- Cliente -->
|
<!-- Cliente -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label" th:text="#{facturas.form.cliente}">Cliente</label>
|
<label class="form-label" th:text="#{facturas.form.cliente}">Cliente</label>
|
||||||
<select class="form-control js-select2-factura" data-url="/users/api/get-users" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
<select id="facturaClienteId" class="form-control js-select2-factura" data-url="/users/api/get-users"
|
||||||
|
th:attr="disabled=${isReadonly}">
|
||||||
<option th:value="${factura.cliente != null ? factura.cliente.id : ''}"
|
<option th:value="${factura.cliente != null ? factura.cliente.id : ''}"
|
||||||
th:text="${factura.cliente != null ? factura.cliente.fullName : ''}" selected>
|
th:text="${factura.cliente != null ? factura.cliente.fullName : ''}" selected>
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fecha emisión -->
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label" th:text="#{facturas.form.fecha-emision}">Fecha</label>
|
<label class="form-label" th:text="#{facturas.form.fecha-emision}">Fecha</label>
|
||||||
<input type="text" class="form-control" th:value="${factura.fechaEmision != null
|
|
||||||
? #temporals.format(factura.fechaEmision, 'dd/MM/yyyy')
|
<input id="facturaFechaEmision" type="text" class="form-control"
|
||||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
th:value="${factura.fechaEmision != null ? #temporals.format(factura.fechaEmision, 'dd/MM/yyyy') : ''}"
|
||||||
|
th:attr="readonly=${isReadonly}, data-estado=${factura.estado.name()}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notas -->
|
<!-- Notas -->
|
||||||
@ -67,7 +68,7 @@
|
|||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label" th:text="#{facturas.direccion.razon-social}">Razón Social</label>
|
<label class="form-label" th:text="#{facturas.direccion.razon-social}">Razón Social</label>
|
||||||
<input type="text" class="form-control" th:value="${direccionFacturacion != null
|
<input type="text" id="dirRazonSocial" class="form-control" th:value="${direccionFacturacion != null
|
||||||
? direccionFacturacion.razonSocial
|
? direccionFacturacion.razonSocial
|
||||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
</div>
|
</div>
|
||||||
@ -75,7 +76,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label" th:text="#{facturas.direccion.identificacion-fiscal}">Identificacion
|
<label class="form-label" th:text="#{facturas.direccion.identificacion-fiscal}">Identificacion
|
||||||
Fiscal</label>
|
Fiscal</label>
|
||||||
<input type="text" class="form-control" th:value="${direccionFacturacion != null
|
<input type="text" id="dirIdentificacionFiscal" class="form-control" th:value="${direccionFacturacion != null
|
||||||
? direccionFacturacion.identificacionFiscal
|
? direccionFacturacion.identificacionFiscal
|
||||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
</div>
|
</div>
|
||||||
@ -83,39 +84,39 @@
|
|||||||
|
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<label class="form-label" th:text="#{facturas.direccion.direccion}">Dirección</label>
|
<label class="form-label" th:text="#{facturas.direccion.direccion}">Dirección</label>
|
||||||
<input type="text" class="form-control" th:value="${direccionFacturacion != null
|
<input type="text" id="dirDireccion" class="form-control" th:value="${direccionFacturacion != null
|
||||||
? direccionFacturacion.direccion
|
? direccionFacturacion.direccion
|
||||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label" th:text="#{facturas.direccion.codigo-postal}">Código Postal</label>
|
<label class="form-label" th:text="#{facturas.direccion.codigo-postal}">Código Postal</label>
|
||||||
<input type="text" class="form-control" th:value="${direccionFacturacion != null
|
<input type="text" id="dirCp" class="form-control" th:value="${direccionFacturacion != null
|
||||||
? direccionFacturacion.cp
|
? direccionFacturacion.cp
|
||||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label" th:text="#{facturas.direccion.ciudad}">Ciudad</label>
|
<label class="form-label" th:text="#{facturas.direccion.ciudad}">Ciudad</label>
|
||||||
<input type="text" class="form-control" th:value="${direccionFacturacion != null
|
<input type="text" id="dirCiudad" class="form-control" th:value="${direccionFacturacion != null
|
||||||
? direccionFacturacion.ciudad
|
? direccionFacturacion.ciudad
|
||||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label" th:text="#{facturas.direccion.provincia}">Provincia</label>
|
<label class="form-label" th:text="#{facturas.direccion.provincia}">Provincia</label>
|
||||||
<input type="text" class="form-control" th:value="${direccionFacturacion != null
|
<input type="text" id="dirProvincia" class="form-control" th:value="${direccionFacturacion != null
|
||||||
? direccionFacturacion.provincia
|
? direccionFacturacion.provincia
|
||||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label" th:text="#{facturas.direccion.pais}">País</label>
|
<label class="form-label" th:text="#{facturas.direccion.pais}">País</label>
|
||||||
<select class="form-control js-select2-factura" data-url="/api/paises" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
<select id="dirPais" class="form-control js-select2-factura" data-url="/api/paises"
|
||||||
|
th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
<option th:value="${direccionFacturacion != null
|
<option th:value="${direccionFacturacion != null
|
||||||
? direccionFacturacion.pais.keyword
|
? direccionFacturacion.pais.code3
|
||||||
: ''}"
|
: ''}" th:text="${direccionFacturacion != null
|
||||||
th:text="${direccionFacturacion != null
|
|
||||||
? #messages.msg('paises.' + direccionFacturacion.pais.keyword)
|
? #messages.msg('paises.' + direccionFacturacion.pais.keyword)
|
||||||
: ''}" selected>
|
: ''}" selected>
|
||||||
</option>
|
</option>
|
||||||
@ -124,7 +125,7 @@
|
|||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label" th:text="#{facturas.direccion.telefono}">Teléfono</label>
|
<label class="form-label" th:text="#{facturas.direccion.telefono}">Teléfono</label>
|
||||||
<input type="text" class="form-control" th:value="${direccionFacturacion != null
|
<input type="text" id="dirTelefono" class="form-control" th:value="${direccionFacturacion != null
|
||||||
? direccionFacturacion.telefono
|
? direccionFacturacion.telefono
|
||||||
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
</div>
|
</div>
|
||||||
@ -133,18 +134,18 @@
|
|||||||
<div class="row g-3 mt-4 justify-content-end">
|
<div class="row g-3 mt-4 justify-content-end">
|
||||||
|
|
||||||
<div class="col-md-12 text-end">
|
<div class="col-md-12 text-end">
|
||||||
<th:block th:if="${factura.estado.name() == 'borrador'}">
|
<th:block th:if="${factura.estado.name() == 'borrador'}">
|
||||||
<button type="button" class="btn btn-secondary me-2" id="btn-validar-factura"
|
<button type="button" class="btn btn-secondary me-2" id="btn-validar-factura"
|
||||||
th:text="#{facturas.form.btn.validar}">Validar factura</button>
|
th:text="#{facturas.form.btn.validar}">Validar factura</button>
|
||||||
<button type="button" class="btn btn-secondary me-2" id="btn-guardar-factura"
|
<button type="button" class="btn btn-secondary me-2" id="btn-guardar-factura"
|
||||||
th:text="#{facturas.form.btn.guardar}">Guardar</button>
|
th:text="#{facturas.form.btn.guardar}">Guardar</button>
|
||||||
</th:block>
|
</th:block>
|
||||||
<th:block th:if="${factura.estado.name() == 'validada'}">
|
<th:block th:if="${factura.estado.name() == 'validada'}">
|
||||||
<button type="button" class="btn btn-secondary me-2" id="btn-borrador-factura"
|
<button type="button" class="btn btn-secondary me-2" id="btn-borrador-factura"
|
||||||
th:text="#{facturas.form.btn.borrador}">Pasar a borrador</button>
|
th:text="#{facturas.form.btn.borrador}">Pasar a borrador</button>
|
||||||
</th:block>
|
</th:block>
|
||||||
<button type="button" class="btn btn-secondary me-2" id="btn-imprimir-factura"
|
<button type="button" class="btn btn-secondary me-2" id="btn-imprimir-factura"
|
||||||
th:text="#{facturas.form.btn.imprimir}">Imprimir factura</button>
|
th:text="#{facturas.form.btn.imprimir}">Imprimir factura</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|||||||
@ -47,7 +47,7 @@
|
|||||||
<div id="pagos" class="accordion-collapse collapse show" aria-labelledby="pagosHeader"
|
<div id="pagos" class="accordion-collapse collapse show" aria-labelledby="pagosHeader"
|
||||||
data-bs-parent="#pagosFactura">
|
data-bs-parent="#pagosFactura">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<!-- pagos -->
|
<div th:replace="~{imprimelibros/facturas/partials/factura-pagos :: factura-pagos (factura=${factura})}"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,10 +7,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
<table class="table table-bordered table-striped table-nowrap w-100">
|
<table class="table table-bordered table-striped table-wrap w-100">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th th:if="${factura.estado != null && factura.estado.name() == 'borrador'}" th:text="#{facturas.lineas.acciones}">Acciones</th>
|
<th th:if="${factura.estado != null && factura.estado.name() == 'borrador'}"
|
||||||
|
th:text="#{facturas.lineas.acciones}">Acciones</th>
|
||||||
<th class="w-75" th:text="#{facturas.lineas.descripcion}">Descripción</th>
|
<th class="w-75" th:text="#{facturas.lineas.descripcion}">Descripción</th>
|
||||||
<th th:text="#{facturas.lineas.base}">Base</th>
|
<th th:text="#{facturas.lineas.base}">Base</th>
|
||||||
<th th:text="#{facturas.lineas.iva_4}">I.V.A. 4%</th>
|
<th th:text="#{facturas.lineas.iva_4}">I.V.A. 4%</th>
|
||||||
@ -21,16 +22,26 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="lineaFactura : ${factura.lineas}">
|
<tr th:each="lineaFactura : ${factura.lineas}">
|
||||||
<td th:if="${factura.estado != null && factura.estado.name() == 'borrador'}">
|
<td th:if="${factura.estado != null && factura.estado.name() == 'borrador'}">
|
||||||
<button type="button" class="btn btn-secondary btn-sm me-2"
|
<button type="button" class="btn btn-secondary btn-sm me-2 btn-edit-linea-factura" th:attr="
|
||||||
th:attr="data-linea-id=${lineaFactura.id}" th:text="#{facturas.lineas.acciones.editar}">
|
data-linea-id=${lineaFactura.id},
|
||||||
<i class="fas fa-edit"></i>
|
data-base=${lineaFactura.baseLinea},
|
||||||
|
data-iva4=${lineaFactura.iva4Linea},
|
||||||
|
data-iva21=${lineaFactura.iva21Linea}
|
||||||
|
">
|
||||||
|
<i class="fas fa-edit me-1"></i>
|
||||||
|
<span th:text="#{facturas.lineas.acciones.editar}">Editar</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-danger btn-sm"
|
|
||||||
th:attr="data-linea-id=${lineaFactura.id}" th:text="#{facturas.lineas.acciones.eliminar}">
|
<button type="button" class="btn btn-danger btn-sm btn-delete-linea-factura" th:attr="data-linea-id=${lineaFactura.id}"
|
||||||
|
th:text="#{facturas.lineas.acciones.eliminar}">
|
||||||
<i class="fas fa-trash-alt"></i>
|
<i class="fas fa-trash-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- IMPORTANTE: guardamos el HTML aquí (no en data-*) -->
|
||||||
|
<textarea class="d-none" th:attr="id=${'linea-desc-' + lineaFactura.id}"
|
||||||
|
th:text="${lineaFactura.descripcion}"></textarea>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td th:utext="${lineaFactura.descripcion}">Descripción de la línea</td>
|
<td th:utext="${lineaFactura.descripcion}">Descripción de la línea</td>
|
||||||
<td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.baseLinea)}">0.00</td>
|
<td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.baseLinea)}">0.00</td>
|
||||||
<td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.iva4Linea)}">0.00</td>
|
<td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.iva4Linea)}">0.00</td>
|
||||||
@ -40,20 +51,33 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-end fw-bold" th:attr="colspan=${factura.estado != null && factura.estado.name() == 'borrador' ? 5 : 4}" th:text="#{facturas.lineas.base}">Base</td>
|
<td class="text-end fw-bold"
|
||||||
|
th:attr="colspan=${factura.estado != null && factura.estado.name() == 'borrador' ? 5 : 4}"
|
||||||
|
th:text="#{facturas.lineas.base}">Base</td>
|
||||||
<td class="text-end" colspan="1" th:text="${#numbers.formatCurrency(factura.baseImponible)}">0.00</td>
|
<td class="text-end" colspan="1" th:text="${#numbers.formatCurrency(factura.baseImponible)}">0.00</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-end fw-bold" th:attr="colspan=${factura.estado != null && factura.estado.name() == 'borrador' ? 5 : 4}" th:text="#{facturas.lineas.iva_4}">I.V.A. 4%</td>
|
<td class="text-end fw-bold"
|
||||||
|
th:attr="colspan=${factura.estado != null && factura.estado.name() == 'borrador' ? 5 : 4}"
|
||||||
|
th:text="#{facturas.lineas.iva_4}">I.V.A. 4%</td>
|
||||||
<td class="text-end" colspan="1" th:text="${#numbers.formatCurrency(factura.iva4)}">0.00</td>
|
<td class="text-end" colspan="1" th:text="${#numbers.formatCurrency(factura.iva4)}">0.00</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-end fw-bold" th:attr="colspan=${factura.estado != null && factura.estado.name() == 'borrador' ? 5 : 4}" th:text="#{facturas.lineas.iva_21}">I.V.A. 21%</td>
|
<td class="text-end fw-bold"
|
||||||
|
th:attr="colspan=${factura.estado != null && factura.estado.name() == 'borrador' ? 5 : 4}"
|
||||||
|
th:text="#{facturas.lineas.iva_21}">I.V.A. 21%</td>
|
||||||
<td class="text-end" colspan="1" th:text="${#numbers.formatCurrency(factura.iva21)}">0.00</td>
|
<td class="text-end" colspan="1" th:text="${#numbers.formatCurrency(factura.iva21)}">0.00</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-end fw-bold text-uppercase" th:attr="colspan=${factura.estado != null && factura.estado.name() == 'borrador' ? 5 : 4}" th:text="#{facturas.lineas.total}">Total</td>
|
<td class="text-end fw-bold text-uppercase"
|
||||||
<td class="text-end fw-bold" colspan="1" th:text="${#numbers.formatCurrency(factura.totalFactura)}">0.00</td>
|
th:attr="colspan=${factura.estado != null && factura.estado.name() == 'borrador' ? 5 : 4}"
|
||||||
|
th:text="#{facturas.lineas.total}">Total</td>
|
||||||
|
<td class="text-end fw-bold" colspan="1" th:text="${#numbers.formatCurrency(factura.totalFactura)}">0.00
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<!-- Modal líneas factura (crear/editar) -->
|
||||||
|
<th:block th:replace="~{imprimelibros/facturas/partials/linea-modal :: linea-modal}"></th:block>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
<!-- imprimelibros/facturas/partials/factura-pagos.html -->
|
||||||
|
<div th:fragment="factura-pagos (factura)">
|
||||||
|
<div class="mb-3">
|
||||||
|
<button type="button" class="btn btn-secondary" id="btn-add-pago-factura">
|
||||||
|
<i class="fas fa-plus-circle me-2"></i>
|
||||||
|
<span th:text="#{facturas.pagos.acciones.agregar}">Agregar pago</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-bordered table-striped table-wrap w-100">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th th:text="#{facturas.pagos.acciones}">Acciones</th>
|
||||||
|
<th th:text="#{facturas.pagos.metodo}">Método</th>
|
||||||
|
<th th:text="#{facturas.pagos.fecha}">Fecha</th>
|
||||||
|
<th class="text-center w-50" th:text="#{facturas.pagos.notas}">Notas</th>
|
||||||
|
<th class="text-end" th:text="#{facturas.pagos.cantidad}">Cantidad</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="pago : ${factura.pagos}" th:if="${pago.deletedAt == null}">
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm me-2 btn-edit-pago-factura" th:attr="
|
||||||
|
data-pago-id=${pago.id},
|
||||||
|
data-metodo=${pago.metodoPago},
|
||||||
|
data-cantidad=${pago.cantidadPagada},
|
||||||
|
data-fecha=${#temporals.format(pago.fechaPago,'yyyy-MM-dd')}">
|
||||||
|
<i class="fas fa-edit me-1"></i>
|
||||||
|
<span th:text="#{facturas.pagos.acciones.editar}">Editar</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-danger btn-sm btn-delete-pago-factura"
|
||||||
|
th:attr="data-pago-id=${pago.id}">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
<span th:text="#{facturas.pagos.acciones.eliminar}">Eliminar</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- notas en HTML (igual que líneas: guardadas en textarea oculto) -->
|
||||||
|
<textarea class="d-none" th:attr="id=${'pago-notas-' + pago.id}" th:text="${pago.notas}"></textarea>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td th:text="${#messages.msg('facturas.pagos.tipo.' + pago.metodoPago.name().toLowerCase())}">
|
||||||
|
TPV/Tarjeta
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Formato visual dd/MM/yyyy -->
|
||||||
|
<td th:text="${#temporals.format(pago.fechaPago,'dd/MM/yyyy')}">01/01/2026 10:00</td>
|
||||||
|
|
||||||
|
<td class="text-muted">
|
||||||
|
<span th:if="${pago.notas == null || #strings.isEmpty(pago.notas)}">—</span>
|
||||||
|
<span th:if="${pago.notas != null && !#strings.isEmpty(pago.notas)}"
|
||||||
|
th:utext="${pago.notas}"></span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-end" th:text="${#numbers.formatCurrency(pago.cantidadPagada)}">0,00 €</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th colspan="4" class="text-end" th:text="#{facturas.pagos.total_pagado}">Total pagado</th>
|
||||||
|
<th class="text-end" th:text="${#numbers.formatCurrency(factura.totalPagado)}">0,00 €</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Modal pagos (crear/editar) -->
|
||||||
|
<th:block th:replace="~{imprimelibros/facturas/partials/pago-modal :: pago-modal}"></th:block>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
<!-- imprimelibros/facturas/partials/linea-modal.html -->
|
||||||
|
<div th:fragment="linea-modal">
|
||||||
|
<div class="modal fade" id="lineaFacturaModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="lineaFacturaModalTitle" th:text="#{facturas.lineas.titulo}">Línea de factura</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- hidden: id de la línea (vacío = nueva) -->
|
||||||
|
<input type="hidden" id="lineaFacturaId" value=""/>
|
||||||
|
|
||||||
|
<!-- Descripción con Quill -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" th:text="#{facturas.lineas.descripcion}">Descripción</label>
|
||||||
|
|
||||||
|
<!-- Quill Snow Editor -->
|
||||||
|
<div id="lineaFacturaDescripcionEditor"
|
||||||
|
class="snow-editor" style="min-height: 200px;"
|
||||||
|
data-contenido=""></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label for="lineaFacturaBase" class="form-label"
|
||||||
|
th:text="#{facturas.lineas.base}">Base</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control text-end"
|
||||||
|
id="lineaFacturaBase"
|
||||||
|
inputmode="decimal"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="0,00">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label for="lineaFacturaIva4" class="form-label"
|
||||||
|
th:text="#{facturas.lineas.iva_4}">I.V.A. 4%</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control text-end"
|
||||||
|
id="lineaFacturaIva4"
|
||||||
|
inputmode="decimal"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="0,00">
|
||||||
|
<div class="form-text" th:text="#{facturas.lineas.iva_4.help}">Introduce el importe del I.V.A. (no el %).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label for="lineaFacturaIva21" class="form-label"
|
||||||
|
th:text="#{facturas.lineas.iva_21}">I.V.A. 21%</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control text-end"
|
||||||
|
id="lineaFacturaIva21"
|
||||||
|
inputmode="decimal"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="0,00">
|
||||||
|
<div class="form-text" th:text="#{facturas.lineas.iva_21.help}">Introduce el importe del I.V.A. (no el %).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- zona errores -->
|
||||||
|
<div class="alert alert-danger d-none mt-3" id="lineaFacturaModalError"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" th:text="#{app.cancelar}">Cancelar</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="btnGuardarLineaFactura" th:text="#{app.guardar}">
|
||||||
|
Guardar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
<!-- imprimelibros/facturas/partials/pago-modal.html -->
|
||||||
|
<div th:fragment="pago-modal">
|
||||||
|
<div class="modal fade" id="pagoFacturaModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="pagoFacturaModalTitle" th:text="#{facturas.pagos.titulo}">
|
||||||
|
Pago de factura
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="pagoFacturaId" value="" />
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label for="pagoFacturaMetodo" class="form-label"
|
||||||
|
th:text="#{facturas.pagos.tipo}">Tipo de pago</label>
|
||||||
|
<select class="form-select" id="pagoFacturaMetodo">
|
||||||
|
<option value="tpv_tarjeta" th:text="#{facturas.pagos.tipo.tpv_tarjeta}">TPV/Tarjeta
|
||||||
|
</option>
|
||||||
|
<option value="tpv_bizum" th:text="#{facturas.pagos.tipo.tpv_bizum}">TPV/Bizum</option>
|
||||||
|
<option value="transferencia" th:text="#{facturas.pagos.tipo.transferencia}">
|
||||||
|
Transferencia</option>
|
||||||
|
<option value="otros" th:text="#{facturas.pagos.tipo.otros}">Otros</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label for="pagoFacturaCantidad" class="form-label"
|
||||||
|
th:text="#{facturas.pagos.cantidad}">Cantidad</label>
|
||||||
|
<input type="text" class="form-control text-end" id="pagoFacturaCantidad"
|
||||||
|
inputmode="decimal" autocomplete="off" placeholder="0,00">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<label for="pagoFacturaFecha" class="form-label" th:text="#{facturas.pagos.fecha}">Fecha de
|
||||||
|
pago</label>
|
||||||
|
<input type="text" class="form-control" id="pagoFacturaFecha" autocomplete="off"
|
||||||
|
placeholder="dd/mm/aaaa hh:mm">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<label class="form-label" th:text="#{facturas.pagos.notas}">Notas</label>
|
||||||
|
<div id="pagoFacturaNotasEditor" class="snow-editor" style="min-height: 180px;"
|
||||||
|
data-contenido=""></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-danger d-none mt-3" id="pagoFacturaModalError"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal"
|
||||||
|
th:text="#{app.cancelar}">Cancelar</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="btnGuardarPagoFactura" th:text="#{app.guardar}">
|
||||||
|
Guardar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -14,6 +14,7 @@
|
|||||||
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
|
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
|
||||||
<link href="/assets/libs/sweetalert2/sweetalert2.min.css" rel="stylesheet" type="text/css" />
|
<link href="/assets/libs/sweetalert2/sweetalert2.min.css" rel="stylesheet" type="text/css" />
|
||||||
<link href="/assets/libs/select2/select2.min.css" rel="stylesheet" />
|
<link href="/assets/libs/select2/select2.min.css" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" th:href="@{/assets/libs/flatpickr/flatpickr.min.css}">
|
||||||
<th:block layout:fragment="pagecss" />
|
<th:block layout:fragment="pagecss" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -38,6 +39,13 @@
|
|||||||
<script src="/assets/libs/jquery/jquery-3.6.0.min.js"></script>
|
<script src="/assets/libs/jquery/jquery-3.6.0.min.js"></script>
|
||||||
<script src="/assets/libs/sweetalert2/sweetalert2.min.js"></script>
|
<script src="/assets/libs/sweetalert2/sweetalert2.min.js"></script>
|
||||||
<script src="/assets/libs/select2/select2.min.js"></script>
|
<script src="/assets/libs/select2/select2.min.js"></script>
|
||||||
|
<script defer th:src="@{/assets/libs/flatpickr/flatpickr.min.js}"></script>
|
||||||
|
<th:block th:with="fpLang=${#locale.language}">
|
||||||
|
<script defer th:src="@{'/assets/libs/flatpickr/l10n/' + ${fpLang} + '.js'}"
|
||||||
|
onerror="console.error('No se pudo cargar flatpickr locale:', this.src)">
|
||||||
|
</script>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
<th:block layout:fragment="pagejs" />
|
<th:block layout:fragment="pagejs" />
|
||||||
<script th:src="@{/assets/js/app.js}"></script>
|
<script th:src="@{/assets/js/app.js}"></script>
|
||||||
<script th:src="@{/assets/js/pages/imprimelibros/languageBundle.js}"></script>
|
<script th:src="@{/assets/js/pages/imprimelibros/languageBundle.js}"></script>
|
||||||
|
|||||||
165
src/main/resources/templates/imprimelibros/pdf/factura-a4.html
Normal file
165
src/main/resources/templates/imprimelibros/pdf/factura-a4.html
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org" lang="es">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title th:text="'Factura ' + ${factura.numeroFactura}">Factura</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/bootstrap-for-pdf.css" />
|
||||||
|
<link rel="stylesheet" href="assets/css/facturapdf.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="has-watermark">
|
||||||
|
|
||||||
|
<div class="watermark">
|
||||||
|
<img src="assets/images/logo-watermark.png" alt="Marca de agua" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PIE -->
|
||||||
|
<div class="pdf-footer-source">
|
||||||
|
<div class="footer" id="pdf-footer">
|
||||||
|
|
||||||
|
<div class="privacy">
|
||||||
|
<div class="pv-title" th:text="#{pdf.politica-privacidad}">Política de privacidad</div>
|
||||||
|
<div class="pv-text" th:text="#{pdf.politica-privacidad.responsable}">Responsable: Impresión Imprime Libros -
|
||||||
|
CIF:
|
||||||
|
B04998886 - Teléfono de contacto: 910052574</div>
|
||||||
|
<div class="pv-text" th:text="#{pdf.politica-privacidad.correo-direccion}">Correo electrónico:
|
||||||
|
info@imprimelibros.com - Dirección postal: Calle José Picón, Nº 28 Local A, 28028, Madrid</div>
|
||||||
|
<div class="pv-text" th:text="#{pdf.politica-privacidad.aviso}">
|
||||||
|
Le comunicamos que los datos que usted nos facilite quedarán incorporados
|
||||||
|
en nuestro registro interno de actividades de tratamiento con el fin de
|
||||||
|
llevar a cabo una adecuada gestión fiscal y contable.
|
||||||
|
Los datos proporcionados se conservarán mientras se mantenga la relación
|
||||||
|
comercial o durante los años necesarios para cumplir con las obligaciones legales.
|
||||||
|
Así mismo, los datos no serán cedidos a terceros salvo en aquellos casos en que exista
|
||||||
|
una obligación legal. Tiene derecho a acceder a sus datos personales, rectificar
|
||||||
|
los datos inexactos, solicitar su supresión, limitar alguno de los tratamientos
|
||||||
|
u oponerse a algún uso vía e-mail, personalmente o mediante correo postal.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page-number">
|
||||||
|
<span th:text="#{pdf.page} ?: 'Página'">Página</span>
|
||||||
|
<span class="pn"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page-content">
|
||||||
|
<!-- HEADER: logo izq + caja empresa dcha -->
|
||||||
|
|
||||||
|
<!-- HEADER: logo izq + caja empresa dcha (tabla, sin flex) -->
|
||||||
|
<table class="il-header">
|
||||||
|
<tr>
|
||||||
|
<td class="il-left">
|
||||||
|
<img src="assets/images/logo-light.png" alt="ImprimeLibros" class="il-logo" />
|
||||||
|
</td>
|
||||||
|
<td class="il-right">
|
||||||
|
<div class="il-company-box">
|
||||||
|
<span class="corner tl"></span>
|
||||||
|
<span class="corner tr"></span>
|
||||||
|
<span class="corner bl"></span>
|
||||||
|
<span class="corner br"></span>
|
||||||
|
|
||||||
|
<div class="company-line company-name" th:text="#{pdf.company.name} ?: 'ImprimeLibros'">
|
||||||
|
ImprimeLibros ERP</div>
|
||||||
|
<div class="company-line" th:text="#{pdf.company.address} ?: ''">C/ José Picón, 28 local A</div>
|
||||||
|
<div class="company-line">
|
||||||
|
<span th:text="#{pdf.company.postalcode} ?: '28028'">28028</span>
|
||||||
|
<span th:text="#{pdf.company.city} ?: 'Madrid'">Madrid</span>
|
||||||
|
</div>
|
||||||
|
<div class="company-line" th:text="#{pdf.company.phone} ?: '+34 910052574'">+34 910052574</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- BANDA SUPERIOR -->
|
||||||
|
<div class="doc-banner">
|
||||||
|
<div th:text="#{pdf.factura} ?: 'FACTURA'" class="banner-text">FACTURA</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FICHA Nº / CLIENTE / FECHA -->
|
||||||
|
<table class="sheet-info">
|
||||||
|
<tr>
|
||||||
|
<td class="text-start w-50"><span th:text="#{'pdf.factura.number'}" class="lbl">FACTURA Nº:</span> <span
|
||||||
|
class="val" th:text="${factura.numeroFactura}">153153</span></td>
|
||||||
|
<td class="text-end"><span class="lbl" th:text="#{pdf.presupuesto.date}">FECHA:</span> <span class="val"
|
||||||
|
th:text="${#temporals.format(factura.fechaEmision, 'dd/MM/yyyy')}">10/10/2025</span></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table class="sheet-info">
|
||||||
|
<tr>
|
||||||
|
<td class="text-start"><span th:text="#{'pdf.factura.razon-social'}" class="lbl">Razón Social:</span> <span
|
||||||
|
class="val" th:text="${direccionFacturacion.razonSocial}">153153</span></td>
|
||||||
|
<td class="text-end"><span th:text="#{'pdf.factura.direccion'}" class="lbl">Dirección:</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-start"><span th:text="#{'pdf.factura.identificacion-fiscal'}" class="lbl">Identificación
|
||||||
|
Fiscal:</span> <span class="val" th:text="${direccionFacturacion.identificacionFiscal}">153153</span></td>
|
||||||
|
<td class="text-end">
|
||||||
|
<span class="val"
|
||||||
|
th:text="${direccionFacturacion.direccion + ', ' + direccionFacturacion.cp + ', ' + direccionFacturacion.ciudad}">153153</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-start">
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<span class="val"
|
||||||
|
th:text="${direccionFacturacion.provincia + ', ' + #messages.msg('paises.' + direccionFacturacion.pais.keyword)}">153153</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- DATOS TÉCNICOS -->
|
||||||
|
<table class="items-table table table-bordered table-striped table-wrap w-100">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-75" th:text="#{pdf.factura.lineas.descripcion}">Descripción</th>
|
||||||
|
<th th:text="#{pdf.factura.lineas.base}">Base</th>
|
||||||
|
<th th:text="#{pdf.factura.lineas.iva_4}">I.V.A. 4%</th>
|
||||||
|
<th th:text="#{pdf.factura.lineas.iva_21}">I.V.A. 21%</th>
|
||||||
|
<th th:text="#{pdf.factura.lineas.total}">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="lineaFactura : ${factura.lineas}">
|
||||||
|
<td th:utext="${lineaFactura.descripcion}">Descripción de la línea</td>
|
||||||
|
<td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.baseLinea)}">0.00</td>
|
||||||
|
<td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.iva4Linea)}">0.00</td>
|
||||||
|
<td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.iva21Linea)}">0.00</td>
|
||||||
|
<td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.totalLinea)}">0.00</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td class="text-end fw-bold" colspan="4" th:text="#{pdf.factura.lineas.base}">Base</td>
|
||||||
|
<td class="text-end" colspan="1" th:text="${#numbers.formatCurrency(factura.baseImponible)}">0.00</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-end fw-bold" colspan="4" th:text="#{pdf.factura.lineas.iva_4}">I.V.A. 4%</td>
|
||||||
|
<td class="text-end" colspan="1" th:text="${#numbers.formatCurrency(factura.iva4)}">0.00</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-end fw-bold" colspan="4" th:text="#{pdf.factura.lineas.iva_21}">I.V.A. 21%</td>
|
||||||
|
<td class="text-end" colspan="1" th:text="${#numbers.formatCurrency(factura.iva21)}">0.00</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="text-end fw-bold text-uppercase" colspan="4" th:text="#{pdf.factura.lineas.total}">Total</td>
|
||||||
|
<td class="text-end fw-bold" colspan="1" th:text="${#numbers.formatCurrency(factura.totalFactura)}">0.00
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -1,6 +1,8 @@
|
|||||||
// src/test/java/com/imprimelibros/erp/pdf/PdfSmokeTest.java
|
// src/test/java/com/imprimelibros/erp/pdf/PdfSmokeTest.java
|
||||||
package com.imprimelibros.erp.pdf;
|
package com.imprimelibros.erp.pdf;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
@ -131,4 +133,10 @@ class PdfSmokeTest {
|
|||||||
System.out.println("✅ PDF generado en: " + out.toAbsolutePath());
|
System.out.println("✅ PDF generado en: " + out.toAbsolutePath());
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void generaFactura() {
|
||||||
|
pdfService.generaFactura(2L, Locale.forLanguageTag("es-ES"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user