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/cart/views.py b/essenza/cart/views.py index 530eb3d..2cbc086 100644 --- a/essenza/cart/views.py +++ b/essenza/cart/views.py @@ -1,4 +1,3 @@ -from django.contrib import messages from django.shortcuts import get_object_or_404, redirect, render from django.views import View from product.models import Product @@ -10,7 +9,7 @@ class CartDetailView(View): """ Muestra el carrito. - Si es usuario logueado: Lee de la base de datos - - Si es anónimo: Lee de la sesión. + - Si es anónimo: Lee de la sesión """ template_name = "cart/cart_detail.html" @@ -72,7 +71,6 @@ def post(self, request, product_id): product = get_object_or_404(Product, pk=product_id) if product.stock <= 0: - messages.error(request, f"Lo sentimos, '{product.name}' está agotado.") return redirect("catalog") try: @@ -102,11 +100,6 @@ def post(self, request, product_id): else: cart_product.quantity += quantity cart_product.save() - msg = f"Se ha añadido otra unidad de {product.name}." - else: - msg = f"{product.name} añadido al carrito." - - messages.success(request, msg) # Si el usuario no está logueado, guardamos en sesión else: @@ -119,17 +112,14 @@ def post(self, request, product_id): return redirect("cart_detail") else: cart_session[product_id_str]["quantity"] += quantity - msg = f"Se ha añadido otra unidad de {product.name}." else: cart_session[product_id_str] = { "quantity": quantity, "price": str(product.price), } - msg = f"{product.name} añadido al carrito." request.session["cart_session"] = cart_session request.session.modified = True - messages.success(request, msg) return redirect("cart_detail") diff --git a/essenza/essenza/settings.py b/essenza/essenza/settings.py index ee674bb..e6f79a4 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 @@ -47,6 +54,7 @@ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", @@ -112,8 +120,7 @@ LANGUAGE_CODE = "es" -TIME_ZONE = "UTC" - +TIME_ZONE = "Europe/Madrid" USE_I18N = True USE_TZ = True @@ -126,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" @@ -140,3 +150,24 @@ # 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 + +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = "smtp.gmail.com" +EMAIL_PORT = 587 +EMAIL_USE_TLS = True + +# Leemos las credenciales del archivo .env (o las pones aquí directamente entre comillas si prefieres) +EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") + +# El remitente que aparecerá en los correos +DEFAULT_FROM_EMAIL = "Essenza " diff --git a/essenza/essenza/urls.py b/essenza/essenza/urls.py index 708c639..5d0bbb8 100644 --- a/essenza/essenza/urls.py +++ b/essenza/essenza/urls.py @@ -15,7 +15,6 @@ path("catalog//", CatalogDetailView.as_view(), name="catalog_detail"), path("cart/", include("cart.urls")), path("order/", include("order.urls")), - ] if settings.DEBUG: diff --git a/essenza/load_samples.bat b/essenza/load_samples.bat index 8374cf9..be0c428 100644 --- a/essenza/load_samples.bat +++ b/essenza/load_samples.bat @@ -1,48 +1,76 @@ @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 CON EXITO! +echo 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/models.py b/essenza/order/models.py index 2e8f9e4..44dc09f 100644 --- a/essenza/order/models.py +++ b/essenza/order/models.py @@ -1,3 +1,7 @@ +import random +import string + +from django.contrib.auth import get_user_model from django.db import models from django.utils import timezone @@ -11,12 +15,24 @@ class Status(models.TextChoices): class Order(models.Model): user = models.ForeignKey( - "user.Usuario", on_delete=models.CASCADE, related_name="orders" + "user.Usuario", + on_delete=models.SET_NULL, + related_name="orders", + null=True, + blank=True, ) - address = models.CharField(max_length=255, null=True, blank=True) + 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=8, + unique=True, + editable=False, # No se puede editar manualmente + verbose_name="Localizador", + ) + @property def total_price(self): total = 0 @@ -24,8 +40,34 @@ def total_price(self): total += product.subtotal return total + def save(self, *args, **kwargs): + """ + Sobrescribimos el método save para generar el tracking_code + automáticamente antes de guardar si aún no tiene uno. + """ + + if not self.tracking_code: + self.tracking_code = self._generate_unique_tracking_code() + + if not self.user and self.email: + User = get_user_model() + existing_user = User.objects.filter(email=self.email).first() + + if existing_user: + self.user = existing_user + super().save(*args, **kwargs) + + def _generate_unique_tracking_code(self): + """Genera un código único de 8 caracteres alfanuméricos.""" + chars = string.ascii_uppercase + string.digits + while True: + 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 + def __str__(self): - return f"Order {self.id} by {self.user.email}" + return f"Order {self.id} [{self.tracking_code}] - {self.email}" class OrderProduct(models.Model): @@ -42,4 +84,4 @@ def subtotal(self): return self.quantity * self.product.price def __str__(self): - return f"{self.quantity} of {self.product.name} in order {self.order.id}" + return f"{self.quantity} of {self.product.name} in order {self.order.tracking_code}" diff --git a/essenza/order/sample/sample.json b/essenza/order/sample/sample.json index e2c9420..690bb1b 100644 --- a/essenza/order/sample/sample.json +++ b/essenza/order/sample/sample.json @@ -4,49 +4,57 @@ "pk": 1, "fields": { "user": 1, + "email": "user1@example.com", "address": "Calle Gran Vía, 23, Madrid, 28013", "placed_at": "2025-11-12T10:30:00Z", - "status": "en_preparacion" + "status": "en_preparacion", + "tracking_code": "3MRRCY5O" } }, { "model": "order.order", "pk": 2, "fields": { - "user": 2, + "email": "ana@example.com", "address": "Avenida de la Constitución, 8, Sevilla, 41001", "placed_at": "2025-11-11T15:10:00Z", - "status": "enviado" + "status": "enviado", + "tracking_code": "FPXUIJS7" } }, { "model": "order.order", "pk": 3, "fields": { - "user": 3, + "email": "luis@example.com", "address": "Carrer de Pau Claris, 60, Barcelona, 08010", "placed_at": "2025-11-10T19:25:00Z", - "status": "entregado" + "status": "entregado", + "tracking_code": "HQYBE6JH" } }, { "model": "order.order", "pk": 4, "fields": { - "user": 1, + "user": 2, + "email": "user2@example.com", "address": "Calle Alcalá, 120, Madrid, 28009", "placed_at": "2025-11-09T09:00:00Z", - "status": "en_preparacion" + "status": "en_preparacion", + "tracking_code": "Y60601X2" } }, { "model": "order.order", "pk": 5, "fields": { - "user": 2, + "user": 3, + "email": "user3@example.com", "address": "Plaza Nueva, 10, Bilbao, 48001", "placed_at": "2025-11-08T12:15:00Z", - "status": "entregado" + "status": "entregado", + "tracking_code": "XUL4SC0R" } }, { @@ -54,39 +62,46 @@ "pk": 6, "fields": { "user": 3, + "email": "user3@example.com", "address": "Calle Larios, 5, Málaga, 29001", "placed_at": "2025-11-07T14:00:00Z", - "status": "enviado" + "status": "enviado", + "tracking_code": "L7WRQHVK" } }, { "model": "order.order", "pk": 7, "fields": { - "user": 1, + "user": 4, + "email": "user4@example.com", "address": "Paseo de Gracia, 92, Barcelona, 08008", "placed_at": "2025-11-06T18:45:00Z", - "status": "en_preparacion" + "status": "en_preparacion", + "tracking_code": "YZPOHNT8" } }, { "model": "order.order", "pk": 8, "fields": { - "user": 2, + "user": 4, + "email": "user4@example.com", "address": "Calle de la Paz, 1, Valencia, 46003", "placed_at": "2025-11-06T10:00:00Z", - "status": "entregado" + "status": "entregado", + "tracking_code": "RZJC560Y" } }, { "model": "order.order", "pk": 9, "fields": { - "user": 3, + "email": "luis@example.com", "address": "Calle Mayor, 30, Zaragoza, 50001", "placed_at": "2025-10-15T11:00:00Z", - "status": "entregado" + "status": "entregado", + "tracking_code": "UT0A32II" } }, { @@ -94,9 +109,11 @@ "pk": 10, "fields": { "user": 1, + "email": "user1@example.com", "address": "Rúa do Vilar, 50, Santiago de Compostela, 15705", "placed_at": "2025-10-28T08:30:00Z", - "status": "enviado" + "status": "enviado", + "tracking_code": "WW0XHB4C" } }, { diff --git a/essenza/order/tests.py b/essenza/order/tests.py index f79f55e..62102e8 100644 --- a/essenza/order/tests.py +++ b/essenza/order/tests.py @@ -1,8 +1,8 @@ -from django.test import TestCase, Client -from django.urls import reverse from django.contrib.auth import get_user_model +from django.test import Client, TestCase +from django.urls import reverse +from product.models import Category, Product -from product.models import Product, Category from order.models import Order, OrderProduct, Status User = get_user_model() @@ -12,147 +12,132 @@ # TESTS: LISTADO DE PEDIDOS DEL USUARIO # ============================================================ + class OrderListUserViewTests(TestCase): @classmethod - def setUpTestData(cls): - cls.client = Client() - - cls.user = User.objects.create_user( - username="user1", - email="user@test.com", - password="1234", - role="user" + def setUpTestData(self): + self.client = Client() + + # Creamos usuario + self.user = User.objects.create_user( + username="user1", email="user@test.com", password="1234" ) - cls.other_user = User.objects.create_user( - username="user2", - email="user2@test.com", - password="1234", - role="user" + # Asignamos rol manualmente por seguridad + self.user.role = "user" + self.user.save() + + self.other_user = User.objects.create_user( + username="user2", email="user2@test.com", password="1234" ) + self.other_user.role = "user" + self.other_user.save() - cls.product = Product.objects.create( + self.product = Product.objects.create( name="Producto A", - brand="Marca A", - description="Desc", - category=Category.MAQUILLAJE, price="10.00", stock=10, is_active=True, + category=Category.MAQUILLAJE, + brand="Marca A", ) - # Pedido 1 del usuario - cls.order1 = Order.objects.create( - user=cls.user, - status=Status.EN_PREPARACION, + # 1. Pedido visible del usuario (ENVIADO) + self.order_user = Order.objects.create( + user=self.user, + email=self.user.email, # Importante: ahora el modelo usa email + status=Status.ENVIADO, address="Calle 1", ) OrderProduct.objects.create( - order=cls.order1, - product=cls.product, - quantity=2 + order=self.order_user, product=self.product, quantity=2 ) - # Pedido 2 del usuario - cls.order2 = Order.objects.create( - user=cls.user, - status=Status.ENVIADO, - address="Calle 2", + # 2. Pedido del usuario OCULTO (EN_PREPARACION - según la lógica de tu vista) + self.order_hidden = Order.objects.create( + user=self.user, + email=self.user.email, + status=Status.EN_PREPARACION, + address="Calle Oculta", ) OrderProduct.objects.create( - order=cls.order2, - product=cls.product, - quantity=1 + order=self.order_hidden, product=self.product, quantity=1 ) - # Pedido de otro usuario (NO debe salir) - cls.order_other = Order.objects.create( - user=cls.other_user, + # 3. Pedido de otro usuario (No debe verse) + self.order_other = Order.objects.create( + user=self.other_user, + email=self.other_user.email, status=Status.ENVIADO, address="Otra calle", ) OrderProduct.objects.create( - order=cls.order_other, - product=cls.product, - quantity=1 + order=self.order_other, product=self.product, quantity=1 ) - cls.url = reverse("order_list_user") + # Asumiendo que la URL se llama 'order_history' en urls.py + try: + 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.""" resp = self.client.get(self.url) self.assertEqual(resp.status_code, 302) - - def test_user_sees_only_his_orders(self): - """Debe ver TODOS sus pedidos, incluyendo EN_PREPARACION.""" - self.client.login(email="user@test.com", password="1234") - resp = self.client.get(self.url) - - self.assertEqual(resp.status_code, 200) - self.assertTemplateUsed(resp, "order/order_list_user.html") - - orders = resp.context["orders"] - self.assertEqual(orders.count(), 2) # 🔥 YA NO FILTRAMOS NADA - - # Los IDs deben coincidir - returned_ids = set(o.id for o in orders) - expected_ids = {self.order1.id, self.order2.id} - self.assertEqual(returned_ids, expected_ids) - - self.assertContains(resp, "Calle 1") - self.assertContains(resp, "Calle 2") - + self.assertTrue("login" in resp.url) # ============================================================ # TESTS: LISTADO DE PEDIDOS DEL ADMIN # ============================================================ + class OrderListAdminViewTests(TestCase): @classmethod - def setUpTestData(cls): - cls.client = Client() - - cls.admin = User.objects.create_user( - username="admin1", - email="admin@test.com", - password="1234", - role="admin", - is_staff=True + def setUpTestData(self): + self.client = Client() + + # Admin + self.admin = User.objects.create_user( + username="admin1", email="admin@test.com", password="1234" ) + self.admin.role = "admin" + self.admin.save() - cls.user = User.objects.create_user( - username="user3", - email="user3@test.com", - password="1234", - role="user" + # User normal + self.user = User.objects.create_user( + username="user3", email="user3@test.com", password="1234" ) + self.user.role = "user" + self.user.save() - cls.product = Product.objects.create( + self.product = Product.objects.create( name="Prod", - brand="Brand", - description="d", - category=Category.PERFUME, price="5.00", stock=10, is_active=True, + category=Category.PERFUME, + brand="Brand", ) - cls.order = Order.objects.create( - user=cls.user, + # Creamos un pedido para probar + self.order = Order.objects.create( + user=self.user, + email="cliente@test.com", status=Status.ENVIADO, - address="Dir", - ) - OrderProduct.objects.create( - order=cls.order, - product=cls.product, - quantity=1 + address="Dir Admin Test", ) + OrderProduct.objects.create(order=self.order, product=self.product, quantity=1) - cls.url = reverse("order_list_admin") + try: + self.url = reverse("order_list_admin") + except Exception: + self.url = "/order/list/" - def test_admin_must_login(self): + def test_anonymous_redirects_to_login(self): resp = self.client.get(self.url) self.assertEqual(resp.status_code, 302) + self.assertTrue("login" in resp.url) def test_admin_can_view_orders(self): self.client.login(email="admin@test.com", password="1234") @@ -161,142 +146,68 @@ def test_admin_can_view_orders(self): self.assertEqual(resp.status_code, 200) self.assertTemplateUsed(resp, "order/order_list_admin.html") self.assertContains(resp, "Prod") - self.assertContains(resp, "user3@test.com") - - + self.assertContains( + resp, self.order.tracking_code + ) # Debe salir el tracking code -# ============================================================ -# TESTS: SEGUIMIENTO DE PEDIDO -# ============================================================ class OrderTrackViewTests(TestCase): @classmethod - def setUpTestData(cls): - cls.client = Client() - - cls.user = User.objects.create_user( - username="trackuser", - email="track@test.com", - password="1234", - role="user" - ) + def setUpTestData(self): + self.client = Client() - cls.product = Product.objects.create( + self.product = Product.objects.create( name="Producto Track", - brand="Marca Track", - description="Desc", - category=Category.CABELLO, price="12.00", stock=5, is_active=True, + category=Category.CABELLO, + brand="Marca Track", ) - cls.order = Order.objects.create( - user=cls.user, + # Creamos un pedido sin usuario (invitado) para probar el tracking público + self.order = Order.objects.create( + user=None, + email="track@test.com", status=Status.ENVIADO, address="Direccion de prueba", ) - OrderProduct.objects.create( - order=cls.order, - product=cls.product, - quantity=1 - ) + OrderProduct.objects.create(order=self.order, product=self.product, quantity=1) - cls.url = reverse("order_track") + # URL de la vista de búsqueda (donde está el formulario) + # Asumiendo que en urls.py se llama 'order_search' + try: + self.url_search = reverse("order_search") + except Exception: + self.url_search = "/order/search/" def test_track_get_returns_form(self): - resp = self.client.get(self.url) + """GET debe mostrar el formulario vacío.""" + resp = self.client.get(self.url_search) self.assertEqual(resp.status_code, 200) - self.assertTemplateUsed(resp, "order/order_track.html") + self.assertTemplateUsed(resp, "order/order_search.html") + self.assertFalse(resp.context["searched"]) def test_track_post_valid_redirects_to_detail(self): - data = {"order_id": str(self.order.id), "email": "track@test.com"} - resp = self.client.post(self.url, data, follow=True) - - self.assertEqual(resp.status_code, 200) - self.assertTemplateUsed(resp, "order/order_detail.html") - self.assertContains(resp, "Direccion de prueba") - - def test_track_post_invalid_shows_error(self): - data = {"order_id": "9999", "email": "track@test.com"} - resp = self.client.post(self.url, data) - - self.assertEqual(resp.status_code, 200) - self.assertContains(resp, "No se ha encontrado ningún pedido") - - - -# ============================================================ -# TESTS: DETALLE DE PEDIDO -# ============================================================ - -class OrderDetailViewTests(TestCase): - @classmethod - def setUpTestData(cls): - cls.client = Client() - - cls.user = User.objects.create_user( - username="user_detail", - email="user_detail@test.com", - password="1234", - role="user" - ) - cls.other_user = User.objects.create_user( - username="other_detail", - email="other_detail@test.com", - password="1234", - role="user" - ) - cls.admin = User.objects.create_user( - username="admin_detail", - email="admin_detail@test.com", - password="1234", - role="admin", - is_staff=True - ) - - cls.product = Product.objects.create( - name="Prod Detalle", - brand="Brand", - description="d", - category=Category.MAQUILLAJE, - price="7.00", - stock=10, - is_active=True, - ) - - cls.order = Order.objects.create( - user=cls.user, - status=Status.ENVIADO, - address="Calle detalle", - ) - OrderProduct.objects.create( - order=cls.order, - product=cls.product, - quantity=2 - ) + """POST correcto debe REDIRIGIR a la vista de detalle.""" + # Usamos los nombres de campo exactos de tu HTML: 'tracking_code' y 'email' + data = {"tracking_code": self.order.tracking_code, "email": "track@test.com"} + resp = self.client.post(self.url_search, data) - cls.url = reverse("order_detail", args=[cls.order.id]) - - def test_user_must_login(self): - resp = self.client.get(self.url) + # Tu vista hace: return redirect("order_tracking", ...) -> Código 302 self.assertEqual(resp.status_code, 302) - def test_owner_can_view_detail(self): - self.client.login(email="user_detail@test.com", password="1234") - resp = self.client.get(self.url) - - self.assertEqual(resp.status_code, 200) - self.assertTemplateUsed(resp, "order/order_detail.html") - self.assertContains(resp, "Prod Detalle") - - def test_other_user_cannot_view_detail(self): - self.client.login(email="other_detail@test.com", password="1234") - resp = self.client.get(self.url) - self.assertEqual(resp.status_code, 403) + # Verificamos que redirige a la URL con el tracking code + # Asumiendo que la url de detalle es /order/track// + expected_url = reverse("order_tracking", args=[self.order.tracking_code]) + self.assertRedirects(resp, expected_url) - def test_admin_can_view_any_detail(self): - self.client.login(email="admin_detail@test.com", password="1234") - resp = self.client.get(self.url) + def test_track_post_invalid_shows_error(self): + """POST con datos incorrectos muestra error en la misma página.""" + data = { + "tracking_code": "wrong", # Código falso + "email": "track@test.com", + } + resp = self.client.post(self.url_search, data) self.assertEqual(resp.status_code, 200) - self.assertTemplateUsed(resp, "order/order_detail.html") + self.assertTrue(resp.context["searched"]) # Indica que se intentó buscar diff --git a/essenza/order/urls.py b/essenza/order/urls.py index 1dad78c..cb022ba 100644 --- a/essenza/order/urls.py +++ b/essenza/order/urls.py @@ -1,16 +1,23 @@ +# order/urls.py from django.urls import path -from .views import ( - OrderListAdminView, - OrderListUserView, - OrderTrackView, - OrderDetailView, -) -urlpatterns = [ - path("admin/listado/", OrderListAdminView.as_view(), name="order_list_admin"), - path("mis-pedidos/", OrderListUserView.as_view(), name="order_list_user"), - path("seguimiento/", OrderTrackView.as_view(), name="order_track"), - path("pedido//", OrderDetailView.as_view(), name="order_detail"), +from . import views +urlpatterns = [ + 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"), + path( + "track//", + views.OrderTrackingView.as_view(), + name="order_tracking", + ), + path("list/", views.OrderListAdminView.as_view(), name="order_list_admin"), + path("history/", views.OrderHistoryView.as_view(), name="order_history"), + path("search/", views.OrderSearchView.as_view(), name="order_search"), + path( + "update-status//", + views.OrderUpdateStatusView.as_view(), + name="order_update_status", + ), ] - diff --git a/essenza/order/views.py b/essenza/order/views.py index efcb448..59f6e35 100644 --- a/essenza/order/views.py +++ b/essenza/order/views.py @@ -1,12 +1,27 @@ +import stripe +from cart.models import Cart +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin -from django.views import View -from django.shortcuts import render, redirect -from django.db.models import Prefetch -from .models import Order, OrderProduct, Status from django.core.exceptions import PermissionDenied -from django.http import Http404 +from django.core.mail import send_mail +from django.db import transaction # Para la integridad de datos +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 from django.urls import reverse +from django.views import View +from product.models import Product + +from .models import Order, OrderProduct, Status +# Configuración de Stripe +stripe.api_key = settings.STRIPE_SECRET_KEY # ======================================================= @@ -27,103 +42,350 @@ def get(self, request): orders = ( Order.objects.select_related("user") .prefetch_related( - Prefetch("order_products", queryset=OrderProduct.objects.select_related("product")) + Prefetch( + "order_products", + queryset=OrderProduct.objects.select_related("product"), + ) ) .order_by("-placed_at") ) + # 2. Lógica de Filtrado + status_filter = request.GET.get("status") + + # Validamos que el estado sea real para evitar errores + valid_statuses = [ + s[0] for s in Status.choices + ] # ['en_preparacion', 'enviado', 'entregado'] + + if status_filter in valid_statuses: + orders = orders.filter(status=status_filter) return render(request, self.template_name, {"orders": orders}) # ======================================================= # 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 + # Esto permite ver pedidos hechos como invitado si el email coincide orders = ( - Order.objects.filter(user=request.user) + 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", queryset=OrderProduct.objects.select_related("product")) + Prefetch( + "order_products", + queryset=OrderProduct.objects.select_related("product"), + ) ) .order_by("-placed_at") + .distinct() # Evita duplicados si user y email coinciden en el mismo pedido ) return render(request, self.template_name, {"orders": orders}) # ======================================================= -# SEGUIMIENTO SIN LOGIN +# BUSQUEDA DE PEDIDO # ======================================================= -class OrderTrackView(View): - template_name = "order/order_track.html" +class OrderSearchView(View): + template_name = "order/order_search.html" def get(self, request): # Solo muestra el formulario vacío return render(request, self.template_name, {"searched": False}) def post(self, request): - order_id = request.POST.get("order_id", "").strip() + order_tracking_code = request.POST.get("tracking_code", "").strip() email = request.POST.get("email", "").strip().lower() - if not order_id or not email: - return render( - request, - self.template_name, - { - "searched": True, - "error": "Debes introducir el número de pedido y el email.", - }, - ) + order = None + error = None - try: - order_pk = int(order_id) - order = ( - Order.objects - .select_related("user") - .prefetch_related( - Prefetch("order_products", queryset=OrderProduct.objects.select_related("product")) + if not order_tracking_code or not email: + error = "Debes introducir el número de pedido y el email." + else: + try: + order = ( + Order.objects.select_related("user") + .prefetch_related( + Prefetch( + "order_products", + queryset=OrderProduct.objects.select_related("product"), + ) + ) + .get(tracking_code=order_tracking_code, email__iexact=email) ) - .get(pk=order_pk, user__email__iexact=email) - ) - except (ValueError, Order.DoesNotExist): - return render( - request, - self.template_name, + except Order.DoesNotExist: + error = "No se ha encontrado ningún pedido con esos datos." + + # Si encontramos el pedido, podemos redirigir a la vista de detalle bonita que ya tienes + if order: + return redirect("order_tracking", tracking_code=order_tracking_code) + + # Si hubo error, volvemos a mostrar el formulario con el mensaje + messages.error(request, error) + context = { + "order": None, + "searched": True, + "tracking_code": order_tracking_code, + "email": email, + } + return render(request, self.template_name, context) + + +# ======================================================= +# SEGUIMIENTO ENVÍO +# ======================================================= +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}) + + +# ======================================================= +# ACTUALIZAR ESTADO (SOLO ADMIN) +# ======================================================= +class OrderUpdateStatusView(LoginRequiredMixin, View): + """ + Permite a un administrador cambiar el estado de un pedido + haciendo clic en la barra de progreso. + """ + + def test_func(self): + return self.request.user.is_authenticated and self.request.user.role == "admin" + + def handle_no_permission(self): + if not self.request.user.is_authenticated: + return redirect("login") + return redirect("dashboard") + + def post(self, request, tracking_code): + order = get_object_or_404(Order, tracking_code=tracking_code) + new_status = request.POST.get("status") + + # Validamos que el estado sea uno de los permitidos + valid_statuses = [choice[0] for choice in Status.choices] + + if new_status in valid_statuses: + order.status = new_status + order.save() + + # Redirigimos a la misma página de tracking para ver el cambio + return redirect("order_tracking", tracking_code=order.tracking_code) + + +def create_checkout(request): + """ + Crea la sesión de pago en Stripe y configura la recolección de dirección. + Restringido: Los administradores NO pueden acceder aquí. + """ + if request.user.is_authenticated and getattr(request.user, "role", None) == "admin": + raise PermissionDenied("Los administradores no pueden realizar compras.") + + domain_url = settings.DOMAIN_URL + cart_items_temp = [] + + if request.user.is_authenticated: + cart = get_object_or_404(Cart, user=request.user) + for item in cart.cart_products.all(): + cart_items_temp.append( { - "searched": True, - "error": "No se ha encontrado ningún pedido con esos datos.", - }, + "product": item.product, + "quantity": item.quantity, + "price": item.product.price, + } ) + else: + cart_session = request.session.get("cart_session", {}) + if not cart_session: + return redirect("cart_detail") - return redirect(f"{reverse('order_detail', kwargs={'pk': order.pk})}?from=track") + product_pks = [int(pk) for pk in cart_session.keys()] + products = Product.objects.filter(pk__in=product_pks) + for product in products: + qty = cart_session[str(product.pk)]["quantity"] + cart_items_temp.append( + {"product": product, "quantity": qty, "price": product.price} + ) + line_items_stripe = [] + for item in cart_items_temp: + amount_in_cents = int(item["price"] * 100) + line_items_stripe.append( + { + "price_data": { + "currency": "eur", + "unit_amount": amount_in_cents, + "product_data": { + "name": item["product"].name, + "description": item["product"].description[:100] + if item["product"].description + else "Producto Essenza", + }, + }, + "quantity": item["quantity"], + } + ) -class OrderDetailView(View): - template_name = "order/order_detail.html" + try: + customer_email = request.user.email if request.user.is_authenticated else None - def get(self, request, pk): - order = ( - Order.objects - .select_related("user") - .prefetch_related( - Prefetch("order_products", queryset=OrderProduct.objects.select_related("product")) - ) - .filter(pk=pk) - .first() + checkout_session = stripe.checkout.Session.create( + payment_method_types=["card"], + line_items=line_items_stripe, + mode="payment", + shipping_address_collection={ + "allowed_countries": ["ES"], + }, + customer_email=customer_email, + success_url=domain_url + "/order/success/?session_id={CHECKOUT_SESSION_ID}", + cancel_url=domain_url + "/order/cancelled/", ) + return redirect(checkout_session.url, code=303) - if not order: - raise Http404("Pedido no encontrado") + except Exception as e: + return HttpResponse(f"Error al conectar con Stripe: {e}") - # ✅ Permitir anónimos SOLO si vienen del seguimiento - if not request.user.is_authenticated: - if request.GET.get("from") == "track": - return render(request, self.template_name, {"order": order}) - return redirect("login") - # 🛡 Permisos normales - if request.user.role != "admin" and order.user != request.user: - raise PermissionDenied +def successful_payment(request): + """ + Verifica el pago, crea el pedido y ACTUALIZA EL STOCK. + Usa una transacción atómica para asegurar que todo se guarda o nada. + """ + session_id = request.GET.get("session_id") + + if not session_id: + return HttpResponse("Error: No se ha recibido confirmación de pago.") + + try: + session = stripe.checkout.Session.retrieve(session_id) + customer_details = session.customer_details + stripe_email = customer_details.email + + address_data = customer_details.address + shipping_address = f"{address_data.line1}, {address_data.city}, {address_data.postal_code}, {address_data.country}" + if address_data.line2: + shipping_address += f", {address_data.line2}" + + if session.payment_status == "paid": + # --- INICIO DE TRANSACCIÓN --- + # Esto asegura que si falla la creación de productos, no se crea el pedido vacío + with transaction.atomic(): + items_to_process = [] + cart_to_delete = None + + # Si esta logueado + if request.user.is_authenticated: + cart = Cart.objects.filter(user=request.user).first() + if cart: + cart_to_delete = cart + for cart_item in cart.cart_products.select_related( + "product" + ).all(): + items_to_process.append( + { + "product": cart_item.product, + "quantity": cart_item.quantity, + } + ) + # Si no esta logueado, usamos la sesion + else: + cart_session = request.session.get("cart_session", {}) + if cart_session: + product_pks = [int(pk) for pk in cart_session.keys()] + products = Product.objects.filter(pk__in=product_pks) + for product in products: + qty = cart_session[str(product.pk)]["quantity"] + items_to_process.append( + {"product": product, "quantity": qty} + ) + + if not items_to_process: + # Si no hay productos, no creamos el pedido. + return HttpResponse( + "Error: No se encontraron productos en el carrito para procesar el pedido." + ) + + # 3. Buscar usuario por email + User = get_user_model() + user_for_order = User.objects.filter(email=stripe_email).first() + + # 4. Crear el Pedido + new_order = Order.objects.create( + user=user_for_order, # Si no existe el usuario, se pone None + status=Status.EN_PREPARACION, + address=shipping_address, + email=stripe_email, + ) + + # 5. Crear OrderProducts y actualizamos el Stock + for item_data in items_to_process: + product = item_data["product"] + qty = item_data["quantity"] + + OrderProduct.objects.create( + order=new_order, product=product, quantity=qty + ) + + Product.objects.filter(pk=product.pk).update(stock=F("stock") - qty) + + # 6. Borrar el carrito + if cart_to_delete: + cart_to_delete.delete() + else: + request.session["cart_session"] = {} + request.session.modified = True + # --- ENVÍO DE CORREO DE CONFIRMACIÓN --- + try: + # 1. Generar la URL absoluta de seguimiento + tracking_url = request.build_absolute_uri(reverse("order_search")) + + # 2. Definir asunto y mensaje + subject = f"Confirmación de Pedido #{new_order.tracking_code} - Essenza" + + # Mensaje simple en texto plano + message = f""" + Hola! + + Gracias por tu compra en Essenza. + Tu pedido ha sido confirmado y se está preparando. + + Detalles del pedido: + Nº de localizador: {new_order.tracking_code} + Total: {new_order.total_price} € + Dirección de envío: {new_order.address} + + Puedes seguir el estado de tu pedido aquí: + {tracking_url} + + Gracias por confiar en nosotros. + """ + + # 3. Enviar el correo + send_mail( + subject, + message, + settings.DEFAULT_FROM_EMAIL, # Asegúrate de tener esto en settings.py + [new_order.email], # El email del destinatario + fail_silently=True, # Si falla, no rompe la web + ) + except Exception as e: + # Si falla el correo, lo imprimimos en consola pero dejamos pasar al usuario + print(f"Error enviando email: {e}") + + return render(request, "order/success.html", {"order": new_order}) + + else: + return HttpResponse("El pago no se ha completado.") + + except Exception as e: + return HttpResponse(f"Error verificando el pago o creando el pedido: {e}") + - return render(request, self.template_name, {"order": order}) +def cancelled_payment(request): + return render(request, "order/cancel.html") diff --git a/essenza/product/tests.py b/essenza/product/tests.py index 6bf1658..a28394b 100644 --- a/essenza/product/tests.py +++ b/essenza/product/tests.py @@ -1,7 +1,6 @@ from decimal import Decimal from django.contrib.auth import get_user_model -from django.contrib.messages import get_messages # Para probar mensajes from django.test import TestCase from django.urls import reverse from django.utils import timezone @@ -92,7 +91,7 @@ def test_admin_user_is_forbidden(self): """Prueba que los 'admin' son bloqueados.""" self.client.login(email="admin@test.com", password="pass") resp = self.client.get(self.dashboard_url) - self.assertEqual(resp.status_code, 403) + self.assertEqual(resp.status_code, 302) # --- 2. Tests de Lógica de Negocio (método get) --- @@ -137,9 +136,6 @@ def test_logic_branch_2_falls_back_to_1_year_products(self): Prueba el segundo 'if': Falla 30 días, muestra 1 año. Ignora ventas de hace más de 1 año. """ - # 1. NO crear ventas recientes - - # 2. Crear ventas antiguas (hace 100 días) order_old = Order.objects.create( user=self.regular_user, address="Test Address 1", @@ -149,7 +145,6 @@ def test_logic_branch_2_falls_back_to_1_year_products(self): order=order_old, product=self.p_1_year, quantity=500 ) - # 3. Crear ventas MUY antiguas (hace 400 días) - debe ignorarse order_ancient = Order.objects.create( user=self.regular_user, address="Test Address 2", @@ -162,7 +157,6 @@ def test_logic_branch_2_falls_back_to_1_year_products(self): resp = self.client.get(self.dashboard_url) products_in_context = list(resp.context["products"]) - # ASERCIÓN: Solo debe aparecer el producto de 1 año self.assertEqual(len(products_in_context), 1) self.assertEqual(products_in_context[0], self.p_1_year) self.assertEqual(products_in_context[0].total_quantity, 500) @@ -172,10 +166,6 @@ def test_logic_branch_3_falls_back_to_stock_products(self): Prueba el tercer 'if': Falla 1 año, muestra por stock. Ignora ventas de productos inactivos. """ - # 1. NO crear ventas recientes - # 2. NO crear ventas en el último año - - # 3. Crear ventas MUY antiguas (hace 400 días) - para forzar el fallback order_ancient = Order.objects.create( user=self.regular_user, address="Test Address 1", @@ -185,11 +175,11 @@ def test_logic_branch_3_falls_back_to_stock_products(self): order=order_ancient, product=self.p_30_day, quantity=999 ) - # 4. Crear ventas de productos INACTIVOS (deben ignorarse siempre) order_inactive = Order.objects.create( user=self.regular_user, address="Test Address 2", placed_at=self.now - timezone.timedelta(days=10), # Reciente, pero inactivo + tracking_code="4957", ) OrderProduct.objects.create( order=order_inactive, product=self.p_inactive, quantity=5000 @@ -198,21 +188,15 @@ def test_logic_branch_3_falls_back_to_stock_products(self): resp = self.client.get(self.dashboard_url) products_in_context = list(resp.context["products"]) - # ASERCIÓN: Debe mostrar los productos activos por stock descendente - # p_stock (999) > p_1_year (20) > p_30_day (10) > p_stock_low (1) - # p_inactive (1000) debe ser ignorado. - self.assertIn(self.p_stock, products_in_context) self.assertIn(self.p_1_year, products_in_context) self.assertNotIn(self.p_inactive, products_in_context) # Clave - # Comprobar el orden por stock self.assertEqual(products_in_context[0], self.p_stock) # stock 999 self.assertEqual(products_in_context[1], self.p_1_year) # stock 20 self.assertEqual(products_in_context[2], self.p_30_day) # stock 10 self.assertEqual(products_in_context[3], self.p_stock_low) # stock 1 - # Comprobar que no hay 'total_quantity' (viene de la consulta de stock) self.assertFalse(hasattr(products_in_context[0], "total_quantity")) def test_logic_branch_4_handles_empty_database(self): @@ -221,16 +205,11 @@ def test_logic_branch_4_handles_empty_database(self): La vista debe devolver una lista vacía, no romperse. """ - # 1. Borrar TODOS los productos creados en el setUp - # (Esto deja la BBDD sin productos activos) Product.objects.all().delete() - # Cargar la vista resp = self.client.get(self.dashboard_url) self.assertEqual(resp.status_code, 200) - # ASERCIÓN: - # El contexto 'products' debe existir, pero estar vacío. self.assertIn("products", resp.context) products_in_context = list(resp.context["products"]) @@ -582,11 +561,6 @@ def test_post_admin_updates_stock_successfully(self): self.product_high.refresh_from_db() self.assertEqual(self.product_high.stock, 15) - # Comprobamos el mensaje de éxito - messages = list(get_messages(resp.context["request"])) - self.assertEqual(len(messages), 1) - self.assertEqual(str(messages[0]), "Stock de 'Producto Alto' actualizado a 15.") - def test_post_admin_invalid_product_returns_404(self): self.client.login(email=self.admin.email, password="pass1234") data = {"product_id": 999, "stock": 15} # ID 999 no existe @@ -608,13 +582,6 @@ def test_post_admin_invalid_stock_value_shows_error(self): self.product_high.refresh_from_db() self.assertEqual(self.product_high.stock, 20) - # Comprobamos el mensaje de error - messages = list(get_messages(resp.context["request"])) - self.assertEqual(len(messages), 1) - self.assertEqual( - str(messages[0]), "El valor de stock 'abc' no es un número válido." - ) - def test_post_admin_negative_stock_value_shows_error(self): self.client.login(email=self.admin.email, password="pass1234") @@ -626,9 +593,3 @@ def test_post_admin_negative_stock_value_shows_error(self): self.product_high.refresh_from_db() self.assertEqual(self.product_high.stock, 20) # No cambia - - messages = list(get_messages(resp.context["request"])) - self.assertEqual(len(messages), 1) - self.assertEqual( - str(messages[0]), "El valor de stock '-5' no es un número válido." - ) diff --git a/essenza/product/views.py b/essenza/product/views.py index 23638b6..fb59271 100644 --- a/essenza/product/views.py +++ b/essenza/product/views.py @@ -1,4 +1,3 @@ -from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.db.models import Sum from django.shortcuts import get_object_or_404, redirect, render @@ -22,7 +21,7 @@ def test_func(self): return ( not self.request.user.is_authenticated or self.request.user.role != "admin" ) - + def handle_no_permission(self): return redirect("stock") @@ -98,14 +97,9 @@ def post(self, request): product.stock = new_stock product.save(update_fields=["stock"]) - messages.success( - request, f"Stock de '{product.name}' actualizado a {new_stock}." - ) except (ValueError, TypeError): - messages.error( - request, f"El valor de stock '{stock_value}' no es un número válido." - ) + pass # Recarga la misma página return redirect("stock") 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/base.html b/essenza/templates/base.html index 32e9948..b15bfc6 100644 --- a/essenza/templates/base.html +++ b/essenza/templates/base.html @@ -73,12 +73,11 @@ /* Centrar el buscador */ .nav-center { flex: 1; - justify-content: center; + justify-content: right; } .search-bar input { - width: 100%; - max-width: 420px; + width: 320px; padding: 11px 20px; border: 1px solid #e0d5ca; border-radius: 24px; @@ -86,6 +85,7 @@ font-size: 14px; transition: all 0.3s ease; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); + margin-right: 20px; } .search-bar input:focus { outline: none; @@ -500,9 +500,6 @@ width: 100%; margin-top: 12px; } - .search-bar input { - max-width: 100%; - } } @@ -513,24 +510,31 @@
+ + diff --git a/essenza/templates/cart/cart_detail.html b/essenza/templates/cart/cart_detail.html index bdee429..7c8561c 100644 --- a/essenza/templates/cart/cart_detail.html +++ b/essenza/templates/cart/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 %} + +{% endblock %} \ No newline at end of file diff --git a/essenza/templates/order/order_list_admin.html b/essenza/templates/order/order_list_admin.html index 63f5822..48c343c 100644 --- a/essenza/templates/order/order_list_admin.html +++ b/essenza/templates/order/order_list_admin.html @@ -1,207 +1,413 @@ {% extends "base.html" %} {% load humanize %} +{% load static %} -{% block title %}Listado de pedidos · Admin · Essenza{% endblock %} +{% block title %}Mis pedidos · Essenza{% endblock %} -{% block content %} +{% block extra_head %} + +{% endblock %} -
-
-
-

Pedidos (Admin)

-

Listado completo de pedidos realizados.

-
+{% block content %} +
+ + + + +
+ + Todos + + + + En Preparación + - {% if orders %} - {% for order in orders %} + + Enviados + + + + Entregados + +
+ + + {% if orders %} + {% for order in orders %} + + + +
-
- {% endfor %} - {% else %} -
-

No hay pedidos para mostrar.

-
- {% endif %} -
+ + {% endfor %} + + {% else %} +
+ +

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 %} +
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/essenza/templates/order/order_list_user.html b/essenza/templates/order/order_list_user.html deleted file mode 100644 index 4f5ad8d..0000000 --- a/essenza/templates/order/order_list_user.html +++ /dev/null @@ -1,209 +0,0 @@ -{% extends "base.html" %} -{% load humanize %} - -{% block title %}Mis pedidos · Essenza{% endblock %} - -{% block content %} - - -
-
-
-

Mis pedidos

-

Consulta tu historial de compras y su estado.

-
- - {% if orders %} - {% for order in orders %} -
-
-
- Pedido #{{ order.id }}
- - {{ order.placed_at|date:"d/m/Y H:i" }} - -
- - {% if order.status == "en_preparacion" %} - {{ order.get_status_display }} - {% elif order.status == "enviado" %} - {{ order.get_status_display }} - {% elif order.status == "entregado" %} - {{ order.get_status_display }} - {% else %} - {{ order.get_status_display }} - {% endif %} -
- -
-
- Productos -
    - {% for op in order.order_products.all %} -
  • {{ op.product.name }} × {{ op.quantity }} — {{ op.subtotal|floatformat:2 }} €
  • - {% empty %} -
  • -
  • - {% endfor %} -
-
- -
-
- Total: {{ order.total_price|floatformat:2 }} € -
-
- Dirección: {{ order.address|default:"-" }} -
- - - Ver detalle del pedido - -
-
-
- {% endfor %} - {% else %} -
-

No tienes pedidos todavía.

-

Cuando compres algo, aparecerá aquí ✨

-
- {% endif %} -
-
-{% endblock %} diff --git a/essenza/templates/order/order_track.html b/essenza/templates/order/order_search.html similarity index 56% rename from essenza/templates/order/order_track.html rename to essenza/templates/order/order_search.html index 82868b3..6a82368 100644 --- a/essenza/templates/order/order_track.html +++ b/essenza/templates/order/order_search.html @@ -145,78 +145,30 @@

Seguimiento de pedido

{% csrf_token %} +
- - + +
+
- - + +
+ -
- - {% if searched %} - {% if error %} -
{{ error }}
- {% elif order %} -
- -
-
Pedido
-
#{{ order.id }}
-
- -
-
Fecha
-
{{ order.placed_at|date:"d/m/Y H:i" }}
-
- -
-
Dirección
-
{{ order.address|default:"-" }}
-
-
-
Estado
-
- {% if order.status == "en_preparacion" %} - {{ order.get_status_display }} - {% elif order.status == "enviado" %} - {{ order.get_status_display }} - {% elif order.status == "entregado" %} - {{ order.get_status_display }} - {% else %} - {{ order.get_status_display }} - {% endif %} -
+ {% if messages %} + {% for message in messages %} +
+ {{ message }}
- -
-
Total
-
{{ order.total_price|floatformat:2 }} €
-
- -
-
Productos
-
    - {% for op in order.order_products.all %} -
  • {{ op.product.name }} × {{ op.quantity }} — {{ op.subtotal|floatformat:2 }} €
  • - {% empty %} -
  • -
  • - {% endfor %} -
-
- - - Ver detalle completo del pedido - - -
+ {% endfor %} {% endif %} - {% endif %} -
-
- -{% endblock %} + +{% endblock %} \ No newline at end of file diff --git a/essenza/templates/order/success.html b/essenza/templates/order/success.html new file mode 100644 index 0000000..75d125c --- /dev/null +++ b/essenza/templates/order/success.html @@ -0,0 +1,42 @@ +{% extends 'base.html' %} {% block content %} +
+
+ ✔ +
+ +

¡Gracias por tu compra!

+ +

+ Tu pedido ha sido procesado y el pago se ha completado correctamente. + {% if user.is_authenticated %} +
Puedes ver el estado en tu perfil. + {% else %} +
Te hemos enviado un email de confirmación. + {% endif %} +

+ + +
+{% endblock %} diff --git a/essenza/templates/order/tracking.html b/essenza/templates/order/tracking.html new file mode 100644 index 0000000..3c110af --- /dev/null +++ b/essenza/templates/order/tracking.html @@ -0,0 +1,437 @@ +{% extends "base.html" %} +{% load static %} +{% load humanize %} + +{% block title %}Seguimiento #{{ order.tracking_code }} · Essenza{% endblock %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} +
+ + +
+
Estado de tu Pedido
+
LOCALIZADOR: {{ order.tracking_code }}
+ + + {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + +
+ + {% if user.is_authenticated and user.role == 'admin' %} + + + + + +
+
+ {% csrf_token %} + + +
+
+ + +
+
+ {% csrf_token %} + + +
+
+ + +
+
+ {% csrf_token %} + + +
+
+ + {% else %} + + + + + +
+
+
En Preparación
+
+ + +
+
+
Enviado
+
+ + +
+
+
Entregado
+
+ {% endif %} + +
+ + {% if user.is_authenticated and user.role == 'admin' %} +
Modo Admin: Haz clic en un icono para cambiar el estado
+ {% endif %} +
+ + +
+
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 diff --git a/essenza/templates/product/catalog.html b/essenza/templates/product/catalog.html index 4a4d727..e672b67 100644 --- a/essenza/templates/product/catalog.html +++ b/essenza/templates/product/catalog.html @@ -1,326 +1,298 @@ {% 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

+ +
-
-