Compare commits

...

16 Commits

Author SHA1 Message Date
562dc2b231 Merge branch 'feat/log_save_presupuesto' into 'main'
añadido log en guardar presupuesto

See merge request jjimenez/erp-imprimelibros!33
2026-01-09 16:55:09 +00:00
9a49ccf6b8 añadido log en guardar presupuesto 2026-01-09 17:54:06 +01:00
b2026f1cab Merge branch 'mod/test_perfil' into 'main'
Modificaciones en el perfil de test

See merge request jjimenez/erp-imprimelibros!32
2026-01-08 11:31:12 +00:00
a5b6bf3a25 Modificaciones en el perfil de test 2026-01-08 12:30:43 +01:00
9a67c2e78f Merge branch 'feat/facturas' into 'main'
Feat/facturas

See merge request jjimenez/erp-imprimelibros!31
2026-01-07 20:22:12 +00:00
8263d97bf7 terminado (provisional) modulo de facturas 2026-01-07 21:21:33 +01:00
292aebcf65 Merge branch 'main' into feat/facturas 2026-01-05 13:00:00 +01:00
e50153205a Merge branch 'hotfix/add_presupuesto_comentarios' into 'main'
arreglado el comentario de administrador cuando no tiene id de presupuesto

See merge request jjimenez/erp-imprimelibros!30
2026-01-05 11:58:46 +00:00
5ecb38f474 arreglado el comentario de administrador cuando no tiene id de presupuesto 2026-01-05 12:56:55 +01:00
d7a85d9bfb Merge branch 'main' into feat/facturas 2026-01-05 11:06:57 +01:00
4343997eb1 Merge branch 'fix/baseline' into 'main'
modificado el baseline. actualmente falla porque hay que rellenar la tabla variables

See merge request jjimenez/erp-imprimelibros!29
2026-01-05 10:06:22 +00:00
6bfc60d158 modificado el baseline. actualmente falla porque hay que rellenar la tabla variables 2026-01-05 11:05:26 +01:00
aa8ecdf75c Merge branch 'mod/docker_compose-plesk' into 'main'
Añadido documento composer para entorno plesk

See merge request jjimenez/erp-imprimelibros!28
2026-01-04 17:44:50 +00:00
dc529ff055 Añadido documento composer para entorno plesk 2026-01-04 18:44:22 +01:00
4a535ab644 añadido seeder para series de facturacion 2026-01-04 13:11:47 +01:00
400251ac3d generación de factura pdf terminada 2026-01-04 12:11:45 +01:00
39 changed files with 13714 additions and 5090 deletions

45
docker-compose.plesk.yml Normal file
View 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

16960
logs/erp.log

File diff suppressed because one or more lines are too long

View File

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

View File

@ -79,6 +79,9 @@ 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)") @Formula("(select u.fullname from users u where u.id = cliente_id)")
private String clienteNombre; private String clienteNombre;
@ -247,4 +250,22 @@ public class Factura extends AbstractAuditedEntitySoftTs {
public void setPagos(List<FacturaPago> pagos) { public void setPagos(List<FacturaPago> pagos) {
this.pagos = 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);
}
} }

View File

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

View File

@ -1,11 +1,15 @@
package com.imprimelibros.erp.facturacion.controller; package com.imprimelibros.erp.facturacion.controller;
import com.imprimelibros.erp.configurationERP.VariableService;
import com.imprimelibros.erp.datatables.DataTable; 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.direcciones.DireccionService;
import com.imprimelibros.erp.facturacion.EstadoFactura; import com.imprimelibros.erp.facturacion.EstadoFactura;
import com.imprimelibros.erp.facturacion.Factura; import com.imprimelibros.erp.facturacion.Factura;
import com.imprimelibros.erp.facturacion.FacturaDireccion;
import com.imprimelibros.erp.facturacion.dto.FacturaAddRequestDto;
import com.imprimelibros.erp.facturacion.dto.FacturaGuardarDto; 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;
@ -23,6 +27,7 @@ import org.springframework.context.MessageSource;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -30,6 +35,7 @@ import org.springframework.web.bind.annotation.*;
import java.security.Principal; import java.security.Principal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.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;
@ -45,17 +51,21 @@ public class FacturasController {
private final TranslationService translationService; private final TranslationService translationService;
private final MessageSource messageSource; private final MessageSource messageSource;
private final PedidoService pedidoService; private final PedidoService pedidoService;
private final VariableService variableService;
private final DireccionService direccionService;
public FacturasController( public FacturasController(
FacturaRepository repo, FacturaRepository repo,
TranslationService translationService, TranslationService translationService,
MessageSource messageSource, MessageSource messageSource,
PedidoService pedidoService, FacturacionService facturacionService) { PedidoService pedidoService, FacturacionService facturacionService, VariableService variableService, DireccionService direccionService) {
this.repo = repo; this.repo = repo;
this.translationService = translationService; this.translationService = translationService;
this.messageSource = messageSource; this.messageSource = messageSource;
this.pedidoService = pedidoService; this.pedidoService = pedidoService;
this.facturacionService = facturacionService; this.facturacionService = facturacionService;
this.direccionService = direccionService;
this.variableService = variableService;
} }
@GetMapping @GetMapping
@ -73,14 +83,56 @@ public class FacturasController {
return "imprimelibros/facturas/facturas-list"; 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}") @GetMapping("/{id}")
public String facturaDetail(@PathVariable Long id, Model model, Locale locale) { public String facturaDetail(@PathVariable Long id, Model model, Locale locale) {
Factura factura = repo.findById(id) Factura factura = repo.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id)); .orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
PedidoDireccion direccionFacturacion = pedidoService
.getPedidoDireccionFacturacionByPedidoId(factura.getPedidoId());
List<String> keys = List.of( List<String> keys = List.of(
"facturas.lineas.error.base", "facturas.lineas.error.base",
"facturas.lineas.delete.title", "facturas.lineas.delete.title",
@ -97,6 +149,8 @@ public class FacturasController {
Map<String, String> translations = translationService.getTranslations(locale, keys); Map<String, String> translations = translationService.getTranslations(locale, keys);
model.addAttribute("languageBundle", translations); model.addAttribute("languageBundle", translations);
FacturaDireccion direccionFacturacion = factura.getDireccionFacturacion();
model.addAttribute("direccionFacturacion", direccionFacturacion); model.addAttribute("direccionFacturacion", direccionFacturacion);
model.addAttribute("factura", factura); model.addAttribute("factura", factura);
@ -116,8 +170,8 @@ public class FacturasController {
Factura factura = repo.findById(id) Factura factura = repo.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id)); .orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
PedidoDireccion direccionFacturacion = pedidoService FacturaDireccion direccionFacturacion = factura.getDireccionFacturacion();
.getPedidoDireccionFacturacionByPedidoId(factura.getPedidoId());
model.addAttribute("direccionFacturacion", direccionFacturacion); model.addAttribute("direccionFacturacion", direccionFacturacion);
model.addAttribute("factura", factura); model.addAttribute("factura", factura);
@ -134,7 +188,7 @@ public class FacturasController {
return ResponseEntity.badRequest().body("Solo se pueden validar facturas en estado 'borrador'."); return ResponseEntity.badRequest().body("Solo se pueden validar facturas en estado 'borrador'.");
} }
factura.setEstado(EstadoFactura.validada); facturacionService.validarFactura(factura.getId());
repo.save(factura); repo.save(factura);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
@ -178,6 +232,8 @@ public class FacturasController {
return ResponseEntity.ok(Map.of("ok", true)); return ResponseEntity.ok(Map.of("ok", true));
} }
/* /*
* ----------------------------- * -----------------------------
* Pagos * Pagos
@ -296,4 +352,39 @@ public class FacturasController {
.toJson(total); .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);
}
} }

View File

@ -1,5 +1,8 @@
package com.imprimelibros.erp.facturacion.dto; package com.imprimelibros.erp.facturacion.dto;
import java.time.Instant;
import com.imprimelibros.erp.facturacion.FacturaDireccion;
import com.imprimelibros.erp.pedidos.PedidoDireccion; import com.imprimelibros.erp.pedidos.PedidoDireccion;
public class DireccionFacturacionDto { public class DireccionFacturacionDto {
@ -76,6 +79,13 @@ public class DireccionFacturacionDto {
this.telefono = telefono; this.telefono = telefono;
} }
public FacturaDireccion toFacturaDireccion() {
FacturaDireccion fd = new FacturaDireccion();
applyTo(fd);
return fd;
}
public PedidoDireccion toPedidoDireccion() { public PedidoDireccion toPedidoDireccion() {
PedidoDireccion pd = new PedidoDireccion(); PedidoDireccion pd = new PedidoDireccion();
applyTo(pd); applyTo(pd);
@ -84,6 +94,7 @@ public class DireccionFacturacionDto {
} }
public void applyTo(PedidoDireccion pd) { public void applyTo(PedidoDireccion pd) {
pd.setAtt("");
pd.setRazonSocial(this.razonSocial); pd.setRazonSocial(this.razonSocial);
pd.setIdentificacionFiscal(this.identificacionFiscal); pd.setIdentificacionFiscal(this.identificacionFiscal);
pd.setDireccion(this.direccion); pd.setDireccion(this.direccion);
@ -107,4 +118,30 @@ public class DireccionFacturacionDto {
pd.setTelefono(this.telefono); 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());
}
} }

View File

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

View File

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

View File

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

View File

@ -1,11 +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 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>, JpaSpecificationExecutor<Factura> { 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);
} }

View File

@ -1,21 +1,28 @@
package com.imprimelibros.erp.facturacion.service; package com.imprimelibros.erp.facturacion.service;
import com.imprimelibros.erp.common.Utils; 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.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.FacturaLineaRepository;
import com.imprimelibros.erp.facturacion.repo.FacturaPagoRepository; import com.imprimelibros.erp.facturacion.repo.FacturaPagoRepository;
import com.imprimelibros.erp.facturacion.repo.FacturaRepository; import com.imprimelibros.erp.facturacion.repo.FacturaRepository;
import com.imprimelibros.erp.facturacion.repo.SerieFacturaRepository; import com.imprimelibros.erp.facturacion.repo.SerieFacturaRepository;
import com.imprimelibros.erp.pedidos.Pedido; import com.imprimelibros.erp.pedidos.Pedido;
import com.imprimelibros.erp.pedidos.PedidoDireccion;
import com.imprimelibros.erp.pedidos.PedidoLinea; import com.imprimelibros.erp.pedidos.PedidoLinea;
import com.imprimelibros.erp.pedidos.PedidoLineaRepository; import com.imprimelibros.erp.pedidos.PedidoLineaRepository;
import com.imprimelibros.erp.pedidos.PedidoService; import com.imprimelibros.erp.pedidos.PedidoService;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto; import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import com.imprimelibros.erp.users.User; import com.imprimelibros.erp.users.User;
import com.imprimelibros.erp.users.UserService; 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;
@ -24,12 +31,17 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.Locale; import java.util.Locale;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.security.Principal; import java.security.Principal;
import java.text.Collator;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -41,41 +53,47 @@ public class FacturacionService {
private final SerieFacturaRepository serieRepo; private final SerieFacturaRepository serieRepo;
private final FacturaPagoRepository pagoRepo; private final FacturaPagoRepository pagoRepo;
private final FacturaLineaRepository lineaFacturaRepository; private final FacturaLineaRepository lineaFacturaRepository;
private final DireccionRepository direccionRepo;
private final PedidoLineaRepository pedidoLineaRepo; private final PedidoLineaRepository pedidoLineaRepo;
private final UserService userService; private final UserService userService;
private final Utils utils; private final Utils utils;
private final MessageSource messageSource; private final MessageSource messageSource;
private final PedidoService pedidoService; private final PedidoService pedidoService;
private final VariableService variableService;
public FacturacionService( public FacturacionService(
FacturaRepository facturaRepo, FacturaRepository facturaRepo,
FacturaLineaRepository lineaFacturaRepository, FacturaLineaRepository lineaFacturaRepository,
SerieFacturaRepository serieRepo, SerieFacturaRepository serieRepo,
FacturaPagoRepository pagoRepo, FacturaPagoRepository pagoRepo,
DireccionRepository direccionRepo,
PedidoLineaRepository pedidoLineaRepo, PedidoLineaRepository pedidoLineaRepo,
UserService userService, UserService userService,
Utils utils, Utils utils,
MessageSource messageSource, MessageSource messageSource,
PedidoService pedidoService) { PedidoService pedidoService,
VariableService variableService) {
this.facturaRepo = facturaRepo; this.facturaRepo = facturaRepo;
this.lineaFacturaRepository = lineaFacturaRepository; this.lineaFacturaRepository = lineaFacturaRepository;
this.serieRepo = serieRepo; this.serieRepo = serieRepo;
this.pagoRepo = pagoRepo; this.pagoRepo = pagoRepo;
this.direccionRepo = direccionRepo;
this.pedidoLineaRepo = pedidoLineaRepo; this.pedidoLineaRepo = pedidoLineaRepo;
this.userService = userService; this.userService = userService;
this.utils = utils; this.utils = utils;
this.messageSource = messageSource; this.messageSource = messageSource;
this.pedidoService = pedidoService; this.pedidoService = pedidoService;
this.variableService = variableService;
} }
public SerieFactura getDefaultSerieFactura() { public SerieFactura getDefaultSerieFactura() {
List<SerieFactura> series = serieRepo.findAll();
if (series.isEmpty()) { 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."); throw new IllegalStateException("No hay ninguna serie de facturación configurada.");
} }
// Aquí simplemente devolvemos la primera. Puedes implementar lógica más return serie;
// compleja si es necesario.
return series.get(0);
} }
public Factura getFactura(Long facturaId) { public Factura getFactura(Long facturaId) {
@ -83,6 +101,14 @@ public class FacturacionService {
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + 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 // Nueva factura
// ----------------------- // -----------------------
@ -128,6 +154,28 @@ public class FacturacionService {
lineaFactura.setFactura(factura); lineaFactura.setFactura(factura);
lineasFactura.add(lineaFactura); 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.setLineas(lineasFactura);
factura = facturaRepo.save(factura); factura = facturaRepo.save(factura);
@ -147,6 +195,46 @@ public class FacturacionService {
return factura; 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
// ----------------------- // -----------------------
@ -206,6 +294,7 @@ public class FacturacionService {
pedidoService.upsertDireccionFacturacion(pedidoId, dto.getDireccionFacturacion()); pedidoService.upsertDireccionFacturacion(pedidoId, dto.getDireccionFacturacion());
} }
upsertDireccionFacturacion(facturaId, dto.getDireccionFacturacion());
facturaRepo.save(factura); facturaRepo.save(factura);
} }
@ -262,12 +351,79 @@ 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 + "/" + LocalDate.now().getYear()); return pref.isBlank() ? num : (pref + " " + num + "/" + LocalDate.now().getYear());
} }
// ----------------------- // -----------------------
// Líneas // Líneas
// ----------------------- // -----------------------

View File

@ -0,0 +1,55 @@
package com.imprimelibros.erp.pedidos;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
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
*/
@Scheduled(cron = "0 0 4 * * *")
public void actualizarEstadosPedidos() {
List<PedidoLinea> pedidosLineas = pedidoLineaRepository.findPedidosLineasParaActualizarEstado();
for (PedidoLinea linea : pedidosLineas) {
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"));
}
} 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;
}
}
}
}

View File

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

View File

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

View File

@ -24,6 +24,7 @@ import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
import com.imprimelibros.erp.users.UserService; import com.imprimelibros.erp.users.UserService;
import com.imprimelibros.erp.direcciones.DireccionService; import com.imprimelibros.erp.direcciones.DireccionService;
import com.imprimelibros.erp.externalApi.skApiClient; import com.imprimelibros.erp.externalApi.skApiClient;
import com.imprimelibros.erp.facturacion.FacturaDireccion;
import com.imprimelibros.erp.facturacion.dto.DireccionFacturacionDto; import com.imprimelibros.erp.facturacion.dto.DireccionFacturacionDto;
import com.imprimelibros.erp.pedidos.PedidoLinea.Estado; import com.imprimelibros.erp.pedidos.PedidoLinea.Estado;
@ -191,24 +192,24 @@ public class PedidoService {
try { try {
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null); Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
if (pedido == null) { if (pedido != null) {
return false;
PedidoDireccion direccionPedido = pedidoDireccionRepository.findByPedidoIdAndFacturacionTrue(pedidoId);
if (direccionPedido == null) {
// crear
direccionPedido = direccionData.toPedidoDireccion();
direccionPedido.setPedido(pedido);
} else {
// actualizar en la existente (NO crees una nueva, para conservar ID)
direccionData.applyTo(direccionPedido); // si implementas applyTo()
direccionPedido.setFacturacion(true); // por si acaso
}
pedidoDireccionRepository.save(direccionPedido);
} }
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; return true;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
@ -299,43 +300,81 @@ 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 || result.get("estado") == null) {
return Map.of(
"success", false,
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
}
String estadoStr = String.valueOf(result.get("estado"));
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) {
@ -406,7 +445,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);
} }

View File

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

View File

@ -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
@ -824,6 +828,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),

View File

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

View File

@ -1,7 +1,7 @@
spring.application.name=erp spring.application.name=erp
# Active profile # Active profile
spring.profiles.active=dev #spring.profiles.active=dev
#spring.profiles.active=test spring.profiles.active=test
#spring.profiles.active=prod #spring.profiles.active=prod

View File

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

View File

@ -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: |

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
facturas.title=Facturas facturas.title=Facturas
facturas.breadcrumb=Facturas facturas.breadcrumb=Facturas
facturas.breadcrumb.ver=Ver Factura facturas.breadcrumb.ver=Ver Factura
facturas.breadcrumb.nueva=Nueva Factura
facturas.tabla.id=ID facturas.tabla.id=ID
facturas.tabla.cliente=Cliente facturas.tabla.cliente=Cliente
@ -19,10 +20,17 @@ facturas.estado.borrador=Borrador
facturas.estado.validada=Validada facturas.estado.validada=Validada
facturas.form.numero-factura=Número de Factura 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=Serie de facturación
facturas.form.serie.placeholder=Seleccione una serie...
facturas.form.fecha-emision=Fecha de Emisión facturas.form.fecha-emision=Fecha de Emisión
facturas.form.cliente=Cliente 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.notas=Notas
facturas.form.factura-rectificada=Factura rectificada
facturas.form.btn.validar=Validar Factura facturas.form.btn.validar=Validar Factura
facturas.form.btn.borrador=Pasar a Borrador facturas.form.btn.borrador=Pasar a Borrador
@ -45,6 +53,8 @@ facturas.lineas.delete.title=¿Eliminar línea de factura?
facturas.lineas.delete.text=Esta acción no se puede deshacer. facturas.lineas.delete.text=Esta acción no se puede deshacer.
facturas.lineas.error.base=La base imponible no es válida. 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.titulo=Dirección de Facturación
facturas.direccion.razon-social=Razón Social facturas.direccion.razon-social=Razón Social
facturas.direccion.identificacion-fiscal=Identificación Fiscal facturas.direccion.identificacion-fiscal=Identificación Fiscal
@ -83,3 +93,8 @@ facturas.delete.title=¿Estás seguro de que deseas eliminar esta factura?
facturas.delete.text=Esta acción no se puede deshacer. facturas.delete.text=Esta acción no se puede deshacer.
facturas.delete.ok.title=Factura eliminada facturas.delete.ok.title=Factura eliminada
facturas.delete.ok.text=La factura ha sido eliminada correctamente. 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.

View File

@ -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
@ -56,7 +57,9 @@ 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.

View File

@ -29,18 +29,16 @@
font-weight: 700; font-weight: 700;
} }
@page { @page {
size: A4; size: A4;
margin: 15mm 14mm 47mm 14mm;
/* Estos márgenes sustituyen a tu padding grande en .page-content */
margin: 15mm 14mm 50mm 14mm; /* bottom grande para el footer */
@bottom-center { @bottom-center {
content: element(pdfFooter); content: element(footer);
/* llamamos al elemento “footer” */
} }
}
}
html, html,
body { body {
@ -52,11 +50,12 @@ body {
.page-content { .page-content {
padding: 0; padding: 0;
/* ↑ deja 10mm extra para no pisar el footer */
box-sizing: border-box; box-sizing: border-box;
/* para que el padding no desborde */
} }
body.has-watermark { body.has-watermark {
background-image: none !important; background-image: none !important;
} }
@ -156,19 +155,22 @@ body.has-watermark {
/* Nueva banda verde PRESUPUESTO */ /* Nueva banda verde PRESUPUESTO */
.doc-banner { .doc-banner {
width: 100%; width: 100%;
background-color: #92b2a7 !important; /* ← tu verde corporativo */ background-color: #92b2a7 !important;
/* ← tu verde corporativo */
color: white; color: white;
text-align: center; text-align: center;
padding: 2mm 0; padding: 2mm 0;
margin-bottom: 4mm; margin-bottom: 4mm;
display: block; /* evita conflictos */ display: block;
/* evita conflictos */
} }
.banner-text { .banner-text {
font-family: "Open Sans", Arial, sans-serif !important; font-family: "Open Sans", Arial, sans-serif !important;
font-weight: 400; font-weight: 400;
font-size: 20pt; font-size: 20pt;
letter-spacing: 8px; /* ← configurable */ letter-spacing: 8px;
/* ← configurable */
} }
@ -212,8 +214,10 @@ body.has-watermark {
/* Specs 2 columnas */ /* Specs 2 columnas */
.specs-wrapper { .specs-wrapper {
width: 180mm; width: 180mm;
margin-left: 15mm; /* ← margen izquierdo real del A4 */ margin-left: 15mm;
margin-right: auto; /* opcional */ /* ← margen izquierdo real del A4 */
margin-right: auto;
/* opcional */
color: #5c5c5c; color: #5c5c5c;
font-size: 9pt; font-size: 9pt;
} }
@ -230,32 +234,44 @@ body.has-watermark {
table-layout: fixed; table-layout: fixed;
margin-bottom: 6mm; margin-bottom: 6mm;
} }
.specs .col { .specs .col {
display: table-cell; display: table-cell;
width: 50%; width: 50%;
padding-right: 6mm; padding-right: 6mm;
vertical-align: top; vertical-align: top;
} }
.specs .col:last-child { .specs .col:last-child {
padding-right: 0; padding-right: 0;
} }
/* Listas sin margen superior por defecto */ /* Listas sin margen superior por defecto */
ul, ol { ul,
margin-top: 0; ol {
margin-bottom: 0rem; /* si quieres algo abajo */ margin-top: 0;
padding-left: 1.25rem; /* sangría */ margin-bottom: 0rem;
/* si quieres algo abajo */
padding-left: 1.25rem;
/* sangría */
} }
/* Párrafos con menos margen inferior */ /* Párrafos con menos margen inferior */
p { p {
margin: 0 0 .5rem; margin: 0 0 .5rem;
} }
/* Si una lista va justo después de un texto o título, que no tenga hueco arriba */ /* Si una lista va justo después de un texto o título, que no tenga hueco arriba */
p + ul, p + ol, p+ul,
h1 + ul, h2 + ul, h3 + ul, h4 + ul, h5 + ul, h6 + ul, p+ol,
div + ul, div + ol { h1+ul,
h2+ul,
h3+ul,
h4+ul,
h5+ul,
h6+ul,
div+ul,
div+ol {
margin-top: 0; margin-top: 0;
} }
@ -336,6 +352,20 @@ div + ul, div + ol {
/* Footer */ /* 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 { .footer .address {
display: table-cell; display: table-cell;
@ -357,70 +387,90 @@ div + ul, div + ol {
line-height: 1.25; line-height: 1.25;
} }
.page-count {
/* Caja a página completa SIN vw/vh y SIN z-index negativo */ margin-top: 2mm;
.watermark { text-align: right;
position: fixed; font-size: 9pt;
top: 0; left: 0; right: 0; bottom: 0; /* ocupa toda la HOJA */ color: var(--muted);
pointer-events: none;
z-index: 0; /* debajo del contenido */
} }
.watermark img { .page::after {
position: absolute; content: counter(page);
top: 245mm; /* baja/sube (7085%) */
left: 155mm; /* desplaza a la derecha si quieres */
transform: translate(-50%, -50%) rotate(-15deg);
width: 60%; /* tamaño grande, ya no hay recorte por márgenes */
max-width: none;
} }
.pages::after {
content: counter(pages);
}
.items-table { .items-table {
width: 100%; width: 100%;
border-color: #92b2a7 ; border-color: #92b2a7;
border-collapse: collapse; border-collapse: collapse;
} }
.items-table thead th { .items-table thead th {
background-color: #f3f6f9; background-color: #f3f6f9;
font-size: small; font-size: small;
} }
.items-table tbody td { .items-table tbody td {
font-size: small; 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 {
/* Saca el footer fuente fuera del papel (pero sigue existiendo para capturarlo) */ content: counter(page) " / " counter(pages);
.pdf-footer-source {
position: absolute;
left: 0;
top: -200mm; /* cualquier valor grande negativo */
width: 100%;
} }
/* El footer que se captura */ .pdf-footer-running {
#pdf-footer { position: running(footer);
position: running(pdfFooter); /* lo registra como elemento repetible */
}
/* Estilo del footer ya dentro del margin-box */
.footer {
border-top: 1px solid var(--line);
padding-top: 4mm;
padding-bottom: 6mm; /* aire para que no quede pegado abajo */
font-size: 7.5pt; font-size: 7.5pt;
color: var(--muted); color: var(--muted);
background: transparent; width: 100%;
} border-top: 1px solid var(--line);
padding-top: 4mm;
/* Numeración */ /* el resto de tus estilos internos (address, privacy, etc.) */
#pdf-footer .page-number {
margin-top: 2mm;
text-align: right;
font-size: 9pt;
}
#pdf-footer .page-number .pn::before {
content: " " counter(page) "/" counter(pages);
} }

View File

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

View File

@ -62,7 +62,9 @@ $(() => {
// ----------------------------- // -----------------------------
// Add // Add
// ----------------------------- // -----------------------------
$addBtn.on(); $addBtn.on('click', () => {
window.location.href = '/facturas/add';
});
// ----------------------------- // -----------------------------
// Edit click // Edit click

View File

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

View File

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

View File

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

View File

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

View File

@ -16,10 +16,14 @@
<div class="row g-3"> <div class="row g-3">
<!-- Número (solo lectura siempre, normalmente) --> <!-- Número (solo lectura siempre, normalmente) -->
<div class="col-md-3"> <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> <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> <input id="facturaNumero" type="text" class="form-control" th:value="${factura.numeroFactura}" readonly>
</div> </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 --> <!-- Serie -->
<div class="col-md-3"> <div class="col-md-3">
@ -43,6 +47,15 @@
</select> </select>
</div> </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"> <div class="col-md-3">
<label class="form-label" th:text="#{facturas.form.fecha-emision}">Fecha</label> <label class="form-label" th:text="#{facturas.form.fecha-emision}">Fecha</label>

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="es"> <html xmlns:th="http://www.thymeleaf.org " lang="es">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@ -10,14 +10,13 @@
<body class="has-watermark"> <body class="has-watermark">
<div class="watermark">
<img src="assets/images/logo-watermark.png" alt="Marca de agua" />
</div>
<!-- PIE --> <!-- PIE -->
<div class="pdf-footer-source"> <div class="pdf-footer-running">
<div class="footer" id="pdf-footer"> <div class="footer" id="pdf-footer">
<div class="privacy"> <div class="privacy">
<div class="pv-title" th:text="#{pdf.politica-privacidad}">Política de privacidad</div> <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 - <div class="pv-text" th:text="#{pdf.politica-privacidad.responsable}">Responsable: Impresión Imprime Libros -
@ -39,10 +38,10 @@
</div> </div>
<div class="page-number"> <div class="page-number">
<span th:text="#{pdf.page} ?: 'Página'">Página</span> <span th:text="#{pdf.page} ?: 'Página'">Página</span><span> </span><span class="pn"></span>
<span class="pn"></span>
</div> </div>
</div> </div>
</div> </div>
@ -121,15 +120,15 @@
<thead> <thead>
<tr> <tr>
<th class="w-75" th:text="#{pdf.factura.lineas.descripcion}">Descripción</th> <th class="w-75" th:text="#{pdf.factura.lineas.descripcion}">Descripción</th>
<th th:text="#{pdf.factura.lineas.base}">Base</th> <th class="num" th:text="#{pdf.factura.lineas.base}">Base</th>
<th th:text="#{pdf.factura.lineas.iva_4}">I.V.A. 4%</th> <th class="num" th:text="#{pdf.factura.lineas.iva_4}">I.V.A. 4%</th>
<th th:text="#{pdf.factura.lineas.iva_21}">I.V.A. 21%</th> <th class="num" th:text="#{pdf.factura.lineas.iva_21}">I.V.A. 21%</th>
<th th:text="#{pdf.factura.lineas.total}">Total</th> <th class="num" th:text="#{pdf.factura.lineas.total}">Total</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr th:each="lineaFactura : ${factura.lineas}"> <tr th:each="lineaFactura : ${factura.lineas}">
<td th:utext="${lineaFactura.descripcion}">Descripción de la línea</td> <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.baseLinea)}">0.00</td>
<td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.iva4Linea)}">0.00</td> <td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.iva4Linea)}">0.00</td>
<td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.iva21Linea)}">0.00</td> <td class="text-end" th:text="${#numbers.formatCurrency(lineaFactura.iva21Linea)}">0.00</td>

View File

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

View File

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

View File

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