diff --git a/proyecto/__init__.py b/proyecto/__init__.py index e69de29..a289a61 100644 --- a/proyecto/__init__.py +++ b/proyecto/__init__.py @@ -0,0 +1,2 @@ +from .celery import app as celery_app +__all__ = ('celery_app',) \ No newline at end of file diff --git a/proyecto/__pycache__/__init__.cpython-314.pyc b/proyecto/__pycache__/__init__.cpython-314.pyc index 68e01f0..3adf1a0 100644 Binary files a/proyecto/__pycache__/__init__.cpython-314.pyc and b/proyecto/__pycache__/__init__.cpython-314.pyc differ diff --git a/proyecto/__pycache__/settings.cpython-314.pyc b/proyecto/__pycache__/settings.cpython-314.pyc index 3c9047e..9f4f766 100644 Binary files a/proyecto/__pycache__/settings.cpython-314.pyc and b/proyecto/__pycache__/settings.cpython-314.pyc differ diff --git a/proyecto/settings.py b/proyecto/settings.py index 6e9f179..913eab2 100644 --- a/proyecto/settings.py +++ b/proyecto/settings.py @@ -10,26 +10,65 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/6.0/ref/settings/ """ +import logging +import os, sys 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'. BASE_DIR = Path(__file__).resolve().parent.parent +load_dotenv(BASE_DIR / '.env') # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ # 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! -DEBUG = True +DEBUG = env_bool('DEBUG', True) -ALLOWED_HOSTS = [ - "192.168.1.142", - "localhost", - "127.0.0.1" -] +ALLOWED_HOSTS = env_list('ALLOWED_HOSTS', [ + '192.168.1.142', + 'localhost', + '127.0.0.1', +]) # Application definition @@ -152,7 +191,7 @@ MEDIA_ROOT = BASE_DIR / 'tienda' / 'static' / 'media' CACHES = { 'default': { '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': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', } @@ -177,11 +216,114 @@ MESSAGE_TAGS = { # Login URL LOGIN_URL = '/tienda/login/' -STRIPE_PUBLISHABLE_KEY = 'pk_test_51SxmSYJ2DN4I0upQDdiPeda51nmpB0ZEWfkNFKHhWBG4knIgtRoC1d9iFRoxRNdJKiLlQsIddlebU06R9XCfiSZH00ffoirwPw' -STRIPE_SECRET_KEY = 'sk_test_51SxmSYJ2DN4I0upQZb42dWKuIKToZxkQeK3vsCdijcaUr17EMEyFcLdIAm5AVEvUs96MAxl4KnZ4Yncp5VykO4ej00MZGs6c1F' +STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY', '') +STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', '') # PayPal Configuration (Sandbox) # Para obtener credenciales: https://sandbox.paypal.com/ -PAYPAL_CLIENT_ID = 'AX3TIklQ41456StP2puciDfkQ6oSWAQWNYB8H9ThDsU6C_VYhWqwDZ1w0dK-No38Aa9IqAbrZbE-1kHJ' # Reemplazar con tu Client ID de PayPal Sandbox -PAYPAL_CLIENT_SECRET = 'EIXny9EkiebiCnwkfmWJa7ufwHwdUCTeSZ5TiUZycBPREradcN7U0vBKCUlg-PYd3SeXTW33D0kZb5BT' # Reemplazar con tu Client Secret de PayPal Sandbox -PAYPAL_MODE = 'sandbox' # Cambiar a 'live' en producción +PAYPAL_CLIENT_ID = os.getenv('PAYPAL_CLIENT_ID', '') # Reemplazar con tu Client ID de PayPal Sandbox +PAYPAL_CLIENT_SECRET = os.getenv('PAYPAL_CLIENT_SECRET', '') # Reemplazar con tu Client Secret de PayPal Sandbox +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' \ No newline at end of file diff --git a/tienda/__pycache__/admin.cpython-314.pyc b/tienda/__pycache__/admin.cpython-314.pyc index b298973..1b0d656 100644 Binary files a/tienda/__pycache__/admin.cpython-314.pyc and b/tienda/__pycache__/admin.cpython-314.pyc differ diff --git a/tienda/__pycache__/models.cpython-314.pyc b/tienda/__pycache__/models.cpython-314.pyc index d30e9f6..e656c0b 100644 Binary files a/tienda/__pycache__/models.cpython-314.pyc and b/tienda/__pycache__/models.cpython-314.pyc differ diff --git a/tienda/__pycache__/urls.cpython-314.pyc b/tienda/__pycache__/urls.cpython-314.pyc index a5269ea..ab71d24 100644 Binary files a/tienda/__pycache__/urls.cpython-314.pyc and b/tienda/__pycache__/urls.cpython-314.pyc differ diff --git a/tienda/__pycache__/vars.cpython-314.pyc b/tienda/__pycache__/vars.cpython-314.pyc index 3d22943..c41a8a3 100644 Binary files a/tienda/__pycache__/vars.cpython-314.pyc and b/tienda/__pycache__/vars.cpython-314.pyc differ diff --git a/tienda/__pycache__/views.cpython-314.pyc b/tienda/__pycache__/views.cpython-314.pyc index af53977..4047b40 100644 Binary files a/tienda/__pycache__/views.cpython-314.pyc and b/tienda/__pycache__/views.cpython-314.pyc differ diff --git a/tienda/admin.py b/tienda/admin.py index 3f42731..685160b 100644 --- a/tienda/admin.py +++ b/tienda/admin.py @@ -1,12 +1,12 @@ 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. admin.site.register(Category) admin.site.register(Image) admin.site.register(Product) - - +admin.site.register(User) +admin.site.register(VerificationCode) class CartItemInline(admin.TabularInline): model = CartItem extra = 0 diff --git a/tienda/migrations/0001_initial.py b/tienda/migrations/0001_initial.py index 428d4c3..71b71ea 100644 --- a/tienda/migrations/0001_initial.py +++ b/tienda/migrations/0001_initial.py @@ -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.utils.timezone +from django.conf import settings from django.db import migrations, models @@ -9,6 +13,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ @@ -17,10 +22,160 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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={ - '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')}, }, ), ] diff --git a/tienda/migrations/0002_alter_category_options_remove_category_parent.py b/tienda/migrations/0002_alter_category_options_remove_category_parent.py deleted file mode 100644 index a7abb93..0000000 --- a/tienda/migrations/0002_alter_category_options_remove_category_parent.py +++ /dev/null @@ -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', - ), - ] diff --git a/tienda/migrations/0002_verificationcode_code_mode_and_more.py b/tienda/migrations/0002_verificationcode_code_mode_and_more.py new file mode 100644 index 0000000..8dfb6c3 --- /dev/null +++ b/tienda/migrations/0002_verificationcode_code_mode_and_more.py @@ -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), + ), + ] diff --git a/tienda/migrations/0003_image.py b/tienda/migrations/0003_image.py deleted file mode 100644 index 93963c8..0000000 --- a/tienda/migrations/0003_image.py +++ /dev/null @@ -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='')), - ], - ), - ] diff --git a/tienda/migrations/0004_alter_image_image.py b/tienda/migrations/0004_alter_image_image.py deleted file mode 100644 index dbd9c16..0000000 --- a/tienda/migrations/0004_alter_image_image.py +++ /dev/null @@ -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/'), - ), - ] diff --git a/tienda/migrations/0005_product.py b/tienda/migrations/0005_product.py deleted file mode 100644 index be53e93..0000000 --- a/tienda/migrations/0005_product.py +++ /dev/null @@ -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')), - ], - ), - ] diff --git a/tienda/migrations/0006_product_secondary_images.py b/tienda/migrations/0006_product_secondary_images.py deleted file mode 100644 index 062bf13..0000000 --- a/tienda/migrations/0006_product_secondary_images.py +++ /dev/null @@ -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'), - ), - ] diff --git a/tienda/migrations/0007_product_briefdesc.py b/tienda/migrations/0007_product_briefdesc.py deleted file mode 100644 index 8a8325d..0000000 --- a/tienda/migrations/0007_product_briefdesc.py +++ /dev/null @@ -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=''), - ), - ] diff --git a/tienda/migrations/0008_cart_cartitem.py b/tienda/migrations/0008_cart_cartitem.py deleted file mode 100644 index ec6272c..0000000 --- a/tienda/migrations/0008_cart_cartitem.py +++ /dev/null @@ -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')}, - }, - ), - ] diff --git a/tienda/migrations/0009_product_creator.py b/tienda/migrations/0009_product_creator.py deleted file mode 100644 index c5cb169..0000000 --- a/tienda/migrations/0009_product_creator.py +++ /dev/null @@ -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), - ), - ] diff --git a/tienda/migrations/0010_order_orderitem.py b/tienda/migrations/0010_order_orderitem.py deleted file mode 100644 index 003b14e..0000000 --- a/tienda/migrations/0010_order_orderitem.py +++ /dev/null @@ -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)), - ], - ), - ] diff --git a/tienda/migrations/0011_ordermessage.py b/tienda/migrations/0011_ordermessage.py deleted file mode 100644 index 17796b6..0000000 --- a/tienda/migrations/0011_ordermessage.py +++ /dev/null @@ -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'], - }, - ), - ] diff --git a/tienda/migrations/0012_image_alt_shippingaddress.py b/tienda/migrations/0012_image_alt_shippingaddress.py deleted file mode 100644 index d6e0f4e..0000000 --- a/tienda/migrations/0012_image_alt_shippingaddress.py +++ /dev/null @@ -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'], - }, - ), - ] diff --git a/tienda/migrations/__pycache__/0001_initial.cpython-314.pyc b/tienda/migrations/__pycache__/0001_initial.cpython-314.pyc index 729551d..851e7f4 100644 Binary files a/tienda/migrations/__pycache__/0001_initial.cpython-314.pyc and b/tienda/migrations/__pycache__/0001_initial.cpython-314.pyc differ diff --git a/tienda/migrations/__pycache__/0002_alter_category_options_remove_category_parent.cpython-314.pyc b/tienda/migrations/__pycache__/0002_alter_category_options_remove_category_parent.cpython-314.pyc deleted file mode 100644 index cd30f79..0000000 Binary files a/tienda/migrations/__pycache__/0002_alter_category_options_remove_category_parent.cpython-314.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0002_verificationcode_code_mode_and_more.cpython-314.pyc b/tienda/migrations/__pycache__/0002_verificationcode_code_mode_and_more.cpython-314.pyc new file mode 100644 index 0000000..21c9811 Binary files /dev/null and b/tienda/migrations/__pycache__/0002_verificationcode_code_mode_and_more.cpython-314.pyc differ diff --git a/tienda/migrations/__pycache__/0003_image.cpython-314.pyc b/tienda/migrations/__pycache__/0003_image.cpython-314.pyc deleted file mode 100644 index 8954823..0000000 Binary files a/tienda/migrations/__pycache__/0003_image.cpython-314.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0004_alter_image_image.cpython-314.pyc b/tienda/migrations/__pycache__/0004_alter_image_image.cpython-314.pyc deleted file mode 100644 index 85b1d13..0000000 Binary files a/tienda/migrations/__pycache__/0004_alter_image_image.cpython-314.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0005_product.cpython-314.pyc b/tienda/migrations/__pycache__/0005_product.cpython-314.pyc deleted file mode 100644 index 1bc7953..0000000 Binary files a/tienda/migrations/__pycache__/0005_product.cpython-314.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0006_product_secondary_images.cpython-314.pyc b/tienda/migrations/__pycache__/0006_product_secondary_images.cpython-314.pyc deleted file mode 100644 index 03f7d36..0000000 Binary files a/tienda/migrations/__pycache__/0006_product_secondary_images.cpython-314.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0007_product_briefdesc.cpython-314.pyc b/tienda/migrations/__pycache__/0007_product_briefdesc.cpython-314.pyc deleted file mode 100644 index 0b36937..0000000 Binary files a/tienda/migrations/__pycache__/0007_product_briefdesc.cpython-314.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0008_cart_cartitem.cpython-314.pyc b/tienda/migrations/__pycache__/0008_cart_cartitem.cpython-314.pyc deleted file mode 100644 index 8df3186..0000000 Binary files a/tienda/migrations/__pycache__/0008_cart_cartitem.cpython-314.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0009_auto_20260206_1042.cpython-314.pyc b/tienda/migrations/__pycache__/0009_auto_20260206_1042.cpython-314.pyc deleted file mode 100644 index 68fa0b9..0000000 Binary files a/tienda/migrations/__pycache__/0009_auto_20260206_1042.cpython-314.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0009_product_creator.cpython-314.pyc b/tienda/migrations/__pycache__/0009_product_creator.cpython-314.pyc deleted file mode 100644 index 32a084f..0000000 Binary files a/tienda/migrations/__pycache__/0009_product_creator.cpython-314.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0010_order_orderitem.cpython-314.pyc b/tienda/migrations/__pycache__/0010_order_orderitem.cpython-314.pyc deleted file mode 100644 index 6d6fc1b..0000000 Binary files a/tienda/migrations/__pycache__/0010_order_orderitem.cpython-314.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0011_ordermessage.cpython-314.pyc b/tienda/migrations/__pycache__/0011_ordermessage.cpython-314.pyc deleted file mode 100644 index b0db5c8..0000000 Binary files a/tienda/migrations/__pycache__/0011_ordermessage.cpython-314.pyc and /dev/null differ diff --git a/tienda/migrations/__pycache__/0012_image_alt_shippingaddress.cpython-314.pyc b/tienda/migrations/__pycache__/0012_image_alt_shippingaddress.cpython-314.pyc deleted file mode 100644 index 31f3562..0000000 Binary files a/tienda/migrations/__pycache__/0012_image_alt_shippingaddress.cpython-314.pyc and /dev/null differ diff --git a/tienda/models.py b/tienda/models.py index 7eddd51..e391fac 100644 --- a/tienda/models.py +++ b/tienda/models.py @@ -1,6 +1,31 @@ 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 +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. 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') + 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) total = models.FloatField(default=0) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PAID) diff --git a/tienda/static/js/checkout.js b/tienda/static/js/checkout.js index bfed0d8..a5215d6 100644 --- a/tienda/static/js/checkout.js +++ b/tienda/static/js/checkout.js @@ -45,6 +45,14 @@ document.addEventListener("DOMContentLoaded", () => { console.log("Stripe initialized"); 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"); button.disabled = true; button.innerHTML = "Procesando..."; @@ -55,7 +63,9 @@ document.addEventListener("DOMContentLoaded", () => { "Content-Type": "application/json", "X-CSRFToken": getCookie("csrftoken") }, - body: JSON.stringify({}) + body: JSON.stringify({ + shipping_address_id: selectedShippingAddress + }) }) .then((res) => { console.log("Session response status:", res.status); diff --git a/tienda/tasks.py b/tienda/tasks.py new file mode 100644 index 0000000..5a7de53 --- /dev/null +++ b/tienda/tasks.py @@ -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)) \ No newline at end of file diff --git a/tienda/templates/tienda/base.html b/tienda/templates/tienda/base.html index e6a162d..e3586db 100644 --- a/tienda/templates/tienda/base.html +++ b/tienda/templates/tienda/base.html @@ -1,4 +1,5 @@ {% load static %} +{% load cache %} {% load compress %} @@ -69,6 +70,7 @@ {% block head %}{% endblock %} + {% cache 500 sidebar request.user.username %} + {% endcache %}
@@ -131,30 +134,31 @@ {% block content %}{% endblock %} + {% cache 500 footer %} + {% endcache %}
- + {% cache 500 scripts %} + {% endcache %} \ No newline at end of file diff --git a/tienda/templates/tienda/checkout.html b/tienda/templates/tienda/checkout.html index fbd9da4..4272896 100644 --- a/tienda/templates/tienda/checkout.html +++ b/tienda/templates/tienda/checkout.html @@ -49,6 +49,30 @@ {% if cart_items %} +
+
+
1) Selecciona la dirección de envío
+ {% if addresses %} +
+ + +
+ {% else %} +
+ No tienes direcciones de envío creadas. + Crear dirección +
+ {% endif %} +
+
+
@@ -87,20 +111,22 @@
-

Selecciona tu método de pago

+

2) Selecciona tu método de pago

@@ -115,6 +141,13 @@ // Manejo del botón de PayPal document.getElementById('paypal-button').addEventListener('click', async function(e) { 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 originalText = button.innerHTML; @@ -138,7 +171,8 @@ headers: { 'X-CSRFToken': csrfToken || '', 'Content-Type': 'application/json', - } + }, + body: JSON.stringify({ shipping_address_id: selectedShippingAddress }) }); console.log('Response status:', response.status); diff --git a/tienda/templates/tienda/editar_direccion.html b/tienda/templates/tienda/editar_direccion.html index 46ddf30..d5bc6e3 100644 --- a/tienda/templates/tienda/editar_direccion.html +++ b/tienda/templates/tienda/editar_direccion.html @@ -35,17 +35,27 @@
- - + + + + {% for town in almeria_municipalities %} + + {% endfor %} + +
Selecciona o escribe un municipio de la provincia de Almería.
+
+ El pueblo/ciudad debe pertenecer a la provincia de Almería. +
- + +
Solo aceptamos códigos postales de Almería (04xxx).
- +
@@ -67,4 +77,64 @@
+ + {% endblock %} diff --git a/tienda/templates/tienda/mis_compras.html b/tienda/templates/tienda/mis_compras.html new file mode 100644 index 0000000..28dfdfd --- /dev/null +++ b/tienda/templates/tienda/mis_compras.html @@ -0,0 +1,63 @@ +{% extends "tienda/base.html" %} +{% load static %} + +{% block content %} +
+
+

Mis Compras

+ +
+
+ +
+
+ +
+
+ +
+
+

Total de compras: {{ total_orders }}

+ {% if orders %} +
+
+ + + + + + + + + + + {% for order in orders %} + + + + + + + + {% endfor %} + +
Pedido #FechaTotalEstadoMétodo
{{ order.id }}{{ order.created_at|date:"d/m/Y H:i" }}{{ order.total }}€{{ order.get_status_display }}{{ order.get_payment_method_display }}
+
+ {% else %} +
+ Aún no has realizado compras. +
+ {% endif %} + + +{% endblock %} diff --git a/tienda/templates/tienda/mis_recibos.html b/tienda/templates/tienda/mis_recibos.html new file mode 100644 index 0000000..49426d0 --- /dev/null +++ b/tienda/templates/tienda/mis_recibos.html @@ -0,0 +1,63 @@ +{% extends "tienda/base.html" %} +{% load static %} + +{% block content %} +
+
+

Mis Recibos

+ +
+
+ +
+
+ +
+
+ +
+
+

Total de recibos: {{ total_receipts }}

+ {% if receipts %} +
+ + + + + + + + + + + + {% for receipt in receipts %} + + + + + + + + {% endfor %} + +
Recibo #FechaTotalMétodoReferencia
{{ receipt.id }}{{ receipt.created_at|date:"d/m/Y H:i" }}{{ receipt.total }}€{{ receipt.get_payment_method_display }}{{ receipt.payment_reference|default:"-" }}
+
+ {% else %} +
+ No tienes recibos disponibles todavía. +
+ {% endif %} +
+
+{% endblock %} diff --git a/tienda/templates/tienda/pedidos_vendedor.html b/tienda/templates/tienda/pedidos_vendedor.html index 70ccbb3..833a9cd 100644 --- a/tienda/templates/tienda/pedidos_vendedor.html +++ b/tienda/templates/tienda/pedidos_vendedor.html @@ -43,6 +43,22 @@
  • Precio total: {{ item.total_price }}€
  • Fecha: {{ item.created_at|date:"d/m/Y H:i" }}
  • +
    Dirección de envío
    + {% if item.order.shipping_address %} + + {% else %} +

    Dirección no disponible.

    + {% endif %}
    Cambiar Estado
    diff --git a/tienda/templates/tienda/portal_usuario.html b/tienda/templates/tienda/portal_usuario.html index 23b51ba..557497e 100644 --- a/tienda/templates/tienda/portal_usuario.html +++ b/tienda/templates/tienda/portal_usuario.html @@ -14,6 +14,8 @@
    Inicio + Compras + Recibos Mi Perfil Direcciones Mensajes @@ -29,6 +31,7 @@
    📦 Mis Pedidos

    {{ total_orders }}

    pedidos realizados

    + Ver compras
    @@ -54,6 +57,18 @@ +
    +
    +
    +
    +
    🧾 Recibos
    +

    consulta tus recibos de pago

    + Ver recibos +
    +
    +
    +
    +
    diff --git a/tienda/urls.py b/tienda/urls.py index d02b7d2..a8f3e3a 100644 --- a/tienda/urls.py +++ b/tienda/urls.py @@ -35,6 +35,8 @@ urlpatterns = [ path("paypal/execute/", views.paypal_execute, name="paypal_execute"), # Portal de 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/cambiar-contrasena/", views.cambiar_contrasena, name="cambiar_contrasena"), path("usuario/direcciones/", views.direcciones_usuario, name="direcciones_usuario"), @@ -42,4 +44,5 @@ urlpatterns = [ path("usuario/direcciones//editar/", views.editar_direccion, name="editar_direccion"), path("usuario/direcciones//eliminar/", views.eliminar_direccion, name="eliminar_direccion"), path("usuario/mensajes/", views.mensajes_comprador, name="mensajes_comprador"), + path("verify/", views.verify, name="verify") ] diff --git a/tienda/utilities.py b/tienda/utilities.py new file mode 100644 index 0000000..8daa611 --- /dev/null +++ b/tienda/utilities.py @@ -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) \ No newline at end of file diff --git a/tienda/vars.py b/tienda/vars.py index 5750f32..3fc1273 100644 --- a/tienda/vars.py +++ b/tienda/vars.py @@ -1,2 +1,42 @@ PAGE_SIZE = 20 -VAT_RATE = 0.21 # IVA 21% \ No newline at end of file +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! +""" \ No newline at end of file diff --git a/tienda/views.py b/tienda/views.py index 1c560ea..ba257d4 100644 --- a/tienda/views.py +++ b/tienda/views.py @@ -1,11 +1,21 @@ 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.models import User + from django.contrib.auth.decorators import login_required from django.contrib import messages -from .models import Product, Category, Cart, CartItem, Image, Order, OrderItem, OrderMessage, ShippingAddress -from .vars import PAGE_SIZE +from .models import User, Product, Category, Cart, CartItem, Image, Order, OrderItem, OrderMessage, ShippingAddress, VerificationCode +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.views.decorators.csrf import csrf_exempt from django.urls import reverse @@ -13,8 +23,78 @@ from decimal import Decimal, ROUND_HALF_UP import stripe from django.db import models, transaction from django.core.cache import cache +import re +import unicodedata +import json +import random, string +import logging # 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): """Página de inicio del sitio""" categorias = Category.objects.all() @@ -50,32 +130,57 @@ def login(request: HttpRequest): email = request.POST.get("email") password = request.POST.get("password") remember = request.POST.get("remember") + client_ip = _get_client_ip(request) # Buscar usuario por email try: user_obj = User.objects.get(email=email) username = user_obj.username 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.") return render(request, "tienda/login.html") # Autenticar usuario 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: auth_login(request, user) # Configurar duración de sesión if not remember: - # Si no marca "Recordarme", la sesión expira al cerrar el navegador request.session.set_expiry(0) else: - # Si marca "Recordarme", la sesión dura 2 semanas 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}!") return redirect("index") 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.") return render(request, "tienda/login.html") @@ -83,22 +188,28 @@ def login(request: HttpRequest): def register(request: HttpRequest): + if request.user.is_authenticated: + return redirect("index") if request.method == "POST": name = request.POST.get("name") email = request.POST.get("email") password = request.POST.get("password") password_confirm = request.POST.get("password_confirm") + client_ip = _get_client_ip(request) # Validaciones 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.") return render(request, "tienda/register.html") 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.") return render(request, "tienda/register.html") 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.") return render(request, "tienda/register.html") @@ -119,19 +230,37 @@ def register(request: HttpRequest): password=password, first_name=name ) + + audit_logger.info( + "REGISTER_SUCCESS user_id=%s username=%s email=%s ip=%s", + user.id, + user.username, + user.email, + client_ip, + ) - # Iniciar sesión automáticamente - auth_login(request, user) - request.session.set_expiry(1209600) # 14 días + 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! Bienvenido {name}.") + messages.success(request, f"¡Cuenta creada exitosamente! Por favor, verifica tu correo entrando al Link enviado.") return redirect("index") return render(request, "tienda/register.html") 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) + audit_logger.info("LOGOUT user_id=%s email=%s ip=%s", user_id, email, client_ip) messages.success(request, "Has cerrado sesión exitosamente.") return redirect("index") @@ -178,25 +307,61 @@ def get_or_create_cart(request): 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.""" 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 + 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(): order = Order.objects.create( 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, - total=cart.get_total(), + total=float(order_total), status=Order.STATUS_PAID, payment_method=payment_method, 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 OrderItem.objects.create( order=order, @@ -204,8 +369,8 @@ def create_order_from_cart(request, payment_method, payment_reference=""): product_name=product.name, seller=product.creator, quantity=item.quantity, - unit_price=product.price, - total_price=product.price * item.quantity, + unit_price=float(unit_price_with_vat), + total_price=float(line_total_with_vat), ) cart.items.all().delete() @@ -322,7 +487,7 @@ def mis_productos(request: HttpRequest): def pedidos_vendedor(request: HttpRequest): """Muestra los pedidos asignados al vendedor autenticado""" 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') return render(request, "tienda/pedidos_vendedor.html", { @@ -540,9 +705,11 @@ def borrar_producto(request: HttpRequest, id: int): def checkout(request: HttpRequest): cart = get_or_create_cart(request) cart_items = cart.items.select_related("product") + addresses = ShippingAddress.objects.filter(user=request.user) return render(request, "tienda/checkout.html", { "cart": cart, - "cart_items": cart_items + "cart_items": cart_items, + "addresses": addresses, }) @csrf_exempt @@ -561,6 +728,10 @@ def create_checkout_session(request: HttpRequest): return JsonResponse({"error": "Método no permitido"}, status=405) 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_items = cart.items.select_related("product") @@ -571,7 +742,8 @@ def create_checkout_session(request: HttpRequest): line_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: continue line_items.append({ @@ -601,18 +773,23 @@ def create_checkout_session(request: HttpRequest): ) request.session['stripe_session_id'] = session.id + request.session['selected_shipping_address_id'] = shipping_address.id return JsonResponse({"sessionId": session.id}) 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) def checkout_success(request: HttpRequest): 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: 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!") 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) 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 cart = get_or_create_cart(request) @@ -669,16 +850,24 @@ def create_paypal_payment(request: HttpRequest): # Crear lista de items para PayPal payment_items = [] + payment_total = Decimal("0.00") 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({ "name": item.product.name, "sku": f"product_{item.product.id}", - "price": str(round(float(item.product.price), 2)), + "price": format(unit_price_with_vat, ".2f"), "currency": "EUR", "quantity": item.quantity }) - total = str(round(float(cart.get_total()), 2)) + total = format(payment_total, ".2f") # Crear el pago payment = paypalrestsdk.Payment({ @@ -713,6 +902,7 @@ def create_paypal_payment(request: HttpRequest): if payment.create(): # Guardar el payment ID en sesión request.session['paypal_payment_id'] = payment.id + request.session['selected_shipping_address_id'] = shipping_address.id # Encontrar el link de aprobación for link in payment.links: @@ -723,16 +913,15 @@ def create_paypal_payment(request: HttpRequest): else: # Loguear el error 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) except ImportError: + logger.error("PAYPAL_SDK_NOT_INSTALLED") return JsonResponse({"error": "SDK de PayPal no instalado"}, status=500) except Exception as e: error_msg = str(e) - print(f"PayPal Exception: {error_msg}") - import traceback - traceback.print_exc() + logger.exception("PAYPAL_CREATE_EXCEPTION user_id=%s error=%s", request.user.id, error_msg) return JsonResponse({"error": f"Error: {error_msg}"}, status=500) @@ -766,11 +955,15 @@ def paypal_execute(request: HttpRequest): # Ejecutar el pago if payment.execute({"payer_id": payer_id}): # 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 if 'paypal_payment_id' in request.session: 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.") return render(request, "tienda/checkout_success.html", {}) @@ -780,6 +973,7 @@ def paypal_execute(request: HttpRequest): return redirect("checkout") 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)}") return redirect("checkout") 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 def editar_perfil(request: HttpRequest): """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() city = request.POST.get("city", "").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() is_default = request.POST.get("is_default") == "on" # Validaciones if not all([full_name, address_line_1, city, postal_code, phone]): 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 ShippingAddress.objects.create( @@ -933,7 +1160,7 @@ def crear_direccion(request: HttpRequest): messages.success(request, "Dirección creada correctamente.") 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 @@ -947,7 +1174,7 @@ def editar_direccion(request: HttpRequest, id: int): direccion.address_line_2 = request.POST.get("address_line_2", "").strip() direccion.city = request.POST.get("city", "").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.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, direccion.postal_code, direccion.phone]): 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() messages.success(request, "Dirección actualizada correctamente.") 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 @@ -990,3 +1225,43 @@ def mensajes_comprador(request: HttpRequest): return render(request, "tienda/mensajes_comprador.html", { "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("

    Error

    No existe el codigo de verificación

    ") + 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("

    Error

    No existe el codigo de verificación

    ") + + +def reset_password(request: HttpRequest): + if request.user.is_authenticated: + return redirect("index") + + + return render(request, "tienda/reset_password", {}) \ No newline at end of file