diff --git a/essenza/Procfile b/essenza/Procfile new file mode 100644 index 0000000..55dff08 --- /dev/null +++ b/essenza/Procfile @@ -0,0 +1 @@ +gunicorn essenza.wsgi:application --workers 2 --log-file - \ No newline at end of file diff --git a/essenza/deploy.sh b/essenza/deploy.sh index 7d64b16..8c686ea 100644 --- a/essenza/deploy.sh +++ b/essenza/deploy.sh @@ -1,7 +1,43 @@ -pip install -r requirements.txt && \ -python manage.py flush --noinput && \ -python manage.py migrate --noinput && \ -# python manage.py collectstatic --noinput && \ -python manage.py loaddata user/sample/sample.json && \ -python manage.py loaddata product/sample/sample.json && \ -python manage.py loaddata order/sample/sample.json \ No newline at end of file +# --------------------------------------------------------- +# IMPORTANTE: Este archivo RESTAURA TOTALMENTE la BD del proyecto. +# --------------------------------------------------------- + +set -e + +echo "" +echo "--- Instalando dependencias (pip)..." +pip install -r requirements.txt + +echo "" +echo "--- Recolectando estáticos (CSS/JS)..." +python3 manage.py collectstatic --no-input + +echo "" +echo "--- Vaciando DB..." +python3 manage.py flush --no-input + +echo "" +echo "--- Aplicando Migraciones (Migrate)..." +python3 manage.py migrate --no-input + +echo "" +echo "--- Copiando imagenes de sampleo a 'media/'..." +mkdir -p media +cp -r _sample_assets/* media/ 2>/dev/null || echo "Aviso: No se encontraron assets en _sample_assets/" + +echo "" +echo "--- Cargando datos de USER..." +python3 manage.py loaddata user/sample/sample.json + +echo "" +echo "--- Cargando datos de PRODUCT..." +python3 manage.py loaddata product/sample/sample.json + +echo "" +echo "--- Cargando datos de ORDER..." +python3 manage.py loaddata order/sample/sample.json + +echo "" +echo "========================================================" +echo "!PROCESO COMPLETADO CON EXITO!" +echo "========================================================" \ No newline at end of file diff --git a/essenza/essenza/settings.py b/essenza/essenza/settings.py index e6f79a4..beefc75 100644 --- a/essenza/essenza/settings.py +++ b/essenza/essenza/settings.py @@ -13,6 +13,7 @@ import os from pathlib import Path +import dj_database_url from dotenv import load_dotenv load_dotenv() @@ -34,6 +35,11 @@ ALLOWED_HOSTS = ["*"] +CSRF_TRUSTED_ORIGINS = [ + "https://pgpi-g1-11.onrender.com", + "http://127.0.0.1:8000", + "http://localhost:8000", +] # Application definition @@ -45,6 +51,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "django.contrib.humanize", + "anymail", "user", "product", "order", @@ -85,17 +92,18 @@ WSGI_APPLICATION = "essenza.wsgi.application" -# Database +# Database (Híbrida) # https://docs.djangoproject.com/en/5.2/ref/settings/#databases DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } + "default": dj_database_url.config( + # Si estás en Render, usará la variable DATABASE_URL automáticamente. + # Si estás en Local (no hay DATABASE_URL), usará este sqlite: + default="sqlite:///" + str(BASE_DIR / "db.sqlite3"), + conn_max_age=600, + ) } - # Password validation # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators @@ -119,10 +127,8 @@ # https://docs.djangoproject.com/en/5.2/topics/i18n/ LANGUAGE_CODE = "es" - TIME_ZONE = "Europe/Madrid" USE_I18N = True - USE_TZ = True @@ -156,18 +162,41 @@ # ----------------------------------------------------------------- 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 +DOMAIN_URL = os.getenv("DOMAIN_URL", "http://127.0.0.1:8000") -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") +# ----------------------------------------------------------------- +# CONFIGURACIÓN DE EMAIL +# ----------------------------------------------------------------- +if not DEBUG: + # ----------------------------------------------------------- + # PRODUCCIÓN (Render) -> Usa SendGrid (Anymail) + # ----------------------------------------------------------- + EMAIL_BACKEND = "anymail.backends.sendgrid.EmailBackend" + ANYMAIL = {"SENDGRID_API_KEY": os.getenv("SENDGRID_API_KEY")} + DEFAULT_FROM_EMAIL = "noreply.essenza@gmail.com" + + # Logging para debuggear emails en la nube + LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": {"console": {"class": "logging.StreamHandler"}}, + "loggers": { + "django.core.mail": { + "handlers": ["console"], + "level": "DEBUG", + "propagate": True, + }, + }, + } -# El remitente que aparecerá en los correos -DEFAULT_FROM_EMAIL = "Essenza " +else: + # ----------------------------------------------------------- + # LOCAL (Tu PC) -> Usa SMTP de Gmail + # ----------------------------------------------------------- + EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" + EMAIL_HOST = "smtp.gmail.com" + EMAIL_PORT = 587 + EMAIL_USE_TLS = True + EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") + EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") + DEFAULT_FROM_EMAIL = "noreply@essenza.com" diff --git a/essenza/essenza/urls.py b/essenza/essenza/urls.py index 47f73cd..c0ea9a7 100644 --- a/essenza/essenza/urls.py +++ b/essenza/essenza/urls.py @@ -1,12 +1,11 @@ from django.conf import settings -from django.conf.urls.static import static from django.contrib import admin -from django.urls import include, path -from info.views import info_view +from django.urls import include, path, re_path +from django.views.static import serve from product.views import CatalogDetailView, CatalogView, DashboardView urlpatterns = [ - path("info/", info_view, name="info-home"), + path("info/", include("info.urls")), path("user/", include("user.urls")), path("admin/", admin.site.urls), path("product/", include("product.urls")), @@ -15,8 +14,15 @@ path("catalog//", CatalogDetailView.as_view(), name="catalog_detail"), path("cart/", include("cart.urls")), path("order/", include("order.urls")), - path('info/', include('info.urls')), ] -if settings.DEBUG: - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +# Arreglo para muestra de imágenes en producción +urlpatterns += [ + re_path( + r"^media/(?P.*)$", + serve, + { + "document_root": settings.MEDIA_ROOT, + }, + ), +] diff --git a/essenza/info/tests.py b/essenza/info/tests.py index 23bdc4a..39369f9 100644 --- a/essenza/info/tests.py +++ b/essenza/info/tests.py @@ -55,9 +55,9 @@ def setUpTestData(cls): OrderProduct.objects.create(order=cls.order1, product=cls.prod2, quantity=5) OrderProduct.objects.create(order=cls.order2, product=cls.prod1, quantity=3) - cls.history_url = reverse("info:sales_reports_view", args=["history"]) - cls.product_url = reverse("info:sales_reports_view", args=["product"]) - cls.user_url = reverse("info:sales_reports_view", args=["user"]) + cls.history_url = reverse("sales_reports_view", args=["history"]) + cls.product_url = reverse("sales_reports_view", args=["product"]) + cls.user_url = reverse("sales_reports_view", args=["user"]) def test_unauthenticated_access_is_denied(self): self.client.logout() diff --git a/essenza/info/urls.py b/essenza/info/urls.py index a398b12..3691c92 100644 --- a/essenza/info/urls.py +++ b/essenza/info/urls.py @@ -1,11 +1,20 @@ # essenza/info/urls.py from django.urls import path -from . import views -app_name = 'info' +from . import views urlpatterns = [ - path("reports/", views.SalesReportsView.as_view(), {'report_type': 'history'}, name="sales_history_report"), - path("reports//", views.SalesReportsView.as_view(), name="sales_reports_view"), -] \ No newline at end of file + path("", views.info_view, name="info-home"), + path( + "reports/", + views.SalesReportsView.as_view(), + {"report_type": "history"}, + name="sales_history_report", + ), + path( + "reports//", + views.SalesReportsView.as_view(), + name="sales_reports_view", + ), +] diff --git a/essenza/info/views.py b/essenza/info/views.py index 1f8605e..bc0492c 100644 --- a/essenza/info/views.py +++ b/essenza/info/views.py @@ -1,63 +1,82 @@ # essenza/info/views.py from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin -from django.db.models import Sum, F, Count -from django.shortcuts import redirect, render +from django.db.models import F, Sum +from django.shortcuts import render from django.urls import reverse from django.views import View -from django.http import HttpResponseForbidden - from order.models import Order, OrderProduct def info_view(request): return render(request, "info/info.html") + class SalesReportsView(LoginRequiredMixin, UserPassesTestMixin, View): """ Maneja la visualización de los tres tipos de reportes: history (Historial de Pedidos), product (Ventas por Producto), user (Ventas por Usuario). """ - raise_exception = True + + raise_exception = True def test_func(self): return self.request.user.is_authenticated and self.request.user.role == "admin" - def get(self, request, report_type='history'): + + def get(self, request, report_type="history"): reports_nav = [ - {'id': 'history', 'name': 'Historial de Ventas', 'url': reverse('info:sales_reports_view', args=['history'])}, - {'id': 'product', 'name': 'Ventas por Producto', 'url': reverse('info:sales_reports_view', args=['product'])}, - {'id': 'user', 'name': 'Ventas por Usuario', 'url': reverse('info:sales_reports_view', args=['user'])}, + { + "id": "history", + "name": "Historial de Ventas", + "url": reverse("sales_reports_view", args=["history"]), + }, + { + "id": "product", + "name": "Ventas por Producto", + "url": reverse("sales_reports_view", args=["product"]), + }, + { + "id": "user", + "name": "Ventas por Usuario", + "url": reverse("sales_reports_view", args=["user"]), + }, ] - + context = { - 'reports_nav': reports_nav, - 'current_report': report_type, + "reports_nav": reports_nav, + "current_report": report_type, } - - if report_type == 'product': - context['report_title'] = 'Ventas Totales por Producto' - context['template_name'] = 'info/product_sales.html' - context['sales_data'] = OrderProduct.objects.values( - 'product__id', 'product__name' - ).annotate( - total_sold=Sum('quantity'), - total_revenue=Sum(F('quantity') * F('product__price')) - ).order_by('-total_revenue') - - elif report_type == 'user': - context['report_title'] = 'Ventas Totales por Usuario' - context['template_name'] = 'info/user_sales.html' - context['sales_data'] = Order.objects.values( - 'user__id', 'user__first_name', 'user__email' - ).annotate( - total_spent=Sum(F('order_products__quantity') * F('order_products__product__price')) - ).exclude( - user__isnull=True - ).order_by('-total_spent') - - else: - context['report_title'] = 'Historial Completo de Ventas' - context['template_name'] = 'info/sales_history.html' - context['orders'] = Order.objects.all().order_by('-placed_at') - - return render(request, 'info/reports_master.html', context) \ No newline at end of file + + if report_type == "product": + context["report_title"] = "Ventas Totales por Producto" + context["template_name"] = "info/product_sales.html" + context["sales_data"] = ( + OrderProduct.objects.values("product__id", "product__name") + .annotate( + total_sold=Sum("quantity"), + total_revenue=Sum(F("quantity") * F("product__price")), + ) + .order_by("-total_revenue") + ) + + elif report_type == "user": + context["report_title"] = "Ventas Totales por Usuario" + context["template_name"] = "info/user_sales.html" + context["sales_data"] = ( + Order.objects.values("user__id", "user__first_name", "user__email") + .annotate( + total_spent=Sum( + F("order_products__quantity") + * F("order_products__product__price") + ) + ) + .exclude(user__isnull=True) + .order_by("-total_spent") + ) + + else: + context["report_title"] = "Historial Completo de Ventas" + context["template_name"] = "info/sales_history.html" + context["orders"] = Order.objects.all().order_by("-placed_at") + + return render(request, "info/reports_master.html", context) diff --git a/essenza/requirements.txt b/essenza/requirements.txt index 87741c7..869bc73 100644 --- a/essenza/requirements.txt +++ b/essenza/requirements.txt @@ -1,11 +1,17 @@ asgiref==3.10.0 certifi==2025.11.12 +cffi==2.0.0 charset-normalizer==3.4.4 +cryptography==46.0.3 +dj-database-url==3.0.1 Django==5.2.8 +django-anymail==13.1 gunicorn==23.0.0 idna==3.11 packaging==25.0 pillow==12.0.0 +psycopg2-binary==2.9.11 +pycparser==2.23 python-dotenv==1.2.1 requests==2.32.5 sqlparse==0.5.3 diff --git a/essenza/templates/base.html b/essenza/templates/base.html index eefd7ed..0d2a1ea 100644 --- a/essenza/templates/base.html +++ b/essenza/templates/base.html @@ -518,7 +518,7 @@ Productos Pedidos Usuarios - Ventas + Ventas {% else %} Escaparate Catalogo diff --git a/essenza/templates/order/order_history.html b/essenza/templates/order/order_history.html index b26e517..582c6c2 100644 --- a/essenza/templates/order/order_history.html +++ b/essenza/templates/order/order_history.html @@ -225,6 +225,24 @@ transition: background 0.2s; } .btn-shop:hover { background-color: #a35a34; } + .payment-badge { + font-size: 0.7rem; + padding: 3px 8px; + border-radius: 4px; + font-weight: 700; + margin-bottom: 10px; + display: inline-block; + } + .pay-ok { + color: #2e7d32; + background: #eeffee; + border: 1px solid #2e7d32; + } + .pay-pending { + color: #c62828; + background: #ffebee; + border: 1px solid #ef9a9a; + } @media (max-width: 768px) { .card-body { @@ -308,16 +326,29 @@

Mis pedidos

- - {% if order.status == "en_preparacion" %} - Preparando - {% elif order.status == "enviado" %} - Enviado - {% elif order.status == "entregado" %} - Entregado - {% else %} - {{ order.get_status_display }} - {% endif %} +
+ + {% if order.is_paid %} + + Pagado + + {% else %} + + Pendiente de pago + + {% endif %} + + + {% if order.status == "en_preparacion" %} + Preparando + {% elif order.status == "enviado" %} + Enviado + {% elif order.status == "entregado" %} + Entregado + {% else %} + {{ order.get_status_display }} + {% endif %} +
diff --git a/essenza/templates/order/order_list_admin.html b/essenza/templates/order/order_list_admin.html index 55d4b3a..3ea1117 100644 --- a/essenza/templates/order/order_list_admin.html +++ b/essenza/templates/order/order_list_admin.html @@ -254,17 +254,35 @@ } .btn-shop { - display: inline-block; - margin-top: 15px; - background-color: #c06b3e; /* btn-create style */ - color: white; - padding: 12px 30px; - border-radius: 5px; - text-decoration: none; - font-weight: bold; - transition: background-color 0.3s; + display: inline-block; + margin-top: 15px; + background-color: #c06b3e; /* btn-create style */ + color: white; + padding: 12px 30px; + border-radius: 5px; + text-decoration: none; + font-weight: bold; + transition: background-color 0.3s; } .btn-shop:hover { background-color: #a35a34; } + .payment-badge { + font-size: 0.7rem; + padding: 3px 8px; + border-radius: 4px; + font-weight: 700; + margin-bottom: 10px; + display: inline-block; + } + .pay-ok { + color: #2e7d32; + background: #eeffee; + border: 1px solid #2e7d32; + } + .pay-pending { + color: #c62828; + background: #ffebee; + border: 1px solid #ef9a9a; + } @media (max-width: 768px) { .card-body { @@ -374,16 +392,29 @@

Pedidos

- - {% if order.status == "en_preparacion" %} - Preparando - {% elif order.status == "enviado" %} - Enviado - {% elif order.status == "entregado" %} - Entregado - {% else %} - {{ order.get_status_display }} - {% endif %} +
+ + {% if order.is_paid %} + + Pagado + + {% else %} + + Pendiente de pago + + {% endif %} + + + {% if order.status == "en_preparacion" %} + Preparando + {% elif order.status == "enviado" %} + Enviado + {% elif order.status == "entregado" %} + Entregado + {% else %} + {{ order.get_status_display }} + {% endif %} +
diff --git a/essenza/templates/order/tracking.html b/essenza/templates/order/tracking.html index 5c4e259..ca642a9 100644 --- a/essenza/templates/order/tracking.html +++ b/essenza/templates/order/tracking.html @@ -390,8 +390,12 @@ {{ order.email }}
- Estado Actual - {{ order.get_status_display }} + Método de Pago + {% if order.is_paid %} + Pago con Tarjeta + {% else %} + Pago en efectivo en la recogida + {% endif %}