From f2a1c7bc2eeecc147cb7cbb539bf7a9eca3c370c Mon Sep 17 00:00:00 2001 From: xgc1564 Date: Tue, 18 Nov 2025 23:02:26 +0100 Subject: [PATCH 1/3] Funcionalidad CRUD carrito de compra implementada --- essenza/essenza/urls.py | 1 + essenza/order/models.py | 4 +- essenza/order/urls.py | 13 ++ essenza/order/views.py | 224 ++++++++++++++++++- essenza/templates/base.html | 29 ++- essenza/templates/order/cart_detail.html | 248 +++++++++++++++++++++ essenza/templates/product/detail_user.html | 56 +++-- 7 files changed, 551 insertions(+), 24 deletions(-) create mode 100644 essenza/order/urls.py create mode 100644 essenza/templates/order/cart_detail.html diff --git a/essenza/essenza/urls.py b/essenza/essenza/urls.py index 2390f456..b5ae40c6 100644 --- a/essenza/essenza/urls.py +++ b/essenza/essenza/urls.py @@ -14,6 +14,7 @@ path("", DashboardView.as_view(), name="dashboard"), path("catalogo/", CatalogView.as_view(), name="catalog"), path("catalogo//", CatalogDetailView.as_view(), name="catalog_detail"), + path("order/", include("order.urls")), ] if settings.DEBUG: diff --git a/essenza/order/models.py b/essenza/order/models.py index 28579759..75eb6193 100644 --- a/essenza/order/models.py +++ b/essenza/order/models.py @@ -1,19 +1,17 @@ from django.db import models from django.utils import timezone - # Create your models here. class Status(models.TextChoices): PENDING = "pending", "Pending" PAID = "paid", "Paid" SHIPPED = "shipped", "Shipped" - class Order(models.Model): user = models.ForeignKey( "user.Usuario", on_delete=models.CASCADE, related_name="orders" ) - address = models.CharField(max_length=255) + address = models.CharField(max_length=255, null=True, blank=True) placed_at = models.DateTimeField(default=timezone.now) status = models.CharField( max_length=10, choices=Status.choices, default=Status.PENDING diff --git a/essenza/order/urls.py b/essenza/order/urls.py new file mode 100644 index 00000000..12fb04c2 --- /dev/null +++ b/essenza/order/urls.py @@ -0,0 +1,13 @@ +# order/urls.py +from django.urls import include, path +from . import views + +urlpatterns = [ + #path('order/', include('order.urls')), + path('', views.CartDetailView.as_view(), name='order_home'), + path('cart/', views.CartDetailView.as_view(), name='cart_detail'), + path('add//', views.AddToCartView.as_view(), name='add_to_cart'), + path('update//', views.UpdateCartItemView.as_view(), name='update_cart_item'), + path('update/session//', views.UpdateCartSessionView.as_view(), name='update_cart_session'), + +] \ No newline at end of file diff --git a/essenza/order/views.py b/essenza/order/views.py index 91ea44a2..9908ed4f 100644 --- a/essenza/order/views.py +++ b/essenza/order/views.py @@ -1,3 +1,223 @@ -from django.shortcuts import render +from django.shortcuts import get_object_or_404, redirect, render +from django.views import View +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib import messages +from django.db.models import F # Importado para operaciones atómicas +from django.core.exceptions import ObjectDoesNotExist -# Create your views here. +# Importaciones de tus modelos +from .models import Order, OrderProduct, Status +from product.models import Product + +# -------------------------------------------------------------------- +# 1. FUNCIÓN AUXILIAR NECESARIA +# -------------------------------------------------------------------- +def get_or_create_cart(request): + """ + Obtiene la Order más reciente con status='PENDING' (asumida como carrito) + o crea una nueva Order en estado 'PENDING'. Solo para usuarios logueados. + """ + if request.user.is_authenticated: + # Lógica para usuarios logueados + try: + cart = Order.objects.filter( + user=request.user, + status=Status.PENDING + ).order_by('-placed_at').first() + + if cart is None: + raise ObjectDoesNotExist + + except ObjectDoesNotExist: + cart = Order.objects.create( + user=request.user, + status=Status.PENDING, + address=None + ) + + return cart + else: + # Los anónimos usan la sesión (o se les niega el acceso POST) + return None + +# -------------------------------------------------------------------- +# 2. VISTAS +# -------------------------------------------------------------------- + +# order/views.py (Fragmento de CartDetailView) + +class CartDetailView(View): + """Muestra el contenido del carrito activo del usuario (DB) o de la sesión (Anónimo).""" + template_name = 'order/cart_detail.html' + + def get(self, request): + cart = None + + if request.user.is_authenticated: + # LÓGICA 1: Usuario logueado (lee de la DB) + cart = get_or_create_cart(request) + cart_items = cart.order_products.all() + + else: + # LÓGICA 2: Usuario anónimo (lee de la Sesión) + cart_session = request.session.get('cart_session', {}) + cart_items = [] + + # Si hay ítems en la sesión, construimos una lista para la plantilla + if cart_session: + product_pks = [int(pk) for pk in cart_session.keys()] + + # Buscamos todos los objetos Product de la DB de una vez + products = Product.objects.filter(pk__in=product_pks) + + # Iteramos sobre los productos para crear la lista de ítems del carrito + for product in products: + pk_str = str(product.pk) + quantity = cart_session[pk_str]['quantity'] + + # Creamos un objeto temporal para pasarlo al template + cart_items.append({ + 'product': product, + 'quantity': quantity, + 'subtotal': quantity * product.price, + 'pk': product.pk, + }) + + context = { + 'cart': cart, # Será None para anónimos + 'cart_items': cart_items, # Lista de DB objects o dicts/temp objects + } + return render(request, self.template_name, context) +# ======================================================= +# AÑADIR AL CARRITO (Añadido/Corregido) +# ======================================================= +class AddToCartView(View): # LoginRequiredMixin eliminado + """ + Añade un producto al carrito, usando DB (Logueado) o Session (Anónimo). + """ + def post(self, request, product_pk): + product = get_object_or_404(Product, pk=product_pk) + + try: + quantity = int(request.POST.get('quantity', 1)) + if quantity < 1: + quantity = 1 + except ValueError: + quantity = 1 + + cart = get_or_create_cart(request) # Devuelve Order (logueado) o None (anónimo) + + # --- LÓGICA DE MANEJO DEL CARRITO --- + if cart: + # 1. USUARIO LOGUEADO (cart es un objeto Order) + + # Línea 88: Ya no falla porque 'cart' es un objeto Order. + cart_item = cart.order_products.filter(product=product).first() + + if cart_item: + # UPDATE (DB) + cart_item.quantity = F('quantity') + quantity + cart_item.save(update_fields=['quantity']) + cart_item.refresh_from_db() + messages.success(request, f"Se ha añadido {quantity} unidad(es) de '{product.name}'. Cantidad total: {cart_item.quantity}") + else: + # CREATE (DB) + OrderProduct.objects.create( + order=cart, + product=product, + quantity=quantity + ) + messages.success(request, f"'{product.name}' se ha añadido al carrito.") + + else: + # 2. USUARIO ANÓNIMO (cart es None, usamos la sesión) + + cart_session = request.session.get('cart_session', {}) + product_pk_str = str(product_pk) + + if product_pk_str in cart_session: + # UPDATE (SESSION) + cart_session[product_pk_str]['quantity'] += quantity + messages.success(request, f"Se ha añadido {quantity} unidad(es) de '{product.name}'. Cantidad total en carrito: {cart_session[product_pk_str]['quantity']}") + else: + # CREATE (SESSION) + cart_session[product_pk_str] = { + 'quantity': quantity, + 'price': str(product.price) + } + messages.success(request, f"'{product.name}' se ha añadido al carrito.") + + # Guardar y marcar la sesión + request.session['cart_session'] = cart_session + request.session.modified = True + + return redirect('cart_detail') +# ======================================================= +# ACTUALIZAR CANTIDAD EN EL CARRITO +# ======================================================= +class UpdateCartSessionView(View): + """Actualiza la cantidad de un ítem existente en el carrito de la sesión (Anónimo).""" + def post(self, request, product_pk): + if request.user.is_authenticated: + # Protección: si un usuario logueado intenta usar esta URL, redirigir a la vista DB + return redirect('cart_detail') + + cart_session = request.session.get('cart_session', {}) + product_pk_str = str(product_pk) + + # 1. Obtener la nueva cantidad + try: + new_quantity = int(request.POST.get('quantity', 0)) + except ValueError: + new_quantity = -1 + + # Necesitamos el objeto Product para el nombre y el stock + product = get_object_or_404(Product, pk=product_pk) + + # 2. Lógica de Actualización/Eliminación + if product_pk_str in cart_session: + if new_quantity <= 0: + # ELIMINAR + del cart_session[product_pk_str] + messages.info(request, f"'{product.name}' ha sido eliminado del carrito.") + else: + # ACTUALIZAR + # Opcional: limitar al stock disponible + if new_quantity > product.stock: + new_quantity = product.stock + messages.warning(request, f"Solo quedan {product.stock} unidades de '{product.name}'. Cantidad limitada.") + + cart_session[product_pk_str]['quantity'] = new_quantity + messages.success(request, f"Cantidad de '{product.name}' actualizada a {new_quantity}.") + + # 3. Guardar sesión + request.session['cart_session'] = cart_session + request.session.modified = True + + return redirect('cart_detail') + +class UpdateCartItemView(View): + """Actualiza la cantidad de un ítem existente en el carrito.""" + def post(self, request, item_pk): + cart_item = get_object_or_404(OrderProduct, pk=item_pk) + cart = get_or_create_cart(request) + + if cart_item.order.pk != cart.pk: + messages.error(request, "El ítem no pertenece a tu carrito activo.") + return redirect('cart_detail') + + try: + new_quantity = int(request.POST.get('quantity', 0)) + except ValueError: + new_quantity = -1 + + if new_quantity <= 0: + item_name = cart_item.product.name + cart_item.delete() + messages.info(request, f"'{item_name}' ha sido eliminado del carrito.") + else: + cart_item.quantity = new_quantity + cart_item.save(update_fields=['quantity']) + messages.success(request, f"Cantidad de '{cart_item.product.name}' actualizada a {new_quantity}.") + + return redirect('cart_detail') \ No newline at end of file diff --git a/essenza/templates/base.html b/essenza/templates/base.html index 5e04e967..036b9908 100644 --- a/essenza/templates/base.html +++ b/essenza/templates/base.html @@ -241,6 +241,25 @@ box-shadow: 0 3px 8px rgba(192, 107, 62, 0.35), inset 0 1px 2px rgba(255, 255, 255, 0.25); } + /* ESTILOS AÑADIDOS PARA EL CARRITO */ + .cart-icon { + color: #c06b3e; + margin-right: 20px; + + display: inline-flex; + align-items: center; + text-decoration: none; + transition: color 0.2s ease; + } + .cart-icon:hover { + color: #bf6230; + } + .cart-icon svg { + /* Tamaño deseado del icono */ + width: 32px; + height: 32px; + } +/* FIN ESTILOS AÑADIDOS */ /* Responsive */ @media (max-width: 768px) { @@ -286,7 +305,15 @@ /> - + {% if not user.role == "admin" %} + + + + + + + + {% endif %} + {% endfor %} + + +
+
+ TOTAL: {{ cart.total_price|floatformat:2|intcomma }} € +
+
+ + {% else %} +
+

Tu carrito está vacío

+

¡Añade productos de nuestro Catálogo para empezar!

+
+ {% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/essenza/templates/product/detail_user.html b/essenza/templates/product/detail_user.html index c038e202..6e2d0736 100644 --- a/essenza/templates/product/detail_user.html +++ b/essenza/templates/product/detail_user.html @@ -2,6 +2,23 @@ - Essenza{% endblock %} {% block extra_head %}