falta la vista de los presupuestos aceptados

This commit is contained in:
2025-11-09 20:43:57 +01:00
parent 032e44b9c5
commit cc696d7a99
18 changed files with 868 additions and 118 deletions

View File

@ -11,6 +11,8 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.math.BigDecimal;
import java.math.RoundingMode;
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
@ -20,10 +22,10 @@ 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.direcciones.Direccion;
import com.imprimelibros.erp.direcciones.DireccionService;
import com.imprimelibros.erp.externalApi.skApiClient;
import com.imprimelibros.erp.pedido.PedidoService;
import com.imprimelibros.erp.pedidos.Pedido;
import com.imprimelibros.erp.pedidos.PedidoService;
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
@Service
@ -191,14 +193,13 @@ public class CartService {
return resumen;
}
public Map<String, Object> getCartSummary(Cart cart, Locale locale) {
public Map<String, Object> getCartSummaryRaw(Cart cart, Locale locale) {
double base = 0.0;
double iva4 = 0.0;
double iva21 = 0.0;
double shipment = 0.0;
Boolean errorShipementCost = false;
boolean errorShipementCost = false;
List<CartItem> items = cart.getItems();
List<CartDireccion> direcciones = cart.getDirecciones();
@ -209,28 +210,29 @@ public class CartService {
base += p.getBaseImponible().doubleValue();
iva4 += p.getIvaImporte4().doubleValue();
iva21 += p.getIvaImporte21().doubleValue();
if (cart.getOnlyOneShipment() != null && cart.getOnlyOneShipment()) {
// Si es envío único, que es a españa y no ha canarias
if (direcciones != null && direcciones.size() > 0) {
if (direcciones != null && !direcciones.isEmpty()) {
CartDireccion cd = direcciones.get(0);
Boolean freeShipment = direccionService.checkFreeShipment(cd.getDireccion().getCp(),
boolean freeShipment = direccionService.checkFreeShipment(
cd.getDireccion().getCp(),
cd.getDireccion().getPaisCode3()) && !cd.getIsPalets();
if (!freeShipment) {
Integer unidades = p.getSelectedTirada();
Map<String, Object> res = getShippingCost(cd, peso, unidades, locale);
if (res.get("success").equals(Boolean.FALSE)) {
if (Boolean.FALSE.equals(res.get("success"))) {
errorShipementCost = true;
} else {
shipment += (Double) res.get("shipment");
iva21 += (Double) res.get("iva21");
}
}
// si tiene prueba de envio, hay que añadir el coste
if (p.getServiciosJson() != null && p.getServiciosJson().contains("ejemplar-prueba")) {
// ejemplar de prueba
if (p.getServiciosJson() != null && p.getServiciosJson().contains("ejemplar-prueba")) {
Map<String, Object> res = getShippingCost(cd, peso, 1, locale);
if (res.get("success").equals(Boolean.FALSE)) {
if (Boolean.FALSE.equals(res.get("success"))) {
errorShipementCost = true;
} else {
shipment += (Double) res.get("shipment");
@ -239,23 +241,27 @@ public class CartService {
}
}
} else {
// envio por cada presupuesto
// buscar la direccion asignada a este presupuesto
if (direcciones == null)
continue;
List<CartDireccion> cd_presupuesto = direcciones.stream()
.filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(p.getId())
&& d.getUnidades() != null && d.getUnidades() != null && d.getUnidades() > 0)
.filter(d -> d.getPresupuesto() != null
&& d.getPresupuesto().getId().equals(p.getId())
&& d.getUnidades() != null
&& d.getUnidades() > 0)
.toList();
Boolean firstDirection = true;
boolean firstDirection = true;
for (CartDireccion cd : cd_presupuesto) {
Integer unidades = cd.getUnidades();
if (firstDirection) {
Boolean freeShipment = direccionService.checkFreeShipment(cd.getDireccion().getCp(),
boolean freeShipment = direccionService.checkFreeShipment(
cd.getDireccion().getCp(),
cd.getDireccion().getPaisCode3()) && !cd.getIsPalets();
if (!freeShipment && unidades != null && unidades > 0) {
Map<String, Object> res = getShippingCost(cd, peso, unidades, locale);
if (res.get("success").equals(Boolean.FALSE)) {
if (Boolean.FALSE.equals(res.get("success"))) {
errorShipementCost = true;
} else {
shipment += (Double) res.get("shipment");
@ -265,7 +271,7 @@ public class CartService {
firstDirection = false;
} else {
Map<String, Object> res = getShippingCost(cd, peso, unidades, locale);
if (res.get("success").equals(Boolean.FALSE)) {
if (Boolean.FALSE.equals(res.get("success"))) {
errorShipementCost = true;
} else {
shipment += (Double) res.get("shipment");
@ -273,15 +279,17 @@ public class CartService {
}
}
}
// ejemplar de prueba
CartDireccion cd_prueba = direcciones.stream()
.filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(p.getId())
.filter(d -> d.getPresupuesto() != null
&& d.getPresupuesto().getId().equals(p.getId())
&& d.getUnidades() == null)
.findFirst().orElse(null);
if (cd_prueba != null) {
if (cd_prueba != null) {
Map<String, Object> res = getShippingCost(cd_prueba, peso, 1, locale);
if (res.get("success").equals(Boolean.FALSE)) {
if (Boolean.FALSE.equals(res.get("success"))) {
errorShipementCost = true;
} else {
shipment += (Double) res.get("shipment");
@ -291,11 +299,45 @@ public class CartService {
}
}
double total = base + iva4 + iva21 + shipment;
double totalBeforeDiscount = base + iva4 + iva21 + shipment;
int fidelizacion = pedidoService.getDescuentoFidelizacion();
double descuento = (total) * fidelizacion / 100.0;
total -= descuento;
double descuento = totalBeforeDiscount * fidelizacion / 100.0;
double total = totalBeforeDiscount - descuento;
// Redondeo a 2 decimales
base = Utils.round2(base);
iva4 = Utils.round2(iva4);
iva21 = Utils.round2(iva21);
shipment = Utils.round2(shipment);
descuento = Utils.round2(descuento);
total = Utils.round2(total);
Map<String, Object> summary = new HashMap<>();
summary.put("base", base);
summary.put("iva4", iva4);
summary.put("iva21", iva21);
summary.put("shipment", shipment);
summary.put("fidelizacion", fidelizacion);
summary.put("descuento", descuento);
summary.put("total", total);
summary.put("amountCents", Math.round(total * 100));
summary.put("errorShipmentCost", errorShipementCost);
summary.put("cartId", cart.getId());
return summary;
}
public Map<String, Object> getCartSummary(Cart cart, Locale locale) {
Map<String, Object> raw = getCartSummaryRaw(cart, locale);
double base = (Double) raw.get("base");
double iva4 = (Double) raw.get("iva4");
double iva21 = (Double) raw.get("iva21");
double shipment = (Double) raw.get("shipment");
int fidelizacion = (Integer) raw.get("fidelizacion");
double descuento = (Double) raw.get("descuento");
double total = (Double) raw.get("total");
Map<String, Object> summary = new HashMap<>();
summary.put("base", Utils.formatCurrency(base, locale));
@ -303,11 +345,11 @@ public class CartService {
summary.put("iva21", Utils.formatCurrency(iva21, locale));
summary.put("shipment", Utils.formatCurrency(shipment, locale));
summary.put("fidelizacion", fidelizacion + "%");
summary.put("descuento", Utils.formatCurrency(-descuento, locale));
summary.put("descuento", Utils.formatCurrency(-descuento, locale)); // negativo para mostrar
summary.put("total", Utils.formatCurrency(total, locale));
summary.put("amountCents", Math.round(total * 100));
summary.put("errorShipmentCost", errorShipementCost);
summary.put("cartId", cart.getId());
summary.put("amountCents", raw.get("amountCents"));
summary.put("errorShipmentCost", raw.get("errorShipmentCost"));
summary.put("cartId", raw.get("cartId"));
return summary;
}
@ -402,17 +444,18 @@ public class CartService {
// delete cart directions by direccion id in ACTIVE carts
@Transactional
public void deleteCartDireccionesByDireccionId(Long direccionId) {
cartDireccionRepo.deleteByDireccionIdAndCartStatus(direccionId, Cart.Status.ACTIVE);
}
@Transactional
public Long crearPedido(Long cartId) {
public Long crearPedido(Long cartId, Locale locale) {
Cart cart = this.getCartById(cartId);
List<CartItem> items = cart.getItems();
List<Map<String, Object>> presupuestoRequests = new ArrayList<>();
List<Long> presupuestoIds = new ArrayList<>();
for (Integer i = 0; i < items.size(); i++) {
CartItem item = items.get(i);
@ -427,9 +470,8 @@ public class CartService {
if (datosCabecera != null) {
Object tituloOriginal = datosCabecera.get("titulo");
datosCabecera.put(
"titulo",
"[" + (i + 1) + "/" + items.size() + "] " + (tituloOriginal != null ? tituloOriginal : "")
);
"titulo",
"[" + (i + 1) + "/" + items.size() + "] " + (tituloOriginal != null ? tituloOriginal : ""));
}
}
Map<String, Object> direcciones_presupuesto = this.getDireccionesPresupuesto(cart, p);
@ -453,24 +495,36 @@ public class CartService {
@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()) {
if (presupuestoRequests.isEmpty()) {
throw new IllegalStateException("No se pudieron guardar los presupuestos en SK.");
}
else{
List<Long> presupuestoIds = new ArrayList<>();
} else {
ArrayList<Long> presupuestoSkIds = new ArrayList<>();
for (Map<String, Object> presData : presupuestoRequests) {
Long presId = ((Number) presData.get("id")).longValue();
presupuestoIds.add(presId);
presupuestoSkIds.add(presId);
}
Map<String, Object> ids = new HashMap<>();
ids.put("presupuesto_ids", presupuestoIds);
ids.put("presupuesto_ids", presupuestoSkIds);
Long pedidoId = skApiClient.crearPedido(ids);
return pedidoId;
if (pedidoId == null) {
throw new IllegalStateException("No se pudo crear el pedido en SK.");
}
Pedido pedidoInterno = pedidoService.crearPedido(presupuestoIds, this.getCartSummaryRaw(cart, locale),
"Safekat", String.valueOf(pedidoId), cart.getUserId());
return pedidoInterno.getId();
}
}
@ -510,7 +564,7 @@ public class CartService {
for (CartDireccion cd : cart.getDirecciones()) {
// direccion de ejemplar de prueba
if(cd.getPresupuesto() == null || !cd.getPresupuesto().getId().equals(presupuesto.getId())) {
if (cd.getPresupuesto() == null || !cd.getPresupuesto().getId().equals(presupuesto.getId())) {
continue;
}
if (cd.getUnidades() == null || cd.getUnidades() <= 0) {
@ -535,9 +589,9 @@ public class CartService {
}
Map<String, Object> direccionesRet = new HashMap<>();
direccionesRet.put("direcciones", direccionesPresupuesto);
if(!direccionesPrueba.isEmpty())
if (!direccionesPrueba.isEmpty())
direccionesRet.put("direccionesFP1", direccionesPrueba.get(0));
else{
else {
direccionesRet.put("direccionesFP1", new ArrayList<>());
}
return direccionesRet;

View File

@ -47,10 +47,17 @@ public class Utils {
this.messageSource = messageSource;
}
public static double round2(double value) {
return BigDecimal.valueOf(value)
.setScale(2, RoundingMode.HALF_UP)
.doubleValue();
}
public static boolean isCurrentUserAdmin() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN"));
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")
|| a.getAuthority().equals("ROLE_SUPERADMIN"));
}
public static Long currentUserId(Principal principal) {

View File

@ -295,11 +295,11 @@ public class PaymentController {
}
@PostMapping(value = "/transfer/completed/{id}", produces = "application/json")
public ResponseEntity<Map<String, Object>> markTransferAsCaptured(@PathVariable Long id) {
public ResponseEntity<Map<String, Object>> markTransferAsCaptured(@PathVariable Long id, Locale locale) {
Map<String, Object> response;
try {
paymentService.markBankTransferAsCaptured(id);
paymentService.markBankTransferAsCaptured(id, locale);
response = Map.of("success", true);
return ResponseEntity.ok(response);

View File

@ -15,6 +15,7 @@ import org.springframework.transaction.annotation.Transactional;
import com.imprimelibros.erp.payments.repo.WebhookEventRepository;
import java.time.LocalDateTime;
import java.util.Locale;
import java.util.Objects;
@Service
@ -82,7 +83,7 @@ public class PaymentService {
}
@Transactional
public void handleRedsysNotification(String dsSignature, String dsMerchantParameters) throws Exception {
public void handleRedsysNotification(String dsSignature, String dsMerchantParameters, Locale locale) throws Exception {
// 0) Intentamos parsear la notificación. Si falla, registramos el webhook crudo
// y salimos.
@ -197,7 +198,7 @@ public class PaymentService {
}
if (authorized) {
processOrder(notif.cartId);
processOrder(notif.cartId, locale);
}
payRepo.save(p);
@ -317,7 +318,7 @@ public class PaymentService {
}
@Transactional
public void markBankTransferAsCaptured(Long paymentId) {
public void markBankTransferAsCaptured(Long paymentId, Locale locale) {
Payment p = payRepo.findById(paymentId)
.orElseThrow(() -> new IllegalArgumentException("Payment no encontrado: " + paymentId));
@ -354,7 +355,7 @@ public class PaymentService {
// 4) Procesar el pedido asociado al carrito (si existe)
if (p.getOrderId() != null) {
processOrder(p.getOrderId());
processOrder(p.getOrderId(), locale);
}
}
@ -450,19 +451,26 @@ public class PaymentService {
return code >= 0 && code <= 99;
}
private Boolean processOrder(Long cartId) {
// GENERAR PEDIDO A PARTIR DEL CARRITO
/**
* Procesa el pedido asociado al carrito:
* - bloquea el carrito
* - crea el pedido a partir del carrito
*
*/
private Boolean processOrder(Long cartId, Locale locale) {
Cart cart = this.cartService.findById(cartId);
if (cart != null) {
// Bloqueamos el carrito
this.cartService.lockCartById(cart.getId());
// Creamos el pedido
this.cartService.crearPedido(cart.getId());
// order ID es generado dentro de createOrderFromCart donde se marcan los
// presupuestos como no editables
// Long orderId =
// this.cartService.pedidoService.createOrderFromCart(cart.getId(), p.getId());
// p.setOrderId(orderId);
Long orderId = this.cartService.crearPedido(cart.getId(), locale);
if(orderId == null){
return false;
}
else{
// envio de correo de confirmacion de pedido podria ir aqui
}
}
return true;

View File

@ -1,42 +0,0 @@
package com.imprimelibros.erp.pedido;
import org.springframework.stereotype.Service;
import com.imprimelibros.erp.externalApi.skApiClient;
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
@Service
public class PedidoService {
protected final skApiClient skApiClient;
protected final PresupuestoService presupuestoService;
public PedidoService(skApiClient skApiClient, PresupuestoService presupuestoService) {
this.skApiClient = skApiClient;
this.presupuestoService = presupuestoService;
}
public int getDescuentoFidelizacion() {
// descuento entre el 1% y el 6% para clientes fidelidad (mas de 1500€ en el
// ultimo año)
double totalGastado = 1600.0; // Ejemplo, deberías obtenerlo del historial del cliente
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;
}
}

View File

@ -0,0 +1,191 @@
package com.imprimelibros.erp.pedidos;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "pedidos")
public class Pedido {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Campos económicos
@Column(name = "base", nullable = false)
private Double base;
@Column(name = "envio", nullable = false)
private Double envio = 0.0;
@Column(name = "iva4", nullable = false)
private Double iva4 = 0.0;
@Column(name = "iva21", nullable = false)
private Double iva21 = 0.0;
@Column(name = "descuento", nullable = false)
private Double descuento = 0.0;
@Column(name = "total", nullable = false)
private Double total = 0.0;
// Datos de proveedor
@Column(name = "proveedor", length = 100)
private String proveedor;
@Column(name = "proveedor_ref", length = 100)
private String proveedorRef;
// Auditoría básica (coincidiendo con las columnas que se ven en la captura)
@Column(name = "created_by")
private Long createdBy;
@Column(name = "updated_by")
private Long updatedBy;
@Column(name = "deleted_by")
private Long deletedBy;
@Column(name = "deleted", nullable = false)
private boolean deleted = false;
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
// --- Getters y setters ---
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Double getBase() {
return base;
}
public void setBase(Double base) {
this.base = base;
}
public Double getEnvio() {
return envio;
}
public void setEnvio(Double envio) {
this.envio = envio;
}
public Double getIva4() {
return iva4;
}
public void setIva4(Double iva4) {
this.iva4 = iva4;
}
public Double getIva21() {
return iva21;
}
public void setIva21(Double iva21) {
this.iva21 = iva21;
}
public Double getDescuento() {
return descuento;
}
public void setDescuento(Double descuento) {
this.descuento = descuento;
}
public Double getTotal() {
return total;
}
public void setTotal(Double total) {
this.total = total;
}
public String getProveedor() {
return proveedor;
}
public void setProveedor(String proveedor) {
this.proveedor = proveedor;
}
public String getProveedorRef() {
return proveedorRef;
}
public void setProveedorRef(String proveedorRef) {
this.proveedorRef = proveedorRef;
}
public Long getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Long createdBy) {
this.createdBy = createdBy;
}
public Long getUpdatedBy() {
return updatedBy;
}
public void setUpdatedBy(Long updatedBy) {
this.updatedBy = updatedBy;
}
public Long getDeletedBy() {
return deletedBy;
}
public void setDeletedBy(Long deletedBy) {
this.deletedBy = deletedBy;
}
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
}

View File

@ -0,0 +1,71 @@
package com.imprimelibros.erp.pedidos;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
@Entity
@Table(name = "pedidos_lineas")
public class PedidoLinea {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "pedido_id", nullable = false)
private Pedido pedido;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "presupuesto_id", nullable = false)
private Presupuesto presupuesto;
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "created_by", nullable = false)
private Long createdBy;
// --- Getters y setters ---
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Pedido getPedido() {
return pedido;
}
public void setPedido(Pedido pedido) {
this.pedido = pedido;
}
public Presupuesto getPresupuesto() {
return presupuesto;
}
public void setPresupuesto(Presupuesto presupuesto) {
this.presupuesto = presupuesto;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public Long getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Long createdBy) {
this.createdBy = createdBy;
}
}

View File

@ -0,0 +1,14 @@
package com.imprimelibros.erp.pedidos;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface PedidoLineaRepository extends JpaRepository<PedidoLinea, Long> {
List<PedidoLinea> findByPedidoId(Long pedidoId);
List<PedidoLinea> findByPresupuestoId(Long presupuestoId);
}

View File

@ -0,0 +1,10 @@
package com.imprimelibros.erp.pedidos;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PedidoRepository extends JpaRepository<Pedido, Long> {
// aquí podrás añadir métodos tipo:
// List<Pedido> findByDeletedFalse();
}

View File

@ -0,0 +1,102 @@
package com.imprimelibros.erp.pedidos;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
@Service
public class PedidoService {
private final PedidoRepository pedidoRepository;
private final PedidoLineaRepository pedidoLineaRepository;
private final PresupuestoRepository presupuestoRepository;
public PedidoService(PedidoRepository pedidoRepository, PedidoLineaRepository pedidoLineaRepository,
PresupuestoRepository presupuestoRepository) {
this.pedidoRepository = pedidoRepository;
this.pedidoLineaRepository = pedidoLineaRepository;
this.presupuestoRepository = presupuestoRepository;
}
public int getDescuentoFidelizacion() {
// descuento entre el 1% y el 6% para clientes fidelidad (mas de 1500€ en el
// ultimo año)
double totalGastado = 1600.0; // Ejemplo, deberías obtenerlo del historial del cliente
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;
}
/**
* 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> cartSummaryRaw,
String proveedor,
String proveedorRef,
Long userId) {
Pedido pedido = new Pedido();
// 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));
pedido.setIva4((Double) cartSummaryRaw.getOrDefault("iva4", 0.0d));
pedido.setIva21((Double) cartSummaryRaw.getOrDefault("iva21", 0.0d));
pedido.setDescuento((Double) cartSummaryRaw.getOrDefault("descuento", 0.0d));
pedido.setTotal((Double) cartSummaryRaw.getOrDefault("total", 0.0d));
// Proveedor
pedido.setProveedor(proveedor);
pedido.setProveedorRef(proveedorRef);
// Auditoría mínima
pedido.setCreatedBy(userId);
pedido.setCreatedAt(LocalDateTime.now());
pedido.setDeleted(false);
pedido.setUpdatedAt(LocalDateTime.now());
pedido.setUpdatedBy(userId);
// Guardamos el pedido
Pedido saved = pedidoRepository.save(pedido);
// Crear líneas del pedido
for (Long presupuestoId : presupuestoIds) {
Presupuesto presupuesto = presupuestoRepository.getReferenceById(presupuestoId);
PedidoLinea linea = new PedidoLinea();
linea.setPedido(saved);
linea.setPresupuesto(presupuesto);
linea.setCreatedBy(userId);
linea.setCreatedAt(LocalDateTime.now());
pedidoLineaRepository.save(linea);
}
return saved;
}
}

View File

@ -573,7 +573,7 @@ public class PresupuestoController {
String path = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest().getRequestURI();
String mode = path.contains("/view/") ? "view" : "edit";
if (mode.equals("view")) {
if (mode.equals("view") || presupuestoOpt.get().getEstado() != Presupuesto.Estado.borrador) {
model.addAttribute("appMode", "view");
} else {
model.addAttribute("cliente_id", presupuestoOpt.get().getUser().getId());

View File

@ -120,7 +120,7 @@ public class PresupuestoDatatableService {
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) ? "eye" : "pencil") + "-line\"></i></a>";
(p.getOrigen().equals(Presupuesto.Origen.publico) || p.getEstado() == Presupuesto.Estado.aceptado ? "eye" : "pencil") + "-line\"></i></a>";
String deleteBtn = borrador ? "<a href=\"javascript:void(0);\" data-id=\"" + id
+ "\" class=\"link-danger btn-delete-"

View File

@ -99,23 +99,27 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
modificado("presupuesto.estado.modificado");
private final String messageKey;
Estado(String messageKey) {
this.messageKey = messageKey;
}
public String getMessageKey() {
return messageKey;
}
}
public enum Entrega{
public enum Entrega {
peninsula("presupuesto.entrega.peninsula"),
canarias("presupuesto.entrega.canarias"),
paises_ue("presupuesto.entrega.paises-ue");
private final String messageKey;
Entrega(String messageKey) {
this.messageKey = messageKey;
}
public String getMessageKey() {
return messageKey;
}
@ -371,6 +375,18 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
@Column(name = "alto_faja")
private Integer altoFaja = 0;
@Column(name = "comentario", columnDefinition = "TEXT")
private String comentario;
@Column(name = "proveedor", length = 100)
private String proveedor;
@Column(name = "proveedor_ref1", length = 100)
private String proveedorRef1;
@Column(name = "proveedor_ref2")
private Long proveedorRef2;
// ====== MÉTODOS AUX ======
public String resumenPresupuesto() {
@ -912,16 +928,48 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
this.altoFaja = altoFaja;
}
public Long getId(){
public Long getId() {
return id;
}
public void setId(Long id){
public String getComentario() {
return comentario;
}
public void setComentario(String comentario) {
this.comentario = comentario;
}
public String getProveedor() {
return proveedor;
}
public void setProveedor(String proveedor) {
this.proveedor = proveedor;
}
public String getProveedorRef1() {
return proveedorRef1;
}
public void setProveedorRef1(String proveedorRef1) {
this.proveedorRef1 = proveedorRef1;
}
public Long getProveedorRef2() {
return proveedorRef2;
}
public void setProveedorRef2(Long proveedorRef2) {
this.proveedorRef2 = proveedorRef2;
}
public void setId(Long id) {
this.id = id;
}
public Double getPeso(){
public Double getPeso() {
// get peso from first element of pricingSnapshotJson (need to parse JSON)
// pricingSnapshotJson = {"xxx":{"peso":0.5,...}} is a String
if (this.pricingSnapshotJson != null && !this.pricingSnapshotJson.isEmpty()) {

View File

@ -103,10 +103,10 @@ public class RedsysController {
@PostMapping(value = "/ok", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseBody
public ResponseEntity<String> okPost(@RequestParam("Ds_Signature") String signature,
@RequestParam("Ds_MerchantParameters") String merchantParameters) {
@RequestParam("Ds_MerchantParameters") String merchantParameters, Locale locale) {
try {
// opcional: idempotente, si /notify ya ha hecho el trabajo no pasa nada
paymentService.handleRedsysNotification(signature, merchantParameters);
paymentService.handleRedsysNotification(signature, merchantParameters, locale);
return ResponseEntity.ok("<h2>Pago realizado correctamente</h2><a href=\"/cart\">Volver</a>");
} catch (Exception e) {
return ResponseEntity.badRequest()
@ -127,11 +127,11 @@ public class RedsysController {
@ResponseBody
public ResponseEntity<String> koPost(
@RequestParam("Ds_Signature") String signature,
@RequestParam("Ds_MerchantParameters") String merchantParameters) {
@RequestParam("Ds_MerchantParameters") String merchantParameters, Locale locale) {
try {
// Procesamos la notificación IGUAL que en /ok y /notify
paymentService.handleRedsysNotification(signature, merchantParameters);
paymentService.handleRedsysNotification(signature, merchantParameters, locale);
// Mensaje para el usuario (pago cancelado/rechazado)
String html = "<h2>Pago cancelado o rechazado</h2><a href=\"/checkout\">Volver</a>";
@ -146,9 +146,9 @@ public class RedsysController {
@PostMapping(value = "/notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ResponseBody
public String notifyRedsys(@RequestParam("Ds_Signature") String signature,
@RequestParam("Ds_MerchantParameters") String merchantParameters) {
@RequestParam("Ds_MerchantParameters") String merchantParameters, Locale locale) {
try {
paymentService.handleRedsysNotification(signature, merchantParameters);
paymentService.handleRedsysNotification(signature, merchantParameters, locale);
return "OK";
} catch (Exception e) {
e.printStackTrace(); // 👈 para ver el motivo del 500 en logs

View File

@ -0,0 +1,210 @@
databaseChangeLog:
- changeSet:
id: 0011-update-pedidos-presupuesto
author: jjo
changes:
# 1) Nuevas columnas en PRESUPUESTO
- addColumn:
tableName: presupuesto
columns:
- column:
name: comentario
type: TEXT
afterColumn: pricing_snapshot
constraints:
nullable: true
- column:
name: proveedor
type: VARCHAR(100)
constraints:
nullable: true
- column:
name: proveedor_ref1
type: VARCHAR(100)
constraints:
nullable: true
- column:
name: proveedor_ref2
type: BIGINT
constraints:
nullable: true
# 2) Cambios en PEDIDOS
# 2.1 Eliminar FK fk_pedidos_presupuesto
- dropForeignKeyConstraint:
baseTableName: pedidos
constraintName: fk_pedidos_presupuesto
# 2.2 Eliminar columna presupuesto_id
- dropColumn:
tableName: pedidos
columnName: presupuesto_id
# 2.3 Añadir nuevas columnas después de id
- addColumn:
tableName: pedidos
columns:
- column:
name: base
type: DOUBLE
afterColumn: id
constraints:
nullable: false
- column:
name: envio
type: DOUBLE
defaultValueNumeric: 0
constraints:
nullable: false
- column:
name: iva4
type: DOUBLE
defaultValueNumeric: 0
constraints:
nullable: false
- column:
name: iva21
type: DOUBLE
defaultValueNumeric: 0
constraints:
nullable: false
- column:
name: descuento
type: DOUBLE
defaultValueNumeric: 0
constraints:
nullable: false
- column:
name: total
type: DOUBLE
defaultValueNumeric: 0
constraints:
nullable: false
- column:
name: proveedor
type: VARCHAR(100)
afterColumn: total
constraints:
nullable: true
- column:
name: proveedor_ref
type: VARCHAR(100)
afterColumn: proveedor
constraints:
nullable: true
# 3) Crear tabla PEDIDOS_LINEAS
- createTable:
tableName: pedidos_lineas
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: pedido_id
type: BIGINT
constraints:
nullable: false
- column:
name: presupuesto_id
type: BIGINT
constraints:
nullable: false
- column:
name: created_at
type: DATETIME(3)
constraints:
nullable: true
- column:
name: created_by
type: BIGINT
constraints:
nullable: false
# FKs de pedidos_lineas
- addForeignKeyConstraint:
baseTableName: pedidos_lineas
baseColumnNames: pedido_id
constraintName: fk_pedidos_lineas_pedido
referencedTableName: pedidos
referencedColumnNames: id
onDelete: RESTRICT
- addForeignKeyConstraint:
baseTableName: pedidos_lineas
baseColumnNames: presupuesto_id
constraintName: fk_pedidos_lineas_presupuesto
referencedTableName: presupuesto
referencedColumnNames: id
onDelete: RESTRICT
- addForeignKeyConstraint:
baseTableName: pedidos_lineas
baseColumnNames: created_by
constraintName: fk_pedidos_lineas_created_by_user
referencedTableName: users
referencedColumnNames: id
onDelete: RESTRICT
rollback:
# 3) Eliminar tabla pedidos_lineas y sus FKs
- dropTable:
tableName: pedidos_lineas
# 2) Revertir cambios en PEDIDOS
- dropColumn:
tableName: pedidos
columns:
- column:
name: base
- column:
name: envio
- column:
name: iva4
- column:
name: iva21
- column:
name: descuento
- column:
name: total
- column:
name: proveedor
- column:
name: proveedor_ref
# 2.2 Volver a crear presupuesto_id
- addColumn:
tableName: pedidos
columns:
- column:
name: presupuesto_id
type: BIGINT
constraints:
nullable: true
# 2.1 Volver a crear la FK fk_pedidos_presupuesto
- addForeignKeyConstraint:
baseTableName: pedidos
baseColumnNames: presupuesto_id
constraintName: fk_pedidos_presupuesto
referencedTableName: presupuesto
referencedColumnNames: id
onDelete: RESTRICT
# 1) Eliminar columnas añadidas en PRESUPUESTO
- dropColumn:
tableName: presupuesto
columns:
- column:
name: comentario
- column:
name: proveedor
- column:
name: proveedor_ref1
- column:
name: proveedor_ref2

View File

@ -18,4 +18,6 @@ databaseChangeLog:
- include:
file: db/changelog/changesets/0009-add-composite-unique-txid-type.yml
- include:
file: db/changelog/changesets/0010-drop-unique-tx-gateway.yml
file: db/changelog/changesets/0010-drop-unique-tx-gateway.yml
- include:
file: db/changelog/changesets/0011-update-pedidos-presupuesto.yml

View File

@ -0,0 +1,72 @@
<div id="presupuesto-app" th:data-mode="${appMode} ?: 'public'" th:data-id="${id} ?: ''" th:fragment="presupuestador">
<div class="row" id="presupuesto-row">
<div class="animate-fadeInUpBounce">
<!-- Ribbon Shape -->
<div class="card ribbon-box border shadow-none mb-lg-0 material-shadow">
<div class="card-body">
<div class="ribbon ribbon-primary ribbon-shape" th:text="#{presupuesto.resumen}">Resumen
</div>
</div>
<div class="ribbon-content mt-4">
<div id="div-extras" class="hstack gap-2 justify-content-center flex-wrap">
</div>
</div>
</div>
<!-- End Ribbon Shape -->
<div class="col-9 mx-auto mt-4">
<h5 id="resumen-titulo" class="text-center"></h5>
<table id="resumen-tabla-final" class="table table-borderless table-striped mt-3"
th:data-currency="#{app.currency}">
<thead>
<tr>
<th></th>
<th th:text="#{presupuesto.resumen.tabla.descripcion}">Descripción</th>
<th th:text="#{presupuesto.resumen.tabla.cantidad}">Cantidad</th>
<th th:text="#{presupuesto.resumen.tabla.precio-unidad}">Precio unitario</th>
<th th:text="#{presupuesto.resumen.tabla.precio-total}">Precio total</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr class="table-active">
<th colspan="4" class="text-end" th:text="#{presupuesto.resumen.tabla.base}">Total</th>
<th class="text-end" id="resumen-base">0,00 €</th>
</tr>
<tr id="tr-resumen-iva4" class="table-active">
<th colspan="4" class="text-end" th:text="#{presupuesto.resumen.tabla.iva4}">IVA (4%)</th>
<th class="text-end" id="resumen-iva4">0,00 €</th>
</tr>
<tr id="tr-resumen-iva21" class="table-active">
<th colspan="4" class="text-end" th:text="#{presupuesto.resumen.tabla.iva21}">IVA (21%)</th>
<th class="text-end" id="resumen-iva21">0,00 €</th>
</tr>
<tr class="table-active">
<th colspan="4" class="text-end" th:text="#{presupuesto.resumen.tabla.total}">Total con IVA
</th>
<th class="text-end" id="resumen-total">0,00 €</th>
</tfoot>
</table>
</div>
<div class="d-flex justify-content-between align-items-center mt-4 w-100">
<button type="button"
class="btn btn-secondary d-flex align-items-center mx-2 btn-imprimir">
<i class="ri-printer-line me-2"></i>
<span th:text="#{app.imprimir}">Imprimir</span>
</button>
<button type="button"
class="btn btn-secondary d-flex align-items-center mx-2 add-cart-btn">
<i class="ri-shopping-cart-line me-2"></i>
<span th:text="#{presupuesto.add-to-cart}">Añadir a la cesta</span>
</button>
</div>
</div>
</div>
<!--end row-->
</div>

View File

@ -1,5 +1,7 @@
package com.imprimelibros.erp.presupuesto;
import java.util.Locale;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@ -14,7 +16,8 @@ public class savePresupuestosTest {
@Test
void testGuardarPresupuesto() {
Long resultado = cartService.crearPedido(9L);
Locale locale = new Locale("es", "ES");
Long resultado = cartService.crearPedido(9L, locale);
System.out.println("📦 Presupuesto guardado:");
System.out.println(resultado);