feat: Add user purchase and receipt management
- Implemented 'Mis Compras' and 'Mis Recibos' pages for users to view their orders and payment receipts. - Enhanced address validation in 'editar_direccion.html' to ensure cities and postal codes belong to Almería. - Added shipping address display in seller order details on 'pedidos_vendedor.html'. - Updated user portal to include links to purchases and receipts. - Introduced email verification functionality during user registration. - Refactored email sending utility for better error handling and logging. - Improved session management for checkout processes with selected shipping addresses.
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
from .celery import app as celery_app
|
||||||
|
__all__ = ('celery_app',)
|
||||||
Binary file not shown.
Binary file not shown.
+155
-13
@@ -10,26 +10,65 @@ For the full list of settings and their values, see
|
|||||||
https://docs.djangoproject.com/en/6.0/ref/settings/
|
https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os, sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def load_dotenv(dotenv_path: Path) -> None:
|
||||||
|
if not dotenv_path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
for raw_line in dotenv_path.read_text(encoding='utf-8').splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line or line.startswith('#') or '=' not in line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
key = key.strip()
|
||||||
|
value = value.strip().strip('"').strip("'")
|
||||||
|
os.environ.setdefault(key, value)
|
||||||
|
|
||||||
|
|
||||||
|
def env_bool(name: str, default: bool = False) -> bool:
|
||||||
|
value = os.getenv(name)
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
return value.strip().lower() in {'1', 'true', 'yes', 'on'}
|
||||||
|
|
||||||
|
|
||||||
|
def env_list(name: str, default: list[str] | None = None) -> list[str]:
|
||||||
|
value = os.getenv(name)
|
||||||
|
if value is None:
|
||||||
|
return default or []
|
||||||
|
return [item.strip() for item in value.split(',') if item.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def env_int(name: str, default: int) -> int:
|
||||||
|
value = os.getenv(name)
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
return int(value)
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
load_dotenv(BASE_DIR / '.env')
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = 'django-insecure-#g((q@lvnkt(j6)2(gvtn0px)r2r(911)pv59i(6w)5e!_-^ao'
|
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-#g((q@lvnkt(j6)2(gvtn0px)r2r(911)pv59i(6w)5e!_-^ao')
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = env_bool('DEBUG', True)
|
||||||
|
|
||||||
ALLOWED_HOSTS = [
|
ALLOWED_HOSTS = env_list('ALLOWED_HOSTS', [
|
||||||
"192.168.1.142",
|
'192.168.1.142',
|
||||||
"localhost",
|
'localhost',
|
||||||
"127.0.0.1"
|
'127.0.0.1',
|
||||||
]
|
])
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
@@ -152,7 +191,7 @@ MEDIA_ROOT = BASE_DIR / 'tienda' / 'static' / 'media'
|
|||||||
CACHES = {
|
CACHES = {
|
||||||
'default': {
|
'default': {
|
||||||
'BACKEND': 'django_redis.cache.RedisCache',
|
'BACKEND': 'django_redis.cache.RedisCache',
|
||||||
'LOCATION': 'redis://127.0.0.1:6379/1',
|
'LOCATION': os.getenv('REDIS_URL', 'redis://127.0.0.1:6379/1'),
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||||
}
|
}
|
||||||
@@ -177,11 +216,114 @@ MESSAGE_TAGS = {
|
|||||||
# Login URL
|
# Login URL
|
||||||
LOGIN_URL = '/tienda/login/'
|
LOGIN_URL = '/tienda/login/'
|
||||||
|
|
||||||
STRIPE_PUBLISHABLE_KEY = 'pk_test_51SxmSYJ2DN4I0upQDdiPeda51nmpB0ZEWfkNFKHhWBG4knIgtRoC1d9iFRoxRNdJKiLlQsIddlebU06R9XCfiSZH00ffoirwPw'
|
STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY', '')
|
||||||
STRIPE_SECRET_KEY = 'sk_test_51SxmSYJ2DN4I0upQZb42dWKuIKToZxkQeK3vsCdijcaUr17EMEyFcLdIAm5AVEvUs96MAxl4KnZ4Yncp5VykO4ej00MZGs6c1F'
|
STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', '')
|
||||||
|
|
||||||
# PayPal Configuration (Sandbox)
|
# PayPal Configuration (Sandbox)
|
||||||
# Para obtener credenciales: https://sandbox.paypal.com/
|
# Para obtener credenciales: https://sandbox.paypal.com/
|
||||||
PAYPAL_CLIENT_ID = 'AX3TIklQ41456StP2puciDfkQ6oSWAQWNYB8H9ThDsU6C_VYhWqwDZ1w0dK-No38Aa9IqAbrZbE-1kHJ' # Reemplazar con tu Client ID de PayPal Sandbox
|
PAYPAL_CLIENT_ID = os.getenv('PAYPAL_CLIENT_ID', '') # Reemplazar con tu Client ID de PayPal Sandbox
|
||||||
PAYPAL_CLIENT_SECRET = 'EIXny9EkiebiCnwkfmWJa7ufwHwdUCTeSZ5TiUZycBPREradcN7U0vBKCUlg-PYd3SeXTW33D0kZb5BT' # Reemplazar con tu Client Secret de PayPal Sandbox
|
PAYPAL_CLIENT_SECRET = os.getenv('PAYPAL_CLIENT_SECRET', '') # Reemplazar con tu Client Secret de PayPal Sandbox
|
||||||
PAYPAL_MODE = 'sandbox' # Cambiar a 'live' en producción
|
PAYPAL_MODE = os.getenv('PAYPAL_MODE', 'sandbox') # Cambiar a 'live' en producción
|
||||||
|
|
||||||
|
|
||||||
|
SMTP_ENDPOINT = os.getenv('SMTP_ENDPOINT', 'smtp.email.eu-paris-1.oci.oraclecloud.com')
|
||||||
|
SMTP_PORT = env_int('SMTP_PORT', 587)
|
||||||
|
SECURITY = os.getenv('SECURITY', 'tls')
|
||||||
|
SMTP_USERNAME = os.getenv('SMTP_USERNAME', None)
|
||||||
|
SMTP_PASSWORD = os.getenv('SMTP_PASSWORD', None)
|
||||||
|
SMTP_EMAIL = os.getenv("SMTP_EMAIL", None)
|
||||||
|
if SMTP_USERNAME is None or SMTP_PASSWORD is None or SMTP_EMAIL is None:
|
||||||
|
print("Se requieren credenciales SMTP")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'tienda.User'
|
||||||
|
|
||||||
|
|
||||||
|
DOMAIN = os.getenv("DOMAIN", "localhost")
|
||||||
|
PROTOCOL = os.getenv("PROTOCOL", "http")
|
||||||
|
|
||||||
|
|
||||||
|
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper()
|
||||||
|
LOG_DIR = Path(os.getenv('LOG_DIR', BASE_DIR / 'logs'))
|
||||||
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
LOG_FILE = LOG_DIR / os.getenv('LOG_FILE', 'app.log')
|
||||||
|
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'standard': {
|
||||||
|
'format': '%(asctime)s | %(levelname)s | %(name)s | %(message)s',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'console': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'standard',
|
||||||
|
'level': LOG_LEVEL,
|
||||||
|
},
|
||||||
|
'file': {
|
||||||
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
|
'formatter': 'standard',
|
||||||
|
'filename': str(LOG_FILE),
|
||||||
|
'maxBytes': 5 * 1024 * 1024,
|
||||||
|
'backupCount': 5,
|
||||||
|
'level': LOG_LEVEL,
|
||||||
|
'encoding': 'utf-8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'tienda': {
|
||||||
|
'handlers': ['console', 'file'],
|
||||||
|
'level': LOG_LEVEL,
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'tienda.audit': {
|
||||||
|
'handlers': ['console', 'file'],
|
||||||
|
'level': LOG_LEVEL,
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'django': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'email.system': {
|
||||||
|
'handlers': ['console', 'file'],
|
||||||
|
'level': LOG_LEVEL,
|
||||||
|
'propagate': False
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
logging.captureWarnings(True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
EMAIL_HOST = SMTP_ENDPOINT
|
||||||
|
EMAIL_PORT = SMTP_PORT
|
||||||
|
EMAIL_USE_TLS = (SECURITY == 'tls') # True si SECURITY es 'tls'
|
||||||
|
EMAIL_USE_SSL = (SECURITY == 'ssl') # True si SECURITY es 'ssl'
|
||||||
|
EMAIL_HOST_USER = SMTP_USERNAME
|
||||||
|
EMAIL_HOST_PASSWORD = SMTP_PASSWORD
|
||||||
|
|
||||||
|
# El correo que se usará como remitente por defecto
|
||||||
|
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", SMTP_EMAIL)
|
||||||
|
|
||||||
|
# URL de Redis (asumiendo que corre en el puerto default 6379)
|
||||||
|
CELERY_BROKER_URL = 'redis://localhost:6379/0'
|
||||||
|
|
||||||
|
# Opcional: para guardar el resultado de las tareas
|
||||||
|
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
|
||||||
|
|
||||||
|
# Configuraciones adicionales recomendadas
|
||||||
|
CELERY_ACCEPT_CONTENT = ['json']
|
||||||
|
CELERY_TASK_SERIALIZER = 'json'
|
||||||
|
CELERY_RESULT_SERIALIZER = 'json'
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem, OrderMessage
|
from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem, OrderMessage, User, VerificationCode
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|
||||||
admin.site.register(Category)
|
admin.site.register(Category)
|
||||||
admin.site.register(Image)
|
admin.site.register(Image)
|
||||||
admin.site.register(Product)
|
admin.site.register(Product)
|
||||||
|
admin.site.register(User)
|
||||||
|
admin.site.register(VerificationCode)
|
||||||
class CartItemInline(admin.TabularInline):
|
class CartItemInline(admin.TabularInline):
|
||||||
model = CartItem
|
model = CartItem
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-01-23 09:33
|
# Generated by Django 6.0.1 on 2026-03-10 07:56
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@@ -9,6 +13,7 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -17,10 +22,160 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(max_length=200)),
|
('name', models.CharField(max_length=200)),
|
||||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subcategories', to='tienda.category')),
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Image',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(default='', max_length=200)),
|
||||||
|
('image', models.ImageField(upload_to='images/')),
|
||||||
|
('alt', models.CharField(blank=True, default='', max_length=255, verbose_name='Texto alternativo')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('registration_status', models.CharField(choices=[('CR', 'Confirmation Required'), ('AC', 'Active'), ('BN', 'Banned')], default='CR', max_length=2)),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'Categories',
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Cart',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('session_key', models.CharField(blank=True, max_length=40, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Order',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('session_key', models.CharField(blank=True, max_length=40, null=True)),
|
||||||
|
('total', models.FloatField(default=0)),
|
||||||
|
('status', models.CharField(choices=[('paid', 'Pagado'), ('cancelled', 'Cancelado')], default='paid', max_length=20)),
|
||||||
|
('payment_method', models.CharField(choices=[('stripe', 'Stripe'), ('paypal', 'PayPal'), ('manual', 'Manual')], default='manual', max_length=20)),
|
||||||
|
('payment_reference', models.CharField(blank=True, default='', max_length=200)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('buyer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OrderItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('product_name', models.CharField(default='', max_length=200)),
|
||||||
|
('quantity', models.PositiveIntegerField(default=1)),
|
||||||
|
('unit_price', models.FloatField(default=0)),
|
||||||
|
('total_price', models.FloatField(default=0)),
|
||||||
|
('status', models.CharField(choices=[('pending', 'Pendiente'), ('processing', 'En preparación'), ('shipped', 'Enviado')], default='pending', max_length=20)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='tienda.order')),
|
||||||
|
('seller', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_items_to_fulfill', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OrderMessage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('message', models.TextField()),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('order_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='tienda.orderitem')),
|
||||||
|
('sender', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Product',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(default='', max_length=200)),
|
||||||
|
('description', models.TextField(default='')),
|
||||||
|
('briefdesc', models.TextField(default='')),
|
||||||
|
('price', models.FloatField(default=0)),
|
||||||
|
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tienda.category')),
|
||||||
|
('creator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='created_products', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('primary_image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='tienda.image')),
|
||||||
|
('secondary_images', models.ManyToManyField(blank=True, related_name='products_secondary', to='tienda.image')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderitem',
|
||||||
|
name='product',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tienda.product'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ShippingAddress',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('full_name', models.CharField(max_length=200, verbose_name='Nombre completo')),
|
||||||
|
('address_line_1', models.CharField(max_length=250, verbose_name='Dirección')),
|
||||||
|
('address_line_2', models.CharField(blank=True, max_length=250, verbose_name='Dirección (línea 2)')),
|
||||||
|
('city', models.CharField(max_length=100, verbose_name='Ciudad')),
|
||||||
|
('postal_code', models.CharField(max_length=20, verbose_name='Código postal')),
|
||||||
|
('country', models.CharField(default='España', max_length=100, verbose_name='País')),
|
||||||
|
('phone', models.CharField(max_length=20, verbose_name='Teléfono')),
|
||||||
|
('is_default', models.BooleanField(default=False, verbose_name='Dirección predeterminada')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shipping_addresses', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Dirección de envío',
|
||||||
|
'verbose_name_plural': 'Direcciones de envío',
|
||||||
|
'ordering': ['-is_default', '-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='shipping_address',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='tienda.shippingaddress'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VerificationCode',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('code', models.TextField(default='')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_belongsto', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CartItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField(default=1)),
|
||||||
|
('added_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='tienda.cart')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tienda.product')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('cart', 'product')},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-01-23 09:38
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('tienda', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='category',
|
|
||||||
options={},
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='category',
|
|
||||||
name='parent',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 6.0.1 on 2026-03-10 11:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tienda', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='verificationcode',
|
||||||
|
name='code_mode',
|
||||||
|
field=models.CharField(choices=[('VA', 'Verify Account'), ('RP', 'Reset Password')], default='VA', max_length=2),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='verificationcode',
|
||||||
|
name='code',
|
||||||
|
field=models.TextField(default='', unique=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-01-23 09:40
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('tienda', '0002_alter_category_options_remove_category_parent'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Image',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(default='', max_length=200)),
|
|
||||||
('image', models.ImageField(upload_to='')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-01-23 09:42
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('tienda', '0003_image'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='image',
|
|
||||||
name='image',
|
|
||||||
field=models.ImageField(upload_to='images/'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-01-23 09:48
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('tienda', '0004_alter_image_image'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Product',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(default='', max_length=200)),
|
|
||||||
('description', models.TextField(default='')),
|
|
||||||
('price', models.FloatField(default=0)),
|
|
||||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tienda.category')),
|
|
||||||
('primary_image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='tienda.image')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-01-23 09:49
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('tienda', '0005_product'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='product',
|
|
||||||
name='secondary_images',
|
|
||||||
field=models.ManyToManyField(blank=True, related_name='products_secondary', to='tienda.image'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-02-06 07:52
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('tienda', '0006_product_secondary_images'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='product',
|
|
||||||
name='briefdesc',
|
|
||||||
field=models.TextField(default=''),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-02-06 10:41
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('tienda', '0007_product_briefdesc'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Cart',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('session_key', models.CharField(blank=True, max_length=40, null=True)),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
|
||||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CartItem',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('quantity', models.PositiveIntegerField(default=1)),
|
|
||||||
('added_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='tienda.cart')),
|
|
||||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tienda.product')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('cart', 'product')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-02-06 10:48
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('tienda', '0008_cart_cartitem'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='product',
|
|
||||||
name='creator',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='created_products', to=settings.AUTH_USER_MODEL),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-02-09 09:06
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('tienda', '0009_product_creator'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Order',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('session_key', models.CharField(blank=True, max_length=40, null=True)),
|
|
||||||
('total', models.FloatField(default=0)),
|
|
||||||
('status', models.CharField(choices=[('paid', 'Pagado'), ('cancelled', 'Cancelado')], default='paid', max_length=20)),
|
|
||||||
('payment_method', models.CharField(choices=[('stripe', 'Stripe'), ('paypal', 'PayPal'), ('manual', 'Manual')], default='manual', max_length=20)),
|
|
||||||
('payment_reference', models.CharField(blank=True, default='', max_length=200)),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
|
||||||
('buyer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='OrderItem',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('product_name', models.CharField(default='', max_length=200)),
|
|
||||||
('quantity', models.PositiveIntegerField(default=1)),
|
|
||||||
('unit_price', models.FloatField(default=0)),
|
|
||||||
('total_price', models.FloatField(default=0)),
|
|
||||||
('status', models.CharField(choices=[('pending', 'Pendiente'), ('processing', 'En preparación'), ('shipped', 'Enviado')], default='pending', max_length=20)),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='tienda.order')),
|
|
||||||
('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tienda.product')),
|
|
||||||
('seller', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_items_to_fulfill', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-02-09 09:12
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('tienda', '0010_order_orderitem'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='OrderMessage',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('message', models.TextField()),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('order_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='tienda.orderitem')),
|
|
||||||
('sender', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'ordering': ['created_at'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# Generated by Django 6.0.1 on 2026-02-16 11:57
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('tienda', '0011_ordermessage'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='image',
|
|
||||||
name='alt',
|
|
||||||
field=models.CharField(blank=True, default='', max_length=255, verbose_name='Texto alternativo'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ShippingAddress',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('full_name', models.CharField(max_length=200, verbose_name='Nombre completo')),
|
|
||||||
('address_line_1', models.CharField(max_length=250, verbose_name='Dirección')),
|
|
||||||
('address_line_2', models.CharField(blank=True, max_length=250, verbose_name='Dirección (línea 2)')),
|
|
||||||
('city', models.CharField(max_length=100, verbose_name='Ciudad')),
|
|
||||||
('postal_code', models.CharField(max_length=20, verbose_name='Código postal')),
|
|
||||||
('country', models.CharField(default='España', max_length=100, verbose_name='País')),
|
|
||||||
('phone', models.CharField(max_length=20, verbose_name='Teléfono')),
|
|
||||||
('is_default', models.BooleanField(default=False, verbose_name='Dirección predeterminada')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shipping_addresses', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Dirección de envío',
|
|
||||||
'verbose_name_plural': 'Direcciones de envío',
|
|
||||||
'ordering': ['-is_default', '-created_at'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+27
-1
@@ -1,6 +1,31 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User, AbstractUser
|
||||||
from .vars import VAT_RATE
|
from .vars import VAT_RATE
|
||||||
|
import random, string
|
||||||
|
class User(AbstractUser):
|
||||||
|
class RegisterStatus(models.TextChoices):
|
||||||
|
CONFIRMATION_REQUIRED = "CR", "Confirmation Required"
|
||||||
|
ACTIVE = "AC", "Active"
|
||||||
|
BANNED = "BN", "Banned"
|
||||||
|
|
||||||
|
registration_status = models.CharField(
|
||||||
|
max_length = 2,
|
||||||
|
choices = RegisterStatus.choices,
|
||||||
|
default = RegisterStatus.CONFIRMATION_REQUIRED
|
||||||
|
)
|
||||||
|
|
||||||
|
class VerificationCode(models.Model):
|
||||||
|
class VerificationModes(models.TextChoices):
|
||||||
|
VERIFY_ACCOUNT = "VA"
|
||||||
|
RESET_PASSWORD = "RP"
|
||||||
|
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="user_belongsto", null=False, blank=False)
|
||||||
|
code = models.TextField(default = "", unique=True)
|
||||||
|
code_mode = models.CharField(
|
||||||
|
max_length=2,
|
||||||
|
choices = VerificationModes.choices,
|
||||||
|
default = VerificationModes.VERIFY_ACCOUNT
|
||||||
|
)
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
class Category(models.Model):
|
class Category(models.Model):
|
||||||
@@ -105,6 +130,7 @@ class Order(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
buyer = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='orders')
|
buyer = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='orders')
|
||||||
|
shipping_address = models.ForeignKey('ShippingAddress', on_delete=models.SET_NULL, null=True, blank=True, related_name='orders')
|
||||||
session_key = models.CharField(max_length=40, null=True, blank=True)
|
session_key = models.CharField(max_length=40, null=True, blank=True)
|
||||||
total = models.FloatField(default=0)
|
total = models.FloatField(default=0)
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PAID)
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PAID)
|
||||||
|
|||||||
@@ -45,6 +45,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
console.log("Stripe initialized");
|
console.log("Stripe initialized");
|
||||||
|
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
|
const shippingAddressSelect = document.getElementById("shipping-address");
|
||||||
|
const selectedShippingAddress = shippingAddressSelect ? shippingAddressSelect.value : "";
|
||||||
|
|
||||||
|
if (!selectedShippingAddress) {
|
||||||
|
alert("Selecciona una dirección de envío para continuar.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Checkout button clicked");
|
console.log("Checkout button clicked");
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
button.innerHTML = "Procesando...";
|
button.innerHTML = "Procesando...";
|
||||||
@@ -55,7 +63,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-CSRFToken": getCookie("csrftoken")
|
"X-CSRFToken": getCookie("csrftoken")
|
||||||
},
|
},
|
||||||
body: JSON.stringify({})
|
body: JSON.stringify({
|
||||||
|
shipping_address_id: selectedShippingAddress
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
console.log("Session response status:", res.status);
|
console.log("Session response status:", res.status);
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from celery import shared_task
|
||||||
|
from .utilities import send_email
|
||||||
|
from .vars import login_message
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def enviar_correo_bienvenida(email_usuario, nombre_usuario):
|
||||||
|
send_email(email_usuario, "Inicio de Sesión correcto", login_message.format(name = nombre_usuario))
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load cache %}
|
||||||
{% load compress %}
|
{% load compress %}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
@@ -69,6 +70,7 @@
|
|||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% cache 500 sidebar request.user.username %}
|
||||||
<nav class="navbar navbar-expand-md header">
|
<nav class="navbar navbar-expand-md header">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="{% url 'home' %}">
|
<a class="navbar-brand" href="{% url 'home' %}">
|
||||||
@@ -102,7 +104,7 @@
|
|||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<a href="{% url 'mis_productos' %}" class="nav-link btn btn-outline-secondary btn-sm">Panel Vendedor</a>
|
<a href="{% url 'mis_productos' %}" class="nav-link btn btn-outline-secondary btn-sm">Panel Vendedor</a>
|
||||||
<span class="nav-text d-none d-md-inline text-white">{{ user.first_name|default:user.username }}</span>
|
<a href="{% url 'portal_usuario' %}" class="nav-link btn btn-outline-light btn-sm">{{ user.first_name|default:user.username }}</a>
|
||||||
<a href="{% url 'logout' %}" class="nav-link btn btn-primary btn-sm">Cerrar Sesión</a>
|
<a href="{% url 'logout' %}" class="nav-link btn btn-primary btn-sm">Cerrar Sesión</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'login' %}" class="nav-link btn btn-primary btn-sm">Iniciar Sesión</a>
|
<a href="{% url 'login' %}" class="nav-link btn btn-primary btn-sm">Iniciar Sesión</a>
|
||||||
@@ -112,6 +114,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
{% endcache %}
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Mensajes -->
|
<!-- Mensajes -->
|
||||||
@@ -131,30 +134,31 @@
|
|||||||
<!-- Contenido-->
|
<!-- Contenido-->
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
|
{% cache 500 footer %}
|
||||||
<!-- Footer-->
|
<!-- Footer-->
|
||||||
<div id="footer" class="row pt-2 pb-2 mt-5">
|
<div id="footer" class="row pt-2 pb-2 mt-5">
|
||||||
<div class="col-md-12 grid">
|
<div class="col-md-12 grid">
|
||||||
<p class="text-center">Enlace 1</p>
|
<p class="text-center">Enlace 1</p>
|
||||||
<p class="text-center">Enlace 2</p>
|
<p class="text-center">Enlace 2</p>
|
||||||
<p class="text-center">Enlace 3</p>
|
<p class="text-center">Enlace 3</p>
|
||||||
<p class="text-center">Enlace 4</p>
|
<p class="text-center">Enlace 4</p>
|
||||||
<p class="text-center">Enlace 5</p>
|
<p class="text-center">Enlace 5</p>
|
||||||
<p class="text-center">Enlace 6</p>
|
<p class="text-center">Enlace 6</p>
|
||||||
<p class="text-center">Enlace 7</p>
|
<p class="text-center">Enlace 7</p>
|
||||||
<p class="text-center">Enlace 8</p>
|
<p class="text-center">Enlace 8</p>
|
||||||
<p class="text-center">Enlace 9</p>
|
<p class="text-center">Enlace 9</p>
|
||||||
<p class="text-center">Enlace 10</p>
|
<p class="text-center">Enlace 10</p>
|
||||||
<p class="text-center">Enlace 11</p>
|
<p class="text-center">Enlace 11</p>
|
||||||
<p class="text-center">Enlace 12</p>
|
<p class="text-center">Enlace 12</p>
|
||||||
<p class="text-center">Enlace 13</p>
|
<p class="text-center">Enlace 13</p>
|
||||||
<p class="text-center">Enlace 14</p>
|
<p class="text-center">Enlace 14</p>
|
||||||
<p class="text-center">Enlace 15</p>
|
<p class="text-center">Enlace 15</p>
|
||||||
<p class="text-center">Enlace 16</p>
|
<p class="text-center">Enlace 16</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endcache %}
|
||||||
</div>
|
</div>
|
||||||
|
{% cache 500 scripts %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -243,5 +247,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
{% endcache %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -49,6 +49,30 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if cart_items %}
|
{% if cart_items %}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title mb-3">1) Selecciona la dirección de envío</h5>
|
||||||
|
{% if addresses %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="shipping-address" class="form-label">Dirección</label>
|
||||||
|
<select id="shipping-address" class="form-select" required>
|
||||||
|
<option value="">Selecciona una dirección...</option>
|
||||||
|
{% for address in addresses %}
|
||||||
|
<option value="{{ address.id }}" {% if address.is_default %}selected{% endif %}>
|
||||||
|
{{ address.full_name }} - {{ address.address_line_1 }}, {{ address.postal_code }} {{ address.city }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning mb-0 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
<span>No tienes direcciones de envío creadas.</span>
|
||||||
|
<a href="{% url 'crear_direccion' %}" class="btn btn-primary btn-sm">Crear dirección</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive mb-4">
|
<div class="table-responsive mb-4">
|
||||||
<table class="table table-striped align-middle">
|
<table class="table table-striped align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -87,20 +111,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="payment-section">
|
<div class="payment-section">
|
||||||
<h3>Selecciona tu método de pago</h3>
|
<h3>2) Selecciona tu método de pago</h3>
|
||||||
<div class="payment-methods">
|
<div class="payment-methods">
|
||||||
<button
|
<button
|
||||||
id="checkout-button"
|
id="checkout-button"
|
||||||
class="btn btn-primary payment-btn"
|
class="btn btn-primary payment-btn"
|
||||||
data-config-url="/tienda/config/"
|
data-config-url="/tienda/config/"
|
||||||
data-session-url="/tienda/create-checkout-session/">
|
data-session-url="/tienda/create-checkout-session/"
|
||||||
|
{% if not addresses %}disabled{% endif %}>
|
||||||
💳 Pagar con Stripe
|
💳 Pagar con Stripe
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
id="paypal-button"
|
id="paypal-button"
|
||||||
class="btn btn-warning payment-btn"
|
class="btn btn-warning payment-btn"
|
||||||
data-payment-url="{% url 'create_paypal_payment' %}">
|
data-payment-url="{% url 'create_paypal_payment' %}"
|
||||||
|
{% if not addresses %}disabled{% endif %}>
|
||||||
🅿️ Pagar con PayPal
|
🅿️ Pagar con PayPal
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,6 +142,13 @@
|
|||||||
document.getElementById('paypal-button').addEventListener('click', async function(e) {
|
document.getElementById('paypal-button').addEventListener('click', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
const shippingAddressSelect = document.getElementById('shipping-address');
|
||||||
|
const selectedShippingAddress = shippingAddressSelect ? shippingAddressSelect.value : '';
|
||||||
|
if (!selectedShippingAddress) {
|
||||||
|
alert('Selecciona una dirección de envío para continuar.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const button = this;
|
const button = this;
|
||||||
const originalText = button.innerHTML;
|
const originalText = button.innerHTML;
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
@@ -138,7 +171,8 @@
|
|||||||
headers: {
|
headers: {
|
||||||
'X-CSRFToken': csrfToken || '',
|
'X-CSRFToken': csrfToken || '',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}
|
},
|
||||||
|
body: JSON.stringify({ shipping_address_id: selectedShippingAddress })
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Response status:', response.status);
|
console.log('Response status:', response.status);
|
||||||
|
|||||||
@@ -35,17 +35,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="city" class="form-label">Ciudad *</label>
|
<label for="city" class="form-label">Ciudad/Pueblo (Almería) *</label>
|
||||||
<input type="text" class="form-control" id="city" name="city" value="{{ direccion.city|default:'' }}" required>
|
<input type="text" class="form-control" id="city" name="city" value="{{ direccion.city|default:'' }}" list="almeria-towns" autocomplete="off" required>
|
||||||
|
<datalist id="almeria-towns">
|
||||||
|
{% for town in almeria_municipalities %}
|
||||||
|
<option value="{{ town }}"></option>
|
||||||
|
{% endfor %}
|
||||||
|
</datalist>
|
||||||
|
<div class="form-text">Selecciona o escribe un municipio de la provincia de Almería.</div>
|
||||||
|
<div class="invalid-feedback" id="city-validation-message">
|
||||||
|
El pueblo/ciudad debe pertenecer a la provincia de Almería.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label for="postal_code" class="form-label">Código Postal *</label>
|
<label for="postal_code" class="form-label">Código Postal *</label>
|
||||||
<input type="text" class="form-control" id="postal_code" name="postal_code" value="{{ direccion.postal_code|default:'' }}" required>
|
<input type="text" class="form-control" id="postal_code" name="postal_code" value="{{ direccion.postal_code|default:'' }}" pattern="04[0-9]{3}" maxlength="5" placeholder="04XXX" required>
|
||||||
|
<div class="form-text">Solo aceptamos códigos postales de Almería (04xxx).</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="country" class="form-label">País *</label>
|
<label for="country" class="form-label">País *</label>
|
||||||
<input type="text" class="form-control" id="country" name="country" value="{{ direccion.country|default:'España' }}" required>
|
<input type="text" class="form-control" id="country" name="country" value="España" readonly>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="phone" class="form-label">Teléfono *</label>
|
<label for="phone" class="form-label">Teléfono *</label>
|
||||||
@@ -67,4 +77,64 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const cityInput = document.getElementById('city');
|
||||||
|
const cityValidationMessage = document.getElementById('city-validation-message');
|
||||||
|
const form = cityInput ? cityInput.form : null;
|
||||||
|
|
||||||
|
if (!cityInput || !form) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const almeriaTowns = new Set([
|
||||||
|
{% for town in almeria_municipalities %}
|
||||||
|
"{{ town|escapejs }}",
|
||||||
|
{% endfor %}
|
||||||
|
].map(normalizeTown));
|
||||||
|
|
||||||
|
function normalizeTown(value) {
|
||||||
|
return (value || '')
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.replace(/[^a-zA-Z0-9\s-]/g, '')
|
||||||
|
.replace(/-/g, ' ')
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.replace(/^(la|los)\s+/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateTown() {
|
||||||
|
const normalized = normalizeTown(cityInput.value);
|
||||||
|
|
||||||
|
if (!normalized) {
|
||||||
|
cityInput.setCustomValidity('');
|
||||||
|
cityInput.classList.remove('is-invalid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = almeriaTowns.has(normalized);
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
cityInput.setCustomValidity('');
|
||||||
|
cityInput.classList.remove('is-invalid');
|
||||||
|
} else {
|
||||||
|
cityInput.setCustomValidity('El pueblo/ciudad debe pertenecer a la provincia de Almería.');
|
||||||
|
cityInput.classList.add('is-invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
cityValidationMessage.textContent = cityInput.validationMessage || 'El pueblo/ciudad debe pertenecer a la provincia de Almería.';
|
||||||
|
}
|
||||||
|
|
||||||
|
cityInput.addEventListener('input', validateTown);
|
||||||
|
cityInput.addEventListener('blur', validateTown);
|
||||||
|
form.addEventListener('submit', function () {
|
||||||
|
validateTown();
|
||||||
|
});
|
||||||
|
|
||||||
|
validateTown();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
{% extends "tienda/base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2>Mis Compras</h2>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'portal_usuario' %}">Portal de Usuario</a></li>
|
||||||
|
<li class="breadcrumb-item active">Compras</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'portal_usuario' %}" class="btn btn-outline-primary">Inicio</a>
|
||||||
|
<a href="{% url 'mis_compras' %}" class="btn btn-primary">Compras</a>
|
||||||
|
<a href="{% url 'mis_recibos' %}" class="btn btn-outline-primary">Recibos</a>
|
||||||
|
<a href="{% url 'mensajes_comprador' %}" class="btn btn-outline-primary">Mensajes</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<p class="text-muted">Total de compras: <strong>{{ total_orders }}</strong></p>
|
||||||
|
{% if orders %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Pedido #</th>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th>Total</th>
|
||||||
|
<th>Estado</th>
|
||||||
|
<th>Método</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for order in orders %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ order.id }}</td>
|
||||||
|
<td>{{ order.created_at|date:"d/m/Y H:i" }}</td>
|
||||||
|
<td>{{ order.total }}€</td>
|
||||||
|
<td><span class="badge bg-success">{{ order.get_status_display }}</span></td>
|
||||||
|
<td>{{ order.get_payment_method_display }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info mb-0">
|
||||||
|
Aún no has realizado compras.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
{% extends "tienda/base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2>Mis Recibos</h2>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'portal_usuario' %}">Portal de Usuario</a></li>
|
||||||
|
<li class="breadcrumb-item active">Recibos</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'portal_usuario' %}" class="btn btn-outline-primary">Inicio</a>
|
||||||
|
<a href="{% url 'mis_compras' %}" class="btn btn-outline-primary">Compras</a>
|
||||||
|
<a href="{% url 'mis_recibos' %}" class="btn btn-primary">Recibos</a>
|
||||||
|
<a href="{% url 'mensajes_comprador' %}" class="btn btn-outline-primary">Mensajes</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<p class="text-muted">Total de recibos: <strong>{{ total_receipts }}</strong></p>
|
||||||
|
{% if receipts %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Recibo #</th>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th>Total</th>
|
||||||
|
<th>Método</th>
|
||||||
|
<th>Referencia</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for receipt in receipts %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ receipt.id }}</td>
|
||||||
|
<td>{{ receipt.created_at|date:"d/m/Y H:i" }}</td>
|
||||||
|
<td>{{ receipt.total }}€</td>
|
||||||
|
<td>{{ receipt.get_payment_method_display }}</td>
|
||||||
|
<td>{{ receipt.payment_reference|default:"-" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info mb-0">
|
||||||
|
No tienes recibos disponibles todavía.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -43,6 +43,22 @@
|
|||||||
<li><strong>Precio total:</strong> {{ item.total_price }}€</li>
|
<li><strong>Precio total:</strong> {{ item.total_price }}€</li>
|
||||||
<li><strong>Fecha:</strong> {{ item.created_at|date:"d/m/Y H:i" }}</li>
|
<li><strong>Fecha:</strong> {{ item.created_at|date:"d/m/Y H:i" }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<h6 class="mt-3">Dirección de envío</h6>
|
||||||
|
{% if item.order.shipping_address %}
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><strong>Destinatario:</strong> {{ item.order.shipping_address.full_name }}</li>
|
||||||
|
<li><strong>Dirección:</strong> {{ item.order.shipping_address.address_line_1 }}</li>
|
||||||
|
{% if item.order.shipping_address.address_line_2 %}
|
||||||
|
<li><strong>Detalle:</strong> {{ item.order.shipping_address.address_line_2 }}</li>
|
||||||
|
{% endif %}
|
||||||
|
<li><strong>Ciudad:</strong> {{ item.order.shipping_address.city }}</li>
|
||||||
|
<li><strong>Código Postal:</strong> {{ item.order.shipping_address.postal_code }}</li>
|
||||||
|
<li><strong>País:</strong> {{ item.order.shipping_address.country }}</li>
|
||||||
|
<li><strong>Teléfono:</strong> {{ item.order.shipping_address.phone }}</li>
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted mb-0">Dirección no disponible.</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h6>Cambiar Estado</h6>
|
<h6>Cambiar Estado</h6>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<a href="{% url 'portal_usuario' %}" class="btn btn-primary">Inicio</a>
|
<a href="{% url 'portal_usuario' %}" class="btn btn-primary">Inicio</a>
|
||||||
|
<a href="{% url 'mis_compras' %}" class="btn btn-outline-primary">Compras</a>
|
||||||
|
<a href="{% url 'mis_recibos' %}" class="btn btn-outline-primary">Recibos</a>
|
||||||
<a href="{% url 'editar_perfil' %}" class="btn btn-outline-primary">Mi Perfil</a>
|
<a href="{% url 'editar_perfil' %}" class="btn btn-outline-primary">Mi Perfil</a>
|
||||||
<a href="{% url 'direcciones_usuario' %}" class="btn btn-outline-primary">Direcciones</a>
|
<a href="{% url 'direcciones_usuario' %}" class="btn btn-outline-primary">Direcciones</a>
|
||||||
<a href="{% url 'mensajes_comprador' %}" class="btn btn-outline-primary">Mensajes</a>
|
<a href="{% url 'mensajes_comprador' %}" class="btn btn-outline-primary">Mensajes</a>
|
||||||
@@ -29,6 +31,7 @@
|
|||||||
<h5 class="card-title">📦 Mis Pedidos</h5>
|
<h5 class="card-title">📦 Mis Pedidos</h5>
|
||||||
<p class="display-4">{{ total_orders }}</p>
|
<p class="display-4">{{ total_orders }}</p>
|
||||||
<p class="text-muted">pedidos realizados</p>
|
<p class="text-muted">pedidos realizados</p>
|
||||||
|
<a href="{% url 'mis_compras' %}" class="btn btn-sm btn-primary">Ver compras</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,6 +57,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h5 class="card-title">🧾 Recibos</h5>
|
||||||
|
<p class="text-muted">consulta tus recibos de pago</p>
|
||||||
|
<a href="{% url 'mis_recibos' %}" class="btn btn-sm btn-primary">Ver recibos</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Pedidos recientes -->
|
<!-- Pedidos recientes -->
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ urlpatterns = [
|
|||||||
path("paypal/execute/", views.paypal_execute, name="paypal_execute"),
|
path("paypal/execute/", views.paypal_execute, name="paypal_execute"),
|
||||||
# Portal de usuario
|
# Portal de usuario
|
||||||
path("usuario/", views.portal_usuario, name="portal_usuario"),
|
path("usuario/", views.portal_usuario, name="portal_usuario"),
|
||||||
|
path("usuario/compras/", views.mis_compras, name="mis_compras"),
|
||||||
|
path("usuario/recibos/", views.mis_recibos, name="mis_recibos"),
|
||||||
path("usuario/perfil/", views.editar_perfil, name="editar_perfil"),
|
path("usuario/perfil/", views.editar_perfil, name="editar_perfil"),
|
||||||
path("usuario/perfil/cambiar-contrasena/", views.cambiar_contrasena, name="cambiar_contrasena"),
|
path("usuario/perfil/cambiar-contrasena/", views.cambiar_contrasena, name="cambiar_contrasena"),
|
||||||
path("usuario/direcciones/", views.direcciones_usuario, name="direcciones_usuario"),
|
path("usuario/direcciones/", views.direcciones_usuario, name="direcciones_usuario"),
|
||||||
@@ -42,4 +44,5 @@ urlpatterns = [
|
|||||||
path("usuario/direcciones/<int:id>/editar/", views.editar_direccion, name="editar_direccion"),
|
path("usuario/direcciones/<int:id>/editar/", views.editar_direccion, name="editar_direccion"),
|
||||||
path("usuario/direcciones/<int:id>/eliminar/", views.eliminar_direccion, name="eliminar_direccion"),
|
path("usuario/direcciones/<int:id>/eliminar/", views.eliminar_direccion, name="eliminar_direccion"),
|
||||||
path("usuario/mensajes/", views.mensajes_comprador, name="mensajes_comprador"),
|
path("usuario/mensajes/", views.mensajes_comprador, name="mensajes_comprador"),
|
||||||
|
path("verify/<str:code>", views.verify, name="verify")
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from django.core.mail import send_mail
|
||||||
|
import logging
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("email.system")
|
||||||
|
#
|
||||||
|
#def send_email(dest: str, title: str, body: str):
|
||||||
|
# context = ssl.create_default_context()
|
||||||
|
# try:
|
||||||
|
# with smtplib.SMTP(settings.SMTP_ENDPOINT, settings.SMTP_PORT) as server:
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# server.ehlo()
|
||||||
|
# server.starttls(context=context)
|
||||||
|
# server.ehlo()
|
||||||
|
# server.login(settings.SMTP_USERNAME, settings.SMTP_PASSWORD)
|
||||||
|
#
|
||||||
|
# message = """\
|
||||||
|
#Subject: {}
|
||||||
|
#{}
|
||||||
|
# """.format(title, body)
|
||||||
|
# server.sendmail(settings.SMTP_EMAIL, dest, message)
|
||||||
|
# logger.info("EMAIL_SENT to=%s subject=%s", dest, title)
|
||||||
|
#
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.exception("EMAIL_SEND_FAILED to=%s subject=%s error=%s", dest, title, str(e))
|
||||||
|
# return (False, e)
|
||||||
|
#
|
||||||
|
# return (True,)
|
||||||
|
|
||||||
|
def send_email(dest: str, title: str, body: str):
|
||||||
|
try:
|
||||||
|
send_mail(
|
||||||
|
subject=title,
|
||||||
|
message=body,
|
||||||
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||||
|
recipient_list=[dest],
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("EMAIL_SENT to=%s subject=%s", dest, title)
|
||||||
|
return (True,)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("EMAIL_SEND_FAILED to=%s subject=%s error=%s", dest, title, str(e))
|
||||||
|
return (False, e)
|
||||||
@@ -1,2 +1,42 @@
|
|||||||
PAGE_SIZE = 20
|
PAGE_SIZE = 20
|
||||||
VAT_RATE = 0.21 # IVA 21%
|
VAT_RATE = 0.21 # IVA 21%
|
||||||
|
|
||||||
|
# Restricciones de envío
|
||||||
|
SHIPPING_COUNTRY = "España"
|
||||||
|
ALMERIA_POSTAL_CODE_PREFIX = "04"
|
||||||
|
ALMERIA_MUNICIPALITIES_DISPLAY = (
|
||||||
|
"Abla", "Abrucena", "Adra", "Albánchez", "Alboloduy", "Albox", "Alcolea", "Alcóntar",
|
||||||
|
"Alcudia de Monteagud", "Alhabia", "Alhama de Almería", "Alicún", "Almería", "Almócita",
|
||||||
|
"Alsodux", "Antas", "Arboleas", "Armuña de Almanzora", "Bacares", "Bayárcal", "Bayarque",
|
||||||
|
"Bédar", "Beires", "Benahadux", "Benitagla", "Benizalón", "Bentarique", "Berja", "Canjáyar",
|
||||||
|
"Cantoria", "Carboneras", "Castro de Filabres", "Chercos", "Chirivel", "Cóbdar",
|
||||||
|
"Cuevas del Almanzora", "Dalías", "Enix", "Félix", "Fines", "Fiñana", "Fondón", "Gádor",
|
||||||
|
"Gallardos", "Los Gallardos", "Garrucha", "Gérgal", "Huécija", "Huércal de Almería",
|
||||||
|
"Huércal-Overa", "Íllar", "Instinción", "Laroya", "Laujar de Andarax", "Líjar", "Lubrín",
|
||||||
|
"Lucainena de las Torres", "Lúcar", "Macael", "María", "Mojácar", "Mojonera", "La Mojonera",
|
||||||
|
"Nacimiento", "Níjar", "Ohanes", "Olula de Castro", "Olula del Río", "Oria", "Padules",
|
||||||
|
"Partaloa", "Paterna del Río", "Pechina", "Pulpí", "Purchena", "Rágol", "Rioja",
|
||||||
|
"Roquetas de Mar", "Santa Cruz de Marchena", "Santa Fe de Mondújar", "Senés", "Serón", "Sierro",
|
||||||
|
"Somontín", "Sorbas", "Suflí", "Tabernas", "Taberno", "Tahal", "Terque", "Tíjola", "Turre",
|
||||||
|
"Turrillas", "Uleila del Campo", "Urrácal", "Velefique", "Vélez-Blanco", "Vélez-Rubio", "Vera",
|
||||||
|
"Viator", "Vícar", "Zurgena"
|
||||||
|
)
|
||||||
|
|
||||||
|
verify_message = """
|
||||||
|
¡Buenas {name}!
|
||||||
|
|
||||||
|
Muchas gracias por registrarte en Comercialmeria, para verificar que el correo que ha empleado es el suyo, por favor, haga click en el siguiente enlace.
|
||||||
|
|
||||||
|
Si por alguna razón no es su correo, eliminelo inmediatamente y no le de click al enlace.
|
||||||
|
|
||||||
|
{protocol}://{domain}/tienda/verify/{code}
|
||||||
|
|
||||||
|
Este email ha sido automatizado
|
||||||
|
"""
|
||||||
|
|
||||||
|
login_message = """
|
||||||
|
¡Buenas {name}!
|
||||||
|
|
||||||
|
Le enviamos este correo para indicarle que se acaba de iniciar sesión en un nuevo dispositivo.
|
||||||
|
En caso de que no sea usted, para proteger compras indebidas, ¡cambie la contraseña inmediatamente!
|
||||||
|
"""
|
||||||
+310
-35
@@ -1,11 +1,21 @@
|
|||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.http import HttpRequest, JsonResponse
|
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||||
from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout
|
from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from .models import Product, Category, Cart, CartItem, Image, Order, OrderItem, OrderMessage, ShippingAddress
|
from .models import User, Product, Category, Cart, CartItem, Image, Order, OrderItem, OrderMessage, ShippingAddress, VerificationCode
|
||||||
from .vars import PAGE_SIZE
|
from .utilities import send_email
|
||||||
|
from . import tasks
|
||||||
|
from .vars import (
|
||||||
|
PAGE_SIZE,
|
||||||
|
VAT_RATE,
|
||||||
|
SHIPPING_COUNTRY,
|
||||||
|
ALMERIA_POSTAL_CODE_PREFIX,
|
||||||
|
ALMERIA_MUNICIPALITIES_DISPLAY,
|
||||||
|
verify_message,
|
||||||
|
login_message
|
||||||
|
)
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -13,8 +23,78 @@ from decimal import Decimal, ROUND_HALF_UP
|
|||||||
import stripe
|
import stripe
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
|
import json
|
||||||
|
import random, string
|
||||||
|
import logging
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("tienda")
|
||||||
|
audit_logger = logging.getLogger("tienda.audit")
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_location_text(value: str) -> str:
|
||||||
|
normalized = unicodedata.normalize("NFD", (value or ""))
|
||||||
|
without_accents = "".join(char for char in normalized if unicodedata.category(char) != "Mn")
|
||||||
|
without_symbols = re.sub(r"[^a-zA-Z0-9\s-]", "", without_accents)
|
||||||
|
collapsed = " ".join(without_symbols.replace("-", " ").lower().split())
|
||||||
|
return collapsed
|
||||||
|
|
||||||
|
|
||||||
|
ALMERIA_MUNICIPALITIES = {
|
||||||
|
_normalize_location_text(municipality)
|
||||||
|
for municipality in ALMERIA_MUNICIPALITIES_DISPLAY
|
||||||
|
}
|
||||||
|
ALMERIA_MUNICIPALITIES.update(
|
||||||
|
{
|
||||||
|
municipality.removeprefix("la ")
|
||||||
|
for municipality in ALMERIA_MUNICIPALITIES
|
||||||
|
if municipality.startswith("la ")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ALMERIA_MUNICIPALITIES.update(
|
||||||
|
{
|
||||||
|
municipality.removeprefix("los ")
|
||||||
|
for municipality in ALMERIA_MUNICIPALITIES
|
||||||
|
if municipality.startswith("los ")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_almeria_postal_code(postal_code: str) -> bool:
|
||||||
|
"""Valida que el código postal pertenezca a la provincia de Almería (04xxx)."""
|
||||||
|
normalized = (postal_code or "").strip()
|
||||||
|
return len(normalized) == 5 and normalized.isdigit() and normalized.startswith(ALMERIA_POSTAL_CODE_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_almeria_city(city: str) -> bool:
|
||||||
|
"""Valida que el municipio/pueblo pertenezca a la provincia de Almería."""
|
||||||
|
return _normalize_location_text(city) in ALMERIA_MUNICIPALITIES
|
||||||
|
|
||||||
|
|
||||||
|
def _address_form_context(direccion=None):
|
||||||
|
return {
|
||||||
|
"direccion": direccion,
|
||||||
|
"almeria_municipalities": ALMERIA_MUNICIPALITIES_DISPLAY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client_ip(request: HttpRequest) -> str:
|
||||||
|
forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
|
||||||
|
if forwarded_for:
|
||||||
|
return forwarded_for.split(",")[0].strip()
|
||||||
|
return request.META.get("REMOTE_ADDR", "")
|
||||||
|
|
||||||
|
|
||||||
|
def get_price_with_vat_decimal(price) -> Decimal:
|
||||||
|
"""Retorna un precio con IVA aplicado y redondeado a 2 decimales."""
|
||||||
|
return (Decimal(str(price)) * (Decimal("1") + Decimal(str(VAT_RATE)))).quantize(
|
||||||
|
Decimal("0.01"),
|
||||||
|
rounding=ROUND_HALF_UP,
|
||||||
|
)
|
||||||
|
|
||||||
def home(request: HttpRequest):
|
def home(request: HttpRequest):
|
||||||
"""Página de inicio del sitio"""
|
"""Página de inicio del sitio"""
|
||||||
categorias = Category.objects.all()
|
categorias = Category.objects.all()
|
||||||
@@ -50,32 +130,57 @@ def login(request: HttpRequest):
|
|||||||
email = request.POST.get("email")
|
email = request.POST.get("email")
|
||||||
password = request.POST.get("password")
|
password = request.POST.get("password")
|
||||||
remember = request.POST.get("remember")
|
remember = request.POST.get("remember")
|
||||||
|
client_ip = _get_client_ip(request)
|
||||||
|
|
||||||
# Buscar usuario por email
|
# Buscar usuario por email
|
||||||
try:
|
try:
|
||||||
user_obj = User.objects.get(email=email)
|
user_obj = User.objects.get(email=email)
|
||||||
username = user_obj.username
|
username = user_obj.username
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
|
audit_logger.warning(
|
||||||
|
"LOGIN_FAILED email=%s reason=user_not_found ip=%s",
|
||||||
|
email,
|
||||||
|
client_ip,
|
||||||
|
)
|
||||||
messages.error(request, "Correo electrónico o contraseña incorrectos.")
|
messages.error(request, "Correo electrónico o contraseña incorrectos.")
|
||||||
return render(request, "tienda/login.html")
|
return render(request, "tienda/login.html")
|
||||||
|
|
||||||
# Autenticar usuario
|
# Autenticar usuario
|
||||||
user = authenticate(request, username=username, password=password)
|
user = authenticate(request, username=username, password=password)
|
||||||
|
user = User.objects.get(username=user.username)
|
||||||
|
if user.registration_status == "CR":
|
||||||
|
audit_logger.info(
|
||||||
|
"LOGIN_FAILED email=%s reason=not_verified", email
|
||||||
|
)
|
||||||
|
messages.error(request, "No se puede iniciar sesión porque no has verificado tu cuenta, comprueba tu email. Si eliminaste el email pero querias verificarte, contacta con el soporte tecnico")
|
||||||
|
return render(request, "tienda/login.html")
|
||||||
|
|
||||||
if user is not None:
|
if user is not None:
|
||||||
auth_login(request, user)
|
auth_login(request, user)
|
||||||
|
|
||||||
# Configurar duración de sesión
|
# Configurar duración de sesión
|
||||||
if not remember:
|
if not remember:
|
||||||
# Si no marca "Recordarme", la sesión expira al cerrar el navegador
|
|
||||||
request.session.set_expiry(0)
|
request.session.set_expiry(0)
|
||||||
else:
|
else:
|
||||||
# Si marca "Recordarme", la sesión dura 2 semanas
|
|
||||||
request.session.set_expiry(1209600) # 14 días en segundos
|
request.session.set_expiry(1209600) # 14 días en segundos
|
||||||
|
|
||||||
|
audit_logger.info(
|
||||||
|
"LOGIN_SUCCESS user_id=%s email=%s ip=%s remember=%s",
|
||||||
|
user.id,
|
||||||
|
user.email,
|
||||||
|
client_ip,
|
||||||
|
bool(remember),
|
||||||
|
)
|
||||||
|
tasks.enviar_correo_bienvenida.delay(user.email, "{} {}".format(user.first_name, user.last_name))
|
||||||
|
# result = send_email(user.email, "Inicio de sesión correcto", login_message.format(name = "{} {}".format(user.first_name, user.last_name)))
|
||||||
messages.success(request, f"¡Bienvenido {user.first_name or user.username}!")
|
messages.success(request, f"¡Bienvenido {user.first_name or user.username}!")
|
||||||
return redirect("index")
|
return redirect("index")
|
||||||
else:
|
else:
|
||||||
|
audit_logger.warning(
|
||||||
|
"LOGIN_FAILED email=%s reason=invalid_credentials ip=%s",
|
||||||
|
email,
|
||||||
|
client_ip,
|
||||||
|
)
|
||||||
messages.error(request, "Correo electrónico o contraseña incorrectos.")
|
messages.error(request, "Correo electrónico o contraseña incorrectos.")
|
||||||
return render(request, "tienda/login.html")
|
return render(request, "tienda/login.html")
|
||||||
|
|
||||||
@@ -83,22 +188,28 @@ def login(request: HttpRequest):
|
|||||||
|
|
||||||
|
|
||||||
def register(request: HttpRequest):
|
def register(request: HttpRequest):
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return redirect("index")
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
name = request.POST.get("name")
|
name = request.POST.get("name")
|
||||||
email = request.POST.get("email")
|
email = request.POST.get("email")
|
||||||
password = request.POST.get("password")
|
password = request.POST.get("password")
|
||||||
password_confirm = request.POST.get("password_confirm")
|
password_confirm = request.POST.get("password_confirm")
|
||||||
|
client_ip = _get_client_ip(request)
|
||||||
|
|
||||||
# Validaciones
|
# Validaciones
|
||||||
if password != password_confirm:
|
if password != password_confirm:
|
||||||
|
audit_logger.warning("REGISTER_FAILED email=%s reason=password_mismatch ip=%s", email, client_ip)
|
||||||
messages.error(request, "Las contraseñas no coinciden.")
|
messages.error(request, "Las contraseñas no coinciden.")
|
||||||
return render(request, "tienda/register.html")
|
return render(request, "tienda/register.html")
|
||||||
|
|
||||||
if len(password) < 8:
|
if len(password) < 8:
|
||||||
|
audit_logger.warning("REGISTER_FAILED email=%s reason=password_too_short ip=%s", email, client_ip)
|
||||||
messages.error(request, "La contraseña debe tener al menos 8 caracteres.")
|
messages.error(request, "La contraseña debe tener al menos 8 caracteres.")
|
||||||
return render(request, "tienda/register.html")
|
return render(request, "tienda/register.html")
|
||||||
|
|
||||||
if User.objects.filter(email=email).exists():
|
if User.objects.filter(email=email).exists():
|
||||||
|
audit_logger.warning("REGISTER_FAILED email=%s reason=email_exists ip=%s", email, client_ip)
|
||||||
messages.error(request, "Ya existe un usuario con este correo electrónico.")
|
messages.error(request, "Ya existe un usuario con este correo electrónico.")
|
||||||
return render(request, "tienda/register.html")
|
return render(request, "tienda/register.html")
|
||||||
|
|
||||||
@@ -120,18 +231,36 @@ def register(request: HttpRequest):
|
|||||||
first_name=name
|
first_name=name
|
||||||
)
|
)
|
||||||
|
|
||||||
# Iniciar sesión automáticamente
|
audit_logger.info(
|
||||||
auth_login(request, user)
|
"REGISTER_SUCCESS user_id=%s username=%s email=%s ip=%s",
|
||||||
request.session.set_expiry(1209600) # 14 días
|
user.id,
|
||||||
|
user.username,
|
||||||
|
user.email,
|
||||||
|
client_ip,
|
||||||
|
)
|
||||||
|
|
||||||
messages.success(request, f"¡Cuenta creada exitosamente! Bienvenido {name}.")
|
ver_code = ''.join(random.choices(string.digits, k=12))
|
||||||
|
|
||||||
|
codigo = VerificationCode.objects.create(
|
||||||
|
user = user,
|
||||||
|
code = ver_code,
|
||||||
|
code_mode = VerificationCode.VerificationModes.VERIFY_ACCOUNT
|
||||||
|
)
|
||||||
|
message = verify_message.format(name = name, protocol = settings.PROTOCOL, domain = settings.DOMAIN, code = ver_code)
|
||||||
|
email_result = send_email(email, "Verificación de cuenta", message)
|
||||||
|
|
||||||
|
messages.success(request, f"¡Cuenta creada exitosamente! Por favor, verifica tu correo entrando al Link enviado.")
|
||||||
return redirect("index")
|
return redirect("index")
|
||||||
|
|
||||||
return render(request, "tienda/register.html")
|
return render(request, "tienda/register.html")
|
||||||
|
|
||||||
|
|
||||||
def logout(request: HttpRequest):
|
def logout(request: HttpRequest):
|
||||||
|
user_id = request.user.id if request.user.is_authenticated else None
|
||||||
|
email = request.user.email if request.user.is_authenticated else None
|
||||||
|
client_ip = _get_client_ip(request)
|
||||||
auth_logout(request)
|
auth_logout(request)
|
||||||
|
audit_logger.info("LOGOUT user_id=%s email=%s ip=%s", user_id, email, client_ip)
|
||||||
messages.success(request, "Has cerrado sesión exitosamente.")
|
messages.success(request, "Has cerrado sesión exitosamente.")
|
||||||
return redirect("index")
|
return redirect("index")
|
||||||
|
|
||||||
@@ -178,25 +307,61 @@ def get_or_create_cart(request):
|
|||||||
return cart
|
return cart
|
||||||
|
|
||||||
|
|
||||||
def create_order_from_cart(request, payment_method, payment_reference=""):
|
def _get_selected_shipping_address(request: HttpRequest):
|
||||||
|
"""Obtiene la dirección seleccionada desde JSON o form-data y valida pertenencia al usuario."""
|
||||||
|
shipping_address_id = request.POST.get("shipping_address_id")
|
||||||
|
|
||||||
|
if not shipping_address_id:
|
||||||
|
try:
|
||||||
|
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||||
|
shipping_address_id = payload.get("shipping_address_id")
|
||||||
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
|
shipping_address_id = None
|
||||||
|
|
||||||
|
if not shipping_address_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
shipping_address_id = int(shipping_address_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return ShippingAddress.objects.filter(id=shipping_address_id, user=request.user).first()
|
||||||
|
|
||||||
|
|
||||||
|
def create_order_from_cart(request, payment_method, payment_reference="", shipping_address=None):
|
||||||
"""Crea un pedido a partir del carrito actual y lo asigna a vendedores."""
|
"""Crea un pedido a partir del carrito actual y lo asigna a vendedores."""
|
||||||
cart = get_or_create_cart(request)
|
cart = get_or_create_cart(request)
|
||||||
cart_items = cart.items.select_related("product", "product__creator")
|
cart_items = list(cart.items.select_related("product", "product__creator"))
|
||||||
|
|
||||||
if not cart_items.exists():
|
if not cart_items:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
order_total = Decimal("0.00")
|
||||||
|
items_with_totals = []
|
||||||
|
|
||||||
|
for item in cart_items:
|
||||||
|
product = item.product
|
||||||
|
unit_price_with_vat = get_price_with_vat_decimal(product.price)
|
||||||
|
line_total_with_vat = (unit_price_with_vat * item.quantity).quantize(
|
||||||
|
Decimal("0.01"),
|
||||||
|
rounding=ROUND_HALF_UP,
|
||||||
|
)
|
||||||
|
order_total += line_total_with_vat
|
||||||
|
items_with_totals.append((item, unit_price_with_vat, line_total_with_vat))
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
order = Order.objects.create(
|
order = Order.objects.create(
|
||||||
buyer=request.user if request.user.is_authenticated else None,
|
buyer=request.user if request.user.is_authenticated else None,
|
||||||
|
shipping_address=shipping_address,
|
||||||
session_key=None if request.user.is_authenticated else request.session.session_key,
|
session_key=None if request.user.is_authenticated else request.session.session_key,
|
||||||
total=cart.get_total(),
|
total=float(order_total),
|
||||||
status=Order.STATUS_PAID,
|
status=Order.STATUS_PAID,
|
||||||
payment_method=payment_method,
|
payment_method=payment_method,
|
||||||
payment_reference=payment_reference or "",
|
payment_reference=payment_reference or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
for item in cart_items:
|
for item, unit_price_with_vat, line_total_with_vat in items_with_totals:
|
||||||
product = item.product
|
product = item.product
|
||||||
OrderItem.objects.create(
|
OrderItem.objects.create(
|
||||||
order=order,
|
order=order,
|
||||||
@@ -204,8 +369,8 @@ def create_order_from_cart(request, payment_method, payment_reference=""):
|
|||||||
product_name=product.name,
|
product_name=product.name,
|
||||||
seller=product.creator,
|
seller=product.creator,
|
||||||
quantity=item.quantity,
|
quantity=item.quantity,
|
||||||
unit_price=product.price,
|
unit_price=float(unit_price_with_vat),
|
||||||
total_price=product.price * item.quantity,
|
total_price=float(line_total_with_vat),
|
||||||
)
|
)
|
||||||
|
|
||||||
cart.items.all().delete()
|
cart.items.all().delete()
|
||||||
@@ -322,7 +487,7 @@ def mis_productos(request: HttpRequest):
|
|||||||
def pedidos_vendedor(request: HttpRequest):
|
def pedidos_vendedor(request: HttpRequest):
|
||||||
"""Muestra los pedidos asignados al vendedor autenticado"""
|
"""Muestra los pedidos asignados al vendedor autenticado"""
|
||||||
pedidos = OrderItem.objects.filter(seller=request.user).select_related(
|
pedidos = OrderItem.objects.filter(seller=request.user).select_related(
|
||||||
'order', 'product', 'order__buyer'
|
'order', 'product', 'order__buyer', 'order__shipping_address'
|
||||||
).prefetch_related('messages__sender').order_by('-created_at')
|
).prefetch_related('messages__sender').order_by('-created_at')
|
||||||
|
|
||||||
return render(request, "tienda/pedidos_vendedor.html", {
|
return render(request, "tienda/pedidos_vendedor.html", {
|
||||||
@@ -540,9 +705,11 @@ def borrar_producto(request: HttpRequest, id: int):
|
|||||||
def checkout(request: HttpRequest):
|
def checkout(request: HttpRequest):
|
||||||
cart = get_or_create_cart(request)
|
cart = get_or_create_cart(request)
|
||||||
cart_items = cart.items.select_related("product")
|
cart_items = cart.items.select_related("product")
|
||||||
|
addresses = ShippingAddress.objects.filter(user=request.user)
|
||||||
return render(request, "tienda/checkout.html", {
|
return render(request, "tienda/checkout.html", {
|
||||||
"cart": cart,
|
"cart": cart,
|
||||||
"cart_items": cart_items
|
"cart_items": cart_items,
|
||||||
|
"addresses": addresses,
|
||||||
})
|
})
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@@ -561,6 +728,10 @@ def create_checkout_session(request: HttpRequest):
|
|||||||
return JsonResponse({"error": "Método no permitido"}, status=405)
|
return JsonResponse({"error": "Método no permitido"}, status=405)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
shipping_address = _get_selected_shipping_address(request)
|
||||||
|
if shipping_address is None:
|
||||||
|
return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400)
|
||||||
|
|
||||||
cart = get_or_create_cart(request)
|
cart = get_or_create_cart(request)
|
||||||
cart_items = cart.items.select_related("product")
|
cart_items = cart.items.select_related("product")
|
||||||
|
|
||||||
@@ -571,7 +742,8 @@ def create_checkout_session(request: HttpRequest):
|
|||||||
|
|
||||||
line_items = []
|
line_items = []
|
||||||
for item in cart_items:
|
for item in cart_items:
|
||||||
unit_amount = int((Decimal(str(item.product.price)) * 100).quantize(0, rounding=ROUND_HALF_UP))
|
unit_price_with_vat = get_price_with_vat_decimal(item.product.price)
|
||||||
|
unit_amount = int((unit_price_with_vat * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP))
|
||||||
if unit_amount <= 0:
|
if unit_amount <= 0:
|
||||||
continue
|
continue
|
||||||
line_items.append({
|
line_items.append({
|
||||||
@@ -601,18 +773,23 @@ def create_checkout_session(request: HttpRequest):
|
|||||||
)
|
)
|
||||||
|
|
||||||
request.session['stripe_session_id'] = session.id
|
request.session['stripe_session_id'] = session.id
|
||||||
|
request.session['selected_shipping_address_id'] = shipping_address.id
|
||||||
|
|
||||||
return JsonResponse({"sessionId": session.id})
|
return JsonResponse({"sessionId": session.id})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Stripe error: {str(e)}")
|
logger.exception("STRIPE_CHECKOUT_SESSION_ERROR user_id=%s error=%s", request.user.id, str(e))
|
||||||
return JsonResponse({"error": f"Error al crear sesión de pago: {str(e)}"}, status=500)
|
return JsonResponse({"error": f"Error al crear sesión de pago: {str(e)}"}, status=500)
|
||||||
|
|
||||||
|
|
||||||
def checkout_success(request: HttpRequest):
|
def checkout_success(request: HttpRequest):
|
||||||
payment_reference = request.session.get('stripe_session_id', "")
|
payment_reference = request.session.get('stripe_session_id', "")
|
||||||
create_order_from_cart(request, Order.PAYMENT_STRIPE, payment_reference)
|
shipping_address_id = request.session.get('selected_shipping_address_id')
|
||||||
|
shipping_address = ShippingAddress.objects.filter(id=shipping_address_id, user=request.user).first()
|
||||||
|
create_order_from_cart(request, Order.PAYMENT_STRIPE, payment_reference, shipping_address)
|
||||||
if 'stripe_session_id' in request.session:
|
if 'stripe_session_id' in request.session:
|
||||||
del request.session['stripe_session_id']
|
del request.session['stripe_session_id']
|
||||||
|
if 'selected_shipping_address_id' in request.session:
|
||||||
|
del request.session['selected_shipping_address_id']
|
||||||
messages.success(request, "Pago realizado correctamente. ¡Gracias por tu compra!")
|
messages.success(request, "Pago realizado correctamente. ¡Gracias por tu compra!")
|
||||||
return render(request, "tienda/checkout_success.html", {})
|
return render(request, "tienda/checkout_success.html", {})
|
||||||
|
|
||||||
@@ -652,6 +829,10 @@ def create_paypal_payment(request: HttpRequest):
|
|||||||
return JsonResponse({"error": "Método no permitido"}, status=405)
|
return JsonResponse({"error": "Método no permitido"}, status=405)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
shipping_address = _get_selected_shipping_address(request)
|
||||||
|
if shipping_address is None:
|
||||||
|
return JsonResponse({"error": "Debes seleccionar una dirección de envío válida."}, status=400)
|
||||||
|
|
||||||
import paypalrestsdk
|
import paypalrestsdk
|
||||||
|
|
||||||
cart = get_or_create_cart(request)
|
cart = get_or_create_cart(request)
|
||||||
@@ -669,16 +850,24 @@ def create_paypal_payment(request: HttpRequest):
|
|||||||
|
|
||||||
# Crear lista de items para PayPal
|
# Crear lista de items para PayPal
|
||||||
payment_items = []
|
payment_items = []
|
||||||
|
payment_total = Decimal("0.00")
|
||||||
for item in cart_items:
|
for item in cart_items:
|
||||||
|
unit_price_with_vat = get_price_with_vat_decimal(item.product.price)
|
||||||
|
line_total_with_vat = (unit_price_with_vat * item.quantity).quantize(
|
||||||
|
Decimal("0.01"),
|
||||||
|
rounding=ROUND_HALF_UP,
|
||||||
|
)
|
||||||
|
payment_total += line_total_with_vat
|
||||||
|
|
||||||
payment_items.append({
|
payment_items.append({
|
||||||
"name": item.product.name,
|
"name": item.product.name,
|
||||||
"sku": f"product_{item.product.id}",
|
"sku": f"product_{item.product.id}",
|
||||||
"price": str(round(float(item.product.price), 2)),
|
"price": format(unit_price_with_vat, ".2f"),
|
||||||
"currency": "EUR",
|
"currency": "EUR",
|
||||||
"quantity": item.quantity
|
"quantity": item.quantity
|
||||||
})
|
})
|
||||||
|
|
||||||
total = str(round(float(cart.get_total()), 2))
|
total = format(payment_total, ".2f")
|
||||||
|
|
||||||
# Crear el pago
|
# Crear el pago
|
||||||
payment = paypalrestsdk.Payment({
|
payment = paypalrestsdk.Payment({
|
||||||
@@ -713,6 +902,7 @@ def create_paypal_payment(request: HttpRequest):
|
|||||||
if payment.create():
|
if payment.create():
|
||||||
# Guardar el payment ID en sesión
|
# Guardar el payment ID en sesión
|
||||||
request.session['paypal_payment_id'] = payment.id
|
request.session['paypal_payment_id'] = payment.id
|
||||||
|
request.session['selected_shipping_address_id'] = shipping_address.id
|
||||||
|
|
||||||
# Encontrar el link de aprobación
|
# Encontrar el link de aprobación
|
||||||
for link in payment.links:
|
for link in payment.links:
|
||||||
@@ -723,16 +913,15 @@ def create_paypal_payment(request: HttpRequest):
|
|||||||
else:
|
else:
|
||||||
# Loguear el error
|
# Loguear el error
|
||||||
error_msg = str(payment.error) if hasattr(payment, 'error') else "Error desconocido"
|
error_msg = str(payment.error) if hasattr(payment, 'error') else "Error desconocido"
|
||||||
print(f"PayPal Error: {error_msg}")
|
logger.error("PAYPAL_CREATE_ERROR user_id=%s error=%s", request.user.id, error_msg)
|
||||||
return JsonResponse({"error": f"Error al crear el pago: {error_msg}"}, status=400)
|
return JsonResponse({"error": f"Error al crear el pago: {error_msg}"}, status=400)
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
logger.error("PAYPAL_SDK_NOT_INSTALLED")
|
||||||
return JsonResponse({"error": "SDK de PayPal no instalado"}, status=500)
|
return JsonResponse({"error": "SDK de PayPal no instalado"}, status=500)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
print(f"PayPal Exception: {error_msg}")
|
logger.exception("PAYPAL_CREATE_EXCEPTION user_id=%s error=%s", request.user.id, error_msg)
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return JsonResponse({"error": f"Error: {error_msg}"}, status=500)
|
return JsonResponse({"error": f"Error: {error_msg}"}, status=500)
|
||||||
|
|
||||||
|
|
||||||
@@ -766,11 +955,15 @@ def paypal_execute(request: HttpRequest):
|
|||||||
# Ejecutar el pago
|
# Ejecutar el pago
|
||||||
if payment.execute({"payer_id": payer_id}):
|
if payment.execute({"payer_id": payer_id}):
|
||||||
# Pago exitoso - crear pedido y limpiar el carrito
|
# Pago exitoso - crear pedido y limpiar el carrito
|
||||||
create_order_from_cart(request, Order.PAYMENT_PAYPAL, payment_id)
|
shipping_address_id = request.session.get('selected_shipping_address_id')
|
||||||
|
shipping_address = ShippingAddress.objects.filter(id=shipping_address_id, user=request.user).first()
|
||||||
|
create_order_from_cart(request, Order.PAYMENT_PAYPAL, payment_id, shipping_address)
|
||||||
|
|
||||||
# Limpiar la sesión
|
# Limpiar la sesión
|
||||||
if 'paypal_payment_id' in request.session:
|
if 'paypal_payment_id' in request.session:
|
||||||
del request.session['paypal_payment_id']
|
del request.session['paypal_payment_id']
|
||||||
|
if 'selected_shipping_address_id' in request.session:
|
||||||
|
del request.session['selected_shipping_address_id']
|
||||||
|
|
||||||
messages.success(request, "¡Pago realizado correctamente con PayPal! Gracias por tu compra.")
|
messages.success(request, "¡Pago realizado correctamente con PayPal! Gracias por tu compra.")
|
||||||
return render(request, "tienda/checkout_success.html", {})
|
return render(request, "tienda/checkout_success.html", {})
|
||||||
@@ -780,6 +973,7 @@ def paypal_execute(request: HttpRequest):
|
|||||||
return redirect("checkout")
|
return redirect("checkout")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.exception("PAYPAL_EXECUTE_EXCEPTION user_id=%s error=%s", request.user.id, str(e))
|
||||||
messages.error(request, f"Error: {str(e)}")
|
messages.error(request, f"Error: {str(e)}")
|
||||||
return redirect("checkout")
|
return redirect("checkout")
|
||||||
def search_suggestions(request: HttpRequest):
|
def search_suggestions(request: HttpRequest):
|
||||||
@@ -829,6 +1023,31 @@ def portal_usuario(request: HttpRequest):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def mis_compras(request: HttpRequest):
|
||||||
|
"""Lista completa de compras del usuario autenticado"""
|
||||||
|
orders = Order.objects.filter(buyer=request.user).prefetch_related('items').order_by('-created_at')
|
||||||
|
|
||||||
|
return render(request, "tienda/mis_compras.html", {
|
||||||
|
"orders": orders,
|
||||||
|
"total_orders": orders.count(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def mis_recibos(request: HttpRequest):
|
||||||
|
"""Lista de recibos (pedidos pagados) del usuario autenticado"""
|
||||||
|
receipts = Order.objects.filter(
|
||||||
|
buyer=request.user,
|
||||||
|
status=Order.STATUS_PAID
|
||||||
|
).prefetch_related('items').order_by('-created_at')
|
||||||
|
|
||||||
|
return render(request, "tienda/mis_recibos.html", {
|
||||||
|
"receipts": receipts,
|
||||||
|
"total_receipts": receipts.count(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def editar_perfil(request: HttpRequest):
|
def editar_perfil(request: HttpRequest):
|
||||||
"""Edita la información del perfil del usuario"""
|
"""Edita la información del perfil del usuario"""
|
||||||
@@ -908,14 +1127,22 @@ def crear_direccion(request: HttpRequest):
|
|||||||
address_line_2 = request.POST.get("address_line_2", "").strip()
|
address_line_2 = request.POST.get("address_line_2", "").strip()
|
||||||
city = request.POST.get("city", "").strip()
|
city = request.POST.get("city", "").strip()
|
||||||
postal_code = request.POST.get("postal_code", "").strip()
|
postal_code = request.POST.get("postal_code", "").strip()
|
||||||
country = request.POST.get("country", "España").strip()
|
country = SHIPPING_COUNTRY
|
||||||
phone = request.POST.get("phone", "").strip()
|
phone = request.POST.get("phone", "").strip()
|
||||||
is_default = request.POST.get("is_default") == "on"
|
is_default = request.POST.get("is_default") == "on"
|
||||||
|
|
||||||
# Validaciones
|
# Validaciones
|
||||||
if not all([full_name, address_line_1, city, postal_code, phone]):
|
if not all([full_name, address_line_1, city, postal_code, phone]):
|
||||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
||||||
return render(request, "tienda/editar_direccion.html")
|
return render(request, "tienda/editar_direccion.html", _address_form_context(request.POST))
|
||||||
|
|
||||||
|
if not _is_almeria_city(city):
|
||||||
|
messages.error(request, "El pueblo/ciudad debe pertenecer a la provincia de Almería.")
|
||||||
|
return render(request, "tienda/editar_direccion.html", _address_form_context(request.POST))
|
||||||
|
|
||||||
|
if not _is_almeria_postal_code(postal_code):
|
||||||
|
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
||||||
|
return render(request, "tienda/editar_direccion.html", _address_form_context(request.POST))
|
||||||
|
|
||||||
# Crear dirección
|
# Crear dirección
|
||||||
ShippingAddress.objects.create(
|
ShippingAddress.objects.create(
|
||||||
@@ -933,7 +1160,7 @@ def crear_direccion(request: HttpRequest):
|
|||||||
messages.success(request, "Dirección creada correctamente.")
|
messages.success(request, "Dirección creada correctamente.")
|
||||||
return redirect("direcciones_usuario")
|
return redirect("direcciones_usuario")
|
||||||
|
|
||||||
return render(request, "tienda/editar_direccion.html", {"direccion": None})
|
return render(request, "tienda/editar_direccion.html", _address_form_context())
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -947,7 +1174,7 @@ def editar_direccion(request: HttpRequest, id: int):
|
|||||||
direccion.address_line_2 = request.POST.get("address_line_2", "").strip()
|
direccion.address_line_2 = request.POST.get("address_line_2", "").strip()
|
||||||
direccion.city = request.POST.get("city", "").strip()
|
direccion.city = request.POST.get("city", "").strip()
|
||||||
direccion.postal_code = request.POST.get("postal_code", "").strip()
|
direccion.postal_code = request.POST.get("postal_code", "").strip()
|
||||||
direccion.country = request.POST.get("country", "España").strip()
|
direccion.country = SHIPPING_COUNTRY
|
||||||
direccion.phone = request.POST.get("phone", "").strip()
|
direccion.phone = request.POST.get("phone", "").strip()
|
||||||
direccion.is_default = request.POST.get("is_default") == "on"
|
direccion.is_default = request.POST.get("is_default") == "on"
|
||||||
|
|
||||||
@@ -955,13 +1182,21 @@ def editar_direccion(request: HttpRequest, id: int):
|
|||||||
if not all([direccion.full_name, direccion.address_line_1, direccion.city,
|
if not all([direccion.full_name, direccion.address_line_1, direccion.city,
|
||||||
direccion.postal_code, direccion.phone]):
|
direccion.postal_code, direccion.phone]):
|
||||||
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
messages.error(request, "Por favor completa todos los campos obligatorios.")
|
||||||
return render(request, "tienda/editar_direccion.html", {"direccion": direccion})
|
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion))
|
||||||
|
|
||||||
|
if not _is_almeria_city(direccion.city):
|
||||||
|
messages.error(request, "El pueblo/ciudad debe pertenecer a la provincia de Almería.")
|
||||||
|
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion))
|
||||||
|
|
||||||
|
if not _is_almeria_postal_code(direccion.postal_code):
|
||||||
|
messages.error(request, "Solo realizamos envíos en la provincia de Almería (código postal 04xxx).")
|
||||||
|
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion))
|
||||||
|
|
||||||
direccion.save()
|
direccion.save()
|
||||||
messages.success(request, "Dirección actualizada correctamente.")
|
messages.success(request, "Dirección actualizada correctamente.")
|
||||||
return redirect("direcciones_usuario")
|
return redirect("direcciones_usuario")
|
||||||
|
|
||||||
return render(request, "tienda/editar_direccion.html", {"direccion": direccion})
|
return render(request, "tienda/editar_direccion.html", _address_form_context(direccion))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -990,3 +1225,43 @@ def mensajes_comprador(request: HttpRequest):
|
|||||||
return render(request, "tienda/mensajes_comprador.html", {
|
return render(request, "tienda/mensajes_comprador.html", {
|
||||||
"order_items": order_items
|
"order_items": order_items
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def send_test_email(request: HttpRequest):
|
||||||
|
message = """
|
||||||
|
|
||||||
|
Correo de prueba, deberias recibir esto bien
|
||||||
|
y esto deberia tener un enter
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = send_email("danilacasito8@gmail.com", "Correo de Prueba", message)
|
||||||
|
if result[0]:
|
||||||
|
return HttpResponse("Mira tu bandeja")
|
||||||
|
else:
|
||||||
|
return HttpResponse(result[1])
|
||||||
|
|
||||||
|
|
||||||
|
def verify(request: HttpRequest, code: str):
|
||||||
|
obj = None
|
||||||
|
try:
|
||||||
|
obj = VerificationCode.objects.get(code=code)
|
||||||
|
except VerificationCode.DoesNotExist:
|
||||||
|
return HttpResponse("<h1>Error</h1><p>No existe el codigo de verificación</p>")
|
||||||
|
if obj:
|
||||||
|
if obj.code_mode == VerificationCode.VerificationModes.VERIFY_ACCOUNT:
|
||||||
|
|
||||||
|
obj.user.registration_status = obj.user.RegisterStatus.ACTIVE
|
||||||
|
obj.user.save()
|
||||||
|
obj.delete()
|
||||||
|
return redirect("index")
|
||||||
|
else:
|
||||||
|
return HttpResponse("<h1>Error</h1><p>No existe el codigo de verificación</p>")
|
||||||
|
|
||||||
|
|
||||||
|
def reset_password(request: HttpRequest):
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return redirect("index")
|
||||||
|
|
||||||
|
|
||||||
|
return render(request, "tienda/reset_password", {})
|
||||||
Reference in New Issue
Block a user