mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-12 16:38:48 +00:00
Compare commits
39 Commits
58f0eee5d9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 562dc2b231 | |||
| 9a49ccf6b8 | |||
| b2026f1cab | |||
| a5b6bf3a25 | |||
| 9a67c2e78f | |||
| 8263d97bf7 | |||
| 292aebcf65 | |||
| e50153205a | |||
| 5ecb38f474 | |||
| d7a85d9bfb | |||
| 4343997eb1 | |||
| 6bfc60d158 | |||
| aa8ecdf75c | |||
| dc529ff055 | |||
| 4a535ab644 | |||
| 400251ac3d | |||
| 6bea279066 | |||
| bf823281a5 | |||
| 9d4320db9a | |||
| d7b5dedb38 | |||
| 089641b601 | |||
| 98a5fcaa0b | |||
| 13a38fcdd5 | |||
| 839301cf94 | |||
| 1ba1b28793 | |||
| 47866ddead | |||
| 982423d766 | |||
| 5b5ce7ccd7 | |||
| 61be8d6d3b | |||
| 3a00702bb1 | |||
| b94a099e01 | |||
| d4120bb486 | |||
| 4cc47b4249 | |||
| cf73801dbe | |||
| 3b9f446195 | |||
| c6e2322132 | |||
| 58fd4815c6 | |||
| 9baf880022 | |||
| 25a7bcf0b8 |
45
docker-compose.plesk.yml
Normal file
45
docker-compose.plesk.yml
Normal file
@ -0,0 +1,45 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
imprimelibros-db:
|
||||
image: mysql:8.0
|
||||
container_name: imprimelibros-db
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: NrXz6DK6UoN
|
||||
MYSQL_DATABASE: imprimelibros
|
||||
MYSQL_USER: imprimelibros_user
|
||||
MYSQL_PASSWORD: om91irrDctd
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
networks:
|
||||
- imprimelibros-network
|
||||
restart: always
|
||||
ports:
|
||||
- "3309:3306" # host:container
|
||||
|
||||
imprimelibros-app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: imprimelibros-app:latest
|
||||
container_name: imprimelibros-app
|
||||
depends_on:
|
||||
- imprimelibros-db
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL: jdbc:mysql://imprimelibros-db:3306/imprimelibros?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Europe/Madrid
|
||||
SPRING_DATASOURCE_USERNAME: imprimelibros_user
|
||||
SPRING_DATASOURCE_PASSWORD: om91irrDctd
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
volumes:
|
||||
- ./logs:/var/log/imprimelibros
|
||||
restart: always
|
||||
networks:
|
||||
- imprimelibros-network
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
|
||||
networks:
|
||||
imprimelibros-network:
|
||||
driver: bridge
|
||||
19251
logs/erp.log
19251
logs/erp.log
File diff suppressed because one or more lines are too long
8
pom.xml
8
pom.xml
@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.7</version>
|
||||
<version>3.5.9</version>
|
||||
<relativePath /> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.imprimelibros</groupId>
|
||||
@ -32,6 +32,12 @@
|
||||
<liquibase.version>4.29.2</liquibase.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
|
||||
@ -5,6 +5,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -15,20 +16,23 @@ import java.util.Objects;
|
||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
|
||||
import com.imprimelibros.erp.users.UserService;
|
||||
import com.imprimelibros.erp.cart.dto.CartDireccionRepository;
|
||||
import com.imprimelibros.erp.cart.dto.DireccionCardDTO;
|
||||
import com.imprimelibros.erp.cart.dto.DireccionShipment;
|
||||
import com.imprimelibros.erp.cart.dto.UpdateCartRequest;
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.common.email.EmailService;
|
||||
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||
import com.imprimelibros.erp.externalApi.skApiClient;
|
||||
import com.imprimelibros.erp.pedidos.Pedido;
|
||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||
import com.imprimelibros.erp.pedidos.PedidoRepository;
|
||||
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
|
||||
|
||||
@Service
|
||||
public class CartService {
|
||||
|
||||
private final EmailService emailService;
|
||||
|
||||
private final CartRepository cartRepo;
|
||||
private final CartDireccionRepository cartDireccionRepo;
|
||||
private final CartItemRepository itemRepo;
|
||||
@ -36,14 +40,14 @@ public class CartService {
|
||||
private final PresupuestoRepository presupuestoRepo;
|
||||
private final DireccionService direccionService;
|
||||
private final skApiClient skApiClient;
|
||||
private final PedidoService pedidoService;
|
||||
private final PresupuestoService presupuestoService;
|
||||
private final PedidoRepository pedidoRepository;
|
||||
private final UserService userService;
|
||||
|
||||
public CartService(CartRepository cartRepo, CartItemRepository itemRepo,
|
||||
CartDireccionRepository cartDireccionRepo, MessageSource messageSource,
|
||||
PresupuestoFormatter presupuestoFormatter, PresupuestoRepository presupuestoRepo,
|
||||
DireccionService direccionService, skApiClient skApiClient,
|
||||
PedidoService pedidoService, PresupuestoService presupuestoService) {
|
||||
PresupuestoFormatter presupuestoFormatter, PresupuestoRepository presupuestoRepo, PedidoRepository pedidoRepository,
|
||||
DireccionService direccionService, skApiClient skApiClient,PresupuestoService presupuestoService, EmailService emailService, UserService userService) {
|
||||
this.cartRepo = cartRepo;
|
||||
this.itemRepo = itemRepo;
|
||||
this.cartDireccionRepo = cartDireccionRepo;
|
||||
@ -51,8 +55,10 @@ public class CartService {
|
||||
this.presupuestoRepo = presupuestoRepo;
|
||||
this.direccionService = direccionService;
|
||||
this.skApiClient = skApiClient;
|
||||
this.pedidoService = pedidoService;
|
||||
this.presupuestoService = presupuestoService;
|
||||
this.emailService = emailService;
|
||||
this.pedidoRepository = pedidoRepository;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
public Cart findById(Long cartId) {
|
||||
@ -264,7 +270,7 @@ public class CartService {
|
||||
}
|
||||
|
||||
double totalBeforeDiscount = base + iva4 + iva21 + shipment;
|
||||
int fidelizacion = pedidoService.getDescuentoFidelizacion(cart.getUserId());
|
||||
int fidelizacion = this.getDescuentoFidelizacion(cart.getUserId());
|
||||
double descuento = totalBeforeDiscount * fidelizacion / 100.0;
|
||||
double total = totalBeforeDiscount - descuento;
|
||||
|
||||
@ -291,6 +297,27 @@ public class CartService {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public int getDescuentoFidelizacion(Long userId) {
|
||||
// descuento entre el 1% y el 6% para clientes fidelidad (mas de 1500€ en el
|
||||
// ultimo año)
|
||||
Instant haceUnAno = Instant.now().minusSeconds(365 * 24 * 60 * 60);
|
||||
double totalGastado = pedidoRepository.sumTotalByCreatedByAndCreatedAtAfter(userId, haceUnAno);
|
||||
if (totalGastado < 1200) {
|
||||
return 0;
|
||||
} else if (totalGastado >= 1200 && totalGastado < 1999) {
|
||||
return 1;
|
||||
} else if (totalGastado >= 2000 && totalGastado < 2999) {
|
||||
return 2;
|
||||
} else if (totalGastado >= 3000 && totalGastado < 3999) {
|
||||
return 3;
|
||||
} else if (totalGastado >= 4000 && totalGastado < 4999) {
|
||||
return 4;
|
||||
} else if (totalGastado >= 5000) {
|
||||
return 5;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Map<String, Object> getCartSummary(Cart cart, Locale locale) {
|
||||
Map<String, Object> raw = getCartSummaryRaw(cart, locale);
|
||||
|
||||
@ -396,6 +423,13 @@ public class CartService {
|
||||
|
||||
cart.setUserId(customerId);
|
||||
cartRepo.save(cart);
|
||||
// Se mueven los presupuestos de cartitems a ese usuario
|
||||
List<CartItem> items = itemRepo.findByCartId(cart.getId());
|
||||
for (CartItem item : items) {
|
||||
Presupuesto p = item.getPresupuesto();
|
||||
p.setUser(userService.findById(customerId));
|
||||
presupuestoRepo.save(p);
|
||||
}
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
@ -411,184 +445,6 @@ public class CartService {
|
||||
cartDireccionRepo.deleteByDireccionIdAndCartStatus(direccionId, Cart.Status.ACTIVE);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Long crearPedido(Long cartId, Long dirFactId, Locale locale) {
|
||||
|
||||
Cart cart = this.getCartById(cartId);
|
||||
List<CartItem> items = cart.getItems();
|
||||
|
||||
List<Map<String, Object>> presupuestoRequests = new ArrayList<>();
|
||||
Map<String, Object> presupuestoDireccionesRequest = new HashMap<>();
|
||||
List<Long> presupuestoIds = new ArrayList<>();
|
||||
|
||||
for (Integer i = 0; i < items.size(); i++) {
|
||||
CartItem item = items.get(i);
|
||||
Presupuesto pCart = item.getPresupuesto();
|
||||
|
||||
// Asegurarnos de trabajar con la entidad gestionada por JPA
|
||||
Presupuesto p = presupuestoRepo.findById(pCart.getId())
|
||||
.orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + pCart.getId()));
|
||||
|
||||
Map<String, Object> data_to_send = presupuestoService.toSkApiRequest(p, true);
|
||||
data_to_send.put("createPedido", 0);
|
||||
|
||||
// Recuperar el mapa anidado datosCabecera
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> datosCabecera = (Map<String, Object>) data_to_send.get("datosCabecera");
|
||||
if (datosCabecera != null) {
|
||||
Object tituloOriginal = datosCabecera.get("titulo");
|
||||
datosCabecera.put(
|
||||
"titulo",
|
||||
"[" + (i + 1) + "/" + items.size() + "] " + (tituloOriginal != null ? tituloOriginal : ""));
|
||||
}
|
||||
|
||||
Map<String, Object> direcciones_presupuesto = this.getDireccionesPresupuesto(cart, p);
|
||||
data_to_send.put("direcciones", direcciones_presupuesto.get("direcciones"));
|
||||
data_to_send.put("direccionesFP1", direcciones_presupuesto.get("direccionesFP1"));
|
||||
|
||||
presupuestoDireccionesRequest.put(p.getId().toString(), direcciones_presupuesto);
|
||||
|
||||
Map<String, Object> result = skApiClient.savePresupuesto(data_to_send);
|
||||
|
||||
if (result.containsKey("error")) {
|
||||
System.out.println("Error al guardar presupuesto en SK");
|
||||
System.out.println("-------------------------");
|
||||
System.out.println(result.get("error"));
|
||||
// decide si seguir con otros items o abortar:
|
||||
// continue; o bien throw ...
|
||||
continue;
|
||||
}
|
||||
|
||||
Object dataObj = result.get("data");
|
||||
if (!(dataObj instanceof Map<?, ?> dataRaw)) {
|
||||
System.out.println("Formato inesperado de 'data' en savePresupuesto: " + result);
|
||||
continue;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> dataMap = (Map<String, Object>) dataRaw;
|
||||
Long presId = ((Number) dataMap.get("id")).longValue();
|
||||
String skin = ((String) dataMap.get("iskn")).toString();
|
||||
p.setProveedor("Safekat");
|
||||
p.setProveedorRef1(skin);
|
||||
p.setProveedorRef2(presId);
|
||||
p.setEstado(Presupuesto.Estado.aceptado);
|
||||
presupuestoRepo.save(p);
|
||||
|
||||
presupuestoIds.add(p.getId());
|
||||
|
||||
presupuestoRequests.add(dataMap);
|
||||
}
|
||||
|
||||
// Crear el pedido en base a los presupuestos guardados
|
||||
if (presupuestoRequests.isEmpty()) {
|
||||
throw new IllegalStateException("No se pudieron guardar los presupuestos en SK.");
|
||||
} else {
|
||||
ArrayList<Long> presupuestoSkIds = new ArrayList<>();
|
||||
for (Map<String, Object> presData : presupuestoRequests) {
|
||||
Long presId = ((Number) presData.get("id")).longValue();
|
||||
presupuestoSkIds.add(presId);
|
||||
}
|
||||
Map<String, Object> ids = new HashMap<>();
|
||||
ids.put("presupuesto_ids", presupuestoSkIds);
|
||||
Long pedidoId = skApiClient.crearPedido(ids);
|
||||
if (pedidoId == null) {
|
||||
throw new IllegalStateException("No se pudo crear el pedido en SK.");
|
||||
}
|
||||
Pedido pedidoInterno = pedidoService.crearPedido(
|
||||
presupuestoIds,
|
||||
presupuestoDireccionesRequest,
|
||||
dirFactId,
|
||||
this.getCartSummaryRaw(cart, locale),
|
||||
"Safekat",
|
||||
String.valueOf(pedidoId),
|
||||
cart.getUserId());
|
||||
return pedidoInterno.getId();
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> getDireccionesPresupuesto(Cart cart, Presupuesto presupuesto) {
|
||||
|
||||
List<Map<String, Object>> direccionesPresupuesto = new ArrayList<>();
|
||||
List<Map<String, Object>> direccionesPrueba = new ArrayList<>();
|
||||
if (cart.getOnlyOneShipment()) {
|
||||
List<CartDireccion> direcciones = cart.getDirecciones().stream().limit(1).toList();
|
||||
if (!direcciones.isEmpty()) {
|
||||
if (presupuesto.getServiciosJson() != null
|
||||
&& presupuesto.getServiciosJson().contains("deposito-legal")) {
|
||||
direccionesPresupuesto.add(direcciones.get(0).toSkMap(
|
||||
presupuesto.getSelectedTirada()-4,
|
||||
presupuesto.getPeso(),
|
||||
direcciones.get(0).getIsPalets(),
|
||||
false));
|
||||
|
||||
direccionesPresupuesto.add(direcciones.get(0).toSkMapDepositoLegal());
|
||||
}
|
||||
else {
|
||||
direccionesPresupuesto.add(direcciones.get(0).toSkMap(
|
||||
presupuesto.getSelectedTirada(),
|
||||
presupuesto.getPeso(),
|
||||
direcciones.get(0).getIsPalets(),
|
||||
false));
|
||||
}
|
||||
if (presupuesto.getServiciosJson() != null
|
||||
&& presupuesto.getServiciosJson().contains("ejemplar-prueba")) {
|
||||
direccionesPrueba.add(direcciones.get(0).toSkMap(
|
||||
1,
|
||||
presupuesto.getPeso(),
|
||||
false,
|
||||
true));
|
||||
}
|
||||
|
||||
Map<String, Object> direccionesRet = new HashMap<>();
|
||||
direccionesRet.put("direcciones", direccionesPresupuesto);
|
||||
if (!direccionesPrueba.isEmpty())
|
||||
direccionesRet.put("direccionesFP1", direccionesPrueba.get(0));
|
||||
else {
|
||||
direccionesRet.put("direccionesFP1", new ArrayList<>());
|
||||
}
|
||||
return direccionesRet;
|
||||
}
|
||||
} else {
|
||||
List<CartDireccion> direcciones = cart.getDirecciones().stream()
|
||||
.filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(presupuesto.getId()))
|
||||
.toList();
|
||||
|
||||
for (CartDireccion cd : direcciones) {
|
||||
|
||||
// direccion de ejemplar de prueba
|
||||
if (cd.getPresupuesto() == null || !cd.getPresupuesto().getId().equals(presupuesto.getId())) {
|
||||
continue;
|
||||
}
|
||||
if (cd.getUnidades() == null || cd.getUnidades() <= 0) {
|
||||
direccionesPrueba.add(cd.toSkMap(
|
||||
1,
|
||||
presupuesto.getPeso(),
|
||||
false,
|
||||
true));
|
||||
} else {
|
||||
direccionesPresupuesto.add(cd.toSkMap(
|
||||
cd.getUnidades(),
|
||||
presupuesto.getPeso(),
|
||||
cd.getIsPalets(),
|
||||
false));
|
||||
}
|
||||
}
|
||||
if (presupuesto.getServiciosJson() != null
|
||||
&& presupuesto.getServiciosJson().contains("deposito-legal")) {
|
||||
CartDireccion cd = new CartDireccion();
|
||||
direccionesPresupuesto.add(cd.toSkMapDepositoLegal());
|
||||
}
|
||||
}
|
||||
Map<String, Object> direccionesRet = new HashMap<>();
|
||||
direccionesRet.put("direcciones", direccionesPresupuesto);
|
||||
if (!direccionesPrueba.isEmpty())
|
||||
direccionesRet.put("direccionesFP1", direccionesPrueba.get(0));
|
||||
else {
|
||||
direccionesRet.put("direccionesFP1", new ArrayList<>());
|
||||
}
|
||||
return direccionesRet;
|
||||
}
|
||||
|
||||
/***************************************
|
||||
* MÉTODOS PRIVADOS
|
||||
|
||||
@ -101,6 +101,24 @@ public class Utils {
|
||||
throw new IllegalStateException("No se pudo obtener el ID del usuario actual");
|
||||
}
|
||||
|
||||
public static User currentUser(Principal principal) {
|
||||
|
||||
if (principal == null) {
|
||||
throw new IllegalStateException("Usuario no autenticado");
|
||||
}
|
||||
|
||||
if (principal instanceof Authentication auth) {
|
||||
Object principalObj = auth.getPrincipal();
|
||||
|
||||
if (principalObj instanceof UserDetailsImpl udi) {
|
||||
return udi.getUser();
|
||||
} else if (principalObj instanceof User u && u.getId() != null) {
|
||||
return u;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("No se pudo obtener el ID del usuario actual");
|
||||
}
|
||||
|
||||
public static String formatCurrency(BigDecimal amount, Locale locale) {
|
||||
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale);
|
||||
return currencyFormatter.format(amount);
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
package com.imprimelibros.erp.common.jpa;
|
||||
|
||||
import com.imprimelibros.erp.users.User;
|
||||
import jakarta.persistence.*;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public abstract class AbstractAuditedEntitySoftTs {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(name = "updated_at")
|
||||
private Instant updatedAt;
|
||||
|
||||
@CreatedBy
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "created_by")
|
||||
private User createdBy;
|
||||
|
||||
@LastModifiedBy
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "updated_by")
|
||||
private User updatedBy;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private Instant deletedAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "deleted_by")
|
||||
private User deletedBy;
|
||||
|
||||
// Getters/Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public Instant getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
|
||||
|
||||
public User getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(User createdBy) { this.createdBy = createdBy; }
|
||||
|
||||
public User getUpdatedBy() { return updatedBy; }
|
||||
public void setUpdatedBy(User updatedBy) { this.updatedBy = updatedBy; }
|
||||
|
||||
public Instant getDeletedAt() { return deletedAt; }
|
||||
public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; }
|
||||
|
||||
public User getDeletedBy() { return deletedBy; }
|
||||
public void setDeletedBy(User deletedBy) { this.deletedBy = deletedBy; }
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.imprimelibros.erp.common.web;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Entities;
|
||||
|
||||
public class HtmlToXhtml {
|
||||
|
||||
public static String toXhtml(String html) {
|
||||
if (html == null || html.isBlank()) return "";
|
||||
|
||||
Document doc = Jsoup.parseBodyFragment(html);
|
||||
|
||||
doc.outputSettings()
|
||||
.syntax(Document.OutputSettings.Syntax.xml) // => <br/>
|
||||
.escapeMode(Entities.EscapeMode.xhtml) // entidades XHTML
|
||||
.prettyPrint(false); // no metas saltos raros
|
||||
|
||||
// devolvemos sólo el contenido del body (sin <html><head>…)
|
||||
return doc.body().html();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.imprimelibros.erp.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
public class MethodSecurityConfig {
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -229,7 +229,7 @@ public class skApiClient {
|
||||
Long id = ((Integer) responseBody.get("id")).longValue();
|
||||
|
||||
if (success != null && id != null && success) {
|
||||
|
||||
|
||||
return Map.of("data", id);
|
||||
} else {
|
||||
// Tu lógica actual: si success es true u otra cosa → error 2
|
||||
@ -248,7 +248,7 @@ public class skApiClient {
|
||||
return (Long) result.get("data");
|
||||
}
|
||||
|
||||
public Integer getMaxSolapas(Map<String, Object> requestBody, Locale locale) {
|
||||
public Map<String, Object> getMaxSolapas(Map<String, Object> requestBody, Locale locale) {
|
||||
try {
|
||||
String jsonResponse = performWithRetry(() -> {
|
||||
String url = this.skApiUrl + "api/calcular-solapas";
|
||||
@ -289,7 +289,11 @@ public class skApiClient {
|
||||
messageSource.getMessage("presupuesto.errores.error-interior", new Object[] { 1 }, locale));
|
||||
}
|
||||
|
||||
return root.get("data").asInt();
|
||||
Integer maxSolapas = root.get("data").asInt();
|
||||
Double lomo = root.get("lomo").asDouble();
|
||||
return Map.of(
|
||||
"maxSolapas", maxSolapas,
|
||||
"lomo", lomo);
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
// Fallback al 80% del ancho
|
||||
@ -302,7 +306,9 @@ public class skApiClient {
|
||||
throw new RuntimeException("Tamaño no válido en la solicitud: " + requestBody);
|
||||
else {
|
||||
int ancho = (int) tamanio.get("ancho");
|
||||
return (int) Math.floor(ancho * 0.8); // 80% del ancho
|
||||
return Map.of(
|
||||
"maxSolapas", (int) (ancho * 0.8),
|
||||
"lomo", 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -389,6 +395,223 @@ public class skApiClient {
|
||||
|
||||
}
|
||||
|
||||
public Map<String, Object> checkPedidoEstado(Long presupuestoId, Locale locale) {
|
||||
|
||||
try {
|
||||
|
||||
String jsonResponse = performWithRetry(() -> {
|
||||
String url = this.skApiUrl + "api/estado-pedido/" + presupuestoId;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(authService.getToken()); // token actualizado
|
||||
|
||||
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
String.class);
|
||||
|
||||
return response.getBody();
|
||||
});
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JsonNode root = mapper.readTree(jsonResponse);
|
||||
|
||||
if (root.get("data") == null) {
|
||||
throw new RuntimeException(
|
||||
"Sin respuesta desde el servidor del proveedor");
|
||||
}
|
||||
|
||||
String estado = root.get("data").asText();
|
||||
return Map.of(
|
||||
"estado", estado);
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
// Fallback al 80% del ancho
|
||||
return Map.of(
|
||||
"estado", null);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> getFilesTypes(Long presupuestoId, Locale locale) {
|
||||
|
||||
try {
|
||||
|
||||
Map<String, Object> result = performWithRetryMap(() -> {
|
||||
String url = this.skApiUrl + "api/files-presupuesto/" + presupuestoId;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(authService.getToken()); // token actualizado
|
||||
|
||||
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
String.class);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
try {
|
||||
Map<String, Object> responseBody = mapper.readValue(
|
||||
response.getBody(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
// Si la API devuelve "error" a nivel raíz
|
||||
if (responseBody.get("error") != null) {
|
||||
// Devolvemos un mapa con sólo el error para que el caller decida
|
||||
return Map.of("error", responseBody.get("error"));
|
||||
}
|
||||
|
||||
Boolean hasError = (Boolean) (responseBody.get("error") == null
|
||||
|| responseBody.get("error") == "null" ? false : true);
|
||||
Map<String, Boolean> files = (Map<String, Boolean>) responseBody.get("data");
|
||||
|
||||
if (files != null && !hasError) {
|
||||
return Map.of("data", files);
|
||||
} else {
|
||||
// Tu lógica actual: si success es true u otra cosa → error 2
|
||||
return Map.of("error", 2);
|
||||
}
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return Map.of("error", 1);
|
||||
}
|
||||
});
|
||||
|
||||
if (result.get("error") != null) {
|
||||
throw new RuntimeException(
|
||||
messageSource.getMessage("pedido.errors.connecting-server-error", null, locale));
|
||||
}
|
||||
Map<String, Object> data = (Map<String, Object>) result.get("data");
|
||||
return data;
|
||||
|
||||
} catch (RuntimeException e) {
|
||||
throw new RuntimeException(
|
||||
messageSource.getMessage("pedido.errors.connecting-server-error", null, locale));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public byte[] downloadFile(Long presupuestoId, String fileType, Locale locale) {
|
||||
return performWithRetryBytes(() -> {
|
||||
|
||||
String normalized = (fileType == null) ? "" : fileType.trim().toLowerCase();
|
||||
|
||||
String endpoint = switch (normalized) {
|
||||
case "ferro" -> "api/get-ferro/" + presupuestoId;
|
||||
case "cubierta" -> "api/get-cubierta/" + presupuestoId;
|
||||
case "tapa" -> "api/get-tapa/" + presupuestoId;
|
||||
default -> throw new IllegalArgumentException("Tipo de fichero no soportado: " + fileType);
|
||||
};
|
||||
|
||||
// OJO: skApiUrl debería terminar en "/" para que concatene bien
|
||||
String url = this.skApiUrl + endpoint;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
// Si tu CI4 requiere Bearer, mantenlo. Si NO lo requiere, puedes quitar esta
|
||||
// línea.
|
||||
headers.setBearerAuth(authService.getToken());
|
||||
headers.setAccept(List.of(MediaType.APPLICATION_PDF, MediaType.APPLICATION_OCTET_STREAM));
|
||||
|
||||
try {
|
||||
ResponseEntity<byte[]> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(headers),
|
||||
byte[].class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
return response.getBody(); // bytes del PDF
|
||||
}
|
||||
return null;
|
||||
|
||||
} catch (HttpClientErrorException.NotFound e) {
|
||||
// CI4 no tiene ese fichero
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Boolean aceptarFerro(Long presupuestoId, Locale locale) {
|
||||
|
||||
String result = performWithRetry(() -> {
|
||||
String url = this.skApiUrl + "api/aceptar-ferro/" + presupuestoId;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(authService.getToken());
|
||||
|
||||
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.POST,
|
||||
entity,
|
||||
String.class);
|
||||
|
||||
try {
|
||||
Map<String, Object> responseBody = new ObjectMapper().readValue(
|
||||
response.getBody(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
Boolean success = (Boolean) (responseBody.get("success") != null ? responseBody.get("success") : false);
|
||||
|
||||
return success.toString();
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return "false"; // Fallback en caso de error
|
||||
}
|
||||
});
|
||||
return Boolean.parseBoolean(result);
|
||||
}
|
||||
|
||||
|
||||
public Boolean cancelarPedido(Long pedidoId) {
|
||||
|
||||
String result = performWithRetry(() -> {
|
||||
String url = this.skApiUrl + "api/cancelar-pedido/" + pedidoId;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(authService.getToken());
|
||||
|
||||
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.POST,
|
||||
entity,
|
||||
String.class);
|
||||
|
||||
try {
|
||||
Map<String, Object> responseBody = new ObjectMapper().readValue(
|
||||
response.getBody(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
Boolean success = (Boolean) (responseBody.get("success") != null ? responseBody.get("success") : false);
|
||||
|
||||
return success.toString();
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return "false"; // Fallback en caso de error
|
||||
}
|
||||
});
|
||||
return Boolean.parseBoolean(result);
|
||||
}
|
||||
|
||||
/******************
|
||||
* PRIVATE METHODS
|
||||
******************/
|
||||
@ -420,6 +643,19 @@ public class skApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] performWithRetryBytes(Supplier<byte[]> request) {
|
||||
try {
|
||||
return request.get();
|
||||
} catch (HttpClientErrorException.Unauthorized e) {
|
||||
authService.invalidateToken();
|
||||
try {
|
||||
return request.get();
|
||||
} catch (HttpClientErrorException ex) {
|
||||
throw new RuntimeException("La autenticación ha fallado tras renovar el token.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BigDecimal calcularMargen(
|
||||
BigDecimal importe, BigDecimal importeMin, BigDecimal importeMax,
|
||||
BigDecimal margenMax, BigDecimal margenMin) {
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
public enum EstadoFactura {
|
||||
borrador,
|
||||
validada
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
public enum EstadoPagoFactura {
|
||||
pendiente,
|
||||
pagada,
|
||||
cancelada
|
||||
}
|
||||
271
src/main/java/com/imprimelibros/erp/facturacion/Factura.java
Normal file
271
src/main/java/com/imprimelibros/erp/facturacion/Factura.java
Normal file
@ -0,0 +1,271 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs;
|
||||
import com.imprimelibros.erp.users.User;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.hibernate.annotations.Formula;
|
||||
|
||||
@Entity
|
||||
@Table(name = "facturas", uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uq_facturas_numero_factura", columnNames = "numero_factura")
|
||||
})
|
||||
public class Factura extends AbstractAuditedEntitySoftTs {
|
||||
|
||||
@Column(name = "pedido_id")
|
||||
private Long pedidoId;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "factura_rectificada_id")
|
||||
private Factura facturaRectificada;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "factura_rectificativa_id")
|
||||
private Factura facturaRectificativa;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "cliente_id")
|
||||
private User cliente;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "serie_id")
|
||||
private SerieFactura serie;
|
||||
|
||||
@Column(name = "numero_factura", length = 50)
|
||||
private String numeroFactura;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "estado", nullable = false, length = 20)
|
||||
private EstadoFactura estado = EstadoFactura.borrador;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "estado_pago", nullable = false, length = 20)
|
||||
private EstadoPagoFactura estadoPago = EstadoPagoFactura.pendiente;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "tipo_pago", nullable = false, length = 30)
|
||||
private TipoPago tipoPago = TipoPago.otros;
|
||||
|
||||
@Column(name = "fecha_emision")
|
||||
private LocalDateTime fechaEmision;
|
||||
|
||||
@Column(name = "base_imponible", precision = 10, scale = 2)
|
||||
private BigDecimal baseImponible;
|
||||
|
||||
@Column(name = "iva_4", precision = 10, scale = 2)
|
||||
private BigDecimal iva4;
|
||||
|
||||
@Column(name = "iva_21", precision = 10, scale = 2)
|
||||
private BigDecimal iva21;
|
||||
|
||||
@Column(name = "total_factura", precision = 10, scale = 2)
|
||||
private BigDecimal totalFactura;
|
||||
|
||||
@Column(name = "total_pagado", precision = 10, scale = 2)
|
||||
private BigDecimal totalPagado = new BigDecimal("0.00");
|
||||
|
||||
@Lob
|
||||
@Column(name = "notas")
|
||||
private String notas;
|
||||
|
||||
@OneToMany(mappedBy = "factura", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<FacturaLinea> lineas = new ArrayList<>();
|
||||
|
||||
@OneToMany(mappedBy = "factura", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<FacturaPago> pagos = new ArrayList<>();
|
||||
|
||||
@OneToMany(mappedBy = "factura", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<FacturaDireccion> direcciones = new ArrayList<>();
|
||||
|
||||
@Formula("(select u.fullname from users u where u.id = cliente_id)")
|
||||
private String clienteNombre;
|
||||
|
||||
// Helpers
|
||||
public void addLinea(FacturaLinea linea) {
|
||||
linea.setFactura(this);
|
||||
this.lineas.add(linea);
|
||||
}
|
||||
|
||||
public void removeLinea(FacturaLinea linea) {
|
||||
this.lineas.remove(linea);
|
||||
linea.setFactura(null);
|
||||
}
|
||||
|
||||
public void addPago(FacturaPago pago) {
|
||||
pago.setFactura(this);
|
||||
this.pagos.add(pago);
|
||||
}
|
||||
|
||||
public void removePago(FacturaPago pago) {
|
||||
this.pagos.remove(pago);
|
||||
pago.setFactura(null);
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
public Long getPedidoId() {
|
||||
return pedidoId;
|
||||
}
|
||||
|
||||
public void setPedidoId(Long pedidoId) {
|
||||
this.pedidoId = pedidoId;
|
||||
}
|
||||
|
||||
public Factura getFacturaRectificada() {
|
||||
return facturaRectificada;
|
||||
}
|
||||
|
||||
public void setFacturaRectificada(Factura facturaRectificada) {
|
||||
this.facturaRectificada = facturaRectificada;
|
||||
}
|
||||
|
||||
public Factura getFacturaRectificativa() {
|
||||
return facturaRectificativa;
|
||||
}
|
||||
|
||||
public void setFacturaRectificativa(Factura facturaRectificativa) {
|
||||
this.facturaRectificativa = facturaRectificativa;
|
||||
}
|
||||
|
||||
public User getCliente() {
|
||||
return cliente;
|
||||
}
|
||||
|
||||
public void setCliente(User cliente) {
|
||||
this.cliente = cliente;
|
||||
}
|
||||
|
||||
public SerieFactura getSerie() {
|
||||
return serie;
|
||||
}
|
||||
|
||||
public void setSerie(SerieFactura serie) {
|
||||
this.serie = serie;
|
||||
}
|
||||
|
||||
public String getNumeroFactura() {
|
||||
return numeroFactura;
|
||||
}
|
||||
|
||||
public void setNumeroFactura(String numeroFactura) {
|
||||
this.numeroFactura = numeroFactura;
|
||||
}
|
||||
|
||||
public EstadoFactura getEstado() {
|
||||
return estado;
|
||||
}
|
||||
|
||||
public void setEstado(EstadoFactura estado) {
|
||||
this.estado = estado;
|
||||
}
|
||||
|
||||
public EstadoPagoFactura getEstadoPago() {
|
||||
return estadoPago;
|
||||
}
|
||||
|
||||
public void setEstadoPago(EstadoPagoFactura estadoPago) {
|
||||
this.estadoPago = estadoPago;
|
||||
}
|
||||
|
||||
public TipoPago getTipoPago() {
|
||||
return tipoPago;
|
||||
}
|
||||
|
||||
public void setTipoPago(TipoPago tipoPago) {
|
||||
this.tipoPago = tipoPago;
|
||||
}
|
||||
|
||||
public LocalDateTime getFechaEmision() {
|
||||
return fechaEmision;
|
||||
}
|
||||
|
||||
public void setFechaEmision(LocalDateTime fechaEmision) {
|
||||
this.fechaEmision = fechaEmision;
|
||||
}
|
||||
|
||||
public BigDecimal getBaseImponible() {
|
||||
return baseImponible;
|
||||
}
|
||||
|
||||
public void setBaseImponible(BigDecimal baseImponible) {
|
||||
this.baseImponible = baseImponible;
|
||||
}
|
||||
|
||||
public BigDecimal getIva4() {
|
||||
return iva4;
|
||||
}
|
||||
|
||||
public void setIva4(BigDecimal iva4) {
|
||||
this.iva4 = iva4;
|
||||
}
|
||||
|
||||
public BigDecimal getIva21() {
|
||||
return iva21;
|
||||
}
|
||||
|
||||
public void setIva21(BigDecimal iva21) {
|
||||
this.iva21 = iva21;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalFactura() {
|
||||
return totalFactura;
|
||||
}
|
||||
|
||||
public void setTotalFactura(BigDecimal totalFactura) {
|
||||
this.totalFactura = totalFactura;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalPagado() {
|
||||
return totalPagado;
|
||||
}
|
||||
|
||||
public void setTotalPagado(BigDecimal totalPagado) {
|
||||
this.totalPagado = totalPagado;
|
||||
}
|
||||
|
||||
public String getNotas() {
|
||||
return notas;
|
||||
}
|
||||
|
||||
public void setNotas(String notas) {
|
||||
this.notas = notas;
|
||||
}
|
||||
|
||||
public List<FacturaLinea> getLineas() {
|
||||
return lineas;
|
||||
}
|
||||
|
||||
public void setLineas(List<FacturaLinea> lineas) {
|
||||
this.lineas = lineas;
|
||||
}
|
||||
|
||||
public List<FacturaPago> getPagos() {
|
||||
return pagos;
|
||||
}
|
||||
|
||||
public void setPagos(List<FacturaPago> pagos) {
|
||||
this.pagos = pagos;
|
||||
}
|
||||
|
||||
public List<FacturaDireccion> getDirecciones() {
|
||||
return direcciones;
|
||||
}
|
||||
|
||||
public void setDirecciones(List<FacturaDireccion> direcciones) {
|
||||
this.direcciones = direcciones;
|
||||
}
|
||||
|
||||
public FacturaDireccion getDireccionFacturacion() {
|
||||
return (direcciones == null || direcciones.isEmpty()) ? null : direcciones.get(0);
|
||||
}
|
||||
|
||||
public void addDireccion(FacturaDireccion direccion) {
|
||||
direccion.setFactura(this);
|
||||
this.direcciones.add(direccion);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,209 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
import com.imprimelibros.erp.direcciones.Direccion.TipoIdentificacionFiscal;
|
||||
import com.imprimelibros.erp.paises.Paises;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "facturas_direcciones",
|
||||
indexes = {
|
||||
@Index(name = "idx_facturas_direcciones_factura_id", columnList = "factura_id")
|
||||
}
|
||||
)
|
||||
public class FacturaDireccion {
|
||||
|
||||
@Column(name = "id")
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "factura_id", nullable = false,
|
||||
foreignKey = @ForeignKey(name = "fk_facturas_direcciones_factura"))
|
||||
private Factura factura;
|
||||
|
||||
@Column(name = "unidades")
|
||||
private Integer unidades; // MEDIUMINT UNSIGNED
|
||||
|
||||
@Column(name = "email", length = 255)
|
||||
private String email;
|
||||
|
||||
@Column(name = "att", length = 150, nullable = false)
|
||||
private String att;
|
||||
|
||||
@Column(name = "direccion", length = 255, nullable = false)
|
||||
private String direccion;
|
||||
|
||||
@Column(name = "cp", nullable = false)
|
||||
private Integer cp; // MEDIUMINT UNSIGNED
|
||||
|
||||
@Column(name = "ciudad", length = 100, nullable = false)
|
||||
private String ciudad;
|
||||
|
||||
@Column(name = "provincia", length = 100, nullable = false)
|
||||
private String provincia;
|
||||
|
||||
@Column(name = "pais_code3", length = 3, nullable = false)
|
||||
private String paisCode3 = "esp";
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "pais_code3", referencedColumnName = "code3", insertable = false, updatable = false)
|
||||
private Paises pais;
|
||||
|
||||
@Column(name = "telefono", length = 30)
|
||||
private String telefono;
|
||||
|
||||
@Column(name = "instrucciones", length = 255)
|
||||
private String instrucciones;
|
||||
|
||||
@Column(name = "razon_social", length = 150)
|
||||
private String razonSocial;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "tipo_identificacion_fiscal", length = 20, nullable = false)
|
||||
private TipoIdentificacionFiscal tipoIdentificacionFiscal = TipoIdentificacionFiscal.DNI;
|
||||
|
||||
@Column(name = "identificacion_fiscal", length = 50)
|
||||
private String identificacionFiscal;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private java.time.Instant createdAt;
|
||||
|
||||
|
||||
// Getters / Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Factura getFactura() {
|
||||
return factura;
|
||||
}
|
||||
|
||||
public void setFactura(Factura factura) {
|
||||
this.factura = factura;
|
||||
}
|
||||
|
||||
public Integer getUnidades() {
|
||||
return unidades;
|
||||
}
|
||||
|
||||
public void setUnidades(Integer unidades) {
|
||||
this.unidades = unidades;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getAtt() {
|
||||
return att;
|
||||
}
|
||||
|
||||
public void setAtt(String att) {
|
||||
this.att = att;
|
||||
}
|
||||
|
||||
public String getDireccion() {
|
||||
return direccion;
|
||||
}
|
||||
|
||||
public void setDireccion(String direccion) {
|
||||
this.direccion = direccion;
|
||||
}
|
||||
|
||||
public Integer getCp() {
|
||||
return cp;
|
||||
}
|
||||
|
||||
public void setCp(Integer cp) {
|
||||
this.cp = cp;
|
||||
}
|
||||
|
||||
public String getCiudad() {
|
||||
return ciudad;
|
||||
}
|
||||
|
||||
public void setCiudad(String ciudad) {
|
||||
this.ciudad = ciudad;
|
||||
}
|
||||
|
||||
public String getProvincia() {
|
||||
return provincia;
|
||||
}
|
||||
|
||||
public void setProvincia(String provincia) {
|
||||
this.provincia = provincia;
|
||||
}
|
||||
|
||||
public String getPaisCode3() {
|
||||
return paisCode3;
|
||||
}
|
||||
|
||||
public void setPaisCode3(String paisCode3) {
|
||||
this.paisCode3 = paisCode3;
|
||||
}
|
||||
|
||||
public Paises getPais() {
|
||||
return pais;
|
||||
}
|
||||
|
||||
public void setPais(Paises pais) {
|
||||
this.pais = pais;
|
||||
}
|
||||
|
||||
public String getTelefono() {
|
||||
return telefono;
|
||||
}
|
||||
|
||||
public void setTelefono(String telefono) {
|
||||
this.telefono = telefono;
|
||||
}
|
||||
|
||||
public String getInstrucciones() {
|
||||
return instrucciones;
|
||||
}
|
||||
|
||||
public void setInstrucciones(String instrucciones) {
|
||||
this.instrucciones = instrucciones;
|
||||
}
|
||||
|
||||
public String getRazonSocial() {
|
||||
return razonSocial;
|
||||
}
|
||||
|
||||
public void setRazonSocial(String razonSocial) {
|
||||
this.razonSocial = razonSocial;
|
||||
}
|
||||
|
||||
public TipoIdentificacionFiscal getTipoIdentificacionFiscal() {
|
||||
return tipoIdentificacionFiscal;
|
||||
}
|
||||
|
||||
public void setTipoIdentificacionFiscal(TipoIdentificacionFiscal tipoIdentificacionFiscal) {
|
||||
this.tipoIdentificacionFiscal = tipoIdentificacionFiscal;
|
||||
}
|
||||
|
||||
public String getIdentificacionFiscal() {
|
||||
return identificacionFiscal;
|
||||
}
|
||||
|
||||
public void setIdentificacionFiscal(String identificacionFiscal) {
|
||||
this.identificacionFiscal = identificacionFiscal;
|
||||
}
|
||||
|
||||
public java.time.Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
public void setCreatedAt(java.time.Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Entity
|
||||
@Table(name = "facturas_lineas")
|
||||
public class FacturaLinea extends AbstractAuditedEntitySoftTs {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "factura_id")
|
||||
private Factura factura;
|
||||
|
||||
@Lob
|
||||
@Column(name = "descripcion")
|
||||
private String descripcion;
|
||||
|
||||
@Column(name = "cantidad")
|
||||
private Integer cantidad;
|
||||
|
||||
@Column(name = "base_linea", precision = 10, scale = 2)
|
||||
private BigDecimal baseLinea;
|
||||
|
||||
@Column(name = "iva_4_linea", precision = 10, scale = 2)
|
||||
private BigDecimal iva4Linea;
|
||||
|
||||
@Column(name = "iva_21_linea", precision = 10, scale = 2)
|
||||
private BigDecimal iva21Linea;
|
||||
|
||||
@Column(name = "total_linea", precision = 10, scale = 2)
|
||||
private BigDecimal totalLinea;
|
||||
|
||||
// Getters/Setters
|
||||
public Factura getFactura() { return factura; }
|
||||
public void setFactura(Factura factura) { this.factura = factura; }
|
||||
|
||||
public String getDescripcion() { return descripcion; }
|
||||
public void setDescripcion(String descripcion) { this.descripcion = descripcion; }
|
||||
|
||||
public Integer getCantidad() { return cantidad; }
|
||||
public void setCantidad(Integer cantidad) { this.cantidad = cantidad; }
|
||||
|
||||
public BigDecimal getBaseLinea() { return baseLinea; }
|
||||
public void setBaseLinea(BigDecimal baseLinea) { this.baseLinea = baseLinea; }
|
||||
|
||||
public BigDecimal getIva4Linea() { return iva4Linea; }
|
||||
public void setIva4Linea(BigDecimal iva4Linea) { this.iva4Linea = iva4Linea; }
|
||||
|
||||
public BigDecimal getIva21Linea() { return iva21Linea; }
|
||||
public void setIva21Linea(BigDecimal iva21Linea) { this.iva21Linea = iva21Linea; }
|
||||
|
||||
public BigDecimal getTotalLinea() { return totalLinea; }
|
||||
public void setTotalLinea(BigDecimal totalLinea) { this.totalLinea = totalLinea; }
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "facturas_pagos")
|
||||
public class FacturaPago extends AbstractAuditedEntitySoftTs {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "factura_id")
|
||||
private Factura factura;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "metodo_pago", nullable = false, length = 30)
|
||||
private TipoPago metodoPago = TipoPago.otros;
|
||||
|
||||
@Column(name = "cantidad_pagada", precision = 10, scale = 2)
|
||||
private BigDecimal cantidadPagada;
|
||||
|
||||
@Column(name = "fecha_pago")
|
||||
private LocalDateTime fechaPago;
|
||||
|
||||
@Lob
|
||||
@Column(name = "notas")
|
||||
private String notas;
|
||||
|
||||
// Getters/Setters
|
||||
public Factura getFactura() { return factura; }
|
||||
public void setFactura(Factura factura) { this.factura = factura; }
|
||||
|
||||
public TipoPago getMetodoPago() { return metodoPago; }
|
||||
public void setMetodoPago(TipoPago metodoPago) { this.metodoPago = metodoPago; }
|
||||
|
||||
public BigDecimal getCantidadPagada() { return cantidadPagada; }
|
||||
public void setCantidadPagada(BigDecimal cantidadPagada) { this.cantidadPagada = cantidadPagada; }
|
||||
|
||||
public LocalDateTime getFechaPago() { return fechaPago; }
|
||||
public void setFechaPago(LocalDateTime fechaPago) { this.fechaPago = fechaPago; }
|
||||
|
||||
public String getNotas() { return notas; }
|
||||
public void setNotas(String notas) { this.notas = notas; }
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "series_facturas")
|
||||
public class SerieFactura extends AbstractAuditedEntitySoftTs {
|
||||
|
||||
@Column(name = "nombre_serie", nullable = false, length = 100)
|
||||
private String nombreSerie;
|
||||
|
||||
@Column(name = "prefijo", nullable = false, length = 10)
|
||||
private String prefijo;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "tipo", nullable = false, length = 50)
|
||||
private TipoSerieFactura tipo = TipoSerieFactura.facturacion;
|
||||
|
||||
@Column(name = "numero_actual", nullable = false)
|
||||
private Long numeroActual = 1L;
|
||||
|
||||
public String getNombreSerie() { return nombreSerie; }
|
||||
public void setNombreSerie(String nombreSerie) { this.nombreSerie = nombreSerie; }
|
||||
|
||||
public String getPrefijo() { return prefijo; }
|
||||
public void setPrefijo(String prefijo) { this.prefijo = prefijo; }
|
||||
|
||||
public TipoSerieFactura getTipo() { return tipo; }
|
||||
public void setTipo(TipoSerieFactura tipo) { this.tipo = tipo; }
|
||||
|
||||
public Long getNumeroActual() { return numeroActual; }
|
||||
public void setNumeroActual(Long numeroActual) { this.numeroActual = numeroActual; }
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
public enum TipoPago {
|
||||
tpv_tarjeta,
|
||||
tpv_bizum,
|
||||
transferencia,
|
||||
otros
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
public enum TipoSerieFactura {
|
||||
facturacion
|
||||
}
|
||||
@ -0,0 +1,390 @@
|
||||
package com.imprimelibros.erp.facturacion.controller;
|
||||
|
||||
import com.imprimelibros.erp.configurationERP.VariableService;
|
||||
import com.imprimelibros.erp.datatables.DataTable;
|
||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||
import com.imprimelibros.erp.facturacion.EstadoFactura;
|
||||
import com.imprimelibros.erp.facturacion.Factura;
|
||||
import com.imprimelibros.erp.facturacion.FacturaDireccion;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaAddRequestDto;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaGuardarDto;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaLineaUpsertDto;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaPagoUpsertDto;
|
||||
import com.imprimelibros.erp.facturacion.repo.FacturaRepository;
|
||||
import com.imprimelibros.erp.facturacion.service.FacturacionService;
|
||||
import com.imprimelibros.erp.i18n.TranslationService;
|
||||
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/facturas")
|
||||
@PreAuthorize("hasRole('SUPERADMIN') || hasRole('ADMIN')")
|
||||
public class FacturasController {
|
||||
|
||||
private final FacturacionService facturacionService;
|
||||
|
||||
private final FacturaRepository repo;
|
||||
private final TranslationService translationService;
|
||||
private final MessageSource messageSource;
|
||||
private final PedidoService pedidoService;
|
||||
private final VariableService variableService;
|
||||
private final DireccionService direccionService;
|
||||
|
||||
public FacturasController(
|
||||
FacturaRepository repo,
|
||||
TranslationService translationService,
|
||||
MessageSource messageSource,
|
||||
PedidoService pedidoService, FacturacionService facturacionService, VariableService variableService, DireccionService direccionService) {
|
||||
this.repo = repo;
|
||||
this.translationService = translationService;
|
||||
this.messageSource = messageSource;
|
||||
this.pedidoService = pedidoService;
|
||||
this.facturacionService = facturacionService;
|
||||
this.direccionService = direccionService;
|
||||
this.variableService = variableService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String facturasList(Model model, Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"app.eliminar",
|
||||
"app.cancelar",
|
||||
"facturas.delete.title",
|
||||
"facturas.delete.text",
|
||||
"facturas.delete.ok.title",
|
||||
"facturas.delete.ok.text");
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
return "imprimelibros/facturas/facturas-list";
|
||||
}
|
||||
|
||||
@GetMapping("/add")
|
||||
public String facturaAdd(Model model, Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"facturas.form.cliente.placeholder",
|
||||
"facturas.add.form.validation.title",
|
||||
"facturas.add.form.validation",
|
||||
"facturas.error.create"
|
||||
);
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
model.addAttribute("defaultSerieRectificativa", variableService.getValorEntero("serie_facturacion_rect_default"));
|
||||
|
||||
return "imprimelibros/facturas/facturas-add-form";
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
@ResponseBody
|
||||
public Map<String, Object> facturaAddPost(
|
||||
Model model,
|
||||
@RequestBody FacturaAddRequestDto request,
|
||||
Locale locale) {
|
||||
|
||||
Factura nuevaFactura = facturacionService.crearNuevaFactura(
|
||||
request.getUser(),
|
||||
request.getSerie(),
|
||||
request.getDireccion(),
|
||||
request.getFactura_rectificada()
|
||||
);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
if(nuevaFactura == null){
|
||||
result.put("success", false);
|
||||
result.put("message", messageSource.getMessage("facturas.error.create", null, "No se ha podido crear la factura. Revise los datos e inténtelo de nuevo.", locale));
|
||||
return result;
|
||||
}
|
||||
else{
|
||||
result.put("success", true);
|
||||
result.put("facturaId", nuevaFactura.getId());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public String facturaDetail(@PathVariable Long id, Model model, Locale locale) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
|
||||
List<String> keys = List.of(
|
||||
"facturas.lineas.error.base",
|
||||
"facturas.lineas.delete.title",
|
||||
"facturas.lineas.delete.text",
|
||||
|
||||
"facturas.pagos.delete.title",
|
||||
"facturas.pagos.delete.text",
|
||||
"facturas.pagos.error.cantidad",
|
||||
"facturas.pagos.error.fecha",
|
||||
|
||||
"app.eliminar",
|
||||
"app.cancelar");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
FacturaDireccion direccionFacturacion = factura.getDireccionFacturacion();
|
||||
|
||||
model.addAttribute("direccionFacturacion", direccionFacturacion);
|
||||
model.addAttribute("factura", factura);
|
||||
|
||||
return "imprimelibros/facturas/facturas-form";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/guardar")
|
||||
public ResponseEntity<?> guardarFacturaCabeceraYDireccion(
|
||||
@PathVariable Long id,
|
||||
@RequestBody @Valid FacturaGuardarDto payload) {
|
||||
facturacionService.guardarCabeceraYDireccionFacturacion(id, payload);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/container")
|
||||
public String facturaContainer(@PathVariable Long id, Model model, Locale locale) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
|
||||
FacturaDireccion direccionFacturacion = factura.getDireccionFacturacion();
|
||||
|
||||
|
||||
model.addAttribute("direccionFacturacion", direccionFacturacion);
|
||||
model.addAttribute("factura", factura);
|
||||
|
||||
return "imprimelibros/facturas/partials/factura-container :: factura-container";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/validar")
|
||||
public ResponseEntity<?> validarFactura(@PathVariable Long id) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
|
||||
if (factura.getEstado() != EstadoFactura.borrador) {
|
||||
return ResponseEntity.badRequest().body("Solo se pueden validar facturas en estado 'borrador'.");
|
||||
}
|
||||
|
||||
facturacionService.validarFactura(factura.getId());
|
||||
repo.save(factura);
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/borrador")
|
||||
public ResponseEntity<?> marcarBorrador(@PathVariable Long id) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
|
||||
if (factura.getEstado() != EstadoFactura.validada) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("Solo se pueden marcar como borrador facturas en estado 'validada'.");
|
||||
}
|
||||
|
||||
factura.setEstado(EstadoFactura.borrador);
|
||||
repo.save(factura);
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{facturaId}/lineas")
|
||||
public ResponseEntity<?> createLinea(@PathVariable Long facturaId,
|
||||
@Valid @RequestBody FacturaLineaUpsertDto req) {
|
||||
facturacionService.createLinea(facturaId, req);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
@PostMapping("/{facturaId}/lineas/{lineaId}")
|
||||
public ResponseEntity<?> updateLinea(@PathVariable Long facturaId,
|
||||
@PathVariable Long lineaId,
|
||||
@Valid @RequestBody FacturaLineaUpsertDto req) {
|
||||
facturacionService.upsertLinea(facturaId, req);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
@PostMapping("/{facturaId}/lineas/{lineaId}/delete")
|
||||
public ResponseEntity<?> deleteLinea(@PathVariable Long facturaId,
|
||||
@PathVariable Long lineaId) {
|
||||
facturacionService.borrarLinea(facturaId, lineaId);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* -----------------------------
|
||||
* Pagos
|
||||
* --------------------------------
|
||||
*/
|
||||
@PostMapping("/{facturaId}/pagos")
|
||||
public ResponseEntity<?> createPago(
|
||||
@PathVariable Long facturaId,
|
||||
@Valid @RequestBody FacturaPagoUpsertDto req, Principal principal) {
|
||||
facturacionService.upsertPago(facturaId, req, principal);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
@PostMapping("/{facturaId}/pagos/{pagoId}")
|
||||
public ResponseEntity<?> updatePago(
|
||||
@PathVariable Long facturaId,
|
||||
@PathVariable Long pagoId,
|
||||
@Valid @RequestBody FacturaPagoUpsertDto req,
|
||||
Principal principal) {
|
||||
// opcional: fuerza consistencia
|
||||
req.setId(pagoId);
|
||||
facturacionService.upsertPago(facturaId, req, principal);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
@PostMapping("/{facturaId}/pagos/{pagoId}/delete")
|
||||
public ResponseEntity<?> deletePago(
|
||||
@PathVariable Long facturaId,
|
||||
@PathVariable Long pagoId, Principal principal) {
|
||||
facturacionService.borrarPago(facturaId, pagoId, principal);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/notas")
|
||||
public ResponseEntity<?> setNotas(
|
||||
@PathVariable Long id,
|
||||
@RequestBody Map<String, String> payload,
|
||||
Model model,
|
||||
Locale locale) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
String notas = payload.get("notas");
|
||||
factura.setNotas(notas);
|
||||
repo.save(factura);
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: DataTables (server-side)
|
||||
// -----------------------------
|
||||
@GetMapping("/api/datatables")
|
||||
@ResponseBody
|
||||
public DataTablesResponse<Map<String, Object>> datatables(HttpServletRequest request, Locale locale) {
|
||||
|
||||
DataTablesRequest dt = DataTablesParser.from(request);
|
||||
|
||||
Specification<Factura> notDeleted = (root, q, cb) -> cb.isNull(root.get("deletedAt"));
|
||||
long total = repo.count(notDeleted);
|
||||
|
||||
return DataTable
|
||||
.of(repo, Factura.class, dt, List.of("clienteNombre", "numeroFactura", "estado", "estadoPago"))
|
||||
.where(notDeleted)
|
||||
.orderable(List.of("id", "clienteNombre", "numeroFactura", "estado", "estadoPago"))
|
||||
.onlyAddedColumns()
|
||||
.add("id", Factura::getId)
|
||||
.add("cliente", f -> {
|
||||
var c = f.getCliente();
|
||||
return c == null ? null : c.getFullName(); // o getNombre(), etc.
|
||||
})
|
||||
|
||||
.add("numero_factura", Factura::getNumeroFactura)
|
||||
.add("estado", Factura::getEstado)
|
||||
.add("estado_label", f -> {
|
||||
String key = "facturas.estado." + f.getEstado().name().toLowerCase();
|
||||
return messageSource.getMessage(key, null, f.getEstado().name(), locale);
|
||||
})
|
||||
.add("estado_pago", Factura::getEstadoPago)
|
||||
.add("estado_pago_label", f -> {
|
||||
String key = "facturas.estado-pago." + f.getEstadoPago().name().toLowerCase();
|
||||
return messageSource.getMessage(key, null, f.getEstadoPago().name(), locale);
|
||||
})
|
||||
.add("total", Factura::getTotalFactura)
|
||||
.add("fecha_emision", f -> {
|
||||
LocalDateTime fecha = f.getFechaEmision();
|
||||
return fecha == null ? null : fecha.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
|
||||
})
|
||||
.add("actions", f -> {
|
||||
if (f.getEstado() == EstadoFactura.borrador) {
|
||||
return """
|
||||
<div class="hstack gap-3 flex-wrap">
|
||||
<button type="button"
|
||||
class="btn p-0 link-success btn-view-factura fs-15"
|
||||
data-id="%d">
|
||||
<i class="ri-eye-line"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn p-0 link-danger btn-delete-factura fs-15"
|
||||
data-id="%d">
|
||||
<i class="ri-delete-bin-5-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
""".formatted(f.getId(), f.getId());
|
||||
} else {
|
||||
return """
|
||||
<div class="hstack gap-3 flex-wrap">
|
||||
<button type="button"
|
||||
class="btn p-0 link-success btn-view-factura fs-15"
|
||||
data-id="%d">
|
||||
<i class="ri-eye-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
""".formatted(f.getId());
|
||||
}
|
||||
})
|
||||
.toJson(total);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: select2 Direcciones
|
||||
// -----------------------------
|
||||
@GetMapping("/api/get-direcciones")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getSelect2Facturacion(
|
||||
@RequestParam(value = "q", required = false) String q1,
|
||||
@RequestParam(value = "term", required = false) String q2,
|
||||
@RequestParam(value = "user_id", required = true) Long userId,
|
||||
Authentication auth) {
|
||||
|
||||
|
||||
return direccionService.getForSelectFacturacion(q1, q2, userId);
|
||||
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: select2 facturas rectificables
|
||||
// -----------------------------
|
||||
@GetMapping("/api/get-facturas-rectificables")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getSelect2FacturasRectificables(
|
||||
@RequestParam(value = "q", required = false) String q1,
|
||||
@RequestParam(value = "term", required = false) String q2,
|
||||
@RequestParam(value = "user_id", required = true) Long userId,
|
||||
Authentication auth) {
|
||||
try {
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return Map.of("results", List.of());
|
||||
}
|
||||
return facturacionService.getForSelectFacturasRectificables(q1, q2, userId);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,226 @@
|
||||
package com.imprimelibros.erp.facturacion.controller;
|
||||
|
||||
import com.imprimelibros.erp.datatables.DataTable;
|
||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||
import com.imprimelibros.erp.facturacion.SerieFactura;
|
||||
import com.imprimelibros.erp.facturacion.TipoSerieFactura;
|
||||
import com.imprimelibros.erp.facturacion.repo.SerieFacturaRepository;
|
||||
import com.imprimelibros.erp.i18n.TranslationService;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/configuracion/series-facturacion")
|
||||
@PreAuthorize("hasRole('SUPERADMIN')")
|
||||
public class SeriesFacturacionController {
|
||||
|
||||
private final SerieFacturaRepository repo;
|
||||
private final TranslationService translationService;
|
||||
private final MessageSource messageSource;
|
||||
|
||||
public SeriesFacturacionController(SerieFacturaRepository repo, TranslationService translationService,
|
||||
MessageSource messageSource) {
|
||||
this.repo = repo;
|
||||
this.translationService = translationService;
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// VISTA
|
||||
// -----------------------------
|
||||
@GetMapping
|
||||
public String listView(Model model, Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"series-facturacion.modal.title.add",
|
||||
"series-facturacion.modal.title.edit",
|
||||
"app.guardar",
|
||||
"app.cancelar",
|
||||
"app.eliminar",
|
||||
"series-facturacion.delete.title",
|
||||
"series-facturacion.delete.text",
|
||||
"series-facturacion.delete.ok.title",
|
||||
"series-facturacion.delete.ok.text");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
return "imprimelibros/configuracion/series-facturas/series-facturas-list";
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: DataTables (server-side)
|
||||
// -----------------------------
|
||||
@GetMapping("/api/datatables")
|
||||
@ResponseBody
|
||||
public DataTablesResponse<Map<String, Object>> datatables(HttpServletRequest request, Locale locale) {
|
||||
|
||||
DataTablesRequest dt = DataTablesParser.from(request);
|
||||
|
||||
Specification<SerieFactura> notDeleted = (root, q, cb) -> cb.isNull(root.get("deletedAt"));
|
||||
long total = repo.count(notDeleted);
|
||||
|
||||
return DataTable
|
||||
.of(repo, SerieFactura.class, dt, List.of("nombreSerie", "prefijo"))
|
||||
.where(notDeleted)
|
||||
.orderable(List.of("id", "nombreSerie", "prefijo", "tipo", "numeroActual"))
|
||||
.onlyAddedColumns()
|
||||
.add("id", SerieFactura::getId)
|
||||
.add("nombre_serie", SerieFactura::getNombreSerie)
|
||||
.add("prefijo", SerieFactura::getPrefijo)
|
||||
.add("tipo", s -> s.getTipo() != null ? s.getTipo().name() : null)
|
||||
.add("tipo_label", s -> {
|
||||
if (s.getTipo() == null)
|
||||
return null;
|
||||
return messageSource.getMessage(
|
||||
"series-facturacion.tipo." + s.getTipo().name(),
|
||||
null,
|
||||
s.getTipo().name(),
|
||||
locale);
|
||||
})
|
||||
.add("numero_actual", SerieFactura::getNumeroActual)
|
||||
.add("actions", s -> """
|
||||
<div class="hstack gap-3 flex-wrap">
|
||||
<button type="button"
|
||||
class="btn p-0 link-success btn-edit-serie fs-15"
|
||||
data-id="%d">
|
||||
<i class="ri-edit-2-line"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn p-0 link-danger btn-delete-serie fs-15"
|
||||
data-id="%d">
|
||||
<i class="ri-delete-bin-5-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
""".formatted(s.getId(), s.getId()))
|
||||
.toJson(total);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: CREATE
|
||||
// -----------------------------
|
||||
@PostMapping(value = "/api", consumes = "application/json")
|
||||
@ResponseBody
|
||||
public Map<String, Object> create(@RequestBody SerieFacturaPayload payload) {
|
||||
validate(payload);
|
||||
|
||||
SerieFactura s = new SerieFactura();
|
||||
s.setNombreSerie(payload.nombre_serie.trim());
|
||||
s.setPrefijo(payload.prefijo.trim());
|
||||
s.setTipo(TipoSerieFactura.facturacion); // fijo
|
||||
s.setNumeroActual(payload.numero_actual);
|
||||
|
||||
repo.save(s);
|
||||
return Map.of("ok", true, "id", s.getId());
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: UPDATE
|
||||
// -----------------------------
|
||||
@PutMapping(value = "/api/{id}", consumes = "application/json")
|
||||
@ResponseBody
|
||||
public Map<String, Object> update(@PathVariable Long id, @RequestBody SerieFacturaPayload payload) {
|
||||
validate(payload);
|
||||
|
||||
SerieFactura s = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + id));
|
||||
|
||||
if (s.getDeletedAt() != null) {
|
||||
throw new IllegalStateException("No se puede editar una serie eliminada.");
|
||||
}
|
||||
|
||||
s.setNombreSerie(payload.nombre_serie.trim());
|
||||
s.setPrefijo(payload.prefijo.trim());
|
||||
s.setTipo(TipoSerieFactura.facturacion);
|
||||
s.setNumeroActual(payload.numero_actual);
|
||||
|
||||
repo.save(s);
|
||||
return Map.of("ok", true);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: DELETE (soft)
|
||||
// -----------------------------
|
||||
@DeleteMapping("/api/{id}")
|
||||
@ResponseBody
|
||||
public ResponseEntity<?> delete(@PathVariable Long id) {
|
||||
SerieFactura s = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + id));
|
||||
|
||||
if (s.getDeletedAt() == null) {
|
||||
s.setDeletedAt(Instant.now());
|
||||
s.setDeletedBy(null); // luego lo conectamos al usuario actual
|
||||
repo.save(s);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: GET for select2
|
||||
// -----------------------------
|
||||
@GetMapping("/api/get-series")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getSeriesForSelect(
|
||||
@RequestParam(value = "q", required = false) String q1,
|
||||
@RequestParam(value = "term", required = false) String q2,
|
||||
Locale locale) {
|
||||
String query = (q1 != null && !q1.isBlank()) ? q1
|
||||
: (q2 != null && !q2.isBlank()) ? q2
|
||||
: "";
|
||||
List<Map<String, Object>> results = repo.searchForSelectSeriesFacturacion(query).stream()
|
||||
.map(s -> {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", s.getId());
|
||||
m.put("text", s.getNombreSerie());
|
||||
return m;
|
||||
})
|
||||
.toList();
|
||||
|
||||
return Map.of("results", results);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Payload + validación
|
||||
// -----------------------------
|
||||
public static class SerieFacturaPayload {
|
||||
public String nombre_serie;
|
||||
public String prefijo;
|
||||
public String tipo; // lo manda UI, pero en backend lo fijamos
|
||||
public Long numero_actual;
|
||||
}
|
||||
|
||||
private void validate(SerieFacturaPayload p) {
|
||||
if (p == null)
|
||||
throw new IllegalArgumentException("Body requerido.");
|
||||
if (p.nombre_serie == null || p.nombre_serie.trim().isBlank()) {
|
||||
throw new IllegalArgumentException("nombre_serie es obligatorio.");
|
||||
}
|
||||
if (p.prefijo == null || p.prefijo.trim().isBlank()) {
|
||||
throw new IllegalArgumentException("prefijo es obligatorio.");
|
||||
}
|
||||
if (p.prefijo.trim().length() > 10) {
|
||||
throw new IllegalArgumentException("prefijo máximo 10 caracteres.");
|
||||
}
|
||||
if (p.numero_actual == null || p.numero_actual < 1) {
|
||||
throw new IllegalArgumentException("numero_actual debe ser >= 1.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import com.imprimelibros.erp.facturacion.FacturaDireccion;
|
||||
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||
|
||||
public class DireccionFacturacionDto {
|
||||
private String razonSocial;
|
||||
private String identificacionFiscal;
|
||||
private String direccion;
|
||||
private String cp;
|
||||
private String ciudad;
|
||||
private String provincia;
|
||||
private String paisKeyword;
|
||||
private String telefono;
|
||||
|
||||
public String getRazonSocial() {
|
||||
return razonSocial;
|
||||
}
|
||||
|
||||
public void setRazonSocial(String razonSocial) {
|
||||
this.razonSocial = razonSocial;
|
||||
}
|
||||
|
||||
public String getIdentificacionFiscal() {
|
||||
return identificacionFiscal;
|
||||
}
|
||||
|
||||
public void setIdentificacionFiscal(String identificacionFiscal) {
|
||||
this.identificacionFiscal = identificacionFiscal;
|
||||
}
|
||||
|
||||
public String getDireccion() {
|
||||
return direccion;
|
||||
}
|
||||
|
||||
public void setDireccion(String direccion) {
|
||||
this.direccion = direccion;
|
||||
}
|
||||
|
||||
public String getCp() {
|
||||
return cp;
|
||||
}
|
||||
|
||||
public void setCp(String cp) {
|
||||
this.cp = cp;
|
||||
}
|
||||
|
||||
public String getCiudad() {
|
||||
return ciudad;
|
||||
}
|
||||
|
||||
public void setCiudad(String ciudad) {
|
||||
this.ciudad = ciudad;
|
||||
}
|
||||
|
||||
public String getProvincia() {
|
||||
return provincia;
|
||||
}
|
||||
|
||||
public void setProvincia(String provincia) {
|
||||
this.provincia = provincia;
|
||||
}
|
||||
|
||||
public String getPaisKeyword() {
|
||||
return paisKeyword;
|
||||
}
|
||||
|
||||
public void setPaisKeyword(String paisKeyword) {
|
||||
this.paisKeyword = paisKeyword;
|
||||
}
|
||||
|
||||
public String getTelefono() {
|
||||
return telefono;
|
||||
}
|
||||
|
||||
public void setTelefono(String telefono) {
|
||||
this.telefono = telefono;
|
||||
}
|
||||
|
||||
|
||||
public FacturaDireccion toFacturaDireccion() {
|
||||
FacturaDireccion fd = new FacturaDireccion();
|
||||
applyTo(fd);
|
||||
return fd;
|
||||
}
|
||||
|
||||
public PedidoDireccion toPedidoDireccion() {
|
||||
PedidoDireccion pd = new PedidoDireccion();
|
||||
applyTo(pd);
|
||||
pd.setFacturacion(true);
|
||||
return pd;
|
||||
}
|
||||
|
||||
public void applyTo(PedidoDireccion pd) {
|
||||
pd.setAtt("");
|
||||
pd.setRazonSocial(this.razonSocial);
|
||||
pd.setIdentificacionFiscal(this.identificacionFiscal);
|
||||
pd.setDireccion(this.direccion);
|
||||
|
||||
// CP robusto
|
||||
Integer cpInt = null;
|
||||
if (this.cp != null && !this.cp.isBlank()) {
|
||||
try {
|
||||
cpInt = Integer.valueOf(this.cp.trim());
|
||||
} catch (NumberFormatException ignored) {
|
||||
// si quieres, lanza IllegalArgumentException para validarlo
|
||||
}
|
||||
}
|
||||
pd.setCp(cpInt);
|
||||
|
||||
pd.setCiudad(this.ciudad);
|
||||
pd.setProvincia(this.provincia);
|
||||
|
||||
pd.setPaisCode3(this.paisKeyword);
|
||||
|
||||
pd.setTelefono(this.telefono);
|
||||
}
|
||||
|
||||
public void applyTo(FacturaDireccion fd ) {
|
||||
fd.setAtt("");
|
||||
fd.setRazonSocial(this.razonSocial);
|
||||
fd.setIdentificacionFiscal(this.identificacionFiscal);
|
||||
fd.setDireccion(this.direccion);
|
||||
|
||||
// CP robusto
|
||||
Integer cpInt = null;
|
||||
if (this.cp != null && !this.cp.isBlank()) {
|
||||
try {
|
||||
cpInt = Integer.valueOf(this.cp.trim());
|
||||
} catch (NumberFormatException ignored) {
|
||||
// si quieres, lanza IllegalArgumentException para validarlo
|
||||
}
|
||||
}
|
||||
fd.setCp(cpInt);
|
||||
|
||||
fd.setCiudad(this.ciudad);
|
||||
fd.setProvincia(this.provincia);
|
||||
|
||||
fd.setPaisCode3(this.paisKeyword);
|
||||
|
||||
fd.setTelefono(this.telefono);
|
||||
fd.setCreatedAt(Instant.now());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
public class FacturaAddRequestDto {
|
||||
|
||||
private Long user;
|
||||
private Long serie;
|
||||
private Long direccion;
|
||||
private Long factura_rectificada;
|
||||
|
||||
// getters y setters
|
||||
public Long getUser() {
|
||||
return user;
|
||||
}
|
||||
public void setUser(Long user) {
|
||||
this.user = user;
|
||||
}
|
||||
public Long getSerie() {
|
||||
return serie;
|
||||
}
|
||||
public void setSerie(Long serie) {
|
||||
this.serie = serie;
|
||||
}
|
||||
public Long getDireccion() {
|
||||
return direccion;
|
||||
}
|
||||
public void setDireccion(Long direccion) {
|
||||
this.direccion = direccion;
|
||||
}
|
||||
public Long getFactura_rectificada() {
|
||||
return factura_rectificada;
|
||||
}
|
||||
public void setFactura_rectificada(Long factura_rectificada) {
|
||||
this.factura_rectificada = factura_rectificada;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class FacturaCabeceraDto {
|
||||
private Long serieId;
|
||||
private Long clienteId;
|
||||
private LocalDateTime fechaEmision;
|
||||
|
||||
public Long getSerieId() {
|
||||
return serieId;
|
||||
}
|
||||
|
||||
public void setSerieId(Long serieId) {
|
||||
this.serieId = serieId;
|
||||
}
|
||||
|
||||
public Long getClienteId() {
|
||||
return clienteId;
|
||||
}
|
||||
|
||||
public void setClienteId(Long clienteId) {
|
||||
this.clienteId = clienteId;
|
||||
}
|
||||
|
||||
public LocalDateTime getFechaEmision() {
|
||||
return fechaEmision;
|
||||
}
|
||||
|
||||
public void setFechaEmision(LocalDateTime fechaEmision) {
|
||||
this.fechaEmision = fechaEmision;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||
import com.imprimelibros.erp.facturacion.FacturaDireccion;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import com.imprimelibros.erp.direcciones.Direccion.TipoIdentificacionFiscal;
|
||||
|
||||
public final class FacturaDireccionMapper {
|
||||
|
||||
private FacturaDireccionMapper() {}
|
||||
|
||||
public static FacturaDireccion fromPedidoDireccion(PedidoDireccion src) {
|
||||
if (src == null) return null;
|
||||
|
||||
FacturaDireccion dst = new FacturaDireccion();
|
||||
|
||||
dst.setUnidades(src.getUnidades());
|
||||
dst.setEmail(src.getEmail());
|
||||
dst.setAtt(src.getAtt());
|
||||
dst.setDireccion(src.getDireccion());
|
||||
dst.setCp(src.getCp());
|
||||
dst.setCiudad(src.getCiudad());
|
||||
dst.setProvincia(src.getProvincia());
|
||||
dst.setPaisCode3(src.getPaisCode3());
|
||||
dst.setTelefono(src.getTelefono());
|
||||
dst.setInstrucciones(src.getInstrucciones());
|
||||
dst.setRazonSocial(src.getRazonSocial());
|
||||
dst.setCreatedAt(Instant.now());
|
||||
|
||||
// OJO: en PedidoDireccion usas Direccion.TipoIdentificacionFiscal
|
||||
// En FacturaDireccion usa el enum que hayas definido/importado.
|
||||
dst.setTipoIdentificacionFiscal(
|
||||
TipoIdentificacionFiscal.valueOf(src.getTipoIdentificacionFiscal().name())
|
||||
);
|
||||
|
||||
dst.setIdentificacionFiscal(src.getIdentificacionFiscal());
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
public static FacturaDireccion fromDireccion(com.imprimelibros.erp.direcciones.Direccion src) {
|
||||
if (src == null) return null;
|
||||
|
||||
FacturaDireccion dst = new FacturaDireccion();
|
||||
|
||||
dst.setUnidades(null);
|
||||
dst.setEmail(src.getUser().getUserName());
|
||||
dst.setAtt(src.getAtt());
|
||||
dst.setDireccion(src.getDireccion());
|
||||
dst.setCp(src.getCp());
|
||||
dst.setCiudad(src.getCiudad());
|
||||
dst.setProvincia(src.getProvincia());
|
||||
dst.setPaisCode3(src.getPais().getCode3());
|
||||
dst.setTelefono(src.getTelefono());
|
||||
dst.setInstrucciones(src.getInstrucciones());
|
||||
dst.setRazonSocial(src.getRazonSocial());
|
||||
dst.setCreatedAt(Instant.now());
|
||||
|
||||
dst.setTipoIdentificacionFiscal(src.getTipoIdentificacionFiscal());
|
||||
|
||||
dst.setIdentificacionFiscal(src.getIdentificacionFiscal());
|
||||
|
||||
return dst;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
public class FacturaGuardarDto {
|
||||
@Valid private FacturaCabeceraDto cabecera;
|
||||
@Valid private DireccionFacturacionDto direccionFacturacion;
|
||||
|
||||
// getters/setters
|
||||
public FacturaCabeceraDto getCabecera() {
|
||||
return cabecera;
|
||||
}
|
||||
public void setCabecera(FacturaCabeceraDto cabecera) {
|
||||
this.cabecera = cabecera;
|
||||
}
|
||||
public DireccionFacturacionDto getDireccionFacturacion() {
|
||||
return direccionFacturacion;
|
||||
}
|
||||
public void setDireccionFacturacion(DireccionFacturacionDto direccionFacturacion) {
|
||||
this.direccionFacturacion = direccionFacturacion;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class FacturaLineaUpsertDto {
|
||||
|
||||
// Para update puedes mandarlo, pero realmente lo sacamos del path
|
||||
private Long id;
|
||||
|
||||
@NotNull
|
||||
private String descripcion; // HTML
|
||||
|
||||
@NotNull
|
||||
private BigDecimal base;
|
||||
|
||||
private BigDecimal iva4;
|
||||
private BigDecimal iva21;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getDescripcion() { return descripcion; }
|
||||
public void setDescripcion(String descripcion) { this.descripcion = descripcion; }
|
||||
|
||||
public BigDecimal getBase() { return base; }
|
||||
public void setBase(BigDecimal base) { this.base = base; }
|
||||
|
||||
public BigDecimal getIva4() { return iva4; }
|
||||
public void setIva4(BigDecimal iva4) { this.iva4 = iva4; }
|
||||
|
||||
public BigDecimal getIva21() { return iva21; }
|
||||
public void setIva21(BigDecimal iva21) { this.iva21 = iva21; }
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
import com.imprimelibros.erp.facturacion.TipoPago;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class FacturaPagoUpsertDto {
|
||||
|
||||
private Long id; // null => nuevo pago
|
||||
|
||||
@NotNull
|
||||
private TipoPago metodoPago;
|
||||
|
||||
@NotNull
|
||||
private BigDecimal cantidadPagada;
|
||||
|
||||
private LocalDateTime fechaPago;
|
||||
private String notas;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public TipoPago getMetodoPago() { return metodoPago; }
|
||||
public void setMetodoPago(TipoPago metodoPago) { this.metodoPago = metodoPago; }
|
||||
|
||||
public BigDecimal getCantidadPagada() { return cantidadPagada; }
|
||||
public void setCantidadPagada(BigDecimal cantidadPagada) { this.cantidadPagada = cantidadPagada; }
|
||||
|
||||
public LocalDateTime getFechaPago() { return fechaPago; }
|
||||
public void setFechaPago(LocalDateTime fechaPago) { this.fechaPago = fechaPago; }
|
||||
|
||||
public String getNotas() { return notas; }
|
||||
public void setNotas(String notas) { this.notas = notas; }
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class SerieFacturaForm {
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 100)
|
||||
private String nombreSerie;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 10)
|
||||
private String prefijo;
|
||||
|
||||
@NotNull
|
||||
private Long numeroActual;
|
||||
|
||||
public String getNombreSerie() { return nombreSerie; }
|
||||
public void setNombreSerie(String nombreSerie) { this.nombreSerie = nombreSerie; }
|
||||
|
||||
public String getPrefijo() { return prefijo; }
|
||||
public void setPrefijo(String prefijo) { this.prefijo = prefijo; }
|
||||
|
||||
public Long getNumeroActual() { return numeroActual; }
|
||||
public void setNumeroActual(Long numeroActual) { this.numeroActual = numeroActual; }
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.imprimelibros.erp.facturacion.repo;
|
||||
|
||||
import com.imprimelibros.erp.facturacion.FacturaLinea;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FacturaLineaRepository extends JpaRepository<FacturaLinea, Long> {
|
||||
List<FacturaLinea> findByFacturaId(Long facturaId);
|
||||
Optional<FacturaLinea> findByIdAndFacturaId(Long id, Long facturaId);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.imprimelibros.erp.facturacion.repo;
|
||||
|
||||
import com.imprimelibros.erp.facturacion.FacturaPago;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FacturaPagoRepository extends JpaRepository<FacturaPago, Long> {
|
||||
List<FacturaPago> findByFacturaIdAndDeletedAtIsNullOrderByFechaPagoDescIdDesc(Long facturaId);
|
||||
Optional<FacturaPago> findByIdAndFacturaIdAndDeletedAtIsNull(Long id, Long facturaId);
|
||||
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
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 org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FacturaRepository extends JpaRepository<Factura, Long>, JpaSpecificationExecutor<Factura> {
|
||||
Optional<Factura> findByNumeroFactura(String numeroFactura);
|
||||
Factura findByPedidoId(Long pedidoId);
|
||||
List<Factura> findByClienteIdAndEstadoAndEstadoPagoAndSerieId(
|
||||
Long clienteId,
|
||||
EstadoFactura estado,
|
||||
EstadoPagoFactura estadoPago,
|
||||
Long serieId);
|
||||
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.imprimelibros.erp.facturacion.repo;
|
||||
|
||||
import com.imprimelibros.erp.facturacion.SerieFactura;
|
||||
import com.imprimelibros.erp.facturacion.TipoSerieFactura;
|
||||
import org.springframework.data.jpa.repository.*;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import jakarta.persistence.LockModeType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface SerieFacturaRepository
|
||||
extends JpaRepository<SerieFactura, Long>, JpaSpecificationExecutor<SerieFactura> {
|
||||
|
||||
Optional<SerieFactura> findByTipo(TipoSerieFactura tipo);
|
||||
|
||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
||||
@Query("select s from SerieFactura s where s.id = :id")
|
||||
Optional<SerieFactura> findByIdForUpdate(@Param("id") Long id);
|
||||
|
||||
List<SerieFactura> findAllByDeletedAtIsNullOrderByNombreSerieAsc();
|
||||
|
||||
@Query("""
|
||||
select s
|
||||
from SerieFactura s
|
||||
where s.deletedAt is null
|
||||
and (:query is null or :query = '' or lower(s.nombreSerie) like lower(concat('%', :query, '%')))
|
||||
order by s.nombreSerie
|
||||
""")
|
||||
List<SerieFactura> searchForSelectSeriesFacturacion(@Param("query") String query);
|
||||
}
|
||||
@ -0,0 +1,725 @@
|
||||
package com.imprimelibros.erp.facturacion.service;
|
||||
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.configurationERP.VariableService;
|
||||
import com.imprimelibros.erp.facturacion.*;
|
||||
import com.imprimelibros.erp.facturacion.dto.DireccionFacturacionDto;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaDireccionMapper;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaGuardarDto;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaLineaUpsertDto;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaPagoUpsertDto;
|
||||
import com.imprimelibros.erp.facturacion.repo.FacturaDireccionRepository;
|
||||
import com.imprimelibros.erp.facturacion.repo.FacturaLineaRepository;
|
||||
import com.imprimelibros.erp.facturacion.repo.FacturaPagoRepository;
|
||||
import com.imprimelibros.erp.facturacion.repo.FacturaRepository;
|
||||
import com.imprimelibros.erp.facturacion.repo.SerieFacturaRepository;
|
||||
import com.imprimelibros.erp.pedidos.Pedido;
|
||||
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||
import com.imprimelibros.erp.pedidos.PedidoLinea;
|
||||
import com.imprimelibros.erp.pedidos.PedidoLineaRepository;
|
||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
import com.imprimelibros.erp.users.User;
|
||||
import com.imprimelibros.erp.users.UserService;
|
||||
import com.imprimelibros.erp.direcciones.Direccion;
|
||||
import com.imprimelibros.erp.direcciones.DireccionRepository;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Locale;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.security.Principal;
|
||||
import java.text.Collator;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class FacturacionService {
|
||||
|
||||
private final FacturaRepository facturaRepo;
|
||||
private final SerieFacturaRepository serieRepo;
|
||||
private final FacturaPagoRepository pagoRepo;
|
||||
private final FacturaLineaRepository lineaFacturaRepository;
|
||||
private final DireccionRepository direccionRepo;
|
||||
private final PedidoLineaRepository pedidoLineaRepo;
|
||||
private final UserService userService;
|
||||
private final Utils utils;
|
||||
private final MessageSource messageSource;
|
||||
private final PedidoService pedidoService;
|
||||
private final VariableService variableService;
|
||||
|
||||
public FacturacionService(
|
||||
FacturaRepository facturaRepo,
|
||||
FacturaLineaRepository lineaFacturaRepository,
|
||||
SerieFacturaRepository serieRepo,
|
||||
FacturaPagoRepository pagoRepo,
|
||||
DireccionRepository direccionRepo,
|
||||
PedidoLineaRepository pedidoLineaRepo,
|
||||
UserService userService,
|
||||
Utils utils,
|
||||
MessageSource messageSource,
|
||||
PedidoService pedidoService,
|
||||
VariableService variableService) {
|
||||
this.facturaRepo = facturaRepo;
|
||||
this.lineaFacturaRepository = lineaFacturaRepository;
|
||||
this.serieRepo = serieRepo;
|
||||
this.pagoRepo = pagoRepo;
|
||||
this.direccionRepo = direccionRepo;
|
||||
this.pedidoLineaRepo = pedidoLineaRepo;
|
||||
this.userService = userService;
|
||||
this.utils = utils;
|
||||
this.messageSource = messageSource;
|
||||
this.pedidoService = pedidoService;
|
||||
this.variableService = variableService;
|
||||
}
|
||||
|
||||
public SerieFactura getDefaultSerieFactura() {
|
||||
|
||||
Long defaultSerieId = variableService.getValorEntero("serie_facturacion_default").longValue();
|
||||
SerieFactura serie = serieRepo.findById(defaultSerieId).orElse(null);
|
||||
if (serie == null) {
|
||||
throw new IllegalStateException("No hay ninguna serie de facturación configurada.");
|
||||
}
|
||||
return serie;
|
||||
}
|
||||
|
||||
public Factura getFactura(Long facturaId) {
|
||||
return facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
}
|
||||
|
||||
public Long getFacturaIdFromPedidoId(Long pedidoId) {
|
||||
Factura factura = facturaRepo.findByPedidoId(pedidoId);
|
||||
if (factura == null) {
|
||||
throw new EntityNotFoundException("Factura no encontrada para el pedido: " + pedidoId);
|
||||
}
|
||||
return factura.getId();
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Nueva factura
|
||||
// -----------------------
|
||||
@Transactional
|
||||
public Factura crearNuevaFacturaAuto(Pedido pedido, SerieFactura serie, TipoPago tipoPago, Locale locale) {
|
||||
|
||||
Factura factura = new Factura();
|
||||
factura.setCliente(pedido.getCreatedBy());
|
||||
factura.setCreatedAt(Instant.now());
|
||||
factura.setUpdatedAt(Instant.now());
|
||||
Boolean pedidoPendientePago = false;
|
||||
List<PedidoLinea> lineasPedido = pedidoLineaRepo.findByPedidoId(pedido.getId());
|
||||
for (PedidoLinea lineaPedido : lineasPedido) {
|
||||
if (lineaPedido.getEstado() == PedidoLinea.Estado.pendiente_pago) {
|
||||
pedidoPendientePago = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
factura.setEstado(pedidoPendientePago ? EstadoFactura.borrador : EstadoFactura.validada);
|
||||
factura.setEstadoPago(pedidoPendientePago ? EstadoPagoFactura.pendiente : EstadoPagoFactura.pagada);
|
||||
factura.setTipoPago(pedidoPendientePago ? TipoPago.otros : tipoPago);
|
||||
factura.setPedidoId(pedido.getId());
|
||||
factura.setSerie(serie);
|
||||
factura.setNumeroFactura(this.getNumberFactura(serie));
|
||||
factura.setFechaEmision(LocalDateTime.now());
|
||||
factura.setBaseImponible(BigDecimal.valueOf(pedido.getBase()).setScale(2, RoundingMode.HALF_UP));
|
||||
factura.setIva4(BigDecimal.valueOf(pedido.getIva4()).setScale(2, RoundingMode.HALF_UP));
|
||||
factura.setIva21(BigDecimal.valueOf(pedido.getIva21()).setScale(2, RoundingMode.HALF_UP));
|
||||
factura.setTotalFactura(BigDecimal.valueOf(pedido.getTotal()).setScale(2, RoundingMode.HALF_UP));
|
||||
factura.setTotalPagado(BigDecimal.valueOf(pedido.getTotal()).setScale(2, RoundingMode.HALF_UP));
|
||||
// rellenar lineas
|
||||
List<FacturaLinea> lineasFactura = new ArrayList<>();
|
||||
for (PedidoLinea lineaPedido : lineasPedido) {
|
||||
Presupuesto p = lineaPedido.getPresupuesto();
|
||||
FacturaLinea lineaFactura = new FacturaLinea();
|
||||
lineaFactura.setDescripcion(this.obtenerLineaFactura(lineaPedido, locale));
|
||||
lineaFactura.setCantidad(p.getSelectedTirada());
|
||||
lineaFactura.setBaseLinea(p.getBaseImponible());
|
||||
lineaFactura.setIva4Linea(p.getIvaImporte4());
|
||||
lineaFactura.setIva21Linea(p.getIvaImporte21());
|
||||
lineaFactura.setTotalLinea(p.getTotalConIva());
|
||||
lineaFactura.setCreatedBy(p.getUser());
|
||||
lineaFactura.setFactura(factura);
|
||||
lineasFactura.add(lineaFactura);
|
||||
}
|
||||
if(pedido.getEnvio() > 0){
|
||||
FacturaLinea lineaEnvio = new FacturaLinea();
|
||||
lineaEnvio.setDescripcion(messageSource.getMessage("facturas.lineas.gastos-envio", null, "Gastos de envío", locale));
|
||||
lineaEnvio.setCantidad(1);
|
||||
BigDecimal baseEnvio = BigDecimal.valueOf(pedido.getEnvio()).setScale(2, RoundingMode.HALF_UP);
|
||||
lineaEnvio.setBaseLinea(baseEnvio);
|
||||
BigDecimal iva21Envio = baseEnvio.multiply(BigDecimal.valueOf(0.21)).setScale(2, RoundingMode.HALF_UP);
|
||||
lineaEnvio.setIva21Linea(iva21Envio);
|
||||
lineaEnvio.setIva4Linea(BigDecimal.ZERO);
|
||||
lineaEnvio.setTotalLinea(baseEnvio.add(iva21Envio));
|
||||
lineaEnvio.setCreatedBy(pedido.getCreatedBy());
|
||||
lineaEnvio.setCreatedAt(Instant.now());
|
||||
lineaEnvio.setFactura(factura);
|
||||
lineasFactura.add(lineaEnvio);
|
||||
}
|
||||
PedidoDireccion direccionPedido = pedidoService.getDireccionFacturacionPedido(pedido.getId());
|
||||
if(direccionPedido == null){
|
||||
throw new IllegalStateException("El pedido no tiene una dirección de facturación asociada.");
|
||||
}
|
||||
FacturaDireccion fd = FacturaDireccionMapper.fromPedidoDireccion(direccionPedido);
|
||||
|
||||
factura.addDireccion(fd);
|
||||
factura.setLineas(lineasFactura);
|
||||
|
||||
factura = facturaRepo.save(factura);
|
||||
|
||||
if (pedidoPendientePago) {
|
||||
return factura;
|
||||
}
|
||||
FacturaPago pago = new FacturaPago();
|
||||
pago.setMetodoPago(tipoPago);
|
||||
pago.setCantidadPagada(factura.getTotalFactura());
|
||||
pago.setFechaPago(LocalDateTime.now());
|
||||
pago.setFactura(factura);
|
||||
pago.setCreatedBy(pedido.getCreatedBy());
|
||||
pago.setCreatedAt(Instant.now());
|
||||
pagoRepo.save(pago);
|
||||
|
||||
return factura;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Factura crearNuevaFactura(Long userId, Long serieId, Long direccionId, Long facturaRectificadaId) {
|
||||
User cliente = userService.findById(userId);
|
||||
if (cliente == null) {
|
||||
throw new EntityNotFoundException("Cliente no encontrado: " + userId);
|
||||
}
|
||||
|
||||
SerieFactura serie = serieRepo.findById(serieId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + serieId));
|
||||
|
||||
Factura factura = new Factura();
|
||||
factura.setCliente(cliente);
|
||||
factura.setPedidoId(null);
|
||||
factura.setSerie(serie);
|
||||
factura.setEstado(EstadoFactura.borrador);
|
||||
factura.setEstadoPago(EstadoPagoFactura.pendiente);
|
||||
factura.setFechaEmision(LocalDateTime.now());
|
||||
factura.setCreatedAt(Instant.now());
|
||||
factura.setUpdatedAt(Instant.now());
|
||||
factura.setNumeroFactura(null);
|
||||
factura.setBaseImponible(BigDecimal.ZERO);
|
||||
factura.setIva4(BigDecimal.ZERO);
|
||||
factura.setIva21(BigDecimal.ZERO);
|
||||
factura.setTotalFactura(BigDecimal.ZERO);
|
||||
factura.setTotalPagado(BigDecimal.ZERO);
|
||||
factura.setLineas(new ArrayList<>());
|
||||
factura.setPagos(new ArrayList<>());
|
||||
Direccion direccion = direccionRepo.findById(direccionId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Dirección de factura no encontrada: " + direccionId));
|
||||
FacturaDireccion facturaDireccion = FacturaDireccionMapper.fromDireccion(direccion);
|
||||
factura.addDireccion(facturaDireccion);
|
||||
if(facturaRectificadaId != null){
|
||||
Factura facturaRectificada = facturaRepo.findById(facturaRectificadaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura rectificada no encontrada: " + facturaRectificadaId));
|
||||
factura.setFacturaRectificativa(facturaRectificada);
|
||||
facturaRectificada.setFacturaRectificada(factura);
|
||||
}
|
||||
return facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Estado / Numeración
|
||||
// -----------------------
|
||||
|
||||
@Transactional
|
||||
public String getNumberFactura(SerieFactura serie) {
|
||||
|
||||
try {
|
||||
long next = (serie.getNumeroActual() == null) ? 1L : serie.getNumeroActual();
|
||||
String numeroFactura = buildNumeroFactura(serie.getPrefijo(), next);
|
||||
|
||||
// Incrementar contador para la siguiente
|
||||
serie.setNumeroActual(next + 1);
|
||||
serieRepo.save(serie);
|
||||
|
||||
return numeroFactura;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void guardarCabeceraYDireccionFacturacion(Long facturaId, FacturaGuardarDto dto) {
|
||||
Factura factura = getFactura(facturaId);
|
||||
|
||||
// ✅ Solo editable si borrador (tu regla actual para cabecera/dirección)
|
||||
if (factura.getEstado() != EstadoFactura.borrador) {
|
||||
throw new IllegalStateException("Solo se puede guardar cabecera/dirección en borrador.");
|
||||
}
|
||||
|
||||
// 1) Cabecera
|
||||
if (dto.getCabecera() != null) {
|
||||
var c = dto.getCabecera();
|
||||
|
||||
if (c.getSerieId() != null) {
|
||||
SerieFactura serie = serieRepo.findById(c.getSerieId())
|
||||
.orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + c.getSerieId()));
|
||||
factura.setSerie(serie);
|
||||
}
|
||||
|
||||
if (c.getClienteId() != null) {
|
||||
User cliente = userService.findById(c.getClienteId());
|
||||
if(cliente == null){
|
||||
throw new EntityNotFoundException("Cliente no encontrado: " + c.getClienteId());
|
||||
}
|
||||
factura.setCliente(cliente);
|
||||
}
|
||||
|
||||
if (c.getFechaEmision() != null) {
|
||||
factura.setFechaEmision(c.getFechaEmision());
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Dirección de facturación del pedido asociado
|
||||
Long pedidoId = factura.getPedidoId();
|
||||
if (pedidoId != null && dto.getDireccionFacturacion() != null) {
|
||||
pedidoService.upsertDireccionFacturacion(pedidoId, dto.getDireccionFacturacion());
|
||||
|
||||
}
|
||||
upsertDireccionFacturacion(facturaId, dto.getDireccionFacturacion());
|
||||
|
||||
facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Factura validarFactura(Long facturaId) {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
|
||||
// Puedes permitir validar desde borrador solamente (lo normal)
|
||||
if (factura.getEstado() == EstadoFactura.validada) {
|
||||
return factura;
|
||||
}
|
||||
|
||||
if (factura.getFechaEmision() == null) {
|
||||
factura.setFechaEmision(LocalDateTime.now());
|
||||
}
|
||||
|
||||
if (factura.getSerie() == null) {
|
||||
throw new IllegalStateException("La factura no tiene serie asignada.");
|
||||
}
|
||||
|
||||
// Si ya tiene numero_factura, no reservamos otro
|
||||
if (factura.getNumeroFactura() == null || factura.getNumeroFactura().isBlank()) {
|
||||
SerieFactura serieLocked = serieRepo.findByIdForUpdate(factura.getSerie().getId())
|
||||
.orElseThrow(
|
||||
() -> new EntityNotFoundException("Serie no encontrada: " + factura.getSerie().getId()));
|
||||
|
||||
long next = (serieLocked.getNumeroActual() == null) ? 1L : serieLocked.getNumeroActual();
|
||||
String numeroFactura = buildNumeroFactura(serieLocked.getPrefijo(), next);
|
||||
|
||||
factura.setNumeroFactura(numeroFactura);
|
||||
|
||||
// Incrementar contador para la siguiente
|
||||
serieLocked.setNumeroActual(next + 1);
|
||||
serieRepo.save(serieLocked);
|
||||
}
|
||||
|
||||
recalcularTotales(factura);
|
||||
factura.setEstado(EstadoFactura.validada);
|
||||
|
||||
return facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Factura volverABorrador(Long facturaId) {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
|
||||
factura.setEstado(EstadoFactura.borrador);
|
||||
// No tocamos numero_factura (se conserva) -> evita duplicados y auditoría rara
|
||||
|
||||
recalcularTotales(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) {
|
||||
String pref = (prefijo == null) ? "" : prefijo.trim();
|
||||
String num = String.format("%05d", numero);
|
||||
return pref.isBlank() ? num : (pref + " " + num + "/" + LocalDate.now().getYear());
|
||||
}
|
||||
|
||||
|
||||
// -----------------------
|
||||
// Líneas
|
||||
// -----------------------
|
||||
@Transactional
|
||||
public void createLinea(Long facturaId, FacturaLineaUpsertDto req) {
|
||||
Factura factura = this.getFactura(facturaId);
|
||||
|
||||
FacturaLinea lf = new FacturaLinea();
|
||||
lf.setFactura(factura);
|
||||
lf.setCantidad(1);
|
||||
|
||||
applyRequest(lf, req);
|
||||
|
||||
lineaFacturaRepository.save(lf);
|
||||
|
||||
this.recalcularTotales(factura);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Factura upsertLinea(Long facturaId, FacturaLineaUpsertDto dto) {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
|
||||
if (factura.getEstado() != EstadoFactura.borrador) {
|
||||
throw new IllegalStateException("Solo se pueden editar líneas en facturas en borrador.");
|
||||
}
|
||||
|
||||
FacturaLinea linea;
|
||||
if (dto.getId() == null) {
|
||||
linea = new FacturaLinea();
|
||||
linea.setFactura(factura);
|
||||
factura.getLineas().add(linea);
|
||||
} else {
|
||||
linea = factura.getLineas().stream()
|
||||
.filter(l -> dto.getId().equals(l.getId()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new EntityNotFoundException("Línea no encontrada: " + dto.getId()));
|
||||
}
|
||||
|
||||
linea.setDescripcion(dto.getDescripcion());
|
||||
|
||||
linea.setBaseLinea(scale2(dto.getBase()));
|
||||
|
||||
linea.setIva4Linea(dto.getIva4());
|
||||
linea.setIva21Linea(dto.getIva21());
|
||||
|
||||
linea.setTotalLinea(scale2(linea.getBaseLinea()
|
||||
.add(nvl(linea.getIva4Linea()))
|
||||
.add(nvl(linea.getIva21Linea()))));
|
||||
|
||||
recalcularTotales(factura);
|
||||
return facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Factura borrarLinea(Long facturaId, Long lineaId) {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
|
||||
if (factura.getEstado() != EstadoFactura.borrador) {
|
||||
throw new IllegalStateException("Solo se pueden borrar líneas en facturas en borrador.");
|
||||
}
|
||||
|
||||
boolean removed = factura.getLineas().removeIf(l -> lineaId.equals(l.getId()));
|
||||
if (!removed) {
|
||||
throw new EntityNotFoundException("Línea no encontrada: " + lineaId);
|
||||
}
|
||||
|
||||
recalcularTotales(factura);
|
||||
return facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Pagos
|
||||
// -----------------------
|
||||
|
||||
@Transactional
|
||||
public Factura upsertPago(Long facturaId, FacturaPagoUpsertDto dto, Principal principal) {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
|
||||
// Permitir añadir pagos tanto en borrador como validada (según tu regla)
|
||||
FacturaPago pago;
|
||||
if (dto.getId() == null) {
|
||||
pago = new FacturaPago();
|
||||
pago.setFactura(factura);
|
||||
pago.setCreatedBy(Utils.currentUser(principal));
|
||||
pago.setCreatedAt(Instant.now());
|
||||
factura.getPagos().add(pago);
|
||||
} else {
|
||||
pago = factura.getPagos().stream()
|
||||
.filter(p -> dto.getId().equals(p.getId()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new EntityNotFoundException("Pago no encontrado: " + dto.getId()));
|
||||
}
|
||||
|
||||
pago.setMetodoPago(dto.getMetodoPago());
|
||||
pago.setCantidadPagada(scale2(dto.getCantidadPagada()));
|
||||
pago.setFechaPago(dto.getFechaPago() != null ? dto.getFechaPago() : LocalDateTime.now());
|
||||
pago.setNotas(dto.getNotas());
|
||||
pago.setUpdatedAt(Instant.now());
|
||||
pago.setUpdatedBy(Utils.currentUser(principal));
|
||||
// El tipo_pago de la factura: si tiene un pago, lo reflejamos (último pago
|
||||
// manda)
|
||||
factura.setTipoPago(dto.getMetodoPago());
|
||||
|
||||
recalcularTotales(factura);
|
||||
return facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Factura borrarPago(Long facturaId, Long pagoId, Principal principal) {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
|
||||
FacturaPago pago = factura.getPagos().stream()
|
||||
.filter(p -> pagoId.equals(p.getId()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new EntityNotFoundException("Pago no encontrado: " + pagoId));
|
||||
|
||||
// soft delete
|
||||
pago.setDeletedAt(Instant.now());
|
||||
pago.setDeletedBy(Utils.currentUser(principal));
|
||||
|
||||
recalcularTotales(factura);
|
||||
return facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Recalcular totales
|
||||
// -----------------------
|
||||
|
||||
@Transactional
|
||||
public void recalcularTotales(Long facturaId) {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
recalcularTotales(factura);
|
||||
facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
private void recalcularTotales(Factura factura) {
|
||||
BigDecimal base = BigDecimal.ZERO;
|
||||
BigDecimal iva4 = BigDecimal.ZERO;
|
||||
BigDecimal iva21 = BigDecimal.ZERO;
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
|
||||
if (factura.getLineas() != null) {
|
||||
for (FacturaLinea l : factura.getLineas()) {
|
||||
base = base.add(nvl(l.getBaseLinea()));
|
||||
iva4 = iva4.add(nvl(l.getIva4Linea()));
|
||||
iva21 = iva21.add(nvl(l.getIva21Linea()));
|
||||
total = total.add(nvl(l.getTotalLinea()));
|
||||
}
|
||||
}
|
||||
|
||||
factura.setBaseImponible(scale2(base));
|
||||
factura.setIva4(scale2(iva4));
|
||||
factura.setIva21(scale2(iva21));
|
||||
factura.setTotalFactura(scale2(total));
|
||||
|
||||
// total_pagado
|
||||
BigDecimal pagado = BigDecimal.ZERO;
|
||||
if (factura.getPagos() != null) {
|
||||
for (FacturaPago p : factura.getPagos()) {
|
||||
if (p.getDeletedAt() != null)
|
||||
continue;
|
||||
pagado = pagado.add(nvl(p.getCantidadPagada()));
|
||||
}
|
||||
}
|
||||
factura.setTotalPagado(scale2(pagado));
|
||||
|
||||
// estado_pago
|
||||
// - cancelada: si la factura está marcada como cancelada manualmente (aquí NO
|
||||
// lo hacemos automático)
|
||||
// - pagada: si total_pagado >= total_factura y total_factura > 0
|
||||
// - pendiente: resto
|
||||
if (factura.getEstadoPago() == EstadoPagoFactura.cancelada) {
|
||||
return;
|
||||
}
|
||||
|
||||
BigDecimal totalFactura = nvl(factura.getTotalFactura());
|
||||
if (totalFactura.compareTo(BigDecimal.ZERO) > 0 &&
|
||||
factura.getTotalPagado().compareTo(totalFactura) >= 0) {
|
||||
factura.setEstadoPago(EstadoPagoFactura.pagada);
|
||||
} else {
|
||||
factura.setEstadoPago(EstadoPagoFactura.pendiente);
|
||||
}
|
||||
}
|
||||
|
||||
private static BigDecimal nvl(BigDecimal v) {
|
||||
return v == null ? BigDecimal.ZERO : v;
|
||||
}
|
||||
|
||||
private static BigDecimal scale2(BigDecimal v) {
|
||||
return (v == null ? BigDecimal.ZERO : v).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private String obtenerLineaFactura(PedidoLinea lineaPedido, Locale locale) {
|
||||
|
||||
Map<String, Object> specs = utils.getTextoPresupuesto(lineaPedido.getPresupuesto(), locale);
|
||||
|
||||
StringBuilder html = new StringBuilder();
|
||||
html.append("<div class=\"specs-wrapper align-with-text \">")
|
||||
.append("<div class=\"specs\">");
|
||||
|
||||
if (specs == null) {
|
||||
return "<div></div>";
|
||||
}
|
||||
|
||||
// 1) Líneas del presupuesto (HTML)
|
||||
Object lineasObj = specs.get("lineas");
|
||||
if (lineasObj instanceof List<?> lineasList) {
|
||||
for (Object o : lineasList) {
|
||||
if (!(o instanceof Map<?, ?> m))
|
||||
continue;
|
||||
|
||||
Object descObj = m.get("descripcion");
|
||||
String descripcionHtml = descObj != null ? descObj.toString() : "";
|
||||
if (descripcionHtml.isBlank())
|
||||
continue;
|
||||
|
||||
html.append("<div class=\"spec-row mb-1\">")
|
||||
.append("<span class=\"spec-label\">")
|
||||
.append(descripcionHtml) // OJO: esto es HTML (como th:utext)
|
||||
.append("</span>")
|
||||
.append("</div>");
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Servicios adicionales (texto)
|
||||
Object serviciosObj = specs.get("servicios");
|
||||
String servicios = (serviciosObj != null) ? serviciosObj.toString().trim() : "";
|
||||
if (!servicios.isBlank()) {
|
||||
String label = messageSource.getMessage("pdf.servicios-adicionales", null, "Servicios adicionales", locale);
|
||||
html.append("<div class=\"spec-row mb-1\">")
|
||||
.append("<span>").append(escapeHtml(label)).append("</span>")
|
||||
.append("<span class=\"spec-label\">").append(escapeHtml(servicios)).append("</span>")
|
||||
.append("</div>");
|
||||
}
|
||||
|
||||
// 3) Datos de maquetación (HTML)
|
||||
Object datosMaqObj = specs.get("datosMaquetacion");
|
||||
if (datosMaqObj != null && !datosMaqObj.toString().isBlank()) {
|
||||
String label = messageSource.getMessage("pdf.datos-maquetacion", null, "Datos de maquetación:", locale);
|
||||
html.append("<div class=\"spec-row mb-1\">")
|
||||
.append("<span>").append(escapeHtml(label)).append("</span>")
|
||||
.append("<span class=\"spec-label\">")
|
||||
.append(datosMaqObj) // HTML (como th:utext)
|
||||
.append("</span>")
|
||||
.append("</div>");
|
||||
}
|
||||
|
||||
// 4) Datos de marcapáginas (HTML)
|
||||
Object datosMarcaObj = specs.get("datosMarcapaginas");
|
||||
if (datosMarcaObj != null && !datosMarcaObj.toString().isBlank()) {
|
||||
String label = messageSource.getMessage("pdf.datos-marcapaginas", null, "Datos de marcapáginas:", locale);
|
||||
html.append("<div class=\"spec-row mb-1\">")
|
||||
.append("<span>").append(escapeHtml(label)).append("</span>")
|
||||
.append("<span class=\"spec-label\">")
|
||||
.append(datosMarcaObj) // HTML (como th:utext)
|
||||
.append("</span>")
|
||||
.append("</div>");
|
||||
}
|
||||
|
||||
html.append("</div></div>");
|
||||
return html.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape mínimo para texto plano (equivalente a th:text).
|
||||
* No lo uses para fragmentos que ya son HTML (th:utext).
|
||||
*/
|
||||
private static String escapeHtml(String s) {
|
||||
if (s == null)
|
||||
return "";
|
||||
return s.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
}
|
||||
|
||||
private void applyRequest(FacturaLinea lf, FacturaLineaUpsertDto req) {
|
||||
// HTML
|
||||
lf.setDescripcion(req.getDescripcion() == null ? "" : req.getDescripcion());
|
||||
|
||||
BigDecimal base = nvl(req.getBase());
|
||||
BigDecimal iva4 = nvl(req.getIva4());
|
||||
BigDecimal iva21 = nvl(req.getIva21());
|
||||
|
||||
lf.setBaseLinea(base);
|
||||
lf.setIva4Linea(iva4);
|
||||
lf.setIva21Linea(iva21);
|
||||
|
||||
// total de línea (por ahora)
|
||||
lf.setTotalLinea(base.add(iva4).add(iva21));
|
||||
}
|
||||
|
||||
}
|
||||
@ -69,4 +69,18 @@ public class PaisesService {
|
||||
}
|
||||
}
|
||||
|
||||
public String getPaisNombrePorCode3(String code3, Locale locale) {
|
||||
if (code3 == null || code3.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
Optional<Paises> opt = repo.findByCode3(code3);
|
||||
if (opt.isPresent()) {
|
||||
Paises pais = opt.get();
|
||||
String key = pais.getKeyword();
|
||||
return messageSource.getMessage("paises." + key, null, key, locale);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -16,8 +16,6 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.datatables.DataTable;
|
||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||
@ -231,20 +229,9 @@ public class PaymentController {
|
||||
})
|
||||
.add("transfer_id", pago -> {
|
||||
if (pago.getPayment() != null) {
|
||||
String responsePayload = pago.getResponsePayload();
|
||||
Long cartId = null;
|
||||
if (responsePayload != null && !responsePayload.isBlank()) {
|
||||
try {
|
||||
JsonNode node = new ObjectMapper().readTree(responsePayload);
|
||||
if (node.has("cartId")) {
|
||||
cartId = node.get("cartId").asLong();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
cartId = null;
|
||||
}
|
||||
}
|
||||
if (cartId != null) {
|
||||
return "TRANSF-" + cartId;
|
||||
Long pedido = pago.getPayment().getOrderId();
|
||||
if (pedido != null) {
|
||||
return "TRANSF-" + pedido;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
|
||||
@ -3,6 +3,9 @@ package com.imprimelibros.erp.payments;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imprimelibros.erp.cart.Cart;
|
||||
import com.imprimelibros.erp.cart.CartService;
|
||||
import com.imprimelibros.erp.facturacion.SerieFactura;
|
||||
import com.imprimelibros.erp.facturacion.TipoPago;
|
||||
import com.imprimelibros.erp.facturacion.service.FacturacionService;
|
||||
import com.imprimelibros.erp.payments.model.*;
|
||||
import com.imprimelibros.erp.payments.repo.PaymentRepository;
|
||||
import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository;
|
||||
@ -13,9 +16,12 @@ import com.imprimelibros.erp.redsys.RedsysService.RedsysNotification;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.imprimelibros.erp.payments.repo.WebhookEventRepository;
|
||||
import com.imprimelibros.erp.pedidos.Pedido;
|
||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
@ -28,18 +34,64 @@ public class PaymentService {
|
||||
private final WebhookEventRepository webhookEventRepo;
|
||||
private final ObjectMapper om = new ObjectMapper();
|
||||
private final CartService cartService;
|
||||
private final PedidoService pedidoService;
|
||||
private final FacturacionService facturacionService;
|
||||
|
||||
public PaymentService(PaymentRepository payRepo,
|
||||
PaymentTransactionRepository txRepo,
|
||||
RefundRepository refundRepo,
|
||||
RedsysService redsysService,
|
||||
WebhookEventRepository webhookEventRepo, CartService cartService) {
|
||||
WebhookEventRepository webhookEventRepo,
|
||||
CartService cartService,
|
||||
PedidoService pedidoService,
|
||||
FacturacionService facturacionService) {
|
||||
this.payRepo = payRepo;
|
||||
this.txRepo = txRepo;
|
||||
this.refundRepo = refundRepo;
|
||||
this.redsysService = redsysService;
|
||||
this.webhookEventRepo = webhookEventRepo;
|
||||
this.cartService = cartService;
|
||||
this.pedidoService = pedidoService;
|
||||
this.facturacionService = facturacionService;
|
||||
}
|
||||
|
||||
public Payment findFailedPaymentByOrderId(Long orderId) {
|
||||
return payRepo.findFirstByOrderIdAndStatusOrderByIdDesc(orderId, PaymentStatus.failed)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public Map<String, Long> getPaymentTransactionData(Long paymentId) {
|
||||
PaymentTransaction tx = txRepo.findByPaymentIdAndType(
|
||||
paymentId,
|
||||
PaymentTransactionType.CAPTURE)
|
||||
.orElse(null);
|
||||
if (tx == null) {
|
||||
return null;
|
||||
}
|
||||
String resp_payload = tx.getResponsePayload();
|
||||
try {
|
||||
ObjectMapper om = new ObjectMapper();
|
||||
var node = om.readTree(resp_payload);
|
||||
Long cartId = null;
|
||||
Long dirFactId = null;
|
||||
if (node.has("Ds_MerchantData")) {
|
||||
// format: "Ds_MerchantData": "{"dirFactId":3,"cartId":90}"
|
||||
String merchantData = node.get("Ds_MerchantData").asText();
|
||||
merchantData = merchantData.replace(""", "\"");
|
||||
var mdNode = om.readTree(merchantData);
|
||||
if (mdNode.has("cartId")) {
|
||||
cartId = mdNode.get("cartId").asLong();
|
||||
}
|
||||
if (mdNode.has("dirFactId")) {
|
||||
dirFactId = mdNode.get("dirFactId").asLong();
|
||||
}
|
||||
}
|
||||
return Map.of(
|
||||
"cartId", cartId,
|
||||
"dirFactId", dirFactId);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,14 +99,15 @@ public class PaymentService {
|
||||
* oficial (ApiMacSha256).
|
||||
*/
|
||||
@Transactional
|
||||
public FormPayload createRedsysPayment(Long cartId, Long dirFactId, Long amountCents, String currency, String method)
|
||||
public FormPayload createRedsysPayment(Long cartId, Long dirFactId, Long amountCents, String currency, String method, Long orderId)
|
||||
throws Exception {
|
||||
Payment p = new Payment();
|
||||
p.setOrderId(null);
|
||||
p.setOrderId(orderId);
|
||||
|
||||
Cart cart = this.cartService.findById(cartId);
|
||||
if (cart != null && cart.getUserId() != null) {
|
||||
p.setUserId(cart.getUserId());
|
||||
this.cartService.lockCartById(cartId);
|
||||
}
|
||||
p.setCurrency(currency);
|
||||
p.setAmountTotalCents(amountCents);
|
||||
@ -62,10 +115,6 @@ public class PaymentService {
|
||||
p.setStatus(PaymentStatus.requires_payment_method);
|
||||
p = payRepo.saveAndFlush(p);
|
||||
|
||||
// ANTES:
|
||||
// String dsOrder = String.format("%012d", p.getId());
|
||||
|
||||
// AHORA: timestamp
|
||||
long now = System.currentTimeMillis();
|
||||
String dsOrder = String.format("%012d", now % 1_000_000_000_000L);
|
||||
|
||||
@ -207,16 +256,17 @@ public class PaymentService {
|
||||
p.setAmountCapturedCents(p.getAmountCapturedCents() + notif.amountCents);
|
||||
p.setAuthorizedAt(LocalDateTime.now());
|
||||
p.setCapturedAt(LocalDateTime.now());
|
||||
pedidoService.setOrderAsPaid(p.getOrderId());
|
||||
|
||||
Pedido pedido = pedidoService.getPedidoById(p.getOrderId());
|
||||
SerieFactura serie = facturacionService.getDefaultSerieFactura();
|
||||
|
||||
facturacionService.crearNuevaFacturaAuto(pedido, serie, notif.isBizum() ? TipoPago.tpv_bizum : TipoPago.tpv_tarjeta, locale);
|
||||
|
||||
} else {
|
||||
p.setStatus(PaymentStatus.failed);
|
||||
p.setFailedAt(LocalDateTime.now());
|
||||
}
|
||||
|
||||
if (authorized) {
|
||||
Long orderId = processOrder(notif.cartId, notif.dirFactId, locale);
|
||||
if (orderId != null) {
|
||||
p.setOrderId(orderId);
|
||||
}
|
||||
pedidoService.markPedidoAsPaymentDenied(p.getOrderId());
|
||||
}
|
||||
|
||||
payRepo.save(p);
|
||||
@ -311,7 +361,7 @@ public class PaymentService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Payment createBankTransferPayment(Long cartId, Long dirFactId, long amountCents, String currency) {
|
||||
public Payment createBankTransferPayment(Long cartId, Long dirFactId, long amountCents, String currency, Locale locale, Long orderId) {
|
||||
Payment p = new Payment();
|
||||
p.setOrderId(null);
|
||||
|
||||
@ -326,6 +376,9 @@ public class PaymentService {
|
||||
p.setAmountTotalCents(amountCents);
|
||||
p.setGateway("bank_transfer");
|
||||
p.setStatus(PaymentStatus.requires_action); // pendiente de ingreso
|
||||
if (orderId != null) {
|
||||
p.setOrderId(orderId);
|
||||
}
|
||||
p = payRepo.save(p);
|
||||
|
||||
// Crear transacción pendiente
|
||||
@ -406,13 +459,22 @@ public class PaymentService {
|
||||
// ignorar
|
||||
}
|
||||
|
||||
// 4) Procesar el pedido asociado al carrito (si existe)
|
||||
if (cartId != null) {
|
||||
Long orderId = processOrder(cartId, dirFactId, locale);
|
||||
// 4) Procesar el pedido asociado al carrito (si existe) o marcar el pedido como pagado
|
||||
if(p.getOrderId() != null) {
|
||||
pedidoService.setOrderAsPaid(p.getOrderId());
|
||||
|
||||
Pedido pedido = pedidoService.getPedidoById(p.getOrderId());
|
||||
SerieFactura serie = facturacionService.getDefaultSerieFactura();
|
||||
|
||||
facturacionService.crearNuevaFacturaAuto(pedido, serie, TipoPago.transferencia, locale);
|
||||
}
|
||||
/*else if (cartId != null) {
|
||||
// Se procesa el pedido dejando el estado calculado en processOrder
|
||||
Long orderId = processOrder(cartId, dirFactId, locale, null);
|
||||
if (orderId != null) {
|
||||
p.setOrderId(orderId);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
payRepo.save(p);
|
||||
}
|
||||
|
||||
@ -508,30 +570,5 @@ public class PaymentService {
|
||||
return code >= 0 && code <= 99;
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa el pedido asociado al carrito:
|
||||
* - bloquea el carrito
|
||||
* - crea el pedido a partir del carrito
|
||||
*
|
||||
*/
|
||||
@Transactional
|
||||
private Long processOrder(Long cartId, Long dirFactId, Locale locale) {
|
||||
|
||||
Cart cart = this.cartService.findById(cartId);
|
||||
if (cart != null) {
|
||||
// Bloqueamos el carrito
|
||||
this.cartService.lockCartById(cart.getId());
|
||||
// Creamos el pedido
|
||||
Long orderId = this.cartService.crearPedido(cart.getId(), dirFactId, locale);
|
||||
if (orderId == null) {
|
||||
return null;
|
||||
} else {
|
||||
// envio de correo de confirmacion de pedido podria ir aqui
|
||||
return orderId;
|
||||
}
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2,10 +2,13 @@
|
||||
package com.imprimelibros.erp.payments.repo;
|
||||
|
||||
import com.imprimelibros.erp.payments.model.Payment;
|
||||
import com.imprimelibros.erp.payments.model.PaymentStatus;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface PaymentRepository extends JpaRepository<Payment, Long> {
|
||||
Optional<Payment> findByGatewayAndGatewayOrderId(String gateway, String gatewayOrderId);
|
||||
Optional<Payment> findFirstByOrderIdAndStatusOrderByIdDesc(Long orderId, PaymentStatus status);
|
||||
}
|
||||
|
||||
@ -14,6 +14,10 @@ import java.util.Optional;
|
||||
public interface PaymentTransactionRepository extends JpaRepository<PaymentTransaction, Long>, JpaSpecificationExecutor<PaymentTransaction> {
|
||||
List<PaymentTransaction> findByGatewayTransactionId(String gatewayTransactionId);
|
||||
Optional<PaymentTransaction> findByIdempotencyKey(String idempotencyKey);
|
||||
Optional<PaymentTransaction> findByPaymentIdAndType(
|
||||
Long paymentId,
|
||||
PaymentTransactionType type
|
||||
);
|
||||
Optional<PaymentTransaction> findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc(
|
||||
Long paymentId,
|
||||
PaymentTransactionType type,
|
||||
|
||||
@ -25,8 +25,8 @@ public class PdfController {
|
||||
@RequestParam(defaultValue = "inline") String mode,
|
||||
Locale locale) {
|
||||
|
||||
if (type.equals(DocumentType.PRESUPUESTO.toString()) && id == null) {
|
||||
throw new IllegalArgumentException("Falta el ID del presupuesto para generar el PDF");
|
||||
if (id == null) {
|
||||
throw new IllegalArgumentException("Falta el ID para generar el PDF");
|
||||
}
|
||||
if (type.equals(DocumentType.PRESUPUESTO.toString())) {
|
||||
Long presupuestoId = Long.valueOf(id);
|
||||
@ -39,7 +39,22 @@ public class PdfController {
|
||||
: ContentDisposition.inline()).filename("presupuesto-" + id + ".pdf").build());
|
||||
|
||||
return new ResponseEntity<>(pdf, headers, HttpStatus.OK);
|
||||
} else {
|
||||
}/*else if(type.equals(DocumentType.PEDIDO.toString())) {
|
||||
|
||||
} */else if (type.equals(DocumentType.FACTURA.toString())) {
|
||||
Long facturaId = Long.valueOf(id);
|
||||
byte[] pdf = pdfService.generaFactura(facturaId, locale);
|
||||
var headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_PDF);
|
||||
headers.setContentDisposition(
|
||||
("download".equals(mode)
|
||||
? ContentDisposition.attachment()
|
||||
: ContentDisposition.inline()).filename("factura-" + id + ".pdf").build());
|
||||
|
||||
return new ResponseEntity<>(pdf, headers, HttpStatus.OK);
|
||||
|
||||
}
|
||||
else {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,11 @@ import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.common.web.HtmlToXhtml;
|
||||
import com.imprimelibros.erp.facturacion.Factura;
|
||||
import com.imprimelibros.erp.facturacion.service.FacturacionService;
|
||||
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||
|
||||
@Service
|
||||
public class PdfService {
|
||||
@ -24,6 +29,8 @@ public class PdfService {
|
||||
private final PdfRenderer renderer;
|
||||
private final PresupuestoRepository presupuestoRepository;
|
||||
private final Utils utils;
|
||||
private final FacturacionService facturacionService;
|
||||
private final PedidoService pedidoService;
|
||||
|
||||
private final Map<String, String> empresa = Map.of(
|
||||
"nombre", "ImprimeLibros ERP",
|
||||
@ -35,7 +42,6 @@ public class PdfService {
|
||||
"poblacion", "Madrid",
|
||||
"web", "www.imprimelibros.com");
|
||||
|
||||
|
||||
private static class PrecioTirada {
|
||||
private Double peso;
|
||||
@JsonProperty("iva_importe_4")
|
||||
@ -88,12 +94,15 @@ public class PdfService {
|
||||
}
|
||||
|
||||
public PdfService(TemplateRegistry registry, PdfTemplateEngine engine, PdfRenderer renderer,
|
||||
PresupuestoRepository presupuestoRepository, Utils utils) {
|
||||
PresupuestoRepository presupuestoRepository, Utils utils, FacturacionService facturacionService,
|
||||
PedidoService pedidoService) {
|
||||
this.registry = registry;
|
||||
this.engine = engine;
|
||||
this.renderer = renderer;
|
||||
this.presupuestoRepository = presupuestoRepository;
|
||||
this.utils = utils;
|
||||
this.pedidoService = pedidoService;
|
||||
this.facturacionService = facturacionService;
|
||||
}
|
||||
|
||||
private byte[] generate(DocumentSpec spec) {
|
||||
@ -122,27 +131,6 @@ public class PdfService {
|
||||
|
||||
model.put("titulo", presupuesto.getTitulo());
|
||||
|
||||
/*
|
||||
* Map<String, Object> resumen = presupuestoService.getTextosResumen(
|
||||
* presupuesto, null, model, model, null)
|
||||
*/
|
||||
model.put("lineas", List.of(
|
||||
Map.of("descripcion", "Impresión interior B/N offset 80 g",
|
||||
"meta", "300 páginas · tinta negra · papel 80 g",
|
||||
"uds", 1000,
|
||||
"precio", 2.15,
|
||||
"dto", 0,
|
||||
"importe", 2150.0),
|
||||
Map.of("descripcion", "Cubierta color 300 g laminado mate",
|
||||
"meta", "Lomo 15 mm · 4/0 · laminado mate",
|
||||
"uds", 1000,
|
||||
"precio", 0.38,
|
||||
"dto", 5.0,
|
||||
"importe", 361.0)));
|
||||
|
||||
model.put("servicios", List.of(
|
||||
Map.of("descripcion", "Transporte península", "unidades", 1, "precio", 90.00)));
|
||||
|
||||
Map<String, Object> specs = utils.getTextoPresupuesto(presupuesto, locale);
|
||||
model.put("specs", specs);
|
||||
|
||||
@ -202,4 +190,54 @@ public class PdfService {
|
||||
throw new RuntimeException("Error generando presupuesto PDF", e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] generaFactura(Long facturaId, Locale locale) {
|
||||
|
||||
try {
|
||||
|
||||
Factura factura = facturacionService.getFactura(facturaId);
|
||||
if (factura == null) {
|
||||
throw new IllegalArgumentException("Factura no encontrada: " + facturaId);
|
||||
}
|
||||
|
||||
factura.getLineas().forEach(l -> l.setDescripcion(HtmlToXhtml.toXhtml(l.getDescripcion())));
|
||||
|
||||
PedidoDireccion direccionFacturacion = pedidoService
|
||||
.getPedidoDireccionFacturacionByPedidoId(factura.getPedidoId());
|
||||
if (direccionFacturacion == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Dirección de facturación no encontrada para el pedido: " + factura.getPedidoId());
|
||||
}
|
||||
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("factura", factura);
|
||||
model.put("direccionFacturacion", direccionFacturacion);
|
||||
|
||||
var spec = new DocumentSpec(
|
||||
DocumentType.FACTURA,
|
||||
"factura-a4",
|
||||
locale,
|
||||
model);
|
||||
|
||||
byte[] pdf = this.generate(spec);
|
||||
|
||||
// HTML
|
||||
// (Opcional) generar HTML de depuración con CSS incrustado
|
||||
try {
|
||||
String templateName = registry.resolve(DocumentType.FACTURA, "factura-a4");
|
||||
String html = engine.render(templateName, locale, model);
|
||||
String css = Files.readString(Path.of("src/main/resources/static/assets/css/facturapdf.css"));
|
||||
String htmlWithCss = html.replaceFirst("(?i)</head>", "<style>\n" + css + "\n</style>\n</head>");
|
||||
Path htmlPath = Path.of("target/factura-test.html");
|
||||
Files.writeString(htmlPath, htmlWithCss, StandardCharsets.UTF_8);
|
||||
} catch (Exception ignore) {
|
||||
/* solo para depuración */ }
|
||||
|
||||
return pdf;
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error generando factura PDF", e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,8 +3,11 @@ package com.imprimelibros.erp.pedidos;
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import com.imprimelibros.erp.direcciones.Direccion.TipoIdentificacionFiscal;
|
||||
import com.imprimelibros.erp.paises.Paises;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Entity
|
||||
@Table(name = "pedidos_direcciones")
|
||||
@ -32,6 +35,9 @@ public class PedidoDireccion {
|
||||
@Column(name = "is_ejemplar_prueba", nullable = false)
|
||||
private boolean ejemplarPrueba = false;
|
||||
|
||||
@Column(name = "email", length = 255)
|
||||
private String email;
|
||||
|
||||
@Column(name = "att", nullable = false, length = 150)
|
||||
private String att;
|
||||
|
||||
@ -50,6 +56,13 @@ public class PedidoDireccion {
|
||||
@Column(name = "pais_code3", nullable = false, length = 3)
|
||||
private String paisCode3 = "esp";
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "pais_code3", referencedColumnName = "code3", insertable = false, updatable = false)
|
||||
private Paises pais;
|
||||
|
||||
@Transient
|
||||
private String paisNombre;
|
||||
|
||||
@Column(name = "telefono", nullable = false, length = 30)
|
||||
private String telefono;
|
||||
|
||||
@ -66,6 +79,9 @@ public class PedidoDireccion {
|
||||
@Column(name = "identificacion_fiscal", length = 50)
|
||||
private String identificacionFiscal;
|
||||
|
||||
@Column(name = "is_palets", nullable = false)
|
||||
private boolean palets = false;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
@ -116,6 +132,14 @@ public class PedidoDireccion {
|
||||
this.ejemplarPrueba = ejemplarPrueba;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getAtt() {
|
||||
return att;
|
||||
}
|
||||
@ -164,6 +188,14 @@ public class PedidoDireccion {
|
||||
this.paisCode3 = paisCode3;
|
||||
}
|
||||
|
||||
public Paises getPais() {
|
||||
return pais;
|
||||
}
|
||||
|
||||
public void setPais(Paises pais) {
|
||||
this.pais = pais;
|
||||
}
|
||||
|
||||
public String getTelefono() {
|
||||
return telefono;
|
||||
}
|
||||
@ -204,8 +236,73 @@ public class PedidoDireccion {
|
||||
this.identificacionFiscal = identificacionFiscal;
|
||||
}
|
||||
|
||||
public boolean isPalets() {
|
||||
return palets;
|
||||
}
|
||||
|
||||
public void setPalets(boolean palets) {
|
||||
this.palets = palets;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
}
|
||||
|
||||
public String getPaisNombre() {
|
||||
return paisNombre;
|
||||
}
|
||||
|
||||
public void setPaisNombre(String paisNombre) {
|
||||
this.paisNombre = paisNombre;
|
||||
}
|
||||
|
||||
|
||||
public Map<String, Object> toSkMap(Double pesoKg) {
|
||||
|
||||
Map<String, Object> direccion = new HashMap<>();
|
||||
direccion.put("cantidad", this.getUnidades());
|
||||
direccion.put("peso", pesoKg);
|
||||
direccion.put("att", this.getAtt());
|
||||
direccion.put("email", this.getEmail());
|
||||
direccion.put("direccion", this.getDireccion());
|
||||
direccion.put("pais_code3", this.getPaisCode3());
|
||||
direccion.put("cp", this.getCp());
|
||||
direccion.put("municipio", this.getCiudad());
|
||||
direccion.put("provincia", this.getProvincia());
|
||||
direccion.put("telefono", this.getTelefono());
|
||||
direccion.put("entregaPieCalle", this.isPalets() ? 1 : 0);
|
||||
direccion.put("is_ferro_prototipo", this.isEjemplarPrueba() ? 1 : 0);
|
||||
direccion.put("num_ferro_prototipo", this.isEjemplarPrueba() ? 1 : 0);
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("direccion", direccion);
|
||||
map.put("unidades", this.getUnidades());
|
||||
map.put("entregaPalets", this.isPalets() ? 1 : 0);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public static Map<String, Object> toSkMapDepositoLegal() {
|
||||
Map<String, Object> direccion = new HashMap<>();
|
||||
direccion.put("cantidad", 4);
|
||||
direccion.put("peso", 0);
|
||||
direccion.put("att", "Unidades para Depósito Legal (sin envío)");
|
||||
direccion.put("email", "");
|
||||
direccion.put("direccion", "");
|
||||
direccion.put("pais_code3", "esp");
|
||||
direccion.put("cp", "");
|
||||
direccion.put("municipio", "");
|
||||
direccion.put("provincia", "");
|
||||
direccion.put("telefono", "");
|
||||
direccion.put("entregaPieCalle", 0);
|
||||
direccion.put("is_ferro_prototipo", 0);
|
||||
direccion.put("num_ferro_prototipo", 0);
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("direccion", direccion);
|
||||
map.put("unidades", 4);
|
||||
map.put("entregaPalets", 0);
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package com.imprimelibros.erp.pedidos;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -11,5 +12,15 @@ public interface PedidoDireccionRepository extends JpaRepository<PedidoDireccion
|
||||
|
||||
// Si en tu código sueles trabajar con el objeto:
|
||||
List<PedidoDireccion> findByPedidoLinea(PedidoLinea pedidoLinea);
|
||||
}
|
||||
|
||||
PedidoDireccion findByPedidoIdAndFacturacionTrue(Long pedidoId);
|
||||
|
||||
@Query("""
|
||||
select distinct d
|
||||
from PedidoDireccion d
|
||||
join d.pedidoLinea pl
|
||||
where d.pedidoLinea.id = :pedidoLineaId
|
||||
""")
|
||||
List<PedidoDireccion> findByPedidoLineaId(Long pedidoLineaId);
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,11 +10,18 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
public class PedidoLinea {
|
||||
|
||||
public enum Estado {
|
||||
aprobado("pedido.estado.aprobado", 1),
|
||||
maquetacion("pedido.estado.maquetacion", 2),
|
||||
haciendo_ferro("pedido.estado.haciendo_ferro", 3),
|
||||
produccion("pedido.estado.produccion", 4),
|
||||
cancelado("pedido.estado.cancelado", 5);
|
||||
pendiente_pago("pedido.estado.pendiente_pago", 1),
|
||||
procesando_pago("pedido.estado.procesando_pago", 2),
|
||||
denegado_pago("pedido.estado.denegado_pago", 3),
|
||||
aprobado("pedido.estado.aprobado", 4),
|
||||
maquetacion("pedido.estado.maquetacion", 5),
|
||||
haciendo_ferro("pedido.estado.haciendo_ferro", 6),
|
||||
esperando_aceptacion_ferro("pedido.estado.esperando_aceptacion_ferro", 7),
|
||||
ferro_cliente("pedido.estado.ferro_cliente", 8),
|
||||
produccion("pedido.estado.produccion", 9),
|
||||
terminado("pedido.estado.terminado", 10),
|
||||
enviado("pedido.estado.enviado", 11),
|
||||
cancelado("pedido.estado.cancelado", 12);
|
||||
|
||||
private final String messageKey;
|
||||
private final int priority;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.imprimelibros.erp.pedidos;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
@ -9,7 +10,25 @@ import java.util.List;
|
||||
public interface PedidoLineaRepository extends JpaRepository<PedidoLinea, Long> {
|
||||
|
||||
List<PedidoLinea> findByPedidoId(Long pedidoId);
|
||||
|
||||
List<PedidoLinea> findByPedidoIdOrderByIdAsc(Long pedidoId);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@ -8,9 +8,14 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.imprimelibros.erp.cart.Cart;
|
||||
import com.imprimelibros.erp.cart.CartDireccion;
|
||||
import com.imprimelibros.erp.cart.CartItem;
|
||||
import com.imprimelibros.erp.cart.CartService;
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.direcciones.Direccion;
|
||||
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
|
||||
@ -18,6 +23,9 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
|
||||
import com.imprimelibros.erp.users.UserService;
|
||||
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||
import com.imprimelibros.erp.externalApi.skApiClient;
|
||||
import com.imprimelibros.erp.facturacion.FacturaDireccion;
|
||||
import com.imprimelibros.erp.facturacion.dto.DireccionFacturacionDto;
|
||||
import com.imprimelibros.erp.pedidos.PedidoLinea.Estado;
|
||||
|
||||
@Service
|
||||
@ -30,10 +38,16 @@ public class PedidoService {
|
||||
private final DireccionService direccionService;
|
||||
private final UserService userService;
|
||||
private final PresupuestoService presupuestoService;
|
||||
private final CartService cartService;
|
||||
private final skApiClient skApiClient;
|
||||
private final PresupuestoRepository presupuestoRepo;
|
||||
private final MessageSource messageSource;
|
||||
|
||||
public PedidoService(PedidoRepository pedidoRepository, PedidoLineaRepository pedidoLineaRepository,
|
||||
PresupuestoRepository presupuestoRepository, PedidoDireccionRepository pedidoDireccionRepository,
|
||||
DireccionService direccionService, UserService userService, PresupuestoService presupuestoService) {
|
||||
DireccionService direccionService, UserService userService, PresupuestoService presupuestoService,
|
||||
CartService cartService, skApiClient skApiClient, PresupuestoRepository presupuestoRepo,
|
||||
MessageSource messageSource) {
|
||||
this.pedidoRepository = pedidoRepository;
|
||||
this.pedidoLineaRepository = pedidoLineaRepository;
|
||||
this.presupuestoRepository = presupuestoRepository;
|
||||
@ -41,48 +55,32 @@ public class PedidoService {
|
||||
this.direccionService = direccionService;
|
||||
this.userService = userService;
|
||||
this.presupuestoService = presupuestoService;
|
||||
this.cartService = cartService;
|
||||
this.skApiClient = skApiClient;
|
||||
this.presupuestoRepo = presupuestoRepo;
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
public int getDescuentoFidelizacion(Long userId) {
|
||||
// descuento entre el 1% y el 6% para clientes fidelidad (mas de 1500€ en el
|
||||
// ultimo año)
|
||||
Instant haceUnAno = Instant.now().minusSeconds(365 * 24 * 60 * 60);
|
||||
double totalGastado = pedidoRepository.sumTotalByCreatedByAndCreatedAtAfter(userId, haceUnAno);
|
||||
if (totalGastado < 1200) {
|
||||
return 0;
|
||||
} else if (totalGastado >= 1200 && totalGastado < 1999) {
|
||||
return 1;
|
||||
} else if (totalGastado >= 2000 && totalGastado < 2999) {
|
||||
return 2;
|
||||
} else if (totalGastado >= 3000 && totalGastado < 3999) {
|
||||
return 3;
|
||||
} else if (totalGastado >= 4000 && totalGastado < 4999) {
|
||||
return 4;
|
||||
} else if (totalGastado >= 5000) {
|
||||
return 5;
|
||||
}
|
||||
return 0;
|
||||
public Pedido getPedidoById(Long pedidoId) {
|
||||
return pedidoRepository.findById(pedidoId).orElse(null);
|
||||
}
|
||||
|
||||
public PedidoDireccion getPedidoDireccionFacturacionByPedidoId(Long pedidoId) {
|
||||
return pedidoDireccionRepository.findByPedidoIdAndFacturacionTrue(pedidoId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un pedido a partir de:
|
||||
* - lista de IDs de presupuesto
|
||||
* - resumen numérico del carrito (getCartSummaryRaw)
|
||||
* - datos de proveedor
|
||||
* - usuario que crea el pedido
|
||||
*/
|
||||
@Transactional
|
||||
public Pedido crearPedido(
|
||||
List<Long> presupuestoIds,
|
||||
Map<String, Object> presupuestoDirecciones,
|
||||
Long cartId,
|
||||
Long direccionFacturacionId,
|
||||
Map<String, Object> cartSummaryRaw,
|
||||
String proveedor,
|
||||
String proveedorRef,
|
||||
Long userId) {
|
||||
String proveedorRef) {
|
||||
|
||||
Pedido pedido = new Pedido();
|
||||
|
||||
Cart cart = cartService.getCartById(cartId);
|
||||
Map<String, Object> cartSummaryRaw = cartService.getCartSummaryRaw(cart, Locale.getDefault());
|
||||
|
||||
// Datos económicos (ojo con las claves, son las del summaryRaw)
|
||||
pedido.setBase((Double) cartSummaryRaw.getOrDefault("base", 0.0d));
|
||||
pedido.setEnvio((Double) cartSummaryRaw.getOrDefault("shipment", 0.0d));
|
||||
@ -92,44 +90,134 @@ public class PedidoService {
|
||||
pedido.setTotal((Double) cartSummaryRaw.getOrDefault("total", 0.0d));
|
||||
|
||||
// Proveedor
|
||||
pedido.setProveedor(proveedor);
|
||||
pedido.setProveedorRef(proveedorRef);
|
||||
if (proveedor != null && proveedorRef != null) {
|
||||
pedido.setProveedor(proveedor);
|
||||
pedido.setProveedorRef(proveedorRef);
|
||||
}
|
||||
|
||||
// Auditoría mínima
|
||||
/*
|
||||
* Long userId = cart.getUserId();
|
||||
* pedido.setCreatedBy(userService.findById(userId));
|
||||
* pedido.setUpdatedBy(userService.findById(userId));
|
||||
*/
|
||||
// Se obtiene el usuario del primer presupuesto del carrito
|
||||
Long userId = null;
|
||||
List<CartItem> cartItems = cart.getItems();
|
||||
if (!cartItems.isEmpty()) {
|
||||
Presupuesto firstPresupuesto = cartItems.get(0).getPresupuesto();
|
||||
if (firstPresupuesto != null) {
|
||||
userId = firstPresupuesto.getUser().getId();
|
||||
}
|
||||
}
|
||||
if (userId == null) {
|
||||
userId = cart.getUserId();
|
||||
}
|
||||
pedido.setCreatedBy(userService.findById(userId));
|
||||
pedido.setUpdatedBy(userService.findById(userId));
|
||||
pedido.setCreatedAt(Instant.now());
|
||||
pedido.setDeleted(false);
|
||||
pedido.setUpdatedAt(Instant.now());
|
||||
pedido.setUpdatedBy(userService.findById(userId));
|
||||
|
||||
// Guardamos el pedido
|
||||
Pedido saved = pedidoRepository.save(pedido);
|
||||
Pedido pedidoGuardado = pedidoRepository.save(pedido);
|
||||
pedidoGuardado.setCreatedBy(userService.findById(userId));
|
||||
pedidoGuardado.setUpdatedBy(userService.findById(userId));
|
||||
pedidoRepository.save(pedidoGuardado);
|
||||
|
||||
// Crear líneas del pedido
|
||||
for (Long presupuestoId : presupuestoIds) {
|
||||
Presupuesto presupuesto = presupuestoRepository.getReferenceById(presupuestoId);
|
||||
List<CartItem> items = cart.getItems();
|
||||
|
||||
for (Integer i = 0; i < items.size(); i++) {
|
||||
CartItem item = items.get(i);
|
||||
Presupuesto pCart = item.getPresupuesto();
|
||||
|
||||
// Asegurarnos de trabajar con la entidad gestionada por JPA
|
||||
Presupuesto p = presupuestoRepository.findById(pCart.getId())
|
||||
.orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + pCart.getId()));
|
||||
p.setEstado(Presupuesto.Estado.aceptado);
|
||||
presupuestoRepository.save(p);
|
||||
|
||||
PedidoLinea linea = new PedidoLinea();
|
||||
linea.setPedido(saved);
|
||||
linea.setPresupuesto(presupuesto);
|
||||
linea.setPedido(pedidoGuardado);
|
||||
linea.setPresupuesto(p);
|
||||
linea.setCreatedBy(userId);
|
||||
linea.setCreatedAt(LocalDateTime.now());
|
||||
linea.setEstado(getEstadoInicial(presupuesto));
|
||||
linea.setEstado(PedidoLinea.Estado.pendiente_pago);
|
||||
linea.setEstadoManual(false);
|
||||
pedidoLineaRepository.save(linea);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Map<String, Object>> direcciones = (Map<String, Map<String, Object>>) presupuestoDirecciones
|
||||
.get(presupuesto.getId().toString());
|
||||
if (direcciones != null) {
|
||||
saveDireccionesPedidoLinea(direcciones, saved, linea, direccionFacturacionId);
|
||||
}
|
||||
// Guardar las direcciones asociadas a la línea del pedido
|
||||
Map<String, Object> direcciones_presupuesto = this.getDireccionesPresupuesto(cart, p);
|
||||
saveDireccionesPedidoLinea(direcciones_presupuesto, pedidoGuardado, linea, direccionFacturacionId);
|
||||
|
||||
}
|
||||
|
||||
return saved;
|
||||
return pedidoGuardado;
|
||||
}
|
||||
|
||||
/** Lista de los items del pedido preparados para la vista*/
|
||||
public Boolean markPedidoAsProcesingPayment(Long pedidoId) {
|
||||
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
||||
if (pedido == null) {
|
||||
return false;
|
||||
}
|
||||
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
|
||||
for (PedidoLinea linea : lineas) {
|
||||
linea.setEstado(PedidoLinea.Estado.procesando_pago);
|
||||
pedidoLineaRepository.save(linea);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Boolean markPedidoAsPaymentDenied(Long pedidoId) {
|
||||
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
||||
if (pedido == null) {
|
||||
return false;
|
||||
}
|
||||
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
|
||||
for (PedidoLinea linea : lineas) {
|
||||
linea.setEstado(PedidoLinea.Estado.denegado_pago);
|
||||
pedidoLineaRepository.save(linea);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Pedido findById(Long pedidoId) {
|
||||
return pedidoRepository.findById(pedidoId).orElse(null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Boolean upsertDireccionFacturacion(Long pedidoId, DireccionFacturacionDto direccionData) {
|
||||
|
||||
try {
|
||||
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
||||
if (pedido != null) {
|
||||
|
||||
PedidoDireccion direccionPedido = pedidoDireccionRepository.findByPedidoIdAndFacturacionTrue(pedidoId);
|
||||
|
||||
if (direccionPedido == null) {
|
||||
// crear
|
||||
direccionPedido = direccionData.toPedidoDireccion();
|
||||
direccionPedido.setPedido(pedido);
|
||||
|
||||
} else {
|
||||
// actualizar en la existente (NO crees una nueva, para conservar ID)
|
||||
direccionData.applyTo(direccionPedido); // si implementas applyTo()
|
||||
direccionPedido.setFacturacion(true); // por si acaso
|
||||
}
|
||||
|
||||
pedidoDireccionRepository.save(direccionPedido);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lista de los items del pedido preparados para la vista */
|
||||
@Transactional
|
||||
public List<Map<String, Object>> getLineas(Long pedidoId, Locale locale) {
|
||||
Pedido p = pedidoRepository.findById(pedidoId).orElse(null);
|
||||
@ -144,31 +232,422 @@ public class PedidoService {
|
||||
Presupuesto presupuesto = item.getPresupuesto();
|
||||
Map<String, Object> elemento = presupuestoService.getPresupuestoInfoForCard(presupuesto, locale);
|
||||
elemento.put("estado", item.getEstado());
|
||||
elemento.put("fechaEntrega", item.getFechaEntrega() != null ?
|
||||
Utils.formatDate(item.getFechaEntrega(), locale) : "");
|
||||
elemento.put("fechaEntrega",
|
||||
item.getFechaEntrega() != null ? Utils.formatDate(item.getFechaEntrega(), locale) : "");
|
||||
elemento.put("lineaId", item.getId());
|
||||
resultados.add(elemento);
|
||||
}
|
||||
return resultados;
|
||||
}
|
||||
|
||||
public PedidoDireccion getDireccionFacturacionPedido(Long pedidoId) {
|
||||
return pedidoDireccionRepository.findByPedidoIdAndFacturacionTrue(pedidoId);
|
||||
}
|
||||
|
||||
public List<PedidoDireccion> getDireccionesEntregaPedidoLinea(Long pedidoLineaId) {
|
||||
return pedidoDireccionRepository.findByPedidoLinea_Id(pedidoLineaId);
|
||||
}
|
||||
|
||||
public Boolean setOrderAsPaid(Long pedidoId) {
|
||||
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
||||
if (pedido == null) {
|
||||
return false;
|
||||
}
|
||||
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
|
||||
List<Map<String, Object>> referenciasProveedor = new ArrayList<>();
|
||||
Integer total = lineas.size();
|
||||
Integer counter = 1;
|
||||
for (PedidoLinea linea : lineas) {
|
||||
if (linea.getEstado() == Estado.pendiente_pago
|
||||
|| linea.getEstado() == Estado.denegado_pago) {
|
||||
|
||||
Presupuesto presupuesto = linea.getPresupuesto();
|
||||
linea.setEstado(getEstadoInicial(presupuesto));
|
||||
pedidoLineaRepository.save(linea);
|
||||
|
||||
// Save presupuesto in SK
|
||||
Map<String, Object> result = savePresupuestoSK(linea.getId(), presupuesto, counter, total);
|
||||
if (result == null) {
|
||||
return false;
|
||||
}
|
||||
referenciasProveedor.add(result);
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
if (referenciasProveedor.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save pedido in SK
|
||||
ArrayList<Long> presupuestoSkIds = new ArrayList<>();
|
||||
for (Map<String, Object> presData : referenciasProveedor) {
|
||||
Long presId = ((Number) presData.get("id")).longValue();
|
||||
presupuestoSkIds.add(presId);
|
||||
}
|
||||
|
||||
Map<String, Object> ids = new HashMap<>();
|
||||
ids.put("presupuesto_ids", presupuestoSkIds);
|
||||
Long skPedidoId = skApiClient.crearPedido(ids);
|
||||
if (skPedidoId == null) {
|
||||
System.out.println("No se pudo crear el pedido en SK.");
|
||||
return false;
|
||||
}
|
||||
pedido.setProveedor("Safekat");
|
||||
pedido.setProveedorRef(skPedidoId.toString());
|
||||
pedidoRepository.save(pedido);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Map<String, Object> actualizarEstado(Long pedidoLineaId, Locale locale) {
|
||||
|
||||
PedidoLinea pedidoLinea = pedidoLineaRepository.findById(pedidoLineaId).orElse(null);
|
||||
if (pedidoLinea == null) {
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", messageSource.getMessage("pedido.errors.linea-not-found", null, locale));
|
||||
}
|
||||
|
||||
PedidoLinea.Estado estadoOld = pedidoLinea.getEstado();
|
||||
if (estadoOld == null) {
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", messageSource.getMessage("pedido.errors.cannot-update", null, locale));
|
||||
}
|
||||
|
||||
// Rango: >= haciendo_ferro y < enviado
|
||||
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(
|
||||
"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);
|
||||
pedidoLineaRepository.save(pedidoLinea);
|
||||
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"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) {
|
||||
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
||||
if (pedido == null) {
|
||||
return false;
|
||||
}
|
||||
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
|
||||
for (PedidoLinea linea : lineas) {
|
||||
if (linea.getEstado() == Estado.maquetacion) {
|
||||
linea.setEstado(Estado.haciendo_ferro);
|
||||
pedidoLineaRepository.save(linea);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Map<String, Object> getFilesType(Long pedidoLineaId, Locale locale) {
|
||||
PedidoLinea pedidoLinea = pedidoLineaRepository.findById(pedidoLineaId).orElse(null);
|
||||
if (pedidoLinea == null) {
|
||||
return Map.of("success", false, "message", "Línea de pedido no encontrada.");
|
||||
}
|
||||
Map<String, Object> files = skApiClient.getFilesTypes(
|
||||
Long.valueOf(pedidoLinea.getPresupuesto().getProveedorRef2().toString()), locale);
|
||||
return files;
|
||||
}
|
||||
|
||||
public byte[] getFerroFileContent(Long pedidoLineaId, Locale locale) {
|
||||
return downloadFile(pedidoLineaId, "ferro", locale);
|
||||
}
|
||||
|
||||
public byte[] getCubiertaFileContent(Long pedidoLineaId, Locale locale) {
|
||||
return downloadFile(pedidoLineaId, "cubierta", locale);
|
||||
}
|
||||
|
||||
public byte[] getTapaFileContent(Long pedidoLineaId, Locale locale) {
|
||||
return downloadFile(pedidoLineaId, "tapa", locale);
|
||||
}
|
||||
|
||||
public Boolean aceptarFerro(Long pedidoLineaId, Locale locale) {
|
||||
PedidoLinea pedidoLinea = pedidoLineaRepository.findById(pedidoLineaId).orElse(null);
|
||||
if (pedidoLinea == null) {
|
||||
return false;
|
||||
}
|
||||
if (pedidoLinea.getEstado() != PedidoLinea.Estado.esperando_aceptacion_ferro) {
|
||||
return false;
|
||||
}
|
||||
Boolean result = skApiClient.aceptarFerro(
|
||||
Long.valueOf(pedidoLinea.getPresupuesto().getProveedorRef2().toString()), locale);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
pedidoLinea.setEstado(PedidoLinea.Estado.produccion);
|
||||
pedidoLineaRepository.save(pedidoLinea);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Boolean cancelarPedido(Long pedidoId) {
|
||||
|
||||
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
||||
if (pedido == null) {
|
||||
return false;
|
||||
}
|
||||
Boolean result = skApiClient.cancelarPedido(Long.valueOf(pedido.getProveedorRef()));
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
|
||||
for (PedidoLinea linea : lineas) {
|
||||
if (linea.getEstado() != PedidoLinea.Estado.terminado && linea.getEstado() != PedidoLinea.Estado.enviado) {
|
||||
linea.setEstado(PedidoLinea.Estado.cancelado);
|
||||
pedidoLineaRepository.save(linea);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/***************************
|
||||
* MÉTODOS PRIVADOS
|
||||
***************************/
|
||||
|
||||
private byte[] downloadFile(Long pedidoLineaId, String fileType, Locale locale) {
|
||||
PedidoLinea pedidoLinea = pedidoLineaRepository.findById(pedidoLineaId).orElse(null);
|
||||
if (pedidoLinea == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] fileData = skApiClient.downloadFile(
|
||||
Long.valueOf(pedidoLinea.getPresupuesto().getProveedorRef2().toString()),
|
||||
fileType,
|
||||
locale);
|
||||
return fileData;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
private Map<String, Object> savePresupuestoSK(Long pedidoLineaId, Presupuesto presupuesto, Integer counter,
|
||||
Integer total) {
|
||||
|
||||
Map<String, Object> data_to_send = presupuestoService.toSkApiRequest(presupuesto, true);
|
||||
data_to_send.put("createPedido", 0);
|
||||
|
||||
// Recuperar el mapa anidado datosCabecera
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> datosCabecera = (Map<String, Object>) data_to_send.get("datosCabecera");
|
||||
if (datosCabecera != null) {
|
||||
Object tituloOriginal = datosCabecera.get("titulo");
|
||||
datosCabecera.put(
|
||||
"titulo",
|
||||
"[" + (counter) + "/" + total + "] " + (tituloOriginal != null ? tituloOriginal : ""));
|
||||
}
|
||||
|
||||
List<PedidoDireccion> direccionesPedidoLinea = pedidoDireccionRepository
|
||||
.findByPedidoLineaId(pedidoLineaId);
|
||||
List<Map<String, Object>> direccionesPresupuesto = new ArrayList<>();
|
||||
List<Map<String, Object>> direccionEjemplarPrueba = new ArrayList<>();
|
||||
|
||||
for (PedidoDireccion pd : direccionesPedidoLinea) {
|
||||
if (pd.isEjemplarPrueba()) {
|
||||
direccionEjemplarPrueba.add(
|
||||
pd.toSkMap(presupuesto.getPeso()));
|
||||
} else {
|
||||
direccionesPresupuesto.add(
|
||||
pd.toSkMap(presupuesto.getPeso() * pd.getUnidades()));
|
||||
}
|
||||
|
||||
}
|
||||
if (presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("deposito-legal")) {
|
||||
direccionesPresupuesto.add(
|
||||
PedidoDireccion.toSkMapDepositoLegal());
|
||||
}
|
||||
data_to_send.put("direcciones", direccionesPresupuesto);
|
||||
if (direccionEjemplarPrueba.size() > 0)
|
||||
data_to_send.put("direccionesFP1", direccionEjemplarPrueba.get(0));
|
||||
else {
|
||||
data_to_send.put("direccionesFP1", new ArrayList<>());
|
||||
}
|
||||
|
||||
Map<String, Object> result = skApiClient.savePresupuesto(data_to_send);
|
||||
|
||||
if (result.containsKey("error")) {
|
||||
System.out.println("Error al guardar presupuesto en SK");
|
||||
System.out.println("-------------------------");
|
||||
System.out.println(result.get("error"));
|
||||
// decide si seguir con otros items o abortar:
|
||||
// continue; o bien throw ...
|
||||
return null;
|
||||
}
|
||||
|
||||
Object dataObj = result.get("data");
|
||||
if (!(dataObj instanceof Map<?, ?> dataRaw)) {
|
||||
System.out.println("Formato inesperado de 'data' en savePresupuesto: " + result);
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> dataMap = (Map<String, Object>) dataRaw;
|
||||
Long presId = ((Number) dataMap.get("id")).longValue();
|
||||
String skin = ((String) dataMap.get("iskn")).toString();
|
||||
presupuesto.setProveedor("Safekat");
|
||||
presupuesto.setProveedorRef1(skin);
|
||||
presupuesto.setProveedorRef2(presId);
|
||||
presupuesto.setEstado(Presupuesto.Estado.aceptado);
|
||||
presupuestoRepo.save(presupuesto);
|
||||
|
||||
return dataMap;
|
||||
}
|
||||
|
||||
// Obtener las direcciones de envío asociadas a un presupuesto en el carrito
|
||||
private Map<String, Object> getDireccionesPresupuesto(Cart cart, Presupuesto presupuesto) {
|
||||
|
||||
List<Map<String, Object>> direccionesPresupuesto = new ArrayList<>();
|
||||
List<Map<String, Object>> direccionesPrueba = new ArrayList<>();
|
||||
if (cart.getOnlyOneShipment()) {
|
||||
List<CartDireccion> direcciones = cart.getDirecciones().stream().limit(1).toList();
|
||||
if (!direcciones.isEmpty()) {
|
||||
if (presupuesto.getServiciosJson() != null
|
||||
&& presupuesto.getServiciosJson().contains("deposito-legal")) {
|
||||
direccionesPresupuesto.add(direcciones.get(0).toSkMap(
|
||||
presupuesto.getSelectedTirada(),
|
||||
presupuesto.getPeso(),
|
||||
direcciones.get(0).getIsPalets(),
|
||||
false));
|
||||
|
||||
direccionesPresupuesto.add(direcciones.get(0).toSkMapDepositoLegal());
|
||||
} else {
|
||||
direccionesPresupuesto.add(direcciones.get(0).toSkMap(
|
||||
presupuesto.getSelectedTirada(),
|
||||
presupuesto.getPeso(),
|
||||
direcciones.get(0).getIsPalets(),
|
||||
false));
|
||||
}
|
||||
if (presupuesto.getServiciosJson() != null
|
||||
&& presupuesto.getServiciosJson().contains("ejemplar-prueba")) {
|
||||
direccionesPrueba.add(direcciones.get(0).toSkMap(
|
||||
1,
|
||||
presupuesto.getPeso(),
|
||||
false,
|
||||
true));
|
||||
}
|
||||
|
||||
Map<String, Object> direccionesRet = new HashMap<>();
|
||||
direccionesRet.put("direcciones", direccionesPresupuesto);
|
||||
if (!direccionesPrueba.isEmpty())
|
||||
direccionesRet.put("direccionesFP1", direccionesPrueba.get(0));
|
||||
else {
|
||||
direccionesRet.put("direccionesFP1", new ArrayList<>());
|
||||
}
|
||||
return direccionesRet;
|
||||
}
|
||||
} else {
|
||||
List<CartDireccion> direcciones = cart.getDirecciones().stream()
|
||||
.filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(presupuesto.getId()))
|
||||
.toList();
|
||||
|
||||
for (CartDireccion cd : direcciones) {
|
||||
|
||||
// direccion de ejemplar de prueba
|
||||
if (cd.getPresupuesto() == null || !cd.getPresupuesto().getId().equals(presupuesto.getId())) {
|
||||
continue;
|
||||
}
|
||||
if (cd.getUnidades() == null || cd.getUnidades() <= 0) {
|
||||
direccionesPrueba.add(cd.toSkMap(
|
||||
1,
|
||||
presupuesto.getPeso(),
|
||||
false,
|
||||
true));
|
||||
} else {
|
||||
direccionesPresupuesto.add(cd.toSkMap(
|
||||
cd.getUnidades(),
|
||||
presupuesto.getPeso(),
|
||||
cd.getIsPalets(),
|
||||
false));
|
||||
}
|
||||
}
|
||||
if (presupuesto.getServiciosJson() != null
|
||||
&& presupuesto.getServiciosJson().contains("deposito-legal")) {
|
||||
CartDireccion cd = new CartDireccion();
|
||||
direccionesPresupuesto.add(cd.toSkMapDepositoLegal());
|
||||
}
|
||||
}
|
||||
Map<String, Object> direccionesRet = new HashMap<>();
|
||||
direccionesRet.put("direcciones", direccionesPresupuesto);
|
||||
if (!direccionesPrueba.isEmpty())
|
||||
direccionesRet.put("direccionesFP1", direccionesPrueba.get(0));
|
||||
else {
|
||||
direccionesRet.put("direccionesFP1", new ArrayList<>());
|
||||
}
|
||||
return direccionesRet;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
private void saveDireccionesPedidoLinea(
|
||||
Map<String, Map<String, Object>> direcciones,
|
||||
Map<String, Object> direcciones,
|
||||
Pedido pedido,
|
||||
PedidoLinea linea, Long direccionFacturacionId) {
|
||||
|
||||
String email = pedido.getCreatedBy().getUserName();
|
||||
|
||||
// direccion prueba
|
||||
if (direcciones.containsKey("direccionesFP1")) {
|
||||
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> fp1 = (Map<String, Object>) direcciones.get("direccionesFP1");
|
||||
@SuppressWarnings("unchecked")
|
||||
PedidoDireccion direccion = saveDireccion(
|
||||
email,
|
||||
false,
|
||||
(HashMap<String, Object>) fp1.get("direccion"),
|
||||
pedido,
|
||||
linea, true,
|
||||
linea,
|
||||
true,
|
||||
false);
|
||||
pedidoDireccionRepository.save(direccion);
|
||||
} catch (Exception e) {
|
||||
@ -187,6 +666,10 @@ public class PedidoService {
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
PedidoDireccion direccion = saveDireccion(
|
||||
email,
|
||||
((Number) ((HashMap<String, Object>) dir)
|
||||
.getOrDefault("entregaPalets", 0))
|
||||
.intValue() == 1,
|
||||
(HashMap<String, Object>) ((HashMap<String, Object>) dir).get("direccion"),
|
||||
pedido,
|
||||
linea, false,
|
||||
@ -211,20 +694,30 @@ public class PedidoService {
|
||||
dirFactMap.put("identificacion_fiscal", dirFact.getIdentificacionFiscal());
|
||||
|
||||
PedidoDireccion direccion = saveDireccion(
|
||||
email,
|
||||
false,
|
||||
dirFactMap,
|
||||
pedido,
|
||||
linea, false,
|
||||
linea,
|
||||
false,
|
||||
true);
|
||||
pedidoDireccionRepository.save(direccion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PedidoDireccion saveDireccion(HashMap<String, Object> dir, Pedido pedido, PedidoLinea linea,
|
||||
private PedidoDireccion saveDireccion(
|
||||
String email,
|
||||
Boolean palets,
|
||||
HashMap<String, Object> dir,
|
||||
Pedido pedido,
|
||||
PedidoLinea linea,
|
||||
Boolean isEjemplarPrueba,
|
||||
Boolean isFacturacion) {
|
||||
|
||||
PedidoDireccion direccion = new PedidoDireccion();
|
||||
direccion.setEmail(email);
|
||||
direccion.setPalets(isEjemplarPrueba || isFacturacion ? false : palets);
|
||||
direccion.setPedidoLinea(isFacturacion ? null : linea);
|
||||
if (isFacturacion) {
|
||||
direccion.setUnidades(null);
|
||||
@ -239,9 +732,9 @@ public class PedidoService {
|
||||
direccion.setUnidades((Integer) dir.getOrDefault("cantidad", 1));
|
||||
direccion.setEjemplarPrueba(false);
|
||||
}
|
||||
direccion.setFacturacion(false);
|
||||
}
|
||||
|
||||
direccion.setFacturacion(false);
|
||||
direccion.setAtt((String) dir.getOrDefault("att", ""));
|
||||
direccion.setDireccion((String) dir.getOrDefault("direccion", ""));
|
||||
direccion.setCp((Integer) dir.getOrDefault("cp", 0));
|
||||
@ -260,9 +753,9 @@ public class PedidoService {
|
||||
|
||||
}
|
||||
|
||||
private Estado getEstadoInicial(Presupuesto p){
|
||||
private Estado getEstadoInicial(Presupuesto p) {
|
||||
|
||||
if(presupuestoService.hasMaquetacion(p)){
|
||||
if (presupuestoService.hasMaquetacion(p)) {
|
||||
return Estado.maquetacion;
|
||||
} else {
|
||||
return Estado.haciendo_ferro;
|
||||
|
||||
@ -4,14 +4,20 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import java.security.Principal;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
@ -20,6 +26,10 @@ import com.imprimelibros.erp.datatables.DataTable;
|
||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||
import com.imprimelibros.erp.facturacion.service.FacturacionService;
|
||||
import com.imprimelibros.erp.i18n.TranslationService;
|
||||
import com.imprimelibros.erp.paises.PaisesService;
|
||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
|
||||
import com.imprimelibros.erp.users.UserDao;
|
||||
|
||||
import jakarta.persistence.criteria.Join;
|
||||
@ -27,28 +37,54 @@ import jakarta.persistence.criteria.JoinType;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/pedidos")
|
||||
public class PedidosController {
|
||||
|
||||
private final PresupuestoService presupuestoService;
|
||||
|
||||
private final PedidoRepository repoPedido;
|
||||
private final PedidoService pedidoService;
|
||||
private final UserDao repoUser;
|
||||
private final MessageSource messageSource;
|
||||
private final PedidoLineaRepository repoPedidoLinea;
|
||||
private final PaisesService paisesService;
|
||||
private final TranslationService translationService;
|
||||
private final FacturacionService facturacionService;
|
||||
|
||||
public PedidosController(PedidoRepository repoPedido, PedidoService pedidoService, UserDao repoUser, MessageSource messageSource,
|
||||
PedidoLineaRepository repoPedidoLinea) {
|
||||
public PedidosController(PedidoRepository repoPedido, PedidoService pedidoService, UserDao repoUser,
|
||||
MessageSource messageSource, TranslationService translationService,
|
||||
PedidoLineaRepository repoPedidoLinea, PaisesService paisesService,
|
||||
FacturacionService facturacionService, PresupuestoService presupuestoService) {
|
||||
this.repoPedido = repoPedido;
|
||||
this.pedidoService = pedidoService;
|
||||
this.repoUser = repoUser;
|
||||
this.messageSource = messageSource;
|
||||
this.translationService = translationService;
|
||||
this.repoPedidoLinea = repoPedidoLinea;
|
||||
this.paisesService = paisesService;
|
||||
this.facturacionService = facturacionService;
|
||||
this.presupuestoService = presupuestoService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listarPedidos() {
|
||||
public String listarPedidos(Model model, Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"app.cancelar",
|
||||
"app.seleccionar",
|
||||
"app.yes",
|
||||
"checkout.payment.card",
|
||||
"checkout.payment.bizum",
|
||||
"checkout.payment.bank-transfer",
|
||||
"checkout.error.select-method");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
if (Utils.isCurrentUserAdmin()) {
|
||||
return "imprimelibros/pedidos/pedidos-list";
|
||||
}
|
||||
@ -68,8 +104,7 @@ public class PedidosController {
|
||||
Long currentUserId = Utils.currentUserId(principal);
|
||||
|
||||
List<String> searchable = List.of(
|
||||
"id"
|
||||
);
|
||||
"id");
|
||||
|
||||
// Campos ordenables
|
||||
List<String> orderable = List.of(
|
||||
@ -157,9 +192,18 @@ public class PedidosController {
|
||||
return text;
|
||||
})
|
||||
.add("actions", pedido -> {
|
||||
return "<span class=\'badge bg-success btn-view \' data-id=\'" + pedido.getId()
|
||||
String data = "<span class=\'badge bg-success btn-view \' data-id=\'" + pedido.getId()
|
||||
+ "\' style=\'cursor: pointer;\'>"
|
||||
+ messageSource.getMessage("app.view", null, locale) + "</span>";
|
||||
List<PedidoLinea> lineas = repoPedidoLinea.findByPedidoId(pedido.getId());
|
||||
boolean hasDenegadoPago = lineas.stream()
|
||||
.anyMatch(linea -> PedidoLinea.Estado.denegado_pago.equals(linea.getEstado()));
|
||||
if (hasDenegadoPago) {
|
||||
data += " <span class='badge bg-danger btn-pay' data-amount='" + (int) (pedido.getTotal() * 100)
|
||||
+ "' data-id='" + pedido.getId() + "' style='cursor: pointer;'>"
|
||||
+ messageSource.getMessage("app.pay", null, locale) + "</span>";
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.where(base)
|
||||
.toJson(total);
|
||||
@ -168,8 +212,17 @@ public class PedidosController {
|
||||
|
||||
@GetMapping("/view/{id}")
|
||||
public String verPedido(
|
||||
@PathVariable(name = "id", required = true) Long id,
|
||||
Model model, Locale locale) {
|
||||
@PathVariable(name = "id", required = true) Long id,
|
||||
Model model, Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"app.cancelar",
|
||||
"app.yes",
|
||||
"pedido.view.cancel-title",
|
||||
"pedido.view.cancel-text");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
Boolean isAdmin = Utils.isCurrentUserAdmin();
|
||||
if (isAdmin) {
|
||||
@ -177,10 +230,225 @@ public class PedidosController {
|
||||
} else {
|
||||
model.addAttribute("isAdmin", false);
|
||||
}
|
||||
|
||||
PedidoDireccion direccionFacturacion = pedidoService.getDireccionFacturacionPedido(id);
|
||||
if (direccionFacturacion != null) {
|
||||
String paisNombre = paisesService.getPaisNombrePorCode3(direccionFacturacion.getPaisCode3(), locale);
|
||||
direccionFacturacion.setPaisNombre(paisNombre);
|
||||
}
|
||||
|
||||
model.addAttribute("direccionFacturacion", direccionFacturacion);
|
||||
|
||||
Boolean showCancel = false;
|
||||
Boolean showDownloadFactura = true;
|
||||
List<Map<String, Object>> lineas = pedidoService.getLineas(id, locale);
|
||||
for (Map<String, Object> linea : lineas) {
|
||||
|
||||
PedidoLinea pedidoLinea = repoPedidoLinea.findById(
|
||||
((Number) linea.get("lineaId")).longValue()).orElse(null);
|
||||
if (pedidoLinea != null) {
|
||||
Map<String, Boolean> buttons = new HashMap<>();
|
||||
if (pedidoLinea.getEstado() != PedidoLinea.Estado.enviado) {
|
||||
showDownloadFactura = false;
|
||||
}
|
||||
if (pedidoLinea.getEstado().getPriority() >= PedidoLinea.Estado.esperando_aceptacion_ferro.getPriority()
|
||||
&& pedidoLinea.getEstado().getPriority() <= PedidoLinea.Estado.produccion.getPriority()) {
|
||||
|
||||
if (pedidoLinea.getEstado() == PedidoLinea.Estado.esperando_aceptacion_ferro) {
|
||||
buttons.put("aceptar_ferro", true);
|
||||
} else {
|
||||
buttons.put("aceptar_ferro", false);
|
||||
}
|
||||
|
||||
Map<String, Object> filesType = pedidoService.getFilesType(pedidoLinea.getId(), locale);
|
||||
if (filesType == null || filesType.get("error") != null) {
|
||||
throw new RuntimeException(
|
||||
messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
||||
}
|
||||
for (String key : filesType.keySet()) {
|
||||
buttons.put(key, (Integer) filesType.get(key) == 1 ? true : false);
|
||||
}
|
||||
linea.put("buttons", buttons);
|
||||
}
|
||||
|
||||
if (pedidoLinea.getEstado() != PedidoLinea.Estado.cancelado
|
||||
&& pedidoLinea.getEstado() != PedidoLinea.Estado.terminado
|
||||
&& pedidoLinea.getEstado() != PedidoLinea.Estado.enviado) {
|
||||
showCancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
List<PedidoDireccion> dirEntrega = pedidoService.getDireccionesEntregaPedidoLinea(
|
||||
((Number) linea.get("lineaId")).longValue());
|
||||
|
||||
if (dirEntrega != null && !dirEntrega.isEmpty()) {
|
||||
for (PedidoDireccion direccion : dirEntrega) {
|
||||
String paisNombre = paisesService.getPaisNombrePorCode3(direccion.getPaisCode3(), locale);
|
||||
direccion.setPaisNombre(paisNombre);
|
||||
}
|
||||
}
|
||||
linea.put("direccionesEntrega", dirEntrega);
|
||||
|
||||
}
|
||||
Long facturaId = null;
|
||||
if (showDownloadFactura) {
|
||||
facturaId = facturacionService.getFacturaIdFromPedidoId(id);
|
||||
}
|
||||
model.addAttribute("lineas", lineas);
|
||||
model.addAttribute("showCancel", showCancel);
|
||||
if (showDownloadFactura && facturaId != null) {
|
||||
model.addAttribute("facturaId", facturaId);
|
||||
model.addAttribute("showDownloadFactura", showDownloadFactura);
|
||||
}
|
||||
model.addAttribute("id", id);
|
||||
return "imprimelibros/pedidos/pedidos-view";
|
||||
}
|
||||
|
||||
}
|
||||
@PostMapping("/cancel/{id}")
|
||||
@ResponseBody
|
||||
public Map<String, Object> cancelPedido(
|
||||
@PathVariable(name = "id", required = true) Long id,
|
||||
Locale locale) {
|
||||
Boolean result = pedidoService.cancelarPedido(id);
|
||||
if (result) {
|
||||
String successMsg = messageSource.getMessage("pedido.success.pedido-cancelado", null, locale);
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", successMsg);
|
||||
} else {
|
||||
String errorMsg = messageSource.getMessage("pedido.errors.cancel-pedido", null, locale);
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
// Acciones sobre las lineas de pedido
|
||||
// -------------------------------------
|
||||
@PostMapping("/linea/{id}/update-status")
|
||||
@ResponseBody
|
||||
public Map<String, Object> updateStatus(
|
||||
@PathVariable(name = "id", required = true) Long id, Locale locale) {
|
||||
|
||||
Map<String, Object> result = pedidoService.actualizarEstado(id, locale);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@PostMapping("/linea/{id}/update-maquetacion")
|
||||
@ResponseBody
|
||||
public Map<String, Object> updateMaquetacion(
|
||||
@PathVariable(name = "id", required = true) Long id,
|
||||
Locale locale) {
|
||||
|
||||
PedidoLinea entity = repoPedidoLinea.findById(id).orElse(null);
|
||||
if (entity == null) {
|
||||
String errorMsg = messageSource.getMessage("pedido.errors.linea-not-found", null, locale);
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", errorMsg);
|
||||
}
|
||||
|
||||
if (entity.getEstado() != PedidoLinea.Estado.maquetacion) {
|
||||
String errorMsg = messageSource.getMessage("pedido.errors.state-error", null, locale);
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", errorMsg);
|
||||
}
|
||||
|
||||
entity.setEstado(PedidoLinea.Estado.haciendo_ferro);
|
||||
repoPedidoLinea.save(entity);
|
||||
String successMsg = messageSource.getMessage("pedido.success.estado-actualizado", null, locale);
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", successMsg,
|
||||
"state", messageSource.getMessage(entity.getEstado().getMessageKey(), null, locale));
|
||||
}
|
||||
|
||||
@GetMapping("/linea/{id}/download-ferro")
|
||||
public ResponseEntity<Resource> downloadFerro(@PathVariable(name = "id", required = true) Long id, Locale locale) {
|
||||
|
||||
byte[] ferroFileContent = pedidoService.getFerroFileContent(id, locale);
|
||||
if (ferroFileContent == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
ByteArrayResource resource = new ByteArrayResource(ferroFileContent);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=ferro_" + id + ".pdf")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
@GetMapping("/linea/{id}/download-cub")
|
||||
public ResponseEntity<Resource> downloadCubierta(@PathVariable(name = "id", required = true) Long id,
|
||||
Locale locale) {
|
||||
|
||||
byte[] cubFileContent = pedidoService.getCubiertaFileContent(id, locale);
|
||||
if (cubFileContent == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
ByteArrayResource resource = new ByteArrayResource(cubFileContent);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=cubierta_" + id + ".pdf")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
@GetMapping("/linea/{id}/download-tapa")
|
||||
public ResponseEntity<Resource> downloadTapa(@PathVariable(name = "id", required = true) Long id, Locale locale) {
|
||||
|
||||
byte[] tapaFileContent = pedidoService.getTapaFileContent(id, locale);
|
||||
if (tapaFileContent == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
ByteArrayResource resource = new ByteArrayResource(tapaFileContent);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=tapa_" + id + ".pdf")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
@PostMapping("/linea/{id}/aceptar-ferro")
|
||||
@ResponseBody
|
||||
public Map<String, Object> aceptarFerro(@PathVariable(name = "id", required = true) Long id,
|
||||
Locale locale) {
|
||||
|
||||
PedidoLinea entity = repoPedidoLinea.findById(id).orElse(null);
|
||||
if (entity == null) {
|
||||
String errorMsg = messageSource.getMessage("pedido.errors.linea-not-found", null, locale);
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", errorMsg);
|
||||
}
|
||||
|
||||
if (entity.getEstado() != PedidoLinea.Estado.esperando_aceptacion_ferro) {
|
||||
String errorMsg = messageSource.getMessage("pedido.errors.state-error", null, locale);
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", errorMsg);
|
||||
}
|
||||
|
||||
Boolean result = pedidoService.aceptarFerro(id, locale);
|
||||
|
||||
if (result) {
|
||||
String successMsg = messageSource.getMessage("pedido.success.estado-actualizado", null, locale);
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", successMsg,
|
||||
"state", messageSource.getMessage(entity.getEstado().getMessageKey(), null, locale));
|
||||
} else {
|
||||
String errorMsg = messageSource.getMessage("pedido.errors.update-server-error", null, locale);
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -14,6 +14,8 @@ import java.util.Optional;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -63,6 +65,8 @@ import jakarta.validation.Valid;
|
||||
@RequestMapping("/presupuesto")
|
||||
public class PresupuestoController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PresupuestoController.class);
|
||||
|
||||
private final PresupuestoRepository presupuestoRepository;
|
||||
|
||||
@Autowired
|
||||
@ -147,7 +151,9 @@ public class PresupuestoController {
|
||||
return ResponseEntity.badRequest().body(errores);
|
||||
}
|
||||
Map<String, Object> resultado = new HashMap<>();
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
|
||||
Map<String , Object> datosInterior = apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale);
|
||||
resultado.put("solapas", datosInterior.get("maxSolapas"));
|
||||
resultado.put("lomo", datosInterior.get("lomo"));
|
||||
resultado.putAll(presupuestoService.obtenerOpcionesAcabadosCubierta(presupuesto, locale));
|
||||
return ResponseEntity.ok(resultado);
|
||||
}
|
||||
@ -267,7 +273,10 @@ public class PresupuestoController {
|
||||
}
|
||||
}
|
||||
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
|
||||
Map<String , Object> datosInterior = apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale);
|
||||
resultado.put("solapas", datosInterior.get("maxSolapas"));
|
||||
resultado.put("lomo", datosInterior.get("lomo"));
|
||||
|
||||
return ResponseEntity.ok(resultado);
|
||||
}
|
||||
|
||||
@ -300,7 +309,10 @@ public class PresupuestoController {
|
||||
presupuesto.setGramajeInterior(Integer.parseInt(opciones.get(0))); // Asignar primera opción
|
||||
}
|
||||
}
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
|
||||
Map<String , Object> datosInterior = apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale);
|
||||
resultado.put("solapas", datosInterior.get("maxSolapas"));
|
||||
resultado.put("lomo", datosInterior.get("lomo"));
|
||||
|
||||
return ResponseEntity.ok(resultado);
|
||||
}
|
||||
|
||||
@ -323,7 +335,10 @@ public class PresupuestoController {
|
||||
}
|
||||
|
||||
Map<String, Object> resultado = new HashMap<>();
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
|
||||
Map<String , Object> datosInterior = apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale);
|
||||
resultado.put("solapas", datosInterior.get("maxSolapas"));
|
||||
resultado.put("lomo", datosInterior.get("lomo"));
|
||||
|
||||
return ResponseEntity.ok(resultado);
|
||||
}
|
||||
|
||||
@ -492,7 +507,8 @@ public class PresupuestoController {
|
||||
String sessionId = request.getSession(true).getId();
|
||||
String ip = IpUtils.getClientIp(request);
|
||||
|
||||
var resumen = presupuestoService.getResumen(p, serviciosList, datosMaquetacion, datosMarcapaginas, save, mode, locale, sessionId, ip);
|
||||
var resumen = presupuestoService.getResumen(p, serviciosList, datosMaquetacion, datosMarcapaginas, save, mode,
|
||||
locale, sessionId, ip);
|
||||
|
||||
return ResponseEntity.ok(resumen);
|
||||
}
|
||||
@ -519,7 +535,27 @@ public class PresupuestoController {
|
||||
"presupuesto.add.cancel",
|
||||
"presupuesto.add.select-client",
|
||||
"presupuesto.add.error.options",
|
||||
"presupuesto.add.error.options-client");
|
||||
"presupuesto.add.error.options-client",
|
||||
"presupuesto.duplicar.title",
|
||||
"presupuesto.duplicar.text",
|
||||
"presupuesto.duplicar.confirm",
|
||||
"presupuesto.duplicar.cancelar",
|
||||
"presupuesto.duplicar.aceptar",
|
||||
"presupuesto.duplicar.required",
|
||||
"presupuesto.duplicar.success.title",
|
||||
"presupuesto.duplicar.success.text",
|
||||
"presupuesto.duplicar.error.title",
|
||||
"presupuesto.duplicar.error.internal",
|
||||
"presupuesto.reimprimir.title",
|
||||
"presupuesto.reimprimir.text",
|
||||
"presupuesto.reimprimir.confirm",
|
||||
"presupuesto.reimprimir.cancelar",
|
||||
"presupuesto.reimprimir.aceptar",
|
||||
"presupuesto.reimprimir.success.title",
|
||||
"presupuesto.reimprimir.success.text",
|
||||
"presupuesto.reimprimir.error.title",
|
||||
"presupuesto.reimprimir.error.internal"
|
||||
);
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
@ -543,7 +579,26 @@ public class PresupuestoController {
|
||||
"presupuesto.exito.guardado",
|
||||
"presupuesto.add.error.save.title",
|
||||
"presupuesto.iva-reducido",
|
||||
"presupuesto.iva-reducido-descripcion");
|
||||
"presupuesto.iva-reducido-descripcion",
|
||||
"presupuesto.duplicar.title",
|
||||
"presupuesto.duplicar.text",
|
||||
"presupuesto.duplicar.confirm",
|
||||
"presupuesto.duplicar.cancelar",
|
||||
"presupuesto.duplicar.aceptar",
|
||||
"presupuesto.duplicar.required",
|
||||
"presupuesto.duplicar.success.title",
|
||||
"presupuesto.duplicar.success.text",
|
||||
"presupuesto.duplicar.error.title",
|
||||
"presupuesto.duplicar.error.internal",
|
||||
"presupuesto.reimprimir.title",
|
||||
"presupuesto.reimprimir.text",
|
||||
"presupuesto.reimprimir.confirm",
|
||||
"presupuesto.reimprimir.cancelar",
|
||||
"presupuesto.reimprimir.aceptar",
|
||||
"presupuesto.reimprimir.success.title",
|
||||
"presupuesto.reimprimir.success.text",
|
||||
"presupuesto.reimprimir.error.title",
|
||||
"presupuesto.reimprimir.error.internal");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
@ -562,15 +617,15 @@ public class PresupuestoController {
|
||||
return "redirect:/presupuesto";
|
||||
}
|
||||
|
||||
if(presupuestoOpt.get().getEstado() == Presupuesto.Estado.aceptado){
|
||||
if (presupuestoOpt.get().getEstado() == Presupuesto.Estado.aceptado) {
|
||||
|
||||
Map<String, Object> resumen = presupuestoService.getTextosResumen(
|
||||
presupuestoOpt.get(),
|
||||
Utils.decodeJsonList(presupuestoOpt.get().getServiciosJson()),
|
||||
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMaquetacionJson()),
|
||||
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMarcapaginasJson()),
|
||||
locale);
|
||||
|
||||
presupuestoOpt.get(),
|
||||
Utils.decodeJsonList(presupuestoOpt.get().getServiciosJson()),
|
||||
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMaquetacionJson()),
|
||||
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMarcapaginasJson()),
|
||||
locale);
|
||||
|
||||
model.addAttribute("resumen", resumen);
|
||||
model.addAttribute("presupuesto", presupuestoOpt.get());
|
||||
return "imprimelibros/presupuestos/presupuestador-view";
|
||||
@ -595,6 +650,7 @@ public class PresupuestoController {
|
||||
model.addAttribute("appMode", "edit");
|
||||
}
|
||||
model.addAttribute("id", presupuestoOpt.get().getId());
|
||||
model.addAttribute("presupuesto", presupuestoOpt.get());
|
||||
return "imprimelibros/presupuestos/presupuesto-form";
|
||||
}
|
||||
|
||||
@ -772,6 +828,7 @@ public class PresupuestoController {
|
||||
return ResponseEntity.ok(Map.of("id", saveResult.get("presupuesto_id"),
|
||||
"message", messageSource.getMessage("presupuesto.exito.guardado", null, locale)));
|
||||
} catch (Exception ex) {
|
||||
log.error("Error al guardar el presupuesto", ex);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message",
|
||||
messageSource.getMessage("presupuesto.error.save-internal-error", null, locale),
|
||||
@ -780,4 +837,30 @@ public class PresupuestoController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/comentario")
|
||||
@ResponseBody
|
||||
public String actualizarComentario(@PathVariable Long id,
|
||||
@RequestParam String comentario) {
|
||||
presupuestoService.updateComentario(id, comentario);
|
||||
return "OK";
|
||||
}
|
||||
|
||||
@PostMapping("/api/duplicar/{id}")
|
||||
@ResponseBody
|
||||
public Map<String, Object> duplicarPresupuesto(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(name = "titulo", defaultValue = "") String titulo) {
|
||||
|
||||
Long entity = presupuestoService.duplicarPresupuesto(id, titulo);
|
||||
return Map.of("id", entity);
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/api/reimprimir/{id}")
|
||||
@ResponseBody
|
||||
public Map<String, Object> reimprimirPresupuesto(@PathVariable Long id) {
|
||||
|
||||
Long entity = presupuestoService.reimprimirPresupuesto(id);
|
||||
return Map.of("id", entity);
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ public class PresupuestoDatatableService {
|
||||
.addIf(publico, "ciudad", Presupuesto::getCiudad)
|
||||
.add("updatedAt", p -> formatDate(p.getUpdatedAt(), locale))
|
||||
.addIf(!publico, "user", p -> p.getUser() != null ? p.getUser().getFullName() : "")
|
||||
.add("actions", this::generarBotones)
|
||||
.add("actions", p -> generarBotones(p, locale))
|
||||
.where(base)
|
||||
.toJson(count);
|
||||
}
|
||||
@ -115,18 +115,27 @@ public class PresupuestoDatatableService {
|
||||
return df.format(instant);
|
||||
}
|
||||
|
||||
private String generarBotones(Presupuesto p) {
|
||||
private String generarBotones(Presupuesto p, Locale locale) {
|
||||
boolean borrador = p.getEstado() == Presupuesto.Estado.borrador;
|
||||
String id = String.valueOf(p.getId());
|
||||
String editBtn = "<a href=\"javascript:void(0);\" data-id=\"" + id + "\" class=\"link-success btn-edit-" +
|
||||
(p.getOrigen().equals(Presupuesto.Origen.publico) ? "anonimo" : "privado") + " fs-15\"><i class=\"ri-" +
|
||||
(p.getOrigen().equals(Presupuesto.Origen.publico) || p.getEstado() == Presupuesto.Estado.aceptado ? "eye" : "pencil") + "-line\"></i></a>";
|
||||
(p.getOrigen().equals(Presupuesto.Origen.publico) || p.getEstado() == Presupuesto.Estado.aceptado ? "eye" : "pencil") + "-line\" " +
|
||||
"data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
|
||||
msg(p.getEstado() == Presupuesto.Estado.aceptado ? "presupuesto.ver" : "presupuesto.editar", locale) + "\"></i></a>";
|
||||
|
||||
String duplicarBtn = !p.getOrigen().equals(Presupuesto.Origen.publico) ? "<a href=\"javascript:void(0);\" data-id=\"" + id
|
||||
+ "\" class=\"link-success btn-duplicate-privado fs-15\"><i class=\"ri-file-copy-2-line\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
|
||||
msg("presupuesto.duplicar", locale) + "\"></i></a>" : "";
|
||||
String reimprimirBtn = p.getEstado() == Presupuesto.Estado.aceptado && !p.getOrigen().equals(Presupuesto.Origen.publico) ? "<a href=\"javascript:void(0);\" data-id=\"" + id
|
||||
+ "\" class=\"link-success btn-reprint-privado fs-15\"><i class=\"ri-printer-line\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
|
||||
msg("presupuesto.reimprimir", locale) + "\"></i></a>" : "";
|
||||
String deleteBtn = borrador ? "<a href=\"javascript:void(0);\" data-id=\"" + id
|
||||
+ "\" class=\"link-danger btn-delete-"
|
||||
+ (p.getOrigen().equals(Presupuesto.Origen.publico) ? "anonimo" : "privado")
|
||||
+ " fs-15\"><i class=\"ri-delete-bin-5-line\"></i></a>" : "";
|
||||
+ " fs-15\"><i class=\"ri-delete-bin-5-line\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
|
||||
msg("presupuesto.borrar", locale) + "\"></i></a>" : "";
|
||||
|
||||
return "<div class=\"hstack gap-3 flex-wrap\">" + editBtn + deleteBtn + "</div>";
|
||||
return "<div class=\"hstack gap-3 flex-wrap\">" + editBtn + duplicarBtn + reimprimirBtn + deleteBtn + "</div>";
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ public class PresupuestoPapeles {
|
||||
);
|
||||
|
||||
private static final Map<String, String> CABEZADA_COLOR_KEYS = Map.of(
|
||||
"NOCABE", "presupuesto.cabezada-sin-cabezada",
|
||||
"WHI", "presupuesto.cabezada-blanca",
|
||||
"GRE", "presupuesto.cabezada-verde",
|
||||
"BLUE", "presupuesto.cabezada-azul",
|
||||
|
||||
@ -351,6 +351,11 @@ public class PresupuestoService {
|
||||
body.put("interior", interior);
|
||||
body.put("cubierta", cubierta);
|
||||
body.put("guardas", null);
|
||||
// Para las reimpresiones
|
||||
if(presupuesto.getIsReimpresion() != null && presupuesto.getIsReimpresion()) {
|
||||
body.put("reimpresion", 1);
|
||||
body.put("iskn", presupuesto.getProveedorRef1());
|
||||
}
|
||||
if (presupuesto.getSobrecubierta()) {
|
||||
Map<String, Object> sobrecubierta = new HashMap<>();
|
||||
sobrecubierta.put("papel", presupuesto.getPapelSobrecubiertaId());
|
||||
@ -1023,6 +1028,7 @@ public class PresupuestoService {
|
||||
resumen.put("iva_importe_4", presupuesto.getIvaImporte4());
|
||||
resumen.put("iva_importe_21", presupuesto.getIvaImporte21());
|
||||
resumen.put("total_con_iva", presupuesto.getTotalConIva());
|
||||
resumen.put("isReimpresion", presupuesto.getIsReimpresion());
|
||||
|
||||
return resumen;
|
||||
}
|
||||
@ -1226,6 +1232,18 @@ public class PresupuestoService {
|
||||
HashMap<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
|
||||
Presupuesto presupuestoExistente = null;
|
||||
if (id != null) {
|
||||
presupuestoExistente = presupuestoRepository.findById(id).orElse(null);
|
||||
}
|
||||
if (presupuestoExistente != null) {
|
||||
// merge de datos que no están en el formulario
|
||||
presupuesto.setIsReimpresion(presupuestoExistente.getIsReimpresion());
|
||||
presupuesto.setProveedor(presupuestoExistente.getProveedor());
|
||||
presupuesto.setProveedorRef1(presupuestoExistente.getProveedorRef1());
|
||||
presupuesto.setProveedorRef2(presupuestoExistente.getProveedorRef2());
|
||||
}
|
||||
|
||||
presupuesto.setDatosMaquetacionJson(
|
||||
datosMaquetacion != null ? new ObjectMapper().writeValueAsString(datosMaquetacion) : null);
|
||||
presupuesto.setDatosMarcapaginasJson(
|
||||
@ -1357,6 +1375,51 @@ public class PresupuestoService {
|
||||
return resumen;
|
||||
}
|
||||
|
||||
public Presupuesto findPresupuestoById(Long id) {
|
||||
return presupuestoRepository.findById(id).orElse(null);
|
||||
}
|
||||
|
||||
public void updateComentario(Long presupuestoId, String comentario) {
|
||||
Presupuesto presupuesto = presupuestoRepository.findById(presupuestoId).orElse(null);
|
||||
if (presupuesto != null) {
|
||||
presupuesto.setComentario(comentario);
|
||||
presupuestoRepository.saveAndFlush(presupuesto);
|
||||
}
|
||||
}
|
||||
|
||||
public long duplicarPresupuesto(Long presupuestoId, String titulo) {
|
||||
|
||||
Presupuesto presupuesto = presupuestoRepository.findById(presupuestoId).orElse(null);
|
||||
if (presupuesto != null) {
|
||||
Presupuesto nuevo = presupuesto.clone();
|
||||
nuevo.setId(null); // para que se genere uno nuevo
|
||||
nuevo.setEstado(Presupuesto.Estado.borrador);
|
||||
nuevo.setTitulo(titulo != null && !titulo.isEmpty() ? titulo : "[D] " + presupuesto.getTitulo());
|
||||
nuevo.setIsReimpresion(false);
|
||||
nuevo.setProveedor(null);
|
||||
nuevo.setProveedorRef1(null);
|
||||
nuevo.setProveedorRef2(null);
|
||||
presupuestoRepository.saveAndFlush(nuevo);
|
||||
return nuevo.getId();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public long reimprimirPresupuesto(Long presupuestoId) {
|
||||
|
||||
Presupuesto presupuesto = presupuestoRepository.findById(presupuestoId).orElse(null);
|
||||
if (presupuesto != null) {
|
||||
Presupuesto nuevo = presupuesto.clone();
|
||||
nuevo.setId(null); // para que se genere uno nuevo
|
||||
nuevo.setEstado(Presupuesto.Estado.borrador);
|
||||
nuevo.setTitulo("[R] " + presupuesto.getTitulo());
|
||||
nuevo.setIsReimpresion(true);
|
||||
presupuestoRepository.saveAndFlush(nuevo);
|
||||
return nuevo.getId();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// Métodos privados
|
||||
// =======================================================================
|
||||
|
||||
@ -3,6 +3,8 @@ package com.imprimelibros.erp.redsys;
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.payments.PaymentService;
|
||||
import com.imprimelibros.erp.payments.model.Payment;
|
||||
import com.imprimelibros.erp.pedidos.Pedido;
|
||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||
import com.imprimelibros.erp.redsys.RedsysService.FormPayload;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
@ -27,6 +29,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Controller
|
||||
@ -37,13 +40,16 @@ public class RedsysController {
|
||||
private final MessageSource messageSource;
|
||||
private final SpringTemplateEngine templateEngine;
|
||||
private final ServletContext servletContext;
|
||||
private final PedidoService pedidoService;
|
||||
|
||||
public RedsysController(PaymentService paymentService, MessageSource messageSource,
|
||||
SpringTemplateEngine templateEngine, ServletContext servletContext) {
|
||||
SpringTemplateEngine templateEngine, ServletContext servletContext,
|
||||
PedidoService pedidoService) {
|
||||
this.paymentService = paymentService;
|
||||
this.messageSource = messageSource;
|
||||
this.templateEngine = templateEngine;
|
||||
this.servletContext = servletContext;
|
||||
this.pedidoService = pedidoService;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/crear", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
@ -55,9 +61,16 @@ public class RedsysController {
|
||||
HttpServletResponse response, Locale locale)
|
||||
throws Exception {
|
||||
|
||||
// Creamos el pedido inteno
|
||||
Pedido order = pedidoService.crearPedido(cartId, dirFactId, null, null);
|
||||
|
||||
if ("bank-transfer".equalsIgnoreCase(method)) {
|
||||
|
||||
// 1) Creamos el Payment interno SIN orderId (null)
|
||||
Payment p = paymentService.createBankTransferPayment(cartId, dirFactId, amountCents, "EUR");
|
||||
Payment p = paymentService.createBankTransferPayment(cartId, dirFactId, amountCents, "EUR", locale,
|
||||
order.getId());
|
||||
|
||||
pedidoService.markPedidoAsProcesingPayment(order.getId());
|
||||
|
||||
// 1️⃣ Crear la "aplicación" web de Thymeleaf (Jakarta)
|
||||
JakartaServletWebApplication app = JakartaServletWebApplication.buildApplication(servletContext);
|
||||
@ -89,7 +102,104 @@ public class RedsysController {
|
||||
}
|
||||
|
||||
// Tarjeta o Bizum (Redsys)
|
||||
FormPayload form = paymentService.createRedsysPayment(cartId, dirFactId, amountCents, "EUR", method);
|
||||
FormPayload form = paymentService.createRedsysPayment(cartId, dirFactId, amountCents, "EUR", method,
|
||||
order.getId());
|
||||
|
||||
String html = """
|
||||
<html><head><meta charset="utf-8"><title>Redirigiendo a Redsys…</title></head>
|
||||
<body onload="document.forms[0].submit()">
|
||||
<form action="%s" method="post">
|
||||
<input type="hidden" name="Ds_SignatureVersion" value="%s"/>
|
||||
<input type="hidden" name="Ds_MerchantParameters" value="%s"/>
|
||||
<input type="hidden" name="Ds_Signature" value="%s"/>
|
||||
<input type="hidden" name="cartId" value="%d"/>
|
||||
<noscript>
|
||||
<p>Haz clic en pagar para continuar</p>
|
||||
<button type="submit">Pagar</button>
|
||||
</noscript>
|
||||
</form>
|
||||
</body></html>
|
||||
""".formatted(
|
||||
form.action(),
|
||||
form.signatureVersion(),
|
||||
form.merchantParameters(),
|
||||
form.signature(), cartId);
|
||||
|
||||
byte[] body = html.getBytes(StandardCharsets.UTF_8);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.TEXT_HTML)
|
||||
.body(body);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/reintentar", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
@ResponseBody
|
||||
public ResponseEntity<byte[]> reintentarPago(@RequestParam("amountCents") Long amountCents,
|
||||
@RequestParam("method") String method, @RequestParam("orderId") Long orderId,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response, Locale locale)
|
||||
throws Exception {
|
||||
|
||||
// Creamos el pedido inteno
|
||||
Pedido order = pedidoService.findById(orderId);
|
||||
|
||||
// Find the payment with orderId = order.getId() and status = failed
|
||||
Payment failedPayment = paymentService.findFailedPaymentByOrderId(order.getId());
|
||||
if (failedPayment == null) {
|
||||
throw new Exception("No se encontró un pago fallido para el pedido " + order.getId());
|
||||
}
|
||||
|
||||
Long cartId = null;
|
||||
Long dirFactId = null;
|
||||
// Find payment transaction details from failedPayment if needed
|
||||
try {
|
||||
Map<String, Long> transactionDetails = paymentService.getPaymentTransactionData(failedPayment.getId());
|
||||
cartId = transactionDetails.get("cartId");
|
||||
dirFactId = transactionDetails.get("dirFactId");
|
||||
} catch (Exception e) {
|
||||
throw new Exception(
|
||||
"No se pudieron obtener los detalles de la transacción para el pago " + failedPayment.getId());
|
||||
}
|
||||
|
||||
if ("bank-transfer".equalsIgnoreCase(method)) {
|
||||
|
||||
// 1) Creamos el Payment interno SIN orderId (null)
|
||||
Payment p = paymentService.createBankTransferPayment(cartId, dirFactId, amountCents, "EUR", locale,
|
||||
order.getId());
|
||||
|
||||
pedidoService.markPedidoAsProcesingPayment(order.getId());
|
||||
|
||||
// 1️⃣ Crear la "aplicación" web de Thymeleaf (Jakarta)
|
||||
JakartaServletWebApplication app = JakartaServletWebApplication.buildApplication(servletContext);
|
||||
|
||||
// 2️⃣ Construir el intercambio web desde request/response
|
||||
response.setContentType("text/html;charset=UTF-8");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
IWebExchange exchange = app.buildExchange(request, response);
|
||||
|
||||
// 3️⃣ Crear el contexto WebContext con Locale
|
||||
WebContext ctx = new WebContext(exchange, locale);
|
||||
|
||||
String importeFormateado = Utils.formatCurrency(amountCents / 100.0, locale);
|
||||
ctx.setVariable("importe", importeFormateado);
|
||||
ctx.setVariable("concepto", "TRANSF-" + p.getOrderId());
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
boolean isAuth = auth != null
|
||||
&& auth.isAuthenticated()
|
||||
&& !(auth instanceof AnonymousAuthenticationToken);
|
||||
ctx.setVariable("isAuth", isAuth);
|
||||
|
||||
// 3) Renderizamos la plantilla a HTML
|
||||
String html = templateEngine.process("imprimelibros/pagos/transfer", ctx);
|
||||
|
||||
byte[] body = html.getBytes(StandardCharsets.UTF_8);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.TEXT_HTML)
|
||||
.body(body);
|
||||
}
|
||||
|
||||
// Tarjeta o Bizum (Redsys)
|
||||
FormPayload form = paymentService.createRedsysPayment(cartId, dirFactId, amountCents, "EUR", method,
|
||||
order.getId());
|
||||
|
||||
String html = """
|
||||
<html><head><meta charset="utf-8"><title>Redirigiendo a Redsys…</title></head>
|
||||
|
||||
@ -359,6 +359,7 @@ public class UserController {
|
||||
@GetMapping(value = "api/get-users", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Map<String, Object> getUsers(
|
||||
@RequestParam(required = false) String role, // puede venir ausente
|
||||
@RequestParam(required = false) Boolean showUsername,
|
||||
@RequestParam(required = false) String q,
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int size) {
|
||||
@ -373,9 +374,15 @@ public class UserController {
|
||||
.map(u -> {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", u.getId());
|
||||
m.put("text", (u.getFullName() != null && !u.getFullName().isBlank())
|
||||
? u.getFullName()
|
||||
: u.getUserName());
|
||||
if (showUsername != null && Boolean.TRUE.equals(showUsername)) {
|
||||
m.put("text", (u.getFullName() != null && !u.getFullName().isBlank())
|
||||
? u.getFullName() + " (" + u.getUserName() + ")"
|
||||
: u.getUserName());
|
||||
} else {
|
||||
m.put("text", (u.getFullName() != null && !u.getFullName().isBlank())
|
||||
? u.getFullName()
|
||||
: u.getUserName());
|
||||
}
|
||||
return m;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
@ -385,4 +392,20 @@ public class UserController {
|
||||
"pagination", Map.of("more", more));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping(value = "api/get-user/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Map<String, Object> getUser(@PathVariable Long id) {
|
||||
User u = userService.findById(id);
|
||||
if (u == null) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", u.getId());
|
||||
m.put("userName", u.getUserName());
|
||||
m.put("fullName", u.getFullName());
|
||||
return m;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -18,6 +18,8 @@ server.error.include-binding-errors=never
|
||||
# Opcional: desactivar Whitelabel y servir tu propia página de error
|
||||
server.error.whitelabel.enabled=false
|
||||
|
||||
# Servelet options
|
||||
server.servlet.context-path=/intranet
|
||||
|
||||
# Archivo principal dentro del contenedor (monta /var/log/imprimelibros como volumen)
|
||||
logging.file.name=/var/log/imprimelibros/erp.log
|
||||
@ -42,6 +44,6 @@ safekat.api.password=Safekat2024
|
||||
redsys.environment=test
|
||||
redsys.url=https://sis-t.redsys.es:25443/sis/realizarPago
|
||||
redsys.refund.url=https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST
|
||||
redsys.urls.ok=https://imprimelibros.jjimenez.eu/pagos/redsys/ok
|
||||
redsys.urls.ko=https://imprimelibros.jjimenez.eu/pagos/redsys/ko
|
||||
redsys.urls.notify=https://imprimelibros.jjimenez.eu/pagos/redsys/notify
|
||||
redsys.urls.ok=https://app.imprimelibros.com/intranet/pagos/redsys/ok
|
||||
redsys.urls.ko=https://app.imprimelibros.com/intranet/pagos/redsys/ko
|
||||
redsys.urls.notify=https://app.imprimelibros.com/intranet/pagos/redsys/notify
|
||||
@ -1,7 +1,7 @@
|
||||
spring.application.name=erp
|
||||
# Active profile
|
||||
spring.profiles.active=dev
|
||||
#spring.profiles.active=test
|
||||
#spring.profiles.active=dev
|
||||
spring.profiles.active=test
|
||||
#spring.profiles.active=prod
|
||||
|
||||
|
||||
|
||||
@ -539,13 +539,13 @@ databaseChangeLog:
|
||||
- column:
|
||||
constraints:
|
||||
nullable: false
|
||||
defaultValueComputed: CURRENT_TIMESTAMP(3)
|
||||
defaultValueComputed: CURRENT_TIMESTAMP
|
||||
name: created_at
|
||||
type: datetime
|
||||
- column:
|
||||
constraints:
|
||||
nullable: false
|
||||
defaultValueComputed: CURRENT_TIMESTAMP(3) on update CURRENT_TIMESTAMP(3)
|
||||
defaultValueComputed: CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP
|
||||
name: updated_at
|
||||
type: datetime
|
||||
- column:
|
||||
@ -573,6 +573,7 @@ databaseChangeLog:
|
||||
name: iva_reducido
|
||||
type: BIT(1)
|
||||
tableName: presupuesto
|
||||
|
||||
- changeSet:
|
||||
id: 1761213112413-10
|
||||
author: jjimenez (generated)
|
||||
@ -840,7 +841,7 @@ databaseChangeLog:
|
||||
associatedWith: ''
|
||||
columns:
|
||||
- column:
|
||||
defaultValueNumeric: !!float '0'
|
||||
defaultValueNumeric: 0
|
||||
name: deleted
|
||||
indexName: idx_presupuesto_deleted
|
||||
tableName: presupuesto
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 0018-change-presupuesto-ch-3
|
||||
author: jjo
|
||||
|
||||
preConditions:
|
||||
- onFail: MARK_RAN
|
||||
- onError: HALT
|
||||
- dbms:
|
||||
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:
|
||||
- sql:
|
||||
dbms: mysql
|
||||
splitStatements: false
|
||||
stripComments: true
|
||||
sql: |
|
||||
ALTER TABLE presupuesto
|
||||
DROP CHECK presupuesto_chk_3;
|
||||
|
||||
rollback:
|
||||
- sql:
|
||||
dbms: mysql
|
||||
splitStatements: false
|
||||
stripComments: true
|
||||
sql: |
|
||||
ALTER TABLE presupuesto
|
||||
ADD CONSTRAINT presupuesto_chk_3
|
||||
CHECK (tipo_cubierta BETWEEN 0 AND 2);
|
||||
@ -0,0 +1,32 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 0019-add-estados-pago-to-pedidos-lineas
|
||||
author: jjo
|
||||
changes:
|
||||
- modifyDataType:
|
||||
tableName: pedidos_lineas
|
||||
columnName: estado
|
||||
newDataType: >
|
||||
enum(
|
||||
'pendiente_pago',
|
||||
'procesando_pago',
|
||||
'aprobado',
|
||||
'maquetacion',
|
||||
'haciendo_ferro',
|
||||
'produccion',
|
||||
'terminado',
|
||||
'cancelado'
|
||||
)
|
||||
rollback:
|
||||
- modifyDataType:
|
||||
tableName: pedidos_lineas
|
||||
columnName: estado
|
||||
newDataType: >
|
||||
enum(
|
||||
'aprobado',
|
||||
'maquetacion',
|
||||
'haciendo_ferro',
|
||||
'produccion',
|
||||
'terminado',
|
||||
'cancelado'
|
||||
)
|
||||
@ -0,0 +1,35 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 0020-add-estados-pago-to-pedidos-lineas-2
|
||||
author: jjo
|
||||
changes:
|
||||
- modifyDataType:
|
||||
tableName: pedidos_lineas
|
||||
columnName: estado
|
||||
newDataType: >
|
||||
enum(
|
||||
'pendiente_pago',
|
||||
'procesando_pago',
|
||||
'denegado_pago',
|
||||
'aprobado',
|
||||
'maquetacion',
|
||||
'haciendo_ferro',
|
||||
'produccion',
|
||||
'terminado',
|
||||
'cancelado'
|
||||
)
|
||||
rollback:
|
||||
- modifyDataType:
|
||||
tableName: pedidos_lineas
|
||||
columnName: estado
|
||||
newDataType: >
|
||||
enum(
|
||||
'pendiente_pago',
|
||||
'procesando_pago',
|
||||
'aprobado',
|
||||
'maquetacion',
|
||||
'haciendo_ferro',
|
||||
'produccion',
|
||||
'terminado',
|
||||
'cancelado'
|
||||
)
|
||||
@ -0,0 +1,23 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 0021-add-email-and-is-palets-to-pedidos-direcciones
|
||||
author: jjo
|
||||
changes:
|
||||
- sql:
|
||||
dbms: mysql
|
||||
splitStatements: false
|
||||
stripComments: true
|
||||
sql: >
|
||||
ALTER TABLE pedidos_direcciones
|
||||
ADD COLUMN is_palets TINYINT(1) NOT NULL DEFAULT 0 AFTER identificacion_fiscal,
|
||||
ADD COLUMN email VARCHAR(255) NULL AFTER is_ejemplar_prueba;
|
||||
|
||||
rollback:
|
||||
- sql:
|
||||
dbms: mysql
|
||||
splitStatements: false
|
||||
stripComments: true
|
||||
sql: >
|
||||
ALTER TABLE pedidos_direcciones
|
||||
DROP COLUMN is_palets,
|
||||
DROP COLUMN email;
|
||||
@ -0,0 +1,37 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 0022-add-estados-pago-to-pedidos-lineas-3
|
||||
author: jjo
|
||||
changes:
|
||||
- modifyDataType:
|
||||
tableName: pedidos_lineas
|
||||
columnName: estado
|
||||
newDataType: >
|
||||
enum(
|
||||
'pendiente_pago',
|
||||
'procesando_pago',
|
||||
'denegado_pago',
|
||||
'aprobado',
|
||||
'maquetacion',
|
||||
'haciendo_ferro',
|
||||
'esperando_aceptacion_ferro',
|
||||
'produccion',
|
||||
'terminado',
|
||||
'cancelado'
|
||||
)
|
||||
rollback:
|
||||
- modifyDataType:
|
||||
tableName: pedidos_lineas
|
||||
columnName: estado
|
||||
newDataType: >
|
||||
enum(
|
||||
'pendiente_pago',
|
||||
'procesando_pago',
|
||||
'denegado_pago',
|
||||
'aprobado',
|
||||
'maquetacion',
|
||||
'haciendo_ferro',
|
||||
'produccion',
|
||||
'terminado',
|
||||
'cancelado'
|
||||
)
|
||||
407
src/main/resources/db/changelog/changesets/0023-facturacion.yml
Normal file
407
src/main/resources/db/changelog/changesets/0023-facturacion.yml
Normal file
@ -0,0 +1,407 @@
|
||||
databaseChangeLog:
|
||||
|
||||
- changeSet:
|
||||
id: 20251230-01-pedidos-lineas-enviado
|
||||
author: jjo
|
||||
changes:
|
||||
- modifyDataType:
|
||||
tableName: pedidos_lineas
|
||||
columnName: estado
|
||||
newDataType: >
|
||||
ENUM(
|
||||
'pendiente_pago',
|
||||
'procesando_pago',
|
||||
'denegado_pago',
|
||||
'aprobado',
|
||||
'maquetacion',
|
||||
'haciendo_ferro',
|
||||
'esperando_aceptacion_ferro',
|
||||
'produccion',
|
||||
'terminado',
|
||||
'enviado',
|
||||
'cancelado'
|
||||
)
|
||||
rollback:
|
||||
- modifyDataType:
|
||||
tableName: pedidos_lineas
|
||||
columnName: estado
|
||||
newDataType: >
|
||||
ENUM(
|
||||
'pendiente_pago',
|
||||
'procesando_pago',
|
||||
'denegado_pago',
|
||||
'aprobado',
|
||||
'maquetacion',
|
||||
'haciendo_ferro',
|
||||
'esperando_aceptacion_ferro',
|
||||
'produccion',
|
||||
'terminado',
|
||||
'cancelado'
|
||||
)
|
||||
|
||||
# -------------------------------------------------
|
||||
|
||||
- changeSet:
|
||||
id: 20251230-02-series-facturas
|
||||
author: jjo
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: series_facturas
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: BIGINT
|
||||
autoIncrement: true
|
||||
constraints:
|
||||
primaryKey: true
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: nombre_serie
|
||||
type: VARCHAR(100)
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: prefijo
|
||||
type: VARCHAR(10)
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: tipo
|
||||
type: ENUM('facturacion')
|
||||
defaultValue: facturacion
|
||||
|
||||
- column:
|
||||
name: numero_actual
|
||||
type: BIGINT
|
||||
defaultValueNumeric: 1
|
||||
|
||||
- column:
|
||||
name: created_at
|
||||
type: TIMESTAMP
|
||||
- column:
|
||||
name: updated_at
|
||||
type: TIMESTAMP
|
||||
- column:
|
||||
name: deleted_at
|
||||
type: TIMESTAMP
|
||||
|
||||
- column:
|
||||
name: created_by
|
||||
type: BIGINT
|
||||
- column:
|
||||
name: updated_by
|
||||
type: BIGINT
|
||||
- column:
|
||||
name: deleted_by
|
||||
type: BIGINT
|
||||
|
||||
- addForeignKeyConstraint:
|
||||
constraintName: fk_series_facturas_created_by
|
||||
baseTableName: series_facturas
|
||||
baseColumnNames: created_by
|
||||
referencedTableName: users
|
||||
referencedColumnNames: id
|
||||
onDelete: SET NULL
|
||||
|
||||
- addForeignKeyConstraint:
|
||||
constraintName: fk_series_facturas_updated_by
|
||||
baseTableName: series_facturas
|
||||
baseColumnNames: updated_by
|
||||
referencedTableName: users
|
||||
referencedColumnNames: id
|
||||
onDelete: SET NULL
|
||||
|
||||
- addForeignKeyConstraint:
|
||||
constraintName: fk_series_facturas_deleted_by
|
||||
baseTableName: series_facturas
|
||||
baseColumnNames: deleted_by
|
||||
referencedTableName: users
|
||||
referencedColumnNames: id
|
||||
onDelete: SET NULL
|
||||
|
||||
rollback:
|
||||
- dropTable:
|
||||
tableName: series_facturas
|
||||
|
||||
# -------------------------------------------------
|
||||
|
||||
- changeSet:
|
||||
id: 20251230-03-facturas
|
||||
author: jjo
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: facturas
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: BIGINT
|
||||
autoIncrement: true
|
||||
constraints:
|
||||
primaryKey: true
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: pedido_id
|
||||
type: BIGINT
|
||||
|
||||
- column:
|
||||
name: factura_rectificada_id
|
||||
type: BIGINT
|
||||
|
||||
- column:
|
||||
name: factura_rectificativa_id
|
||||
type: BIGINT
|
||||
|
||||
- column:
|
||||
name: cliente_id
|
||||
type: BIGINT
|
||||
|
||||
- column:
|
||||
name: serie_id
|
||||
type: BIGINT
|
||||
|
||||
- column:
|
||||
name: numero_factura
|
||||
type: VARCHAR(50)
|
||||
|
||||
- column:
|
||||
name: estado
|
||||
type: ENUM('borrador','validada')
|
||||
defaultValue: borrador
|
||||
|
||||
- column:
|
||||
name: estado_pago
|
||||
type: ENUM('pendiente','pagada','cancelada')
|
||||
defaultValue: pendiente
|
||||
|
||||
- column:
|
||||
name: tipo_pago
|
||||
type: ENUM('tpv_tarjeta','tpv_bizum','transferencia','otros')
|
||||
defaultValue: otros
|
||||
|
||||
- column:
|
||||
name: fecha_emision
|
||||
type: DATETIME
|
||||
|
||||
- column:
|
||||
name: base_imponible
|
||||
type: DECIMAL(10,2)
|
||||
- column:
|
||||
name: iva_4
|
||||
type: DECIMAL(10,2)
|
||||
- column:
|
||||
name: iva_21
|
||||
type: DECIMAL(10,2)
|
||||
- column:
|
||||
name: total_factura
|
||||
type: DECIMAL(10,2)
|
||||
- column:
|
||||
name: total_pagado
|
||||
type: DECIMAL(10,2)
|
||||
defaultValueNumeric: 0.00
|
||||
|
||||
- column:
|
||||
name: notas
|
||||
type: TEXT
|
||||
|
||||
- column:
|
||||
name: created_at
|
||||
type: TIMESTAMP
|
||||
- column:
|
||||
name: updated_at
|
||||
type: TIMESTAMP
|
||||
- column:
|
||||
name: deleted_at
|
||||
type: TIMESTAMP
|
||||
|
||||
- column:
|
||||
name: created_by
|
||||
type: BIGINT
|
||||
- column:
|
||||
name: updated_by
|
||||
type: BIGINT
|
||||
- column:
|
||||
name: deleted_by
|
||||
type: BIGINT
|
||||
|
||||
- addUniqueConstraint:
|
||||
constraintName: uq_facturas_numero_factura
|
||||
tableName: facturas
|
||||
columnNames: numero_factura
|
||||
|
||||
- addForeignKeyConstraint:
|
||||
constraintName: fk_facturas_pedido
|
||||
baseTableName: facturas
|
||||
baseColumnNames: pedido_id
|
||||
referencedTableName: pedidos
|
||||
referencedColumnNames: id
|
||||
|
||||
- addForeignKeyConstraint:
|
||||
constraintName: fk_facturas_cliente
|
||||
baseTableName: facturas
|
||||
baseColumnNames: cliente_id
|
||||
referencedTableName: users
|
||||
referencedColumnNames: id
|
||||
|
||||
- addForeignKeyConstraint:
|
||||
constraintName: fk_facturas_serie
|
||||
baseTableName: facturas
|
||||
baseColumnNames: serie_id
|
||||
referencedTableName: series_facturas
|
||||
referencedColumnNames: id
|
||||
|
||||
- addForeignKeyConstraint:
|
||||
constraintName: fk_facturas_rectificada
|
||||
baseTableName: facturas
|
||||
baseColumnNames: factura_rectificada_id
|
||||
referencedTableName: facturas
|
||||
referencedColumnNames: id
|
||||
|
||||
- addForeignKeyConstraint:
|
||||
constraintName: fk_facturas_rectificativa
|
||||
baseTableName: facturas
|
||||
baseColumnNames: factura_rectificativa_id
|
||||
referencedTableName: facturas
|
||||
referencedColumnNames: id
|
||||
|
||||
rollback:
|
||||
- dropTable:
|
||||
tableName: facturas
|
||||
|
||||
# -------------------------------------------------
|
||||
|
||||
- changeSet:
|
||||
id: 20251230-04-facturas-lineas
|
||||
author: jjo
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: facturas_lineas
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: BIGINT
|
||||
autoIncrement: true
|
||||
constraints:
|
||||
primaryKey: true
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: factura_id
|
||||
type: BIGINT
|
||||
- column:
|
||||
name: descripcion
|
||||
type: TEXT
|
||||
- column:
|
||||
name: cantidad
|
||||
type: INT
|
||||
- column:
|
||||
name: base_linea
|
||||
type: DECIMAL(10,2)
|
||||
- column:
|
||||
name: iva_4_linea
|
||||
type: DECIMAL(10,2)
|
||||
- column:
|
||||
name: iva_21_linea
|
||||
type: DECIMAL(10,2)
|
||||
- column:
|
||||
name: total_linea
|
||||
type: DECIMAL(10,2)
|
||||
|
||||
- column:
|
||||
name: created_at
|
||||
type: TIMESTAMP
|
||||
- column:
|
||||
name: updated_at
|
||||
type: TIMESTAMP
|
||||
- column:
|
||||
name: deleted_at
|
||||
type: TIMESTAMP
|
||||
|
||||
- column:
|
||||
name: created_by
|
||||
type: BIGINT
|
||||
- column:
|
||||
name: updated_by
|
||||
type: BIGINT
|
||||
- column:
|
||||
name: deleted_by
|
||||
type: BIGINT
|
||||
|
||||
- addForeignKeyConstraint:
|
||||
constraintName: fk_facturas_lineas_factura
|
||||
baseTableName: facturas_lineas
|
||||
baseColumnNames: factura_id
|
||||
referencedTableName: facturas
|
||||
referencedColumnNames: id
|
||||
|
||||
rollback:
|
||||
- dropTable:
|
||||
tableName: facturas_lineas
|
||||
|
||||
# -------------------------------------------------
|
||||
|
||||
- changeSet:
|
||||
id: 20251230-05-facturas-pagos
|
||||
author: jjo
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: facturas_pagos
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: BIGINT
|
||||
autoIncrement: true
|
||||
constraints:
|
||||
primaryKey: true
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: factura_id
|
||||
type: BIGINT
|
||||
- column:
|
||||
name: metodo_pago
|
||||
type: ENUM('tpv_tarjeta','tpv_bizum','transferencia','otros')
|
||||
defaultValue: otros
|
||||
- column:
|
||||
name: cantidad_pagada
|
||||
type: DECIMAL(10,2)
|
||||
- column:
|
||||
name: fecha_pago
|
||||
type: DATETIME
|
||||
- column:
|
||||
name: notas
|
||||
type: TEXT
|
||||
|
||||
- column:
|
||||
name: created_at
|
||||
type: TIMESTAMP
|
||||
- column:
|
||||
name: updated_at
|
||||
type: TIMESTAMP
|
||||
- column:
|
||||
name: deleted_at
|
||||
type: TIMESTAMP
|
||||
|
||||
- column:
|
||||
name: created_by
|
||||
type: BIGINT
|
||||
- column:
|
||||
name: updated_by
|
||||
type: BIGINT
|
||||
- column:
|
||||
name: deleted_by
|
||||
type: BIGINT
|
||||
|
||||
- addForeignKeyConstraint:
|
||||
constraintName: fk_facturas_pagos_factura
|
||||
baseTableName: facturas_pagos
|
||||
baseColumnNames: factura_id
|
||||
referencedTableName: facturas
|
||||
referencedColumnNames: id
|
||||
|
||||
rollback:
|
||||
- dropTable:
|
||||
tableName: facturas_pagos
|
||||
@ -0,0 +1,67 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 0024-series-facturacion-seeder
|
||||
author: jjo
|
||||
context: demo
|
||||
changes:
|
||||
|
||||
# --- SERIES ---
|
||||
- sql:
|
||||
splitStatements: true
|
||||
stripComments: true
|
||||
sql: |
|
||||
INSERT INTO series_facturas
|
||||
(nombre_serie, prefijo, tipo, numero_actual, created_at, updated_at, created_by, updated_by)
|
||||
SELECT
|
||||
'IMPRESIÓN DIGITAL', 'IMPR', 'facturacion', 1, NOW(), NOW(), 1, 1
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM series_facturas WHERE prefijo = 'IMPR'
|
||||
);
|
||||
|
||||
INSERT INTO series_facturas
|
||||
(nombre_serie, prefijo, tipo, numero_actual, created_at, updated_at, created_by, updated_by)
|
||||
SELECT
|
||||
'RECT. IMPRESIÓN DIGITAL', 'REC IL', 'facturacion', 1, NOW(), NOW(), 1, 1
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM series_facturas WHERE prefijo = 'REC IL'
|
||||
);
|
||||
|
||||
# --- VARIABLES (con el id real de la serie) ---
|
||||
# serie_facturacion_default -> id de la serie con prefijo IMPR
|
||||
- sql:
|
||||
splitStatements: true
|
||||
stripComments: true
|
||||
sql: |
|
||||
INSERT INTO variables (clave, valor)
|
||||
SELECT
|
||||
'serie_facturacion_default',
|
||||
CAST(sf.id AS CHAR)
|
||||
FROM series_facturas sf
|
||||
WHERE sf.prefijo = 'IMPR'
|
||||
LIMIT 1
|
||||
ON DUPLICATE KEY UPDATE valor = VALUES(valor);
|
||||
|
||||
# sere_facturacion_rect_default -> id de la serie con prefijo REC IL
|
||||
- sql:
|
||||
splitStatements: true
|
||||
stripComments: true
|
||||
sql: |
|
||||
INSERT INTO variables (clave, valor)
|
||||
SELECT
|
||||
'serie_facturacion_rect_default',
|
||||
CAST(sf.id AS CHAR)
|
||||
FROM series_facturas sf
|
||||
WHERE sf.prefijo = 'REC IL'
|
||||
LIMIT 1
|
||||
ON DUPLICATE KEY UPDATE valor = VALUES(valor);
|
||||
|
||||
rollback:
|
||||
- sql:
|
||||
splitStatements: true
|
||||
stripComments: true
|
||||
sql: |
|
||||
DELETE FROM variables
|
||||
WHERE clave IN ('serie_facturacion_default', 'sere_facturacion_rect_default');
|
||||
|
||||
DELETE FROM series_facturas
|
||||
WHERE prefijo IN ('IMPR', 'REC IL');
|
||||
@ -0,0 +1,114 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: create-facturas-direcciones
|
||||
author: jjo
|
||||
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: facturas_direcciones
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: BIGINT
|
||||
autoIncrement: true
|
||||
constraints:
|
||||
primaryKey: true
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: factura_id
|
||||
type: BIGINT
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: unidades
|
||||
type: MEDIUMINT UNSIGNED
|
||||
|
||||
- column:
|
||||
name: email
|
||||
type: VARCHAR(255)
|
||||
|
||||
- column:
|
||||
name: att
|
||||
type: VARCHAR(150)
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: direccion
|
||||
type: VARCHAR(255)
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: cp
|
||||
type: MEDIUMINT UNSIGNED
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: ciudad
|
||||
type: VARCHAR(100)
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: provincia
|
||||
type: VARCHAR(100)
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: pais_code3
|
||||
type: CHAR(3)
|
||||
defaultValue: esp
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: telefono
|
||||
type: VARCHAR(30)
|
||||
|
||||
- column:
|
||||
name: instrucciones
|
||||
type: VARCHAR(255)
|
||||
|
||||
- column:
|
||||
name: razon_social
|
||||
type: VARCHAR(150)
|
||||
|
||||
- column:
|
||||
name: tipo_identificacion_fiscal
|
||||
type: ENUM('DNI','NIE','CIF','Pasaporte','VAT_ID')
|
||||
defaultValue: DNI
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- column:
|
||||
name: identificacion_fiscal
|
||||
type: VARCHAR(50)
|
||||
|
||||
- column:
|
||||
name: created_at
|
||||
type: TIMESTAMP
|
||||
defaultValueComputed: CURRENT_TIMESTAMP
|
||||
constraints:
|
||||
nullable: false
|
||||
|
||||
- addForeignKeyConstraint:
|
||||
constraintName: fk_facturas_direcciones_factura
|
||||
baseTableName: facturas_direcciones
|
||||
baseColumnNames: factura_id
|
||||
referencedTableName: facturas
|
||||
referencedColumnNames: id
|
||||
onDelete: CASCADE
|
||||
onUpdate: RESTRICT
|
||||
|
||||
rollback:
|
||||
- dropForeignKeyConstraint:
|
||||
baseTableName: facturas_direcciones
|
||||
constraintName: fk_facturas_direcciones_factura
|
||||
|
||||
- dropTable:
|
||||
tableName: facturas_direcciones
|
||||
@ -32,4 +32,20 @@ databaseChangeLog:
|
||||
- include:
|
||||
file: db/changelog/changesets/0016-fix-enum-estado-pedidos-lineas.yml
|
||||
- include:
|
||||
file: db/changelog/changesets/0017-add-fecha-entrega-to-pedidos-lineas.yml
|
||||
file: db/changelog/changesets/0017-add-fecha-entrega-to-pedidos-lineas.yml
|
||||
- include:
|
||||
file: db/changelog/changesets/0018-change-presupuesto-ch-3.yml
|
||||
- include:
|
||||
file: db/changelog/changesets/0019-add-estados-pago-to-pedidos-lineas.yml
|
||||
- include:
|
||||
file: db/changelog/changesets/0020-add-estados-pago-to-pedidos-lineas-2.yml
|
||||
- include:
|
||||
file: db/changelog/changesets/0021-add-email-and-is-palets-to-pedidos-direcciones.yml
|
||||
- include:
|
||||
file: db/changelog/changesets/0022-add-estados-pago-to-pedidos-lineas-3.yml
|
||||
- include:
|
||||
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
|
||||
@ -11,6 +11,7 @@ app.back=Volver
|
||||
app.eliminar=Eliminar
|
||||
app.imprimir=Imprimir
|
||||
app.view=Ver
|
||||
app.pay=Pagar
|
||||
app.acciones.siguiente=Siguiente
|
||||
app.acciones.anterior=Anterior
|
||||
|
||||
@ -22,10 +23,13 @@ app.logout=Cerrar sesión
|
||||
app.sidebar.inicio=Inicio
|
||||
app.sidebar.presupuestos=Presupuestos
|
||||
app.sidebar.pedidos=Pedidos
|
||||
app.sidebar.facturas=Facturas
|
||||
app.sidebar.configuracion=Configuración
|
||||
app.sidebar.usuarios=Usuarios
|
||||
app.sidebar.direcciones=Mis Direcciones
|
||||
app.sidebar.direcciones-admin=Administrar Direcciones
|
||||
app.sidebar.gestion-pagos=Gestión de Pagos
|
||||
|
||||
app.errors.403=No tienes permiso para acceder a esta página.
|
||||
app.errors.403=No tienes permiso para acceder a esta página.
|
||||
|
||||
app.validation.required=Campo obligatorio
|
||||
@ -35,6 +35,8 @@ direcciones.pasaporte=Pasaporte
|
||||
direcciones.cif=C.I.F.
|
||||
direcciones.vat_id=VAT ID
|
||||
|
||||
direcciones.direccionFacturacion=Dirección de facturación
|
||||
|
||||
direcciones.delete.title=Eliminar dirección
|
||||
direcciones.delete.button=Si, ELIMINAR
|
||||
direcciones.delete.text=¿Está seguro de que desea eliminar esta dirección?<br>Esta acción no se puede deshacer.
|
||||
|
||||
0
src/main/resources/i18n/facturas_en.properties
Normal file
0
src/main/resources/i18n/facturas_en.properties
Normal file
100
src/main/resources/i18n/facturas_es.properties
Normal file
100
src/main/resources/i18n/facturas_es.properties
Normal file
@ -0,0 +1,100 @@
|
||||
facturas.title=Facturas
|
||||
facturas.breadcrumb=Facturas
|
||||
facturas.breadcrumb.ver=Ver Factura
|
||||
facturas.breadcrumb.nueva=Nueva Factura
|
||||
|
||||
facturas.tabla.id=ID
|
||||
facturas.tabla.cliente=Cliente
|
||||
facturas.tabla.num-factura=Número de Factura
|
||||
facturas.tabla.estado=Estado
|
||||
facturas.tabla.estado-pago=Estado de Pago
|
||||
facturas.tabla.total=Total
|
||||
facturas.tabla.fecha-emision=Fecha de Emisión
|
||||
facturas.tabla.acciones=Acciones
|
||||
|
||||
facturas.estado-pago.pendiente=Pendiente
|
||||
facturas.estado-pago.pagada=Pagada
|
||||
facturas.estado-pago.cancelada=Cancelada
|
||||
|
||||
facturas.estado.borrador=Borrador
|
||||
facturas.estado.validada=Validada
|
||||
|
||||
facturas.form.numero-factura=Número de Factura
|
||||
facturas.form.id=ID de la Factura
|
||||
facturas.form.factura-rectificada=Factura rectificada
|
||||
facturas.form.serie=Serie de facturación
|
||||
facturas.form.serie.placeholder=Seleccione una serie...
|
||||
facturas.form.fecha-emision=Fecha de Emisión
|
||||
facturas.form.cliente=Cliente
|
||||
facturas.form.direccion-facturacion=Dirección de Facturación
|
||||
facturas.form.direccion-facturacion.placeholder=Seleccione una dirección...
|
||||
facturas.form.cliente.placeholder=Seleccione un cliente...
|
||||
facturas.form.notas=Notas
|
||||
facturas.form.factura-rectificada=Factura rectificada
|
||||
|
||||
facturas.form.btn.validar=Validar Factura
|
||||
facturas.form.btn.borrador=Pasar a Borrador
|
||||
facturas.form.btn.guardar=Guardar
|
||||
facturas.form.btn.imprimir=Imprimir Factura
|
||||
|
||||
facturas.lineas.acciones=Acciones
|
||||
facturas.lineas.acciones.editar=Editar
|
||||
facturas.lineas.acciones.eliminar=Eliminar
|
||||
facturas.lineas.acciones.agregar=Agregar línea
|
||||
facturas.lineas.descripcion=Descripción
|
||||
facturas.lineas.base=Base Imponible
|
||||
facturas.lineas.iva_4=I.V.A. 4%
|
||||
facturas.lineas.iva_21=I.V.A. 21%
|
||||
facturas.lineas.total=Total
|
||||
facturas.lineas.titulo=Líneas de la Factura
|
||||
facturas.lineas.iva_4.help=Introduce el importe del I.V.A. (no el %).
|
||||
facturas.lineas.iva_21.help=Introduce el importe del I.V.A. (no el %).
|
||||
facturas.lineas.delete.title=¿Eliminar línea de factura?
|
||||
facturas.lineas.delete.text=Esta acción no se puede deshacer.
|
||||
facturas.lineas.error.base=La base imponible no es válida.
|
||||
|
||||
facturas.lineas.gastos-envio=Gastos de envío
|
||||
|
||||
facturas.direccion.titulo=Dirección de Facturación
|
||||
facturas.direccion.razon-social=Razón Social
|
||||
facturas.direccion.identificacion-fiscal=Identificación Fiscal
|
||||
facturas.direccion.direccion=Dirección
|
||||
facturas.direccion.codigo-postal=Código Postal
|
||||
facturas.direccion.ciudad=Ciudad
|
||||
facturas.direccion.provincia=Provincia
|
||||
facturas.direccion.pais=País
|
||||
facturas.direccion.telefono=Teléfono
|
||||
|
||||
|
||||
facturas.pagos.titulo=Pago de factura
|
||||
facturas.pagos.acciones=Acciones
|
||||
facturas.pagos.acciones.agregar=Agregar pago
|
||||
facturas.pagos.acciones.editar=Editar
|
||||
facturas.pagos.acciones.eliminar=Eliminar
|
||||
facturas.pagos.metodo=Método de pago
|
||||
facturas.pagos.notas=Notas
|
||||
facturas.pagos.cantidad=Cantidad pagada
|
||||
facturas.pagos.fecha=Fecha de pago
|
||||
facturas.pagos.tipo=Tipo de pago
|
||||
facturas.pagos.tipo.tpv_tarjeta=TPV/Tarjeta
|
||||
facturas.pagos.tipo.tpv_bizum=TPV/Bizum
|
||||
facturas.pagos.tipo.transferencia=Transferencia
|
||||
facturas.pagos.tipo.otros=Otros
|
||||
facturas.pagos.total_pagado=Total pagado
|
||||
|
||||
|
||||
facturas.pagos.delete.title=Eliminar pago
|
||||
facturas.pagos.delete.text=Esta acción no se puede deshacer.
|
||||
facturas.pagos.error.cantidad=La cantidad no es válida.
|
||||
facturas.pagos.error.fecha=La fecha no es válida.
|
||||
|
||||
|
||||
facturas.delete.title=¿Estás seguro de que deseas eliminar esta factura?
|
||||
facturas.delete.text=Esta acción no se puede deshacer.
|
||||
facturas.delete.ok.title=Factura eliminada
|
||||
facturas.delete.ok.text=La factura ha sido eliminada correctamente.
|
||||
|
||||
facturas.add.form.validation.title=Error al crear la factura
|
||||
facturas.add.form.validation=Revise que todos los campos están rellenos
|
||||
|
||||
facturas.error.create=No se ha podido crear la factura. Revise los datos e inténtelo de nuevo.
|
||||
@ -4,6 +4,8 @@ pdf.company.postalcode=28028
|
||||
pdf.company.city=Madrid
|
||||
pdf.company.phone=+34 910052574
|
||||
|
||||
pdf.page=Página
|
||||
|
||||
pdf.presupuesto=PRESUPUESTO
|
||||
pdf.factura=FACTURA
|
||||
pdf.pedido=PEDIDO
|
||||
@ -29,6 +31,26 @@ pdf.datos-marcapaginas=Datos de marcapáginas:
|
||||
pdf.incluye-envio=El presupuesto incluye el envío a una dirección de la península.
|
||||
pdf.presupuesto-validez=Validez del presupuesto: 30 días desde la fecha de emisión.
|
||||
|
||||
# Factura
|
||||
pdf.factura.number=FACTURA Nº:
|
||||
pdf.factura.razon-social=RAZÓN SOCIAL:
|
||||
pdf.factura.identificacion-fiscal=IDENTIFICACIÓN FISCAL:
|
||||
pdf.factura.direccion=DIRECCIÓN:
|
||||
pdf.factura.codigo-postal=CÓDIGO POSTAL:
|
||||
pdf.factura.ciudad=CIUDAD:
|
||||
pdf.factura.provincia=PROVINCIA:
|
||||
pdf.factura.pais=PAÍS:
|
||||
|
||||
pdf.factura.lineas.descripcion=DESCRIPCIÓN
|
||||
pdf.factura.lineas.base=BASE IMPONIBLE
|
||||
pdf.factura.lineas.iva_4=IVA 4%
|
||||
pdf.factura.lineas.iva_21=IVA 21%
|
||||
pdf.factura.lineas.total=TOTAL
|
||||
pdf.factura.total-base=TOTAL BASE IMPONIBLE
|
||||
pdf.factura.total-iva_4=TOTAL IVA 4%
|
||||
pdf.factura.total-iva_21=TOTAL IVA 21%
|
||||
pdf.factura.total-general=TOTAL GENERAL
|
||||
|
||||
pdf.politica-privacidad=Política de privacidad
|
||||
pdf.politica-privacidad.responsable=Responsable: Impresión Imprime Libros - CIF: B04998886 - Teléfono de contacto: 910052574
|
||||
pdf.politica-privacidad.correo-direccion=Correo electrónico: info@imprimelibros.com - Dirección postal: Calle José Picón, Nº 28 Local A, 28028, Madrid
|
||||
|
||||
@ -13,20 +13,36 @@ checkout.payment.bizum=Bizum
|
||||
checkout.payment.bank-transfer=Transferencia bancaria
|
||||
checkout.error.payment=Error al procesar el pago: el pago ha sido cancelado o rechazado Por favor, inténtelo de nuevo.
|
||||
checkout.success.payment=Pago realizado con éxito. Gracias por su compra.
|
||||
checkout.error.select-method=Por favor, seleccione un método de pago.
|
||||
|
||||
checkout.make-payment=Realizar el pago
|
||||
checkout.authorization-required=Certifico que tengo los derechos para imprimir los archivos incluidos en mi pedido y me hago responsable en caso de reclamación de los mismos
|
||||
|
||||
pedido.estado.pendiente_pago=Pendiente de pago
|
||||
pedido.estado.procesando_pago=Procesando pago
|
||||
pedido.estado.denegado_pago=Pago denegado
|
||||
pedido.estado.aprobado=Aprobado
|
||||
pedido.estado.maquetacion=Maquetación
|
||||
pedido.estado.haciendo_ferro=Haciendo ferro
|
||||
pedido.estado.esperando_aceptacion_ferro=Esperando aceptación de ferro
|
||||
pedido.estado.ferro_cliente=Esperando aprobación de ferro
|
||||
pedido.estado.produccion=Producción
|
||||
pedido.estado.terminado=Terminado
|
||||
pedido.estado.enviado=Enviado
|
||||
pedido.estado.cancelado=Cancelado
|
||||
|
||||
pedido.module-title=Pedidos
|
||||
pedido.pedido=Pedido
|
||||
pedido.fecha-entrega=Fecha de entrega
|
||||
pedido.cancelar=Cancelar pedido
|
||||
pedido.update-estado=Actualizar estado
|
||||
pedido.maquetacion_finalizada=Maquetación finalizada
|
||||
pedido.ferro=Ferro
|
||||
pedido.cubierta=Cubierta
|
||||
pedido.tapa=Tapa
|
||||
pedido.aceptar_ferro=Aceptar ferro
|
||||
pedido.shipping-addresses=Direcciones de envío
|
||||
pedido.prueba=Prueba
|
||||
|
||||
pedido.table.id=Num. Pedido
|
||||
pedido.table.cliente=Cliente
|
||||
@ -36,4 +52,23 @@ pedido.table.estado=Estado
|
||||
pedido.table.acciones=Acciones
|
||||
|
||||
pedido.view.tirada=Tirada
|
||||
pedido.view.view-presupuesto=Ver presupuesto
|
||||
pedido.view.view-presupuesto=Ver presupuesto
|
||||
pedido.view.aceptar-ferro=Aceptar ferro
|
||||
pedido.view.ferro-download=Descargar ferro
|
||||
pedido.view.cub-download=Descargar cubierta
|
||||
pedido.view.tapa-download=Descargar tapa
|
||||
pedido.view.descargar-factura=Descargar factura
|
||||
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-text=Esta acción no se puede deshacer.
|
||||
|
||||
pedido.errors.linea-not-found=No se ha encontrado la línea de pedido.
|
||||
pedido.errors.cancel-pedido=Error al cancelar el pedido
|
||||
pedido.errors.state-error=Estado de línea no válido.
|
||||
pedido.errors.update-server-error=Error al actualizar el estado desde el servidor externo.
|
||||
pedido.errors.connecting-server-error=Error al conectar con el servidor externo.
|
||||
pedido.errors.cannot-update=No se puede actualizar el estado de una línea con ese estado inicial.
|
||||
pedido.success.estado-actualizado=Estado del pedido actualizado correctamente.
|
||||
pedido.success.same-estado=Sin cambios en el estado.
|
||||
pedido.success.pedido-cancelado=Pedido cancelado correctamente.
|
||||
@ -11,6 +11,12 @@ presupuesto.add-to-presupuesto=Añadir al presupuesto
|
||||
presupuesto.calcular=Calcular
|
||||
presupuesto.add=Añadir presupuesto
|
||||
presupuesto.guardar=Guardar
|
||||
presupuesto.duplicar=Duplicar
|
||||
presupuesto.reimprimir=Reimprimir
|
||||
presupuesto.reimpresion=Reimpresión
|
||||
presupuesto.editar=Editar
|
||||
presupuesto.ver=Ver
|
||||
presupuesto.borrar=Eliminar
|
||||
presupuesto.add-to-cart=Añadir a la cesta
|
||||
|
||||
presupuesto.nav.presupuestos-cliente=Presupuestos cliente
|
||||
@ -37,10 +43,13 @@ presupuesto.tabla.region=Región
|
||||
presupuesto.tabla.ciudad=Ciudad
|
||||
presupuesto.tabla.acciones=Acciones
|
||||
|
||||
presupuesto.comentario-administrador=Comentarios
|
||||
|
||||
# Pestaña datos generales de presupuesto
|
||||
presupuesto.informacion-libro=Información del libro
|
||||
presupuesto.datos-generales-descripcion=Datos generales del presupuesto
|
||||
presupuesto.titulo=Título*
|
||||
presupuesto.cliente=Cliente*
|
||||
presupuesto.autor=Autor
|
||||
presupuesto.isbn=ISBN
|
||||
presupuesto.tirada=Tirada
|
||||
@ -130,6 +139,7 @@ presupuesto.papel-guardas=Papel de guardas
|
||||
presupuesto.guardas-impresas=Guardas impresas
|
||||
presupuesto.no=No
|
||||
presupuesto.cabezada=Cabezada
|
||||
presupuesto.cabezada-sin-cabezada=Sin cabezada
|
||||
presupuesto.cabezada-blanca=Blanca
|
||||
presupuesto.cabezada-verde=Verde
|
||||
presupuesto.cabezada-azul=Azul
|
||||
@ -294,6 +304,30 @@ presupuesto.error.delete-permission-denied=No se puede eliminar: permiso denegad
|
||||
presupuesto.error.delete-not-found=No se puede eliminar: presupuesto no encontrado.
|
||||
presupuesto.error.delete-not-draft=Solo se pueden eliminar presupuestos en estado Borrador.
|
||||
|
||||
# Mensajes de duplicar presupuesto
|
||||
presupuesto.duplicar.title=Duplicar presupuesto
|
||||
presupuesto.duplicar.confirm=Si, DUPLICAR
|
||||
presupuesto.duplicar.cancelar=Cancelar
|
||||
presupuesto.duplicar.text=¿Está seguro de que desea duplicar este presupuesto?<br>Se creará una copia exacta del mismo en estado Borrador con el título introducido a continuación.
|
||||
presupuesto.duplicar.required=El título es obligatorio.
|
||||
presupuesto.duplicar.success.title=Presupuesto duplicado
|
||||
presupuesto.duplicar.success.text=El presupuesto ha sido duplicado con éxito.
|
||||
presupuesto.duplicar.aceptar=Aceptar
|
||||
presupuesto.duplicar.error.title=Error al duplicar presupuesto
|
||||
presupuesto.duplicar.error.internal=No se puede duplicar: error interno.
|
||||
|
||||
# Mensajes de reimprimir presupuesto
|
||||
presupuesto.reimprimir.title=Reimprimir presupuesto
|
||||
presupuesto.reimprimir.confirm=Si, REIMPRIMIR
|
||||
presupuesto.reimprimir.cancelar=Cancelar
|
||||
presupuesto.reimprimir.text=¿Está seguro de que desea reimprimir este presupuesto?<br>Se generará una nuevo presupuesto usando los mismos ficheros para su impresión.
|
||||
presupuesto.reimprimir.success.title=Presupuesto generado
|
||||
presupuesto.reimprimir.success.text=El presupuesto ha sido generado con éxito.
|
||||
presupuesto.reimprimir.aceptar=Aceptar
|
||||
presupuesto.reimprimir.error.title=Error al generar el presupuesto
|
||||
presupuesto.reimprimir.error.internal=No se puede generar el nuevo presupuesto: error interno.
|
||||
|
||||
|
||||
# Añadir presupuesto
|
||||
presupuesto.add.tipo=Tipo de presupuesto
|
||||
presupuesto.add.anonimo=Anónimo
|
||||
|
||||
26
src/main/resources/i18n/series_facturacion_es.properties
Normal file
26
src/main/resources/i18n/series_facturacion_es.properties
Normal file
@ -0,0 +1,26 @@
|
||||
series-facturacion.title=Series de Facturación
|
||||
series-facturacion.breadcrumb=Series de Facturación
|
||||
|
||||
series-facturacion.tabla.id=ID
|
||||
series-facturacion.tabla.nombre=Nombre
|
||||
series-facturacion.tabla.prefijo=Prefijo
|
||||
series-facturacion.tabla.tipo=Tipo
|
||||
series-facturacion.tabla.numero-actual=Número Actual
|
||||
series-facturacion.tabla.acciones=Acciones
|
||||
|
||||
series-facturacion.delete.title=¿Estás seguro de que deseas eliminar esta serie de facturación?
|
||||
series-facturacion.delete.text=Esta acción no se puede deshacer.
|
||||
series-facturacion.delete.ok.title=Serie de facturación eliminada
|
||||
series-facturacion.delete.ok.text=La serie de facturación ha sido eliminada correctamente.
|
||||
|
||||
series-facturacion.tipo.facturacion=Facturación
|
||||
|
||||
series-facturacion.form.nombre=Nombre
|
||||
series-facturacion.form.prefijo=Prefijo
|
||||
series-facturacion.form.prefijo.help=Ej: FAC, DIG, REC...
|
||||
series-facturacion.form.tipo=Tipo
|
||||
series-facturacion.tipo.facturacion=Facturación
|
||||
series-facturacion.form.numero-actual=Número actual
|
||||
|
||||
series-facturacion.modal.title.add=Nueva Serie de Facturación
|
||||
series-facturacion.modal.title.edit=Editar Serie de Facturación
|
||||
@ -2914,6 +2914,19 @@ File: Main Css File
|
||||
background-color: #0ac7fb !important;
|
||||
}
|
||||
|
||||
.accordion-fill-imprimelibros .accordion-item .accordion-button {
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.accordion-fill-imprimelibros .accordion-item .accordion-button:not(.collapsed) {
|
||||
color: #fff;
|
||||
background-color: #92b2a7 !important;
|
||||
}
|
||||
.accordion-fill-imprimelibros .accordion-item .accordion-button:is(.collapsed) {
|
||||
color: #fff;
|
||||
background-color: #4c5c63 !important;
|
||||
}
|
||||
|
||||
.accordion-warning .accordion-item {
|
||||
border-color: rgba(239, 174, 78, 0.6);
|
||||
}
|
||||
@ -11985,19 +11998,19 @@ div.dtr-modal div.dtr-modal-close:hover {
|
||||
bottom: 100%;
|
||||
}
|
||||
.flatpickr-calendar.arrowTop::before {
|
||||
border-bottom-color: #687cfe;
|
||||
border-bottom-color: #92b2a7;
|
||||
}
|
||||
.flatpickr-calendar.arrowTop::after {
|
||||
border-bottom-color: #687cfe;
|
||||
border-bottom-color: #92b2a7;
|
||||
}
|
||||
.flatpickr-calendar.arrowBottom::before, .flatpickr-calendar.arrowBottom::after {
|
||||
top: 100%;
|
||||
}
|
||||
.flatpickr-calendar.arrowBottom::before {
|
||||
border-top-color: #687cfe;
|
||||
border-top-color: #92b2a7;
|
||||
}
|
||||
.flatpickr-calendar.arrowBottom::after {
|
||||
border-top-color: #687cfe;
|
||||
border-top-color: #92b2a7;
|
||||
}
|
||||
.flatpickr-calendar:focus {
|
||||
outline: 0;
|
||||
@ -12012,7 +12025,7 @@ div.dtr-modal div.dtr-modal-close:hover {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
background-color: #687cfe;
|
||||
background-color: #92b2a7;
|
||||
border-radius: 5px 5px 0px 0px;
|
||||
}
|
||||
.flatpickr-months .flatpickr-month {
|
||||
@ -12284,7 +12297,7 @@ div.dtr-modal div.dtr-modal-close:hover {
|
||||
}
|
||||
|
||||
.flatpickr-weekdays {
|
||||
background-color: #687cfe;
|
||||
background-color: #92b2a7;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
@ -12309,7 +12322,7 @@ div.dtr-modal div.dtr-modal-close:hover {
|
||||
span.flatpickr-weekday {
|
||||
cursor: default;
|
||||
font-size: 90%;
|
||||
background: #687cfe;
|
||||
background: #92b2a7;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
@ -12411,11 +12424,11 @@ span.flatpickr-weekday {
|
||||
color: var(--vz-dark);
|
||||
}
|
||||
.flatpickr-day.selected, .flatpickr-day.startRange, .flatpickr-day.endRange, .flatpickr-day.selected.inRange, .flatpickr-day.startRange.inRange, .flatpickr-day.endRange.inRange, .flatpickr-day.selected:focus, .flatpickr-day.startRange:focus, .flatpickr-day.endRange:focus, .flatpickr-day.selected:hover, .flatpickr-day.startRange:hover, .flatpickr-day.endRange:hover, .flatpickr-day.selected.prevMonthDay, .flatpickr-day.startRange.prevMonthDay, .flatpickr-day.endRange.prevMonthDay, .flatpickr-day.selected.nextMonthDay, .flatpickr-day.startRange.nextMonthDay, .flatpickr-day.endRange.nextMonthDay {
|
||||
background: #687cfe;
|
||||
background: #92b2a7;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
color: #fff;
|
||||
border-color: #687cfe;
|
||||
border-color: #92b2a7;
|
||||
}
|
||||
.flatpickr-day.selected.startRange, .flatpickr-day.startRange.startRange, .flatpickr-day.endRange.startRange {
|
||||
border-radius: 50px 0 0 50px;
|
||||
|
||||
476
src/main/resources/static/assets/css/facturapdf.css
Normal file
476
src/main/resources/static/assets/css/facturapdf.css
Normal file
@ -0,0 +1,476 @@
|
||||
:root {
|
||||
--verde: #92b2a7;
|
||||
--letterspace: 8px;
|
||||
/* ← puedes ajustar este valor en el root */
|
||||
-ink: #1b1e28;
|
||||
--muted: #5b6472;
|
||||
--accent: #0ea5e9;
|
||||
/* azul tira a cyan */
|
||||
--line: #e6e8ef;
|
||||
--bg-tag: #f4f7fb;
|
||||
}
|
||||
|
||||
/* Open Sans (rutas relativas desde css → fonts) */
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
src: url("../fonts/OpenSans-Regular.ttf") format("truetype");
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
src: url("../fonts/OpenSans-SemiBold.ttf") format("truetype");
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
src: url("../fonts/OpenSans-Bold.ttf") format("truetype");
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 15mm 14mm 47mm 14mm;
|
||||
|
||||
@bottom-center {
|
||||
content: element(footer);
|
||||
/* llamamos al elemento “footer” */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: "Open Sans" !important;
|
||||
color: var(--ink);
|
||||
font-size: 11pt;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 0;
|
||||
/* ↑ deja 10mm extra para no pisar el footer */
|
||||
box-sizing: border-box;
|
||||
/* para que el padding no desborde */
|
||||
}
|
||||
|
||||
|
||||
body.has-watermark {
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
/* ====== HEADER (tabla) ====== */
|
||||
.il-header {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0 0 8mm 0;
|
||||
/* ↓ espacio bajo el header */
|
||||
}
|
||||
|
||||
.il-left,
|
||||
.il-right {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.il-left {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.il-right {
|
||||
width: 50%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.il-logo {
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
/* ← tamaño logo */
|
||||
|
||||
/* Caja superior derecha con esquinas */
|
||||
.il-company-box {
|
||||
display: inline-block;
|
||||
align-items: end;
|
||||
/* para alinear a la derecha sin ocupar todo */
|
||||
position: relative;
|
||||
padding: 4mm 4mm;
|
||||
/* ← espacio texto ↔ esquinas */
|
||||
color: #000;
|
||||
font-size: 10.5pt;
|
||||
/* ← tamaño de letra */
|
||||
line-height: 1;
|
||||
/* ← separación entre líneas */
|
||||
max-width: 75mm;
|
||||
/* ← ancho máximo de la caja */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Esquinas */
|
||||
.il-company-box .corner {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
/* ← anchura esquina */
|
||||
height: 20px;
|
||||
/* ← altura esquina */
|
||||
border-color: #92b2a7;
|
||||
/* ← color esquina */
|
||||
}
|
||||
|
||||
.corner.tl {
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-top: 2px solid #92b2a7;
|
||||
border-left: 2px solid #92b2a7;
|
||||
}
|
||||
|
||||
.corner.tr {
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-top: 2px solid #92b2a7;
|
||||
border-right: 2px solid #92b2a7;
|
||||
}
|
||||
|
||||
.corner.bl {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-bottom: 2px solid #92b2a7;
|
||||
border-left: 2px solid #92b2a7;
|
||||
}
|
||||
|
||||
.corner.br {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-bottom: 2px solid #92b2a7;
|
||||
border-right: 2px solid #92b2a7;
|
||||
}
|
||||
|
||||
|
||||
.company-line {
|
||||
margin: 1.5mm 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Nueva banda verde PRESUPUESTO */
|
||||
.doc-banner {
|
||||
width: 100%;
|
||||
background-color: #92b2a7 !important;
|
||||
/* ← tu verde corporativo */
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 2mm 0;
|
||||
margin-bottom: 4mm;
|
||||
display: block;
|
||||
/* evita conflictos */
|
||||
}
|
||||
|
||||
.banner-text {
|
||||
font-family: "Open Sans", Arial, sans-serif !important;
|
||||
font-weight: 400;
|
||||
font-size: 20pt;
|
||||
letter-spacing: 8px;
|
||||
/* ← configurable */
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ficha superior */
|
||||
.sheet-info {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 4mm 0 6mm 0;
|
||||
font-size: 10.5pt;
|
||||
}
|
||||
|
||||
.sheet-info td {
|
||||
border: 1px solid var(--line);
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.sheet-info .lbl {
|
||||
color: var(--muted);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/*.sheet-info .val {
|
||||
}*/
|
||||
|
||||
/* Línea título libro */
|
||||
.line-title {
|
||||
font-family: "Open Sans", Arial, sans-serif !important;
|
||||
margin: 3mm 0 5mm 0;
|
||||
padding: 2px 4px;
|
||||
font-size: 10.5pt;
|
||||
font-weight: 600;
|
||||
color: #5c5c5c;
|
||||
}
|
||||
|
||||
.line-title .lbl {
|
||||
margin-right: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Specs 2 columnas */
|
||||
.specs-wrapper {
|
||||
width: 180mm;
|
||||
margin-left: 15mm;
|
||||
/* ← margen izquierdo real del A4 */
|
||||
margin-right: auto;
|
||||
/* opcional */
|
||||
color: #5c5c5c;
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
.align-with-text {
|
||||
margin-left: 1mm;
|
||||
margin-right: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.specs {
|
||||
display: table;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
margin-bottom: 6mm;
|
||||
}
|
||||
|
||||
.specs .col {
|
||||
display: table-cell;
|
||||
width: 50%;
|
||||
padding-right: 6mm;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.specs .col:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* Listas sin margen superior por defecto */
|
||||
ul,
|
||||
ol {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0rem;
|
||||
/* si quieres algo abajo */
|
||||
padding-left: 1.25rem;
|
||||
/* sangría */
|
||||
}
|
||||
|
||||
/* Párrafos con menos margen inferior */
|
||||
p {
|
||||
margin: 0 0 .5rem;
|
||||
}
|
||||
|
||||
/* Si una lista va justo después de un texto o título, que no tenga hueco arriba */
|
||||
p+ul,
|
||||
p+ol,
|
||||
h1+ul,
|
||||
h2+ul,
|
||||
h3+ul,
|
||||
h4+ul,
|
||||
h5+ul,
|
||||
h6+ul,
|
||||
div+ul,
|
||||
div+ol {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
||||
.block-title {
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
font-size: 8pt;
|
||||
margin: 2mm 0 1mm 0;
|
||||
}
|
||||
|
||||
.kv {
|
||||
margin: 1mm 0;
|
||||
}
|
||||
|
||||
.kv span {
|
||||
color: var(--muted);
|
||||
display: inline-block;
|
||||
min-width: 55%;
|
||||
}
|
||||
|
||||
.kv b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subblock {
|
||||
margin-top: 3mm;
|
||||
}
|
||||
|
||||
.services {
|
||||
margin: 0;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.services li {
|
||||
margin: 1mm 0;
|
||||
}
|
||||
|
||||
/* Bloque marcapáginas */
|
||||
.bookmark {
|
||||
margin-top: 4mm;
|
||||
border: 1px dashed var(--line);
|
||||
padding: 3mm;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.bookmark .bk-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 2mm;
|
||||
}
|
||||
|
||||
/* Tabla de precios (tiradas) */
|
||||
.prices {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 6mm;
|
||||
font-size: 10.5pt;
|
||||
}
|
||||
|
||||
.prices thead th {
|
||||
text-align: left;
|
||||
padding: 3px;
|
||||
border-bottom: 2px solid var(--accent);
|
||||
background: #eef8fe;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.prices tbody td {
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.prices .col-tirada {
|
||||
width: 22%;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 14mm;
|
||||
right: 14mm;
|
||||
bottom: 18mm;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 4mm;
|
||||
font-size: 7.5pt;
|
||||
color: var(--muted);
|
||||
z-index: 10;
|
||||
/* sobre la marca */
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
.footer .address {
|
||||
display: table-cell;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.footer .privacy {
|
||||
display: table-cell;
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
.pv-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 1mm;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.pv-text {
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.page-count {
|
||||
margin-top: 2mm;
|
||||
text-align: right;
|
||||
font-size: 9pt;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.page::after {
|
||||
content: counter(page);
|
||||
}
|
||||
|
||||
.pages::after {
|
||||
content: counter(pages);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-color: #92b2a7;
|
||||
border-collapse: collapse;
|
||||
|
||||
}
|
||||
|
||||
.items-table thead th {
|
||||
background-color: #f3f6f9;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.items-table tbody td {
|
||||
font-size: small;
|
||||
color: #000
|
||||
}
|
||||
|
||||
.items-table td.desc{
|
||||
font-family: "Open Sans" !important;
|
||||
color: #5c5c5c !important;
|
||||
font-size: 9pt !important;
|
||||
line-height: 1.25 !important;
|
||||
}
|
||||
|
||||
/* TODO lo que esté dentro (p, span, ul, li, b, i, etc. del HTML manual) */
|
||||
.items-table td.desc *{
|
||||
font-family: "Open Sans" !important;
|
||||
color: #5c5c5c !important;
|
||||
font-size: 9pt !important;
|
||||
line-height: 1.25 !important;
|
||||
}
|
||||
|
||||
|
||||
.items-table thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
.items-table tfoot {
|
||||
display: table-footer-group;
|
||||
}
|
||||
|
||||
.items-table tr,
|
||||
.items-table td,
|
||||
.items-table th {
|
||||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
|
||||
.page-number {
|
||||
position: absolute;
|
||||
bottom: -3mm;
|
||||
right: 0;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.page-number .pn:before {
|
||||
content: counter(page) " / " counter(pages);
|
||||
}
|
||||
|
||||
.pdf-footer-running {
|
||||
position: running(footer);
|
||||
/* lo registra como elemento repetible */
|
||||
font-size: 7.5pt;
|
||||
color: var(--muted);
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 4mm;
|
||||
/* el resto de tus estilos internos (address, privacy, etc.) */
|
||||
}
|
||||
BIN
src/main/resources/static/assets/images/billin_address.gif
Normal file
BIN
src/main/resources/static/assets/images/billin_address.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 629 KiB |
BIN
src/main/resources/static/assets/images/billing_address2.gif
Normal file
BIN
src/main/resources/static/assets/images/billing_address2.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 986 KiB |
BIN
src/main/resources/static/assets/images/billing_address2.gif.gif
Normal file
BIN
src/main/resources/static/assets/images/billing_address2.gif.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 750 KiB |
BIN
src/main/resources/static/assets/images/delivery-truck.gif
Normal file
BIN
src/main/resources/static/assets/images/delivery-truck.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 847 KiB |
@ -21,6 +21,7 @@ $(() => {
|
||||
$(this).find('.direccion-id').attr('name', 'direcciones[' + i + '].id');
|
||||
$(this).find('.direccion-cp').attr('name', 'direcciones[' + i + '].cp');
|
||||
$(this).find('.direccion-pais-code3').attr('name', 'direcciones[' + i + '].paisCode3');
|
||||
$(this).find('.is-palets').attr('name', 'direcciones[' + i + '].isPalets');
|
||||
if ($(this).find('.presupuesto-id').length > 0 && $(this).find('.presupuesto-id').val() !== null
|
||||
&& $(this).find('.presupuesto-id').val() !== "")
|
||||
$(this).find('.presupuesto-id').attr('name', 'direcciones[' + i + '].presupuestoId');
|
||||
|
||||
@ -0,0 +1,222 @@
|
||||
/* global $, bootstrap, window */
|
||||
$(() => {
|
||||
// si jQuery está cargado, añade CSRF a AJAX
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||
if (window.$ && csrfToken && csrfHeader) {
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader(csrfHeader, csrfToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const language = document.documentElement.lang || 'es-ES';
|
||||
|
||||
const $table = $('#series-datatable'); // en tu HTML está así, aunque el id sea raro
|
||||
const $addBtn = $('#addButton');
|
||||
|
||||
const $modal = $('#serieFacturacionModal');
|
||||
const modal = new bootstrap.Modal($modal[0]);
|
||||
|
||||
const $form = $('#serieFacturacionForm');
|
||||
const $alert = $('#serieFacturacionAlert');
|
||||
const $saveBtn = $('#serieFacturacionSaveBtn');
|
||||
|
||||
function showError(msg) {
|
||||
$alert.removeClass('d-none').text(msg || 'Error');
|
||||
}
|
||||
|
||||
function clearError() {
|
||||
$alert.addClass('d-none').text('');
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
clearError();
|
||||
$form[0].reset();
|
||||
$form.removeClass('was-validated');
|
||||
$('#serie_id').val('');
|
||||
$('#numero_actual').val('1');
|
||||
$('#tipo').val('facturacion');
|
||||
}
|
||||
|
||||
function openAddModal() {
|
||||
resetForm();
|
||||
$('#serieFacturacionModalTitle').text(window.languageBundle?.['series-facturacion.modal.title.add'] || 'Añadir serie');
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function openEditModal(row) {
|
||||
resetForm();
|
||||
$('#serieFacturacionModalTitle').text(window.languageBundle?.['series-facturacion.modal.title.edit'] || 'Editar serie');
|
||||
|
||||
$('#serie_id').val(row.id);
|
||||
$('#nombre_serie').val(row.nombre_serie);
|
||||
$('#prefijo').val(row.prefijo);
|
||||
$('#tipo').val(row.tipo || 'facturacion');
|
||||
$('#numero_actual').val(row.numero_actual);
|
||||
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// DataTable server-side
|
||||
// -----------------------------
|
||||
const dt = $table.DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
searching: true,
|
||||
orderMulti: false,
|
||||
pageLength: 10,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
|
||||
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
||||
|
||||
ajax: {
|
||||
url: '/configuracion/series-facturacion/api/datatables',
|
||||
type: 'GET',
|
||||
dataSrc: function (json) {
|
||||
// DataTables espera {draw, recordsTotal, recordsFiltered, data}
|
||||
return json.data || [];
|
||||
},
|
||||
error: function (xhr) {
|
||||
console.error('DataTables error', xhr);
|
||||
}
|
||||
},
|
||||
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'nombre_serie' },
|
||||
{ data: 'prefijo' },
|
||||
{ data: 'tipo_label', name: 'tipo' },
|
||||
{ data: 'numero_actual' },
|
||||
{
|
||||
data: 'actions',
|
||||
orderable: false,
|
||||
searchable: false
|
||||
}
|
||||
],
|
||||
|
||||
order: [[0, 'desc']]
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// Add
|
||||
// -----------------------------
|
||||
$addBtn.on('click', () => openAddModal());
|
||||
|
||||
// -----------------------------
|
||||
// Edit click
|
||||
// -----------------------------
|
||||
$table.on('click', '.btn-edit-serie', function () {
|
||||
const row = dt.row($(this).closest('tr')).data();
|
||||
openEditModal(row);
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// Delete click
|
||||
// -----------------------------
|
||||
$table.on('click', '.btn-delete-serie', function () {
|
||||
const row = dt.row($(this).closest('tr')).data();
|
||||
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['series-facturacion.delete.title']) || 'Eliminar serie',
|
||||
html: window.languageBundle.get(['series-facturacion.delete.text']) || 'Esta acción no se puede deshacer.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-danger w-xs mt-2',
|
||||
cancelButton: 'btn btn-light w-xs mt-2'
|
||||
},
|
||||
confirmButtonText: window.languageBundle.get(['app.eliminar']) || 'Eliminar',
|
||||
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
$.ajax({
|
||||
url: `/configuracion/series-facturacion/api/${row.id}`,
|
||||
method: 'DELETE',
|
||||
success: function () {
|
||||
Swal.fire({
|
||||
icon: 'success', title: window.languageBundle.get(['series-facturacion.delete.ok.title']) || 'Eliminado',
|
||||
text: window.languageBundle.get(['series-facturacion.delete.ok.text']) || 'La serie de facturación ha sido eliminada correctamente.',
|
||||
showConfirmButton: false,
|
||||
timer: 1800,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary w-xs mt-2',
|
||||
},
|
||||
});
|
||||
dt.ajax.reload(null, false);
|
||||
},
|
||||
error: function (xhr) {
|
||||
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
||||
|| 'Error al eliminar la serie de facturación.';
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'No se pudo eliminar',
|
||||
text: msg,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// -----------------------------
|
||||
// Save (create/update)
|
||||
// -----------------------------
|
||||
$saveBtn.on('click', function () {
|
||||
clearError();
|
||||
|
||||
// Validación Bootstrap
|
||||
const formEl = $form[0];
|
||||
if (!formEl.checkValidity()) {
|
||||
$form.addClass('was-validated');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = $('#serie_id').val();
|
||||
const payload = {
|
||||
nombre_serie: $('#nombre_serie').val().trim(),
|
||||
prefijo: $('#prefijo').val().trim(),
|
||||
tipo: $('#tipo').val(),
|
||||
numero_actual: Number($('#numero_actual').val())
|
||||
};
|
||||
|
||||
const isEdit = !!id;
|
||||
const url = isEdit
|
||||
? `/configuracion/series-facturacion/api/${id}`
|
||||
: `/configuracion/series-facturacion/api`;
|
||||
|
||||
const method = isEdit ? 'PUT' : 'POST';
|
||||
|
||||
$saveBtn.prop('disabled', true);
|
||||
|
||||
$.ajax({
|
||||
url,
|
||||
method,
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
data: JSON.stringify(payload),
|
||||
success: function () {
|
||||
modal.hide();
|
||||
dt.ajax.reload(null, false);
|
||||
},
|
||||
error: function (xhr) {
|
||||
const msg = xhr.responseJSON?.message || xhr.responseText || 'No se pudo guardar.';
|
||||
showError(msg);
|
||||
},
|
||||
complete: function () {
|
||||
$saveBtn.prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// limpiar estado al cerrar
|
||||
$modal.on('hidden.bs.modal', () => resetForm());
|
||||
});
|
||||
@ -0,0 +1,216 @@
|
||||
$(() => {
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||
if (window.$ && csrfToken && csrfHeader) {
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader(csrfHeader, csrfToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let $addBtn = $('#save-btn');
|
||||
let $cancelBtn = $('#cancel-btn');
|
||||
let $selectCliente = $('#clienteSelect');
|
||||
let $serieInput = $('#serieInput');
|
||||
let $direccionFacturacion = $('#direccionFacturacion');
|
||||
let $facturaRectificada = $('#facturaRectificada');
|
||||
let $divFacturaRectificada = $('#div-factura-rectificada');
|
||||
|
||||
// -----------------------------
|
||||
// Initialize select2 for cliente selection
|
||||
// -----------------------------
|
||||
$selectCliente.select2({
|
||||
placeholder: languageBundle['facturas.form.cliente.placeholder'],
|
||||
width: '100%',
|
||||
ajax: {
|
||||
url: '/users/api/get-users',
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
data: function (params) {
|
||||
return {
|
||||
showUsername: true,
|
||||
q: params.term,
|
||||
page: params.page || 1
|
||||
};
|
||||
}
|
||||
},
|
||||
minimumInputLength: 0
|
||||
});
|
||||
|
||||
$selectCliente.on('select2:select', function (e) {
|
||||
const data = e.params.data;
|
||||
$serieInput.val(null).trigger('change');
|
||||
$direccionFacturacion.val(null).trigger('change');
|
||||
$facturaRectificada.val(null).trigger('change');
|
||||
if (data && data.id) {
|
||||
$serieInput.prop('disabled', false);
|
||||
$direccionFacturacion.prop('disabled', false);
|
||||
}
|
||||
else {
|
||||
$serieInput.prop('disabled', true);
|
||||
$direccionFacturacion.prop('disabled', true);
|
||||
}
|
||||
});
|
||||
|
||||
$serieInput.select2({
|
||||
placeholder: languageBundle['facturas.form.serie.placeholder'],
|
||||
width: '100%',
|
||||
ajax: {
|
||||
url: '/configuracion/series-facturacion/api/get-series',
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
data: function (params) {
|
||||
return {
|
||||
q: params.term,
|
||||
page: params.page || 1
|
||||
};
|
||||
}
|
||||
},
|
||||
minimumInputLength: 0
|
||||
});
|
||||
|
||||
$serieInput.on('select2:select', function (e) {
|
||||
const data = e.params.data;
|
||||
const defaultRectSerieId = $serieInput.data('default-serie-rect');
|
||||
if (data && data.id) {
|
||||
if (data.id === defaultRectSerieId) {
|
||||
$divFacturaRectificada.removeClass('d-none');
|
||||
$facturaRectificada.val(null).trigger('change');
|
||||
}
|
||||
else {
|
||||
$divFacturaRectificada.addClass('d-none');
|
||||
$facturaRectificada.val(null).trigger('change');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$direccionFacturacion.select2({
|
||||
placeholder: languageBundle['facturas.form.direccion-facturacion.placeholder'],
|
||||
width: '100%',
|
||||
ajax: {
|
||||
url: '/facturas/api/get-direcciones',
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
data: function (params) {
|
||||
const clienteId = $selectCliente.val();
|
||||
return {
|
||||
user_id: clienteId,
|
||||
q: params.term,
|
||||
page: params.page || 1
|
||||
};
|
||||
},
|
||||
processResults: (data) => {
|
||||
const items = Array.isArray(data) ? data : (data.results || []);
|
||||
return {
|
||||
results: items.map(item => ({
|
||||
id: item.id,
|
||||
text: item.text, // ← Select2 necesita 'id' y 'text'
|
||||
alias: item.alias || 'Sin alias',
|
||||
att: item.att || '',
|
||||
direccion: item.direccion || '',
|
||||
cp: item.cp || '',
|
||||
ciudad: item.ciudad || '',
|
||||
html: `
|
||||
<div>
|
||||
<strong>${item.alias || 'Sin alias'}</strong><br>
|
||||
${item.att ? `<small>${item.att}</small><br>` : ''}
|
||||
<small>${item.direccion || ''}${item.cp ? ', ' + item.cp : ''}${item.ciudad ? ', ' + item.ciudad : ''}</small>
|
||||
</div>
|
||||
`
|
||||
})),
|
||||
pagination: { more: false } // opcional, evita que espere más páginas
|
||||
};
|
||||
}
|
||||
},
|
||||
minimumInputLength: 0,
|
||||
templateResult: data => {
|
||||
if (data.loading) return data.text;
|
||||
return $(data.html || data.text);
|
||||
},
|
||||
// Selección más compacta (solo alias + ciudad)
|
||||
templateSelection: data => {
|
||||
if (!data.id) return data.text;
|
||||
const alias = data.alias || data.text;
|
||||
const ciudad = data.ciudad ? ` — ${data.ciudad}` : '';
|
||||
return $(`<span>${alias}${ciudad}</span>`);
|
||||
},
|
||||
escapeMarkup: m => m
|
||||
});
|
||||
|
||||
$facturaRectificada.select2({
|
||||
placeholder: languageBundle['facturas.form.factura-rectificada.placeholder'],
|
||||
width: '100%',
|
||||
ajax: {
|
||||
url: '/facturas/api/get-facturas-rectificables',
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
data: function (params) {
|
||||
const clienteId = $selectCliente.val();
|
||||
return {
|
||||
user_id: clienteId,
|
||||
q: params.term,
|
||||
page: params.page || 1
|
||||
};
|
||||
}
|
||||
},
|
||||
minimumInputLength: 0
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// Cancel button click
|
||||
// -----------------------------
|
||||
$cancelBtn.on('click', () => {
|
||||
window.location.href = '/facturas';
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// Save button click
|
||||
// -----------------------------
|
||||
$addBtn.on('click', () => {
|
||||
const clienteId = $selectCliente.val();
|
||||
const serieId = $serieInput.val();
|
||||
const direccionId = $direccionFacturacion.val();
|
||||
const facturaRectificadaId = $facturaRectificada.val();
|
||||
|
||||
if (!clienteId && !serieId && !direccionId) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: languageBundle['facturas.add.form.validation.title'],
|
||||
text: languageBundle['facturas.add.form.validation']
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/facturas/add',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
user: clienteId,
|
||||
serie: serieId,
|
||||
direccion: direccionId,
|
||||
factura_rectificada: facturaRectificadaId
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
window.location.href = '/facturas/' + response.facturaId;
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
text: response.message
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
text: languageBundle['facturas.error.create']
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,131 @@
|
||||
/* global $, bootstrap, window */
|
||||
$(() => {
|
||||
// si jQuery está cargado, añade CSRF a AJAX
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||
if (window.$ && csrfToken && csrfHeader) {
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader(csrfHeader, csrfToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const language = document.documentElement.lang || 'es-ES';
|
||||
|
||||
const $table = $('#facturas-datatable'); // en tu HTML está así, aunque el id sea raro
|
||||
const $addBtn = $('#addButton');
|
||||
|
||||
// -----------------------------
|
||||
// DataTable server-side
|
||||
// -----------------------------
|
||||
const dt = $table.DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
searching: true,
|
||||
orderMulti: false,
|
||||
pageLength: 10,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
|
||||
language: { url: '/assets/libs/datatables/i18n/' + language + '.json' },
|
||||
|
||||
ajax: {
|
||||
url: '/facturas/api/datatables',
|
||||
type: 'GET',
|
||||
dataSrc: function (json) {
|
||||
// DataTables espera {draw, recordsTotal, recordsFiltered, data}
|
||||
return json.data || [];
|
||||
},
|
||||
error: function (xhr) {
|
||||
console.error('DataTables error', xhr);
|
||||
}
|
||||
},
|
||||
|
||||
columns: [
|
||||
{ data: 'id' },
|
||||
{ data: 'cliente' },
|
||||
{ data: 'numero_factura' },
|
||||
{ data: 'estado_label', name: 'estado' },
|
||||
{ data: 'estado_pago_label', name: 'estado_pago' },
|
||||
{ data: 'total' },
|
||||
{ data: 'fecha_emision' },
|
||||
{
|
||||
data: 'actions',
|
||||
orderable: false,
|
||||
searchable: false
|
||||
}
|
||||
],
|
||||
|
||||
order: [[0, 'desc']]
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// Add
|
||||
// -----------------------------
|
||||
$addBtn.on('click', () => {
|
||||
window.location.href = '/facturas/add';
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// Edit click
|
||||
// -----------------------------
|
||||
$table.on('click', '.btn-view-factura', function () {
|
||||
const row = dt.row($(this).closest('tr')).data();
|
||||
window.location.href = `/facturas/${row.id}`;
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// Delete click
|
||||
// -----------------------------
|
||||
$table.on('click', '.btn-delete-factura', function () {
|
||||
const row = dt.row($(this).closest('tr')).data();
|
||||
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['facturas.delete.title']) || 'Eliminar factura',
|
||||
html: window.languageBundle.get(['facturas.delete.text']) || 'Esta acción no se puede deshacer.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-danger w-xs mt-2',
|
||||
cancelButton: 'btn btn-light w-xs mt-2'
|
||||
},
|
||||
confirmButtonText: window.languageBundle.get(['app.eliminar']) || 'Eliminar',
|
||||
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
$.ajax({
|
||||
url: `/facturas/api/${row.id}`,
|
||||
method: 'DELETE',
|
||||
success: function () {
|
||||
Swal.fire({
|
||||
icon: 'success', title: window.languageBundle.get(['facturas.delete.ok.title']) || 'Eliminado',
|
||||
text: window.languageBundle.get(['facturas.delete.ok.text']) || 'La factura ha sido eliminada correctamente.',
|
||||
showConfirmButton: false,
|
||||
timer: 1800,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary w-xs mt-2',
|
||||
},
|
||||
});
|
||||
dt.ajax.reload(null, false);
|
||||
},
|
||||
error: function (xhr) {
|
||||
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
||||
|| 'Error al eliminar la serie de facturación.';
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'No se pudo eliminar',
|
||||
text: msg,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@ -0,0 +1,768 @@
|
||||
$(() => {
|
||||
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||
if (window.$ && csrfToken && csrfHeader) {
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader(csrfHeader, csrfToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const $container = $('#factura-container');
|
||||
|
||||
const MIN_LOADER_TIME = 500; // ms (ajusta a gusto)
|
||||
let loaderStartTime = 0;
|
||||
|
||||
|
||||
function showLoader() {
|
||||
loaderStartTime = Date.now();
|
||||
$('#factura-loader').removeClass('d-none');
|
||||
}
|
||||
|
||||
function hideLoader() {
|
||||
const elapsed = Date.now() - loaderStartTime;
|
||||
const remaining = Math.max(MIN_LOADER_TIME - elapsed, 0);
|
||||
|
||||
setTimeout(() => {
|
||||
$('#factura-loader').addClass('d-none');
|
||||
}, remaining);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getFacturaId() {
|
||||
return $container.data('factura-id');
|
||||
}
|
||||
|
||||
function getFlatpickrLocale() {
|
||||
const lang = (document.documentElement.lang || 'es').toLowerCase().split('-')[0]; // es-ES -> es
|
||||
const l10ns = window.flatpickr?.l10ns;
|
||||
return (l10ns && l10ns[lang]) ? l10ns[lang] : (l10ns?.default || null);
|
||||
}
|
||||
|
||||
|
||||
function reloadFacturaContainer() {
|
||||
const id = getFacturaId();
|
||||
if (!id) return $.Deferred().reject('No factura id').promise();
|
||||
|
||||
showLoader();
|
||||
|
||||
return $.ajax({
|
||||
url: `/facturas/${id}/container`,
|
||||
method: 'GET',
|
||||
cache: false,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.done((html) => {
|
||||
const $c = $('#factura-container');
|
||||
|
||||
// conserva loader dentro del container
|
||||
const $loader = $c.find('#factura-loader').detach();
|
||||
|
||||
// reemplaza el contenido completo
|
||||
$c.empty().append($loader).append(html);
|
||||
|
||||
afterFacturaRender();
|
||||
})
|
||||
.fail((xhr) => {
|
||||
console.error('Error recargando container:', xhr.status, xhr.responseText);
|
||||
})
|
||||
.always(() => {
|
||||
hideLoader();
|
||||
});
|
||||
}
|
||||
|
||||
function postAndReload(url, data) {
|
||||
showLoader();
|
||||
const isJson = data !== undefined && data !== null;
|
||||
|
||||
return $.ajax({
|
||||
url,
|
||||
method: 'POST',
|
||||
data: isJson ? JSON.stringify(data) : null,
|
||||
contentType: isJson ? 'application/json; charset=UTF-8' : undefined,
|
||||
processData: !isJson, // importante: si es JSON, no procesar
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.done(() => reloadFacturaContainer())
|
||||
.fail((xhr) => {
|
||||
console.error('Error en acción:', xhr.status, xhr.responseText);
|
||||
})
|
||||
.always(() => {
|
||||
hideLoader();
|
||||
});
|
||||
}
|
||||
|
||||
$container.on('click', '#btn-imprimir-factura', function () {
|
||||
const id = getFacturaId();
|
||||
const url = `/api/pdf/factura/${id}?mode=download`;
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.target = '_self'; // descarga en la misma pestaña
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
|
||||
// Delegación (funciona aunque reemplacemos el contenido interno)
|
||||
$container.on('click', '#btn-validar-factura', function () {
|
||||
const id = getFacturaId();
|
||||
postAndReload(`/facturas/${id}/validar`);
|
||||
});
|
||||
|
||||
$container.on('click', '#btn-borrador-factura', function () {
|
||||
const id = getFacturaId();
|
||||
postAndReload(`/facturas/${id}/borrador`);
|
||||
});
|
||||
|
||||
$container.on('focusin', 'textarea[name="notas"]', function () {
|
||||
$(this).data('initial', $(this).val() ?? '');
|
||||
});
|
||||
|
||||
$container.on('focusout', 'textarea[name="notas"]', function () {
|
||||
const before = $(this).data('initial') ?? '';
|
||||
const now = $(this).val() ?? '';
|
||||
if (before === now) return;
|
||||
|
||||
const id = getFacturaId();
|
||||
postAndReload(`/facturas/${id}/notas`, { notas: now });
|
||||
});
|
||||
|
||||
|
||||
$container.on('click', '#btn-guardar-factura', function () {
|
||||
const facturaId = getFacturaId();
|
||||
|
||||
const fechaEmisionStr = $('#facturaFechaEmision').val();
|
||||
const fechaEmision = parseEsDateToIsoLocal(fechaEmisionStr);
|
||||
|
||||
const payload = {
|
||||
cabecera: {
|
||||
serieId: $('#facturaSerieId').val() || null,
|
||||
clienteId: $('#facturaClienteId').val() || null,
|
||||
fechaEmision: fechaEmision // ISO LocalDateTime (00:00:00)
|
||||
},
|
||||
direccionFacturacion: {
|
||||
razonSocial: $('#dirRazonSocial').val() || '',
|
||||
identificacionFiscal: $('#dirIdentificacionFiscal').val() || '',
|
||||
direccion: $('#dirDireccion').val() || '',
|
||||
cp: $('#dirCp').val() || '',
|
||||
ciudad: $('#dirCiudad').val() || '',
|
||||
provincia: $('#dirProvincia').val() || '',
|
||||
paisKeyword: $('#dirPais').val() || '', // lo que tú guardas como keyword
|
||||
telefono: $('#dirTelefono').val() || ''
|
||||
}
|
||||
};
|
||||
|
||||
postAndReload(`/facturas/${facturaId}/guardar`, payload);
|
||||
});
|
||||
|
||||
|
||||
function destroySelect2($root) {
|
||||
$root.find('.js-select2-factura').each(function () {
|
||||
const $el = $(this);
|
||||
if ($el.hasClass('select2-hidden-accessible')) {
|
||||
$el.select2('destroy');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initSelect2ForDraft($root) {
|
||||
$root.find('.js-select2-factura').each(function () {
|
||||
const $el = $(this);
|
||||
|
||||
// evita doble init
|
||||
if ($el.hasClass('select2-hidden-accessible')) return;
|
||||
|
||||
const url = $el.data('url');
|
||||
const placeholder = $el.data('placeholder') || '';
|
||||
|
||||
$el.select2({
|
||||
width: '100%',
|
||||
placeholder,
|
||||
allowClear: true,
|
||||
minimumInputLength: 0,
|
||||
|
||||
ajax: {
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
delay: 250,
|
||||
|
||||
data: function (params) {
|
||||
return {
|
||||
q: params.term, // texto buscado
|
||||
page: params.page || 1
|
||||
};
|
||||
},
|
||||
|
||||
processResults: function (data) {
|
||||
return data;
|
||||
},
|
||||
|
||||
cache: true
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// MODIFICACIÓN: Modal + Quill para líneas de factura
|
||||
// =========================================================
|
||||
|
||||
let lineaModalInstance = null;
|
||||
let lineaQuill = null;
|
||||
|
||||
function getLineaModal() {
|
||||
const el = document.getElementById('lineaFacturaModal');
|
||||
if (!el) return null;
|
||||
lineaModalInstance = bootstrap.Modal.getOrCreateInstance(el);
|
||||
return lineaModalInstance;
|
||||
}
|
||||
|
||||
function showLineaModal() {
|
||||
const m = getLineaModal();
|
||||
if (m) m.show();
|
||||
}
|
||||
|
||||
function hideLineaModal() {
|
||||
const m = getLineaModal();
|
||||
if (m) m.hide();
|
||||
}
|
||||
|
||||
function showLineaModalError(msg) {
|
||||
const $err = $('#lineaFacturaModalError');
|
||||
if (!msg) {
|
||||
$err.addClass('d-none').text('');
|
||||
return;
|
||||
}
|
||||
$err.removeClass('d-none').text(msg);
|
||||
}
|
||||
|
||||
// Quill config (igual que en presupuestos, pero solo para este editor del modal)
|
||||
function buildSnowConfig() {
|
||||
return {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'font': [] }, { 'size': [] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
[{ 'script': 'super' }, { 'script': 'sub' }],
|
||||
[{ 'header': [false, 1, 2, 3, 4, 5, 6] }, 'blockquote', 'code-block'],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'indent': '-1' }, { 'indent': '+1' }],
|
||||
['direction', { 'align': [] }]
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getLineaQuill() {
|
||||
const el = document.getElementById('lineaFacturaDescripcionEditor');
|
||||
if (!el) return null;
|
||||
|
||||
// Evita doble init y evita apuntar a DOM viejo tras reload
|
||||
if (!lineaQuill) {
|
||||
lineaQuill = new Quill(el, buildSnowConfig());
|
||||
}
|
||||
return lineaQuill;
|
||||
}
|
||||
|
||||
function setLineaDescripcionHtml(html) {
|
||||
const q = getLineaQuill();
|
||||
if (!q) return;
|
||||
q.clipboard.dangerouslyPasteHTML(html ?? '');
|
||||
}
|
||||
|
||||
function getLineaDescripcionHtml() {
|
||||
const q = getLineaQuill();
|
||||
if (!q) return '';
|
||||
return q.root.innerHTML ?? '';
|
||||
}
|
||||
|
||||
function clearLineaDescripcion() {
|
||||
const q = getLineaQuill();
|
||||
if (!q) return;
|
||||
q.setText('');
|
||||
}
|
||||
|
||||
// Formato ES: coma decimal
|
||||
const nfEs = new Intl.NumberFormat('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
|
||||
function parseEsNumber(str) {
|
||||
if (str == null) return null;
|
||||
const s = String(str).trim();
|
||||
if (!s) return null;
|
||||
const normalized = s.replace(/\./g, '').replace(',', '.');
|
||||
const n = Number(normalized);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
function formatEsNumber(n) {
|
||||
const num = Number(n);
|
||||
if (!Number.isFinite(num)) return '';
|
||||
return nfEs.format(num);
|
||||
}
|
||||
|
||||
function attachEsDecimalHandlers(selector) {
|
||||
// input: permitir dígitos y separador decimal, normalizar '.' a ','
|
||||
$container.on('input', selector, function () {
|
||||
let v = $(this).val() ?? '';
|
||||
v = String(v).replace(/[^\d\.,]/g, '');
|
||||
|
||||
const parts = v.split(/[.,]/);
|
||||
if (parts.length > 1) {
|
||||
v = parts[0] + ',' + parts.slice(1).join('');
|
||||
}
|
||||
$(this).val(v);
|
||||
});
|
||||
|
||||
// blur: formatear a 2 decimales
|
||||
$container.on('blur', selector, function () {
|
||||
const n = parseEsNumber($(this).val());
|
||||
$(this).val(formatEsNumber(n ?? 0));
|
||||
});
|
||||
}
|
||||
|
||||
// (handlers una sola vez; usan delegación)
|
||||
attachEsDecimalHandlers('#lineaFacturaBase');
|
||||
attachEsDecimalHandlers('#lineaFacturaIva4');
|
||||
attachEsDecimalHandlers('#lineaFacturaIva21');
|
||||
|
||||
|
||||
function resetLineaModal() {
|
||||
$('#lineaFacturaModalTitle').text('Nueva línea');
|
||||
$('#lineaFacturaId').val('');
|
||||
|
||||
clearLineaDescripcion();
|
||||
|
||||
$('#lineaFacturaBase').val(formatEsNumber(0));
|
||||
$('#lineaFacturaIva4').val(formatEsNumber(0));
|
||||
$('#lineaFacturaIva21').val(formatEsNumber(0));
|
||||
showLineaModalError(null);
|
||||
}
|
||||
|
||||
function fillLineaModalForEdit({ id, descripcionHtml, base, iva4, iva21 }) {
|
||||
$('#lineaFacturaModalTitle').text('Editar línea');
|
||||
$('#lineaFacturaId').val(id ?? '');
|
||||
|
||||
setLineaDescripcionHtml(descripcionHtml ?? '');
|
||||
|
||||
$('#lineaFacturaBase').val(formatEsNumber(Number(base) ?? 0));
|
||||
$('#lineaFacturaIva4').val(formatEsNumber(Number(iva4) ?? 0));
|
||||
$('#lineaFacturaIva21').val(formatEsNumber(Number(iva21) ?? 0));
|
||||
|
||||
|
||||
showLineaModalError(null);
|
||||
}
|
||||
|
||||
// Abrir modal: crear
|
||||
$container.on('click', '#btn-add-linea-factura', function () {
|
||||
getLineaQuill(); // asegura init sobre DOM actual
|
||||
resetLineaModal();
|
||||
showLineaModal();
|
||||
});
|
||||
|
||||
// Abrir modal: editar
|
||||
$container.on('click', '.btn-edit-linea-factura', function () {
|
||||
getLineaQuill(); // asegura init sobre DOM actual
|
||||
|
||||
const $btn = $(this);
|
||||
const id = $btn.data('linea-id');
|
||||
|
||||
// Leemos HTML guardado en textarea hidden (por seguridad)
|
||||
const descripcionHtml = $(`#linea-desc-${id}`).val() ?? '';
|
||||
|
||||
fillLineaModalForEdit({
|
||||
id,
|
||||
descripcionHtml,
|
||||
base: $btn.data('base'),
|
||||
iva4: $btn.data('iva4'),
|
||||
iva21: $btn.data('iva21')
|
||||
});
|
||||
|
||||
showLineaModal();
|
||||
});
|
||||
|
||||
// Borrar línea
|
||||
$container.on('click', '.btn-delete-linea-factura', function () {
|
||||
const $btn = $(this);
|
||||
const id = $btn.data('linea-id');
|
||||
const facturaId = getFacturaId();
|
||||
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['facturas.lineas.delete.title']) || 'Eliminar línea',
|
||||
html: window.languageBundle.get(['facturas.lineas.delete.text']) || 'Esta acción no se puede deshacer.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-danger w-xs mt-2',
|
||||
cancelButton: 'btn btn-light w-xs mt-2'
|
||||
},
|
||||
confirmButtonText: window.languageBundle.get(['app.eliminar']) || 'Eliminar',
|
||||
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) return;
|
||||
postAndReload(`/facturas/${facturaId}/lineas/${id}/delete`);
|
||||
});
|
||||
});
|
||||
|
||||
// Guardar (todavía sin endpoint; deja el payload preparado)
|
||||
$container.on('click', '#btnGuardarLineaFactura', function () {
|
||||
showLineaModalError(null);
|
||||
|
||||
const idLinea = $('#lineaFacturaId').val() || null;
|
||||
|
||||
const descripcionHtml = getLineaDescripcionHtml();
|
||||
const base = parseEsNumber($('#lineaFacturaBase').val());
|
||||
const iva4 = parseEsNumber($('#lineaFacturaIva4').val()) ?? 0;
|
||||
const iva21 = parseEsNumber($('#lineaFacturaIva21').val()) ?? 0;
|
||||
|
||||
if (base == null) {
|
||||
showLineaModalError(window.languageBundle['facturas.lineas.error.base'] || 'La base no es válida.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Vacío real de Quill
|
||||
const descTrim = (descripcionHtml ?? '').trim();
|
||||
const isEmptyQuill = (descTrim === '' || descTrim === '<p><br></p>');
|
||||
const descripcion = isEmptyQuill ? '' : descripcionHtml;
|
||||
|
||||
const payload = {
|
||||
id: idLinea,
|
||||
descripcion, // HTML
|
||||
base,
|
||||
iva4,
|
||||
iva21
|
||||
};
|
||||
|
||||
|
||||
// Aquí conectaremos endpoints:
|
||||
const facturaId = getFacturaId();
|
||||
const url = idLinea
|
||||
? `/facturas/${facturaId}/lineas/${idLinea}`
|
||||
: `/facturas/${facturaId}/lineas`;
|
||||
postAndReload(url, payload).done(() => hideLineaModal());
|
||||
|
||||
hideLineaModal();
|
||||
});
|
||||
|
||||
// =========================================================
|
||||
// FIN MODAL
|
||||
// =========================================================
|
||||
|
||||
|
||||
// =========================================================
|
||||
// MODAL + Quill + Flatpickr para pagos de factura
|
||||
// =========================================================
|
||||
|
||||
let pagoModalInstance = null;
|
||||
let pagoQuill = null;
|
||||
let pagoFlatpickr = null;
|
||||
|
||||
function getPagoModal() {
|
||||
const el = document.getElementById('pagoFacturaModal');
|
||||
if (!el) return null;
|
||||
pagoModalInstance = bootstrap.Modal.getOrCreateInstance(el);
|
||||
return pagoModalInstance;
|
||||
}
|
||||
|
||||
function showPagoModal() {
|
||||
const m = getPagoModal();
|
||||
if (m) m.show();
|
||||
}
|
||||
|
||||
function hidePagoModal() {
|
||||
const m = getPagoModal();
|
||||
if (m) m.hide();
|
||||
}
|
||||
|
||||
function showPagoModalError(msg) {
|
||||
const $err = $('#pagoFacturaModalError');
|
||||
if (!msg) {
|
||||
$err.addClass('d-none').text('');
|
||||
return;
|
||||
}
|
||||
$err.removeClass('d-none').text(msg);
|
||||
}
|
||||
|
||||
function getPagoQuill() {
|
||||
const el = document.getElementById('pagoFacturaNotasEditor');
|
||||
if (!el) return null;
|
||||
|
||||
// Evita doble init y evita apuntar a DOM viejo tras reload
|
||||
if (!pagoQuill) {
|
||||
pagoQuill = new Quill(el, buildSnowConfig());
|
||||
}
|
||||
return pagoQuill;
|
||||
}
|
||||
|
||||
function setPagoNotasHtml(html) {
|
||||
const q = getPagoQuill();
|
||||
if (!q) return;
|
||||
q.clipboard.dangerouslyPasteHTML(html ?? '');
|
||||
}
|
||||
|
||||
function getPagoNotasHtml() {
|
||||
const q = getPagoQuill();
|
||||
if (!q) return '';
|
||||
return q.root.innerHTML ?? '';
|
||||
}
|
||||
|
||||
function clearPagoNotas() {
|
||||
const q = getPagoQuill();
|
||||
if (!q) return;
|
||||
q.setText('');
|
||||
}
|
||||
|
||||
function getPagoFlatpickr() {
|
||||
const input = document.getElementById('pagoFacturaFecha');
|
||||
if (!input) return null;
|
||||
|
||||
if (!pagoFlatpickr) {
|
||||
pagoFlatpickr = flatpickr(input, {
|
||||
enableTime: false,
|
||||
dateFormat: "d/m/Y",
|
||||
placeholder: "",
|
||||
locale: getFlatpickrLocale()
|
||||
});
|
||||
}
|
||||
return pagoFlatpickr;
|
||||
}
|
||||
|
||||
// Convierte "dd/MM/yyyy HH:mm" => "yyyy-MM-ddTHH:mm:00" (LocalDateTime)
|
||||
function parseEsDateTimeToIsoLocal(str) {
|
||||
if (!str) return null;
|
||||
const s = String(str).trim();
|
||||
if (!s) return null;
|
||||
|
||||
const parts = s.split(' ');
|
||||
if (parts.length < 2) return null;
|
||||
|
||||
const [dmy, hm] = parts;
|
||||
const [dd, mm, yyyy] = dmy.split('/').map(n => Number(n));
|
||||
const [HH, MM] = hm.split(':').map(n => Number(n));
|
||||
|
||||
if (!dd || !mm || !yyyy || Number.isNaN(HH) || Number.isNaN(MM)) return null;
|
||||
|
||||
const pad2 = (n) => String(n).padStart(2, '0');
|
||||
return `${String(yyyy).padStart(4, '0')}-${pad2(mm)}-${pad2(dd)}T${pad2(HH)}:${pad2(MM)}:00`;
|
||||
}
|
||||
|
||||
function parseEsDateToIsoLocal(str) {
|
||||
if (!str) return null;
|
||||
const s = String(str).trim();
|
||||
if (!s) return null;
|
||||
|
||||
const [dd, mm, yyyy] = s.split('/').map(n => Number(n));
|
||||
if (!dd || !mm || !yyyy) return null;
|
||||
|
||||
const pad2 = (n) => String(n).padStart(2, '0');
|
||||
return `${String(yyyy).padStart(4, '0')}-${pad2(mm)}-${pad2(dd)}T00:00:00`;
|
||||
}
|
||||
|
||||
|
||||
function setPagoFechaFromDataAttr(fechaRaw) {
|
||||
const fp = getPagoFlatpickr();
|
||||
if (!fp) return;
|
||||
|
||||
if (!fechaRaw) { fp.clear(); return; }
|
||||
|
||||
// fechaRaw: "yyyy-MM-dd"
|
||||
const [Y, M, D] = String(fechaRaw).split('-').map(Number);
|
||||
if (!Y || !M || !D) return;
|
||||
|
||||
fp.setDate(new Date(Y, M - 1, D), true);
|
||||
}
|
||||
|
||||
|
||||
function resetPagoModal() {
|
||||
$('#pagoFacturaModalTitle').text('Nuevo pago');
|
||||
$('#pagoFacturaId').val('');
|
||||
|
||||
$('#pagoFacturaMetodo').val('tpv_tarjeta');
|
||||
$('#pagoFacturaCantidad').val(formatEsNumber(0));
|
||||
|
||||
const fp = getPagoFlatpickr();
|
||||
if (fp) fp.clear();
|
||||
|
||||
clearPagoNotas();
|
||||
showPagoModalError(null);
|
||||
}
|
||||
|
||||
function fillPagoModalForEdit({ id, metodo, cantidad, fechaRaw, notasHtml }) {
|
||||
$('#pagoFacturaModalTitle').text('Editar pago');
|
||||
$('#pagoFacturaId').val(id ?? '');
|
||||
|
||||
$('#pagoFacturaMetodo').val(metodo ?? 'tpv_tarjeta');
|
||||
$('#pagoFacturaCantidad').val(formatEsNumber(Number(cantidad) ?? 0));
|
||||
|
||||
setPagoFechaFromDataAttr(fechaRaw);
|
||||
setPagoNotasHtml(notasHtml ?? '');
|
||||
|
||||
showPagoModalError(null);
|
||||
}
|
||||
|
||||
// Formato ES para cantidad (mismo handler que líneas)
|
||||
attachEsDecimalHandlers('#pagoFacturaCantidad');
|
||||
|
||||
// Abrir modal: crear
|
||||
$container.on('click', '#btn-add-pago-factura', function () {
|
||||
getPagoQuill(); // init sobre DOM actual
|
||||
getPagoFlatpickr(); // init sobre DOM actual
|
||||
resetPagoModal();
|
||||
showPagoModal();
|
||||
});
|
||||
|
||||
// Abrir modal: editar
|
||||
$container.on('click', '.btn-edit-pago-factura', function () {
|
||||
getPagoQuill();
|
||||
getPagoFlatpickr();
|
||||
|
||||
const $btn = $(this);
|
||||
const id = $btn.data('pago-id');
|
||||
const notasHtml = $(`#pago-notas-${id}`).val() ?? '';
|
||||
|
||||
fillPagoModalForEdit({
|
||||
id,
|
||||
metodo: $btn.data('metodo'),
|
||||
cantidad: $btn.data('cantidad'),
|
||||
fechaRaw: $btn.data('fecha'), // "yyyy-MM-dd HH:mm" desde Thymeleaf
|
||||
notasHtml
|
||||
});
|
||||
|
||||
showPagoModal();
|
||||
});
|
||||
|
||||
// Borrar pago (Swal igual que líneas)
|
||||
$container.on('click', '.btn-delete-pago-factura', function () {
|
||||
const pagoId = $(this).data('pago-id');
|
||||
const facturaId = getFacturaId();
|
||||
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['facturas.pagos.delete.title']) || 'Eliminar pago',
|
||||
html: window.languageBundle.get(['facturas.pagos.delete.text']) || 'Esta acción no se puede deshacer.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-danger w-xs mt-2',
|
||||
cancelButton: 'btn btn-light w-xs mt-2'
|
||||
},
|
||||
confirmButtonText: window.languageBundle.get(['app.eliminar']) || 'Eliminar',
|
||||
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
|
||||
}).then((result) => {
|
||||
if (!result.isConfirmed) return;
|
||||
postAndReload(`/facturas/${facturaId}/pagos/${pagoId}/delete`);
|
||||
});
|
||||
});
|
||||
|
||||
// Guardar pago
|
||||
$container.on('click', '#btnGuardarPagoFactura', function () {
|
||||
showPagoModalError(null);
|
||||
|
||||
const facturaId = getFacturaId();
|
||||
const pagoId = $('#pagoFacturaId').val() || null;
|
||||
|
||||
const metodoPago = $('#pagoFacturaMetodo').val();
|
||||
const cantidad = parseEsNumber($('#pagoFacturaCantidad').val());
|
||||
const fechaStr = $('#pagoFacturaFecha').val();
|
||||
|
||||
if (cantidad == null || cantidad <= 0) {
|
||||
showPagoModalError(window.languageBundle.get(['facturas.pagos.error.cantidad']) || 'La cantidad no es válida.');
|
||||
return;
|
||||
}
|
||||
|
||||
const fechaPago = parseEsDateToIsoLocal($('#pagoFacturaFecha').val());
|
||||
|
||||
if (!fechaPago) {
|
||||
showPagoModalError(window.languageBundle.get(['facturas.pagos.error.fecha']) || 'La fecha no es válida.');
|
||||
return;
|
||||
}
|
||||
|
||||
const notasHtml = getPagoNotasHtml();
|
||||
const notasTrim = (notasHtml ?? '').trim();
|
||||
const isEmptyQuill = (notasTrim === '' || notasTrim === '<p><br></p>');
|
||||
const notas = isEmptyQuill ? '' : notasHtml;
|
||||
|
||||
const payload = {
|
||||
id: pagoId,
|
||||
metodoPago,
|
||||
cantidadPagada: cantidad,
|
||||
fechaPago,
|
||||
notas
|
||||
};
|
||||
|
||||
const url = pagoId
|
||||
? `/facturas/${facturaId}/pagos/${pagoId}`
|
||||
: `/facturas/${facturaId}/pagos`;
|
||||
|
||||
postAndReload(url, payload).done(() => hidePagoModal());
|
||||
});
|
||||
// =========================================================
|
||||
|
||||
|
||||
let facturaFechaEmisionFp = null;
|
||||
|
||||
function getFacturaFechaEmisionFlatpickr() {
|
||||
const input = document.getElementById('facturaFechaEmision');
|
||||
if (!input) return null;
|
||||
|
||||
if (!facturaFechaEmisionFp) {
|
||||
facturaFechaEmisionFp = flatpickr(input, {
|
||||
enableTime: false,
|
||||
dateFormat: "d/m/Y",
|
||||
locale: getFlatpickrLocale()
|
||||
});
|
||||
}
|
||||
return facturaFechaEmisionFp;
|
||||
}
|
||||
|
||||
// dd/MM/yyyy -> yyyy-MM-ddT00:00:00
|
||||
function parseEsDateToIsoLocal(str) {
|
||||
if (!str) return null;
|
||||
const s = String(str).trim();
|
||||
if (!s) return null;
|
||||
const [dd, mm, yyyy] = s.split('/').map(n => Number(n));
|
||||
if (!dd || !mm || !yyyy) return null;
|
||||
|
||||
const pad2 = (n) => String(n).padStart(2, '0');
|
||||
return `${String(yyyy).padStart(4, '0')}-${pad2(mm)}-${pad2(dd)}T00:00:00`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function afterFacturaRender() {
|
||||
const $root = $('#factura-container');
|
||||
const estado = $root.find('[data-factura-estado]').first().data('factura-estado');
|
||||
|
||||
destroySelect2($root);
|
||||
|
||||
if (estado === 'borrador') {
|
||||
initSelect2ForDraft($root);
|
||||
|
||||
// ✅ Fecha emisión editable con flatpickr solo en borrador
|
||||
facturaFechaEmisionFp = null; // reset por si cambió el DOM
|
||||
getFacturaFechaEmisionFlatpickr(); // init sobre DOM actual
|
||||
} else {
|
||||
facturaFechaEmisionFp = null;
|
||||
}
|
||||
|
||||
// resets que ya tenías
|
||||
lineaQuill = null;
|
||||
lineaModalInstance = null;
|
||||
|
||||
// si tienes pagos: resetea también...
|
||||
pagoQuill = null;
|
||||
pagoModalInstance = null;
|
||||
pagoFlatpickr = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
afterFacturaRender();
|
||||
});
|
||||
@ -5,4 +5,93 @@ $(() => {
|
||||
window.location.href = url;
|
||||
});
|
||||
|
||||
$(document).on('click', '.btn-pay', async function () {
|
||||
const pedidoId = parseInt($(this).data('id'));
|
||||
const amount = parseInt($(this).data('amount'));
|
||||
|
||||
const result = await swalMetodoPago();
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
const method = result.value;
|
||||
|
||||
// crear y enviar un form normal (NO ajax)
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/pagos/redsys/reintentar';
|
||||
|
||||
// CSRF (Spring Security)
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||
const csrfParam = document.querySelector('meta[name="_csrf_parameter"]')?.getAttribute('content') || '_csrf';
|
||||
|
||||
const add = (name, value) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = name;
|
||||
input.value = String(value);
|
||||
form.appendChild(input);
|
||||
};
|
||||
|
||||
add('amountCents', amount);
|
||||
add('orderId', pedidoId);
|
||||
add('method', method);
|
||||
if (csrfToken) add(csrfParam, csrfToken);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
});
|
||||
|
||||
|
||||
|
||||
function swalMetodoPago() {
|
||||
return Swal.fire({
|
||||
title: window.languageBundle['checkout.payment'] || 'Método de pago',
|
||||
width: '32rem',
|
||||
html: `
|
||||
<div style="width: 100%;" class="g-3 text-start">
|
||||
|
||||
<div class="form-check card-radio">
|
||||
<input id="swalPaymentCard" name="paymentMethod" type="radio" class="form-check-input" value="card" checked>
|
||||
<label class="form-check-label" for="swalPaymentCard">
|
||||
${window.languageBundle['checkout.payment.card'] || 'Tarjeta'}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check card-radio">
|
||||
<input id="swalPaymentBizum" name="paymentMethod" type="radio" class="form-check-input" value="bizum">
|
||||
<label class="form-check-label" for="swalPaymentBizum">
|
||||
${window.languageBundle['checkout.payment.bizum'] || 'Bizum'}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check card-radio">
|
||||
<input id="swalPaymentTransfer" name="paymentMethod" type="radio" class="form-check-input" value="bank-transfer">
|
||||
<label class="form-check-label" for="swalPaymentTransfer">
|
||||
${window.languageBundle['checkout.payment.bank-transfer'] || 'Transferencia bancaria'}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`,
|
||||
focusConfirm: false,
|
||||
showCancelButton: true,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2',
|
||||
cancelButton: 'btn btn-light'
|
||||
},
|
||||
confirmButtonText: window.languageBundle['app.aceptar'] || 'Aceptar',
|
||||
cancelButtonText: window.languageBundle['app.cancelar'] || 'Cancelar',
|
||||
|
||||
preConfirm: () => {
|
||||
const selected = document.querySelector('input[name="paymentMethod"]:checked');
|
||||
if (!selected) {
|
||||
Swal.showValidationMessage(
|
||||
window.languageBundle['checkout.error.select-method'] || 'Selecciona un método de pago'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return selected.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
@ -0,0 +1,219 @@
|
||||
$(() => {
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||
const csrfHeader = document.querySelector('meta[name="_csrf_header"]')?.getAttribute('content');
|
||||
if (window.$ && csrfToken && csrfHeader) {
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader(csrfHeader, csrfToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const language = document.documentElement.lang || 'es-ES';
|
||||
|
||||
|
||||
$(document).on('click', '.update-status-item', function () {
|
||||
const lineaId = $(this).data('linea-id');
|
||||
if (!lineaId) {
|
||||
console.error('No se ha encontrado el ID de la línea del pedido.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Llamada AJAX para actualizar el estado del pedido
|
||||
$.ajax({
|
||||
url: `/pedidos/linea/${lineaId}/update-status`,
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (!response || !response.success) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: response.message || "Error",
|
||||
timer: 1800,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2',
|
||||
cancelButton: 'btn btn-light'
|
||||
},
|
||||
showConfirmButton: false
|
||||
});
|
||||
}
|
||||
else {
|
||||
const estadoSpan = $(`.estado-linea[data-linea-id='${lineaId}']`);
|
||||
if (estadoSpan.length) {
|
||||
estadoSpan.text(response.state);
|
||||
}
|
||||
if (response.stateKey === 'enviado' || response.stateKey === 'cancelado') {
|
||||
$(`.update-estado-button[data-linea-id='${lineaId}']`)
|
||||
.closest('.update-estado-button')
|
||||
.addClass('d-none');
|
||||
}
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: response.message || "Exito",
|
||||
timer: 1800,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2',
|
||||
cancelButton: 'btn btn-light'
|
||||
},
|
||||
showConfirmButton: false
|
||||
}).then((result) => {
|
||||
if (result.dismiss === Swal.DismissReason.timer) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
;
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('Error al actualizar el estado del pedido:', error);
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: xhr.responseJSON?.message || 'Error',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.maquetacion-ok', function () {
|
||||
|
||||
const lineaId = $(this).data('linea-id');
|
||||
if (!lineaId) {
|
||||
console.error('No se ha encontrado el ID de la línea del pedido.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Llamada AJAX para marcar la maquetación como OK
|
||||
$.ajax({
|
||||
url: `/pedidos/linea/${lineaId}/update-maquetacion`,
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (!response || !response.success) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: response.message || "Error",
|
||||
timer: 1800,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2',
|
||||
cancelButton: 'btn btn-light'
|
||||
},
|
||||
showConfirmButton: false
|
||||
});
|
||||
}
|
||||
else {
|
||||
const estadoSpan = $(`.estado-linea[data-linea-id='${lineaId}']`);
|
||||
if (estadoSpan.length) {
|
||||
estadoSpan.text(response.state);
|
||||
// hide the maquetacion-ok button
|
||||
$(`.maquetacion-ok[data-linea-id='${lineaId}']`)
|
||||
.closest('.maquetacion-ok-button')
|
||||
.addClass('d-none');
|
||||
}
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: response.message || "Exito",
|
||||
timer: 1800,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2',
|
||||
cancelButton: 'btn btn-light'
|
||||
},
|
||||
showConfirmButton: false
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('Error al actualizar la maquetación del pedido:', error);
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: xhr.responseJSON?.message || 'Error',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.btn-cancel-pedido', function () {
|
||||
const pedidoId = $(this).data('pedido-id');
|
||||
if (!pedidoId) {
|
||||
console.error('No se ha encontrado el ID del pedido.');
|
||||
return;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: window.languageBundle['pedido.view.cancel-title'] || '¿Estás seguro de que deseas cancelar este pedido?',
|
||||
text: window.languageBundle['pedido.view.cancel-text'] || "Esta acción no se puede deshacer.",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: window.languageBundle['app.yes'] || 'Sí, cancelar pedido',
|
||||
cancelButtonText: window.languageBundle['app.cancel'] || 'No, mantener pedido',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-danger me-2',
|
||||
cancelButton: 'btn btn-light'
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
// Llamada AJAX para cancelar el pedido
|
||||
$.ajax({
|
||||
url: `/pedidos/cancel/${pedidoId}`,
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (!response || !response.success) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: response.message || "Error",
|
||||
timer: 1800,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2',
|
||||
cancelButton: 'btn btn-light'
|
||||
},
|
||||
showConfirmButton: false
|
||||
});
|
||||
}
|
||||
else {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: response.message || "Éxito",
|
||||
timer: 1800,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2',
|
||||
cancelButton: 'btn btn-light'
|
||||
},
|
||||
showConfirmButton: false
|
||||
}).then((result) => {
|
||||
if (result.dismiss === Swal.DismissReason.timer) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('Error al cancelar el pedido:', error);
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: xhr.responseJSON?.message || 'Error',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
@ -0,0 +1,102 @@
|
||||
$(() => {
|
||||
if ($(".btn-download-ferro").length) {
|
||||
$(document).on('click', '.btn-download-ferro', function () {
|
||||
const lineaId = $(this).data('linea-id');
|
||||
|
||||
window.open(`/pedidos/linea/${lineaId}/download-ferro`, '_blank');
|
||||
});
|
||||
}
|
||||
if ($(".btn-download-cub").length) {
|
||||
$(document).on('click', '.btn-download-cub', function () {
|
||||
const lineaId = $(this).data('linea-id');
|
||||
|
||||
window.open(`/pedidos/linea/${lineaId}/download-cub`, '_blank');
|
||||
});
|
||||
}
|
||||
if ($(".btn-download-tapa").length) {
|
||||
$(document).on('click', '.btn-download-tapa', function () {
|
||||
const lineaId = $(this).data('linea-id');
|
||||
|
||||
window.open(`/pedidos/linea/${lineaId}/download-tapa`, '_blank');
|
||||
});
|
||||
}
|
||||
|
||||
if ($(".btn-aceptar-ferro").length) {
|
||||
$(document).on('click', '.btn-aceptar-ferro', function () {
|
||||
const lineaId = $(this).data('linea-id');
|
||||
|
||||
$.ajax({
|
||||
url: `/pedidos/linea/${lineaId}/aceptar-ferro`,
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (!response || !response.success) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: response.message || "Error",
|
||||
timer: 1800,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2',
|
||||
cancelButton: 'btn btn-light'
|
||||
},
|
||||
showConfirmButton: false
|
||||
});
|
||||
}
|
||||
else {
|
||||
const estadoSpan = $(`.estado-linea[data-linea-id='${lineaId}']`);
|
||||
if (estadoSpan.length) {
|
||||
estadoSpan.text(response.state);
|
||||
}
|
||||
$(`.btn-aceptar-ferro[data-linea-id='${lineaId}']`)
|
||||
.closest('.btn-aceptar-ferro')
|
||||
.addClass('d-none');
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: response.message || "Exito",
|
||||
timer: 1800,
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2',
|
||||
cancelButton: 'btn btn-light'
|
||||
},
|
||||
showConfirmButton: false
|
||||
}).then((result) => {
|
||||
if (result.dismiss === Swal.DismissReason.timer) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
;
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('Error al aceptar el ferro del pedido:', error);
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: xhr.responseJSON?.message || 'Error',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
@ -0,0 +1,82 @@
|
||||
$(() => {
|
||||
var snowEditor = document.querySelectorAll(".snow-editor");
|
||||
if (snowEditor) {
|
||||
Array.from(snowEditor).forEach(function (item) {
|
||||
var snowEditorData = {};
|
||||
var issnowEditorVal = item.classList.contains("snow-editor");
|
||||
if (issnowEditorVal == true) {
|
||||
snowEditorData.theme = 'snow',
|
||||
snowEditorData.modules = {
|
||||
'toolbar': [
|
||||
[{
|
||||
'font': []
|
||||
}, {
|
||||
'size': []
|
||||
}],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{
|
||||
'color': []
|
||||
}, {
|
||||
'background': []
|
||||
}],
|
||||
[{
|
||||
'script': 'super'
|
||||
}, {
|
||||
'script': 'sub'
|
||||
}],
|
||||
[{
|
||||
'header': [false, 1, 2, 3, 4, 5, 6]
|
||||
}, 'blockquote', 'code-block'],
|
||||
[{
|
||||
'list': 'ordered'
|
||||
}, {
|
||||
'list': 'bullet'
|
||||
}, {
|
||||
'indent': '-1'
|
||||
}, {
|
||||
'indent': '+1'
|
||||
}],
|
||||
['direction', {
|
||||
'align': []
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
var quill = new Quill(item, snowEditorData);
|
||||
|
||||
var initialContent = item.dataset.contenido || "";
|
||||
// Contenido inicial desde Thymeleaf
|
||||
var initialContent = item.dataset.contenido || "";
|
||||
if (initialContent) {
|
||||
if(initialContent.trim() !== "" && initialContent.trim() !== "<p><br></p>")
|
||||
$('.badge-comentario').removeClass('d-none');
|
||||
quill.clipboard.dangerouslyPasteHTML(initialContent);
|
||||
}
|
||||
|
||||
quill.root.addEventListener("blur", function () {
|
||||
|
||||
let contenido = quill.root.innerHTML;
|
||||
if(contenido.trim() !== "" && contenido.trim() !== "<p><br></p>"){
|
||||
$('.badge-comentario').removeClass('d-none');
|
||||
} else {
|
||||
$('.badge-comentario').addClass('d-none');
|
||||
}
|
||||
if ($('#presupuesto_id').length > 0 && $('#presupuesto_id').val() !== "") {
|
||||
|
||||
$.ajax({
|
||||
url: "/presupuesto/" + $('#presupuesto_id').val() + "/comentario",
|
||||
method: "POST",
|
||||
data: {
|
||||
comentario: contenido
|
||||
},
|
||||
success: function () {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
@ -332,7 +332,7 @@ export default class PresupuestoWizard {
|
||||
servicios: this.formData.servicios.servicios,
|
||||
datosMaquetacion: this.formData.servicios.datosMaquetacion,
|
||||
datosMarcapaginas: this.formData.servicios.datosMarcapaginas,
|
||||
cliente_id: $('#cliente_id').val() || null,
|
||||
cliente_id: $('#user_id').val() != null ? $('#user_id').val() : $('#cliente_id').val() != null ? $('#cliente_id').val() : null,
|
||||
};
|
||||
|
||||
try {
|
||||
@ -402,8 +402,6 @@ export default class PresupuestoWizard {
|
||||
...this.#getInteriorData(),
|
||||
...this.#getCubiertaData(),
|
||||
selectedTirada: this.formData.selectedTirada
|
||||
|
||||
|
||||
};
|
||||
|
||||
const sobrecubierta = data.sobrecubierta;
|
||||
@ -927,6 +925,7 @@ export default class PresupuestoWizard {
|
||||
this.#changeTab('pills-general-data');
|
||||
} else {
|
||||
const maxSolapas = data.solapas ?? 120;
|
||||
const lomo = data.lomo ?? 0;
|
||||
$('.solapas-presupuesto').attr('max', maxSolapas);
|
||||
$('.max-solapa-text').text(function (_, textoActual) {
|
||||
return textoActual.replace(/\d+/, maxSolapas);
|
||||
@ -951,6 +950,20 @@ export default class PresupuestoWizard {
|
||||
this.acabadoSobrecubierta.val(this.formData.cubierta.sobrecubierta.acabado);
|
||||
this.fajaSobrecubierta.val(this.formData.cubierta.faja.acabado);
|
||||
|
||||
if(lomo < 10){
|
||||
this.formData.cubierta.cabezada = "NOCAB";
|
||||
this.cabezada.val("NOCAB");
|
||||
this.cabezada.prop("disabled", true);
|
||||
if(this.formData.cubierta.tipoCubierta === 'tapaDuraLomoRedondo'){
|
||||
this.formData.cubierta.tipoCubierta = 'tapaDura';
|
||||
}
|
||||
$("#tapaDuraLomoRedondo").addClass("d-none");
|
||||
}
|
||||
else{
|
||||
this.cabezada.prop("disabled", false);
|
||||
$("#tapaDuraLomoRedondo").removeClass("d-none");
|
||||
}
|
||||
|
||||
this.#loadCubiertaData();
|
||||
this.summaryTableCubierta.removeClass('d-none');
|
||||
if (this.sobrecubierta.hasClass('active')) {
|
||||
@ -1683,7 +1696,7 @@ export default class PresupuestoWizard {
|
||||
|
||||
const body = {
|
||||
presupuesto: this.#getPresupuestoData(),
|
||||
save: this.opts.canSave,
|
||||
save: this.opts.mode == 'public' ? true : this.opts.canSave,
|
||||
mode: this.opts.mode,
|
||||
servicios: servicios,
|
||||
datosMaquetacion: this.formData.servicios.datosMaquetacion,
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
$(() => {
|
||||
|
||||
// Inicializar select2 para el campo de cliente en el formulario de presupuesto
|
||||
if ($('#user_id').length) {
|
||||
$('#user_id').select2({
|
||||
allowClear: false,
|
||||
width: '100%',
|
||||
ajax: {
|
||||
url: '/users/api/get-users',
|
||||
dataType: 'json',
|
||||
data: function (params) {
|
||||
return {
|
||||
q: params.term, // término de búsqueda
|
||||
page: params.page || 1,
|
||||
size: 10,
|
||||
showUsername: true
|
||||
};
|
||||
},
|
||||
delay: 250,
|
||||
processResults: function (data) {
|
||||
return {
|
||||
results: data.results || [],
|
||||
pagination: data.pagination || { more: false }
|
||||
};
|
||||
},
|
||||
cache: true
|
||||
},
|
||||
minimumInputLength: 0
|
||||
});
|
||||
|
||||
// Si hay un valor inicial, cargar y establecer el usuario seleccionado
|
||||
const initialUserId = $('#user_id').val();
|
||||
if (initialUserId) {
|
||||
$.ajax({
|
||||
url: `/users/api/get-user/${initialUserId}`,
|
||||
dataType: 'json'
|
||||
}).then(function (data) {
|
||||
const option = new Option(`${data.fullName} (${data.userName})`, data.id, true, true);
|
||||
$('#user_id').append(option).trigger('change');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
import { duplicar, reimprimir } from './presupuesto-utils.js';
|
||||
|
||||
$(()=> {
|
||||
$('.duplicar-btn').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
const id = $('#presupuesto_id').val();
|
||||
const tituloOriginal = $('#titulo').text().trim() == '' ? $('#titulo').val().trim() : $('#titulo').text().trim();
|
||||
duplicar(id, tituloOriginal);
|
||||
})
|
||||
|
||||
$('.reimprimir-btn').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
const id = $('#presupuesto_id').val();
|
||||
reimprimir(id);
|
||||
})
|
||||
})
|
||||
@ -1,3 +1,4 @@
|
||||
import { duplicar, reimprimir } from './presupuesto-utils.js';
|
||||
(() => {
|
||||
// si jQuery está cargado, añade CSRF a AJAX
|
||||
const csrfToken = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||
@ -46,7 +47,7 @@
|
||||
url: '/presupuesto/datatable/clientes',
|
||||
method: 'GET',
|
||||
},
|
||||
order: [[0, 'asc']],
|
||||
order: [[0, 'desc']],
|
||||
columns: [
|
||||
{ data: 'id', name: 'id', orderable: true },
|
||||
{ data: 'titulo', name: 'titulo', orderable: true },
|
||||
@ -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) {
|
||||
const colName = $(this).data('col');
|
||||
const colIndex = table.column(colName + ':name').index();
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { preguntarTipoPresupuesto } from './presupuesto-utils.js';
|
||||
import { preguntarTipoPresupuesto, duplicar, reimprimir } from './presupuesto-utils.js';
|
||||
|
||||
(() => {
|
||||
// si jQuery está cargado, añade CSRF a AJAX
|
||||
@ -48,7 +48,7 @@ import { preguntarTipoPresupuesto } from './presupuesto-utils.js';
|
||||
url: '/presupuesto/datatable/anonimos',
|
||||
method: 'GET',
|
||||
},
|
||||
order: [[0, 'asc']],
|
||||
order: [[0, 'desc']],
|
||||
columns: [
|
||||
{ data: 'id', name: 'id', orderable: true },
|
||||
{ data: 'titulo', name: 'titulo', orderable: true },
|
||||
@ -174,7 +174,7 @@ import { preguntarTipoPresupuesto } from './presupuesto-utils.js';
|
||||
url: '/presupuesto/datatable/clientes',
|
||||
method: 'GET',
|
||||
},
|
||||
order: [[0, 'asc']],
|
||||
order: [[0, 'desc']],
|
||||
columns: [
|
||||
{ data: 'id', name: 'id', orderable: true },
|
||||
{ data: 'user', name: 'user.fullName', orderable: true },
|
||||
@ -200,6 +200,26 @@ import { preguntarTipoPresupuesto } from './presupuesto-utils.js';
|
||||
}
|
||||
});
|
||||
|
||||
$('#presupuestos-clientes-datatable').on('click', '.btn-duplicate-privado', function (e) {
|
||||
|
||||
e.preventDefault();
|
||||
const id = $(this).data('id');
|
||||
let data = table_clientes.row($(this).parents('tr')).data();
|
||||
const tituloOriginal = data.titulo;
|
||||
|
||||
duplicar(id, tituloOriginal);
|
||||
});
|
||||
|
||||
$('#presupuestos-clientes-datatable').on('click', '.btn-reprint-privado', function (e) {
|
||||
|
||||
e.preventDefault();
|
||||
const id = $(this).data('id');
|
||||
let data = table_clientes.row($(this).parents('tr')).data();
|
||||
const tituloOriginal = data.titulo;
|
||||
|
||||
reimprimir(id, tituloOriginal);
|
||||
});
|
||||
|
||||
$('#presupuestos-clientes-datatable').on('click', '.btn-delete-privado', function (e) {
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
@ -75,3 +75,171 @@ export async function preguntarTipoPresupuesto() {
|
||||
}
|
||||
}).then((r) => (r.isConfirmed ? r.value : null));
|
||||
}
|
||||
|
||||
export function duplicar(id, titulo) {
|
||||
|
||||
// swal with input
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['presupuesto.duplicar.title']) || 'Duplicar presupuesto',
|
||||
html: window.languageBundle.get(['presupuesto.duplicar.text']) || `¿Deseas duplicar el presupuesto "${titulo}"?`,
|
||||
icon: 'question',
|
||||
input: 'text',
|
||||
inputValue: `[D] ${titulo}`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: window.languageBundle.get(['presupuesto.duplicar.confirm']) || 'Sí, duplicar',
|
||||
cancelButtonText: window.languageBundle.get(['presupuesto.duplicar.cancelar']) || 'Cancelar',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
inputValidator: (value) => {
|
||||
if (!value) {
|
||||
return window.languageBundle.get(['presupuesto.duplicar.required']) || 'El título no puede estar vacío';
|
||||
}
|
||||
},
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
|
||||
const tituloNuevo = result.value;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: `/presupuesto/api/duplicar/${id}`,
|
||||
data: {
|
||||
titulo: tituloNuevo,
|
||||
},
|
||||
method: 'POST',
|
||||
success: function (response) {
|
||||
|
||||
if (response.id && response.id > 0) {
|
||||
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['presupuesto.duplicar.success.title']) || 'Presupuesto duplicado',
|
||||
text: window.languageBundle.get(['presupuesto.duplicar.success.text']) || `El presupuesto "${titulo}" ha sido duplicado correctamente.`,
|
||||
icon: 'success',
|
||||
confirmButtonText: window.languageBundle.get(['presupuesto.duplicar.aceptar']) || 'Aceptar',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
|
||||
},
|
||||
}).then(() => {
|
||||
// Recargar la página o redirigir a la lista de presupuestos
|
||||
window.location.href = '/presupuesto/edit/' + response.id;
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['presupuesto.duplicar.error.title']) || 'Error al duplicar',
|
||||
text: response.message || window.languageBundle.get(['presupuesto.duplicar.error.internal']) || 'Ha ocurrido un error al duplicar el presupuesto.',
|
||||
icon: 'error',
|
||||
confirmButtonText: window.languageBundle.get(['presupuesto.duplicar.aceptar']) || 'Aceptar',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['presupuesto.duplicar.error.title']) || 'Error al duplicar',
|
||||
text: xhr.responseJSON?.message || window.languageBundle.get(['presupuesto.duplicar.error.internal']) || 'Ha ocurrido un error al duplicar el presupuesto.',
|
||||
icon: 'error',
|
||||
confirmButtonText: window.languageBundle.get(['presupuesto.duplicar.aceptar']) || 'Aceptar',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function reimprimir(id) {
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['presupuesto.reimprimir.title']) || 'Reimprimir presupuesto',
|
||||
html: window.languageBundle.get(['presupuesto.reimprimir.text']) || `¿Deseas reimprimir el presupuesto?`,
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: window.languageBundle.get(['presupuesto.reimprimir.confirm']) || 'Sí, reimprimir',
|
||||
cancelButtonText: window.languageBundle.get(['presupuesto.reimprimir.cancelar']) || 'Cancelar',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary me-2', // clases para el botón confirmar
|
||||
cancelButton: 'btn btn-light' // clases para cancelar
|
||||
},
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: `/presupuesto/api/reimprimir/${id}`,
|
||||
method: 'POST',
|
||||
success: function (response) {
|
||||
|
||||
if (response.id && response.id > 0) {
|
||||
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['presupuesto.reimprimir.success.title']) || 'Presupuesto reimpreso',
|
||||
text: window.languageBundle.get(['presupuesto.reimprimir.success.text']) || `El presupuesto ha sido reimpreso correctamente.`,
|
||||
icon: 'success',
|
||||
confirmButtonText: window.languageBundle.get(['presupuesto.reimprimir.aceptar']) || 'Aceptar',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
|
||||
},
|
||||
}).then(() => {
|
||||
// Recargar la página o redirigir a la lista de presupuestos
|
||||
window.location.href = '/presupuesto/edit/' + response.id;
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['presupuesto.reimprimir.error.title']) || 'Error al reimprimir',
|
||||
text: response.message || window.languageBundle.get(['presupuesto.reimprimir.error.internal']) || 'Ha ocurrido un error al reimprimir el presupuesto.',
|
||||
icon: 'error',
|
||||
confirmButtonText: window.languageBundle.get(['presupuesto.reimprimir.aceptar']) || 'Aceptar',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
Swal.fire({
|
||||
title: window.languageBundle.get(['presupuesto.reimprimir.error.title']) || 'Error al reimprimir',
|
||||
text: xhr.responseJSON?.message || window.languageBundle.get(['presupuesto.reimprimir.error.internal']) || 'Ha ocurrido un error al reimprimir el presupuesto.',
|
||||
icon: 'error',
|
||||
confirmButtonText: window.languageBundle.get(['presupuesto.reimprimir.aceptar']) || 'Aceptar',
|
||||
buttonsStyling: false,
|
||||
customClass: {
|
||||
confirmButton: 'btn btn-secondary' // clases para el botón confirmar
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ $(() => {
|
||||
e.preventDefault();
|
||||
|
||||
// obtén el id de donde lo tengas (data-attr o variable global)
|
||||
const id = $('#presupuesto-id').val();
|
||||
const id = $('#presupuesto_id').val();
|
||||
|
||||
const url = `/api/pdf/presupuesto/${id}?mode=download`;
|
||||
|
||||
@ -47,7 +47,7 @@ $(() => {
|
||||
|
||||
$('.add-cart-btn').on('click', async () => {
|
||||
|
||||
const presupuestoId = $('#presupuesto-id').val();
|
||||
const presupuestoId = $('#presupuesto_id').val();
|
||||
const res = await $.ajax({
|
||||
url: `/cart/add/${presupuestoId}`,
|
||||
method: 'POST',
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
const scripts = [
|
||||
"/assets/libs/toastify-js/src/toastify.js",
|
||||
"/assets/libs/choices.js/public/assets/scripts/choices.min.js",
|
||||
"/assets/libs/flatpickr/flatpickr.min.js",
|
||||
"/assets/libs/feather-icons/feather.min.js" // <- AÑADIMOS feather aquí
|
||||
];
|
||||
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
<!doctype html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<body>
|
||||
|
||||
<!-- Fragment: Modal para Alta/Edición de Serie de Facturación -->
|
||||
<th:block th:fragment="modal">
|
||||
|
||||
<div class="modal fade" id="serieFacturacionModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="serieFacturacionModalTitle" th:text="#{series-facturacion.modal.title.add}">
|
||||
Añadir serie
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="modal-body">
|
||||
|
||||
<!-- Alert placeholder (JS lo rellena) -->
|
||||
<div id="serieFacturacionAlert" class="alert alert-danger d-none" role="alert"></div>
|
||||
|
||||
<form id="serieFacturacionForm" novalidate>
|
||||
<!-- Para editar: el JS setea este id -->
|
||||
<input type="hidden" id="serie_id" name="id" value="">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="nombre_serie" class="form-label" th:text="#{series-facturacion.form.nombre}">
|
||||
Nombre
|
||||
</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="nombre_serie"
|
||||
name="nombre_serie"
|
||||
maxlength="100"
|
||||
required>
|
||||
<div class="invalid-feedback" th:text="#{app.validation.required}">
|
||||
Campo obligatorio
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="prefijo" class="form-label" th:text="#{series-facturacion.form.prefijo}">
|
||||
Prefijo
|
||||
</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="prefijo"
|
||||
name="prefijo"
|
||||
maxlength="10"
|
||||
required>
|
||||
<div class="invalid-feedback" th:text="#{app.validation.required}">
|
||||
Campo obligatorio
|
||||
</div>
|
||||
<div class="form-text" th:text="#{series-facturacion.form.prefijo.help}">
|
||||
Ej: FAC, F25...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="tipo" class="form-label" th:text="#{series-facturacion.form.tipo}">
|
||||
Tipo
|
||||
</label>
|
||||
<!-- En BD solo hay facturacion, pero lo dejamos como select por UI -->
|
||||
<select class="form-select" id="tipo" name="tipo" required>
|
||||
<option value="facturacion" th:text="#{series-facturacion.tipo.facturacion}">
|
||||
Facturación
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="numero_actual" class="form-label" th:text="#{series-facturacion.form.numero-actual}">
|
||||
Número actual
|
||||
</label>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
id="numero_actual"
|
||||
name="numero_actual"
|
||||
min="1"
|
||||
step="1"
|
||||
value="1"
|
||||
required>
|
||||
<div class="invalid-feedback" th:text="#{app.validation.required}">
|
||||
Campo obligatorio
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="modal-footer">
|
||||
<button type="button"
|
||||
class="btn btn-light"
|
||||
data-bs-dismiss="modal"
|
||||
th:text="#{app.cancelar}">
|
||||
Cancelar
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="btn btn-secondary"
|
||||
id="serieFacturacionSaveBtn">
|
||||
<i class="ri-save-line align-bottom me-1"></i>
|
||||
<span th:text="#{app.guardar}">Guardar</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</th:block>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user