first commit

This commit is contained in:
2026-02-15 09:23:44 +01:00
commit 5a22d3abae
276 changed files with 231906 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
# Copilot Instructions - Django Tienda Project
## Project Overview
This is a Django 6.0.1 e-commerce application ("tienda" = store) with a monolithic architecture. The project uses SQLite for development and Bootstrap 5.0.2 for styling.
## Architecture
- **Main project**: `proyecto/` - Django settings, root URL configuration, WSGI/ASGI config
- **Tienda app**: `tienda/` - Core e-commerce functionality (currently minimal, ready for expansion)
- **Database**: SQLite (`db.sqlite3`) for local development
- **Static files**: Bootstrap 5.0.2 bundled in `tienda/static/css/styles.css` (9929 lines), images in `tienda/static/img/`
## URL Routing Pattern
URLs follow a two-tier structure:
1. Root: [proyecto/urls.py](proyecto/urls.py) includes app URLs
2. App level: [tienda/urls.py](tienda/urls.py) defines app-specific routes
- Main store accessible at `/tienda/` (not root `/`)
- Pattern: `path('tienda/', include('tienda.urls'))`
## Template Structure
Templates use Django's inheritance pattern:
- **Base**: [tienda/templates/tienda/base.html](tienda/templates/tienda/base.html) - Contains navigation header with logo, search bar, login/register buttons
- **Pages**: Extend base using `{% extends "tienda/base.html" %}`
- **Static files**: Load with `{% load static %}` at template top
- App templates must be in `tienda/templates/tienda/` (app-namespaced)
## Static Files Configuration
- **Collection directory**: `STATIC_ROOT = BASE_DIR / 'staticfiles'` for production
- **Development dirs**: `STATICFILES_DIRS` includes `tienda/static/`
- **URL prefix**: `STATIC_URL = 'static/'`
- **SCSS compilation**: Styles are authored in [tienda/static/scss/custom.scss](tienda/static/scss/custom.scss) and compiled to [tienda/static/css/custom.css](tienda/static/css/custom.css)
- **IMPORTANT**: Always edit `custom.scss`, never `custom.css` - CSS file is auto-generated
- Compile with Sass compiler (npm sass or similar)
- Bootstrap CSS is vendored in `tienda/static/css/styles.css` - don't add CDN links
## Media Files (User Uploads) Configuration
- **Storage location**: `MEDIA_ROOT = BASE_DIR / 'tienda' / 'static' / 'media'` - all uploads go here
- **URL prefix**: `MEDIA_URL = 'media/'`
- **Image uploads**: Organized in `tienda/static/media/images/` via `upload_to='images/'` in ImageField
- **Access**: Media files served automatically in development via Django's static file handler
- **Image model**: Located in [tienda/models.py](tienda/models.py) with `ImageField(upload_to='images/')`
## Key Conventions
1. **App registration**: Apps use `AppConfig` - see `'tienda.apps.TiendaConfig'` in `INSTALLED_APPS`
2. **View pattern**: Function-based views in [tienda/views.py](tienda/views.py) (e.g., `index` renders template with context dict)
3. **Spanish naming**: UI elements use Spanish (`Iniciar Sesión`, `Registrarse`, `Menú`)
4. **Models**: Located in [tienda/models.py](tienda/models.py) - includes Product, Category, Cart, CartItem, Image
5. **Product caching**: Products are cached in Redis for 5 minutes when visited
- Cache key pattern: `product_{id}` (stored as `:1:product_{id}` in Redis)
- Cache is invalidated automatically when product is edited or deleted
- Improves performance by ~15x for product detail pages
## Development Workflow
- **Python environment**: This project uses venv at `.venv/bin/python`. Always use `/home/daniel/projects/proyecto/proyecto2/proyecto/.venv/bin/python` when running Python commands
- **Redis**: Sessions and product cache are stored in Redis (db 1). Redis must be running on localhost:6379
- Start Redis: `sudo systemctl start redis-server` (Linux) or `brew services start redis` (macOS)
- For Arch Linux: `sudo systemctl start valkey` (uses valkey-cli instead of redis-cli)
- Verify: `redis-cli ping` or `valkey-cli ping` (should return PONG)
- Test product cache: `python test_product_cache.py`
- See [REDIS_SETUP.md](REDIS_SETUP.md) for complete setup instructions
- **Run server**: `/home/daniel/projects/proyecto/proyecto2/proyecto/.venv/bin/python manage.py runserver`
- **Migrations**: `/home/daniel/projects/proyecto/proyecto2/proyecto/.venv/bin/python manage.py makemigrations && /home/daniel/projects/proyecto/proyecto2/proyecto/.venv/bin/python manage.py migrate`
- **IMPORTANT**: If `makemigrations` appears to fail (exit code 130 or similar), ALWAYS check `tienda/migrations/` directory first - the migration file is often created successfully despite the error message
- Before attempting to recreate migrations, verify if the file already exists
- **Static files**: `python manage.py collectstatic` (for production)
- **Admin**: Access at `/admin/` (not `/tienda/admin/`)
- **Main app URL**: http://localhost:8000/tienda/
## Critical Details
- Django 6.0.1 specific - use current Django patterns
- SQLite database committed (`db.sqlite3`) - migrations already applied
- Bootstrap requires JavaScript for modals/toggles (reference data-bs-* attributes in base.html)
- SECRET_KEY is development-only (contains 'insecure' marker)
- DEBUG=True - not production-ready
## When Adding Features
- New models: Add to [tienda/models.py](tienda/models.py), run migrations, register in [tienda/admin.py](tienda/admin.py)
- New views: Add to [tienda/views.py](tienda/views.py), register route in [tienda/urls.py](tienda/urls.py)
- New templates: Create in `tienda/templates/tienda/`, extend `base.html`
- Static assets: Place in `tienda/static/` subdirectories (css/, img/, js/)
## Seller Panel (Panel de Vendedor)
- **URL Base**: `/tienda/venta/` - All seller-related features under this path
- **Main Page**: `/tienda/venta/` - Lists all products created by the authenticated user
- **Create Product**: `/tienda/venta/crear-producto/` - Form to create new products
- **Header Button**: "Panel Vendedor" - Links to the seller main page (mis_productos view)
- **Concept**: Centralized area for sellers to manage their products (list, create, and future: edit, delete, analytics)
## Working Pattern with Developer
- **"Lo recuerdes"**: When the developer says to remember something, document it in this file immediately
- Update this file as the source of truth for project-specific decisions and patterns
+82
View File
@@ -0,0 +1,82 @@
# Configuración de PayPal
## Pasos para configurar PayPal en el proyecto
### 1. Crear una cuenta de PayPal Developer
- Ve a https://developer.paypal.com/
- Crea una cuenta o inicia sesión con tu cuenta de PayPal
### 2. Acceder al Sandbox
- En el dashboard de PayPal Developer, ve a "Sandbox"
- Aquí encontrarás credenciales de prueba
### 3. Obtener las credenciales
- En la sección "Accounts" del Sandbox, encontrarás:
- **Business Account** (vendedor)
- **Personal Account** (comprador de prueba)
- Para cada cuenta, haz clic en "Show" junto a "API Signature" o "API Certificate"
- También puedes usar las credenciales REST API
### 4. Obtener Client ID y Client Secret (REST API)
- En el dashboard, ve a **"Apps & Credentials"**
- Selecciona **Sandbox** (arriba a la derecha)
- Ve a la pestaña **"REST API apps"**
- Haz clic en el app por defecto o crea uno nuevo
- Verás:
- **Client ID** (tu PAYPAL_CLIENT_ID)
- **Secret** (tu PAYPAL_CLIENT_SECRET)
### 5. Actualizar settings.py
Edita el archivo `proyecto/settings.py` y reemplaza:
```python
PAYPAL_CLIENT_ID = 'YOUR_PAYPAL_CLIENT_ID'
PAYPAL_CLIENT_SECRET = 'YOUR_PAYPAL_CLIENT_SECRET'
PAYPAL_MODE = 'sandbox' # Cambiar a 'live' en producción
```
Con tus valores reales del Sandbox.
### 6. Instalar el SDK de PayPal
```bash
pip install paypalrestsdk
```
### 7. Usar cuentas de prueba para transacciones
En Sandbox puedes usar las cuentas de prueba:
- **Email de vendedor**: El que aparece en el Business Account
- **Email de comprador**: El que aparece en el Personal Account
Ambas cuentas tienen saldo de prueba disponible.
## Pasar a Producción
Para usar PayPal en producción:
1. Cambia `PAYPAL_MODE = 'live'` en settings.py
2. Reemplaza `PAYPAL_CLIENT_ID` y `PAYPAL_CLIENT_SECRET` con las credenciales reales (no de Sandbox)
3. Obten tus credenciales reales en https://www.paypal.com/cgi-bin/customerprofileweb?cmd=_profile-api-signature
## Métodos de Pago Disponibles
Actualmente el proyecto soporta:
- **Stripe** 💳
- **PayPal** 🅿️
Ambos métodos están disponibles en la página de checkout.
## Notas de Seguridad
- NUNCA commits las credenciales reales en git
- Usa variables de entorno en producción
- Mantén tus claves secretas privadas
- El modo Sandbox es solo para pruebas
## Testear PayPal
1. Ve a la página de Checkout
2. Haz clic en "Pagar con PayPal"
3. Se abrirá PayPal Sandbox
4. Usa las credenciales de la cuenta de prueba (Personal Account)
5. Completa el pago simulado
6. Serás redirigido a la página de éxito
+99
View File
@@ -0,0 +1,99 @@
# Solución de Problemas - PayPal
## Si ves "Error al procesar el pago con PayPal"
### Paso 1: Abre la Consola del Navegador
1. Presiona **F12** o **Cmd+Option+I** (Mac)
2. Ve a la pestaña **Console**
### Paso 2: Busca el mensaje de error
Cuando hagas clic en "Pagar con PayPal", deberías ver en la consola:
- `CSRF Token encontrado: Sí`
- `Response status: 200`
- `Response data: {"redirect": "https://www.sandbox.paypal.com/..."}`
- `Redirigiendo a: https://www.sandbox.paypal.com/...`
### Paso 3: Interpreta los errores
#### Error: "CSRF Token encontrado: No"
**Solución:**
- El formulario no tiene token CSRF
- Asegúrate de que `{% csrf_token %}` esté en el template checkout.html
- Recarga la página (Ctrl+Shift+R)
#### Error: "Response status: 403"
**Causa:** Error de CSRF token o permisos
**Solución:**
- Verifica que estés logueado
- Limpia cookies: Settings > Clear browsing data > Cookies
- Recarga la página
#### Error: "Response status: 500"
**Causa:** Error en el servidor
**Solución:**
- Ve a la terminal donde corre Django
- Busca el mensaje de error (stack trace en rojo)
- Verifica que `PAYPAL_CLIENT_ID` y `PAYPAL_CLIENT_SECRET` estén correctos en settings.py
#### Error: "Error inesperado al procesar el pago"
**Solución:**
- Mira en la consola qué dice exactamente
- Copia el error completo
- Revisa los logs de Django en la terminal
### Paso 4: Verifica el Backend
En la terminal donde corre Django, deberías ver:
```
[00/Month/2026 12:00:00] "POST /tienda/paypal/create-payment/ HTTP/1.1" 200 123
```
Si ves un error (4xx o 5xx), el problema está en el servidor.
### Paso 5: Test Manual
Ejecuta en la terminal:
```bash
cd /home/daniel/projects/proyecto/proyecto2/proyecto
.venv/bin/python test_paypal.py
```
Si todo está bien, deberías ver:
```
✓ paypalrestsdk importado correctamente
✓ Configuración de PayPal aplicada
✓ Pago creado exitosamente
```
## Checklist de Configuración
- [ ] `pip install paypalrestsdk` (verificar con: `.venv/bin/pip list | grep paypal`)
- [ ] `PAYPAL_CLIENT_ID` en settings.py (no vacío)
- [ ] `PAYPAL_CLIENT_SECRET` en settings.py (no vacío)
- [ ] `PAYPAL_MODE = 'sandbox'` en settings.py
- [ ] `{% csrf_token %}` en checkout.html
- [ ] El usuario está autenticado (login requerido)
- [ ] El carrito tiene items
## Credenciales de Prueba
Si necesitas nuevas credenciales:
1. Ve a https://sandbox.paypal.com/
2. Login con tu cuenta
3. Ve a Dashboard > Apps & Credentials
4. Copia Client ID y Secret
5. Actualiza en settings.py
6. Recarga la página
## Logs Útiles
Para ver más detalles, edita `/tienda/views.py` y busca la función `create_paypal_payment`:
- Ya tiene `print()` para loguear errores
- Verás los mensajes en la terminal de Django
## Contacto con PayPal
Si todo lo anterior no funciona:
- El problema puede ser con las credenciales de PayPal
- Verifica que sean del **SANDBOX** (no production)
- Intenta regenerar las credenciales en paypal.com
+214
View File
@@ -0,0 +1,214 @@
# Redis Setup - Configuración de Redis para Sesiones
## Configuración Implementada
Este proyecto utiliza Redis para almacenar las sesiones de usuario, mejorando el rendimiento y la escalabilidad.
### Paquetes Instalados
- **django-redis 5.4.0**: Backend de caché Redis para Django
- **redis 5.2.1**: Cliente Python para Redis
### Configuración en settings.py
```python
# Redis Configuration
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
# Session Configuration - Use Redis for session storage
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
```
### Uso en el Proyecto
El proyecto utiliza Redis para:
1. **Sesiones de usuario**: Todas las sesiones se almacenan en Redis (base de datos 1)
2. **Cacheo de productos**: Los productos visitados se cachean por 5 minutos
- Primera visita: Se carga desde la base de datos y se cachea
- Siguientes visitas: Se sirve desde caché (mucho más rápido)
- Después de 5 minutos: Se recarga desde la BD y se vuelve a cachear
- Al editar/borrar un producto: Se invalida automáticamente su caché
## Instalación de Redis
### Linux (Ubuntu/Debian)
```bash
sudo apt update
sudo apt install redis-server
sudo systemctl start redis-server
sudo systemctl enable redis-server
```
### Linux (Arch Linux)
```bash
sudo pacman -S redis
sudo systemctl start valkey
sudo systemctl enable valkey
# En Arch, Redis se llama Valkey (fork de Redis)
# Comando CLI: valkey-cli en lugar de redis-cli
```
### macOS
```bash
brew install redis
brew services start redis
```
### Windows
Descargar desde: https://github.com/microsoftarchive/redis/releases
## Verificar que Redis está Funcionando
```bash
# Verificar estado del servicio
sudo systemctl status redis-server
# Conectarse a Redis CLI
redis-cli
# En Redis CLI, probar conexión:
ping
# Debería responder: PONG
# Ver todas las claves almacenadas
keys *
# Salir de Redis CLI
exit
```
## Probar la Configuración
```bash
# Conectarse a Redis y ver sesiones y productos cacheados
valkey-cli # o redis-cli según tu sistema
SELECT 1 # Usa la base de datos 1 (definida en LOCATION)
KEYS * # Ver todas las claves almacenadas
# Ver productos cacheados
KEYS *product*
# Ver sesiones
KEYS *session*
# Ver el contenido de una clave específica
GET <key_name>
# Ver el TTL (tiempo restante) de una clave
TTL <key_name>
```
### Ejemplo de Claves en Redis
- Productos: `:1:product_<id>` (ej: `:1:product_7`)
- Sesiones: `:1:django.contrib.sessions.cache<session_id>`
- Cache general: `:1:test_key`
## Configuración de Producción
Para producción, considera:
1. **Configurar contraseña en Redis**:
```bash
# En /etc/redis/redis.conf
requirepass tu_contraseña_segura
```
2. **Actualizar settings.py**:
```python
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://:tu_contraseña_segura@127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
```
3. **Persistencia de datos**: Redis guarda snapshots automáticamente, pero puedes configurar:
```bash
# En /etc/redis/redis.conf
save 900 1 # Guardar cada 15 min si hay al menos 1 cambio
save 300 10 # Guardar cada 5 min si hay al menos 10 cambios
save 60 10000 # Guardar cada 1 min si hay al menos 10000 cambios
```
## Ventajas de Redis para Sesiones
1. **Rendimiento**: Redis es extremadamente rápido (en memoria)
2. **Escalabilidad**: Permite múltiples servidores compartiendo sesiones
3. **Expiración automática**: Las sesiones expiran automáticamente
4. **Persistencia**: Opcionalmente puede persistir datos en disco
## Ventajas del Cacheo de Productos
1. **Velocidad**: ~15x más rápido cargar desde caché vs base de datos
2. **Reducción de carga**: Menos queries a la base de datos
3. **Mejor UX**: Páginas de producto cargan instantáneamente
4. **Auto-invalidación**: El caché se limpia automáticamente al editar/borrar productos
### Flujo de Cacheo de Productos
```
Usuario visita producto
¿Está en caché? → NO → Cargar de BD → Cachear por 5 min → Mostrar
¿Pasaron 5 min? → SÍ → Cargar de BD → Cachear por 5 min → Mostrar
NO
Servir desde caché → Mostrar
```
## Monitoreo
```bash
# Ver estadísticas en tiempo real
redis-cli --stat
# Monitor de comandos en tiempo real
redis-cli monitor
# Ver información del servidor
redis-cli INFO
```
## Limpieza de Sesiones
Las sesiones en Redis expiran automáticamente según `SESSION_COOKIE_AGE` de Django (por defecto 2 semanas).
Para limpiar manualmente todas las sesiones:
```bash
redis-cli
SELECT 1
FLUSHDB
```
## Troubleshooting
### Error: "Connection refused"
- Verificar que Redis está corriendo: `sudo systemctl status redis-server`
- Iniciar Redis: `sudo systemctl start redis-server`
### Sesiones no se guardan
- Verificar conexión a Redis: `redis-cli ping`
- Revisar logs: `sudo journalctl -u redis-server -n 50`
### Alto uso de memoria
- Ver uso de memoria: `redis-cli INFO memory`
- Configurar límite: En redis.conf agregar `maxmemory 256mb`
- Política de desalojo: `maxmemory-policy allkeys-lru`
BIN
View File
Binary file not shown.
Executable
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "proyecto.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+16
View File
@@ -0,0 +1,16 @@
"""
ASGI config for proyecto project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proyecto.settings')
application = get_asgi_application()
+187
View File
@@ -0,0 +1,187 @@
"""
Django settings for proyecto project.
Generated by 'django-admin startproject' using Django 6.0.1.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# 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'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = [
"192.168.1.142",
"localhost",
"127.0.0.1"
]
# Application definition
INSTALLED_APPS = [
'tienda.apps.TiendaConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'compressor',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'proyecto.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'tienda.context_processors.cart_context',
],
},
},
]
WSGI_APPLICATION = 'proyecto.wsgi.application'
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'tienda' / 'static',
]
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder'
]
COMPRESS_PRECOMPILERS = (
('text/x-scss', 'sass {infile} {outfile} --load-path=tienda/static/scss'),
)
import shutil
SASS_BINARY = shutil.which('sass')
if SASS_BINARY:
COMPRESS_PRECOMPILERS = (
('text/x-scss', f'{SASS_BINARY} {{infile}} {{outfile}} --load-path=tienda/static/scss'),
)
# Media files (User uploads)
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'tienda' / 'static' / 'media'
# Redis Configuration
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
# Session Configuration - Use Redis for session storage
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
# Configuración de mensajes para Bootstrap
from django.contrib.messages import constants as messages
MESSAGE_TAGS = {
messages.DEBUG: 'debug',
messages.INFO: 'info',
messages.SUCCESS: 'success',
messages.WARNING: 'warning',
messages.ERROR: 'danger',
}
# Login URL
LOGIN_URL = '/tienda/login/'
STRIPE_PUBLISHABLE_KEY = 'pk_test_51SxmSYJ2DN4I0upQDdiPeda51nmpB0ZEWfkNFKHhWBG4knIgtRoC1d9iFRoxRNdJKiLlQsIddlebU06R9XCfiSZH00ffoirwPw'
STRIPE_SECRET_KEY = 'sk_test_51SxmSYJ2DN4I0upQZb42dWKuIKToZxkQeK3vsCdijcaUr17EMEyFcLdIAm5AVEvUs96MAxl4KnZ4Yncp5VykO4ej00MZGs6c1F'
# 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
+30
View File
@@ -0,0 +1,30 @@
"""
URL configuration for proyecto project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/6.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from tienda import views as tienda_views
urlpatterns = [
path('', tienda_views.home, name='home'),
path('admin/', admin.site.urls),
path('tienda/', include('tienda.urls'))
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+16
View File
@@ -0,0 +1,16 @@
"""
WSGI config for proyecto project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proyecto.settings')
application = get_wsgi_application()
+19
View File
@@ -0,0 +1,19 @@
asgiref==3.11.0
certifi==2026.1.4
charset-normalizer==3.4.4
Django==6.0.1
django-appconf==1.2.0
django-libsass==0.9
django_compressor==4.6.0
django-redis==5.4.0
idna==3.11
libsass==0.23.0
pillow==12.1.0
rcssmin==1.2.2
redis==5.2.1
requests==2.32.5
rjsmin==1.2.5
sqlparse==0.5.5
stripe==14.3.0
typing_extensions==4.15.0
urllib3==2.6.3
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python
"""
Script para testear la configuración de PayPal
Ejecutar: python test_paypal.py
"""
import os
import sys
import django
# Configurar Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proyecto.settings')
sys.path.insert(0, os.path.dirname(__file__))
django.setup()
from django.conf import settings
print("=" * 60)
print("TEST DE CONFIGURACIÓN DE PAYPAL")
print("=" * 60)
# Verificar configuración
print("\n1. Verificando configuración en settings.py:")
print(f" PAYPAL_MODE: {settings.PAYPAL_MODE}")
print(f" PAYPAL_CLIENT_ID: {settings.PAYPAL_CLIENT_ID[:20]}..." if settings.PAYPAL_CLIENT_ID else " ❌ NO CONFIGURADO")
print(f" PAYPAL_CLIENT_SECRET: {settings.PAYPAL_CLIENT_SECRET[:20]}..." if settings.PAYPAL_CLIENT_SECRET else " ❌ NO CONFIGURADO")
# Intentar importar paypalrestsdk
print("\n2. Verificando SDK de PayPal:")
try:
import paypalrestsdk
print(" ✓ paypalrestsdk importado correctamente")
print(f" Versión: {paypalrestsdk.__version__ if hasattr(paypalrestsdk, '__version__') else 'Desconocida'}")
except ImportError as e:
print(f" ❌ Error: {e}")
print(" SOLUCIÓN: pip install paypalrestsdk")
sys.exit(1)
# Intentar conectar a PayPal
print("\n3. Probando conexión a PayPal:")
try:
paypalrestsdk.configure({
"mode": settings.PAYPAL_MODE,
"client_id": settings.PAYPAL_CLIENT_ID,
"client_secret": settings.PAYPAL_CLIENT_SECRET
})
print(" ✓ Configuración de PayPal aplicada")
# Intentar crear un pago de prueba
print("\n4. Creando pago de prueba:")
test_payment = paypalrestsdk.Payment({
"intent": "sale",
"payer": {
"payment_method": "paypal"
},
"redirect_urls": {
"return_url": "http://localhost:8000/test-return",
"cancel_url": "http://localhost:8000/test-cancel"
},
"transactions": [
{
"amount": {
"total": "10.00",
"currency": "EUR",
"details": {
"subtotal": "10.00",
"tax": "0",
"shipping": "0"
}
},
"description": "Pago de prueba",
"item_list": {
"items": [
{
"name": "Test Item",
"sku": "test_1",
"price": "10.00",
"currency": "EUR",
"quantity": 1
}
]
}
}
]
})
if test_payment.create():
print(" ✓ Pago creado exitosamente")
print(f" Payment ID: {test_payment.id}")
for link in test_payment.links:
if link.rel == "approval_url":
print(f" URL de aprobación: {link.href}")
else:
print(" ❌ Error al crear el pago:")
if hasattr(test_payment, 'error') and test_payment.error:
print(f" {test_payment.error}")
except Exception as e:
print(f" ❌ Error de conexión: {e}")
import traceback
traceback.print_exc()
print("\n" + "=" * 60)
print("TEST COMPLETADO")
print("=" * 60)
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python
"""
Script de prueba para el cacheo de productos en Redis
Ejecutar: python test_product_cache.py
"""
import os
import django
# Configurar Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proyecto.settings')
django.setup()
from tienda.models import Product
from django.core.cache import cache
import time
def test_product_cache():
"""Prueba el sistema de cacheo de productos"""
print("=" * 60)
print("TEST: Sistema de Cacheo de Productos en Redis")
print("=" * 60)
# Obtener un producto de prueba
try:
product = Product.objects.first()
if not product:
print("❌ No hay productos en la base de datos para probar")
return
product_id = product.id
cache_key = f'product_{product_id}'
print(f"\n📦 Producto de prueba: {product.name} (ID: {product_id})")
# 1. Limpiar caché del producto
cache.delete(cache_key)
print(f"\n1️⃣ Caché limpiado")
# 2. Primera visita (debe cargar desde BD)
print(f"\n2️⃣ Primera visita - Cargando desde BD...")
start_time = time.time()
cached_product = cache.get(cache_key)
if cached_product is None:
print(" ✅ No está en caché (esperado)")
product_from_db = Product.objects.select_related('category', 'primary_image', 'creator').prefetch_related('secondary_images').get(id=product_id)
cache.set(cache_key, product_from_db, 300)
print(f" ✅ Producto cacheado por 5 minutos")
db_time = (time.time() - start_time) * 1000
# 3. Segunda visita (debe cargar desde caché)
print(f"\n3️⃣ Segunda visita - Cargando desde caché...")
start_time = time.time()
cached_product = cache.get(cache_key)
if cached_product:
print(f" ✅ Encontrado en caché: {cached_product.name}")
cache_time = (time.time() - start_time) * 1000
# 4. Comparar tiempos
print(f"\n⏱️ Comparación de rendimiento:")
print(f" - Desde BD: {db_time:.2f}ms")
print(f" - Desde caché: {cache_time:.2f}ms")
speedup = db_time / cache_time if cache_time > 0 else float('inf')
print(f" - Mejora: {speedup:.1f}x más rápido")
# 5. Verificar TTL
ttl = cache.ttl(cache_key)
print(f"\n⏳ TTL (tiempo de vida): {ttl} segundos (~5 minutos)")
# 6. Verificar en Redis
print(f"\n🔍 Verificación en Redis:")
print(f" - Clave: {cache_key}")
print(f" - Base de datos: 1")
print(f" - Comando para ver: valkey-cli -n 1 GET ':1:{cache_key}'")
print("\n" + "=" * 60)
print("✅ TEST COMPLETADO EXITOSAMENTE")
print("=" * 60)
except Exception as e:
print(f"❌ Error durante el test: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
test_product_cache()
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+70
View File
@@ -0,0 +1,70 @@
from django.contrib import admin
from .models import Category, Image, Product, Cart, CartItem, Order, OrderItem, OrderMessage
# Register your models here.
admin.site.register(Category)
admin.site.register(Image)
admin.site.register(Product)
class CartItemInline(admin.TabularInline):
model = CartItem
extra = 0
@admin.register(Cart)
class CartAdmin(admin.ModelAdmin):
list_display = ('id', 'user', 'session_key', 'get_items_count', 'get_total', 'created_at', 'updated_at')
list_filter = ('created_at', 'updated_at')
search_fields = ('user__username', 'user__email', 'session_key')
inlines = [CartItemInline]
def get_items_count(self, obj):
return obj.get_items_count()
get_items_count.short_description = 'Productos'
def get_total(self, obj):
return f"{obj.get_total()}"
get_total.short_description = 'Total'
@admin.register(CartItem)
class CartItemAdmin(admin.ModelAdmin):
list_display = ('id', 'cart', 'product', 'quantity', 'get_subtotal', 'added_at')
list_filter = ('added_at',)
search_fields = ('product__name', 'cart__user__username')
def get_subtotal(self, obj):
return f"{obj.get_subtotal()}"
get_subtotal.short_description = 'Subtotal'
class OrderItemInline(admin.TabularInline):
model = OrderItem
extra = 0
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ('id', 'buyer', 'total', 'status', 'payment_method', 'payment_reference', 'created_at')
list_filter = ('status', 'payment_method', 'created_at')
search_fields = ('buyer__username', 'buyer__email', 'payment_reference')
inlines = [OrderItemInline]
@admin.register(OrderItem)
class OrderItemAdmin(admin.ModelAdmin):
list_display = ('id', 'order', 'product_name', 'seller', 'quantity', 'total_price', 'status', 'created_at')
list_filter = ('status', 'created_at')
search_fields = ('product_name', 'seller__username', 'order__buyer__username')
@admin.register(OrderMessage)
class OrderMessageAdmin(admin.ModelAdmin):
list_display = ('id', 'order_item', 'sender', 'message_preview', 'created_at')
list_filter = ('created_at',)
search_fields = ('sender__username', 'message', 'order_item__product_name')
def message_preview(self, obj):
return obj.message[:50] + "..." if len(obj.message) > 50 else obj.message
message_preview.short_description = 'Mensaje'
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class TiendaConfig(AppConfig):
name = 'tienda'
+22
View File
@@ -0,0 +1,22 @@
from .models import Cart
def cart_context(request):
"""Context processor para hacer el carrito disponible en todas las plantillas"""
cart_count = 0
if request.user.is_authenticated:
try:
cart = Cart.objects.get(user=request.user)
cart_count = cart.get_items_count()
except Cart.DoesNotExist:
cart_count = 0
elif request.session.session_key:
try:
cart = Cart.objects.get(session_key=request.session.session_key)
cart_count = cart.get_items_count()
except Cart.DoesNotExist:
cart_count = 0
return {
'cart_count': cart_count
}
+26
View File
@@ -0,0 +1,26 @@
# Generated by Django 6.0.1 on 2026-01-23 09:33
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Category',
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')),
],
options={
'verbose_name_plural': 'Categories',
},
),
]
@@ -0,0 +1,21 @@
# 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',
),
]
+21
View File
@@ -0,0 +1,21 @@
# 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='')),
],
),
]
@@ -0,0 +1,18 @@
# 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
@@ -0,0 +1,25 @@
# 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')),
],
),
]
@@ -0,0 +1,18 @@
# 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'),
),
]
@@ -0,0 +1,18 @@
# 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
@@ -0,0 +1,39 @@
# 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
@@ -0,0 +1,21 @@
# 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
@@ -0,0 +1,45 @@
# 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
@@ -0,0 +1,29 @@
# 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'],
},
),
]
View File
+185
View File
@@ -0,0 +1,185 @@
from django.db import models
from django.contrib.auth.models import User
from .vars import VAT_RATE
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=200)
def __str__(self):
return self.name
class Image(models.Model):
name = models.CharField(max_length=200, default="")
image = models.ImageField(upload_to='images/')
def __str__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length=200, default="")
description = models.TextField(default = "")
briefdesc = models.TextField(default = "")
price = models.FloatField(default = 0)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
primary_image = models.ForeignKey(Image, on_delete=models.SET_NULL, null=True)
secondary_images = models.ManyToManyField(Image, related_name='products_secondary', blank=True)
creator = models.ForeignKey(User, on_delete=models.CASCADE, related_name='created_products', null=True, blank=True)
def __str__(self):
return self.name + " " + str(self.price)
def get_price_with_vat(self):
"""Retorna el precio con IVA incluido"""
return round(self.price * (1 + VAT_RATE), 2)
def get_vat_amount(self):
"""Retorna la cantidad de IVA"""
return round(self.price * VAT_RATE, 2)
class Cart(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
session_key = models.CharField(max_length=40, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Cart {self.id} - {self.user or self.session_key}"
def get_total(self):
return sum(item.get_subtotal() for item in self.items.all())
def get_total_with_vat(self):
"""Retorna el total del carrito con IVA incluido"""
return round(self.get_total() * (1 + VAT_RATE), 2)
def get_vat_amount(self):
"""Retorna la cantidad total de IVA"""
return round(self.get_total() * VAT_RATE, 2)
def get_items_count(self):
return sum(item.quantity for item in self.items.all())
class CartItem(models.Model):
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items')
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(default=1)
added_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('cart', 'product')
def __str__(self):
return f"{self.quantity}x {self.product.name}"
def get_subtotal(self):
return self.product.price * self.quantity
def get_subtotal_with_vat(self):
"""Retorna el subtotal del item con IVA incluido"""
return round(self.get_subtotal() * (1 + VAT_RATE), 2)
def get_vat_amount(self):
"""Retorna la cantidad de IVA de este item"""
return round(self.get_subtotal() * VAT_RATE, 2)
class Order(models.Model):
STATUS_PAID = "paid"
STATUS_CANCELLED = "cancelled"
STATUS_CHOICES = [
(STATUS_PAID, "Pagado"),
(STATUS_CANCELLED, "Cancelado"),
]
PAYMENT_STRIPE = "stripe"
PAYMENT_PAYPAL = "paypal"
PAYMENT_MANUAL = "manual"
PAYMENT_CHOICES = [
(PAYMENT_STRIPE, "Stripe"),
(PAYMENT_PAYPAL, "PayPal"),
(PAYMENT_MANUAL, "Manual"),
]
buyer = models.ForeignKey(User, 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)
payment_method = models.CharField(max_length=20, choices=PAYMENT_CHOICES, default=PAYMENT_MANUAL)
payment_reference = models.CharField(max_length=200, blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Pedido {self.id} - {self.buyer or self.session_key}"
def get_items_count(self):
return sum(item.quantity for item in self.items.all())
class OrderItem(models.Model):
STATUS_PENDING = "pending"
STATUS_PROCESSING = "processing"
STATUS_SHIPPED = "shipped"
STATUS_CHOICES = [
(STATUS_PENDING, "Pendiente"),
(STATUS_PROCESSING, "En preparación"),
(STATUS_SHIPPED, "Enviado"),
]
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True, blank=True)
product_name = models.CharField(max_length=200, default="")
seller = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='order_items_to_fulfill')
quantity = models.PositiveIntegerField(default=1)
unit_price = models.FloatField(default=0)
total_price = models.FloatField(default=0)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PENDING)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.quantity}x {self.product_name} (Pedido {self.order_id})"
class OrderMessage(models.Model):
order_item = models.ForeignKey(OrderItem, on_delete=models.CASCADE, related_name='messages')
sender = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='sent_messages')
message = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['created_at']
def __str__(self):
return f"Mensaje de {self.sender} - {self.created_at}"
class ShippingAddress(models.Model):
"""Direcciones de entrega de los usuarios"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='shipping_addresses')
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(max_length=250, blank=True, 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(max_length=100, default="España", 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)
class Meta:
verbose_name = "Dirección de envío"
verbose_name_plural = "Direcciones de envío"
ordering = ['-is_default', '-created_at']
def __str__(self):
return f"{self.full_name} - {self.city}"
def save(self, *args, **kwargs):
# Si se marca como predeterminada, desmarcar las demás del usuario
if self.is_default:
ShippingAddress.objects.filter(user=self.user, is_default=True).update(is_default=False)
super().save(*args, **kwargs)
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 858 KiB

+92
View File
@@ -0,0 +1,92 @@
const getCookie = (name) => {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + "=")) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
};
document.addEventListener("DOMContentLoaded", () => {
const button = document.getElementById("checkout-button");
console.log("Button found:", button);
if (!button) {
console.error("Checkout button not found");
return;
}
const configUrl = button.dataset.configUrl;
const sessionUrl = button.dataset.sessionUrl;
console.log("Config URL:", configUrl);
console.log("Session URL:", sessionUrl);
fetch(configUrl)
.then((result) => {
console.log("Config response status:", result.status);
return result.json();
})
.then((data) => {
console.log("Config data:", data);
if (!data.publicKey) {
console.error("No publicKey in response");
return;
}
const stripe = Stripe(data.publicKey);
console.log("Stripe initialized");
button.addEventListener("click", () => {
console.log("Checkout button clicked");
button.disabled = true;
button.innerHTML = "Procesando...";
fetch(sessionUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": getCookie("csrftoken")
},
body: JSON.stringify({})
})
.then((res) => {
console.log("Session response status:", res.status);
return res.json();
})
.then((data) => {
console.log("Session data:", data);
if (data.sessionId) {
console.log("Redirecting to Stripe Checkout with session:", data.sessionId);
return stripe.redirectToCheckout({ sessionId: data.sessionId });
} else if (data.error) {
alert("Error: " + data.error);
button.disabled = false;
button.innerHTML = "Pagar con Stripe";
} else {
alert("Error desconocido al procesar el pago");
button.disabled = false;
button.innerHTML = "Pagar con Stripe";
}
})
.catch((error) => {
console.error("Fetch error:", error);
alert("Error de conexión: " + error.message);
button.disabled = false;
button.innerHTML = "Pagar con Stripe";
});
});
})
.catch((error) => {
console.error("Config fetch error:", error);
alert("Error al cargar la configuración de pago: " + error.message);
});
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 985 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Some files were not shown because too many files have changed in this diff Show More