mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-03-01 06:09:13 +00:00
Compare commits
39 Commits
d7b5dedb38
...
fix/import
| Author | SHA1 | Date | |
|---|---|---|---|
| 35967b93a0 | |||
| cef0af1bd2 | |||
| 8282c92419 | |||
| 433a055b14 | |||
| fe4d180e2d | |||
| cc2d2ef193 | |||
| 11a5918c37 | |||
| 88769ddaeb | |||
| 9acb105127 | |||
| 6dab15afbc | |||
| a0783c2062 | |||
| d0ccfb5626 | |||
| 2e569a7ffd | |||
| bc8ce4fa81 | |||
| 1bfe0cf3a2 | |||
| 61e55e014f | |||
| 06a3521f6b | |||
| ecf1472f58 | |||
| 48993a34c4 | |||
| a0bf8552f1 | |||
| 562dc2b231 | |||
| 9a49ccf6b8 | |||
| b2026f1cab | |||
| a5b6bf3a25 | |||
| 9a67c2e78f | |||
| 8263d97bf7 | |||
| 292aebcf65 | |||
| e50153205a | |||
| 5ecb38f474 | |||
| d7a85d9bfb | |||
| 4343997eb1 | |||
| 6bfc60d158 | |||
| aa8ecdf75c | |||
| dc529ff055 | |||
| 4a535ab644 | |||
| 400251ac3d | |||
| 6bea279066 | |||
| bf823281a5 | |||
| 9d4320db9a |
4
.gitignore
vendored
4
.gitignore
vendored
@ -33,4 +33,6 @@ build/
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
### Logs ###
|
### Logs ###
|
||||||
erp-*.log
|
/Logs/
|
||||||
|
erp.log
|
||||||
|
erp*.log
|
||||||
|
|||||||
45
docker-compose.plesk.yml
Normal file
45
docker-compose.plesk.yml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
imprimelibros-db:
|
||||||
|
image: mysql:8.0
|
||||||
|
container_name: imprimelibros-db
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: NrXz6DK6UoN
|
||||||
|
MYSQL_DATABASE: imprimelibros
|
||||||
|
MYSQL_USER: imprimelibros_user
|
||||||
|
MYSQL_PASSWORD: om91irrDctd
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/mysql
|
||||||
|
networks:
|
||||||
|
- imprimelibros-network
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "3309:3306" # host:container
|
||||||
|
|
||||||
|
imprimelibros-app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: imprimelibros-app:latest
|
||||||
|
container_name: imprimelibros-app
|
||||||
|
depends_on:
|
||||||
|
- imprimelibros-db
|
||||||
|
environment:
|
||||||
|
SPRING_DATASOURCE_URL: jdbc:mysql://imprimelibros-db:3306/imprimelibros?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Europe/Madrid
|
||||||
|
SPRING_DATASOURCE_USERNAME: imprimelibros_user
|
||||||
|
SPRING_DATASOURCE_PASSWORD: om91irrDctd
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./logs:/var/log/imprimelibros
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- imprimelibros-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
imprimelibros-network:
|
||||||
|
driver: bridge
|
||||||
11660
logs/erp.log
11660
logs/erp.log
File diff suppressed because one or more lines are too long
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -149,6 +149,10 @@ public class SecurityConfig {
|
|||||||
"/pagos/redsys/**"
|
"/pagos/redsys/**"
|
||||||
)
|
)
|
||||||
.permitAll()
|
.permitAll()
|
||||||
|
.requestMatchers("/impersonate/exit")
|
||||||
|
.hasRole("PREVIOUS_ADMINISTRATOR")
|
||||||
|
.requestMatchers("/impersonate")
|
||||||
|
.hasAnyRole("SUPERADMIN", "ADMIN")
|
||||||
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
|
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
|
||||||
.anyRequest().authenticated())
|
.anyRequest().authenticated())
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.imprimelibros.erp.configurationERP;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/configuracion/variables-sistema")
|
||||||
|
@PreAuthorize("hasRole('SUPERADMIN')")
|
||||||
|
public class VariablesController {
|
||||||
|
|
||||||
|
@GetMapping()
|
||||||
|
public String list(Model model, Locale locale) {
|
||||||
|
return new String();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -398,13 +398,12 @@ public class skApiClient {
|
|||||||
public Map<String, Object> checkPedidoEstado(Long presupuestoId, Locale locale) {
|
public Map<String, Object> checkPedidoEstado(Long presupuestoId, Locale locale) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
String jsonResponse = performWithRetry(() -> {
|
String jsonResponse = performWithRetry(() -> {
|
||||||
String url = this.skApiUrl + "api/estado-pedido/" + presupuestoId;
|
String url = this.skApiUrl + "api/estado-pedido/" + presupuestoId;
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setBearerAuth(authService.getToken());
|
||||||
headers.setBearerAuth(authService.getToken()); // token actualizado
|
headers.setAccept(java.util.List.of(MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
||||||
|
|
||||||
@ -420,19 +419,34 @@ public class skApiClient {
|
|||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
JsonNode root = mapper.readTree(jsonResponse);
|
JsonNode root = mapper.readTree(jsonResponse);
|
||||||
|
|
||||||
if (root.get("data") == null) {
|
// ✅ Si falta data, devolvemos mapa sin "estado" (o con estado=null pero con
|
||||||
throw new RuntimeException(
|
// HashMap)
|
||||||
"Sin respuesta desde el servidor del proveedor");
|
if (root == null || root.get("data") == null || root.get("data").isNull()) {
|
||||||
|
Map<String, Object> out = new HashMap<>();
|
||||||
|
out.put("message", "Respuesta sin campo 'data' desde el servidor del proveedor");
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
String estado = root.get("data").asText();
|
String estado = root.get("data").asText();
|
||||||
return Map.of(
|
return Map.of("estado", estado); // aquí NO es null, así que Map.of OK
|
||||||
"estado", estado);
|
|
||||||
|
} catch (HttpClientErrorException ex) {
|
||||||
|
|
||||||
|
if (ex.getStatusCode() == HttpStatus.NOT_FOUND) {
|
||||||
|
// ✅ 404: devolvemos mapa sin "estado" para evitar null en Map.of
|
||||||
|
Map<String, Object> out = new HashMap<>();
|
||||||
|
out.put("notFound", true);
|
||||||
|
out.put("message", "Orden de trabajo no encontrada para presupuestoId=" + presupuestoId);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ex;
|
||||||
|
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
// Fallback al 80% del ancho
|
// ✅ no parseable (HTML, debugbar, etc.)
|
||||||
return Map.of(
|
Map<String, Object> out = new HashMap<>();
|
||||||
"estado", null);
|
out.put("message", "Respuesta no-JSON o JSON inválido desde el proveedor");
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -542,7 +556,7 @@ public class skApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Boolean aceptarFerro(Long presupuestoId, Locale locale) {
|
public Boolean aceptarFerro(Long presupuestoId, Locale locale) {
|
||||||
|
|
||||||
String result = performWithRetry(() -> {
|
String result = performWithRetry(() -> {
|
||||||
String url = this.skApiUrl + "api/aceptar-ferro/" + presupuestoId;
|
String url = this.skApiUrl + "api/aceptar-ferro/" + presupuestoId;
|
||||||
|
|
||||||
@ -576,9 +590,8 @@ public class skApiClient {
|
|||||||
return Boolean.parseBoolean(result);
|
return Boolean.parseBoolean(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Boolean cancelarPedido(Long pedidoId) {
|
public Boolean cancelarPedido(Long pedidoId) {
|
||||||
|
|
||||||
String result = performWithRetry(() -> {
|
String result = performWithRetry(() -> {
|
||||||
String url = this.skApiUrl + "api/cancelar-pedido/" + pedidoId;
|
String url = this.skApiUrl + "api/cancelar-pedido/" + pedidoId;
|
||||||
|
|
||||||
@ -618,12 +631,21 @@ public class skApiClient {
|
|||||||
private String performWithRetry(Supplier<String> request) {
|
private String performWithRetry(Supplier<String> request) {
|
||||||
try {
|
try {
|
||||||
return request.get();
|
return request.get();
|
||||||
|
|
||||||
} catch (HttpClientErrorException.Unauthorized e) {
|
} catch (HttpClientErrorException.Unauthorized e) {
|
||||||
// Token expirado, renovar y reintentar
|
// Token expirado, renovar y reintentar
|
||||||
authService.invalidateToken();
|
authService.invalidateToken();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return request.get(); // segundo intento
|
return request.get(); // segundo intento
|
||||||
|
|
||||||
} catch (HttpClientErrorException ex) {
|
} catch (HttpClientErrorException ex) {
|
||||||
|
// ✅ IMPORTANTe: si el segundo intento es 404, NO lo envuelvas
|
||||||
|
if (ex.getStatusCode() == HttpStatus.NOT_FOUND) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si es otro 4xx/5xx, sí lo envolvemos
|
||||||
throw new RuntimeException("La autenticación ha fallado tras renovar el token.", ex);
|
throw new RuntimeException("La autenticación ha fallado tras renovar el token.", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,13 +9,12 @@ import java.time.LocalDateTime;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.hibernate.annotations.Formula;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(name = "facturas", uniqueConstraints = {
|
||||||
name = "facturas",
|
|
||||||
uniqueConstraints = {
|
|
||||||
@UniqueConstraint(name = "uq_facturas_numero_factura", columnNames = "numero_factura")
|
@UniqueConstraint(name = "uq_facturas_numero_factura", columnNames = "numero_factura")
|
||||||
}
|
})
|
||||||
)
|
|
||||||
public class Factura extends AbstractAuditedEntitySoftTs {
|
public class Factura extends AbstractAuditedEntitySoftTs {
|
||||||
|
|
||||||
@Column(name = "pedido_id")
|
@Column(name = "pedido_id")
|
||||||
@ -80,11 +79,18 @@ public class Factura extends AbstractAuditedEntitySoftTs {
|
|||||||
@OneToMany(mappedBy = "factura", cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(mappedBy = "factura", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
private List<FacturaPago> pagos = new ArrayList<>();
|
private List<FacturaPago> pagos = new ArrayList<>();
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "factura", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
private List<FacturaDireccion> direcciones = new ArrayList<>();
|
||||||
|
|
||||||
|
@Formula("(select u.fullname from users u where u.id = cliente_id)")
|
||||||
|
private String clienteNombre;
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
public void addLinea(FacturaLinea linea) {
|
public void addLinea(FacturaLinea linea) {
|
||||||
linea.setFactura(this);
|
linea.setFactura(this);
|
||||||
this.lineas.add(linea);
|
this.lineas.add(linea);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeLinea(FacturaLinea linea) {
|
public void removeLinea(FacturaLinea linea) {
|
||||||
this.lineas.remove(linea);
|
this.lineas.remove(linea);
|
||||||
linea.setFactura(null);
|
linea.setFactura(null);
|
||||||
@ -94,63 +100,172 @@ public class Factura extends AbstractAuditedEntitySoftTs {
|
|||||||
pago.setFactura(this);
|
pago.setFactura(this);
|
||||||
this.pagos.add(pago);
|
this.pagos.add(pago);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removePago(FacturaPago pago) {
|
public void removePago(FacturaPago pago) {
|
||||||
this.pagos.remove(pago);
|
this.pagos.remove(pago);
|
||||||
pago.setFactura(null);
|
pago.setFactura(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters/Setters
|
// Getters/Setters
|
||||||
public Long getPedidoId() { return pedidoId; }
|
public Long getPedidoId() {
|
||||||
public void setPedidoId(Long pedidoId) { this.pedidoId = pedidoId; }
|
return pedidoId;
|
||||||
|
}
|
||||||
|
|
||||||
public Factura getFacturaRectificada() { return facturaRectificada; }
|
public void setPedidoId(Long pedidoId) {
|
||||||
public void setFacturaRectificada(Factura facturaRectificada) { this.facturaRectificada = facturaRectificada; }
|
this.pedidoId = pedidoId;
|
||||||
|
}
|
||||||
|
|
||||||
public Factura getFacturaRectificativa() { return facturaRectificativa; }
|
public Factura getFacturaRectificada() {
|
||||||
public void setFacturaRectificativa(Factura facturaRectificativa) { this.facturaRectificativa = facturaRectificativa; }
|
return facturaRectificada;
|
||||||
|
}
|
||||||
|
|
||||||
public User getCliente() { return cliente; }
|
public void setFacturaRectificada(Factura facturaRectificada) {
|
||||||
public void setCliente(User cliente) { this.cliente = cliente; }
|
this.facturaRectificada = facturaRectificada;
|
||||||
|
}
|
||||||
|
|
||||||
public SerieFactura getSerie() { return serie; }
|
public Factura getFacturaRectificativa() {
|
||||||
public void setSerie(SerieFactura serie) { this.serie = serie; }
|
return facturaRectificativa;
|
||||||
|
}
|
||||||
|
|
||||||
public String getNumeroFactura() { return numeroFactura; }
|
public void setFacturaRectificativa(Factura facturaRectificativa) {
|
||||||
public void setNumeroFactura(String numeroFactura) { this.numeroFactura = numeroFactura; }
|
this.facturaRectificativa = facturaRectificativa;
|
||||||
|
}
|
||||||
|
|
||||||
public EstadoFactura getEstado() { return estado; }
|
public User getCliente() {
|
||||||
public void setEstado(EstadoFactura estado) { this.estado = estado; }
|
return cliente;
|
||||||
|
}
|
||||||
|
|
||||||
public EstadoPagoFactura getEstadoPago() { return estadoPago; }
|
public void setCliente(User cliente) {
|
||||||
public void setEstadoPago(EstadoPagoFactura estadoPago) { this.estadoPago = estadoPago; }
|
this.cliente = cliente;
|
||||||
|
}
|
||||||
|
|
||||||
public TipoPago getTipoPago() { return tipoPago; }
|
public SerieFactura getSerie() {
|
||||||
public void setTipoPago(TipoPago tipoPago) { this.tipoPago = tipoPago; }
|
return serie;
|
||||||
|
}
|
||||||
|
|
||||||
public LocalDateTime getFechaEmision() { return fechaEmision; }
|
public void setSerie(SerieFactura serie) {
|
||||||
public void setFechaEmision(LocalDateTime fechaEmision) { this.fechaEmision = fechaEmision; }
|
this.serie = serie;
|
||||||
|
}
|
||||||
|
|
||||||
public BigDecimal getBaseImponible() { return baseImponible; }
|
public String getNumeroFactura() {
|
||||||
public void setBaseImponible(BigDecimal baseImponible) { this.baseImponible = baseImponible; }
|
return numeroFactura;
|
||||||
|
}
|
||||||
|
|
||||||
public BigDecimal getIva4() { return iva4; }
|
public void setNumeroFactura(String numeroFactura) {
|
||||||
public void setIva4(BigDecimal iva4) { this.iva4 = iva4; }
|
this.numeroFactura = numeroFactura;
|
||||||
|
}
|
||||||
|
|
||||||
public BigDecimal getIva21() { return iva21; }
|
public EstadoFactura getEstado() {
|
||||||
public void setIva21(BigDecimal iva21) { this.iva21 = iva21; }
|
return estado;
|
||||||
|
}
|
||||||
|
|
||||||
public BigDecimal getTotalFactura() { return totalFactura; }
|
public void setEstado(EstadoFactura estado) {
|
||||||
public void setTotalFactura(BigDecimal totalFactura) { this.totalFactura = totalFactura; }
|
this.estado = estado;
|
||||||
|
}
|
||||||
|
|
||||||
public BigDecimal getTotalPagado() { return totalPagado; }
|
public EstadoPagoFactura getEstadoPago() {
|
||||||
public void setTotalPagado(BigDecimal totalPagado) { this.totalPagado = totalPagado; }
|
return estadoPago;
|
||||||
|
}
|
||||||
|
|
||||||
public String getNotas() { return notas; }
|
public void setEstadoPago(EstadoPagoFactura estadoPago) {
|
||||||
public void setNotas(String notas) { this.notas = notas; }
|
this.estadoPago = estadoPago;
|
||||||
|
}
|
||||||
|
|
||||||
public List<FacturaLinea> getLineas() { return lineas; }
|
public TipoPago getTipoPago() {
|
||||||
public void setLineas(List<FacturaLinea> lineas) { this.lineas = lineas; }
|
return tipoPago;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTipoPago(TipoPago tipoPago) {
|
||||||
|
this.tipoPago = tipoPago;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getFechaEmision() {
|
||||||
|
return fechaEmision;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFechaEmision(LocalDateTime fechaEmision) {
|
||||||
|
this.fechaEmision = fechaEmision;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getBaseImponible() {
|
||||||
|
return baseImponible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBaseImponible(BigDecimal baseImponible) {
|
||||||
|
this.baseImponible = baseImponible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getIva4() {
|
||||||
|
return iva4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIva4(BigDecimal iva4) {
|
||||||
|
this.iva4 = iva4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getIva21() {
|
||||||
|
return iva21;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIva21(BigDecimal iva21) {
|
||||||
|
this.iva21 = iva21;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getTotalFactura() {
|
||||||
|
return totalFactura;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalFactura(BigDecimal totalFactura) {
|
||||||
|
this.totalFactura = totalFactura;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getTotalPagado() {
|
||||||
|
return totalPagado;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalPagado(BigDecimal totalPagado) {
|
||||||
|
this.totalPagado = totalPagado;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNotas() {
|
||||||
|
return notas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNotas(String notas) {
|
||||||
|
this.notas = notas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FacturaLinea> getLineas() {
|
||||||
|
return lineas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLineas(List<FacturaLinea> lineas) {
|
||||||
|
this.lineas = lineas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FacturaPago> getPagos() {
|
||||||
|
return pagos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPagos(List<FacturaPago> pagos) {
|
||||||
|
this.pagos = pagos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FacturaDireccion> getDirecciones() {
|
||||||
|
return direcciones;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDirecciones(List<FacturaDireccion> direcciones) {
|
||||||
|
this.direcciones = direcciones;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FacturaDireccion getDireccionFacturacion() {
|
||||||
|
return (direcciones == null || direcciones.isEmpty()) ? null : direcciones.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDireccion(FacturaDireccion direccion) {
|
||||||
|
direccion.setFactura(this);
|
||||||
|
this.direcciones.add(direccion);
|
||||||
|
}
|
||||||
|
|
||||||
public List<FacturaPago> getPagos() { return pagos; }
|
|
||||||
public void setPagos(List<FacturaPago> pagos) { this.pagos = pagos; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,209 @@
|
|||||||
|
package com.imprimelibros.erp.facturacion;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.direcciones.Direccion.TipoIdentificacionFiscal;
|
||||||
|
import com.imprimelibros.erp.paises.Paises;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "facturas_direcciones",
|
||||||
|
indexes = {
|
||||||
|
@Index(name = "idx_facturas_direcciones_factura_id", columnList = "factura_id")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
public class FacturaDireccion {
|
||||||
|
|
||||||
|
@Column(name = "id")
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "factura_id", nullable = false,
|
||||||
|
foreignKey = @ForeignKey(name = "fk_facturas_direcciones_factura"))
|
||||||
|
private Factura factura;
|
||||||
|
|
||||||
|
@Column(name = "unidades")
|
||||||
|
private Integer unidades; // MEDIUMINT UNSIGNED
|
||||||
|
|
||||||
|
@Column(name = "email", length = 255)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Column(name = "att", length = 150, nullable = false)
|
||||||
|
private String att;
|
||||||
|
|
||||||
|
@Column(name = "direccion", length = 255, nullable = false)
|
||||||
|
private String direccion;
|
||||||
|
|
||||||
|
@Column(name = "cp", nullable = false)
|
||||||
|
private Integer cp; // MEDIUMINT UNSIGNED
|
||||||
|
|
||||||
|
@Column(name = "ciudad", length = 100, nullable = false)
|
||||||
|
private String ciudad;
|
||||||
|
|
||||||
|
@Column(name = "provincia", length = 100, nullable = false)
|
||||||
|
private String provincia;
|
||||||
|
|
||||||
|
@Column(name = "pais_code3", length = 3, nullable = false)
|
||||||
|
private String paisCode3 = "esp";
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "pais_code3", referencedColumnName = "code3", insertable = false, updatable = false)
|
||||||
|
private Paises pais;
|
||||||
|
|
||||||
|
@Column(name = "telefono", length = 30)
|
||||||
|
private String telefono;
|
||||||
|
|
||||||
|
@Column(name = "instrucciones", length = 255)
|
||||||
|
private String instrucciones;
|
||||||
|
|
||||||
|
@Column(name = "razon_social", length = 150)
|
||||||
|
private String razonSocial;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "tipo_identificacion_fiscal", length = 20, nullable = false)
|
||||||
|
private TipoIdentificacionFiscal tipoIdentificacionFiscal = TipoIdentificacionFiscal.DNI;
|
||||||
|
|
||||||
|
@Column(name = "identificacion_fiscal", length = 50)
|
||||||
|
private String identificacionFiscal;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private java.time.Instant createdAt;
|
||||||
|
|
||||||
|
|
||||||
|
// Getters / Setters
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Factura getFactura() {
|
||||||
|
return factura;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFactura(Factura factura) {
|
||||||
|
this.factura = factura;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getUnidades() {
|
||||||
|
return unidades;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUnidades(Integer unidades) {
|
||||||
|
this.unidades = unidades;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmail(String email) {
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAtt() {
|
||||||
|
return att;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAtt(String att) {
|
||||||
|
this.att = att;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDireccion() {
|
||||||
|
return direccion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDireccion(String direccion) {
|
||||||
|
this.direccion = direccion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getCp() {
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCp(Integer 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 getPaisCode3() {
|
||||||
|
return paisCode3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPaisCode3(String paisCode3) {
|
||||||
|
this.paisCode3 = paisCode3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Paises getPais() {
|
||||||
|
return pais;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPais(Paises pais) {
|
||||||
|
this.pais = pais;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTelefono() {
|
||||||
|
return telefono;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTelefono(String telefono) {
|
||||||
|
this.telefono = telefono;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInstrucciones() {
|
||||||
|
return instrucciones;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInstrucciones(String instrucciones) {
|
||||||
|
this.instrucciones = instrucciones;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRazonSocial() {
|
||||||
|
return razonSocial;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRazonSocial(String razonSocial) {
|
||||||
|
this.razonSocial = razonSocial;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TipoIdentificacionFiscal getTipoIdentificacionFiscal() {
|
||||||
|
return tipoIdentificacionFiscal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTipoIdentificacionFiscal(TipoIdentificacionFiscal tipoIdentificacionFiscal) {
|
||||||
|
this.tipoIdentificacionFiscal = tipoIdentificacionFiscal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIdentificacionFiscal() {
|
||||||
|
return identificacionFiscal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIdentificacionFiscal(String identificacionFiscal) {
|
||||||
|
this.identificacionFiscal = identificacionFiscal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public java.time.Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
public void setCreatedAt(java.time.Instant createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,390 @@
|
|||||||
|
package com.imprimelibros.erp.facturacion.controller;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.configurationERP.VariableService;
|
||||||
|
import com.imprimelibros.erp.datatables.DataTable;
|
||||||
|
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||||
|
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||||
|
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||||
|
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||||
|
import com.imprimelibros.erp.facturacion.EstadoFactura;
|
||||||
|
import com.imprimelibros.erp.facturacion.Factura;
|
||||||
|
import com.imprimelibros.erp.facturacion.FacturaDireccion;
|
||||||
|
import com.imprimelibros.erp.facturacion.dto.FacturaAddRequestDto;
|
||||||
|
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.service.FacturacionService;
|
||||||
|
import com.imprimelibros.erp.i18n.TranslationService;
|
||||||
|
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||||
|
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/facturas")
|
||||||
|
@PreAuthorize("hasRole('SUPERADMIN') || hasRole('ADMIN')")
|
||||||
|
public class FacturasController {
|
||||||
|
|
||||||
|
private final FacturacionService facturacionService;
|
||||||
|
|
||||||
|
private final FacturaRepository repo;
|
||||||
|
private final TranslationService translationService;
|
||||||
|
private final MessageSource messageSource;
|
||||||
|
private final PedidoService pedidoService;
|
||||||
|
private final VariableService variableService;
|
||||||
|
private final DireccionService direccionService;
|
||||||
|
|
||||||
|
public FacturasController(
|
||||||
|
FacturaRepository repo,
|
||||||
|
TranslationService translationService,
|
||||||
|
MessageSource messageSource,
|
||||||
|
PedidoService pedidoService, FacturacionService facturacionService, VariableService variableService, DireccionService direccionService) {
|
||||||
|
this.repo = repo;
|
||||||
|
this.translationService = translationService;
|
||||||
|
this.messageSource = messageSource;
|
||||||
|
this.pedidoService = pedidoService;
|
||||||
|
this.facturacionService = facturacionService;
|
||||||
|
this.direccionService = direccionService;
|
||||||
|
this.variableService = variableService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String facturasList(Model model, Locale locale) {
|
||||||
|
|
||||||
|
List<String> keys = List.of(
|
||||||
|
"app.eliminar",
|
||||||
|
"app.cancelar",
|
||||||
|
"facturas.delete.title",
|
||||||
|
"facturas.delete.text",
|
||||||
|
"facturas.delete.ok.title",
|
||||||
|
"facturas.delete.ok.text");
|
||||||
|
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||||
|
model.addAttribute("languageBundle", translations);
|
||||||
|
return "imprimelibros/facturas/facturas-list";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/add")
|
||||||
|
public String facturaAdd(Model model, Locale locale) {
|
||||||
|
|
||||||
|
List<String> keys = List.of(
|
||||||
|
"facturas.form.cliente.placeholder",
|
||||||
|
"facturas.add.form.validation.title",
|
||||||
|
"facturas.add.form.validation",
|
||||||
|
"facturas.error.create"
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||||
|
model.addAttribute("languageBundle", translations);
|
||||||
|
|
||||||
|
model.addAttribute("defaultSerieRectificativa", variableService.getValorEntero("serie_facturacion_rect_default"));
|
||||||
|
|
||||||
|
return "imprimelibros/facturas/facturas-add-form";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/add")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> facturaAddPost(
|
||||||
|
Model model,
|
||||||
|
@RequestBody FacturaAddRequestDto request,
|
||||||
|
Locale locale) {
|
||||||
|
|
||||||
|
Factura nuevaFactura = facturacionService.crearNuevaFactura(
|
||||||
|
request.getUser(),
|
||||||
|
request.getSerie(),
|
||||||
|
request.getDireccion(),
|
||||||
|
request.getFactura_rectificada()
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
if(nuevaFactura == null){
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", messageSource.getMessage("facturas.error.create", null, "No se ha podido crear la factura. Revise los datos e inténtelo de nuevo.", locale));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("facturaId", nuevaFactura.getId());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public String facturaDetail(@PathVariable Long id, Model model, Locale locale) {
|
||||||
|
Factura factura = repo.findById(id)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
FacturaDireccion direccionFacturacion = factura.getDireccionFacturacion();
|
||||||
|
|
||||||
|
model.addAttribute("direccionFacturacion", direccionFacturacion);
|
||||||
|
model.addAttribute("factura", factura);
|
||||||
|
|
||||||
|
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")
|
||||||
|
public String facturaContainer(@PathVariable Long id, Model model, Locale locale) {
|
||||||
|
Factura factura = repo.findById(id)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||||
|
|
||||||
|
FacturaDireccion direccionFacturacion = factura.getDireccionFacturacion();
|
||||||
|
|
||||||
|
|
||||||
|
model.addAttribute("direccionFacturacion", direccionFacturacion);
|
||||||
|
model.addAttribute("factura", factura);
|
||||||
|
|
||||||
|
return "imprimelibros/facturas/partials/factura-container :: factura-container";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/validar")
|
||||||
|
public ResponseEntity<?> validarFactura(@PathVariable Long id) {
|
||||||
|
Factura factura = repo.findById(id)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||||
|
|
||||||
|
if (factura.getEstado() != EstadoFactura.borrador) {
|
||||||
|
return ResponseEntity.badRequest().body("Solo se pueden validar facturas en estado 'borrador'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
facturacionService.validarFactura(factura.getId());
|
||||||
|
repo.save(factura);
|
||||||
|
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/borrador")
|
||||||
|
public ResponseEntity<?> marcarBorrador(@PathVariable Long id) {
|
||||||
|
Factura factura = repo.findById(id)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||||
|
|
||||||
|
if (factura.getEstado() != EstadoFactura.validada) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body("Solo se pueden marcar como borrador facturas en estado 'validada'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
factura.setEstado(EstadoFactura.borrador);
|
||||||
|
repo.save(factura);
|
||||||
|
|
||||||
|
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")
|
||||||
|
public ResponseEntity<?> setNotas(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestBody Map<String, String> payload,
|
||||||
|
Model model,
|
||||||
|
Locale locale) {
|
||||||
|
Factura factura = repo.findById(id)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||||
|
String notas = payload.get("notas");
|
||||||
|
factura.setNotas(notas);
|
||||||
|
repo.save(factura);
|
||||||
|
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// API: DataTables (server-side)
|
||||||
|
// -----------------------------
|
||||||
|
@GetMapping("/api/datatables")
|
||||||
|
@ResponseBody
|
||||||
|
public DataTablesResponse<Map<String, Object>> datatables(HttpServletRequest request, Locale locale) {
|
||||||
|
|
||||||
|
DataTablesRequest dt = DataTablesParser.from(request);
|
||||||
|
|
||||||
|
Specification<Factura> notDeleted = (root, q, cb) -> cb.isNull(root.get("deletedAt"));
|
||||||
|
long total = repo.count(notDeleted);
|
||||||
|
|
||||||
|
return DataTable
|
||||||
|
.of(repo, Factura.class, dt, List.of("clienteNombre", "numeroFactura", "estado", "estadoPago"))
|
||||||
|
.where(notDeleted)
|
||||||
|
.orderable(List.of("id", "clienteNombre", "numeroFactura", "estado", "estadoPago"))
|
||||||
|
.onlyAddedColumns()
|
||||||
|
.add("id", Factura::getId)
|
||||||
|
.add("cliente", f -> {
|
||||||
|
var c = f.getCliente();
|
||||||
|
return c == null ? null : c.getFullName(); // o getNombre(), etc.
|
||||||
|
})
|
||||||
|
|
||||||
|
.add("numero_factura", Factura::getNumeroFactura)
|
||||||
|
.add("estado", Factura::getEstado)
|
||||||
|
.add("estado_label", f -> {
|
||||||
|
String key = "facturas.estado." + f.getEstado().name().toLowerCase();
|
||||||
|
return messageSource.getMessage(key, null, f.getEstado().name(), locale);
|
||||||
|
})
|
||||||
|
.add("estado_pago", Factura::getEstadoPago)
|
||||||
|
.add("estado_pago_label", f -> {
|
||||||
|
String key = "facturas.estado-pago." + f.getEstadoPago().name().toLowerCase();
|
||||||
|
return messageSource.getMessage(key, null, f.getEstadoPago().name(), locale);
|
||||||
|
})
|
||||||
|
.add("total", Factura::getTotalFactura)
|
||||||
|
.add("fecha_emision", f -> {
|
||||||
|
LocalDateTime fecha = f.getFechaEmision();
|
||||||
|
return fecha == null ? null : fecha.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
|
||||||
|
})
|
||||||
|
.add("actions", f -> {
|
||||||
|
if (f.getEstado() == EstadoFactura.borrador) {
|
||||||
|
return """
|
||||||
|
<div class="hstack gap-3 flex-wrap">
|
||||||
|
<button type="button"
|
||||||
|
class="btn p-0 link-success btn-view-factura fs-15"
|
||||||
|
data-id="%d">
|
||||||
|
<i class="ri-eye-line"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn p-0 link-danger btn-delete-factura fs-15"
|
||||||
|
data-id="%d">
|
||||||
|
<i class="ri-delete-bin-5-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
""".formatted(f.getId(), f.getId());
|
||||||
|
} else {
|
||||||
|
return """
|
||||||
|
<div class="hstack gap-3 flex-wrap">
|
||||||
|
<button type="button"
|
||||||
|
class="btn p-0 link-success btn-view-factura fs-15"
|
||||||
|
data-id="%d">
|
||||||
|
<i class="ri-eye-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
""".formatted(f.getId());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.toJson(total);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// API: select2 Direcciones
|
||||||
|
// -----------------------------
|
||||||
|
@GetMapping("/api/get-direcciones")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> getSelect2Facturacion(
|
||||||
|
@RequestParam(value = "q", required = false) String q1,
|
||||||
|
@RequestParam(value = "term", required = false) String q2,
|
||||||
|
@RequestParam(value = "user_id", required = true) Long userId,
|
||||||
|
Authentication auth) {
|
||||||
|
|
||||||
|
|
||||||
|
return direccionService.getForSelectFacturacion(q1, q2, userId);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// API: select2 facturas rectificables
|
||||||
|
// -----------------------------
|
||||||
|
@GetMapping("/api/get-facturas-rectificables")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> getSelect2FacturasRectificables(
|
||||||
|
@RequestParam(value = "q", required = false) String q1,
|
||||||
|
@RequestParam(value = "term", required = false) String q2,
|
||||||
|
@RequestParam(value = "user_id", required = true) Long userId,
|
||||||
|
Authentication auth) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return Map.of("results", List.of());
|
||||||
|
}
|
||||||
|
return facturacionService.getForSelectFacturasRectificables(q1, q2, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -21,6 +21,7 @@ import org.springframework.ui.Model;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -98,12 +99,12 @@ public class SeriesFacturacionController {
|
|||||||
.add("actions", s -> """
|
.add("actions", s -> """
|
||||||
<div class="hstack gap-3 flex-wrap">
|
<div class="hstack gap-3 flex-wrap">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-link p-0 link-success btn-edit-serie fs-15"
|
class="btn p-0 link-success btn-edit-serie fs-15"
|
||||||
data-id="%d">
|
data-id="%d">
|
||||||
<i class="ri-edit-2-line"></i>
|
<i class="ri-edit-2-line"></i>
|
||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-link p-0 link-danger btn-delete-serie fs-15"
|
class="btn p-0 link-danger btn-delete-serie fs-15"
|
||||||
data-id="%d">
|
data-id="%d">
|
||||||
<i class="ri-delete-bin-5-line"></i>
|
<i class="ri-delete-bin-5-line"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -172,6 +173,30 @@ public class SeriesFacturacionController {
|
|||||||
return ResponseEntity.ok(Map.of("ok", true));
|
return ResponseEntity.ok(Map.of("ok", true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// API: GET for select2
|
||||||
|
// -----------------------------
|
||||||
|
@GetMapping("/api/get-series")
|
||||||
|
@ResponseBody
|
||||||
|
public Map<String, Object> getSeriesForSelect(
|
||||||
|
@RequestParam(value = "q", required = false) String q1,
|
||||||
|
@RequestParam(value = "term", required = false) String q2,
|
||||||
|
Locale locale) {
|
||||||
|
String query = (q1 != null && !q1.isBlank()) ? q1
|
||||||
|
: (q2 != null && !q2.isBlank()) ? q2
|
||||||
|
: "";
|
||||||
|
List<Map<String, Object>> results = repo.searchForSelectSeriesFacturacion(query).stream()
|
||||||
|
.map(s -> {
|
||||||
|
Map<String, Object> m = new HashMap<>();
|
||||||
|
m.put("id", s.getId());
|
||||||
|
m.put("text", s.getNombreSerie());
|
||||||
|
return m;
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Map.of("results", results);
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
// Payload + validación
|
// Payload + validación
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
|
|||||||
@ -0,0 +1,147 @@
|
|||||||
|
package com.imprimelibros.erp.facturacion.dto;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.facturacion.FacturaDireccion;
|
||||||
|
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 FacturaDireccion toFacturaDireccion() {
|
||||||
|
FacturaDireccion fd = new FacturaDireccion();
|
||||||
|
applyTo(fd);
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PedidoDireccion toPedidoDireccion() {
|
||||||
|
PedidoDireccion pd = new PedidoDireccion();
|
||||||
|
applyTo(pd);
|
||||||
|
pd.setFacturacion(true);
|
||||||
|
return pd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyTo(PedidoDireccion pd) {
|
||||||
|
pd.setAtt("");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyTo(FacturaDireccion fd ) {
|
||||||
|
fd.setAtt("");
|
||||||
|
fd.setRazonSocial(this.razonSocial);
|
||||||
|
fd.setIdentificacionFiscal(this.identificacionFiscal);
|
||||||
|
fd.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fd.setCp(cpInt);
|
||||||
|
|
||||||
|
fd.setCiudad(this.ciudad);
|
||||||
|
fd.setProvincia(this.provincia);
|
||||||
|
|
||||||
|
fd.setPaisCode3(this.paisKeyword);
|
||||||
|
|
||||||
|
fd.setTelefono(this.telefono);
|
||||||
|
fd.setCreatedAt(Instant.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package com.imprimelibros.erp.facturacion.dto;
|
||||||
|
|
||||||
|
public class FacturaAddRequestDto {
|
||||||
|
|
||||||
|
private Long user;
|
||||||
|
private Long serie;
|
||||||
|
private Long direccion;
|
||||||
|
private Long factura_rectificada;
|
||||||
|
|
||||||
|
// getters y setters
|
||||||
|
public Long getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
public void setUser(Long user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
public Long getSerie() {
|
||||||
|
return serie;
|
||||||
|
}
|
||||||
|
public void setSerie(Long serie) {
|
||||||
|
this.serie = serie;
|
||||||
|
}
|
||||||
|
public Long getDireccion() {
|
||||||
|
return direccion;
|
||||||
|
}
|
||||||
|
public void setDireccion(Long direccion) {
|
||||||
|
this.direccion = direccion;
|
||||||
|
}
|
||||||
|
public Long getFactura_rectificada() {
|
||||||
|
return factura_rectificada;
|
||||||
|
}
|
||||||
|
public void setFactura_rectificada(Long factura_rectificada) {
|
||||||
|
this.factura_rectificada = factura_rectificada;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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,67 @@
|
|||||||
|
package com.imprimelibros.erp.facturacion.dto;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||||
|
import com.imprimelibros.erp.facturacion.FacturaDireccion;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.direcciones.Direccion.TipoIdentificacionFiscal;
|
||||||
|
|
||||||
|
public final class FacturaDireccionMapper {
|
||||||
|
|
||||||
|
private FacturaDireccionMapper() {}
|
||||||
|
|
||||||
|
public static FacturaDireccion fromPedidoDireccion(PedidoDireccion src) {
|
||||||
|
if (src == null) return null;
|
||||||
|
|
||||||
|
FacturaDireccion dst = new FacturaDireccion();
|
||||||
|
|
||||||
|
dst.setUnidades(src.getUnidades());
|
||||||
|
dst.setEmail(src.getEmail());
|
||||||
|
dst.setAtt(src.getAtt());
|
||||||
|
dst.setDireccion(src.getDireccion());
|
||||||
|
dst.setCp(src.getCp());
|
||||||
|
dst.setCiudad(src.getCiudad());
|
||||||
|
dst.setProvincia(src.getProvincia());
|
||||||
|
dst.setPaisCode3(src.getPaisCode3());
|
||||||
|
dst.setTelefono(src.getTelefono());
|
||||||
|
dst.setInstrucciones(src.getInstrucciones());
|
||||||
|
dst.setRazonSocial(src.getRazonSocial());
|
||||||
|
dst.setCreatedAt(Instant.now());
|
||||||
|
|
||||||
|
// OJO: en PedidoDireccion usas Direccion.TipoIdentificacionFiscal
|
||||||
|
// En FacturaDireccion usa el enum que hayas definido/importado.
|
||||||
|
dst.setTipoIdentificacionFiscal(
|
||||||
|
TipoIdentificacionFiscal.valueOf(src.getTipoIdentificacionFiscal().name())
|
||||||
|
);
|
||||||
|
|
||||||
|
dst.setIdentificacionFiscal(src.getIdentificacionFiscal());
|
||||||
|
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FacturaDireccion fromDireccion(com.imprimelibros.erp.direcciones.Direccion src) {
|
||||||
|
if (src == null) return null;
|
||||||
|
|
||||||
|
FacturaDireccion dst = new FacturaDireccion();
|
||||||
|
|
||||||
|
dst.setUnidades(null);
|
||||||
|
dst.setEmail(src.getUser().getUserName());
|
||||||
|
dst.setAtt(src.getAtt());
|
||||||
|
dst.setDireccion(src.getDireccion());
|
||||||
|
dst.setCp(src.getCp());
|
||||||
|
dst.setCiudad(src.getCiudad());
|
||||||
|
dst.setProvincia(src.getProvincia());
|
||||||
|
dst.setPaisCode3(src.getPais().getCode3());
|
||||||
|
dst.setTelefono(src.getTelefono());
|
||||||
|
dst.setInstrucciones(src.getInstrucciones());
|
||||||
|
dst.setRazonSocial(src.getRazonSocial());
|
||||||
|
dst.setCreatedAt(Instant.now());
|
||||||
|
|
||||||
|
dst.setTipoIdentificacionFiscal(src.getTipoIdentificacionFiscal());
|
||||||
|
|
||||||
|
dst.setIdentificacionFiscal(src.getIdentificacionFiscal());
|
||||||
|
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
package com.imprimelibros.erp.facturacion.repo;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import com.imprimelibros.erp.facturacion.FacturaDireccion;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface FacturaDireccionRepository extends JpaRepository<FacturaDireccion, Long> {
|
||||||
|
|
||||||
|
List<FacturaDireccion> findByFacturaId(Long facturaId);
|
||||||
|
|
||||||
|
Optional<FacturaDireccion> findFirstByFacturaIdOrderByIdAsc(Long facturaId);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,21 @@
|
|||||||
package com.imprimelibros.erp.facturacion.repo;
|
package com.imprimelibros.erp.facturacion.repo;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.facturacion.EstadoFactura;
|
||||||
|
import com.imprimelibros.erp.facturacion.EstadoPagoFactura;
|
||||||
import com.imprimelibros.erp.facturacion.Factura;
|
import com.imprimelibros.erp.facturacion.Factura;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface FacturaRepository extends JpaRepository<Factura, Long> {
|
public interface FacturaRepository extends JpaRepository<Factura, Long>, JpaSpecificationExecutor<Factura> {
|
||||||
Optional<Factura> findByNumeroFactura(String numeroFactura);
|
Optional<Factura> findByNumeroFactura(String numeroFactura);
|
||||||
|
Factura findByPedidoId(Long pedidoId);
|
||||||
|
List<Factura> findByClienteIdAndEstadoAndEstadoPagoAndSerieId(
|
||||||
|
Long clienteId,
|
||||||
|
EstadoFactura estado,
|
||||||
|
EstadoPagoFactura estadoPago,
|
||||||
|
Long serieId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,19 +4,29 @@ import com.imprimelibros.erp.facturacion.SerieFactura;
|
|||||||
import com.imprimelibros.erp.facturacion.TipoSerieFactura;
|
import com.imprimelibros.erp.facturacion.TipoSerieFactura;
|
||||||
import org.springframework.data.jpa.repository.*;
|
import org.springframework.data.jpa.repository.*;
|
||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
|
||||||
|
|
||||||
import jakarta.persistence.LockModeType;
|
import jakarta.persistence.LockModeType;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface SerieFacturaRepository extends JpaRepository<SerieFactura, Long>, JpaSpecificationExecutor<SerieFactura> {
|
public interface SerieFacturaRepository
|
||||||
|
extends JpaRepository<SerieFactura, Long>, JpaSpecificationExecutor<SerieFactura> {
|
||||||
|
|
||||||
Optional<SerieFactura> findByTipo(TipoSerieFactura tipo);
|
Optional<SerieFactura> findByTipo(TipoSerieFactura tipo);
|
||||||
|
|
||||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
||||||
@Query("select s from SerieFactura s where s.id = :id")
|
@Query("select s from SerieFactura s where s.id = :id")
|
||||||
Optional<SerieFactura> findByIdForUpdate(@Param("id") Long id);
|
Optional<SerieFactura> findByIdForUpdate(@Param("id") Long id);
|
||||||
|
|
||||||
List<SerieFactura> findAllByDeletedAtIsNullOrderByNombreSerieAsc();
|
List<SerieFactura> findAllByDeletedAtIsNullOrderByNombreSerieAsc();
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
select s
|
||||||
|
from SerieFactura s
|
||||||
|
where s.deletedAt is null
|
||||||
|
and (:query is null or :query = '' or lower(s.nombreSerie) like lower(concat('%', :query, '%')))
|
||||||
|
order by s.nombreSerie
|
||||||
|
""")
|
||||||
|
List<SerieFactura> searchForSelectSeriesFacturacion(@Param("query") String query);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,49 @@
|
|||||||
package com.imprimelibros.erp.facturacion.service;
|
package com.imprimelibros.erp.facturacion.service;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.common.Utils;
|
||||||
|
import com.imprimelibros.erp.configurationERP.VariableService;
|
||||||
import com.imprimelibros.erp.facturacion.*;
|
import com.imprimelibros.erp.facturacion.*;
|
||||||
|
import com.imprimelibros.erp.facturacion.dto.DireccionFacturacionDto;
|
||||||
|
import com.imprimelibros.erp.facturacion.dto.FacturaDireccionMapper;
|
||||||
|
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.FacturaDireccionRepository;
|
||||||
|
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.PedidoDireccion;
|
||||||
|
import com.imprimelibros.erp.pedidos.PedidoLinea;
|
||||||
|
import com.imprimelibros.erp.pedidos.PedidoLineaRepository;
|
||||||
|
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||||
|
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||||
|
import com.imprimelibros.erp.users.User;
|
||||||
|
import com.imprimelibros.erp.users.UserService;
|
||||||
|
import com.imprimelibros.erp.direcciones.Direccion;
|
||||||
|
import com.imprimelibros.erp.direcciones.DireccionRepository;
|
||||||
|
|
||||||
import jakarta.persistence.EntityNotFoundException;
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
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.text.Collator;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@ -20,21 +52,253 @@ 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 DireccionRepository direccionRepo;
|
||||||
|
private final PedidoLineaRepository pedidoLineaRepo;
|
||||||
|
private final UserService userService;
|
||||||
|
private final Utils utils;
|
||||||
|
private final MessageSource messageSource;
|
||||||
|
private final PedidoService pedidoService;
|
||||||
|
private final VariableService variableService;
|
||||||
|
|
||||||
public FacturacionService(
|
public FacturacionService(
|
||||||
FacturaRepository facturaRepo,
|
FacturaRepository facturaRepo,
|
||||||
|
FacturaLineaRepository lineaFacturaRepository,
|
||||||
SerieFacturaRepository serieRepo,
|
SerieFacturaRepository serieRepo,
|
||||||
FacturaPagoRepository pagoRepo
|
FacturaPagoRepository pagoRepo,
|
||||||
) {
|
DireccionRepository direccionRepo,
|
||||||
|
PedidoLineaRepository pedidoLineaRepo,
|
||||||
|
UserService userService,
|
||||||
|
Utils utils,
|
||||||
|
MessageSource messageSource,
|
||||||
|
PedidoService pedidoService,
|
||||||
|
VariableService variableService) {
|
||||||
this.facturaRepo = facturaRepo;
|
this.facturaRepo = facturaRepo;
|
||||||
|
this.lineaFacturaRepository = lineaFacturaRepository;
|
||||||
this.serieRepo = serieRepo;
|
this.serieRepo = serieRepo;
|
||||||
this.pagoRepo = pagoRepo;
|
this.pagoRepo = pagoRepo;
|
||||||
|
this.direccionRepo = direccionRepo;
|
||||||
|
this.pedidoLineaRepo = pedidoLineaRepo;
|
||||||
|
this.userService = userService;
|
||||||
|
this.utils = utils;
|
||||||
|
this.messageSource = messageSource;
|
||||||
|
this.pedidoService = pedidoService;
|
||||||
|
this.variableService = variableService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SerieFactura getDefaultSerieFactura() {
|
||||||
|
|
||||||
|
Long defaultSerieId = variableService.getValorEntero("serie_facturacion_default").longValue();
|
||||||
|
SerieFactura serie = serieRepo.findById(defaultSerieId).orElse(null);
|
||||||
|
if (serie == null) {
|
||||||
|
throw new IllegalStateException("No hay ninguna serie de facturación configurada.");
|
||||||
|
}
|
||||||
|
return serie;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Factura getFactura(Long facturaId) {
|
||||||
|
return facturaRepo.findById(facturaId)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getFacturaIdFromPedidoId(Long pedidoId) {
|
||||||
|
Factura factura = facturaRepo.findByPedidoId(pedidoId);
|
||||||
|
if (factura == null) {
|
||||||
|
throw new EntityNotFoundException("Factura no encontrada para el pedido: " + pedidoId);
|
||||||
|
}
|
||||||
|
return factura.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// Nueva factura
|
||||||
|
// -----------------------
|
||||||
|
@Transactional
|
||||||
|
public Factura crearNuevaFacturaAuto(Pedido pedido, SerieFactura serie, TipoPago tipoPago, Locale locale) {
|
||||||
|
|
||||||
|
Factura factura = new Factura();
|
||||||
|
factura.setCliente(pedido.getCreatedBy());
|
||||||
|
factura.setCreatedAt(Instant.now());
|
||||||
|
factura.setUpdatedAt(Instant.now());
|
||||||
|
Boolean pedidoPendientePago = false;
|
||||||
|
List<PedidoLinea> lineasPedido = pedidoLineaRepo.findByPedidoId(pedido.getId());
|
||||||
|
for (PedidoLinea lineaPedido : lineasPedido) {
|
||||||
|
if (lineaPedido.getEstado() == PedidoLinea.Estado.pendiente_pago) {
|
||||||
|
pedidoPendientePago = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
factura.setEstado(pedidoPendientePago ? EstadoFactura.borrador : EstadoFactura.validada);
|
||||||
|
factura.setEstadoPago(pedidoPendientePago ? EstadoPagoFactura.pendiente : EstadoPagoFactura.pagada);
|
||||||
|
factura.setTipoPago(pedidoPendientePago ? TipoPago.otros : tipoPago);
|
||||||
|
factura.setPedidoId(pedido.getId());
|
||||||
|
factura.setSerie(serie);
|
||||||
|
factura.setNumeroFactura(this.getNumberFactura(serie));
|
||||||
|
factura.setFechaEmision(LocalDateTime.now());
|
||||||
|
factura.setBaseImponible(BigDecimal.valueOf(pedido.getBase()).setScale(2, RoundingMode.HALF_UP));
|
||||||
|
factura.setIva4(BigDecimal.valueOf(pedido.getIva4()).setScale(2, RoundingMode.HALF_UP));
|
||||||
|
factura.setIva21(BigDecimal.valueOf(pedido.getIva21()).setScale(2, RoundingMode.HALF_UP));
|
||||||
|
factura.setTotalFactura(BigDecimal.valueOf(pedido.getTotal()).setScale(2, RoundingMode.HALF_UP));
|
||||||
|
factura.setTotalPagado(BigDecimal.valueOf(pedido.getTotal()).setScale(2, RoundingMode.HALF_UP));
|
||||||
|
// rellenar lineas
|
||||||
|
List<FacturaLinea> lineasFactura = new ArrayList<>();
|
||||||
|
for (PedidoLinea lineaPedido : lineasPedido) {
|
||||||
|
Presupuesto p = lineaPedido.getPresupuesto();
|
||||||
|
FacturaLinea lineaFactura = new FacturaLinea();
|
||||||
|
lineaFactura.setDescripcion(this.obtenerLineaFactura(lineaPedido, locale));
|
||||||
|
lineaFactura.setCantidad(p.getSelectedTirada());
|
||||||
|
lineaFactura.setBaseLinea(p.getBaseImponible());
|
||||||
|
lineaFactura.setIva4Linea(p.getIvaImporte4());
|
||||||
|
lineaFactura.setIva21Linea(p.getIvaImporte21());
|
||||||
|
lineaFactura.setTotalLinea(p.getTotalConIva());
|
||||||
|
lineaFactura.setCreatedBy(p.getUser());
|
||||||
|
lineaFactura.setFactura(factura);
|
||||||
|
lineasFactura.add(lineaFactura);
|
||||||
|
}
|
||||||
|
if(pedido.getEnvio() > 0){
|
||||||
|
FacturaLinea lineaEnvio = new FacturaLinea();
|
||||||
|
lineaEnvio.setDescripcion(messageSource.getMessage("facturas.lineas.gastos-envio", null, "Gastos de envío", locale));
|
||||||
|
lineaEnvio.setCantidad(1);
|
||||||
|
BigDecimal baseEnvio = BigDecimal.valueOf(pedido.getEnvio()).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
lineaEnvio.setBaseLinea(baseEnvio);
|
||||||
|
BigDecimal iva21Envio = baseEnvio.multiply(BigDecimal.valueOf(0.21)).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
lineaEnvio.setIva21Linea(iva21Envio);
|
||||||
|
lineaEnvio.setIva4Linea(BigDecimal.ZERO);
|
||||||
|
lineaEnvio.setTotalLinea(baseEnvio.add(iva21Envio));
|
||||||
|
lineaEnvio.setCreatedBy(pedido.getCreatedBy());
|
||||||
|
lineaEnvio.setCreatedAt(Instant.now());
|
||||||
|
lineaEnvio.setFactura(factura);
|
||||||
|
lineasFactura.add(lineaEnvio);
|
||||||
|
}
|
||||||
|
PedidoDireccion direccionPedido = pedidoService.getDireccionFacturacionPedido(pedido.getId());
|
||||||
|
if(direccionPedido == null){
|
||||||
|
throw new IllegalStateException("El pedido no tiene una dirección de facturación asociada.");
|
||||||
|
}
|
||||||
|
FacturaDireccion fd = FacturaDireccionMapper.fromPedidoDireccion(direccionPedido);
|
||||||
|
|
||||||
|
factura.addDireccion(fd);
|
||||||
|
factura.setLineas(lineasFactura);
|
||||||
|
|
||||||
|
factura = facturaRepo.save(factura);
|
||||||
|
|
||||||
|
if (pedidoPendientePago) {
|
||||||
|
return factura;
|
||||||
|
}
|
||||||
|
FacturaPago pago = new FacturaPago();
|
||||||
|
pago.setMetodoPago(tipoPago);
|
||||||
|
pago.setCantidadPagada(factura.getTotalFactura());
|
||||||
|
pago.setFechaPago(LocalDateTime.now());
|
||||||
|
pago.setFactura(factura);
|
||||||
|
pago.setCreatedBy(pedido.getCreatedBy());
|
||||||
|
pago.setCreatedAt(Instant.now());
|
||||||
|
pagoRepo.save(pago);
|
||||||
|
|
||||||
|
return factura;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Factura crearNuevaFactura(Long userId, Long serieId, Long direccionId, Long facturaRectificadaId) {
|
||||||
|
User cliente = userService.findById(userId);
|
||||||
|
if (cliente == null) {
|
||||||
|
throw new EntityNotFoundException("Cliente no encontrado: " + userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
SerieFactura serie = serieRepo.findById(serieId)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + serieId));
|
||||||
|
|
||||||
|
Factura factura = new Factura();
|
||||||
|
factura.setCliente(cliente);
|
||||||
|
factura.setPedidoId(null);
|
||||||
|
factura.setSerie(serie);
|
||||||
|
factura.setEstado(EstadoFactura.borrador);
|
||||||
|
factura.setEstadoPago(EstadoPagoFactura.pendiente);
|
||||||
|
factura.setFechaEmision(LocalDateTime.now());
|
||||||
|
factura.setCreatedAt(Instant.now());
|
||||||
|
factura.setUpdatedAt(Instant.now());
|
||||||
|
factura.setNumeroFactura(null);
|
||||||
|
factura.setBaseImponible(BigDecimal.ZERO);
|
||||||
|
factura.setIva4(BigDecimal.ZERO);
|
||||||
|
factura.setIva21(BigDecimal.ZERO);
|
||||||
|
factura.setTotalFactura(BigDecimal.ZERO);
|
||||||
|
factura.setTotalPagado(BigDecimal.ZERO);
|
||||||
|
factura.setLineas(new ArrayList<>());
|
||||||
|
factura.setPagos(new ArrayList<>());
|
||||||
|
Direccion direccion = direccionRepo.findById(direccionId)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Dirección de factura no encontrada: " + direccionId));
|
||||||
|
FacturaDireccion facturaDireccion = FacturaDireccionMapper.fromDireccion(direccion);
|
||||||
|
factura.addDireccion(facturaDireccion);
|
||||||
|
if(facturaRectificadaId != null){
|
||||||
|
Factura facturaRectificada = facturaRepo.findById(facturaRectificadaId)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Factura rectificada no encontrada: " + facturaRectificadaId));
|
||||||
|
factura.setFacturaRectificativa(facturaRectificada);
|
||||||
|
facturaRectificada.setFacturaRectificada(factura);
|
||||||
|
}
|
||||||
|
return facturaRepo.save(factura);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------
|
// -----------------------
|
||||||
// Estado / Numeración
|
// Estado / Numeración
|
||||||
// -----------------------
|
// -----------------------
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public String getNumberFactura(SerieFactura serie) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
long next = (serie.getNumeroActual() == null) ? 1L : serie.getNumeroActual();
|
||||||
|
String numeroFactura = buildNumeroFactura(serie.getPrefijo(), next);
|
||||||
|
|
||||||
|
// Incrementar contador para la siguiente
|
||||||
|
serie.setNumeroActual(next + 1);
|
||||||
|
serieRepo.save(serie);
|
||||||
|
|
||||||
|
return numeroFactura;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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());
|
||||||
|
|
||||||
|
}
|
||||||
|
upsertDireccionFacturacion(facturaId, 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)
|
||||||
@ -56,7 +320,8 @@ public class FacturacionService {
|
|||||||
// Si ya tiene numero_factura, no reservamos otro
|
// Si ya tiene numero_factura, no reservamos otro
|
||||||
if (factura.getNumeroFactura() == null || factura.getNumeroFactura().isBlank()) {
|
if (factura.getNumeroFactura() == null || factura.getNumeroFactura().isBlank()) {
|
||||||
SerieFactura serieLocked = serieRepo.findByIdForUpdate(factura.getSerie().getId())
|
SerieFactura serieLocked = serieRepo.findByIdForUpdate(factura.getSerie().getId())
|
||||||
.orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + factura.getSerie().getId()));
|
.orElseThrow(
|
||||||
|
() -> new EntityNotFoundException("Serie no encontrada: " + factura.getSerie().getId()));
|
||||||
|
|
||||||
long next = (serieLocked.getNumeroActual() == null) ? 1L : serieLocked.getNumeroActual();
|
long next = (serieLocked.getNumeroActual() == null) ? 1L : serieLocked.getNumeroActual();
|
||||||
String numeroFactura = buildNumeroFactura(serieLocked.getPrefijo(), next);
|
String numeroFactura = buildNumeroFactura(serieLocked.getPrefijo(), next);
|
||||||
@ -86,15 +351,96 @@ public class FacturacionService {
|
|||||||
return facturaRepo.save(factura);
|
return facturaRepo.save(factura);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Boolean upsertDireccionFacturacion(Long facturaId, DireccionFacturacionDto direccionData) {
|
||||||
|
try {
|
||||||
|
Factura factura = facturaRepo.findById(facturaId)
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||||
|
|
||||||
|
// ✅ Solo editable si borrador (tu regla actual para cabecera/dirección)
|
||||||
|
if (factura.getEstado() != EstadoFactura.borrador) {
|
||||||
|
throw new IllegalStateException("Solo se puede guardar dirección en borrador.");
|
||||||
|
}
|
||||||
|
|
||||||
|
factura.getDirecciones().clear();
|
||||||
|
factura.addDireccion(direccionData.toFacturaDireccion());
|
||||||
|
facturaRepo.save(factura);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getForSelectFacturasRectificables(String q1, String q2, Long userId) {
|
||||||
|
try {
|
||||||
|
String search = Optional.ofNullable(q1).orElse(q2);
|
||||||
|
if (search != null) {
|
||||||
|
search = search.trim();
|
||||||
|
}
|
||||||
|
final String q = (search == null || search.isEmpty())
|
||||||
|
? null
|
||||||
|
: search.toLowerCase();
|
||||||
|
|
||||||
|
List<Factura> all = facturaRepo.findByClienteIdAndEstadoAndEstadoPagoAndSerieId(
|
||||||
|
userId,
|
||||||
|
EstadoFactura.validada,
|
||||||
|
EstadoPagoFactura.pagada,
|
||||||
|
variableService.getValorEntero("serie_facturacion_default").longValue());
|
||||||
|
|
||||||
|
// Mapear a opciones id/text con i18n y filtrar por búsqueda si llega
|
||||||
|
List<Map<String, String>> options = all.stream()
|
||||||
|
.map(f -> {
|
||||||
|
String id = f.getId().toString();
|
||||||
|
String text = f.getNumeroFactura();
|
||||||
|
Map<String, String> m = new HashMap<>();
|
||||||
|
m.put("id", id); // lo normal en Select2: id = valor que guardarás (code3)
|
||||||
|
m.put("text", text); // texto mostrado, i18n con fallback a keyword
|
||||||
|
return m;
|
||||||
|
})
|
||||||
|
.filter(opt -> {
|
||||||
|
if (q == null || q.isEmpty())
|
||||||
|
return true;
|
||||||
|
String text = opt.get("text").toLowerCase();
|
||||||
|
return text.contains(q);
|
||||||
|
})
|
||||||
|
.sorted(Comparator.comparing(m -> m.get("text"), Collator.getInstance()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Estructura Select2
|
||||||
|
Map<String, Object> resp = new HashMap<>();
|
||||||
|
resp.put("results", options);
|
||||||
|
return resp;
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return Map.of("results", List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String buildNumeroFactura(String prefijo, long numero) {
|
private String buildNumeroFactura(String prefijo, long numero) {
|
||||||
String pref = (prefijo == null) ? "" : prefijo.trim();
|
String pref = (prefijo == null) ? "" : prefijo.trim();
|
||||||
String num = String.format("%07d", numero);
|
String num = String.format("%05d", numero);
|
||||||
return pref.isBlank() ? num : (pref + "-" + num);
|
return pref.isBlank() ? num : (pref + " " + num + "/" + LocalDate.now().getYear());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// -----------------------
|
// -----------------------
|
||||||
// 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) {
|
||||||
@ -118,29 +464,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);
|
||||||
@ -169,7 +501,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));
|
||||||
|
|
||||||
@ -178,6 +510,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()
|
||||||
@ -190,8 +524,10 @@ 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());
|
||||||
// El tipo_pago de la factura: si tiene un pago, lo reflejamos (último pago manda)
|
pago.setUpdatedBy(Utils.currentUser(principal));
|
||||||
|
// El tipo_pago de la factura: si tiene un pago, lo reflejamos (último pago
|
||||||
|
// manda)
|
||||||
factura.setTipoPago(dto.getMetodoPago());
|
factura.setTipoPago(dto.getMetodoPago());
|
||||||
|
|
||||||
recalcularTotales(factura);
|
recalcularTotales(factura);
|
||||||
@ -199,14 +535,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);
|
||||||
@ -248,13 +588,16 @@ 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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
factura.setTotalPagado(scale2(pagado));
|
factura.setTotalPagado(scale2(pagado));
|
||||||
|
|
||||||
// estado_pago
|
// estado_pago
|
||||||
// - cancelada: si la factura está marcada como cancelada manualmente (aquí NO lo hacemos automático)
|
// - cancelada: si la factura está marcada como cancelada manualmente (aquí NO
|
||||||
|
// lo hacemos automático)
|
||||||
// - pagada: si total_pagado >= total_factura y total_factura > 0
|
// - pagada: si total_pagado >= total_factura y total_factura > 0
|
||||||
// - pendiente: resto
|
// - pendiente: resto
|
||||||
if (factura.getEstadoPago() == EstadoPagoFactura.cancelada) {
|
if (factura.getEstadoPago() == EstadoPagoFactura.cancelada) {
|
||||||
@ -277,4 +620,106 @@ public class FacturacionService {
|
|||||||
private static BigDecimal scale2(BigDecimal v) {
|
private static BigDecimal scale2(BigDecimal v) {
|
||||||
return (v == null ? BigDecimal.ZERO : v).setScale(2, RoundingMode.HALF_UP);
|
return (v == null ? BigDecimal.ZERO : v).setScale(2, RoundingMode.HALF_UP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String obtenerLineaFactura(PedidoLinea lineaPedido, Locale locale) {
|
||||||
|
|
||||||
|
Map<String, Object> specs = utils.getTextoPresupuesto(lineaPedido.getPresupuesto(), locale);
|
||||||
|
|
||||||
|
StringBuilder html = new StringBuilder();
|
||||||
|
html.append("<div class=\"specs-wrapper align-with-text \">")
|
||||||
|
.append("<div class=\"specs\">");
|
||||||
|
|
||||||
|
if (specs == null) {
|
||||||
|
return "<div></div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Líneas del presupuesto (HTML)
|
||||||
|
Object lineasObj = specs.get("lineas");
|
||||||
|
if (lineasObj instanceof List<?> lineasList) {
|
||||||
|
for (Object o : lineasList) {
|
||||||
|
if (!(o instanceof Map<?, ?> m))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Object descObj = m.get("descripcion");
|
||||||
|
String descripcionHtml = descObj != null ? descObj.toString() : "";
|
||||||
|
if (descripcionHtml.isBlank())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
html.append("<div class=\"spec-row mb-1\">")
|
||||||
|
.append("<span class=\"spec-label\">")
|
||||||
|
.append(descripcionHtml) // OJO: esto es HTML (como th:utext)
|
||||||
|
.append("</span>")
|
||||||
|
.append("</div>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Servicios adicionales (texto)
|
||||||
|
Object serviciosObj = specs.get("servicios");
|
||||||
|
String servicios = (serviciosObj != null) ? serviciosObj.toString().trim() : "";
|
||||||
|
if (!servicios.isBlank()) {
|
||||||
|
String label = messageSource.getMessage("pdf.servicios-adicionales", null, "Servicios adicionales", locale);
|
||||||
|
html.append("<div class=\"spec-row mb-1\">")
|
||||||
|
.append("<span>").append(escapeHtml(label)).append("</span>")
|
||||||
|
.append("<span class=\"spec-label\">").append(escapeHtml(servicios)).append("</span>")
|
||||||
|
.append("</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Datos de maquetación (HTML)
|
||||||
|
Object datosMaqObj = specs.get("datosMaquetacion");
|
||||||
|
if (datosMaqObj != null && !datosMaqObj.toString().isBlank()) {
|
||||||
|
String label = messageSource.getMessage("pdf.datos-maquetacion", null, "Datos de maquetación:", locale);
|
||||||
|
html.append("<div class=\"spec-row mb-1\">")
|
||||||
|
.append("<span>").append(escapeHtml(label)).append("</span>")
|
||||||
|
.append("<span class=\"spec-label\">")
|
||||||
|
.append(datosMaqObj) // HTML (como th:utext)
|
||||||
|
.append("</span>")
|
||||||
|
.append("</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Datos de marcapáginas (HTML)
|
||||||
|
Object datosMarcaObj = specs.get("datosMarcapaginas");
|
||||||
|
if (datosMarcaObj != null && !datosMarcaObj.toString().isBlank()) {
|
||||||
|
String label = messageSource.getMessage("pdf.datos-marcapaginas", null, "Datos de marcapáginas:", locale);
|
||||||
|
html.append("<div class=\"spec-row mb-1\">")
|
||||||
|
.append("<span>").append(escapeHtml(label)).append("</span>")
|
||||||
|
.append("<span class=\"spec-label\">")
|
||||||
|
.append(datosMarcaObj) // HTML (como th:utext)
|
||||||
|
.append("</span>")
|
||||||
|
.append("</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
html.append("</div></div>");
|
||||||
|
return html.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape mínimo para texto plano (equivalente a th:text).
|
||||||
|
* No lo uses para fragmentos que ya son HTML (th:utext).
|
||||||
|
*/
|
||||||
|
private static String escapeHtml(String s) {
|
||||||
|
if (s == null)
|
||||||
|
return "";
|
||||||
|
return s.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,13 +7,18 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
|
|
||||||
import com.imprimelibros.erp.configurationERP.VariableService;
|
import com.imprimelibros.erp.configurationERP.VariableService;
|
||||||
import com.imprimelibros.erp.i18n.TranslationService;
|
import com.imprimelibros.erp.i18n.TranslationService;
|
||||||
|
import com.imprimelibros.erp.pedidos.Pedido;
|
||||||
|
import com.imprimelibros.erp.pedidos.PedidoRepository;
|
||||||
|
|
||||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.Instant;
|
||||||
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 com.imprimelibros.erp.common.Utils;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
public class HomeController {
|
public class HomeController {
|
||||||
@ -22,9 +27,10 @@ public class HomeController {
|
|||||||
private TranslationService translationService;
|
private TranslationService translationService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private VariableService variableService;
|
private VariableService variableService;
|
||||||
|
@Autowired PedidoRepository pedidoRepository;
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
public String index(Model model, Authentication authentication, Locale locale) {
|
public String index(Model model, Authentication authentication, Principal principal,Locale locale) {
|
||||||
|
|
||||||
boolean isAuthenticated = authentication != null && authentication.isAuthenticated()
|
boolean isAuthenticated = authentication != null && authentication.isAuthenticated()
|
||||||
&& !(authentication instanceof AnonymousAuthenticationToken);
|
&& !(authentication instanceof AnonymousAuthenticationToken);
|
||||||
@ -37,7 +43,8 @@ public class HomeController {
|
|||||||
"presupuesto.impresion-cubierta",
|
"presupuesto.impresion-cubierta",
|
||||||
"presupuesto.impresion-cubierta-help",
|
"presupuesto.impresion-cubierta-help",
|
||||||
"presupuesto.iva-reducido",
|
"presupuesto.iva-reducido",
|
||||||
"presupuesto.iva-reducido-descripcion");
|
"presupuesto.iva-reducido-descripcion",
|
||||||
|
"pedido.gasto-anual");
|
||||||
|
|
||||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||||
model.addAttribute("languageBundle", translations);
|
model.addAttribute("languageBundle", translations);
|
||||||
@ -51,6 +58,11 @@ public class HomeController {
|
|||||||
// empty translations for authenticated users
|
// empty translations for authenticated users
|
||||||
Map<String, String> translations = Map.of();
|
Map<String, String> translations = Map.of();
|
||||||
model.addAttribute("languageBundle", translations);
|
model.addAttribute("languageBundle", translations);
|
||||||
|
|
||||||
|
Instant haceUnAno = Instant.now().minusSeconds(365 * 24 * 60 * 60);
|
||||||
|
Long userId = Utils.currentUserId(principal);
|
||||||
|
double totalGastado = pedidoRepository.sumTotalByCreatedByAndCreatedAtAfter(userId, haceUnAno);
|
||||||
|
model.addAttribute("totalGastado", totalGastado);
|
||||||
}
|
}
|
||||||
return "imprimelibros/home/home";
|
return "imprimelibros/home/home";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,6 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.imprimelibros.erp.common.Utils;
|
import com.imprimelibros.erp.common.Utils;
|
||||||
import com.imprimelibros.erp.datatables.DataTable;
|
import com.imprimelibros.erp.datatables.DataTable;
|
||||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||||
|
|||||||
@ -3,6 +3,9 @@ package com.imprimelibros.erp.payments;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.imprimelibros.erp.cart.Cart;
|
import com.imprimelibros.erp.cart.Cart;
|
||||||
import com.imprimelibros.erp.cart.CartService;
|
import com.imprimelibros.erp.cart.CartService;
|
||||||
|
import com.imprimelibros.erp.facturacion.SerieFactura;
|
||||||
|
import com.imprimelibros.erp.facturacion.TipoPago;
|
||||||
|
import com.imprimelibros.erp.facturacion.service.FacturacionService;
|
||||||
import com.imprimelibros.erp.payments.model.*;
|
import com.imprimelibros.erp.payments.model.*;
|
||||||
import com.imprimelibros.erp.payments.repo.PaymentRepository;
|
import com.imprimelibros.erp.payments.repo.PaymentRepository;
|
||||||
import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository;
|
import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository;
|
||||||
@ -14,7 +17,6 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import com.imprimelibros.erp.payments.repo.WebhookEventRepository;
|
import com.imprimelibros.erp.payments.repo.WebhookEventRepository;
|
||||||
import com.imprimelibros.erp.pedidos.Pedido;
|
import com.imprimelibros.erp.pedidos.Pedido;
|
||||||
import com.imprimelibros.erp.pedidos.PedidoLinea;
|
|
||||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@ -33,6 +35,7 @@ public class PaymentService {
|
|||||||
private final ObjectMapper om = new ObjectMapper();
|
private final ObjectMapper om = new ObjectMapper();
|
||||||
private final CartService cartService;
|
private final CartService cartService;
|
||||||
private final PedidoService pedidoService;
|
private final PedidoService pedidoService;
|
||||||
|
private final FacturacionService facturacionService;
|
||||||
|
|
||||||
public PaymentService(PaymentRepository payRepo,
|
public PaymentService(PaymentRepository payRepo,
|
||||||
PaymentTransactionRepository txRepo,
|
PaymentTransactionRepository txRepo,
|
||||||
@ -40,7 +43,8 @@ public class PaymentService {
|
|||||||
RedsysService redsysService,
|
RedsysService redsysService,
|
||||||
WebhookEventRepository webhookEventRepo,
|
WebhookEventRepository webhookEventRepo,
|
||||||
CartService cartService,
|
CartService cartService,
|
||||||
PedidoService pedidoService) {
|
PedidoService pedidoService,
|
||||||
|
FacturacionService facturacionService) {
|
||||||
this.payRepo = payRepo;
|
this.payRepo = payRepo;
|
||||||
this.txRepo = txRepo;
|
this.txRepo = txRepo;
|
||||||
this.refundRepo = refundRepo;
|
this.refundRepo = refundRepo;
|
||||||
@ -48,6 +52,7 @@ public class PaymentService {
|
|||||||
this.webhookEventRepo = webhookEventRepo;
|
this.webhookEventRepo = webhookEventRepo;
|
||||||
this.cartService = cartService;
|
this.cartService = cartService;
|
||||||
this.pedidoService = pedidoService;
|
this.pedidoService = pedidoService;
|
||||||
|
this.facturacionService = facturacionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Payment findFailedPaymentByOrderId(Long orderId) {
|
public Payment findFailedPaymentByOrderId(Long orderId) {
|
||||||
@ -253,6 +258,11 @@ public class PaymentService {
|
|||||||
p.setCapturedAt(LocalDateTime.now());
|
p.setCapturedAt(LocalDateTime.now());
|
||||||
pedidoService.setOrderAsPaid(p.getOrderId());
|
pedidoService.setOrderAsPaid(p.getOrderId());
|
||||||
|
|
||||||
|
Pedido pedido = pedidoService.getPedidoById(p.getOrderId());
|
||||||
|
SerieFactura serie = facturacionService.getDefaultSerieFactura();
|
||||||
|
|
||||||
|
facturacionService.crearNuevaFacturaAuto(pedido, serie, notif.isBizum() ? TipoPago.tpv_bizum : TipoPago.tpv_tarjeta, locale);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
p.setStatus(PaymentStatus.failed);
|
p.setStatus(PaymentStatus.failed);
|
||||||
p.setFailedAt(LocalDateTime.now());
|
p.setFailedAt(LocalDateTime.now());
|
||||||
@ -452,6 +462,11 @@ public class PaymentService {
|
|||||||
// 4) Procesar el pedido asociado al carrito (si existe) o marcar el pedido como pagado
|
// 4) Procesar el pedido asociado al carrito (si existe) o marcar el pedido como pagado
|
||||||
if(p.getOrderId() != null) {
|
if(p.getOrderId() != null) {
|
||||||
pedidoService.setOrderAsPaid(p.getOrderId());
|
pedidoService.setOrderAsPaid(p.getOrderId());
|
||||||
|
|
||||||
|
Pedido pedido = pedidoService.getPedidoById(p.getOrderId());
|
||||||
|
SerieFactura serie = facturacionService.getDefaultSerieFactura();
|
||||||
|
|
||||||
|
facturacionService.crearNuevaFacturaAuto(pedido, serie, TipoPago.transferencia, locale);
|
||||||
}
|
}
|
||||||
/*else if (cartId != null) {
|
/*else if (cartId != null) {
|
||||||
// Se procesa el pedido dejando el estado calculado en processOrder
|
// Se procesa el pedido dejando el estado calculado en processOrder
|
||||||
|
|||||||
@ -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) {
|
||||||
@ -122,27 +131,6 @@ public class PdfService {
|
|||||||
|
|
||||||
model.put("titulo", presupuesto.getTitulo());
|
model.put("titulo", presupuesto.getTitulo());
|
||||||
|
|
||||||
/*
|
|
||||||
* Map<String, Object> resumen = presupuestoService.getTextosResumen(
|
|
||||||
* presupuesto, null, model, model, null)
|
|
||||||
*/
|
|
||||||
model.put("lineas", List.of(
|
|
||||||
Map.of("descripcion", "Impresión interior B/N offset 80 g",
|
|
||||||
"meta", "300 páginas · tinta negra · papel 80 g",
|
|
||||||
"uds", 1000,
|
|
||||||
"precio", 2.15,
|
|
||||||
"dto", 0,
|
|
||||||
"importe", 2150.0),
|
|
||||||
Map.of("descripcion", "Cubierta color 300 g laminado mate",
|
|
||||||
"meta", "Lomo 15 mm · 4/0 · laminado mate",
|
|
||||||
"uds", 1000,
|
|
||||||
"precio", 0.38,
|
|
||||||
"dto", 5.0,
|
|
||||||
"importe", 361.0)));
|
|
||||||
|
|
||||||
model.put("servicios", List.of(
|
|
||||||
Map.of("descripcion", "Transporte península", "unidades", 1, "precio", 90.00)));
|
|
||||||
|
|
||||||
Map<String, Object> specs = utils.getTextoPresupuesto(presupuesto, locale);
|
Map<String, Object> specs = utils.getTextoPresupuesto(presupuesto, locale);
|
||||||
model.put("specs", specs);
|
model.put("specs", specs);
|
||||||
|
|
||||||
@ -202,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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,72 @@
|
|||||||
|
package com.imprimelibros.erp.pedidos;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PedidoEstadoService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(PedidoEstadoService.class);
|
||||||
|
|
||||||
|
private final PedidoLineaRepository pedidoLineaRepository;
|
||||||
|
private final PedidoService pedidoService;
|
||||||
|
|
||||||
|
public PedidoEstadoService(PedidoLineaRepository pedidoLineaRepository, PedidoService pedidoService) {
|
||||||
|
this.pedidoLineaRepository = pedidoLineaRepository;
|
||||||
|
this.pedidoService = pedidoService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ejecuta cada noche a las 4:00 AM
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
// test @Scheduled(cron = "0 * * * * *")
|
||||||
|
@Scheduled(cron = "0 0 4 * * *")
|
||||||
|
public void actualizarEstadosPedidos() {
|
||||||
|
|
||||||
|
log.info("JOB actualizarEstadosPedidos iniciado");
|
||||||
|
|
||||||
|
List<PedidoLinea> pedidosLineas = pedidoLineaRepository.findPedidosLineasParaActualizarEstado();
|
||||||
|
|
||||||
|
log.info("Pedidos líneas a procesar: {}", pedidosLineas.size());
|
||||||
|
|
||||||
|
|
||||||
|
for (PedidoLinea linea : pedidosLineas) {
|
||||||
|
|
||||||
|
log.info("Actualizando estado pedidoLineaId={}", linea.getId());
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, Object> resultado = pedidoService.actualizarEstado(linea.getId(), Locale.getDefault());
|
||||||
|
|
||||||
|
if (!Boolean.TRUE.equals(resultado.get("success"))) {
|
||||||
|
log.error("Error al actualizar estado. pedidoLineaId={} message={}",
|
||||||
|
linea.getId(), resultado.get("message"));
|
||||||
|
} else {
|
||||||
|
String msg = String.valueOf(resultado.get("message"));
|
||||||
|
if (msg != null && msg.contains("Orden de trabajo no encontrada")) {
|
||||||
|
log.warn("OT no encontrada. pedidoLineaId={} message={}", linea.getId(), msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Excepción actualizando estado. pedidoLineaId={}", linea.getId(), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// rate limit / delay
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
log.error("Job interrumpido mientras dormía (rate limit).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,7 +20,8 @@ public class PedidoLinea {
|
|||||||
ferro_cliente("pedido.estado.ferro_cliente", 8),
|
ferro_cliente("pedido.estado.ferro_cliente", 8),
|
||||||
produccion("pedido.estado.produccion", 9),
|
produccion("pedido.estado.produccion", 9),
|
||||||
terminado("pedido.estado.terminado", 10),
|
terminado("pedido.estado.terminado", 10),
|
||||||
cancelado("pedido.estado.cancelado", 11);
|
enviado("pedido.estado.enviado", 11),
|
||||||
|
cancelado("pedido.estado.cancelado", 12);
|
||||||
|
|
||||||
private final String messageKey;
|
private final String messageKey;
|
||||||
private final int priority;
|
private final int priority;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.imprimelibros.erp.pedidos;
|
package com.imprimelibros.erp.pedidos;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -9,7 +10,25 @@ import java.util.List;
|
|||||||
public interface PedidoLineaRepository extends JpaRepository<PedidoLinea, Long> {
|
public interface PedidoLineaRepository extends JpaRepository<PedidoLinea, Long> {
|
||||||
|
|
||||||
List<PedidoLinea> findByPedidoId(Long pedidoId);
|
List<PedidoLinea> findByPedidoId(Long pedidoId);
|
||||||
|
|
||||||
List<PedidoLinea> findByPedidoIdOrderByIdAsc(Long pedidoId);
|
List<PedidoLinea> findByPedidoIdOrderByIdAsc(Long pedidoId);
|
||||||
|
|
||||||
List<PedidoLinea> findByPresupuestoId(Long presupuestoId);
|
List<PedidoLinea> findByPresupuestoId(Long presupuestoId);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT pl
|
||||||
|
FROM PedidoLinea pl
|
||||||
|
JOIN pl.presupuesto p
|
||||||
|
WHERE pl.estadoManual = false
|
||||||
|
AND pl.estado IN (
|
||||||
|
'haciendo_ferro',
|
||||||
|
'esperando_aceptacion_ferro',
|
||||||
|
'produccion',
|
||||||
|
'terminado'
|
||||||
|
)
|
||||||
|
AND p.proveedor = 'Safekat'
|
||||||
|
AND p.proveedorRef1 IS NOT NULL
|
||||||
|
AND p.proveedorRef2 IS NOT NULL
|
||||||
|
""")
|
||||||
|
List<PedidoLinea> findPedidosLineasParaActualizarEstado();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,8 +24,12 @@ 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;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.client.HttpClientErrorException;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class PedidoService {
|
public class PedidoService {
|
||||||
|
|
||||||
@ -59,6 +63,14 @@ public class PedidoService {
|
|||||||
this.messageSource = messageSource;
|
this.messageSource = messageSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Pedido getPedidoById(Long pedidoId) {
|
||||||
|
return pedidoRepository.findById(pedidoId).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PedidoDireccion getPedidoDireccionFacturacionByPedidoId(Long pedidoId) {
|
||||||
|
return pedidoDireccionRepository.findByPedidoIdAndFacturacionTrue(pedidoId);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Pedido crearPedido(
|
public Pedido crearPedido(
|
||||||
Long cartId,
|
Long cartId,
|
||||||
@ -86,10 +98,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();
|
||||||
@ -99,7 +112,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));
|
||||||
@ -107,7 +120,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);
|
||||||
@ -177,6 +189,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) {
|
||||||
|
|
||||||
|
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) {
|
||||||
@ -260,43 +302,94 @@ public class PedidoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> actualizarEstado(Long pedidoLineaId, Locale locale) {
|
public Map<String, Object> actualizarEstado(Long pedidoLineaId, Locale locale) {
|
||||||
|
|
||||||
PedidoLinea pedidoLinea = pedidoLineaRepository.findById(pedidoLineaId).orElse(null);
|
PedidoLinea pedidoLinea = pedidoLineaRepository.findById(pedidoLineaId).orElse(null);
|
||||||
if (pedidoLinea == null) {
|
if (pedidoLinea == null) {
|
||||||
return Map.of("success", false,
|
return Map.of(
|
||||||
|
"success", false,
|
||||||
"message", messageSource.getMessage("pedido.errors.linea-not-found", null, locale));
|
"message", messageSource.getMessage("pedido.errors.linea-not-found", null, locale));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pedidoLinea.getEstado().getPriority() >= PedidoLinea.Estado.haciendo_ferro.getPriority() &&
|
PedidoLinea.Estado estadoOld = pedidoLinea.getEstado();
|
||||||
pedidoLinea.getEstado().getPriority() < PedidoLinea.Estado.terminado.getPriority()) {
|
if (estadoOld == null) {
|
||||||
PedidoLinea.Estado estadoOld = pedidoLinea.getEstado();
|
return Map.of(
|
||||||
Map<String, Object> result = skApiClient.checkPedidoEstado(
|
"success", false,
|
||||||
Long.valueOf(pedidoLinea.getPresupuesto().getProveedorRef2().toString()), locale);
|
"message", messageSource.getMessage("pedido.errors.cannot-update", null, locale));
|
||||||
if (result == null || !result.containsKey("estado")) {
|
}
|
||||||
return Map.of(
|
|
||||||
"success", false,
|
|
||||||
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
|
||||||
}
|
|
||||||
PedidoLinea.Estado estadoSk = PedidoLinea.Estado.valueOf((String) result.get("estado"));
|
|
||||||
if (estadoOld == estadoSk) {
|
|
||||||
return Map.of(
|
|
||||||
"success", true,
|
|
||||||
"state", messageSource.getMessage("pedido.estado." + estadoSk.name(), null, locale),
|
|
||||||
"stateKey", estadoSk.name(),
|
|
||||||
"message", messageSource.getMessage("pedido.success.same-estado", null, locale));
|
|
||||||
}
|
|
||||||
|
|
||||||
pedidoLinea.setEstado(estadoSk);
|
// Rango: >= haciendo_ferro y < enviado
|
||||||
pedidoLineaRepository.save(pedidoLinea);
|
if (estadoOld.getPriority() < PedidoLinea.Estado.haciendo_ferro.getPriority()
|
||||||
|
|| estadoOld.getPriority() >= PedidoLinea.Estado.enviado.getPriority()) {
|
||||||
|
return Map.of(
|
||||||
|
"success", false,
|
||||||
|
"message", messageSource.getMessage("pedido.errors.cannot-update", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
var presupuesto = pedidoLinea.getPresupuesto();
|
||||||
|
if (presupuesto == null || presupuesto.getProveedorRef2() == null) {
|
||||||
|
return Map.of(
|
||||||
|
"success", false,
|
||||||
|
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
Long refExterna;
|
||||||
|
try {
|
||||||
|
refExterna = Long.valueOf(presupuesto.getProveedorRef2().toString());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return Map.of(
|
||||||
|
"success", false,
|
||||||
|
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> result = skApiClient.checkPedidoEstado(refExterna, locale);
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
return Map.of(
|
||||||
|
"success", false,
|
||||||
|
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Boolean.TRUE.equals(result.get("notFound"))) {
|
||||||
|
return Map.of(
|
||||||
|
"success", true,
|
||||||
|
"message", String.valueOf(result.getOrDefault("message", "OT no encontrada (404). Se omite.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Object estadoObj = result.get("estado");
|
||||||
|
if (estadoObj == null) {
|
||||||
|
return Map.of(
|
||||||
|
"success", false,
|
||||||
|
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
String estadoStr = String.valueOf(estadoObj);
|
||||||
|
|
||||||
|
PedidoLinea.Estado estadoSk;
|
||||||
|
try {
|
||||||
|
// si la API devuelve minúsculas tipo "produccion", esto funciona
|
||||||
|
estadoSk = PedidoLinea.Estado.valueOf(estadoStr.trim().toLowerCase());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return Map.of(
|
||||||
|
"success", false,
|
||||||
|
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (estadoOld == estadoSk) {
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"success", true,
|
"success", true,
|
||||||
"state", messageSource.getMessage("pedido.estado." + estadoSk.name(), null, locale),
|
"state", messageSource.getMessage("pedido.estado." + estadoSk.name(), null, locale),
|
||||||
"stateKey", estadoSk.name(),
|
"stateKey", estadoSk.name(),
|
||||||
"message", messageSource.getMessage("pedido.success.estado-actualizado", null, locale));
|
"message", messageSource.getMessage("pedido.success.same-estado", null, locale));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pedidoLinea.setEstado(estadoSk);
|
||||||
|
pedidoLineaRepository.save(pedidoLinea);
|
||||||
|
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"success", false,
|
"success", true,
|
||||||
"message", messageSource.getMessage("pedido.errors.cannot-update", null, locale));
|
"state", messageSource.getMessage("pedido.estado." + estadoSk.name(), null, locale),
|
||||||
|
"stateKey", estadoSk.name(),
|
||||||
|
"message", messageSource.getMessage("pedido.success.estado-actualizado", null, locale));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean markPedidoAsMaquetacionDone(Long pedidoId) {
|
public Boolean markPedidoAsMaquetacionDone(Long pedidoId) {
|
||||||
@ -325,7 +418,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);
|
||||||
}
|
}
|
||||||
@ -356,7 +448,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);
|
||||||
@ -369,7 +460,7 @@ public class PedidoService {
|
|||||||
}
|
}
|
||||||
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
|
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
|
||||||
for (PedidoLinea linea : lineas) {
|
for (PedidoLinea linea : lineas) {
|
||||||
if (linea.getEstado() != PedidoLinea.Estado.terminado) {
|
if (linea.getEstado() != PedidoLinea.Estado.terminado && linea.getEstado() != PedidoLinea.Estado.enviado) {
|
||||||
linea.setEstado(PedidoLinea.Estado.cancelado);
|
linea.setEstado(PedidoLinea.Estado.cancelado);
|
||||||
pedidoLineaRepository.save(linea);
|
pedidoLineaRepository.save(linea);
|
||||||
}
|
}
|
||||||
@ -378,8 +469,6 @@ public class PedidoService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************
|
/***************************
|
||||||
* MÉTODOS PRIVADOS
|
* MÉTODOS PRIVADOS
|
||||||
***************************/
|
***************************/
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import com.imprimelibros.erp.datatables.DataTable;
|
|||||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||||
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||||
|
import com.imprimelibros.erp.facturacion.service.FacturacionService;
|
||||||
import com.imprimelibros.erp.i18n.TranslationService;
|
import com.imprimelibros.erp.i18n.TranslationService;
|
||||||
import com.imprimelibros.erp.paises.PaisesService;
|
import com.imprimelibros.erp.paises.PaisesService;
|
||||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
|
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
|
||||||
@ -52,10 +53,12 @@ public class PedidosController {
|
|||||||
private final PedidoLineaRepository repoPedidoLinea;
|
private final PedidoLineaRepository repoPedidoLinea;
|
||||||
private final PaisesService paisesService;
|
private final PaisesService paisesService;
|
||||||
private final TranslationService translationService;
|
private final TranslationService translationService;
|
||||||
|
private final FacturacionService facturacionService;
|
||||||
|
|
||||||
public PedidosController(PedidoRepository repoPedido, PedidoService pedidoService, UserDao repoUser,
|
public PedidosController(PedidoRepository repoPedido, PedidoService pedidoService, UserDao repoUser,
|
||||||
MessageSource messageSource, TranslationService translationService,
|
MessageSource messageSource, TranslationService translationService,
|
||||||
PedidoLineaRepository repoPedidoLinea, PaisesService paisesService, PresupuestoService presupuestoService) {
|
PedidoLineaRepository repoPedidoLinea, PaisesService paisesService,
|
||||||
|
FacturacionService facturacionService, PresupuestoService presupuestoService) {
|
||||||
this.repoPedido = repoPedido;
|
this.repoPedido = repoPedido;
|
||||||
this.pedidoService = pedidoService;
|
this.pedidoService = pedidoService;
|
||||||
this.repoUser = repoUser;
|
this.repoUser = repoUser;
|
||||||
@ -63,6 +66,7 @@ public class PedidosController {
|
|||||||
this.translationService = translationService;
|
this.translationService = translationService;
|
||||||
this.repoPedidoLinea = repoPedidoLinea;
|
this.repoPedidoLinea = repoPedidoLinea;
|
||||||
this.paisesService = paisesService;
|
this.paisesService = paisesService;
|
||||||
|
this.facturacionService = facturacionService;
|
||||||
this.presupuestoService = presupuestoService;
|
this.presupuestoService = presupuestoService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,6 +240,7 @@ public class PedidosController {
|
|||||||
model.addAttribute("direccionFacturacion", direccionFacturacion);
|
model.addAttribute("direccionFacturacion", direccionFacturacion);
|
||||||
|
|
||||||
Boolean showCancel = false;
|
Boolean showCancel = false;
|
||||||
|
Boolean showDownloadFactura = true;
|
||||||
List<Map<String, Object>> lineas = pedidoService.getLineas(id, locale);
|
List<Map<String, Object>> lineas = pedidoService.getLineas(id, locale);
|
||||||
for (Map<String, Object> linea : lineas) {
|
for (Map<String, Object> linea : lineas) {
|
||||||
|
|
||||||
@ -243,6 +248,9 @@ public class PedidosController {
|
|||||||
((Number) linea.get("lineaId")).longValue()).orElse(null);
|
((Number) linea.get("lineaId")).longValue()).orElse(null);
|
||||||
if (pedidoLinea != null) {
|
if (pedidoLinea != null) {
|
||||||
Map<String, Boolean> buttons = new HashMap<>();
|
Map<String, Boolean> buttons = new HashMap<>();
|
||||||
|
if (pedidoLinea.getEstado() != PedidoLinea.Estado.enviado) {
|
||||||
|
showDownloadFactura = false;
|
||||||
|
}
|
||||||
if (pedidoLinea.getEstado().getPriority() >= PedidoLinea.Estado.esperando_aceptacion_ferro.getPriority()
|
if (pedidoLinea.getEstado().getPriority() >= PedidoLinea.Estado.esperando_aceptacion_ferro.getPriority()
|
||||||
&& pedidoLinea.getEstado().getPriority() <= PedidoLinea.Estado.produccion.getPriority()) {
|
&& pedidoLinea.getEstado().getPriority() <= PedidoLinea.Estado.produccion.getPriority()) {
|
||||||
|
|
||||||
@ -263,8 +271,10 @@ public class PedidosController {
|
|||||||
linea.put("buttons", buttons);
|
linea.put("buttons", buttons);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(pedidoLinea.getEstado() != PedidoLinea.Estado.cancelado && pedidoLinea.getEstado() != PedidoLinea.Estado.terminado) {
|
if (pedidoLinea.getEstado() != PedidoLinea.Estado.cancelado
|
||||||
showCancel = true;
|
&& pedidoLinea.getEstado() != PedidoLinea.Estado.terminado
|
||||||
|
&& pedidoLinea.getEstado() != PedidoLinea.Estado.enviado) {
|
||||||
|
showCancel = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,8 +290,16 @@ public class PedidosController {
|
|||||||
linea.put("direccionesEntrega", dirEntrega);
|
linea.put("direccionesEntrega", dirEntrega);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Long facturaId = null;
|
||||||
|
if (showDownloadFactura) {
|
||||||
|
facturaId = facturacionService.getFacturaIdFromPedidoId(id);
|
||||||
|
}
|
||||||
model.addAttribute("lineas", lineas);
|
model.addAttribute("lineas", lineas);
|
||||||
model.addAttribute("showCancel", showCancel);
|
model.addAttribute("showCancel", showCancel);
|
||||||
|
if (showDownloadFactura && facturaId != null) {
|
||||||
|
model.addAttribute("facturaId", facturaId);
|
||||||
|
model.addAttribute("showDownloadFactura", showDownloadFactura);
|
||||||
|
}
|
||||||
model.addAttribute("id", id);
|
model.addAttribute("id", id);
|
||||||
return "imprimelibros/pedidos/pedidos-view";
|
return "imprimelibros/pedidos/pedidos-view";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import java.util.Optional;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@ -63,6 +65,8 @@ import jakarta.validation.Valid;
|
|||||||
@RequestMapping("/presupuesto")
|
@RequestMapping("/presupuesto")
|
||||||
public class PresupuestoController {
|
public class PresupuestoController {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(PresupuestoController.class);
|
||||||
|
|
||||||
private final PresupuestoRepository presupuestoRepository;
|
private final PresupuestoRepository presupuestoRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@ -509,6 +513,102 @@ public class PresupuestoController {
|
|||||||
return ResponseEntity.ok(resumen);
|
return ResponseEntity.ok(resumen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/public/prepare-claim")
|
||||||
|
public ResponseEntity<?> prepareClaim(
|
||||||
|
@RequestBody Map<String, Object> body,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
|
||||||
|
Long presupuestoId = objectMapper.convertValue(body.get("presupuestoId"), Long.class);
|
||||||
|
if (presupuestoId == null) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "missing presupuestoId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Presupuesto p = presupuestoRepository.findById(presupuestoId).orElse(null);
|
||||||
|
if (p == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||||
|
.body(Map.of("message", "presupuesto not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.getOrigen() != Presupuesto.Origen.publico) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "presupuesto not public"));
|
||||||
|
}
|
||||||
|
|
||||||
|
request.getSession(true).setAttribute("presupuesto_claim_id", presupuestoId);
|
||||||
|
return ResponseEntity.ok(Map.of("success", true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/claim")
|
||||||
|
@Transactional
|
||||||
|
public String claimPresupuesto(
|
||||||
|
HttpServletRequest request,
|
||||||
|
Authentication authentication,
|
||||||
|
RedirectAttributes redirectAttributes,
|
||||||
|
Locale locale) {
|
||||||
|
|
||||||
|
Object attr = request.getSession(false) != null
|
||||||
|
? request.getSession(false).getAttribute("presupuesto_claim_id")
|
||||||
|
: null;
|
||||||
|
|
||||||
|
Long presupuestoId = null;
|
||||||
|
if (attr instanceof Long) {
|
||||||
|
presupuestoId = (Long) attr;
|
||||||
|
} else if (attr != null) {
|
||||||
|
try {
|
||||||
|
presupuestoId = Long.valueOf(attr.toString());
|
||||||
|
} catch (NumberFormatException ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presupuestoId == null) {
|
||||||
|
redirectAttributes.addFlashAttribute("errorMessage",
|
||||||
|
messageSource.getMessage("presupuesto.errores.presupuesto-no-existe", new Object[] { 0 }, locale));
|
||||||
|
return "redirect:/presupuesto";
|
||||||
|
}
|
||||||
|
|
||||||
|
Presupuesto p = presupuestoRepository.findById(presupuestoId).orElse(null);
|
||||||
|
if (p == null) {
|
||||||
|
redirectAttributes.addFlashAttribute("errorMessage",
|
||||||
|
messageSource.getMessage("presupuesto.errores.presupuesto-no-existe",
|
||||||
|
new Object[] { presupuestoId }, locale));
|
||||||
|
return "redirect:/presupuesto";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.getUser() != null && authentication != null) {
|
||||||
|
Long currentUserId = null;
|
||||||
|
if (authentication.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||||
|
currentUserId = udi.getId();
|
||||||
|
} else {
|
||||||
|
currentUserId = userRepo.findIdByUserNameIgnoreCase(authentication.getName()).orElse(null);
|
||||||
|
}
|
||||||
|
if (currentUserId != null && p.getUser().getId().equals(currentUserId)) {
|
||||||
|
request.getSession().removeAttribute("presupuesto_claim_id");
|
||||||
|
return "redirect:/presupuesto/edit/" + p.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.getOrigen() != Presupuesto.Origen.publico) {
|
||||||
|
redirectAttributes.addFlashAttribute("errorMessage",
|
||||||
|
messageSource.getMessage("presupuesto.errores.presupuesto-no-existe",
|
||||||
|
new Object[] { presupuestoId }, locale));
|
||||||
|
return "redirect:/presupuesto";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authentication != null) {
|
||||||
|
if (authentication.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||||
|
p.setUser(userRepo.getReferenceById(udi.getId()));
|
||||||
|
} else {
|
||||||
|
userRepo.findByUserNameIgnoreCase(authentication.getName()).ifPresent(p::setUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.setOrigen(Presupuesto.Origen.privado);
|
||||||
|
p.setEstado(Presupuesto.Estado.borrador);
|
||||||
|
presupuestoRepository.saveAndFlush(p);
|
||||||
|
|
||||||
|
request.getSession().removeAttribute("presupuesto_claim_id");
|
||||||
|
return "redirect:/presupuesto/edit/" + p.getId();
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================
|
// =============================================
|
||||||
// MÉTODOS PARA USUARIOS AUTENTICADOS
|
// MÉTODOS PARA USUARIOS AUTENTICADOS
|
||||||
// =============================================
|
// =============================================
|
||||||
@ -824,6 +924,7 @@ public class PresupuestoController {
|
|||||||
return ResponseEntity.ok(Map.of("id", saveResult.get("presupuesto_id"),
|
return ResponseEntity.ok(Map.of("id", saveResult.get("presupuesto_id"),
|
||||||
"message", messageSource.getMessage("presupuesto.exito.guardado", null, locale)));
|
"message", messageSource.getMessage("presupuesto.exito.guardado", null, locale)));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
log.error("Error al guardar el presupuesto", ex);
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
.body(Map.of("message",
|
.body(Map.of("message",
|
||||||
messageSource.getMessage("presupuesto.error.save-internal-error", null, locale),
|
messageSource.getMessage("presupuesto.error.save-internal-error", null, locale),
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
package com.imprimelibros.erp.redsys;
|
package com.imprimelibros.erp.redsys;
|
||||||
|
|
||||||
import com.imprimelibros.erp.cart.Cart;
|
|
||||||
import com.imprimelibros.erp.common.Utils;
|
import com.imprimelibros.erp.common.Utils;
|
||||||
import com.imprimelibros.erp.payments.PaymentService;
|
import com.imprimelibros.erp.payments.PaymentService;
|
||||||
import com.imprimelibros.erp.payments.model.Payment;
|
import com.imprimelibros.erp.payments.model.Payment;
|
||||||
import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository;
|
|
||||||
import com.imprimelibros.erp.pedidos.Pedido;
|
import com.imprimelibros.erp.pedidos.Pedido;
|
||||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||||
import com.imprimelibros.erp.redsys.RedsysService.FormPayload;
|
import com.imprimelibros.erp.redsys.RedsysService.FormPayload;
|
||||||
|
|
||||||
import groovy.util.logging.Log;
|
|
||||||
import jakarta.servlet.ServletContext;
|
import jakarta.servlet.ServletContext;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|||||||
@ -0,0 +1,115 @@
|
|||||||
|
package com.imprimelibros.erp.users;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.config.Sanitizer;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class ImpersonationController {
|
||||||
|
|
||||||
|
private static final String PREVIOUS_ADMIN_ROLE = "ROLE_PREVIOUS_ADMINISTRATOR";
|
||||||
|
private static final String SESSION_ATTR = "IMPERSONATOR_AUTH";
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
private final Sanitizer sanitizer;
|
||||||
|
|
||||||
|
public ImpersonationController(UserService userService, Sanitizer sanitizer) {
|
||||||
|
this.userService = userService;
|
||||||
|
this.sanitizer = sanitizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/impersonate")
|
||||||
|
@PreAuthorize("hasRole('ADMIN') or hasRole('SUPERADMIN')")
|
||||||
|
public ResponseEntity<Void> impersonate(
|
||||||
|
@RequestParam("username") String username,
|
||||||
|
Authentication authentication,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
|
||||||
|
if (authentication == null) {
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasRole(authentication, PREVIOUS_ADMIN_ROLE)) {
|
||||||
|
return ResponseEntity.status(409).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = sanitizer.plain(username);
|
||||||
|
if (normalized == null || normalized.isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
normalized = normalized.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (authentication.getName() != null
|
||||||
|
&& authentication.getName().equalsIgnoreCase(normalized)) {
|
||||||
|
return ResponseEntity.status(409).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDetails target;
|
||||||
|
try {
|
||||||
|
target = userService.loadUserByUsername(normalized);
|
||||||
|
} catch (UsernameNotFoundException ex) {
|
||||||
|
throw new AccessDeniedException("No autorizado");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean targetIsSuperAdmin = target.getAuthorities().stream()
|
||||||
|
.anyMatch(a -> "ROLE_SUPERADMIN".equals(a.getAuthority()));
|
||||||
|
if (targetIsSuperAdmin) {
|
||||||
|
throw new AccessDeniedException("No autorizado");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpSession session = request.getSession(true);
|
||||||
|
if (session.getAttribute(SESSION_ATTR) == null) {
|
||||||
|
session.setAttribute(SESSION_ATTR, authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GrantedAuthority> authorities = new ArrayList<>(target.getAuthorities());
|
||||||
|
authorities.add(new SimpleGrantedAuthority(PREVIOUS_ADMIN_ROLE));
|
||||||
|
|
||||||
|
UsernamePasswordAuthenticationToken newAuth = new UsernamePasswordAuthenticationToken(
|
||||||
|
target, target.getPassword(), authorities);
|
||||||
|
newAuth.setDetails(authentication.getDetails());
|
||||||
|
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(newAuth);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/impersonate/exit")
|
||||||
|
@PreAuthorize("hasRole('PREVIOUS_ADMINISTRATOR')")
|
||||||
|
public String exit(HttpServletRequest request) {
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
if (session != null) {
|
||||||
|
Object previous = session.getAttribute(SESSION_ATTR);
|
||||||
|
if (previous instanceof Authentication previousAuth) {
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(previousAuth);
|
||||||
|
} else {
|
||||||
|
SecurityContextHolder.clearContext();
|
||||||
|
}
|
||||||
|
session.removeAttribute(SESSION_ATTR);
|
||||||
|
}
|
||||||
|
return "redirect:/";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasRole(Authentication auth, String role) {
|
||||||
|
return auth != null
|
||||||
|
&& auth.getAuthorities().stream()
|
||||||
|
.anyMatch(a -> role.equals(a.getAuthority()));
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src/main/java/com/imprimelibros/erp/users/ProfileController.java
Normal file
155
src/main/java/com/imprimelibros/erp/users/ProfileController.java
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package com.imprimelibros.erp.users;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.validation.BindingResult;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.config.Sanitizer;
|
||||||
|
import com.imprimelibros.erp.users.validation.ProfileForm;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/pages-profile")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public class ProfileController {
|
||||||
|
|
||||||
|
private final UserDao userDao;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final MessageSource messageSource;
|
||||||
|
private final Sanitizer sanitizer;
|
||||||
|
|
||||||
|
public ProfileController(UserDao userDao, PasswordEncoder passwordEncoder,
|
||||||
|
MessageSource messageSource, Sanitizer sanitizer) {
|
||||||
|
this.userDao = userDao;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.messageSource = messageSource;
|
||||||
|
this.sanitizer = sanitizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String view(
|
||||||
|
Authentication authentication,
|
||||||
|
@RequestParam(name = "success", required = false) String success,
|
||||||
|
Model model,
|
||||||
|
Locale locale) {
|
||||||
|
|
||||||
|
if (authentication == null) {
|
||||||
|
return "redirect:/login";
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userDao.findByUserNameIgnoreCase(authentication.getName()).orElse(null);
|
||||||
|
if (user == null) {
|
||||||
|
return "redirect:/login";
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileForm form = new ProfileForm();
|
||||||
|
form.setId(user.getId());
|
||||||
|
form.setFullName(user.getFullName());
|
||||||
|
form.setUserName(user.getUserName());
|
||||||
|
|
||||||
|
model.addAttribute("user", form);
|
||||||
|
model.addAttribute("success", success != null);
|
||||||
|
return "imprimelibros/users/profile";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public String update(
|
||||||
|
Authentication authentication,
|
||||||
|
@Validated @ModelAttribute("user") ProfileForm form,
|
||||||
|
BindingResult binding,
|
||||||
|
Model model,
|
||||||
|
RedirectAttributes redirectAttributes,
|
||||||
|
Locale locale) {
|
||||||
|
|
||||||
|
if (authentication == null) {
|
||||||
|
return "redirect:/login";
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userDao.findByUserNameIgnoreCase(authentication.getName()).orElse(null);
|
||||||
|
if (user == null) {
|
||||||
|
return "redirect:/login";
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = sanitizer.plain(form.getUserName());
|
||||||
|
if (normalized != null) {
|
||||||
|
normalized = normalized.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized == null || normalized.isBlank()) {
|
||||||
|
binding.rejectValue("userName", "usuarios.error.email",
|
||||||
|
messageSource.getMessage("usuarios.error.email", null, locale));
|
||||||
|
} else if (userDao.existsByUserNameIgnoreCaseAndIdNot(normalized, user.getId())) {
|
||||||
|
binding.rejectValue("userName", "usuarios.error.duplicado",
|
||||||
|
messageSource.getMessage("usuarios.error.duplicado", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
String cleanName = sanitizer.plain(form.getFullName());
|
||||||
|
if (cleanName == null || cleanName.isBlank()) {
|
||||||
|
binding.rejectValue("fullName", "usuarios.error.nombre",
|
||||||
|
messageSource.getMessage("usuarios.error.nombre", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean wantsPasswordChange = hasText(form.getCurrentPassword())
|
||||||
|
|| hasText(form.getNewPassword())
|
||||||
|
|| hasText(form.getConfirmPassword());
|
||||||
|
|
||||||
|
if (wantsPasswordChange) {
|
||||||
|
if (!hasText(form.getCurrentPassword())) {
|
||||||
|
binding.rejectValue("currentPassword", "usuarios.error.password.actual",
|
||||||
|
messageSource.getMessage("usuarios.error.password.actual", null, locale));
|
||||||
|
} else if (!passwordEncoder.matches(form.getCurrentPassword(), user.getPassword())) {
|
||||||
|
binding.rejectValue("currentPassword", "usuarios.error.password.actual.incorrecta",
|
||||||
|
messageSource.getMessage("usuarios.error.password.actual.incorrecta", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasText(form.getNewPassword())) {
|
||||||
|
binding.rejectValue("newPassword", "usuarios.error.password.nueva.requerida",
|
||||||
|
messageSource.getMessage("usuarios.error.password.nueva.requerida", null, locale));
|
||||||
|
} else if (form.getNewPassword().length() < 6) {
|
||||||
|
binding.rejectValue("newPassword", "usuarios.error.password.min",
|
||||||
|
messageSource.getMessage("usuarios.error.password.min", null, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasText(form.getConfirmPassword())) {
|
||||||
|
binding.rejectValue("confirmPassword", "usuarios.error.confirmPassword.requerida",
|
||||||
|
messageSource.getMessage("usuarios.error.confirmPassword.requerida", null, locale));
|
||||||
|
} else if (hasText(form.getNewPassword()) && !form.getNewPassword().equals(form.getConfirmPassword())) {
|
||||||
|
binding.rejectValue("confirmPassword", "usuarios.error.password-coinciden",
|
||||||
|
messageSource.getMessage("usuarios.error.password-coinciden", null, locale));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.hasErrors()) {
|
||||||
|
model.addAttribute("success", false);
|
||||||
|
return "imprimelibros/users/profile";
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setFullName(cleanName.trim());
|
||||||
|
user.setUserName(normalized);
|
||||||
|
|
||||||
|
if (wantsPasswordChange) {
|
||||||
|
user.setPassword(passwordEncoder.encode(form.getNewPassword()));
|
||||||
|
}
|
||||||
|
|
||||||
|
userDao.save(user);
|
||||||
|
|
||||||
|
redirectAttributes.addAttribute("success", "1");
|
||||||
|
return "redirect:/pages-profile";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasText(String value) {
|
||||||
|
return value != null && !value.isBlank();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -81,6 +81,9 @@ public class UserController {
|
|||||||
"usuarios.delete.button",
|
"usuarios.delete.button",
|
||||||
"app.yes",
|
"app.yes",
|
||||||
"app.cancelar",
|
"app.cancelar",
|
||||||
|
"usuarios.impersonate.title",
|
||||||
|
"usuarios.impersonate.text",
|
||||||
|
"usuarios.impersonate.button",
|
||||||
"usuarios.delete.ok.title",
|
"usuarios.delete.ok.title",
|
||||||
"usuarios.delete.ok.text");
|
"usuarios.delete.ok.text");
|
||||||
|
|
||||||
@ -132,26 +135,36 @@ public class UserController {
|
|||||||
.collect(Collectors.joining(" ")))
|
.collect(Collectors.joining(" ")))
|
||||||
.add("actions", (user) -> {
|
.add("actions", (user) -> {
|
||||||
|
|
||||||
boolean isSuperAdmin = authentication.getAuthorities().stream()
|
boolean isSuperAdmin = authentication != null && authentication.getAuthorities().stream()
|
||||||
.anyMatch(a -> a.getAuthority().equals("ROLE_SUPERADMIN"));
|
.anyMatch(a -> a.getAuthority().equals("ROLE_SUPERADMIN"));
|
||||||
|
|
||||||
if (!isSuperAdmin) {
|
boolean isSelf = authentication != null
|
||||||
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
|
&& authentication.getName() != null
|
||||||
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
&& authentication.getName().equalsIgnoreCase(user.getUserName());
|
||||||
+ "\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n"
|
|
||||||
+
|
boolean targetIsSuperAdmin = user.getRoles().stream()
|
||||||
" </div>";
|
.anyMatch(r -> "SUPERADMIN".equalsIgnoreCase(r.getName()));
|
||||||
} else {
|
|
||||||
// Admin editando otro admin o usuario normal: puede editarse y eliminarse
|
StringBuilder actions = new StringBuilder();
|
||||||
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
|
actions.append("<div class=\"hstack gap-3 flex-wrap\">");
|
||||||
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
actions.append("<a href=\"javascript:void(0);\" data-id=\"")
|
||||||
+ "\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n"
|
.append(user.getId())
|
||||||
+
|
.append("\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>");
|
||||||
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
|
||||||
+ "\" class=\"link-danger btn-delete-user fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>\n"
|
if (!isSelf && !targetIsSuperAdmin) {
|
||||||
+
|
actions.append("<a href=\"javascript:void(0);\" data-username=\"")
|
||||||
" </div>";
|
.append(user.getUserName())
|
||||||
|
.append("\" class=\"link-info btn-impersonate-user fs-15\"><i class=\"ri-user-shared-line\"></i></a>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSuperAdmin) {
|
||||||
|
actions.append("<a href=\"javascript:void(0);\" data-id=\"")
|
||||||
|
.append(user.getId())
|
||||||
|
.append("\" class=\"link-danger btn-delete-user fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>");
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.append("</div>");
|
||||||
|
return actions.toString();
|
||||||
})
|
})
|
||||||
.where(base)
|
.where(base)
|
||||||
// Filtros custom:
|
// Filtros custom:
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
package com.imprimelibros.erp.users.validation;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public class ProfileForm {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@NotBlank(message = "{usuarios.error.nombre}")
|
||||||
|
private String fullName;
|
||||||
|
|
||||||
|
@NotBlank(message = "{usuarios.error.email}")
|
||||||
|
@Email(message = "{usuarios.error.email.formato}")
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
private String currentPassword;
|
||||||
|
private String newPassword;
|
||||||
|
private String confirmPassword;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFullName() {
|
||||||
|
return fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFullName(String fullName) {
|
||||||
|
this.fullName = fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserName() {
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserName(String userName) {
|
||||||
|
this.userName = userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrentPassword() {
|
||||||
|
return currentPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentPassword(String currentPassword) {
|
||||||
|
this.currentPassword = currentPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNewPassword() {
|
||||||
|
return newPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNewPassword(String newPassword) {
|
||||||
|
this.newPassword = newPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConfirmPassword() {
|
||||||
|
return confirmPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConfirmPassword(String confirmPassword) {
|
||||||
|
this.confirmPassword = confirmPassword;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -33,7 +33,7 @@ logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} -
|
|||||||
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n
|
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n
|
||||||
|
|
||||||
# Datos de la API de Safekat
|
# Datos de la API de Safekat
|
||||||
safekat.api.url=http://localhost:8000/
|
safekat.api.url=https://erp-dev.safekat.es/
|
||||||
safekat.api.email=imnavajas@coit.es
|
safekat.api.email=imnavajas@coit.es
|
||||||
safekat.api.password=Safekat2024
|
safekat.api.password=Safekat2024
|
||||||
|
|
||||||
@ -41,6 +41,6 @@ safekat.api.password=Safekat2024
|
|||||||
redsys.environment=test
|
redsys.environment=test
|
||||||
redsys.url=https://sis-t.redsys.es:25443/sis/realizarPago
|
redsys.url=https://sis-t.redsys.es:25443/sis/realizarPago
|
||||||
redsys.refund.url=https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST
|
redsys.refund.url=https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST
|
||||||
redsys.urls.ok=http://localhost:8080/pagos/redsys/ok
|
redsys.urls.ok=https://imprimelibros.jjimenez.eu/pagos/redsys/ok
|
||||||
redsys.urls.ko=http://localhost:8080/pagos/redsys/ko
|
redsys.urls.ko=https://imprimelibros.jjimenez.eu/pagos/redsys/ko
|
||||||
redsys.urls.notify=https://orological-sacrilegiously-lucille.ngrok-free.dev/pagos/redsys/notify
|
redsys.urls.notify=https://imprimelibros.jjimenez.eu/pagos/redsys/notify
|
||||||
46
src/main/resources/application-local.properties
Normal file
46
src/main/resources/application-local.properties
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Profile desarrollo
|
||||||
|
|
||||||
|
#
|
||||||
|
# Logging
|
||||||
|
#
|
||||||
|
logging.level.root=INFO
|
||||||
|
logging.level.org.springframework.security=ERROR
|
||||||
|
logging.level.org.springframework=ERROR
|
||||||
|
logging.level.org.springframework.web=ERROR
|
||||||
|
logging.level.org.thymeleaf=ERROR
|
||||||
|
logging.level.org.apache.catalina.core=ERROR
|
||||||
|
# Debug JPA / Hibernate
|
||||||
|
#logging.level.org.hibernate.SQL=DEBUG
|
||||||
|
#logging.level.org.hibernate.orm.jdbc.bind=TRACE
|
||||||
|
#spring.jpa.properties.hibernate.format_sql=true
|
||||||
|
|
||||||
|
server.error.include-message=always
|
||||||
|
server.error.include-stacktrace=on_param
|
||||||
|
server.error.include-binding-errors=on_param
|
||||||
|
|
||||||
|
|
||||||
|
# Archivo relativo a tu proyecto (asegúrate de que exista el directorio ./logs)
|
||||||
|
logging.file.name=logs/erp.log
|
||||||
|
|
||||||
|
# Rotación tiempo+tamaño (mismo patrón, pero en ./logs)
|
||||||
|
logging.logback.rollingpolicy.file-name-pattern=logs/erp-%d{yyyy-MM-dd}.%i.log
|
||||||
|
logging.logback.rollingpolicy.max-file-size=10MB
|
||||||
|
logging.logback.rollingpolicy.max-history=10
|
||||||
|
logging.logback.rollingpolicy.total-size-cap=1GB
|
||||||
|
|
||||||
|
# Formatos con timestamp
|
||||||
|
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n
|
||||||
|
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n
|
||||||
|
|
||||||
|
# Datos de la API de Safekat
|
||||||
|
safekat.api.url=http://localhost:8000/
|
||||||
|
safekat.api.email=imnavajas@coit.es
|
||||||
|
safekat.api.password=Safekat2024
|
||||||
|
|
||||||
|
# Configuración Redsys
|
||||||
|
redsys.environment=test
|
||||||
|
redsys.url=https://sis-t.redsys.es:25443/sis/realizarPago
|
||||||
|
redsys.refund.url=https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST
|
||||||
|
redsys.urls.ok=http://localhost:8080/pagos/redsys/ok
|
||||||
|
redsys.urls.ko=http://localhost:8080/pagos/redsys/ko
|
||||||
|
redsys.urls.notify=https://orological-sacrilegiously-lucille.ngrok-free.dev/pagos/redsys/notify
|
||||||
@ -18,6 +18,8 @@ server.error.include-binding-errors=never
|
|||||||
# Opcional: desactivar Whitelabel y servir tu propia página de error
|
# Opcional: desactivar Whitelabel y servir tu propia página de error
|
||||||
server.error.whitelabel.enabled=false
|
server.error.whitelabel.enabled=false
|
||||||
|
|
||||||
|
# Servelet options
|
||||||
|
server.servlet.context-path=/intranet
|
||||||
|
|
||||||
# Archivo principal dentro del contenedor (monta /var/log/imprimelibros como volumen)
|
# Archivo principal dentro del contenedor (monta /var/log/imprimelibros como volumen)
|
||||||
logging.file.name=/var/log/imprimelibros/erp.log
|
logging.file.name=/var/log/imprimelibros/erp.log
|
||||||
@ -42,6 +44,6 @@ safekat.api.password=Safekat2024
|
|||||||
redsys.environment=test
|
redsys.environment=test
|
||||||
redsys.url=https://sis-t.redsys.es:25443/sis/realizarPago
|
redsys.url=https://sis-t.redsys.es:25443/sis/realizarPago
|
||||||
redsys.refund.url=https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST
|
redsys.refund.url=https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST
|
||||||
redsys.urls.ok=https://imprimelibros.jjimenez.eu/pagos/redsys/ok
|
redsys.urls.ok=https://app.imprimelibros.com/intranet/pagos/redsys/ok
|
||||||
redsys.urls.ko=https://imprimelibros.jjimenez.eu/pagos/redsys/ko
|
redsys.urls.ko=https://app.imprimelibros.com/intranet/pagos/redsys/ko
|
||||||
redsys.urls.notify=https://imprimelibros.jjimenez.eu/pagos/redsys/notify
|
redsys.urls.notify=https://app.imprimelibros.com/intranet/pagos/redsys/notify
|
||||||
@ -1,6 +1,7 @@
|
|||||||
spring.application.name=erp
|
spring.application.name=erp
|
||||||
# Active profile
|
# Active profile
|
||||||
spring.profiles.active=dev
|
spring.profiles.active=local
|
||||||
|
#spring.profiles.active=dev
|
||||||
#spring.profiles.active=test
|
#spring.profiles.active=test
|
||||||
#spring.profiles.active=prod
|
#spring.profiles.active=prod
|
||||||
|
|
||||||
|
|||||||
@ -539,13 +539,13 @@ databaseChangeLog:
|
|||||||
- column:
|
- column:
|
||||||
constraints:
|
constraints:
|
||||||
nullable: false
|
nullable: false
|
||||||
defaultValueComputed: CURRENT_TIMESTAMP(3)
|
defaultValueComputed: CURRENT_TIMESTAMP
|
||||||
name: created_at
|
name: created_at
|
||||||
type: datetime
|
type: datetime
|
||||||
- column:
|
- column:
|
||||||
constraints:
|
constraints:
|
||||||
nullable: false
|
nullable: false
|
||||||
defaultValueComputed: CURRENT_TIMESTAMP(3) on update CURRENT_TIMESTAMP(3)
|
defaultValueComputed: CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP
|
||||||
name: updated_at
|
name: updated_at
|
||||||
type: datetime
|
type: datetime
|
||||||
- column:
|
- column:
|
||||||
@ -573,6 +573,7 @@ databaseChangeLog:
|
|||||||
name: iva_reducido
|
name: iva_reducido
|
||||||
type: BIT(1)
|
type: BIT(1)
|
||||||
tableName: presupuesto
|
tableName: presupuesto
|
||||||
|
|
||||||
- changeSet:
|
- changeSet:
|
||||||
id: 1761213112413-10
|
id: 1761213112413-10
|
||||||
author: jjimenez (generated)
|
author: jjimenez (generated)
|
||||||
@ -840,7 +841,7 @@ databaseChangeLog:
|
|||||||
associatedWith: ''
|
associatedWith: ''
|
||||||
columns:
|
columns:
|
||||||
- column:
|
- column:
|
||||||
defaultValueNumeric: !!float '0'
|
defaultValueNumeric: 0
|
||||||
name: deleted
|
name: deleted
|
||||||
indexName: idx_presupuesto_deleted
|
indexName: idx_presupuesto_deleted
|
||||||
tableName: presupuesto
|
tableName: presupuesto
|
||||||
|
|||||||
@ -2,12 +2,25 @@ databaseChangeLog:
|
|||||||
- changeSet:
|
- changeSet:
|
||||||
id: 0018-change-presupuesto-ch-3
|
id: 0018-change-presupuesto-ch-3
|
||||||
author: jjo
|
author: jjo
|
||||||
|
|
||||||
preConditions:
|
preConditions:
|
||||||
|
- onFail: MARK_RAN
|
||||||
|
- onError: HALT
|
||||||
- dbms:
|
- dbms:
|
||||||
type: mysql
|
type: mysql
|
||||||
|
- sqlCheck:
|
||||||
|
expectedResult: 1
|
||||||
|
sql: |
|
||||||
|
SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END
|
||||||
|
FROM information_schema.TABLE_CONSTRAINTS
|
||||||
|
WHERE CONSTRAINT_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'presupuesto'
|
||||||
|
AND CONSTRAINT_NAME = 'presupuesto_chk_3'
|
||||||
|
AND CONSTRAINT_TYPE = 'CHECK';
|
||||||
|
|
||||||
changes:
|
changes:
|
||||||
- sql:
|
- sql:
|
||||||
|
dbms: mysql
|
||||||
splitStatements: false
|
splitStatements: false
|
||||||
stripComments: true
|
stripComments: true
|
||||||
sql: |
|
sql: |
|
||||||
@ -16,6 +29,7 @@ databaseChangeLog:
|
|||||||
|
|
||||||
rollback:
|
rollback:
|
||||||
- sql:
|
- sql:
|
||||||
|
dbms: mysql
|
||||||
splitStatements: false
|
splitStatements: false
|
||||||
stripComments: true
|
stripComments: true
|
||||||
sql: |
|
sql: |
|
||||||
|
|||||||
@ -0,0 +1,67 @@
|
|||||||
|
databaseChangeLog:
|
||||||
|
- changeSet:
|
||||||
|
id: 0024-series-facturacion-seeder
|
||||||
|
author: jjo
|
||||||
|
context: demo
|
||||||
|
changes:
|
||||||
|
|
||||||
|
# --- SERIES ---
|
||||||
|
- sql:
|
||||||
|
splitStatements: true
|
||||||
|
stripComments: true
|
||||||
|
sql: |
|
||||||
|
INSERT INTO series_facturas
|
||||||
|
(nombre_serie, prefijo, tipo, numero_actual, created_at, updated_at, created_by, updated_by)
|
||||||
|
SELECT
|
||||||
|
'IMPRESIÓN DIGITAL', 'IMPR', 'facturacion', 1, NOW(), NOW(), 1, 1
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM series_facturas WHERE prefijo = 'IMPR'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO series_facturas
|
||||||
|
(nombre_serie, prefijo, tipo, numero_actual, created_at, updated_at, created_by, updated_by)
|
||||||
|
SELECT
|
||||||
|
'RECT. IMPRESIÓN DIGITAL', 'REC IL', 'facturacion', 1, NOW(), NOW(), 1, 1
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM series_facturas WHERE prefijo = 'REC IL'
|
||||||
|
);
|
||||||
|
|
||||||
|
# --- VARIABLES (con el id real de la serie) ---
|
||||||
|
# serie_facturacion_default -> id de la serie con prefijo IMPR
|
||||||
|
- sql:
|
||||||
|
splitStatements: true
|
||||||
|
stripComments: true
|
||||||
|
sql: |
|
||||||
|
INSERT INTO variables (clave, valor)
|
||||||
|
SELECT
|
||||||
|
'serie_facturacion_default',
|
||||||
|
CAST(sf.id AS CHAR)
|
||||||
|
FROM series_facturas sf
|
||||||
|
WHERE sf.prefijo = 'IMPR'
|
||||||
|
LIMIT 1
|
||||||
|
ON DUPLICATE KEY UPDATE valor = VALUES(valor);
|
||||||
|
|
||||||
|
# sere_facturacion_rect_default -> id de la serie con prefijo REC IL
|
||||||
|
- sql:
|
||||||
|
splitStatements: true
|
||||||
|
stripComments: true
|
||||||
|
sql: |
|
||||||
|
INSERT INTO variables (clave, valor)
|
||||||
|
SELECT
|
||||||
|
'serie_facturacion_rect_default',
|
||||||
|
CAST(sf.id AS CHAR)
|
||||||
|
FROM series_facturas sf
|
||||||
|
WHERE sf.prefijo = 'REC IL'
|
||||||
|
LIMIT 1
|
||||||
|
ON DUPLICATE KEY UPDATE valor = VALUES(valor);
|
||||||
|
|
||||||
|
rollback:
|
||||||
|
- sql:
|
||||||
|
splitStatements: true
|
||||||
|
stripComments: true
|
||||||
|
sql: |
|
||||||
|
DELETE FROM variables
|
||||||
|
WHERE clave IN ('serie_facturacion_default', 'sere_facturacion_rect_default');
|
||||||
|
|
||||||
|
DELETE FROM series_facturas
|
||||||
|
WHERE prefijo IN ('IMPR', 'REC IL');
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
databaseChangeLog:
|
||||||
|
- changeSet:
|
||||||
|
id: create-facturas-direcciones
|
||||||
|
author: jjo
|
||||||
|
|
||||||
|
changes:
|
||||||
|
- createTable:
|
||||||
|
tableName: facturas_direcciones
|
||||||
|
columns:
|
||||||
|
- column:
|
||||||
|
name: id
|
||||||
|
type: BIGINT
|
||||||
|
autoIncrement: true
|
||||||
|
constraints:
|
||||||
|
primaryKey: true
|
||||||
|
nullable: false
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: factura_id
|
||||||
|
type: BIGINT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: unidades
|
||||||
|
type: MEDIUMINT UNSIGNED
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: email
|
||||||
|
type: VARCHAR(255)
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: att
|
||||||
|
type: VARCHAR(150)
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: direccion
|
||||||
|
type: VARCHAR(255)
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: cp
|
||||||
|
type: MEDIUMINT UNSIGNED
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: ciudad
|
||||||
|
type: VARCHAR(100)
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: provincia
|
||||||
|
type: VARCHAR(100)
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: pais_code3
|
||||||
|
type: CHAR(3)
|
||||||
|
defaultValue: esp
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: telefono
|
||||||
|
type: VARCHAR(30)
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: instrucciones
|
||||||
|
type: VARCHAR(255)
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: razon_social
|
||||||
|
type: VARCHAR(150)
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: tipo_identificacion_fiscal
|
||||||
|
type: ENUM('DNI','NIE','CIF','Pasaporte','VAT_ID')
|
||||||
|
defaultValue: DNI
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: identificacion_fiscal
|
||||||
|
type: VARCHAR(50)
|
||||||
|
|
||||||
|
- column:
|
||||||
|
name: created_at
|
||||||
|
type: TIMESTAMP
|
||||||
|
defaultValueComputed: CURRENT_TIMESTAMP
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
|
||||||
|
- addForeignKeyConstraint:
|
||||||
|
constraintName: fk_facturas_direcciones_factura
|
||||||
|
baseTableName: facturas_direcciones
|
||||||
|
baseColumnNames: factura_id
|
||||||
|
referencedTableName: facturas
|
||||||
|
referencedColumnNames: id
|
||||||
|
onDelete: CASCADE
|
||||||
|
onUpdate: RESTRICT
|
||||||
|
|
||||||
|
rollback:
|
||||||
|
- dropForeignKeyConstraint:
|
||||||
|
baseTableName: facturas_direcciones
|
||||||
|
constraintName: fk_facturas_direcciones_factura
|
||||||
|
|
||||||
|
- dropTable:
|
||||||
|
tableName: facturas_direcciones
|
||||||
@ -44,4 +44,8 @@ databaseChangeLog:
|
|||||||
- include:
|
- include:
|
||||||
file: db/changelog/changesets/0022-add-estados-pago-to-pedidos-lineas-3.yml
|
file: db/changelog/changesets/0022-add-estados-pago-to-pedidos-lineas-3.yml
|
||||||
- include:
|
- include:
|
||||||
file: db/changelog/changesets/0023-facturacion.yml
|
file: db/changelog/changesets/0023-facturacion.yml
|
||||||
|
- include:
|
||||||
|
file: db/changelog/changesets/0024-series-facturacion-seeder.yml
|
||||||
|
- include:
|
||||||
|
file: db/changelog/changesets/0025-create-facturas-direcciones.yml
|
||||||
@ -6,4 +6,5 @@ app.cancelar=Cancel
|
|||||||
app.guardar=Save
|
app.guardar=Save
|
||||||
app.editar=Edit
|
app.editar=Edit
|
||||||
app.eliminar=Delete
|
app.eliminar=Delete
|
||||||
app.imprimir=Print
|
app.imprimir=Print
|
||||||
|
app.impersonate.exit=Return to my user
|
||||||
|
|||||||
@ -23,6 +23,7 @@ app.logout=Cerrar sesión
|
|||||||
app.sidebar.inicio=Inicio
|
app.sidebar.inicio=Inicio
|
||||||
app.sidebar.presupuestos=Presupuestos
|
app.sidebar.presupuestos=Presupuestos
|
||||||
app.sidebar.pedidos=Pedidos
|
app.sidebar.pedidos=Pedidos
|
||||||
|
app.sidebar.facturas=Facturas
|
||||||
app.sidebar.configuracion=Configuración
|
app.sidebar.configuracion=Configuración
|
||||||
app.sidebar.usuarios=Usuarios
|
app.sidebar.usuarios=Usuarios
|
||||||
app.sidebar.direcciones=Mis Direcciones
|
app.sidebar.direcciones=Mis Direcciones
|
||||||
@ -31,4 +32,5 @@ app.sidebar.gestion-pagos=Gestión de Pagos
|
|||||||
|
|
||||||
app.errors.403=No tienes permiso para acceder a esta página.
|
app.errors.403=No tienes permiso para acceder a esta página.
|
||||||
|
|
||||||
app.validation.required=Campo obligatorio
|
app.validation.required=Campo obligatorio
|
||||||
|
app.impersonate.exit=Volver a mi usuario
|
||||||
|
|||||||
0
src/main/resources/i18n/facturas_en.properties
Normal file
0
src/main/resources/i18n/facturas_en.properties
Normal file
100
src/main/resources/i18n/facturas_es.properties
Normal file
100
src/main/resources/i18n/facturas_es.properties
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
facturas.title=Facturas
|
||||||
|
facturas.breadcrumb=Facturas
|
||||||
|
facturas.breadcrumb.ver=Ver Factura
|
||||||
|
facturas.breadcrumb.nueva=Nueva Factura
|
||||||
|
|
||||||
|
facturas.tabla.id=ID
|
||||||
|
facturas.tabla.cliente=Cliente
|
||||||
|
facturas.tabla.num-factura=Número de Factura
|
||||||
|
facturas.tabla.estado=Estado
|
||||||
|
facturas.tabla.estado-pago=Estado de Pago
|
||||||
|
facturas.tabla.total=Total
|
||||||
|
facturas.tabla.fecha-emision=Fecha de Emisión
|
||||||
|
facturas.tabla.acciones=Acciones
|
||||||
|
|
||||||
|
facturas.estado-pago.pendiente=Pendiente
|
||||||
|
facturas.estado-pago.pagada=Pagada
|
||||||
|
facturas.estado-pago.cancelada=Cancelada
|
||||||
|
|
||||||
|
facturas.estado.borrador=Borrador
|
||||||
|
facturas.estado.validada=Validada
|
||||||
|
|
||||||
|
facturas.form.numero-factura=Número de Factura
|
||||||
|
facturas.form.id=ID de la Factura
|
||||||
|
facturas.form.factura-rectificada=Factura rectificada
|
||||||
|
facturas.form.serie=Serie de facturación
|
||||||
|
facturas.form.serie.placeholder=Seleccione una serie...
|
||||||
|
facturas.form.fecha-emision=Fecha de Emisión
|
||||||
|
facturas.form.cliente=Cliente
|
||||||
|
facturas.form.direccion-facturacion=Dirección de Facturación
|
||||||
|
facturas.form.direccion-facturacion.placeholder=Seleccione una dirección...
|
||||||
|
facturas.form.cliente.placeholder=Seleccione un cliente...
|
||||||
|
facturas.form.notas=Notas
|
||||||
|
facturas.form.factura-rectificada=Factura rectificada
|
||||||
|
|
||||||
|
facturas.form.btn.validar=Validar Factura
|
||||||
|
facturas.form.btn.borrador=Pasar a Borrador
|
||||||
|
facturas.form.btn.guardar=Guardar
|
||||||
|
facturas.form.btn.imprimir=Imprimir Factura
|
||||||
|
|
||||||
|
facturas.lineas.acciones=Acciones
|
||||||
|
facturas.lineas.acciones.editar=Editar
|
||||||
|
facturas.lineas.acciones.eliminar=Eliminar
|
||||||
|
facturas.lineas.acciones.agregar=Agregar línea
|
||||||
|
facturas.lineas.descripcion=Descripción
|
||||||
|
facturas.lineas.base=Base Imponible
|
||||||
|
facturas.lineas.iva_4=I.V.A. 4%
|
||||||
|
facturas.lineas.iva_21=I.V.A. 21%
|
||||||
|
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.lineas.gastos-envio=Gastos de envío
|
||||||
|
|
||||||
|
facturas.direccion.titulo=Dirección de Facturación
|
||||||
|
facturas.direccion.razon-social=Razón Social
|
||||||
|
facturas.direccion.identificacion-fiscal=Identificación Fiscal
|
||||||
|
facturas.direccion.direccion=Dirección
|
||||||
|
facturas.direccion.codigo-postal=Código Postal
|
||||||
|
facturas.direccion.ciudad=Ciudad
|
||||||
|
facturas.direccion.provincia=Provincia
|
||||||
|
facturas.direccion.pais=País
|
||||||
|
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.text=Esta acción no se puede deshacer.
|
||||||
|
facturas.delete.ok.title=Factura eliminada
|
||||||
|
facturas.delete.ok.text=La factura ha sido eliminada correctamente.
|
||||||
|
|
||||||
|
facturas.add.form.validation.title=Error al crear la factura
|
||||||
|
facturas.add.form.validation=Revise que todos los campos están rellenos
|
||||||
|
|
||||||
|
facturas.error.create=No se ha podido crear la factura. Revise los datos e inténtelo de nuevo.
|
||||||
@ -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
|
||||||
|
|||||||
@ -28,6 +28,7 @@ pedido.estado.esperando_aceptacion_ferro=Esperando aceptación de ferro
|
|||||||
pedido.estado.ferro_cliente=Esperando aprobación de ferro
|
pedido.estado.ferro_cliente=Esperando aprobación de ferro
|
||||||
pedido.estado.produccion=Producción
|
pedido.estado.produccion=Producción
|
||||||
pedido.estado.terminado=Terminado
|
pedido.estado.terminado=Terminado
|
||||||
|
pedido.estado.enviado=Enviado
|
||||||
pedido.estado.cancelado=Cancelado
|
pedido.estado.cancelado=Cancelado
|
||||||
|
|
||||||
pedido.module-title=Pedidos
|
pedido.module-title=Pedidos
|
||||||
@ -50,13 +51,17 @@ pedido.table.importe=Importe
|
|||||||
pedido.table.estado=Estado
|
pedido.table.estado=Estado
|
||||||
pedido.table.acciones=Acciones
|
pedido.table.acciones=Acciones
|
||||||
|
|
||||||
|
pedido.gasto-anual=Gasto últimos 12 meses
|
||||||
|
|
||||||
pedido.view.tirada=Tirada
|
pedido.view.tirada=Tirada
|
||||||
pedido.view.view-presupuesto=Ver presupuesto
|
pedido.view.view-presupuesto=Ver presupuesto
|
||||||
pedido.view.aceptar-ferro=Aceptar ferro
|
pedido.view.aceptar-ferro=Aceptar ferro
|
||||||
pedido.view.ferro-download=Descargar ferro
|
pedido.view.ferro-download=Descargar ferro
|
||||||
pedido.view.cub-download=Descargar cubierta
|
pedido.view.cub-download=Descargar cubierta
|
||||||
pedido.view.tapa-download=Descargar tapa
|
pedido.view.tapa-download=Descargar tapa
|
||||||
|
pedido.view.descargar-factura=Descargar factura
|
||||||
pedido.view.admin-actions=Acciones de administrador
|
pedido.view.admin-actions=Acciones de administrador
|
||||||
|
pedido.view.actions=Acciones
|
||||||
pedido.view.cancel-title=¿Estás seguro de que deseas cancelar este pedido?
|
pedido.view.cancel-title=¿Estás seguro de que deseas cancelar este pedido?
|
||||||
pedido.view.cancel-text=Esta acción no se puede deshacer.
|
pedido.view.cancel-text=Esta acción no se puede deshacer.
|
||||||
|
|
||||||
|
|||||||
@ -1 +1,23 @@
|
|||||||
|
usuarios.form.nombre=Full name
|
||||||
|
usuarios.form.email=Email
|
||||||
|
usuarios.form.confirmarPassword=Confirm password
|
||||||
|
usuarios.form.password.actual=Current password
|
||||||
|
usuarios.form.password.nueva=New password
|
||||||
|
usuarios.form.password.nota=You can only change the password if you provide the current one.
|
||||||
|
|
||||||
|
usuarios.error.nombre=Name is required.
|
||||||
|
usuarios.error.email=Email is required.
|
||||||
|
usuarios.error.email.formato=Email is not valid.
|
||||||
|
usuarios.error.password.min=Password must be at least 6 characters.
|
||||||
|
usuarios.error.password.actual=Current password is required.
|
||||||
|
usuarios.error.password.actual.incorrecta=Current password is not correct.
|
||||||
|
usuarios.error.password.nueva.requerida=New password is required.
|
||||||
|
usuarios.error.confirmPassword.requerida=Password confirmation is required.
|
||||||
|
usuarios.error.password-coinciden=Passwords do not match.
|
||||||
|
usuarios.error.duplicado=There is already a user with that email.
|
||||||
|
|
||||||
|
usuarios.impersonate.title=Sign in as user
|
||||||
|
usuarios.impersonate.text=You are about to sign in as <b>{0}</b>. You can return to your user from the menu.
|
||||||
|
usuarios.impersonate.button=Continue
|
||||||
|
usuarios.profile.title=Edit profile
|
||||||
|
usuarios.profile.success=Profile updated successfully.
|
||||||
|
|||||||
@ -20,6 +20,9 @@ usuarios.form.nombre=Nombre completo
|
|||||||
usuarios.form.email=Correo electrónico
|
usuarios.form.email=Correo electrónico
|
||||||
usuarios.form.password=Contraseña
|
usuarios.form.password=Contraseña
|
||||||
usuarios.form.confirmarPassword=Confirmar contraseña
|
usuarios.form.confirmarPassword=Confirmar contraseña
|
||||||
|
usuarios.form.password.actual=Contraseña actual
|
||||||
|
usuarios.form.password.nueva=Nueva contraseña
|
||||||
|
usuarios.form.password.nota=Solo podrás cambiar la contraseña si indicas la actual.
|
||||||
usuarios.form.rol=Rol
|
usuarios.form.rol=Rol
|
||||||
usuarios.form.estado=Estado
|
usuarios.form.estado=Estado
|
||||||
|
|
||||||
@ -37,6 +40,9 @@ usuarios.error.email.formato=El correo electrónico no es válido.
|
|||||||
usuarios.error.rol=El rol seleccionado no es válido.
|
usuarios.error.rol=El rol seleccionado no es válido.
|
||||||
usuarios.error.password.requerida=La contraseña es obligatoria.
|
usuarios.error.password.requerida=La contraseña es obligatoria.
|
||||||
usuarios.error.password.min=La contraseña debe tener al menos 6 caracteres.
|
usuarios.error.password.min=La contraseña debe tener al menos 6 caracteres.
|
||||||
|
usuarios.error.password.actual=La contraseña actual es obligatoria.
|
||||||
|
usuarios.error.password.actual.incorrecta=La contraseña actual no es correcta.
|
||||||
|
usuarios.error.password.nueva.requerida=La nueva contraseña es obligatoria.
|
||||||
usuarios.error.confirmPassword.requerida=La confirmación de la contraseña es obligatoria.
|
usuarios.error.confirmPassword.requerida=La confirmación de la contraseña es obligatoria.
|
||||||
usuarios.error.password-coinciden=Las contraseñas no coinciden.
|
usuarios.error.password-coinciden=Las contraseñas no coinciden.
|
||||||
usuarios.error.delete-relational-data=No se puede eliminar el usuario porque tiene datos relacionados.
|
usuarios.error.delete-relational-data=No se puede eliminar el usuario porque tiene datos relacionados.
|
||||||
@ -53,4 +59,9 @@ usuarios.delete.title=Eliminar usuario
|
|||||||
usuarios.delete.button=Si, ELIMINAR
|
usuarios.delete.button=Si, ELIMINAR
|
||||||
usuarios.delete.text=¿Está seguro de que desea eliminar al usuario?<br>Esta acción no se puede deshacer.
|
usuarios.delete.text=¿Está seguro de que desea eliminar al usuario?<br>Esta acción no se puede deshacer.
|
||||||
usuarios.delete.ok.title=Usuario eliminado
|
usuarios.delete.ok.title=Usuario eliminado
|
||||||
usuarios.delete.ok.text=El usuario ha sido eliminado con éxito.
|
usuarios.delete.ok.text=El usuario ha sido eliminado con éxito.
|
||||||
|
usuarios.impersonate.title=Entrar como usuario
|
||||||
|
usuarios.impersonate.text=Vas a iniciar sesión como <b>{0}</b>. Podrás volver a tu usuario desde el menú.
|
||||||
|
usuarios.impersonate.button=Entrar
|
||||||
|
usuarios.profile.title=Editar perfil
|
||||||
|
usuarios.profile.success=Perfil actualizado correctamente.
|
||||||
|
|||||||
@ -2914,6 +2914,19 @@ File: Main Css File
|
|||||||
background-color: #0ac7fb !important;
|
background-color: #0ac7fb !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accordion-fill-imprimelibros .accordion-item .accordion-button {
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.accordion-fill-imprimelibros .accordion-item .accordion-button:not(.collapsed) {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #92b2a7 !important;
|
||||||
|
}
|
||||||
|
.accordion-fill-imprimelibros .accordion-item .accordion-button:is(.collapsed) {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #4c5c63 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.accordion-warning .accordion-item {
|
.accordion-warning .accordion-item {
|
||||||
border-color: rgba(239, 174, 78, 0.6);
|
border-color: rgba(239, 174, 78, 0.6);
|
||||||
}
|
}
|
||||||
@ -11985,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;
|
||||||
@ -12012,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 {
|
||||||
@ -12284,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%;
|
||||||
@ -12309,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;
|
||||||
@ -12411,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;
|
||||||
|
|||||||
476
src/main/resources/static/assets/css/facturapdf.css
Normal file
476
src/main/resources/static/assets/css/facturapdf.css
Normal file
@ -0,0 +1,476 @@
|
|||||||
|
: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;
|
||||||
|
margin: 15mm 14mm 47mm 14mm;
|
||||||
|
|
||||||
|
@bottom-center {
|
||||||
|
content: element(footer);
|
||||||
|
/* llamamos al elemento “footer” */
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
font-family: "Open Sans" !important;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 11pt;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
padding: 0;
|
||||||
|
/* ↑ deja 10mm extra para no pisar el footer */
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* para que el padding no desborde */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 {
|
||||||
|
position: fixed;
|
||||||
|
left: 14mm;
|
||||||
|
right: 14mm;
|
||||||
|
bottom: 18mm;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
padding-top: 4mm;
|
||||||
|
font-size: 7.5pt;
|
||||||
|
color: var(--muted);
|
||||||
|
z-index: 10;
|
||||||
|
/* sobre la marca */
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-count {
|
||||||
|
margin-top: 2mm;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 9pt;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page::after {
|
||||||
|
content: counter(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pages::after {
|
||||||
|
content: counter(pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.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;
|
||||||
|
color: #000
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-table td.desc{
|
||||||
|
font-family: "Open Sans" !important;
|
||||||
|
color: #5c5c5c !important;
|
||||||
|
font-size: 9pt !important;
|
||||||
|
line-height: 1.25 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO lo que esté dentro (p, span, ul, li, b, i, etc. del HTML manual) */
|
||||||
|
.items-table td.desc *{
|
||||||
|
font-family: "Open Sans" !important;
|
||||||
|
color: #5c5c5c !important;
|
||||||
|
font-size: 9pt !important;
|
||||||
|
line-height: 1.25 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.items-table thead {
|
||||||
|
display: table-header-group;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-table tfoot {
|
||||||
|
display: table-footer-group;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-table tr,
|
||||||
|
.items-table td,
|
||||||
|
.items-table th {
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.page-number {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -3mm;
|
||||||
|
right: 0;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-number .pn:before {
|
||||||
|
content: counter(page) " / " counter(pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-footer-running {
|
||||||
|
position: running(footer);
|
||||||
|
/* lo registra como elemento repetible */
|
||||||
|
font-size: 7.5pt;
|
||||||
|
color: var(--muted);
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
padding-top: 4mm;
|
||||||
|
/* el resto de tus estilos internos (address, privacy, etc.) */
|
||||||
|
}
|
||||||
372
src/main/resources/static/assets/css/home.css
Normal file
372
src/main/resources/static/assets/css/home.css
Normal file
@ -0,0 +1,372 @@
|
|||||||
|
:root {
|
||||||
|
/* ====== Colores (cámbialos a tu gusto) ====== */
|
||||||
|
--banner-bg-1: #a5a091;
|
||||||
|
--banner-bg-2: #8292a8;
|
||||||
|
|
||||||
|
--banner-panel-bg: #a1b1b2;
|
||||||
|
--banner-panel-border: rgba(255, 255, 255, .75);
|
||||||
|
|
||||||
|
--text-main: #ffffff;
|
||||||
|
--text-muted: rgba(255, 255, 255, .8);
|
||||||
|
|
||||||
|
--accent-1: #e5745b;
|
||||||
|
/* salmón */
|
||||||
|
--accent-2: #92b2a7;
|
||||||
|
/* tu verde corporativo */
|
||||||
|
--accent-3: #7cc7ff;
|
||||||
|
/* toque azul claro */
|
||||||
|
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--card-border: rgba(8, 42, 67, .18);
|
||||||
|
--card-title: #0a314b;
|
||||||
|
--card-chip-bg: var(--accent-1);
|
||||||
|
--card-chip-text: #ffffff;
|
||||||
|
|
||||||
|
--shadow: 0 10px 30px rgba(0, 0, 0, .18);
|
||||||
|
|
||||||
|
/* ====== Medidas ====== */
|
||||||
|
--radius-lg: 18px;
|
||||||
|
--radius-md: 14px;
|
||||||
|
--radius-sm: 10px;
|
||||||
|
|
||||||
|
--pad: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-banner {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
color: var(--text-main);
|
||||||
|
|
||||||
|
/* padding fluido */
|
||||||
|
padding: clamp(14px, 2.2vw, 22px);
|
||||||
|
|
||||||
|
/* Importante: reserva espacio inferior para decoraciones (libro) en desktop */
|
||||||
|
padding-bottom: clamp(18px, 3.2vw, 46px);
|
||||||
|
|
||||||
|
/* Fondo con gradiente + textura sutil */
|
||||||
|
background:
|
||||||
|
radial-gradient(1200px 500px at 20% 0%, rgba(124, 199, 255, .20), transparent 60%),
|
||||||
|
radial-gradient(900px 450px at 90% 20%, rgba(243, 162, 133, .22), transparent 65%),
|
||||||
|
linear-gradient(135deg, var(--banner-bg-1), var(--banner-bg-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Opcional pero ayuda a que haya “lienzo” para el libro */
|
||||||
|
@media (min-width: 1101px) {
|
||||||
|
.ib-loyalty-banner {
|
||||||
|
min-height: 240px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Decoraciones generales ===== */
|
||||||
|
.ib-loyalty-banner .decor {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: .95;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Círculos/“sellos” */
|
||||||
|
.ib-loyalty-banner .decor::before,
|
||||||
|
.ib-loyalty-banner .decor::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 10px solid rgba(243, 162, 133, .65);
|
||||||
|
box-shadow: inset 0 0 0 10px rgba(255, 255, 255, .08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-banner .decor::before {
|
||||||
|
top: -42px;
|
||||||
|
right: -50px;
|
||||||
|
transform: rotate(10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-banner .decor::after {
|
||||||
|
bottom: -55px;
|
||||||
|
left: -55px;
|
||||||
|
border-color: rgba(243, 162, 133, .55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Libros “dibujados” con SVG como background ===== */
|
||||||
|
.ib-loyalty-banner .book {
|
||||||
|
position: absolute;
|
||||||
|
width: 190px;
|
||||||
|
height: 150px;
|
||||||
|
opacity: .9;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
background-image: url("/assets/images/open-book.svg");
|
||||||
|
|
||||||
|
/* Sombra sin cargarte otros filtros */
|
||||||
|
filter: drop-shadow(0 10px 18px rgba(0, 0, 0, .25));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Libro pequeño: lo subimos un poco para que no se quede “bajo” cuando el layout crece */
|
||||||
|
.ib-loyalty-banner .book.small {
|
||||||
|
width: 150px;
|
||||||
|
height: 120px;
|
||||||
|
left: 22px;
|
||||||
|
bottom: 18px;
|
||||||
|
/* antes 10px */
|
||||||
|
opacity: .85;
|
||||||
|
z-index: 1;
|
||||||
|
/* por encima del fondo, por debajo del contenido (contenido z-index 2) */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Contenido ===== */
|
||||||
|
.ib-loyalty-inner {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
gap: clamp(12px, 2vw, 18px);
|
||||||
|
|
||||||
|
/* CLAVE: NO estires ambas columnas a la misma altura */
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
/* Dos columnas con mínimos reales */
|
||||||
|
grid-template-columns: minmax(320px, 1.05fr) minmax(320px, .95fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apila antes para que no se estrangule */
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.ib-loyalty-inner {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Panel principal (logo + textos) */
|
||||||
|
.ib-loyalty-hero {
|
||||||
|
border: 2px solid var(--banner-panel-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background: var(--banner-panel-bg);
|
||||||
|
padding: 18px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*.ib-loyalty-hero::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: -2px;
|
||||||
|
height: 6px;
|
||||||
|
background: linear-gradient(90deg, transparent, var(--accent-1), transparent);
|
||||||
|
opacity: .9;
|
||||||
|
border-radius: 999px;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
.ib-loyalty-head {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
/* responsive */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-logo {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-logo img {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(1.05rem, 1.2vw, 1.35rem);
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: .2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-sub {
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: clamp(.9rem, 1vw, 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Rewards ===== */
|
||||||
|
.ib-rewards {
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 14px 14px 10px;
|
||||||
|
background: rgba(255, 255, 255, .04);
|
||||||
|
border: 1px solid rgba(255, 255, 255, .12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-rewards h6 {
|
||||||
|
margin: 4px 6px 12px;
|
||||||
|
font-size: .95rem;
|
||||||
|
letter-spacing: .25px;
|
||||||
|
opacity: .95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-rewards-grid{
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
/* Nunca más de 3 columnas, pero baja si no hay sitio */
|
||||||
|
grid-template-columns: repeat(3, minmax(160px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px){
|
||||||
|
.ib-rewards-grid{
|
||||||
|
grid-template-columns: repeat(2, minmax(160px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px){
|
||||||
|
.ib-rewards-grid{
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 10px 10px 9px;
|
||||||
|
color: var(--card-title);
|
||||||
|
box-shadow: 0 8px 18px rgba(0, 0, 0, .10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-card .range {
|
||||||
|
font-size: .82rem;
|
||||||
|
opacity: .85;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-card .percent {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
margin: 2px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-card .chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--card-chip-bg);
|
||||||
|
color: var(--card-chip-text);
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: .78rem;
|
||||||
|
letter-spacing: .2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tarjeta “0%” con borde punteado (si la usas) */
|
||||||
|
.ib-card.is-empty {
|
||||||
|
background: rgba(255, 255, 255, .9);
|
||||||
|
border: 2px dashed rgba(10, 49, 75, .35);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Ajustes extra para móviles ===== */
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.ib-rewards {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-card {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Libro: reduce presencia o desaparece en móvil ===== */
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.ib-loyalty-banner .book.small {
|
||||||
|
opacity: .55;
|
||||||
|
left: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
width: 120px;
|
||||||
|
height: 95px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.ib-loyalty-banner .book.small {
|
||||||
|
display: none;
|
||||||
|
/* fuera en móviles pequeños */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== OPCIONAL: hero más compacto en móvil ===== */
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.ib-loyalty-head {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-logo {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-logo img {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-left{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-stat-card{
|
||||||
|
|
||||||
|
/* centra en el espacio libre */
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
|
||||||
|
/* mismo look que el hero */
|
||||||
|
background: var(--banner-panel-bg);
|
||||||
|
border: 2px solid var(--banner-panel-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
|
||||||
|
padding: 22px;
|
||||||
|
max-width: 360px; /* evita que se haga enorme */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-stat-card h6{
|
||||||
|
letter-spacing: .4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-stat-card h2{
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ib-loyalty-stat-card i{
|
||||||
|
opacity: .85;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px){
|
||||||
|
.ib-loyalty-stat-card{
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
84
src/main/resources/static/assets/images/open-book.svg
Normal file
84
src/main/resources/static/assets/images/open-book.svg
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#ffffff" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 511 511" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path d="M487.5,128.106H479v-24.5c0-2.905-1.678-5.549-4.307-6.786C405.088,64.066,325.408,63.6,255.5,95.371
|
||||||
|
C185.592,63.6,105.912,64.067,36.307,96.82C33.678,98.057,32,100.701,32,103.606v24.5h-8.5c-12.958,0-23.5,10.542-23.5,23.5v264
|
||||||
|
c0,12.958,10.542,23.5,23.5,23.5h464c12.958,0,23.5-10.542,23.5-23.5v-264C511,138.648,500.458,128.106,487.5,128.106z
|
||||||
|
M263,239.583c0-0.009,0-0.019,0-0.028V108.416c64.137-28.707,136.861-28.707,201,0v27.161c0,0.01-0.001,0.02-0.001,0.029
|
||||||
|
s0.001,0.02,0.001,0.029v244.438c-32.237-13.461-66.371-20.193-100.5-20.193c-34.129,0-68.264,6.732-100.5,20.193V239.583z
|
||||||
|
M215,96.391c11.187,3.204,22.217,7.198,33,12.025v117.177l-12.34-8.227c-2.52-1.68-5.801-1.68-8.32,0L215,225.593V96.391z
|
||||||
|
M47,135.626c0-0.007,0.001-0.013,0.001-0.02S47,135.594,47,135.587v-27.171c48.563-21.736,102.046-26.999,153-15.82v32.856
|
||||||
|
c-26.767-5.505-54.078-6.777-81.328-3.75c-4.117,0.457-7.083,4.165-6.626,8.282c0.458,4.116,4.162,7.085,8.282,6.626
|
||||||
|
c26.708-2.967,53.479-1.562,79.671,4.165v48.686c-15.912-3.265-32.14-5.067-48.377-5.323c-4.145-0.078-7.552,3.239-7.618,7.38
|
||||||
|
c-0.065,4.142,3.239,7.552,7.38,7.618c16.331,0.258,32.654,2.164,48.614,5.647v16.66c-43.389-8.909-88.39-6.644-130.748,6.665
|
||||||
|
c-3.952,1.241-6.148,5.451-4.907,9.403c1.007,3.204,3.964,5.254,7.153,5.254c0.745,0,1.502-0.112,2.25-0.347
|
||||||
|
c40.908-12.852,84.428-14.773,126.252-5.638v2.825c0,2.766,1.522,5.308,3.961,6.612c2.438,1.306,5.398,1.162,7.699-0.372
|
||||||
|
l19.84-13.227l16.5,11v136.454c-32.237-13.461-66.371-20.193-100.5-20.193c-34.129,0-68.264,6.732-100.5,20.193V135.626z
|
||||||
|
M224,424.106H23.5c-4.687,0-8.5-3.813-8.5-8.5v-264c0-4.687,3.813-8.5,8.5-8.5H32v248.5v8c0,4.142,3.358,7.5,7.5,7.5H224V424.106z
|
||||||
|
M57.29,392.106c58.099-22.934,122.32-22.935,180.42,0H57.29z M272,424.106h-33v-17h33V424.106z M453.71,392.106H273.29
|
||||||
|
C331.389,369.172,395.61,369.172,453.71,392.106z M496,415.606c0,4.687-3.813,8.5-8.5,8.5H287v-17h184.5c4.142,0,7.5-3.358,7.5-7.5
|
||||||
|
v-8v-248.5h8.5c4.687,0,8.5,3.813,8.5,8.5V415.606z"/>
|
||||||
|
<path d="M309.96,317.749c-8.302,1.74-16.615,3.911-24.708,6.454c-3.952,1.242-6.148,5.452-4.907,9.403
|
||||||
|
c1.007,3.204,3.964,5.254,7.153,5.254c0.745,0,1.502-0.112,2.25-0.347c7.628-2.396,15.464-4.443,23.288-6.083
|
||||||
|
c4.054-0.85,6.652-4.825,5.802-8.879C317.989,319.497,314.011,316.9,309.96,317.749z"/>
|
||||||
|
<path d="M439.502,338.859c3.189,0,6.147-2.051,7.153-5.254c1.241-3.952-0.956-8.162-4.907-9.403
|
||||||
|
c-32.073-10.076-65.329-13.842-98.844-11.188c-4.129,0.326-7.211,3.938-6.885,8.068s3.935,7.213,8.068,6.885
|
||||||
|
c31.59-2.499,62.935,1.048,93.165,10.546C438,338.748,438.757,338.859,439.502,338.859z"/>
|
||||||
|
<path d="M287.498,306.767c0.745,0,1.502-0.112,2.25-0.347c48.249-15.159,99.256-15.159,147.504,0
|
||||||
|
c3.952,1.24,8.162-0.956,9.403-4.907c1.241-3.952-0.956-8.162-4.907-9.403c-51.191-16.083-105.306-16.083-156.496,0
|
||||||
|
c-3.952,1.241-6.149,5.451-4.907,9.403C281.352,304.716,284.309,306.767,287.498,306.767z"/>
|
||||||
|
<path d="M287.498,274.859c0.745,0,1.502-0.112,2.25-0.347c27.681-8.697,56.409-12.412,85.399-11.037
|
||||||
|
c4.147,0.192,7.651-2.999,7.847-7.137c0.196-4.138-2.999-7.65-7.137-7.847c-30.753-1.456-61.236,2.483-90.605,11.71
|
||||||
|
c-3.952,1.242-6.149,5.452-4.907,9.403C281.352,272.81,284.309,274.859,287.498,274.859z"/>
|
||||||
|
<path d="M441.748,260.202c-10.76-3.38-21.846-6.086-32.952-8.043c-4.08-0.719-7.968,2.006-8.688,6.085
|
||||||
|
c-0.719,4.079,2.005,7.969,6.085,8.688c10.467,1.844,20.917,4.395,31.058,7.581c0.749,0.235,1.505,0.347,2.25,0.347
|
||||||
|
c3.189,0,6.147-2.051,7.153-5.254C447.896,265.653,445.7,261.443,441.748,260.202z"/>
|
||||||
|
<path d="M287.498,242.767c0.745,0,1.502-0.112,2.25-0.347c48.249-15.159,99.256-15.159,147.504,0
|
||||||
|
c3.952,1.24,8.162-0.956,9.403-4.907c1.241-3.952-0.956-8.162-4.907-9.403c-51.191-16.083-105.306-16.083-156.496,0
|
||||||
|
c-3.952,1.241-6.149,5.451-4.907,9.403C281.352,240.716,284.309,242.767,287.498,242.767z"/>
|
||||||
|
<path d="M334.678,185.702c-16.732,1.858-33.362,5.36-49.426,10.407c-3.952,1.241-6.148,5.451-4.907,9.403
|
||||||
|
c1.007,3.204,3.964,5.254,7.153,5.254c0.745,0,1.502-0.112,2.25-0.347c15.141-4.757,30.815-8.057,46.585-9.809
|
||||||
|
c4.117-0.457,7.083-4.165,6.626-8.282S338.79,185.244,334.678,185.702z"/>
|
||||||
|
<path d="M367.386,199.137c23.725,0.375,47.231,4.17,69.866,11.283c0.748,0.234,1.505,0.347,2.25,0.347
|
||||||
|
c3.189,0,6.146-2.051,7.153-5.254c1.241-3.952-0.956-8.162-4.907-9.403c-24.015-7.545-48.955-11.572-74.125-11.97
|
||||||
|
c-4.125-0.078-7.552,3.239-7.618,7.38S363.244,199.072,367.386,199.137z"/>
|
||||||
|
<path d="M390.671,168.704c4.116,0.46,7.825-2.509,8.282-6.626c0.458-4.117-2.509-7.825-6.626-8.282
|
||||||
|
c-36.252-4.027-72.278-0.526-107.075,10.406c-3.952,1.242-6.148,5.452-4.907,9.403c1.007,3.204,3.964,5.254,7.153,5.254
|
||||||
|
c0.745,0,1.502-0.112,2.25-0.347C322.545,168.208,356.5,164.909,390.671,168.704z"/>
|
||||||
|
<path d="M441.748,164.202c-5.418-1.702-10.96-3.246-16.472-4.588c-4.03-0.98-8.082,1.488-9.062,5.512
|
||||||
|
c-0.98,4.024,1.488,8.082,5.512,9.062c5.196,1.265,10.419,2.72,15.526,4.324c0.748,0.235,1.505,0.347,2.25,0.347
|
||||||
|
c3.189,0,6.147-2.051,7.153-5.254C447.896,169.653,445.7,165.443,441.748,164.202z"/>
|
||||||
|
<path d="M287.498,146.767c0.745,0,1.502-0.112,2.25-0.347c5.103-1.604,10.325-3.058,15.521-4.324
|
||||||
|
c4.024-0.98,6.492-5.037,5.512-9.062s-5.038-6.492-9.062-5.512c-5.513,1.342-11.053,2.886-16.468,4.587
|
||||||
|
c-3.951,1.242-6.148,5.452-4.907,9.403C281.352,144.716,284.309,146.767,287.498,146.767z"/>
|
||||||
|
<path d="M336.329,136.611c34.172-3.796,68.126-0.496,100.923,9.809c0.748,0.234,1.505,0.347,2.25,0.347
|
||||||
|
c3.189,0,6.146-2.051,7.153-5.254c1.241-3.952-0.956-8.162-4.907-9.403c-34.797-10.933-70.824-14.435-107.076-10.406
|
||||||
|
c-4.117,0.457-7.083,4.165-6.626,8.282C328.504,134.102,332.21,137.07,336.329,136.611z"/>
|
||||||
|
<path d="M93.96,317.749c-8.302,1.74-16.615,3.911-24.708,6.454c-3.952,1.242-6.148,5.452-4.907,9.403
|
||||||
|
c1.007,3.204,3.964,5.254,7.153,5.254c0.745,0,1.502-0.112,2.25-0.347c7.628-2.396,15.464-4.443,23.288-6.083
|
||||||
|
c4.054-0.85,6.652-4.825,5.802-8.879S98.011,316.9,93.96,317.749z"/>
|
||||||
|
<path d="M223.502,338.859c3.189,0,6.147-2.051,7.153-5.254c1.241-3.952-0.956-8.162-4.907-9.403
|
||||||
|
c-32.073-10.076-65.331-13.842-98.844-11.188c-4.129,0.326-7.211,3.938-6.885,8.068s3.934,7.213,8.068,6.885
|
||||||
|
c31.591-2.499,62.935,1.048,93.165,10.546C222,338.748,222.757,338.859,223.502,338.859z"/>
|
||||||
|
<path d="M71.498,306.767c0.745,0,1.502-0.112,2.25-0.347c48.249-15.159,99.256-15.159,147.504,0
|
||||||
|
c3.952,1.24,8.162-0.956,9.403-4.907c1.241-3.952-0.956-8.162-4.907-9.403c-51.191-16.083-105.307-16.083-156.496,0
|
||||||
|
c-3.952,1.241-6.149,5.451-4.907,9.403C65.352,304.716,68.309,306.767,71.498,306.767z"/>
|
||||||
|
<path d="M71.498,274.859c0.745,0,1.502-0.112,2.25-0.347c27.681-8.697,56.411-12.412,85.399-11.037
|
||||||
|
c4.158,0.192,7.65-2.999,7.847-7.137c0.196-4.138-2.999-7.65-7.137-7.847c-30.756-1.456-61.236,2.483-90.605,11.71
|
||||||
|
c-3.952,1.242-6.149,5.452-4.907,9.403C65.352,272.81,68.309,274.859,71.498,274.859z"/>
|
||||||
|
<path d="M190.194,266.932c10.467,1.844,20.917,4.395,31.058,7.581c0.749,0.235,1.505,0.347,2.25,0.347
|
||||||
|
c3.189,0,6.147-2.051,7.153-5.254c1.241-3.952-0.956-8.162-4.907-9.403c-10.76-3.38-21.846-6.086-32.952-8.043
|
||||||
|
c-4.079-0.719-7.969,2.006-8.688,6.085C183.39,262.323,186.114,266.213,190.194,266.932z"/>
|
||||||
|
<path d="M118.678,185.702c-16.732,1.858-33.362,5.36-49.426,10.407c-3.952,1.241-6.148,5.451-4.907,9.403
|
||||||
|
c1.007,3.204,3.964,5.254,7.153,5.254c0.745,0,1.502-0.112,2.25-0.347c15.141-4.757,30.815-8.057,46.585-9.809
|
||||||
|
c4.117-0.457,7.083-4.165,6.626-8.282C126.503,188.212,122.788,185.244,118.678,185.702z"/>
|
||||||
|
<path d="M64.345,173.605c1.007,3.204,3.964,5.254,7.153,5.254c0.745,0,1.502-0.112,2.25-0.347
|
||||||
|
c32.797-10.305,66.752-13.604,100.923-9.809c4.116,0.46,7.825-2.509,8.282-6.626c0.458-4.117-2.509-7.825-6.626-8.282
|
||||||
|
c-36.253-4.027-72.278-0.526-107.075,10.406C65.3,165.444,63.104,169.654,64.345,173.605z"/>
|
||||||
|
<path d="M71.498,146.767c0.745,0,1.502-0.112,2.25-0.347c5.103-1.604,10.325-3.058,15.521-4.324
|
||||||
|
c4.024-0.98,6.492-5.037,5.512-9.062s-5.038-6.492-9.062-5.512c-5.513,1.342-11.053,2.886-16.468,4.587
|
||||||
|
c-3.951,1.242-6.148,5.452-4.907,9.403C65.352,144.716,68.309,146.767,71.498,146.767z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.0 KiB |
@ -0,0 +1,34 @@
|
|||||||
|
import {formateaMoneda} from "./utils.js";
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
// Contador animado
|
||||||
|
function counter() {
|
||||||
|
var counter = document.querySelectorAll(".counter-value");
|
||||||
|
var speed = 250; // The lower the slower
|
||||||
|
counter &&
|
||||||
|
Array.from(counter).forEach(function (counter_value) {
|
||||||
|
function updateCount() {
|
||||||
|
var target = +counter_value.getAttribute("data-target");
|
||||||
|
var count = +counter_value.innerText;
|
||||||
|
var inc = target / speed;
|
||||||
|
if (inc < 1) {
|
||||||
|
inc = 1;
|
||||||
|
}
|
||||||
|
// Check if target is reached
|
||||||
|
if (count < target) {
|
||||||
|
// Add inc to count and output in counter_value
|
||||||
|
counter_value.innerText = (count + inc).toFixed(0);
|
||||||
|
// Call function every ms
|
||||||
|
setTimeout(updateCount, 1);
|
||||||
|
} else {
|
||||||
|
counter_value.innerText = formateaMoneda(target);
|
||||||
|
}
|
||||||
|
formateaMoneda(counter_value.innerText);
|
||||||
|
}
|
||||||
|
updateCount();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
counter();
|
||||||
|
|
||||||
|
})
|
||||||
@ -0,0 +1,216 @@
|
|||||||
|
$(() => {
|
||||||
|
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||||
|
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||||
|
if (window.$ && csrfToken && csrfHeader) {
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function (xhr) {
|
||||||
|
xhr.setRequestHeader(csrfHeader, csrfToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let $addBtn = $('#save-btn');
|
||||||
|
let $cancelBtn = $('#cancel-btn');
|
||||||
|
let $selectCliente = $('#clienteSelect');
|
||||||
|
let $serieInput = $('#serieInput');
|
||||||
|
let $direccionFacturacion = $('#direccionFacturacion');
|
||||||
|
let $facturaRectificada = $('#facturaRectificada');
|
||||||
|
let $divFacturaRectificada = $('#div-factura-rectificada');
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Initialize select2 for cliente selection
|
||||||
|
// -----------------------------
|
||||||
|
$selectCliente.select2({
|
||||||
|
placeholder: languageBundle['facturas.form.cliente.placeholder'],
|
||||||
|
width: '100%',
|
||||||
|
ajax: {
|
||||||
|
url: '/users/api/get-users',
|
||||||
|
dataType: 'json',
|
||||||
|
delay: 250,
|
||||||
|
data: function (params) {
|
||||||
|
return {
|
||||||
|
showUsername: true,
|
||||||
|
q: params.term,
|
||||||
|
page: params.page || 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
minimumInputLength: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
$selectCliente.on('select2:select', function (e) {
|
||||||
|
const data = e.params.data;
|
||||||
|
$serieInput.val(null).trigger('change');
|
||||||
|
$direccionFacturacion.val(null).trigger('change');
|
||||||
|
$facturaRectificada.val(null).trigger('change');
|
||||||
|
if (data && data.id) {
|
||||||
|
$serieInput.prop('disabled', false);
|
||||||
|
$direccionFacturacion.prop('disabled', false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$serieInput.prop('disabled', true);
|
||||||
|
$direccionFacturacion.prop('disabled', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$serieInput.select2({
|
||||||
|
placeholder: languageBundle['facturas.form.serie.placeholder'],
|
||||||
|
width: '100%',
|
||||||
|
ajax: {
|
||||||
|
url: '/configuracion/series-facturacion/api/get-series',
|
||||||
|
dataType: 'json',
|
||||||
|
delay: 250,
|
||||||
|
data: function (params) {
|
||||||
|
return {
|
||||||
|
q: params.term,
|
||||||
|
page: params.page || 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
minimumInputLength: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
$serieInput.on('select2:select', function (e) {
|
||||||
|
const data = e.params.data;
|
||||||
|
const defaultRectSerieId = $serieInput.data('default-serie-rect');
|
||||||
|
if (data && data.id) {
|
||||||
|
if (data.id === defaultRectSerieId) {
|
||||||
|
$divFacturaRectificada.removeClass('d-none');
|
||||||
|
$facturaRectificada.val(null).trigger('change');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$divFacturaRectificada.addClass('d-none');
|
||||||
|
$facturaRectificada.val(null).trigger('change');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$direccionFacturacion.select2({
|
||||||
|
placeholder: languageBundle['facturas.form.direccion-facturacion.placeholder'],
|
||||||
|
width: '100%',
|
||||||
|
ajax: {
|
||||||
|
url: '/facturas/api/get-direcciones',
|
||||||
|
dataType: 'json',
|
||||||
|
delay: 250,
|
||||||
|
data: function (params) {
|
||||||
|
const clienteId = $selectCliente.val();
|
||||||
|
return {
|
||||||
|
user_id: clienteId,
|
||||||
|
q: params.term,
|
||||||
|
page: params.page || 1
|
||||||
|
};
|
||||||
|
},
|
||||||
|
processResults: (data) => {
|
||||||
|
const items = Array.isArray(data) ? data : (data.results || []);
|
||||||
|
return {
|
||||||
|
results: items.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
text: item.text, // ← Select2 necesita 'id' y 'text'
|
||||||
|
alias: item.alias || 'Sin alias',
|
||||||
|
att: item.att || '',
|
||||||
|
direccion: item.direccion || '',
|
||||||
|
cp: item.cp || '',
|
||||||
|
ciudad: item.ciudad || '',
|
||||||
|
html: `
|
||||||
|
<div>
|
||||||
|
<strong>${item.alias || 'Sin alias'}</strong><br>
|
||||||
|
${item.att ? `<small>${item.att}</small><br>` : ''}
|
||||||
|
<small>${item.direccion || ''}${item.cp ? ', ' + item.cp : ''}${item.ciudad ? ', ' + item.ciudad : ''}</small>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})),
|
||||||
|
pagination: { more: false } // opcional, evita que espere más páginas
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
minimumInputLength: 0,
|
||||||
|
templateResult: data => {
|
||||||
|
if (data.loading) return data.text;
|
||||||
|
return $(data.html || data.text);
|
||||||
|
},
|
||||||
|
// Selección más compacta (solo alias + ciudad)
|
||||||
|
templateSelection: data => {
|
||||||
|
if (!data.id) return data.text;
|
||||||
|
const alias = data.alias || data.text;
|
||||||
|
const ciudad = data.ciudad ? ` — ${data.ciudad}` : '';
|
||||||
|
return $(`<span>${alias}${ciudad}</span>`);
|
||||||
|
},
|
||||||
|
escapeMarkup: m => m
|
||||||
|
});
|
||||||
|
|
||||||
|
$facturaRectificada.select2({
|
||||||
|
placeholder: languageBundle['facturas.form.factura-rectificada.placeholder'],
|
||||||
|
width: '100%',
|
||||||
|
ajax: {
|
||||||
|
url: '/facturas/api/get-facturas-rectificables',
|
||||||
|
dataType: 'json',
|
||||||
|
delay: 250,
|
||||||
|
data: function (params) {
|
||||||
|
const clienteId = $selectCliente.val();
|
||||||
|
return {
|
||||||
|
user_id: clienteId,
|
||||||
|
q: params.term,
|
||||||
|
page: params.page || 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
minimumInputLength: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Cancel button click
|
||||||
|
// -----------------------------
|
||||||
|
$cancelBtn.on('click', () => {
|
||||||
|
window.location.href = '/facturas';
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Save button click
|
||||||
|
// -----------------------------
|
||||||
|
$addBtn.on('click', () => {
|
||||||
|
const clienteId = $selectCliente.val();
|
||||||
|
const serieId = $serieInput.val();
|
||||||
|
const direccionId = $direccionFacturacion.val();
|
||||||
|
const facturaRectificadaId = $facturaRectificada.val();
|
||||||
|
|
||||||
|
if (!clienteId && !serieId && !direccionId) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: languageBundle['facturas.add.form.validation.title'],
|
||||||
|
text: languageBundle['facturas.add.form.validation']
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/facturas/add',
|
||||||
|
method: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
user: clienteId,
|
||||||
|
serie: serieId,
|
||||||
|
direccion: direccionId,
|
||||||
|
factura_rectificada: facturaRectificadaId
|
||||||
|
}),
|
||||||
|
success: function (response) {
|
||||||
|
if (response.success) {
|
||||||
|
window.location.href = '/facturas/' + response.facturaId;
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
text: response.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Error',
|
||||||
|
text: languageBundle['facturas.error.create']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,131 @@
|
|||||||
|
/* global $, bootstrap, window */
|
||||||
|
$(() => {
|
||||||
|
// si jQuery está cargado, añade CSRF a AJAX
|
||||||
|
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||||
|
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||||
|
if (window.$ && csrfToken && csrfHeader) {
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function (xhr) {
|
||||||
|
xhr.setRequestHeader(csrfHeader, csrfToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = document.documentElement.lang || 'es-ES';
|
||||||
|
|
||||||
|
const $table = $('#facturas-datatable'); // en tu HTML está así, aunque el id sea raro
|
||||||
|
const $addBtn = $('#addButton');
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// DataTable server-side
|
||||||
|
// -----------------------------
|
||||||
|
const dt = $table.DataTable({
|
||||||
|
processing: true,
|
||||||
|
serverSide: true,
|
||||||
|
searching: true,
|
||||||
|
orderMulti: false,
|
||||||
|
pageLength: 10,
|
||||||
|
lengthMenu: [10, 25, 50, 100],
|
||||||
|
|
||||||
|
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
||||||
|
|
||||||
|
ajax: {
|
||||||
|
url: '/facturas/api/datatables',
|
||||||
|
type: 'GET',
|
||||||
|
dataSrc: function (json) {
|
||||||
|
// DataTables espera {draw, recordsTotal, recordsFiltered, data}
|
||||||
|
return json.data || [];
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
console.error('DataTables error', xhr);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
columns: [
|
||||||
|
{ data: 'id' },
|
||||||
|
{ data: 'cliente' },
|
||||||
|
{ data: 'numero_factura' },
|
||||||
|
{ data: 'estado_label', name: 'estado' },
|
||||||
|
{ data: 'estado_pago_label', name: 'estado_pago' },
|
||||||
|
{ data: 'total' },
|
||||||
|
{ data: 'fecha_emision' },
|
||||||
|
{
|
||||||
|
data: 'actions',
|
||||||
|
orderable: false,
|
||||||
|
searchable: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
order: [[0, 'desc']]
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Add
|
||||||
|
// -----------------------------
|
||||||
|
$addBtn.on('click', () => {
|
||||||
|
window.location.href = '/facturas/add';
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Edit click
|
||||||
|
// -----------------------------
|
||||||
|
$table.on('click', '.btn-view-factura', function () {
|
||||||
|
const row = dt.row($(this).closest('tr')).data();
|
||||||
|
window.location.href = `/facturas/${row.id}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Delete click
|
||||||
|
// -----------------------------
|
||||||
|
$table.on('click', '.btn-delete-factura', function () {
|
||||||
|
const row = dt.row($(this).closest('tr')).data();
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: window.languageBundle.get(['facturas.delete.title']) || 'Eliminar factura',
|
||||||
|
html: window.languageBundle.get(['facturas.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;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: `/facturas/api/${row.id}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
success: function () {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success', title: window.languageBundle.get(['facturas.delete.ok.title']) || 'Eliminado',
|
||||||
|
text: window.languageBundle.get(['facturas.delete.ok.text']) || 'La factura ha sido eliminada correctamente.',
|
||||||
|
showConfirmButton: false,
|
||||||
|
timer: 1800,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary w-xs mt-2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dt.ajax.reload(null, false);
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
||||||
|
|| 'Error al eliminar la serie de facturación.';
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'No se pudo eliminar',
|
||||||
|
text: msg,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||||
|
cancelButton: 'btn btn-light' // clases para cancelar
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@ -0,0 +1,768 @@
|
|||||||
|
$(() => {
|
||||||
|
|
||||||
|
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||||
|
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||||
|
if (window.$ && csrfToken && csrfHeader) {
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function (xhr) {
|
||||||
|
xhr.setRequestHeader(csrfHeader, csrfToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const $container = $('#factura-container');
|
||||||
|
|
||||||
|
const MIN_LOADER_TIME = 500; // ms (ajusta a gusto)
|
||||||
|
let loaderStartTime = 0;
|
||||||
|
|
||||||
|
|
||||||
|
function showLoader() {
|
||||||
|
loaderStartTime = Date.now();
|
||||||
|
$('#factura-loader').removeClass('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLoader() {
|
||||||
|
const elapsed = Date.now() - loaderStartTime;
|
||||||
|
const remaining = Math.max(MIN_LOADER_TIME - elapsed, 0);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
$('#factura-loader').addClass('d-none');
|
||||||
|
}, remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function getFacturaId() {
|
||||||
|
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() {
|
||||||
|
const id = getFacturaId();
|
||||||
|
if (!id) return $.Deferred().reject('No factura id').promise();
|
||||||
|
|
||||||
|
showLoader();
|
||||||
|
|
||||||
|
return $.ajax({
|
||||||
|
url: `/facturas/${id}/container`,
|
||||||
|
method: 'GET',
|
||||||
|
cache: false,
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
})
|
||||||
|
.done((html) => {
|
||||||
|
const $c = $('#factura-container');
|
||||||
|
|
||||||
|
// conserva loader dentro del container
|
||||||
|
const $loader = $c.find('#factura-loader').detach();
|
||||||
|
|
||||||
|
// reemplaza el contenido completo
|
||||||
|
$c.empty().append($loader).append(html);
|
||||||
|
|
||||||
|
afterFacturaRender();
|
||||||
|
})
|
||||||
|
.fail((xhr) => {
|
||||||
|
console.error('Error recargando container:', xhr.status, xhr.responseText);
|
||||||
|
})
|
||||||
|
.always(() => {
|
||||||
|
hideLoader();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function postAndReload(url, data) {
|
||||||
|
showLoader();
|
||||||
|
const isJson = data !== undefined && data !== null;
|
||||||
|
|
||||||
|
return $.ajax({
|
||||||
|
url,
|
||||||
|
method: 'POST',
|
||||||
|
data: isJson ? JSON.stringify(data) : null,
|
||||||
|
contentType: isJson ? 'application/json; charset=UTF-8' : undefined,
|
||||||
|
processData: !isJson, // importante: si es JSON, no procesar
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
})
|
||||||
|
.done(() => reloadFacturaContainer())
|
||||||
|
.fail((xhr) => {
|
||||||
|
console.error('Error en acción:', xhr.status, xhr.responseText);
|
||||||
|
})
|
||||||
|
.always(() => {
|
||||||
|
hideLoader();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$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)
|
||||||
|
$container.on('click', '#btn-validar-factura', function () {
|
||||||
|
const id = getFacturaId();
|
||||||
|
postAndReload(`/facturas/${id}/validar`);
|
||||||
|
});
|
||||||
|
|
||||||
|
$container.on('click', '#btn-borrador-factura', function () {
|
||||||
|
const id = getFacturaId();
|
||||||
|
postAndReload(`/facturas/${id}/borrador`);
|
||||||
|
});
|
||||||
|
|
||||||
|
$container.on('focusin', 'textarea[name="notas"]', function () {
|
||||||
|
$(this).data('initial', $(this).val() ?? '');
|
||||||
|
});
|
||||||
|
|
||||||
|
$container.on('focusout', 'textarea[name="notas"]', function () {
|
||||||
|
const before = $(this).data('initial') ?? '';
|
||||||
|
const now = $(this).val() ?? '';
|
||||||
|
if (before === now) return;
|
||||||
|
|
||||||
|
const id = getFacturaId();
|
||||||
|
postAndReload(`/facturas/${id}/notas`, { notas: now });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$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) {
|
||||||
|
$root.find('.js-select2-factura').each(function () {
|
||||||
|
const $el = $(this);
|
||||||
|
if ($el.hasClass('select2-hidden-accessible')) {
|
||||||
|
$el.select2('destroy');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSelect2ForDraft($root) {
|
||||||
|
$root.find('.js-select2-factura').each(function () {
|
||||||
|
const $el = $(this);
|
||||||
|
|
||||||
|
// evita doble init
|
||||||
|
if ($el.hasClass('select2-hidden-accessible')) return;
|
||||||
|
|
||||||
|
const url = $el.data('url');
|
||||||
|
const placeholder = $el.data('placeholder') || '';
|
||||||
|
|
||||||
|
$el.select2({
|
||||||
|
width: '100%',
|
||||||
|
placeholder,
|
||||||
|
allowClear: true,
|
||||||
|
minimumInputLength: 0,
|
||||||
|
|
||||||
|
ajax: {
|
||||||
|
url: url,
|
||||||
|
dataType: 'json',
|
||||||
|
delay: 250,
|
||||||
|
|
||||||
|
data: function (params) {
|
||||||
|
return {
|
||||||
|
q: params.term, // texto buscado
|
||||||
|
page: params.page || 1
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
processResults: function (data) {
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
cache: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 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() {
|
||||||
|
const $root = $('#factura-container');
|
||||||
|
const estado = $root.find('[data-factura-estado]').first().data('factura-estado');
|
||||||
|
|
||||||
|
destroySelect2($root);
|
||||||
|
|
||||||
|
if (estado === 'borrador') {
|
||||||
|
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();
|
||||||
|
});
|
||||||
@ -42,7 +42,7 @@ $(() => {
|
|||||||
if (estadoSpan.length) {
|
if (estadoSpan.length) {
|
||||||
estadoSpan.text(response.state);
|
estadoSpan.text(response.state);
|
||||||
}
|
}
|
||||||
if (response.stateKey === 'terminado' || response.stateKey === 'cancelado') {
|
if (response.stateKey === 'enviado' || response.stateKey === 'cancelado') {
|
||||||
$(`.update-estado-button[data-linea-id='${lineaId}']`)
|
$(`.update-estado-button[data-linea-id='${lineaId}']`)
|
||||||
.closest('.update-estado-button')
|
.closest('.update-estado-button')
|
||||||
.addClass('d-none');
|
.addClass('d-none');
|
||||||
|
|||||||
@ -83,4 +83,20 @@ $(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($(".btn-download-factura").length) {
|
||||||
|
$(document).on('click', '.btn-download-factura', function () {
|
||||||
|
const facturaId = $(this).data('factura-id');
|
||||||
|
|
||||||
|
const url = `/api/pdf/factura/${facturaId}?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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -318,6 +318,50 @@ export default class PresupuestoWizard {
|
|||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document)
|
||||||
|
.off('click.login-required', '.btn-login-required')
|
||||||
|
.on('click.login-required', '.btn-login-required', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const rawId = this.opts.presupuestoId || window.PRESUPUESTO_ID || $('#presupuesto_id').val();
|
||||||
|
const presupuestoId = rawId ? parseInt(rawId, 10) : null;
|
||||||
|
|
||||||
|
if (!presupuestoId || Number.isNaN(presupuestoId)) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'No se encontró el presupuesto',
|
||||||
|
text: 'Vuelve a generar el resumen e inténtalo de nuevo.',
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2',
|
||||||
|
cancelButton: 'btn btn-light'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $.ajax({
|
||||||
|
url: '/presupuesto/public/prepare-claim',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({ presupuestoId })
|
||||||
|
});
|
||||||
|
window.location.assign('/presupuesto/claim');
|
||||||
|
} catch (err) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'No se pudo continuar',
|
||||||
|
text: 'Inténtalo de nuevo en unos segundos.',
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2',
|
||||||
|
cancelButton: 'btn btn-light'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1853,6 +1897,15 @@ export default class PresupuestoWizard {
|
|||||||
...result,
|
...result,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!this.formData.servicios.servicios.some(s => s.id === "marcapaginas") && result.precio > 0) {
|
||||||
|
this.formData.servicios.servicios.push({
|
||||||
|
id: "marcapaginas",
|
||||||
|
label: $(`label[for="marcapaginas"] .service-title`).text().trim(),
|
||||||
|
units: 1,
|
||||||
|
price: result.precio,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.#cacheFormData();
|
this.#cacheFormData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { duplicar, reimprimir } from './presupuesto-utils.js';
|
||||||
(() => {
|
(() => {
|
||||||
// si jQuery está cargado, añade CSRF a AJAX
|
// si jQuery está cargado, añade CSRF a AJAX
|
||||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||||
@ -124,6 +125,27 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#presupuestos-clientes-user-datatable').on('click', '.btn-duplicate-privado', function (e) {
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const id = $(this).data('id');
|
||||||
|
let data = table.row($(this).parents('tr')).data();
|
||||||
|
const tituloOriginal = data.titulo;
|
||||||
|
|
||||||
|
duplicar(id, tituloOriginal);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#presupuestos-clientes-user-datatable').on('click', '.btn-reprint-privado', function (e) {
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const id = $(this).data('id');
|
||||||
|
let data = table.row($(this).parents('tr')).data();
|
||||||
|
const tituloOriginal = data.titulo;
|
||||||
|
|
||||||
|
reimprimir(id, tituloOriginal);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
$('#presupuestos-clientes-user-datatable').on('keyup', '.presupuesto-filter', function (e) {
|
$('#presupuestos-clientes-user-datatable').on('keyup', '.presupuesto-filter', function (e) {
|
||||||
const colName = $(this).data('col');
|
const colName = $(this).data('col');
|
||||||
const colIndex = table.column(colName + ':name').index();
|
const colIndex = table.column(colName + ':name').index();
|
||||||
|
|||||||
@ -147,6 +147,55 @@ $(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Botón "Entrar como"
|
||||||
|
$(document).on('click', '.btn-impersonate-user', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const username = $(this).data('username');
|
||||||
|
|
||||||
|
const title = window.languageBundle.get(['usuarios.impersonate.title']) || 'Entrar como usuario';
|
||||||
|
const textTpl = window.languageBundle.get(['usuarios.impersonate.text'])
|
||||||
|
|| 'Vas a iniciar sesión como <b>{0}</b>.';
|
||||||
|
const confirmText = window.languageBundle.get(['usuarios.impersonate.button']) || 'Entrar';
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title,
|
||||||
|
html: textTpl.replace('{0}', username),
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-info w-xs mt-2',
|
||||||
|
cancelButton: 'btn btn-light w-xs mt-2'
|
||||||
|
},
|
||||||
|
confirmButtonText: confirmText,
|
||||||
|
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
|
||||||
|
}).then((result) => {
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/impersonate',
|
||||||
|
type: 'POST',
|
||||||
|
data: { username },
|
||||||
|
success: function () {
|
||||||
|
window.location.href = '/';
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
||||||
|
|| 'No se pudo iniciar sesión como ese usuario.';
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'No se pudo suplantar',
|
||||||
|
text: msg,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Submit del form en el modal
|
// Submit del form en el modal
|
||||||
$(document).on('submit', '#userForm', function (e) {
|
$(document).on('submit', '#userForm', function (e) {
|
||||||
|
|||||||
@ -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í
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,120 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
|
layout:decorate="~{imprimelibros/layout}">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<th:block layout:fragment="pagetitle" />
|
||||||
|
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
|
||||||
|
<th:block layout:fragment="pagecss">
|
||||||
|
<link th:href="@{/assets/css/presupuestador.css}" rel="stylesheet"
|
||||||
|
th:unless="${#authorization.expression('isAuthenticated()')}" />
|
||||||
|
<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>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div th:replace="~{imprimelibros/partials/topbar :: topbar}" />
|
||||||
|
<div th:replace="~{imprimelibros/partials/sidebar :: sidebar}" />
|
||||||
|
|
||||||
|
<th:block layout:fragment="content">
|
||||||
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
|
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="/facturas" th:text="#{facturas.breadcrumb}"></a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page" th:text="#{facturas.breadcrumb.nueva}">
|
||||||
|
Nueva factura</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid position-relative">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-6 mb-3">
|
||||||
|
<label for="clienteSelect" class="form-label" th:text="#{facturas.form.cliente}">Cliente</label>
|
||||||
|
<select id="clienteSelect" class="form-select select2"
|
||||||
|
th:placeholder="#{facturas.form.cliente.placeholder}">
|
||||||
|
<option th:each="cliente : ${clientes}" th:value="${cliente.id}"
|
||||||
|
th:text="${cliente.nombre} + ' (' + cliente.email + ')'"></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-6 mb-3">
|
||||||
|
<label for="serieInput" class="form-label" th:text="#{facturas.form.serie}">Serie de
|
||||||
|
facturación</label>
|
||||||
|
<select id="serieInput" class="form-select select2" disabled
|
||||||
|
th:data-default-serie-rect="${defaultSerieRectificativa}">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- salto de fila SOLO en md+ -->
|
||||||
|
<div class="w-100 d-none d-md-block"></div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label for="direccionFacturacion" class="form-label"
|
||||||
|
th:text="#{facturas.form.direccion-facturacion}">
|
||||||
|
Factura rectificada
|
||||||
|
</label>
|
||||||
|
<select id="direccionFacturacion" class="form-select select2" disabled></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6 mb-3 d-none" id="div-factura-rectificada">
|
||||||
|
<label for="facturaRectificada" class="form-label"
|
||||||
|
th:text="#{facturas.form.factura-rectificada}">
|
||||||
|
Factura rectificada
|
||||||
|
</label>
|
||||||
|
<select id="facturaRectificada" class="form-select select2"></select>
|
||||||
|
</div>
|
||||||
|
</div> <!-- end row -->
|
||||||
|
|
||||||
|
<div class="row mt-3 justify-content-md-end g-2">
|
||||||
|
<div class="col-12 col-md-auto">
|
||||||
|
<button type="button" th:text="#{app.guardar}" id="save-btn" class="btn btn-secondary w-100">
|
||||||
|
Guardar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-auto">
|
||||||
|
<button type="button" th:text="#{app.cancelar}" id="cancel-btn" class="btn btn-light w-100">
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<th:block layout:fragment="modal" />
|
||||||
|
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
|
||||||
|
<th:block layout:fragment="pagejs">
|
||||||
|
<script th:inline="javascript">
|
||||||
|
window.languageBundle = /*[[${languageBundle}]]*/ {};
|
||||||
|
</script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/datatables.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/dataTables.bootstrap5.min.js}"></script>
|
||||||
|
|
||||||
|
<!-- JS de Buttons y dependencias -->
|
||||||
|
<script th:src="@{/assets/libs/datatables/dataTables.buttons.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/jszip/jszip.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/pdfmake/pdfmake.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/pdfmake/vfs_fonts.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/buttons.html5.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 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/add.js}"></script>
|
||||||
|
|
||||||
|
</th:block>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
|
layout:decorate="~{imprimelibros/layout}">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<th:block layout:fragment="pagetitle" />
|
||||||
|
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
|
||||||
|
<th:block layout:fragment="pagecss">
|
||||||
|
<link th:href="@{/assets/css/presupuestador.css}" rel="stylesheet"
|
||||||
|
th:unless="${#authorization.expression('isAuthenticated()')}" />
|
||||||
|
<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>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div th:replace="~{imprimelibros/partials/topbar :: topbar}" />
|
||||||
|
<div th:replace="~{imprimelibros/partials/sidebar :: sidebar}" />
|
||||||
|
|
||||||
|
<th:block layout:fragment="content">
|
||||||
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
|
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="/facturas" th:text="#{facturas.breadcrumb}"></a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page" th:text="#{facturas.breadcrumb.ver}">
|
||||||
|
Ver factura</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid position-relative" id="factura-container"
|
||||||
|
th:attr="data-factura-id=${factura.id}">
|
||||||
|
|
||||||
|
<!-- overlay loader -->
|
||||||
|
<div id="factura-loader" class="d-none position-absolute top-0 start-0 w-100 h-100"
|
||||||
|
style="background: rgba(255,255,255,.6); z-index: 10;">
|
||||||
|
<div class="position-absolute top-50 start-50 translate-middle">
|
||||||
|
<div class="spinner-border" role="status" aria-hidden="true"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="factura-inner"
|
||||||
|
th:replace="~{imprimelibros/facturas/partials/factura-container :: factura-container (factura=${factura})}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<th:block layout:fragment="modal" />
|
||||||
|
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
|
||||||
|
<th:block layout:fragment="pagejs">
|
||||||
|
<script th:inline="javascript">
|
||||||
|
window.languageBundle = /*[[${languageBundle}]]*/ {};
|
||||||
|
</script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/datatables.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/dataTables.bootstrap5.min.js}"></script>
|
||||||
|
|
||||||
|
<!-- JS de Buttons y dependencias -->
|
||||||
|
<script th:src="@{/assets/libs/datatables/dataTables.buttons.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/jszip/jszip.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/pdfmake/pdfmake.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/pdfmake/vfs_fonts.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/buttons.html5.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 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>
|
||||||
|
|
||||||
|
</th:block>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
|
layout:decorate="~{imprimelibros/layout}">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<th:block layout:fragment="pagetitle" />
|
||||||
|
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
|
||||||
|
<th:block layout:fragment="pagecss">
|
||||||
|
<link th:href="@{/assets/css/presupuestador.css}" rel="stylesheet"
|
||||||
|
th:unless="${#authorization.expression('isAuthenticated()')}" />
|
||||||
|
<link th:href="@{/assets/libs/datatables/dataTables.bootstrap5.min.css}" rel="stylesheet" />
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div th:replace="~{imprimelibros/partials/topbar :: topbar}" />
|
||||||
|
<div th:replace="~{imprimelibros/partials/sidebar :: sidebar}" />
|
||||||
|
|
||||||
|
<th:block layout:fragment="content">
|
||||||
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
|
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page" th:text="#{facturas.breadcrumb}">
|
||||||
|
Facturas</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-secondary mb-3" id="addButton">
|
||||||
|
<i class="ri-add-line align-bottom me-1"></i> <span
|
||||||
|
th:text="#{app.add}">Añadir</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<table id="facturas-datatable" class="table table-striped table-nowrap responsive w-100">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-start" scope="col" th:text="#{facturas.tabla.id}">ID</th>
|
||||||
|
<th class="text-start" scope="col" th:text="#{facturas.tabla.cliente}">Cliente</th>
|
||||||
|
<th class="text-start" scope="col" th:text="#{facturas.tabla.num-factura}">Número de Factura</th>
|
||||||
|
<th class="text-start" scope="col" th:text="#{facturas.tabla.estado}">Estado</th>
|
||||||
|
<th class="text-start" scope="col" th:text="#{facturas.tabla.estado-pago}">Estado de Pago</th>
|
||||||
|
<th class="text-start" scope="col" th:text="#{facturas.tabla.total}">Total</th>
|
||||||
|
<th class="text-start" scope="col" th:text="#{facturas.tabla.fecha-emision}">Fecha de Emisión</th>
|
||||||
|
<th class="text-start" scope="col" th:text="#{facturas.tabla.acciones}">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<th:block layout:fragment="modal" />
|
||||||
|
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
|
||||||
|
<th:block layout:fragment="pagejs">
|
||||||
|
<script th:inline="javascript">
|
||||||
|
window.languageBundle = /*[[${languageBundle}]]*/ {};
|
||||||
|
</script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/datatables.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/dataTables.bootstrap5.min.js}"></script>
|
||||||
|
|
||||||
|
<!-- JS de Buttons y dependencias -->
|
||||||
|
<script th:src="@{/assets/libs/datatables/dataTables.buttons.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/jszip/jszip.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/pdfmake/pdfmake.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/pdfmake/vfs_fonts.min.js}"></script>
|
||||||
|
<script th:src="@{/assets/libs/datatables/buttons.html5.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 type="module" th:src="@{/assets/js/pages/imprimelibros/facturas/list.js}"></script>
|
||||||
|
|
||||||
|
</th:block>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,165 @@
|
|||||||
|
<div th:fragment="factura-cabecera (factura)">
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
<span th:class="|text-${factura.estado.name() == 'borrador' ? 'warning' : 'success'}|"
|
||||||
|
th:text="#{|facturas.estado.${factura.estado.name()}|}">
|
||||||
|
</span>
|
||||||
|
/
|
||||||
|
<span th:class="|text-${factura.estadoPago.name() == 'pendiente' ? 'warning' : 'success'}|"
|
||||||
|
th:text="#{|facturas.estado-pago.${factura.estadoPago.name()}|}">
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- flag readonly -->
|
||||||
|
<th:block th:with="isReadonly=${factura.estado.name() == 'validada'}">
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
|
||||||
|
<!-- Número (solo lectura siempre, normalmente) -->
|
||||||
|
<div th:if="${factura.numeroFactura != null}" class="col-md-3">
|
||||||
|
<label class="form-label" th:text="#{facturas.form.numero-factura}">Número de factura</label>
|
||||||
|
<input id="facturaNumero" type="text" class="form-control" th:value="${factura.numeroFactura}" readonly>
|
||||||
|
</div>
|
||||||
|
<div th:if="${factura.numeroFactura == null}" class="col-md-3">
|
||||||
|
<label class="form-label" th:text="#{facturas.form.id}">ID de la factura</label>
|
||||||
|
<input id="facturaId" type="text" class="form-control" th:value="${factura.id}" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Serie -->
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label" th:text="#{facturas.form.serie}">Serie</label>
|
||||||
|
<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 : ''}"
|
||||||
|
th:text="${factura.serie != null ? factura.serie.nombreSerie : ''}" selected>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cliente -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" th:text="#{facturas.form.cliente}">Cliente</label>
|
||||||
|
<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 : ''}"
|
||||||
|
th:text="${factura.cliente != null ? factura.cliente.fullName : ''}" selected>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Factura rectificada -->
|
||||||
|
<div th:if="${factura.facturaRectificada != null}" class="w-100 d-md-block"></div>
|
||||||
|
<div th:if="${factura.facturaRectificada != null}" class="col-md-3">
|
||||||
|
<label class="form-label" th:text="#{facturas.form.factura-rectificada}">Factura rectificada</label>
|
||||||
|
<input readonly id="facturaRectificadaId" class="form-control"
|
||||||
|
th:value="${factura.facturaRectificada.numeroFactura}" />
|
||||||
|
</div>
|
||||||
|
<div th:if="${factura.facturaRectificada != null}" class="w-100 d-md-block"></div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label" th:text="#{facturas.form.fecha-emision}">Fecha</label>
|
||||||
|
|
||||||
|
<input id="facturaFechaEmision" type="text" class="form-control"
|
||||||
|
th:value="${factura.fechaEmision != null ? #temporals.format(factura.fechaEmision, 'dd/MM/yyyy') : ''}"
|
||||||
|
th:attr="readonly=${isReadonly}, data-estado=${factura.estado.name()}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notas -->
|
||||||
|
<div class="col-md-9">
|
||||||
|
<label class="form-label" th:text="#{facturas.form.notas}">Notas</label>
|
||||||
|
<textarea class="form-control" rows="3" name="notas" th:text="${factura.notas}">
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h5 class="mt-4" th:text="#{facturas.direccion.titulo}">Dirección de facturación</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" th:text="#{facturas.direccion.razon-social}">Razón Social</label>
|
||||||
|
<input type="text" id="dirRazonSocial" class="form-control" th:value="${direccionFacturacion != null
|
||||||
|
? direccionFacturacion.razonSocial
|
||||||
|
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label" th:text="#{facturas.direccion.identificacion-fiscal}">Identificacion
|
||||||
|
Fiscal</label>
|
||||||
|
<input type="text" id="dirIdentificacionFiscal" class="form-control" th:value="${direccionFacturacion != null
|
||||||
|
? direccionFacturacion.identificacionFiscal
|
||||||
|
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-md-9">
|
||||||
|
<label class="form-label" th:text="#{facturas.direccion.direccion}">Dirección</label>
|
||||||
|
<input type="text" id="dirDireccion" class="form-control" th:value="${direccionFacturacion != null
|
||||||
|
? direccionFacturacion.direccion
|
||||||
|
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label" th:text="#{facturas.direccion.codigo-postal}">Código Postal</label>
|
||||||
|
<input type="text" id="dirCp" class="form-control" th:value="${direccionFacturacion != null
|
||||||
|
? direccionFacturacion.cp
|
||||||
|
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label" th:text="#{facturas.direccion.ciudad}">Ciudad</label>
|
||||||
|
<input type="text" id="dirCiudad" class="form-control" th:value="${direccionFacturacion != null
|
||||||
|
? direccionFacturacion.ciudad
|
||||||
|
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label" th:text="#{facturas.direccion.provincia}">Provincia</label>
|
||||||
|
<input type="text" id="dirProvincia" class="form-control" th:value="${direccionFacturacion != null
|
||||||
|
? direccionFacturacion.provincia
|
||||||
|
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label" th:text="#{facturas.direccion.pais}">País</label>
|
||||||
|
<select id="dirPais" class="form-control js-select2-factura" data-url="/api/paises"
|
||||||
|
th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
|
<option th:value="${direccionFacturacion != null
|
||||||
|
? direccionFacturacion.pais.code3
|
||||||
|
: ''}" th:text="${direccionFacturacion != null
|
||||||
|
? #messages.msg('paises.' + direccionFacturacion.pais.keyword)
|
||||||
|
: ''}" selected>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label" th:text="#{facturas.direccion.telefono}">Teléfono</label>
|
||||||
|
<input type="text" id="dirTelefono" class="form-control" th:value="${direccionFacturacion != null
|
||||||
|
? direccionFacturacion.telefono
|
||||||
|
: ''}" th:attrappend="readonly=${isReadonly} ? 'readonly' : null">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3 mt-4 justify-content-end">
|
||||||
|
|
||||||
|
<div class="col-md-12 text-end">
|
||||||
|
<th:block th:if="${factura.estado.name() == 'borrador'}">
|
||||||
|
<button type="button" class="btn btn-secondary me-2" id="btn-validar-factura"
|
||||||
|
th:text="#{facturas.form.btn.validar}">Validar factura</button>
|
||||||
|
<button type="button" class="btn btn-secondary me-2" id="btn-guardar-factura"
|
||||||
|
th:text="#{facturas.form.btn.guardar}">Guardar</button>
|
||||||
|
</th:block>
|
||||||
|
<th:block th:if="${factura.estado.name() == 'validada'}">
|
||||||
|
<button type="button" class="btn btn-secondary me-2" id="btn-borrador-factura"
|
||||||
|
th:text="#{facturas.form.btn.borrador}">Pasar a borrador</button>
|
||||||
|
</th:block>
|
||||||
|
<button type="button" class="btn btn-secondary me-2" id="btn-imprimir-factura"
|
||||||
|
th:text="#{facturas.form.btn.imprimir}">Imprimir factura</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
<div th:fragment="factura-container (factura)"
|
||||||
|
th:attr="data-factura-estado=${factura.estado.name()}">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="accordion accordion-fill-imprimelibros mb-3" id="cabeceraFactura">
|
||||||
|
<div class="accordion-item material-shadow">
|
||||||
|
<h2 class="accordion-header" id="cabeceraHeader">
|
||||||
|
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#cabecera" aria-expanded="true" aria-controls="cabecera">
|
||||||
|
Datos de la factura
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="cabecera" class="accordion-collapse collapse show" aria-labelledby="cabeceraHeader"
|
||||||
|
data-bs-parent="#cabeceraFactura">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div th:replace="~{imprimelibros/facturas/partials/factura-cabecera :: factura-cabecera (factura=${factura})}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion accordion-fill-imprimelibros mb-3" id="lineasFactura">
|
||||||
|
<div class="accordion-item material-shadow">
|
||||||
|
<h2 class="accordion-header" id="lineasHeader">
|
||||||
|
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#lineas" aria-expanded="true" aria-controls="lineas">
|
||||||
|
Líneas de factura
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="lineas" class="accordion-collapse collapse show" aria-labelledby="lineasHeader"
|
||||||
|
data-bs-parent="#lineasFactura">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div th:replace="~{imprimelibros/facturas/partials/factura-lineas :: factura-lineas (factura=${factura})}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion accordion-fill-imprimelibros mb-3" id="pagosFactura">
|
||||||
|
<div class="accordion-item material-shadow">
|
||||||
|
<h2 class="accordion-header" id="pagosHeader">
|
||||||
|
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#pagos" aria-expanded="true" aria-controls="pagos">
|
||||||
|
Pagos de factura
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="pagos" class="accordion-collapse collapse show" aria-labelledby="pagosHeader"
|
||||||
|
data-bs-parent="#pagosFactura">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div th:replace="~{imprimelibros/facturas/partials/factura-pagos :: factura-pagos (factura=${factura})}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
<div th:fragment="factura-lineas (factura)">
|
||||||
|
<th:block th:if="${factura.estado != null && factura.estado.name() == 'borrador'}">
|
||||||
|
<div class="mb-3">
|
||||||
|
<button type="button" class="btn btn-secondary" id="btn-add-linea-factura">
|
||||||
|
<i class="fas fa-plus-circle me-2"></i>
|
||||||
|
<span th:text="#{facturas.lineas.acciones.agregar}">Agregar línea</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
<table class="table table-bordered table-striped table-wrap w-100">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<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 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_21}">I.V.A. 21%</th>
|
||||||
|
<th th:text="#{facturas.lineas.total}">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="lineaFactura : ${factura.lineas}">
|
||||||
|
<td th:if="${factura.estado != null && factura.estado.name() == 'borrador'}">
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm me-2 btn-edit-linea-factura" th:attr="
|
||||||
|
data-linea-id=${lineaFactura.id},
|
||||||
|
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 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>
|
||||||
|
</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 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"
|
||||||
|
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>
|
||||||
|
</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" colspan="1" th:text="${#numbers.formatCurrency(factura.iva4)}">0.00</td>
|
||||||
|
</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" colspan="1" th:text="${#numbers.formatCurrency(factura.iva21)}">0.00</td>
|
||||||
|
</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" colspan="1" th:text="${#numbers.formatCurrency(factura.totalFactura)}">0.00
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Modal líneas factura (crear/editar) -->
|
||||||
|
<th:block th:replace="~{imprimelibros/facturas/partials/linea-modal :: linea-modal}"></th:block>
|
||||||
|
|
||||||
|
</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>
|
||||||
@ -0,0 +1,105 @@
|
|||||||
|
<div id="fidelity-banner" th:fragment="home-container-user">
|
||||||
|
|
||||||
|
<div class="ib-loyalty-banner">
|
||||||
|
|
||||||
|
<!-- Decoraciones -->
|
||||||
|
<div class="decor"></div>
|
||||||
|
<div class="book small"></div>
|
||||||
|
|
||||||
|
<div class="ib-loyalty-inner">
|
||||||
|
|
||||||
|
<!-- ===================== -->
|
||||||
|
<!-- COLUMNA IZQUIERDA -->
|
||||||
|
<!-- ===================== -->
|
||||||
|
<div class="ib-loyalty-left">
|
||||||
|
|
||||||
|
<!-- PANEL SUPERIOR: TÍTULO -->
|
||||||
|
<div class="ib-loyalty-hero">
|
||||||
|
<div class="ib-loyalty-head">
|
||||||
|
<div class="ib-loyalty-logo">
|
||||||
|
<img src="/assets/images/logo-sm.png" alt="Logo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="ib-loyalty-title">Programa de Fidelidad</h3>
|
||||||
|
<p class="ib-loyalty-sub">
|
||||||
|
Aumenta tus compras en los últimos 12 meses y obtén descuentos automáticos.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PANEL INFERIOR: ESTADÍSTICA (RECUADRO INDEPENDIENTE) -->
|
||||||
|
<div class="ib-loyalty-stat-card">
|
||||||
|
|
||||||
|
<h6 class="text-uppercase fs-13 mb-3">
|
||||||
|
<span th:text="#{pedido.gasto-anual}">Gasto últimos 12 meses</span>
|
||||||
|
<i class="ri-arrow-up-circle-line text-success fs-18 float-end align-middle"></i>
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<i class="ri-money-euro-circle-line display-6 "></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ms-3">
|
||||||
|
<h2 class="mb-0">
|
||||||
|
<h2 class="mb-0"><span class="counter-value" th:attr="data-target=${totalGastado}">0</span></h2>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== -->
|
||||||
|
<!-- COLUMNA DERECHA -->
|
||||||
|
<!-- ===================== -->
|
||||||
|
<div class="ib-rewards">
|
||||||
|
|
||||||
|
<h6>Recompensas</h6>
|
||||||
|
|
||||||
|
<div class="ib-rewards-grid">
|
||||||
|
|
||||||
|
<div class="ib-card">
|
||||||
|
<div class="range">Menos de 1.200€</div>
|
||||||
|
<div class="percent">0%</div>
|
||||||
|
<span class="chip">Descuento</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ib-card">
|
||||||
|
<div class="range">1.200€ – 1.999€</div>
|
||||||
|
<div class="percent">1%</div>
|
||||||
|
<span class="chip">Descuento</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ib-card">
|
||||||
|
<div class="range">2.000€ – 2.999€</div>
|
||||||
|
<div class="percent">2%</div>
|
||||||
|
<span class="chip">Descuento</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ib-card">
|
||||||
|
<div class="range">3.000€ – 3.999€</div>
|
||||||
|
<div class="percent">3%</div>
|
||||||
|
<span class="chip">Descuento</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ib-card">
|
||||||
|
<div class="range">4.000€ – 4.999€</div>
|
||||||
|
<div class="percent">4%</div>
|
||||||
|
<span class="chip">Descuento</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ib-card">
|
||||||
|
<div class="range">Más de 5.000€</div>
|
||||||
|
<div class="percent">5%</div>
|
||||||
|
<span class="chip">Descuento</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -8,6 +8,8 @@
|
|||||||
<th:block layout:fragment="pagecss">
|
<th:block layout:fragment="pagecss">
|
||||||
<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/css/home.css}" rel="stylesheet"
|
||||||
|
th:if="${#authorization.expression('isAuthenticated()')}" />
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
@ -22,7 +24,7 @@
|
|||||||
<th:block layout:fragment="content">
|
<th:block layout:fragment="content">
|
||||||
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
<div th:insert="~{imprimelibros/home/home-container-user :: home-container-user}"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div th:unless="${#authorization.expression('isAuthenticated()')}">
|
<div th:unless="${#authorization.expression('isAuthenticated()')}">
|
||||||
@ -41,6 +43,9 @@
|
|||||||
<script type="module"
|
<script type="module"
|
||||||
th:src="@{/assets/js/pages/imprimelibros/presupuestador/presupuesto-marcapaginas.js}"></script>
|
th:src="@{/assets/js/pages/imprimelibros/presupuestador/presupuesto-marcapaginas.js}"></script>
|
||||||
</div>
|
</div>
|
||||||
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
|
<script type="module" th:src="@{/assets/js/pages/imprimelibros/counter-widget.js}"></script>
|
||||||
|
</div>
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
window.languageBundle = /*[[${languageBundle}]]*/ {};
|
window.languageBundle = /*[[${languageBundle}]]*/ {};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -48,6 +48,11 @@
|
|||||||
<i class="ri-book-3-line"></i> <span th:text="#{app.sidebar.pedidos}">Pedidos</span>
|
<i class="ri-book-3-line"></i> <span th:text="#{app.sidebar.pedidos}">Pedidos</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li th:if="${#authentication.principal.role == 'SUPERADMIN' or #authentication.principal.role == 'ADMIN'}" class="nav-item">
|
||||||
|
<a class="nav-link menu-link" href="/facturas">
|
||||||
|
<i class="ri-bill-line"></i> <span th:text="#{app.sidebar.facturas}">Facturas</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link menu-link" href="/direcciones">
|
<a class="nav-link menu-link" href="/direcciones">
|
||||||
<i class="ri-truck-line"></i>
|
<i class="ri-truck-line"></i>
|
||||||
|
|||||||
@ -100,9 +100,14 @@
|
|||||||
<a class="dropdown-item" href="/pages-profile"><i
|
<a class="dropdown-item" href="/pages-profile"><i
|
||||||
class="mdi mdi-account-circle text-muted fs-16 align-middle me-1"></i> <span
|
class="mdi mdi-account-circle text-muted fs-16 align-middle me-1"></i> <span
|
||||||
class="align-middle" th:text="#{app.perfil}">Perfil</span></a>
|
class="align-middle" th:text="#{app.perfil}">Perfil</span></a>
|
||||||
<a class="dropdown-item" href="/apps-chat"><i
|
<div sec:authorize="hasRole('PREVIOUS_ADMINISTRATOR')">
|
||||||
class="mdi mdi-message-text-outline text-muted fs-16 align-middle me-1"></i>
|
<div class="dropdown-divider"></div>
|
||||||
<span class="align-middle" th:text="#{app.mensajes}">Mensajes</span></a>
|
<a class="dropdown-item" href="#"
|
||||||
|
onclick="document.getElementById('exitImpersonationForm').submit(); return false;">
|
||||||
|
<i class="mdi mdi-account-switch text-muted fs-16 align-middle me-1"></i>
|
||||||
|
<span class="align-middle" th:text="#{app.impersonate.exit}">Volver a mi usuario</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a class="dropdown-item" href="#"
|
<a class="dropdown-item" href="#"
|
||||||
onclick="document.getElementById('logoutForm').submit(); return false;">
|
onclick="document.getElementById('logoutForm').submit(); return false;">
|
||||||
@ -127,7 +132,10 @@
|
|||||||
<form id="logoutForm" th:action="@{/logout}" method="post" class="d-none">
|
<form id="logoutForm" th:action="@{/logout}" method="post" class="d-none">
|
||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
</form>
|
</form>
|
||||||
|
<form id="exitImpersonationForm" th:action="@{/impersonate/exit}" method="post" class="d-none">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
|
</form>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
164
src/main/resources/templates/imprimelibros/pdf/factura-a4.html
Normal file
164
src/main/resources/templates/imprimelibros/pdf/factura-a4.html
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
<!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">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- PIE -->
|
||||||
|
<div class="pdf-footer-running">
|
||||||
|
<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> </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 class="num" th:text="#{pdf.factura.lineas.base}">Base</th>
|
||||||
|
<th class="num" th:text="#{pdf.factura.lineas.iva_4}">I.V.A. 4%</th>
|
||||||
|
<th class="num" th:text="#{pdf.factura.lineas.iva_21}">I.V.A. 21%</th>
|
||||||
|
<th class="num" th:text="#{pdf.factura.lineas.total}">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="lineaFactura : ${factura.lineas}">
|
||||||
|
<td class="desc" 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>
|
||||||
@ -73,24 +73,24 @@
|
|||||||
Aceptar ferro
|
Aceptar ferro
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button th:if="${item.estado.priority >= 7 && item.estado.priority < 11 && item.buttons.ferro}"
|
<button
|
||||||
|
th:if="${item.estado.priority >= 7 and item.estado.priority <= 11 and item['buttons'] != null and item['buttons']['ferro'] == true}"
|
||||||
type="button" class="btn btn-light w-100 btn-download-ferro"
|
type="button" class="btn btn-light w-100 btn-download-ferro"
|
||||||
th:text="#{pedido.view.ferro-download}"
|
th:text="#{pedido.view.ferro-download}" th:attr="data-linea-id=${item.lineaId}">
|
||||||
th:attr="data-linea-id=${item.lineaId}">
|
|
||||||
Descargar ferro
|
Descargar ferro
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button th:if="${item.estado.priority >= 7 && item.estado.priority < 11 && item.buttons.cub}"
|
<button
|
||||||
|
th:if="${item.estado.priority >= 7 and item.estado.priority <= 11 and item['buttons'] != null and item['buttons']['cub'] == true}"
|
||||||
type="button" class="btn btn-light w-100 btn-download-cub"
|
type="button" class="btn btn-light w-100 btn-download-cub"
|
||||||
th:text="#{pedido.view.cub-download}"
|
th:text="#{pedido.view.cub-download}" th:attr="data-linea-id=${item.lineaId}">
|
||||||
th:attr="data-linea-id=${item.lineaId}">
|
|
||||||
Descargar cubierta
|
Descargar cubierta
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button th:if="${item.estado.priority >= 7 && item.estado.priority < 11 && item.buttons.tapa}"
|
<button
|
||||||
|
th:if="${item.estado.priority >= 7 and item.estado.priority <= 11 and item['buttons'] != null and item['buttons']['tapa'] == true}"
|
||||||
type="button" class="btn btn-light w-100 btn-download-tapa"
|
type="button" class="btn btn-light w-100 btn-download-tapa"
|
||||||
th:text="#{pedido.view.tapa-download}"
|
th:text="#{pedido.view.tapa-download}" th:attr="data-linea-id=${item.lineaId}">
|
||||||
th:attr="data-linea-id=${item.lineaId}">
|
|
||||||
Descargar tapa
|
Descargar tapa
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -136,7 +136,7 @@
|
|||||||
<div class="d-flex flex-wrap my-n1">
|
<div class="d-flex flex-wrap my-n1">
|
||||||
<!-- Actualizar estado-->
|
<!-- Actualizar estado-->
|
||||||
<div class="update-estado-button"
|
<div class="update-estado-button"
|
||||||
th:if="${item.estado.name != 'cancelado' && item.estado.name != 'maquetacion' && item.estado.name != 'terminado'}">
|
th:if="${item.estado.name != 'cancelado' && item.estado.name != 'maquetacion' && item.estado.name != 'enviado'}">
|
||||||
<a href="javascript:void(0);" class="d-block text-body p-1 px-2 update-status-item"
|
<a href="javascript:void(0);" class="d-block text-body p-1 px-2 update-status-item"
|
||||||
th:attr="data-linea-id=${item.lineaId}">
|
th:attr="data-linea-id=${item.lineaId}">
|
||||||
<i class="ri-refresh-line text-muted align-bottom me-1"><span
|
<i class="ri-refresh-line text-muted align-bottom me-1"><span
|
||||||
|
|||||||
@ -43,6 +43,7 @@
|
|||||||
pais=${direccionFacturacion != null ? direccionFacturacion.paisNombre : ''}
|
pais=${direccionFacturacion != null ? direccionFacturacion.paisNombre : ''}
|
||||||
)}">
|
)}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<th:block th:if="${isAdmin and showCancel}">
|
<th:block th:if="${isAdmin and showCancel}">
|
||||||
<div sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')"
|
<div sec:authorize="isAuthenticated() and hasAnyRole('SUPERADMIN','ADMIN')"
|
||||||
@ -60,11 +61,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th:block>
|
</th:block>
|
||||||
|
<th:block th:if="${showDownloadFactura}">
|
||||||
|
<div class="col-12 col-md-auto">
|
||||||
|
<div class="card card border mb-3">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<span class="fs-16" th:text="#{'pedido.view.actions'}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<button type="button" th:attr="data-factura-id=${facturaId}" class="btn btn-secondary w-100 btn-download-factura"
|
||||||
|
th:text="#{pedido.view.descargar-factura}">
|
||||||
|
Descargar factura
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<th:block th:each="linea: ${lineas}">
|
<th:block th:each="linea: ${lineas}">
|
||||||
<div
|
<div
|
||||||
th:insert="~{imprimelibros/pedidos/pedidos-linea :: pedido-linea (item=${linea}, isAdmin=${isAdmin})}">
|
th:insert="~{imprimelibros/pedidos/pedidos-linea :: pedido-linea (item=${linea}, isAdmin=${isAdmin})}">
|
||||||
|
|||||||
@ -158,7 +158,7 @@
|
|||||||
aria-labelledby="accordionComentario" data-bs-parent="#accordionlefticon">
|
aria-labelledby="accordionComentario" data-bs-parent="#accordionlefticon">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<div class="snow-editor" id="comentario" name="comentario"
|
<div class="snow-editor" id="comentario" name="comentario"
|
||||||
th:attr="data-contenido=${presupuesto.comentario} "
|
th:attr="data-contenido=${presupuesto != null ? presupuesto.comentario : ''} "
|
||||||
style=" height: 300px;">
|
style=" height: 300px;">
|
||||||
</div> <!-- end Snow-editor-->
|
</div> <!-- end Snow-editor-->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
107
src/main/resources/templates/imprimelibros/users/profile.html
Normal file
107
src/main/resources/templates/imprimelibros/users/profile.html
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||||
|
layout:decorate="~{imprimelibros/layout}">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<th:block layout:fragment="pagetitle" />
|
||||||
|
<th:block th:replace="~{imprimelibros/partials/head-css :: head-css}" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div th:replace="~{imprimelibros/partials/topbar :: topbar}" />
|
||||||
|
<div th:replace="~{imprimelibros/partials/sidebar :: sidebar}" />
|
||||||
|
|
||||||
|
<th:block layout:fragment="content">
|
||||||
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
|
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/"><i class="ri-home-5-fill"></i></a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page" th:text="#{app.perfil}">Perfil</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0" th:text="#{usuarios.profile.title}">Editar perfil</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<div th:if="${success}" class="alert alert-success"
|
||||||
|
th:text="#{usuarios.profile.success}">Perfil actualizado.</div>
|
||||||
|
|
||||||
|
<form id="profileForm" novalidate th:action="@{/pages-profile}" th:object="${user}"
|
||||||
|
method="post">
|
||||||
|
|
||||||
|
<div th:if="${#fields.hasGlobalErrors()}" class="alert alert-danger">
|
||||||
|
<div th:each="e : ${#fields.globalErrors()}" th:text="${e}"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label th:text="#{usuarios.form.nombre}" for="fullName">Nombre</label>
|
||||||
|
<input type="text" class="form-control" id="fullName" th:field="*{fullName}"
|
||||||
|
th:classappend="${#fields.hasErrors('fullName')} ? ' is-invalid'" required>
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('fullName')}"
|
||||||
|
th:errors="*{fullName}">Error</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label th:text="#{usuarios.form.email}" for="userName">Correo electrónico</label>
|
||||||
|
<input type="email" class="form-control" id="userName" th:field="*{userName}"
|
||||||
|
th:classappend="${#fields.hasErrors('userName')} ? ' is-invalid'" required>
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('userName')}"
|
||||||
|
th:errors="*{userName}">Error</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label th:text="#{usuarios.form.password.actual}" for="currentPassword">Contraseña actual</label>
|
||||||
|
<input type="password" class="form-control" id="currentPassword"
|
||||||
|
th:field="*{currentPassword}"
|
||||||
|
th:classappend="${#fields.hasErrors('currentPassword')} ? ' is-invalid'">
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('currentPassword')}"
|
||||||
|
th:errors="*{currentPassword}">Error</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label th:text="#{usuarios.form.password.nueva}" for="newPassword">Nueva contraseña</label>
|
||||||
|
<input type="password" class="form-control" id="newPassword"
|
||||||
|
th:field="*{newPassword}"
|
||||||
|
th:classappend="${#fields.hasErrors('newPassword')} ? ' is-invalid'">
|
||||||
|
<div class="text-muted" th:text="#{usuarios.form.password.nota}">
|
||||||
|
Solo podrás cambiar la contraseña si indicas la actual.
|
||||||
|
</div>
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('newPassword')}"
|
||||||
|
th:errors="*{newPassword}">Error</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label th:text="#{usuarios.form.confirmarPassword}" for="confirmPassword">Confirmar contraseña</label>
|
||||||
|
<input type="password" class="form-control" id="confirmPassword"
|
||||||
|
th:field="*{confirmPassword}"
|
||||||
|
th:classappend="${#fields.hasErrors('confirmPassword')} ? ' is-invalid'">
|
||||||
|
<div class="invalid-feedback" th:if="${#fields.hasErrors('confirmPassword')}"
|
||||||
|
th:errors="*{confirmPassword}">Error</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<button type="submit" class="btn btn-secondary" th:text="#{app.guardar}">Guardar</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
||||||
|
|
||||||
|
<th:block layout:fragment="modal" />
|
||||||
|
<th:block th:replace="~{theme/partials/vendor-scripts :: scripts}" />
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
30
src/test/java/com/imprimelibros/erp/genericTest.java
Normal file
30
src/test/java/com/imprimelibros/erp/genericTest.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package com.imprimelibros.erp;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.common.Utils;
|
||||||
|
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
|
||||||
|
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
public class genericTest {
|
||||||
|
@Autowired
|
||||||
|
private Utils utils;
|
||||||
|
@Autowired
|
||||||
|
private PresupuestoRepository presupuestoRepository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getTextoPresupuesto() {
|
||||||
|
|
||||||
|
Locale locale = Locale.forLanguageTag("es-ES");
|
||||||
|
Presupuesto presupuesto = presupuestoRepository.findById(86L).orElse(null);
|
||||||
|
Map<String, Object> texto = utils.getTextoPresupuesto(presupuesto, locale);
|
||||||
|
|
||||||
|
System.out.println("🧾 Texto del presupuesto:" + texto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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