feat: Add stock management features including stock reservations and updates to cart and checkout processes

This commit is contained in:
2026-04-09 10:03:57 +02:00
parent 618c65accb
commit cbcdb823db
25 changed files with 279 additions and 25 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,45 @@
# Generated by Django 6.0.1 on 2026-04-09 06:55
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tienda', '0003_order_transaction_code'),
]
operations = [
migrations.AddField(
model_name='product',
name='stock',
field=models.PositiveIntegerField(default=0),
),
migrations.CreateModel(
name='StockReservation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('session_key', models.CharField(blank=True, max_length=40, null=True)),
('status', models.CharField(choices=[('active', 'Activa'), ('completed', 'Completada'), ('cancelled', 'Cancelada'), ('expired', 'Expirada')], default='active', max_length=20)),
('payment_method', models.CharField(choices=[('stripe', 'Stripe'), ('paypal', 'PayPal')], max_length=20)),
('expires_at', models.DateTimeField(db_index=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stock_reservations', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='StockReservationItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(default=1)),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stock_reservation_items', to='tienda.product')),
('reservation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='tienda.stockreservation')),
],
options={
'unique_together': {('reservation', 'product')},
},
),
]
+21 -2
View File
@@ -12,6 +12,17 @@
{% if cart_items %}
<div class="row mt-4">
<div class="col-md-8">
{% if stock_issues %}
<div class="alert alert-warning">
<strong>Hay productos con stock insuficiente:</strong>
<ul class="mb-0 mt-2">
{% for issue in stock_issues %}
<li>{{ issue.product_name }} - disponible: {{ issue.available }}, en carrito: {{ issue.requested }}</li>
{% endfor %}
</ul>
<div class="mt-2">Actualiza las cantidades antes de continuar al pago.</div>
</div>
{% endif %}
<div class="card">
<div class="card-body">
<table class="table">
@@ -20,6 +31,7 @@
<th>Producto</th>
<th>Precio (sin IVA)</th>
<th>Cantidad</th>
<th>Stock</th>
<th>Subtotal (con IVA)</th>
<th>Acciones</th>
</tr>
@@ -39,10 +51,17 @@
<td>
<form method="post" action="{% url 'update_cart_item' item.id %}" class="d-flex align-items-center" style="max-width: 150px;">
{% csrf_token %}
<input type="number" name="quantity" value="{{ item.quantity }}" min="1" class="form-control form-control-sm me-2" style="width: 70px;">
<input type="number" name="quantity" value="{{ item.quantity }}" min="1" max="{{ item.product.stock }}" class="form-control form-control-sm me-2" style="width: 70px;">
<button type="submit" class="btn btn-sm btn-primary">Actualizar</button>
</form>
</td>
<td>
{% if item.product.stock > 0 %}
{{ item.product.stock }}
{% else %}
<span class="text-danger">0</span>
{% endif %}
</td>
<td class="price">{{ item.get_subtotal_with_vat|format_price }} €</td>
<td>
<form method="post" action="{% url 'remove_from_cart' item.id %}" style="display: inline;">
@@ -82,7 +101,7 @@
<strong class="price">{{ cart.get_total_with_vat|format_price }} €</strong>
</div>
<div class="d-grid gap-2">
<a href="{% url 'checkout' %}" class="btn btn-primary btn-lg">Proceder al Pago</a>
<a href="{% url 'checkout' %}" class="btn btn-primary btn-lg {% if stock_issues %}disabled{% endif %}" {% if stock_issues %}aria-disabled="true"{% endif %}>Proceder al Pago</a>
<a href="{% url 'index' %}" class="btn btn-outline-secondary">Continuar Comprando</a>
<form method="post" action="{% url 'clear_cart' %}">
{% csrf_token %}
+25 -3
View File
@@ -49,6 +49,23 @@
</div>
{% if cart_items %}
{% if stock_issues %}
<div class="alert alert-warning">
<strong>No hay stock suficiente para algunos productos:</strong>
<ul class="mb-0 mt-2">
{% for issue in stock_issues %}
<li>{{ issue.product_name }} - disponible: {{ issue.available }}, en carrito: {{ issue.requested }}</li>
{% endfor %}
</ul>
<div class="mt-2">Vuelve al carrito y ajusta las cantidades antes de pagar.</div>
</div>
{% endif %}
<div class="alert alert-info">
Al pulsar en pagar se reservará tu stock durante <strong>{{ reservation_minutes }} minutos</strong>.
Si el pago no se completa en ese tiempo, la reserva se cancelará automáticamente.
</div>
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title mb-3">1) Selecciona la dirección de envío</h5>
@@ -80,6 +97,7 @@
<th>Producto</th>
<th class="text-end">Precio (sin IVA)</th>
<th class="text-end">Cantidad</th>
<th class="text-end">Stock actual</th>
<th class="text-end">Subtotal (con IVA)</th>
</tr>
</thead>
@@ -89,6 +107,7 @@
<td>{{ item.product.name }}</td>
<td class="text-end">{{ item.product.price|format_price }}€</td>
<td class="text-end">{{ item.quantity }}</td>
<td class="text-end">{{ item.product.stock }}</td>
<td class="text-end">{{ item.get_subtotal_with_vat|format_price }}€</td>
</tr>
{% endfor %}
@@ -118,7 +137,7 @@
class="btn btn-primary payment-btn"
data-config-url="/tienda/config/"
data-session-url="/tienda/create-checkout-session/"
{% if not addresses %}disabled{% endif %}>
{% if not addresses or stock_issues %}disabled{% endif %}>
💳 Pagar con Stripe
</button>
@@ -126,7 +145,7 @@
id="paypal-button"
class="btn btn-warning payment-btn"
data-payment-url="{% url 'create_paypal_payment' %}"
{% if not addresses %}disabled{% endif %}>
{% if not addresses or stock_issues %}disabled{% endif %}>
🅿️ Pagar con PayPal
</button>
</div>
@@ -139,7 +158,9 @@
<script>
// Manejo del botón de PayPal
document.getElementById('paypal-button').addEventListener('click', async function(e) {
const paypalButton = document.getElementById('paypal-button');
if (paypalButton) {
paypalButton.addEventListener('click', async function(e) {
e.preventDefault();
const shippingAddressSelect = document.getElementById('shipping-address');
@@ -202,5 +223,6 @@
button.innerHTML = originalText;
}
});
}
</script>
{% endblock %}
@@ -46,6 +46,14 @@
</div>
</div>
<!-- Stock -->
<div class="mb-3">
<label for="stock" class="form-label">Stock disponible <span class="text-danger">*</span></label>
<input type="number" class="form-control" id="stock" name="stock" required
min="0" step="1" placeholder="0">
<div class="form-text">Cantidad máxima que podrán comprar los clientes.</div>
</div>
<!-- Categoría -->
<div class="mb-3">
<label for="category" class="form-label">Categoría <span class="text-danger">*</span></label>
@@ -46,6 +46,14 @@
</div>
</div>
<!-- Stock -->
<div class="mb-3">
<label for="stock" class="form-label">Stock disponible <span class="text-danger">*</span></label>
<input type="number" class="form-control" id="stock" name="stock" required
min="0" step="1" value="{{ producto.stock }}" placeholder="0">
<div class="form-text">Cantidad máxima que podrán comprar los clientes.</div>
</div>
<!-- Categoría -->
<div class="mb-3">
<label for="category" class="form-label">Categoría <span class="text-danger">*</span></label>
@@ -31,6 +31,7 @@
<th>Categoría</th>
<th class="text-end">Precio (sin IVA)</th>
<th class="text-end">Precio (con IVA)</th>
<th class="text-end">Stock</th>
<th class="text-end">Acciones</th>
</tr>
</thead>
@@ -53,6 +54,7 @@
<td>{{ producto.category.name }}</td>
<td class="text-end">{{ producto.price|format_price }}€</td>
<td class="text-end text-success"><strong>{{ producto.get_price_with_vat|format_price }}€</strong></td>
<td class="text-end">{{ producto.stock }}</td>
<td class="text-end">
<div class="d-flex justify-content-end gap-2">
<a href="{% url 'editar_producto' producto.id %}" class="btn btn-outline-primary btn-sm">Editar</a>
+9 -2
View File
@@ -39,14 +39,21 @@
<div id="descripcion">
{{ product.briefdesc }}
</div>
<div class="mt-3">
{% if product.stock > 0 %}
<span class="badge bg-success">Stock disponible: {{ product.stock }}</span>
{% else %}
<span class="badge bg-danger">Sin stock</span>
{% endif %}
</div>
<form method="post" action="{% url 'add_to_cart' product.id %}" class="mt-4">
{% csrf_token %}
<div class="mb-3">
<label for="quantity" class="form-label">Cantidad:</label>
<input type="number" name="quantity" id="quantity" value="1" min="1" class="form-control" style="max-width: 100px;">
<input type="number" name="quantity" id="quantity" value="1" min="1" max="{{ product.stock }}" class="form-control" style="max-width: 100px;" {% if product.stock == 0 %}disabled{% endif %}>
</div>
<button type="submit" class="btn btn-primary btn-lg">🛒 Agregar al Carrito</button>
<button type="submit" class="btn btn-primary btn-lg" {% if product.stock == 0 %}disabled{% endif %}>🛒 Agregar al Carrito</button>
</form>
</div>
</div>
+161 -18
View File
@@ -335,7 +335,7 @@ def _cancel_active_stock_reservations_for_request(request: HttpRequest):
).update(status=StockReservation.STATUS_CANCELLED)
def _get_reserved_quantities_by_product(product_ids, exclude_reservation_id=None):
def _get_reserved_quantities_by_product(product_ids, exclude_reservation_ids=None):
if not product_ids:
return {}
@@ -344,27 +344,38 @@ def _get_reserved_quantities_by_product(product_ids, exclude_reservation_id=None
reservation__status=StockReservation.STATUS_ACTIVE,
reservation__expires_at__gt=timezone.now(),
)
if exclude_reservation_id:
reserved_qs = reserved_qs.exclude(reservation_id=exclude_reservation_id)
if exclude_reservation_ids:
reserved_qs = reserved_qs.exclude(reservation_id__in=exclude_reservation_ids)
reserved_totals = reserved_qs.values("product_id").annotate(total_reserved=models.Sum("quantity"))
return {row["product_id"]: row["total_reserved"] for row in reserved_totals}
def _get_available_stock_by_product(product_ids, exclude_reservation_id=None):
def _get_active_reservation_ids_for_request(request: HttpRequest):
_release_expired_stock_reservations()
return list(
StockReservation.objects.filter(
**_get_reservation_owner_filters(request),
status=StockReservation.STATUS_ACTIVE,
expires_at__gt=timezone.now(),
).values_list("id", flat=True)
)
def _get_available_stock_by_product(product_ids, exclude_reservation_ids=None):
_release_expired_stock_reservations()
products = Product.objects.filter(id__in=product_ids)
stocks = {product.id: product.stock for product in products}
reserved = _get_reserved_quantities_by_product(product_ids, exclude_reservation_id=exclude_reservation_id)
reserved = _get_reserved_quantities_by_product(product_ids, exclude_reservation_ids=exclude_reservation_ids)
return {
product_id: max(stocks.get(product_id, 0) - reserved.get(product_id, 0), 0)
for product_id in product_ids
}
def _get_cart_stock_issues(cart_items, exclude_reservation_id=None):
def _get_cart_stock_issues(cart_items, exclude_reservation_ids=None):
product_ids = [item.product_id for item in cart_items]
available_by_product = _get_available_stock_by_product(product_ids, exclude_reservation_id=exclude_reservation_id)
available_by_product = _get_available_stock_by_product(product_ids, exclude_reservation_ids=exclude_reservation_ids)
issues = []
for item in cart_items:
@@ -527,7 +538,7 @@ def create_order_from_cart(request, payment_method, payment_reference="", shippi
reserved_from_others = _get_reserved_quantities_by_product(
product_ids,
exclude_reservation_id=locked_reservation.id if locked_reservation else None,
exclude_reservation_ids=[locked_reservation.id] if locked_reservation else None,
)
for item in cart_items:
@@ -604,9 +615,25 @@ def add_to_cart(request: HttpRequest, product_id: int):
try:
product = Product.objects.get(id=product_id)
cart = get_or_create_cart(request)
_cancel_active_stock_reservations_for_request(request)
_clear_stock_reservation_session(request)
# Obtener cantidad del POST o por defecto 1
quantity = int(request.POST.get('quantity', 1))
if quantity <= 0:
messages.error(request, "La cantidad debe ser mayor que cero.")
return redirect('producto', id=product_id)
existing_item = CartItem.objects.filter(cart=cart, product=product).first()
desired_quantity = quantity if existing_item is None else existing_item.quantity + quantity
available = _get_available_stock_by_product([product.id]).get(product.id, 0)
if desired_quantity > available:
messages.error(
request,
f"No hay stock suficiente de '{product.name}'. Disponible: {available}, solicitado: {desired_quantity}.",
)
return redirect('producto', id=product_id)
# Buscar si ya existe el producto en el carrito
cart_item, created = CartItem.objects.get_or_create(
@@ -632,6 +659,10 @@ def add_to_cart(request: HttpRequest, product_id: int):
})
return redirect('view_cart')
except ValueError:
messages.error(request, "Cantidad no válida.")
return redirect('producto', id=product_id)
except Product.DoesNotExist:
messages.error(request, "Producto no encontrado.")
@@ -641,9 +672,13 @@ def add_to_cart(request: HttpRequest, product_id: int):
def view_cart(request: HttpRequest):
"""Muestra el contenido del carrito"""
cart = get_or_create_cart(request)
cart_items = list(cart.items.select_related("product"))
active_reservation_ids = _get_active_reservation_ids_for_request(request)
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
return render(request, "tienda/cart.html", {
"cart": cart,
"cart_items": cart.items.all()
"cart_items": cart_items,
"stock_issues": stock_issues,
})
@@ -652,10 +687,21 @@ def update_cart_item(request: HttpRequest, item_id: int):
try:
cart = get_or_create_cart(request)
cart_item = CartItem.objects.get(id=item_id, cart=cart)
_cancel_active_stock_reservations_for_request(request)
_clear_stock_reservation_session(request)
quantity = int(request.POST.get('quantity', 1))
if quantity > 0:
available = _get_available_stock_by_product([cart_item.product_id]).get(cart_item.product_id, 0)
if quantity > available:
messages.error(
request,
f"No hay stock suficiente de '{cart_item.product.name}'. Disponible: {available}, solicitado: {quantity}.",
)
return redirect('view_cart')
cart_item.quantity = quantity
cart_item.save()
messages.success(request, "Cantidad actualizada.")
@@ -668,12 +714,17 @@ def update_cart_item(request: HttpRequest, item_id: int):
except CartItem.DoesNotExist:
messages.error(request, "Producto no encontrado en el carrito.")
return redirect('view_cart')
except ValueError:
messages.error(request, "Cantidad no válida.")
return redirect('view_cart')
def remove_from_cart(request: HttpRequest, item_id: int):
"""Elimina un producto del carrito"""
try:
cart = get_or_create_cart(request)
_cancel_active_stock_reservations_for_request(request)
_clear_stock_reservation_session(request)
cart_item = CartItem.objects.get(id=item_id, cart=cart)
product_name = cart_item.product.name
cart_item.delete()
@@ -688,6 +739,8 @@ def remove_from_cart(request: HttpRequest, item_id: int):
def clear_cart(request: HttpRequest):
"""Vacía todo el carrito"""
cart = get_or_create_cart(request)
_cancel_active_stock_reservations_for_request(request)
_clear_stock_reservation_session(request)
cart.items.all().delete()
messages.success(request, "Carrito vaciado.")
return redirect('view_cart')
@@ -769,12 +822,13 @@ def crear_producto(request: HttpRequest):
briefdesc = request.POST.get("briefdesc")
description = request.POST.get("description")
price = request.POST.get("price")
stock = request.POST.get("stock")
category_id = request.POST.get("category")
primary_image_file = request.FILES.get("primary_image")
secondary_images_files = request.FILES.getlist("secondary_images")
# Validaciones
if not all([name, description, price, category_id]):
if not all([name, description, price, stock, category_id]):
messages.error(request, "Por favor completa todos los campos obligatorios.")
categories = Category.objects.all()
return render(request, "tienda/crear_producto.html", {"categories": categories})
@@ -787,6 +841,15 @@ def crear_producto(request: HttpRequest):
messages.error(request, "El precio debe ser un número válido.")
categories = Category.objects.all()
return render(request, "tienda/crear_producto.html", {"categories": categories})
try:
stock = int(stock)
if stock < 0:
raise ValueError("El stock no puede ser negativo")
except ValueError:
messages.error(request, "El stock debe ser un número entero válido.")
categories = Category.objects.all()
return render(request, "tienda/crear_producto.html", {"categories": categories})
try:
category = Category.objects.get(id=category_id)
@@ -809,6 +872,7 @@ def crear_producto(request: HttpRequest):
briefdesc=briefdesc or "",
description=description,
price=price,
stock=stock,
category=category,
primary_image=primary_image,
creator=request.user
@@ -841,11 +905,12 @@ def editar_producto(request: HttpRequest, id: int):
briefdesc = request.POST.get("briefdesc")
description = request.POST.get("description")
price = request.POST.get("price")
stock = request.POST.get("stock")
category_id = request.POST.get("category")
primary_image_file = request.FILES.get("primary_image")
secondary_images_files = request.FILES.getlist("secondary_images")
if not all([name, description, price, category_id]):
if not all([name, description, price, stock, category_id]):
messages.error(request, "Por favor completa todos los campos obligatorios.")
categories = Category.objects.all()
return render(request, "tienda/editar_producto.html", {
@@ -865,6 +930,18 @@ def editar_producto(request: HttpRequest, id: int):
"producto": producto
})
try:
stock = int(stock)
if stock < 0:
raise ValueError("El stock no puede ser negativo")
except ValueError:
messages.error(request, "El stock debe ser un número entero válido.")
categories = Category.objects.all()
return render(request, "tienda/editar_producto.html", {
"categories": categories,
"producto": producto
})
try:
category = Category.objects.get(id=category_id)
except Category.DoesNotExist:
@@ -879,6 +956,7 @@ def editar_producto(request: HttpRequest, id: int):
producto.briefdesc = briefdesc or ""
producto.description = description
producto.price = price
producto.stock = stock
producto.category = category
if primary_image_file:
@@ -925,12 +1003,16 @@ def borrar_producto(request: HttpRequest, id: int):
@login_required
def checkout(request: HttpRequest):
cart = get_or_create_cart(request)
cart_items = cart.items.select_related("product")
cart_items = list(cart.items.select_related("product"))
active_reservation_ids = _get_active_reservation_ids_for_request(request)
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
addresses = ShippingAddress.objects.filter(user=request.user)
return render(request, "tienda/checkout.html", {
"cart": cart,
"cart_items": cart_items,
"addresses": addresses,
"stock_issues": stock_issues,
"reservation_minutes": STOCK_RESERVATION_MINUTES,
})
@csrf_exempt
@@ -954,11 +1036,24 @@ def create_checkout_session(request: HttpRequest):
return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400)
cart = get_or_create_cart(request)
cart_items = cart.items.select_related("product")
cart_items = list(cart.items.select_related("product"))
if not cart_items.exists():
if not cart_items:
return JsonResponse({"error": "El carrito está vacío"}, status=400)
active_reservation_ids = _get_active_reservation_ids_for_request(request)
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
if stock_issues:
return JsonResponse({"error": _build_stock_issue_message(stock_issues[0])}, status=400)
reservation, reservation_issues = _create_stock_reservation_for_cart(
request,
cart_items,
StockReservation.PAYMENT_STRIPE,
)
if reservation is None:
return JsonResponse({"error": reservation_issues[0]}, status=400)
stripe.api_key = settings.STRIPE_SECRET_KEY
line_items = []
@@ -995,6 +1090,8 @@ def create_checkout_session(request: HttpRequest):
request.session['stripe_session_id'] = session.id
request.session['selected_shipping_address_id'] = shipping_address.id
request.session[STOCK_RESERVATION_SESSION_KEY] = reservation.id
request.session[STOCK_RESERVATION_PAYMENT_SESSION_KEY] = StockReservation.PAYMENT_STRIPE
return JsonResponse({"sessionId": session.id})
except Exception as e:
@@ -1002,20 +1099,37 @@ def create_checkout_session(request: HttpRequest):
return JsonResponse({"error": f"Error al crear sesión de pago: {str(e)}"}, status=500)
@login_required
def checkout_success(request: HttpRequest):
payment_reference = request.session.get('stripe_session_id', "")
shipping_address_id = request.session.get('selected_shipping_address_id')
shipping_address = ShippingAddress.objects.filter(id=shipping_address_id, user=request.user).first()
order = create_order_from_cart(request, Order.PAYMENT_STRIPE, payment_reference, shipping_address)
reservation = _get_session_stock_reservation(request, StockReservation.PAYMENT_STRIPE)
order, order_error = create_order_from_cart(
request,
Order.PAYMENT_STRIPE,
payment_reference,
shipping_address,
stock_reservation=reservation,
)
if order is None:
messages.error(request, order_error)
return redirect("checkout")
if 'stripe_session_id' in request.session:
del request.session['stripe_session_id']
if 'selected_shipping_address_id' in request.session:
del request.session['selected_shipping_address_id']
_clear_stock_reservation_session(request)
messages.success(request, "Pago realizado correctamente. ¡Gracias por tu compra!")
return render(request, "tienda/checkout_success.html", {"order": order})
@login_required
def checkout_cancel(request: HttpRequest):
_cancel_active_stock_reservations_for_request(request)
_clear_stock_reservation_session(request)
messages.info(request, "Pago cancelado. Puedes intentarlo de nuevo cuando quieras.")
return render(request, "tienda/checkout_cancel.html", {})
@@ -1057,11 +1171,24 @@ def create_paypal_payment(request: HttpRequest):
import paypalrestsdk
cart = get_or_create_cart(request)
cart_items = cart.items.select_related("product")
cart_items = list(cart.items.select_related("product"))
if not cart_items.exists():
if not cart_items:
return JsonResponse({"error": "El carrito está vacío"}, status=400)
active_reservation_ids = _get_active_reservation_ids_for_request(request)
stock_issues = _get_cart_stock_issues(cart_items, exclude_reservation_ids=active_reservation_ids)
if stock_issues:
return JsonResponse({"error": _build_stock_issue_message(stock_issues[0])}, status=400)
reservation, reservation_issues = _create_stock_reservation_for_cart(
request,
cart_items,
StockReservation.PAYMENT_PAYPAL,
)
if reservation is None:
return JsonResponse({"error": reservation_issues[0]}, status=400)
# Configurar PayPal
paypalrestsdk.configure({
"mode": settings.PAYPAL_MODE,
@@ -1124,6 +1251,8 @@ def create_paypal_payment(request: HttpRequest):
# Guardar el payment ID en sesión
request.session['paypal_payment_id'] = payment.id
request.session['selected_shipping_address_id'] = shipping_address.id
request.session[STOCK_RESERVATION_SESSION_KEY] = reservation.id
request.session[STOCK_RESERVATION_PAYMENT_SESSION_KEY] = StockReservation.PAYMENT_PAYPAL
# Encontrar el link de aprobación
for link in payment.links:
@@ -1178,13 +1307,25 @@ def paypal_execute(request: HttpRequest):
# Pago exitoso - crear pedido y limpiar el carrito
shipping_address_id = request.session.get('selected_shipping_address_id')
shipping_address = ShippingAddress.objects.filter(id=shipping_address_id, user=request.user).first()
order = create_order_from_cart(request, Order.PAYMENT_PAYPAL, payment_id, shipping_address)
reservation = _get_session_stock_reservation(request, StockReservation.PAYMENT_PAYPAL)
order, order_error = create_order_from_cart(
request,
Order.PAYMENT_PAYPAL,
payment_id,
shipping_address,
stock_reservation=reservation,
)
if order is None:
messages.error(request, order_error)
return redirect("checkout")
# Limpiar la sesión
if 'paypal_payment_id' in request.session:
del request.session['paypal_payment_id']
if 'selected_shipping_address_id' in request.session:
del request.session['selected_shipping_address_id']
_clear_stock_reservation_session(request)
messages.success(request, "¡Pago realizado correctamente con PayPal! Gracias por tu compra.")
return render(request, "tienda/checkout_success.html", {"order": order})
@@ -1197,6 +1338,8 @@ def paypal_execute(request: HttpRequest):
logger.exception("PAYPAL_EXECUTE_EXCEPTION user_id=%s error=%s", request.user.id, str(e))
messages.error(request, f"Error: {str(e)}")
return redirect("checkout")
def search_suggestions(request: HttpRequest):
"""API AJAX que retorna sugerencias de búsqueda en JSON"""
query = request.GET.get('q', '').strip()