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:
2026-03-10 13:08:10 +01:00
parent 01024bb97e
commit 162b63cae9
51 changed files with 1082 additions and 385 deletions
+2
View File
@@ -0,0 +1,2 @@
from .celery import app as celery_app
__all__ = ('celery_app',)
Binary file not shown.
Binary file not shown.
+155 -13
View File
@@ -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'
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -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
+158 -3
View File
@@ -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')},
},
),
]
@@ -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),
),
]
-21
View File
@@ -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/'),
),
]
-25
View File
@@ -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=''),
),
]
-39
View File
@@ -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')},
},
),
]
-21
View File
@@ -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),
),
]
-45
View File
@@ -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)),
],
),
]
-29
View File
@@ -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'],
},
),
]
+27 -1
View File
@@ -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)
+11 -1
View File
@@ -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);
+7
View File
@@ -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))
+26 -21
View File
@@ -1,4 +1,5 @@
{% load static %}
{% load cache %}
{% load compress %}
<!DOCTYPE html>
<html lang="es">
@@ -69,6 +70,7 @@
{% block head %}{% endblock %}
</head>
<body>
{% cache 500 sidebar request.user.username %}
<nav class="navbar navbar-expand-md header">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'home' %}">
@@ -102,7 +104,7 @@
{% if user.is_authenticated %}
<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>
{% else %}
<a href="{% url 'login' %}" class="nav-link btn btn-primary btn-sm">Iniciar Sesión</a>
@@ -112,6 +114,7 @@
</div>
</div>
</nav>
{% endcache %}
<div class="container-fluid">
<!-- Mensajes -->
@@ -131,30 +134,31 @@
<!-- Contenido-->
{% block content %}{% endblock %}
{% cache 500 footer %}
<!-- Footer-->
<div id="footer" class="row pt-2 pb-2 mt-5">
<div class="col-md-12 grid">
<p class="text-center">Enlace 1</p>
<p class="text-center">Enlace 2</p>
<p class="text-center">Enlace 3</p>
<p class="text-center">Enlace 4</p>
<p class="text-center">Enlace 5</p>
<p class="text-center">Enlace 6</p>
<p class="text-center">Enlace 7</p>
<p class="text-center">Enlace 8</p>
<p class="text-center">Enlace 9</p>
<p class="text-center">Enlace 10</p>
<p class="text-center">Enlace 11</p>
<p class="text-center">Enlace 12</p>
<p class="text-center">Enlace 13</p>
<p class="text-center">Enlace 14</p>
<p class="text-center">Enlace 15</p>
<p class="text-center">Enlace 16</p>
</div>
<div class="col-md-12 grid">
<p class="text-center">Enlace 1</p>
<p class="text-center">Enlace 2</p>
<p class="text-center">Enlace 3</p>
<p class="text-center">Enlace 4</p>
<p class="text-center">Enlace 5</p>
<p class="text-center">Enlace 6</p>
<p class="text-center">Enlace 7</p>
<p class="text-center">Enlace 8</p>
<p class="text-center">Enlace 9</p>
<p class="text-center">Enlace 10</p>
<p class="text-center">Enlace 11</p>
<p class="text-center">Enlace 12</p>
<p class="text-center">Enlace 13</p>
<p class="text-center">Enlace 14</p>
<p class="text-center">Enlace 15</p>
<p class="text-center">Enlace 16</p>
</div>
</div>
{% endcache %}
</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>
@@ -243,5 +247,6 @@
}
});
</script>
{% endcache %}
</body>
</html>
+38 -4
View File
@@ -49,6 +49,30 @@
</div>
{% 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">
<table class="table table-striped align-middle">
<thead>
@@ -87,20 +111,22 @@
</div>
<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">
<button
id="checkout-button"
class="btn btn-primary payment-btn"
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
</button>
<button
id="paypal-button"
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
</button>
</div>
@@ -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);
+74 -4
View File
@@ -35,17 +35,27 @@
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="city" class="form-label">Ciudad *</label>
<input type="text" class="form-control" id="city" name="city" value="{{ direccion.city|default:'' }}" required>
<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:'' }}" 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 class="col-md-6 mb-3">
<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 class="mb-3">
<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 class="mb-3">
<label for="phone" class="form-label">Teléfono *</label>
@@ -67,4 +77,64 @@
</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 %}
+63
View File
@@ -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 %}
+63
View File
@@ -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>Fecha:</strong> {{ item.created_at|date:"d/m/Y H:i" }}</li>
</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 class="col-md-6">
<h6>Cambiar Estado</h6>
@@ -14,6 +14,8 @@
<div class="col-12">
<div class="btn-group" role="group">
<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 'direcciones_usuario' %}" class="btn btn-outline-primary">Direcciones</a>
<a href="{% url 'mensajes_comprador' %}" class="btn btn-outline-primary">Mensajes</a>
@@ -29,6 +31,7 @@
<h5 class="card-title">📦 Mis Pedidos</h5>
<p class="display-4">{{ total_orders }}</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>
@@ -54,6 +57,18 @@
</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 -->
<div class="row mt-4">
<div class="col-12">
+3
View File
@@ -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/<int:id>/editar/", views.editar_direccion, name="editar_direccion"),
path("usuario/direcciones/<int:id>/eliminar/", views.eliminar_direccion, name="eliminar_direccion"),
path("usuario/mensajes/", views.mensajes_comprador, name="mensajes_comprador"),
path("verify/<str:code>", views.verify, name="verify")
]
+46
View File
@@ -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)
+41 -1
View File
@@ -1,2 +1,42 @@
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!
"""
+311 -36
View File
@@ -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("<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", {})