From c27e0c6443ee6b1789c98f109d95122d9bd9641f Mon Sep 17 00:00:00 2001 From: FRANCISCO DE CASTRO Date: Thu, 20 Nov 2025 14:11:06 +0100 Subject: [PATCH 1/8] =?UTF-8?q?Borrador=20checkout=20y=20cambios=20de=20va?= =?UTF-8?q?riables=20de=20entorno=20y=20dem=C3=A1s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- essenza/essenza/settings.py | 26 +- essenza/load_samples.bat | 109 ++++--- essenza/order/urls.py | 28 +- essenza/order/views.py | 375 +++++++++++++++-------- essenza/requirements.txt | 11 + essenza/templates/order/cancel.html | 23 ++ essenza/templates/order/cart_detail.html | 104 ++++--- essenza/templates/order/success.html | 42 +++ 9 files changed, 482 insertions(+), 238 deletions(-) create mode 100644 essenza/templates/order/cancel.html create mode 100644 essenza/templates/order/success.html diff --git a/.gitignore b/.gitignore index 1abf015..e8e27da 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ __pycache__/ # Entorno virtual venv/ -.env/ +.env .venv # Archivos de base de datos diff --git a/essenza/essenza/settings.py b/essenza/essenza/settings.py index 73abfdc..cf8b0be 100644 --- a/essenza/essenza/settings.py +++ b/essenza/essenza/settings.py @@ -10,8 +10,13 @@ https://docs.djangoproject.com/en/5.2/ref/settings/ """ +import os from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -20,12 +25,14 @@ # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-7+c*kj699pt34%5ub-x04i3%nlbhc@y+7sdew3+7!z5h-z1k_v" +SECRET_KEY = os.getenv( + "SECRET_KEY", "django-insecure-7+c*kj699pt34%5ub-x04i3%nlbhc@y+7sdew3+7!z5h-z1k_v" +) # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = os.getenv("DEBUG", "False") == "True" -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ["*"] # Application definition @@ -46,6 +53,7 @@ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", @@ -125,6 +133,9 @@ STATICFILES_DIRS = [BASE_DIR / "static"] STATIC_ROOT = BASE_DIR / "staticfiles" +# Configuración para que Whitenoise sirva los estáticos comprimidos y rápido +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" + MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media" @@ -139,3 +150,12 @@ # es el modelo de autenticación oficial. # ----------------------------------------------------------------- AUTH_USER_MODEL = "user.Usuario" + +# ----------------------------------------------------------------- +# CONFIGURACIÓN DE STRIPE Y DOMINIO (Leen del .env) +# ----------------------------------------------------------------- +STRIPE_PUBLIC_KEY = os.getenv("STRIPE_PUBLIC_KEY") +STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY") +DOMAIN_URL = os.getenv( + "DOMAIN_URL", "http://127.0.0.1:8000" +) # Default a localhost si falla diff --git a/essenza/load_samples.bat b/essenza/load_samples.bat index 8374cf9..3b62e30 100644 --- a/essenza/load_samples.bat +++ b/essenza/load_samples.bat @@ -1,48 +1,75 @@ @echo off REM --------------------------------------------------------- -REM IMPORTANTE: Este archivo borra todos los datos de tu BD local (y la crea con los datos de sampleo). -REM Las imágenes de sampleo se copian a la carpeta 'media/'. -REM También instala las dependencias necesarias definidas en 'requirements.txt' (si aun no lo están). +REM IMPORTANTE: Este archivo RESTAURA TOTALMENTE la BD del proyecto. +REM 1. Verifica entorno virtual. +REM 2. Instala dependencias. +REM 3. Borra BD y Media. +REM 4. Recrea BD y copia assets de sampleo. REM --------------------------------------------------------- -echo --- Instalando dependencias (pip)... -pip install -r requirements.txt && ( - - echo --- Borrando TODOS los datos de la BD... - python manage.py flush --noinput && ( - - echo. - echo --- Aplicando migraciones... - python manage.py migrate --noinput && ( - - echo. - echo --- Copiando imagenes de sampleo a 'media/'... - REM XCOPY [origen] [destino] /E /I /Y - REM /E = Copia subdirectorios (incluso vacíos) - REM /I = Si el destino no existe, asume que es un directorio - REM /Y = Suprime la pregunta de "sobreescribir archivo" - XCOPY _sample_assets media /E /I /Y && ( - - echo. - echo --- Cargando datos de USER... - python manage.py loaddata user/sample/sample.json && ( - - echo. - echo --- Cargando datos de PRODUCT... - python manage.py loaddata product/sample/sample.json && ( - - echo. - echo --- Cargando datos de ORDER... - python manage.py loaddata order/sample/sample.json && ( - - echo. - echo --- !Proceso completado! La base de datos esta lista. --- - ) - ) - ) - ) - ) - ) +IF "%VIRTUAL_ENV%"=="" ( + echo. + echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + echo ERROR: No se detecta un entorno virtual activo. + echo Por favor, activa tu '.venv' antes de ejecutar este script. + echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + pause + exit /b 1 ) +echo. +echo --- Instalando dependencias (pip)... +pip install -r requirements.txt +IF %ERRORLEVEL% NEQ 0 GOTO :ERROR + +echo. +echo --- Borrando TODOS los datos de la BD... +python manage.py flush --noinput +IF %ERRORLEVEL% NEQ 0 GOTO :ERROR + +echo. +echo --- Aplicando migraciones... +python manage.py migrate --noinput +IF %ERRORLEVEL% NEQ 0 GOTO :ERROR + +echo. +echo --- Copiando imagenes de sampleo a 'media/'... +REM XCOPY [origen] [destino] /E /I /Y +REM /E = Copia subdirectorios (incluso vacíos) +REM /I = Si el destino no existe, asume que es un directorio +REM /Y = Suprime la pregunta de "sobreescribir archivo" +XCOPY _sample_assets media /E /I /Y +IF %ERRORLEVEL% NEQ 0 GOTO :ERROR + +echo. +echo --- Cargando datos de USER... +python manage.py loaddata user/sample/sample.json +IF %ERRORLEVEL% NEQ 0 GOTO :ERROR + +echo. +echo --- Cargando datos de PRODUCT... +python manage.py loaddata product/sample/sample.json +IF %ERRORLEVEL% NEQ 0 GOTO :ERROR + +echo. +echo --- Cargando datos de ORDER... +python manage.py loaddata order/sample/sample.json +IF %ERRORLEVEL% NEQ 0 GOTO :ERROR + +echo. +echo ======================================================== +echo !PROCESO COMPLETADO! Los datos de sampleo se han cargado en la base de datos. +echo ======================================================== +GOTO :END + +:ERROR +echo. +echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +echo ERROR: El script se detuvo porque un comando ha fallado. +echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +pause +exit /b 1 + +:END + @echo on \ No newline at end of file diff --git a/essenza/order/urls.py b/essenza/order/urls.py index 12fb04c..5bff867 100644 --- a/essenza/order/urls.py +++ b/essenza/order/urls.py @@ -1,13 +1,23 @@ # order/urls.py -from django.urls import include, path +from django.urls import 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 + 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", + ), + path("create_checkout/", views.create_checkout, name="create_checkout"), + path("success/", views.successful_payment, name="successful_payment"), + path("cancelled/", views.cancelled_payment, name="cancelled_payment"), +] diff --git a/essenza/order/views.py b/essenza/order/views.py index 3678864..90a73e3 100644 --- a/essenza/order/views.py +++ b/essenza/order/views.py @@ -1,229 +1,338 @@ -from django.shortcuts import get_object_or_404, redirect, render -from django.views import View -from django.contrib.auth.mixins import LoginRequiredMixin +import stripe +from django.conf import settings from django.contrib import messages -from django.db.models import F # Importado para operaciones atómicas from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.db.models import F +from django.http import HttpResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.views import View # Importaciones de tus modelos +from product.models import Product + from .models import Order, OrderProduct, Status -from product.models import Product + +# Configuración de Stripe +stripe.api_key = settings.STRIPE_SECRET_KEY + # -------------------------------------------------------------------- -# 1. FUNCIÓN AUXILIAR NECESARIA +# 1. FUNCIÓN AUXILIAR NECESARIA # -------------------------------------------------------------------- -def get_or_create_cart(request): +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. """ # Deny access to admin/staff users explicitly - if request.user.is_authenticated and (getattr(request.user, 'role', None) == 'admin' or getattr(request.user, 'is_staff', False)): - raise PermissionDenied("Acceso denegado: administradores no pueden usar el carrito.") + if request.user.is_authenticated and ( + getattr(request.user, "role", None) == "admin" + or getattr(request.user, "is_staff", False) + ): + raise PermissionDenied( + "Acceso denegado: administradores no pueden usar el carrito." + ) 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() - + 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, + user=request.user, status=Status.PENDING, - address="", # Provide an empty string to avoid IntegrityError + address="", ) - return cart else: - # Los anónimos usan la sesión return None + # -------------------------------------------------------------------- -# 2. VISTAS +# 2. VISTAS DEL CARRITO (TUS CLASES EXISTENTES) # -------------------------------------------------------------------- -# 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' - + 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() - cart_total = sum(item.product.price * item.quantity for item in cart_items) if cart_items else 0 + cart_total = ( + sum(item.product.price * item.quantity for item in cart_items) + if cart_items + else 0 + ) else: - # LÓGICA 2: Usuario anónimo (lee de la Sesión) - cart_session = request.session.get('cart_session', {}) + cart_session = request.session.get("cart_session", {}) cart_items = [] cart_total = 0 - - # 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, - }) - cart_total = sum(item['product'].price * item['quantity'] for item in cart_items) + quantity = cart_session[pk_str]["quantity"] + cart_items.append( + { + "product": product, + "quantity": quantity, + "subtotal": quantity * product.price, + "pk": product.pk, + } + ) + cart_total = sum( + item["product"].price * item["quantity"] for item in cart_items + ) context = { - 'cart': cart, # Será None para anónimos - 'cart_items': cart_items, # Lista de DB objects o dicts/temp objects - 'cart_total': cart_total, # Total calculado + "cart": cart, + "cart_items": cart_items, + "cart_total": cart_total, } 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). - """ + + +class AddToCartView(View): def post(self, request, product_pk): product = get_object_or_404(Product, pk=product_pk) - + try: - quantity = int(request.POST.get('quantity', 1)) + 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() + cart = get_or_create_cart(request) + + if cart: + # LOGUEADO + 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}") + 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}'.", + ) else: - # CREATE (DB) OrderProduct.objects.create( - order=cart, - product=product, - quantity=quantity + 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) + # ANÓNIMO (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']}") + cart_session[product_pk_str]["quantity"] += quantity + messages.success( + request, + f"Se ha añadido {quantity} unidad(es) de '{product.name}'.", + ) else: - # CREATE (SESSION) cart_session[product_pk_str] = { - 'quantity': quantity, - 'price': str(product.price) + "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 -# ======================================================= + + request.session["cart_session"] = cart_session + request.session.modified = True + + return redirect("cart_detail") + + 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') + return redirect("cart_detail") - cart_session = request.session.get('cart_session', {}) + cart_session = request.session.get("cart_session", {}) product_pk_str = str(product_pk) + product = get_object_or_404(Product, pk=product_pk) - # 1. Obtener la nueva cantidad try: - new_quantity = int(request.POST.get('quantity', 0)) + 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 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.") + messages.info(request, f"'{product.name}' eliminado.") else: - # ACTUALIZAR - # Opcional: limitar al stock disponible - if new_quantity > product.stock: + if new_quantity > product.stock: new_quantity = product.stock - messages.warning(request, f"Solo quedan {product.stock} unidades de '{product.name}'. Cantidad limitada.") - else: - 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') - + messages.warning(request, f"Stock limitado a {product.stock}.") + + cart_session[product_pk_str]["quantity"] = new_quantity + messages.success(request, "Cantidad actualizada.") + + 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) - + 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') - + return redirect("cart_detail") + try: - new_quantity = int(request.POST.get('quantity', 0)) + 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.") + messages.info(request, "Producto eliminado.") 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}.") + cart_item.save(update_fields=["quantity"]) + messages.success(request, "Cantidad actualizada.") + + return redirect("cart_detail") + + +# -------------------------------------------------------------------- +# 3. VISTAS DE STRIPE (Integración Final con tus Modelos) +# -------------------------------------------------------------------- + + +def create_checkout(request): + """ + Crea la sesión de pago. Calcula el precio total real basándose en + si el usuario es logueado (DB) o anónimo (Session). + """ + domain_url = settings.DOMAIN_URL + total_amount = 0 + + # --- 1. Calcular el total REAL --- + if request.user.is_authenticated: + # A. Usuario Logueado: Usamos la Order de la DB + cart = get_or_create_cart(request) + cart_items = cart.order_products.all() + + if not cart_items: + messages.error(request, "Tu carrito está vacío.") + return redirect( + "order:cart_detail" + ) # Ajusta el nombre de la url si es necesario + + for item in cart_items: + total_amount += item.product.price * item.quantity + + else: + # B. Usuario Anónimo: Usamos la Sesión + cart_session = request.session.get("cart_session", {}) + + if not cart_session: + messages.error(request, "Tu carrito está vacío.") + return redirect("order:cart_detail") + + # Recuperamos precios reales de la DB para evitar fraudes + product_pks = [int(pk) for pk in cart_session.keys()] + products = Product.objects.filter(pk__in=product_pks) + + for product in products: + pk_str = str(product.pk) + qty = cart_session[pk_str]["quantity"] + total_amount += product.price * qty + + # --- 2. Crear Sesión de Stripe --- + try: + # Convertir a céntimos (Stripe trabaja con enteros: 10.00€ -> 1000) + amount_in_cents = int(total_amount * 100) + + checkout_session = stripe.checkout.Session.create( + payment_method_types=["card"], + line_items=[ + { + "price_data": { + "currency": "eur", + "unit_amount": amount_in_cents, + "product_data": { + "name": "Pedido Essenza", # Puedes personalizar esto + "description": "Compra de productos cosméticos", + }, + }, + "quantity": 1, + }, + ], + mode="payment", + success_url=domain_url + "/order/success/?session_id={CHECKOUT_SESSION_ID}", + cancel_url=domain_url + "/order/cancelled/", + ) + return redirect(checkout_session.url, code=303) + + except Exception as e: + return HttpResponse(f"Error al conectar con Stripe: {e}") + + +def successful_payment(request): + """ + Verifica con Stripe que el pago sea real. + Si es correcto, actualiza el estado a PAID (Logueado) o limpia sesión (Anónimo). + """ + session_id = request.GET.get("session_id") + + if not session_id: + return HttpResponse("Error: No se ha recibido confirmación de pago.") + + try: + # Preguntar a Stripe directamente + session = stripe.checkout.Session.retrieve(session_id) + + if session.payment_status == "paid": + # --- PAGO CONFIRMADO --- + + if request.user.is_authenticated: + # 1. Usuario Logueado: Actualizar DB + cart = get_or_create_cart(request) + + # ACTUALIZACIÓN CORRECTA SEGÚN TUS MODELOS: + cart.status = Status.PAID + cart.save() + + print(f"✅ Orden {cart.id} pagada y actualizada a PAID.") + + else: + # 2. Usuario Anónimo: Limpiar Sesión + request.session["cart_session"] = {} + request.session.modified = True + print("✅ Pago anónimo verificado. Sesión limpiada.") + + # Renderizar página de gracias + # Asegúrate de tener este template creado en templates/order/success.html + return render(request, "order/success.html") + + else: + return HttpResponse("El pago no se ha completado.") + + except Exception as e: + return HttpResponse(f"Error verificando el pago: {e}") + - return redirect('cart_detail') \ No newline at end of file +def cancelled_payment(request): + # Asegúrate de tener este template creado en templates/order/cancel.html + return render(request, "order/cancel.html") diff --git a/essenza/requirements.txt b/essenza/requirements.txt index e81440b..87741c7 100644 --- a/essenza/requirements.txt +++ b/essenza/requirements.txt @@ -1,5 +1,16 @@ asgiref==3.10.0 +certifi==2025.11.12 +charset-normalizer==3.4.4 Django==5.2.8 +gunicorn==23.0.0 +idna==3.11 +packaging==25.0 pillow==12.0.0 +python-dotenv==1.2.1 +requests==2.32.5 sqlparse==0.5.3 +stripe==14.0.0 +typing_extensions==4.15.0 tzdata==2025.2 +urllib3==2.5.0 +whitenoise==6.11.0 diff --git a/essenza/templates/order/cancel.html b/essenza/templates/order/cancel.html new file mode 100644 index 0000000..dbaa986 --- /dev/null +++ b/essenza/templates/order/cancel.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block content %} +
+ +
+ ✘ +
+ +

Pago cancelado

+ +

+ Has cancelado el proceso de pago o ha ocurrido un error. +
No se ha realizado ningún cargo en tu tarjeta. +

+ + +
+{% endblock %} \ No newline at end of file diff --git a/essenza/templates/order/cart_detail.html b/essenza/templates/order/cart_detail.html index 7156353..a834606 100644 --- a/essenza/templates/order/cart_detail.html +++ b/essenza/templates/order/cart_detail.html @@ -2,7 +2,7 @@ {% load static %} {% load humanize %} -{% block title %}Mi Carrito - Essenza{% endblock %} +{% block title %}Mi Carrito · Essenza{% endblock %} {% block extra_head %} +{% endblock %} + +{% block content %} +
+ + +
+
Estado de tu Pedido
+
LOCALIZADOR: {{ order.tracking_code }}
+ +
+ +
+
+
En Preparación
+
+ + +
+
+
Enviado
+
+ + +
+
+
Entregado
+
+
+
+ + +
+
Detalles del Envío
+ +
+
+ Dirección de Entrega + {{ order.address }} +
+
+ Fecha del Pedido + {{ order.placed_at|date:"d F Y, H:i" }} +
+
+ Email de Contacto + {{ order.email }} +
+
+ Estado Actual + {{ order.get_status_display }} +
+
+ +
Resumen de Compra
+ +
+ {% for item in order.order_products.all %} +
+ +
+ {% if item.product.photo %} + {{ item.product.name }} + {% else %} + + Sin imagen + {% endif %} +
+ + +
+
{{ item.product.name }}
+
Cantidad: {{ item.quantity }} unidad(es)
+
+ + +
+ {{ item.subtotal|floatformat:2 }} € +
+
+ {% endfor %} +
+ + +
+ TOTAL PAGADO: + {{ order.total_price|floatformat:2 }} € +
+
+ + + +
+ + + +{% endblock %} \ No newline at end of file From 4979eb2af952d7677493d1295ab9119b41a10c6d Mon Sep 17 00:00:00 2001 From: Celia Date: Fri, 21 Nov 2025 19:27:43 +0100 Subject: [PATCH 4/8] Arreglos sobre mostrar pedidos de un usuario --- essenza/order/models.py | 5 ++--- essenza/order/views.py | 9 +++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/essenza/order/models.py b/essenza/order/models.py index 3ba2136..93c85b3 100644 --- a/essenza/order/models.py +++ b/essenza/order/models.py @@ -21,8 +21,8 @@ class Order(models.Model): null=True, blank=True, ) - email = models.EmailField(max_length=254, null=True) - address = models.CharField(max_length=255, null=True, blank=True) + email = models.EmailField(max_length=254) + address = models.CharField(max_length=255) placed_at = models.DateTimeField(default=timezone.now) status = models.CharField(choices=Status.choices, default=Status.EN_PREPARACION) @@ -51,7 +51,6 @@ def save(self, *args, **kwargs): if not self.user and self.email: User = get_user_model() - # Buscamos si existe algún usuario registrado con ese correo existing_user = User.objects.filter(email=self.email).first() if existing_user: diff --git a/essenza/order/views.py b/essenza/order/views.py index 9414e9c..82559a4 100644 --- a/essenza/order/views.py +++ b/essenza/order/views.py @@ -10,6 +10,7 @@ from django.db.models import ( F, # Para restar el stock de forma segura Prefetch, + Q, ) from django.http import HttpResponse from django.shortcuts import get_object_or_404, redirect, render @@ -55,9 +56,12 @@ class OrderListUserView(LoginRequiredMixin, View): template_name = "order/order_list_user.html" def get(self, request): + # CORRECCIÓN 1: Usamos Q para buscar por Usuario O por Email + # Esto permite ver pedidos hechos como invitado si el email coincide orders = ( - Order.objects.filter(user=request.user) - .exclude(status=Status.EN_PREPARACION) # carrito / en preparación NO + Order.objects.filter(Q(user=request.user) | Q(email=request.user.email)) + # CORRECCIÓN 2: Eliminado .exclude(status=Status.EN_PREPARACION) + # Ahora los pedidos 'en preparación' (recién pagados) SÍ se muestran. .prefetch_related( Prefetch( "order_products", @@ -65,6 +69,7 @@ def get(self, request): ) ) .order_by("-placed_at") + .distinct() # Evita duplicados si user y email coinciden en el mismo pedido ) return render(request, self.template_name, {"orders": orders}) From 71b5425fd48400cd70633888a8ab7a6f366c7be1 Mon Sep 17 00:00:00 2001 From: FRANCISCO DE CASTRO Date: Fri, 21 Nov 2025 20:04:05 +0100 Subject: [PATCH 5/8] Arreglos varios checkout --- essenza/order/models.py | 9 ++++---- essenza/order/sample/sample.json | 20 ++++++++-------- essenza/order/tests.py | 14 +++++------ essenza/order/urls.py | 4 ++-- essenza/order/views.py | 23 +++++++++---------- ...rder_list_user.html => order_history.html} | 0 essenza/templates/product/confirm_delete.html | 2 +- essenza/templates/product/detail.html | 2 +- 8 files changed, 37 insertions(+), 37 deletions(-) rename essenza/templates/order/{order_list_user.html => order_history.html} (100%) diff --git a/essenza/order/models.py b/essenza/order/models.py index 93c85b3..44dc09f 100644 --- a/essenza/order/models.py +++ b/essenza/order/models.py @@ -21,13 +21,13 @@ class Order(models.Model): null=True, blank=True, ) - email = models.EmailField(max_length=254) + email = models.EmailField(max_length=255) address = models.CharField(max_length=255) placed_at = models.DateTimeField(default=timezone.now) status = models.CharField(choices=Status.choices, default=Status.EN_PREPARACION) tracking_code = models.CharField( - max_length=4, + max_length=8, unique=True, editable=False, # No se puede editar manualmente verbose_name="Localizador", @@ -58,9 +58,10 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) def _generate_unique_tracking_code(self): - """Genera un código único de 4 dígitos numéricos.""" + """Genera un código único de 8 caracteres alfanuméricos.""" + chars = string.ascii_uppercase + string.digits while True: - code = "".join(random.choices(string.digits, k=4)) + code = "".join(random.choices(chars, k=8)) # Verifica que no exista para evitar duplicados if not Order.objects.filter(tracking_code=code).exists(): return code diff --git a/essenza/order/sample/sample.json b/essenza/order/sample/sample.json index c574dc2..690bb1b 100644 --- a/essenza/order/sample/sample.json +++ b/essenza/order/sample/sample.json @@ -8,7 +8,7 @@ "address": "Calle Gran Vía, 23, Madrid, 28013", "placed_at": "2025-11-12T10:30:00Z", "status": "en_preparacion", - "tracking_code": "9283" + "tracking_code": "3MRRCY5O" } }, { @@ -19,7 +19,7 @@ "address": "Avenida de la Constitución, 8, Sevilla, 41001", "placed_at": "2025-11-11T15:10:00Z", "status": "enviado", - "tracking_code": "1823" + "tracking_code": "FPXUIJS7" } }, { @@ -30,7 +30,7 @@ "address": "Carrer de Pau Claris, 60, Barcelona, 08010", "placed_at": "2025-11-10T19:25:00Z", "status": "entregado", - "tracking_code": "4829" + "tracking_code": "HQYBE6JH" } }, { @@ -42,7 +42,7 @@ "address": "Calle Alcalá, 120, Madrid, 28009", "placed_at": "2025-11-09T09:00:00Z", "status": "en_preparacion", - "tracking_code": "1029" + "tracking_code": "Y60601X2" } }, { @@ -54,7 +54,7 @@ "address": "Plaza Nueva, 10, Bilbao, 48001", "placed_at": "2025-11-08T12:15:00Z", "status": "entregado", - "tracking_code": "5823" + "tracking_code": "XUL4SC0R" } }, { @@ -66,7 +66,7 @@ "address": "Calle Larios, 5, Málaga, 29001", "placed_at": "2025-11-07T14:00:00Z", "status": "enviado", - "tracking_code": "3921" + "tracking_code": "L7WRQHVK" } }, { @@ -78,7 +78,7 @@ "address": "Paseo de Gracia, 92, Barcelona, 08008", "placed_at": "2025-11-06T18:45:00Z", "status": "en_preparacion", - "tracking_code": "1192" + "tracking_code": "YZPOHNT8" } }, { @@ -90,7 +90,7 @@ "address": "Calle de la Paz, 1, Valencia, 46003", "placed_at": "2025-11-06T10:00:00Z", "status": "entregado", - "tracking_code": "8492" + "tracking_code": "RZJC560Y" } }, { @@ -101,7 +101,7 @@ "address": "Calle Mayor, 30, Zaragoza, 50001", "placed_at": "2025-10-15T11:00:00Z", "status": "entregado", - "tracking_code": "2910" + "tracking_code": "UT0A32II" } }, { @@ -113,7 +113,7 @@ "address": "Rúa do Vilar, 50, Santiago de Compostela, 15705", "placed_at": "2025-10-28T08:30:00Z", "status": "enviado", - "tracking_code": "3849" + "tracking_code": "WW0XHB4C" } }, { diff --git a/essenza/order/tests.py b/essenza/order/tests.py index 60da81e..fce0c97 100644 --- a/essenza/order/tests.py +++ b/essenza/order/tests.py @@ -69,11 +69,11 @@ def setUpTestData(self): order=self.order_other, product=self.product, quantity=1 ) - # Asumiendo que la URL se llama 'order_list_user' en urls.py + # Asumiendo que la URL se llama 'order_history' en urls.py try: - self.url = reverse("order_list_user") - except: - self.url = "/order/my-orders/" # Fallback si no existe el name + self.url = reverse("order_history") + except Exception: + self.url = "/order/history/" # Fallback si no existe el name def test_user_must_login(self): """Un usuario anónimo debe ser redirigido al login.""" @@ -87,7 +87,7 @@ def test_user_sees_only_his_non_preparacion_orders(self): resp = self.client.get(self.url) self.assertEqual(resp.status_code, 200) - self.assertTemplateUsed(resp, "order/order_list_user.html") + self.assertTemplateUsed(resp, "order/order_history.html") orders = resp.context["orders"] @@ -143,7 +143,7 @@ def setUpTestData(self): try: self.url = reverse("order_list_admin") - except: + except Exception: self.url = "/order/admin/list/" def test_anonymous_redirects_to_login(self): @@ -190,7 +190,7 @@ def setUpTestData(self): # Asumiendo que en urls.py se llama 'order_search' try: self.url_search = reverse("order_search") - except: + except Exception: self.url_search = "/order/search/" def test_track_get_returns_form(self): diff --git a/essenza/order/urls.py b/essenza/order/urls.py index fe1e2bc..f6c4b5c 100644 --- a/essenza/order/urls.py +++ b/essenza/order/urls.py @@ -13,6 +13,6 @@ name="order_tracking", ), path("list/", views.OrderListAdminView.as_view(), name="order_list_admin"), - path("my-orders/", views.OrderListUserView.as_view(), name="order_list_user"), - path("search/", views.OrderTrackView.as_view(), name="order_search"), + path("history/", views.OrderHistoryView.as_view(), name="order_history"), + path("search/", views.OrderSearchView.as_view(), name="order_search"), ] diff --git a/essenza/order/views.py b/essenza/order/views.py index 82559a4..1f0dbb5 100644 --- a/essenza/order/views.py +++ b/essenza/order/views.py @@ -20,6 +20,9 @@ from .models import Order, OrderProduct, Status +# Configuración de Stripe +stripe.api_key = settings.STRIPE_SECRET_KEY + # ======================================================= # LISTADO DE PEDIDOS - ADMIN @@ -52,8 +55,8 @@ def get(self, request): # ======================================================= # LISTADO DE PEDIDOS - USER # ======================================================= -class OrderListUserView(LoginRequiredMixin, View): - template_name = "order/order_list_user.html" +class OrderHistoryView(LoginRequiredMixin, View): + template_name = "order/order_history.html" def get(self, request): # CORRECCIÓN 1: Usamos Q para buscar por Usuario O por Email @@ -77,7 +80,7 @@ def get(self, request): # ======================================================= # SEGUIMIENTO SIN LOGIN # ======================================================= -class OrderTrackView(View): +class OrderSearchView(View): template_name = "order/order_search.html" def get(self, request): @@ -119,8 +122,11 @@ def post(self, request): return render(request, self.template_name, {"order": None, "searched": True}) -# Configuración de Stripe -stripe.api_key = settings.STRIPE_SECRET_KEY +class OrderTrackingView(View): + def get(self, request, tracking_code): + # Buscamos el pedido por su código único + order = get_object_or_404(Order, tracking_code=tracking_code) + return render(request, "order/tracking.html", {"order": order}) def create_checkout(request): @@ -335,10 +341,3 @@ def successful_payment(request): def cancelled_payment(request): return render(request, "order/cancel.html") - - -class OrderTrackingView(View): - def get(self, request, tracking_code): - # Buscamos el pedido por su código único - order = get_object_or_404(Order, tracking_code=tracking_code) - return render(request, "order/tracking.html", {"order": order}) diff --git a/essenza/templates/order/order_list_user.html b/essenza/templates/order/order_history.html similarity index 100% rename from essenza/templates/order/order_list_user.html rename to essenza/templates/order/order_history.html diff --git a/essenza/templates/product/confirm_delete.html b/essenza/templates/product/confirm_delete.html index b9ff04b..a32b933 100644 --- a/essenza/templates/product/confirm_delete.html +++ b/essenza/templates/product/confirm_delete.html @@ -4,7 +4,7 @@ - Confirmar Borrado - Essenza + Confirmar Borrado · Essenza @@ -513,24 +510,31 @@
+ + diff --git a/essenza/templates/order/order_list_admin.html b/essenza/templates/order/order_list_admin.html index a7f88a0..48c343c 100644 --- a/essenza/templates/order/order_list_admin.html +++ b/essenza/templates/order/order_list_admin.html @@ -2,139 +2,114 @@ {% load humanize %} {% load static %} -{% block title %}Administración de Pedidos · Essenza{% endblock %} +{% block title %}Mis pedidos · Essenza{% endblock %} {% block extra_head %} - {% endblock %} {% block content %} -
+
-
-
-

Pedidos

-

Gestión y seguimiento de envíos

-
+ + + + + {% if orders %} {% for order in orders %} - + +
+
{{ order.tracking_code }} @@ -325,19 +331,17 @@

Pedidos

+
- +
-
{{ order.email }}
- {{ order.address|default:"Sin dirección"|truncatechars:35 }} + {{ order.address|default:"Dirección no disponible"|truncatechars:40 }} +
+
+ {{ order.email }}
- {% if order.user %} -
- ID Usuario: {{ order.user.id }} -
- {% endif %}
@@ -362,9 +366,9 @@

Pedidos

- +
- + {% if order.status == "en_preparacion" %} Preparando {% elif order.status == "enviado" %} @@ -387,13 +391,20 @@

Pedidos

{% endfor %} {% else %} -
- -

No se encontraron pedidos

-

No hay pedidos que coincidan con tu búsqueda o la lista está vacía.

- {% if request.GET.q %} -
Ver todos los pedidos + +

No tienes pedidos {% if request.GET.status %}en esta categoría{% endif %}

+

+ {% if request.GET.status %} + Prueba seleccionando "Todos" para ver tu historial completo. + {% else %} + Cuando realices tu primera compra, aparecerá aquí para que puedas seguirla. + {% endif %} +

+ {% if request.GET.status %} + Ver todos + {% else %} + Ir a la tienda {% endif %}
{% endif %} diff --git a/essenza/templates/order/order_search.html b/essenza/templates/order/order_search.html index 3953feb..8c1da86 100644 --- a/essenza/templates/order/order_search.html +++ b/essenza/templates/order/order_search.html @@ -134,7 +134,7 @@

Seguimiento de pedido

diff --git a/essenza/templates/order/tracking.html b/essenza/templates/order/tracking.html index 072d167..3c110af 100644 --- a/essenza/templates/order/tracking.html +++ b/essenza/templates/order/tracking.html @@ -56,7 +56,7 @@ content: ''; position: absolute; top: 15px; - left: 40px; + left: 30px; right: 40px; height: 4px; background: #eee; @@ -109,14 +109,39 @@ content: ''; position: absolute; top: 15px; - left: -50%; - width: 100%; + left: -42%; + width: 98%; height: 4px; background: #c06b3e; z-index: -1; } .step:first-child.active::after { display: none; } + /* ESTILOS BOTONES ADMIN */ + .admin-step-form { + display: inline; + width: 100%; + } + .admin-step-btn { + background: none; + border: none; + padding: 0; + width: 100%; + cursor: pointer; /* Manita al pasar por encima */ + } + .admin-step-btn:hover .step-circle { + transform: scale(1.1); /* Pequeño efecto zoom para admins */ + border-color: #c06b3e; + } + .admin-hint { + display: block; + margin-top: 5px; + font-size: 0.7rem; + color: #c06b3e; + text-transform: uppercase; + font-weight: 700; + } + /* --- DETALLES DEL PEDIDO (GENERAL) --- */ .order-details-card { @@ -164,9 +189,6 @@ } /* --- NUEVO ESTILO DE LISTA DE PRODUCTOS --- */ - .product-list { - /* Ya no necesita borde superior porque el título lo separa */ - } .product-item { display: flex; @@ -259,28 +281,90 @@
-
Estado del Pedido
+
Estado de tu Pedido
LOCALIZADOR: {{ order.tracking_code }}
+ + + {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %}
- -
-
-
En Preparación
-
+ + {% if user.is_authenticated and user.role == 'admin' %} + + + + + +
+
+ {% csrf_token %} + + +
+
- -
-
-
Enviado
-
+ +
+
+ {% csrf_token %} + + +
+
+ + +
+
+ {% csrf_token %} + + +
+
+ + {% else %} + + + + + +
+
+
En Preparación
+
+ + +
+
+
Enviado
+
+ + +
+
+
Entregado
+
+ {% endif %} - -
-
-
Entregado
-
+ + {% if user.is_authenticated and user.role == 'admin' %} +
Modo Admin: Haz clic en un icono para cambiar el estado
+ {% endif %}
@@ -343,7 +427,7 @@
diff --git a/essenza/templates/product/catalog.html b/essenza/templates/product/catalog.html index 4a4d727..304d196 100644 --- a/essenza/templates/product/catalog.html +++ b/essenza/templates/product/catalog.html @@ -1,326 +1,289 @@ {% extends "base.html" %} - - {% load static %} {% load humanize %} {% block title %}Catálogo · Essenza{% endblock %} {% block content %} - - - - - - -
-

Catálogo Essenza

-

Explora nuestra selección de productos mejor valorados

-
- - + + +
+
+

Catálogo Essenza

+

Explora nuestra selección de productos mejor valorados

+ +
-
- {% endblock %} diff --git a/essenza/templates/product/stock.html b/essenza/templates/product/stock.html index 07efc72..972694c 100644 --- a/essenza/templates/product/stock.html +++ b/essenza/templates/product/stock.html @@ -13,6 +13,37 @@ padding: 0 24px; } + .list-page * { + box-sizing: border-box; + } + .list-page { + background-color: #faf7f2; + font-family: "Segoe UI", Arial, sans-serif; + color: #333; + padding: 20px 0; + } + .list-page .container { + /* Contenedor principal centrado para el contenido */ + max-width: 1200px; + margin: 0 auto; + padding: 20px; + background: transparent; + } + + h1 { + color: #c06b3e; + text-align: center; + margin-bottom: 30px; + font-size: 32px; + } + + .filters { + max-width: 1100px; + margin: 25px auto 0 auto; + display: flex; + margin-bottom: 30px; + } + /* ====== TARJETA DE PRODUCTO ====== */ .product-card { @@ -123,67 +154,46 @@ } - - -
-
- -