🎯 Objetivo del Proyecto
Crear desde CERO un Sistema Web Completo de Gestión de Tienda con Django y MySQL, que incluye:
- ✅ Autenticación segura con sesiones de Django
- ✅ CRUD completo para 5 módulos (Productos, Categorías, Proveedores, Clientes, Ventas)
- ✅ Relaciones con llaves foráneas (combobox de categorías en productos)
- ✅ Sistema de permisos por roles (Vendedor, Gerente, Administrador)
- ✅ Módulo de ventas completo con reporte diario y formato de millares
- ✅ Dashboard con estadísticas y ventas del día
- ✅ Diseño moderno con Bootstrap 5
- ✅ Código 100% documentado para su comprensión
Tiempo estimado: 3-4 horas completas | Complejidad: Intermedio
📚 Lo que aprenderás:
- Patrón MVT (Model-View-Template) de Django
- ORM para gestionar base de datos sin SQL directo
- Sistema de autenticación y permisos
- Relaciones entre modelos (ForeignKey)
- Formularios dinámicos con validación
- Templates con herencia
📋 Requisitos Previos
Software Necesario
- Python 3.8+ (Recomendado: Python 3.13)
- MySQL Server 8.0+ (o WAMP/XAMPP con MySQL)
- Editor de código (VS Code, PyCharm, Sublime Text)
- Navegador web moderno (Chrome, Firefox, Edge, Opera, etc.)
Verificar Instalaciones
# Verificar Python
python --version
# Debe mostrar: Python 3.x.x
# Verificar pip
pip --version
# Verificar MySQL
mysql --version
Conocimientos Previos
- ✓ Conocimientos básicos de Python
- ✓ Conceptos básicos de HTML/CSS
- ✓ Familiaridad con línea de comandos
- ✓ Conceptos básicos de bases de datos
📂 Estructura del Proyecto
El proyecto completo tiene la siguiente estructura:
practica_u3_examen/
│
├── 📁 tienda_proyecto/ # Configuración principal de Django
│ ├── settings.py # Configuración (BD, apps instaladas, timezone UTC, humanize)
│ ├── urls.py # URLs principales del proyecto
│ ├── wsgi.py # Configuración para deployment
│ └── __init__.py
│
├── 📁 tienda/ # Aplicación principal
│ ├── models.py # 6 modelos (PerfilUsuario, Categoria, Producto, Proveedor, Cliente, Venta)
│ ├── views.py # 28 vistas (login, CRUD completo, ventas, reporte)
│ ├── forms.py # 5 formularios con validación (incluye VentaForm)
│ ├── urls.py # 24 rutas URL (incluye rutas de ventas)
│ ├── admin.py # Configuración del panel admin
│ ├── 📁 templates/tienda/ # 17 plantillas HTML
│ │ ├── base.html # Template base
│ │ ├── login.html # Página de login
│ │ ├── home.html # Dashboard con estadísticas
│ │ ├── producto_*.html # Templates de productos (lista, form, eliminar)
│ │ ├── categoria_*.html # Templates de categorías (lista, form, eliminar)
│ │ ├── proveedor_*.html # Templates de proveedores (lista, form, eliminar)
│ │ ├── cliente_*.html # Templates de clientes (lista, form, eliminar)
│ │ ├── venta_form.html # Formulario para registrar ventas
│ │ └── reporte_ventas.html # Reporte de ventas del día con millares
│ └── 📁 migrations/ # Migraciones de BD (incluye migración de Venta)
│
├── 📁 static/css/ # Archivos estáticos
│ └── styles.css # CSS personalizado
│
├── 📄 manage.py # Script principal de Django
├── 📄 crear_bd.sql # Script para crear BD en MySQL
├── 📄 cargar_datos_ejemplo.py # Script para cargar datos de prueba
├── 📄 cargar_ventas_ejemplo.py # Script para cargar ventas de ejemplo
├── 📄 crear_usuarios_con_roles.py # Script para crear usuarios con roles
├── 📄 actualizar_passwords.py # Script para actualizar contraseñas
├── 📄 actualizar_ventas.py # Script para actualizar precio unitario en ventas
├── 📄 verificar_ventas.py # Script para verificar ventas en BD
├── 📄 ver_usuarios.py # Script para ver usuarios y roles
├── 📄 test_filtro_fecha.py # Script para probar filtros de fecha
├── 📄 ejecutar_proyecto.bat # Ejecutar proyecto en Windows
└── 📄 GUIA_COMPLETA_PROYECTO.md # Documentación completa del proyecto
PARTE 1: Preparación del Entorno
Esta sección cubre la instalación y configuración inicial de Python, el entorno virtual, y las librerías necesarias (Django, mysqlclient, bootstrap5, crispy-forms).
Paso 1.1: Instalar Python
- Descarga Python 3.8 o superior desde https://www.python.org/downloads/
- Durante la instalación:
- Marca "Add Python to PATH"
- Instala para todos los usuarios
- Verifica la instalación abriendo CMD o PowerShell:
# Comando de Python para verificar la instalación y versión.
python --version
Respuesta esperada: Python 3.x.x
Paso 1.2: Crear y Activar Entorno Virtual
- Navega a la carpeta de tu proyecto.
- Crea el entorno virtual (venv):
# Comando de Python para crear un entorno virtual llamado 'venv'.
python -m venv venv
- Activa el entorno virtual:
- Windows (CMD):
venv\Scripts\activate - Windows (PowerShell):
.\venv\Scripts\Activate.ps1 - Linux/macOS:
source venv/bin/activate
- Windows (CMD):
Observación: Verás (venv) al inicio de tu línea de comandos.
Paso 1.3: Instalar Dependencias de Python
Instala las librerías necesarias para el proyecto:
# Instala Django (Framework web)
pip install django
# Instala el conector de MySQL para Django
pip install mysqlclient
# NOTA: Si mysqlclient da error en Windows:
# 1. Descarga el archivo .whl desde: https://www.lfd.uci.edu/~gohlke/pythonlibs/#mysqlclient
# 2. Instala con: pip install nombre_archivo.whl
⚠️ Problema común en Windows con mysqlclient
Si obtienes error al instalar mysqlclient, sigue estos pasos:
- Ve a: https://www.lfd.uci.edu/~gohlke/pythonlibs/#mysqlclient
- Descarga el archivo .whl correcto para tu versión de Python
- Ejemplo: mysqlclient-2.1.1-cp313-cp313-win_amd64.whl (para Python 3.13 de 64 bits)
- Instala:
pip install mysqlclient-2.1.1-cp313-cp313-win_amd64.whl
Paso 1.4: Verificar Instalación
# Listar paquetes instalados
pip list
# Deberías ver:
# Django 5.x.x
# mysqlclient 2.x.x
PARTE 2: Crear Proyecto Django
Aquí se establecen la estructura básica de Django, el proyecto principal y la aplicación tienda, y se registran las dependencias en settings.py.
Paso 2.1: Crear Proyecto
Crea el proyecto principal (por ejemplo, sistema_tienda):
# Comando de Django para crear el proyecto 'sistema_tienda' en el directorio actual (por el punto final '.').
django-admin startproject sistema_tienda .
(El . es importante para crearlo en la carpeta actual)
Paso 2.2: Crear Aplicación (App)
Crea la aplicación para la lógica de la tienda:
# Comando de Django para crear la aplicación 'tienda' dentro del proyecto.
python manage.py startapp tienda
Paso 2.3: Registrar la Aplicación
Abre tienda_proyecto/settings.py y agrega tienda a INSTALLED_APPS:
# tienda_proyecto/settings.py
INSTALLED_APPS = [
'django.contrib.admin', # Panel de administración de Django
'django.contrib.auth', # Sistema de autenticación
'django.contrib.contenttypes', # Framework de tipos de contenido
'django.contrib.sessions', # Manejo de sesiones
'django.contrib.messages', # Sistema de mensajes
'django.contrib.staticfiles', # Archivos estáticos (CSS, JS, imágenes)
'tienda', # 👈 NUESTRA APLICACIÓN
]
Paso 2.4: Configurar Rutas de Login/Logout
Al final de settings.py, agrega:
# tienda_proyecto/settings.py (al final del archivo)
# URL a la que redirige si el usuario no está autenticado
LOGIN_URL = 'login'
# URL a la que redirige después de login exitoso
LOGIN_REDIRECT_URL = 'home'
# URL a la que redirige después de logout
LOGOUT_REDIRECT_URL = 'login'
PARTE 3: Configurar Base de Datos
Esta sección se centra en la conexión del proyecto Django a MySQL y la preparación para las migraciones iniciales.
Paso 3.1: Crear Base de Datos en MySQL
Opción A: Usando el script SQL incluido
El proyecto incluye el archivo crear_bd.sql. Ejecútalo desde la línea de comandos:
# Desde CMD o PowerShell (ajusta la ruta si es necesario)
mysql -u root -p < crear_bd.sql
# Te pedirá la contraseña de MySQL root
Opción B: Manualmente desde MySQL Workbench o phpMyAdmin
-- Ejecuta este comando SQL:
CREATE DATABASE IF NOT EXISTS tienda_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
Opción C: Desde línea de comandos MySQL
# Conectar a MySQL
mysql -u root -p
# Una vez dentro de MySQL:
CREATE DATABASE tienda_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE tienda_db;
SHOW DATABASES;
EXIT;
Paso 3.2: Configurar Conexión en Django
En tienda_proyecto/settings.py, busca la sección DATABASES (aproximadamente línea 75) y reemplázala:
# sistema_tienda/settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # Especifica el motor de base de datos MySQL.
'NAME': 'tienda_db', # Nombre de tu DB en MySQL.
'USER': 'root', # Tu usuario de MySQL.
'PASSWORD': 'tu_password', # Tu contraseña de MySQL.
'HOST': '127.0.0.1', # Dirección del servidor de MySQL (localhost).
'PORT': '3306', # Puerto predeterminado de MySQL.
'OPTIONS': {
'charset': 'utf8mb4',
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'; SET innodb_strict_mode=1;",
},
'TEST': {
'CHARSET': 'utf8mb4',
'COLLATION': 'utf8mb4_general_ci',
}
}
}
Paso 3.2: Configuración de Zona Horaria y Lenguaje
Asegura que tu configuración de idioma y zona horaria sea correcta en settings.py:
# sistema_tienda/settings.py
LANGUAGE_CODE = 'es-mx' # Establece el código de idioma a español de México.
TIME_ZONE = 'America/Hermosillo' # Establece la zona horaria (ejemplo: Hermosillo).
USE_I18N = True # Activa el soporte para internacionalización.
USE_TZ = True # Activa el soporte para zonas horarias.
⚠️ MUY IMPORTANTE: Configuración de MySQL para InnoDB
Si usas MySQL 8.0+ y obtienes errores de "Specified key was too long" o problemas con índices, ejecuta estos comandos en MySQL:
-- En MySQL Workbench, phpMyAdmin o línea de comandos MySQL:
SET GLOBAL innodb_default_row_format='dynamic';
SET GLOBAL innodb_file_format='Barracuda';
SET GLOBAL innodb_large_prefix=ON;
-- Estos comandos permiten:
-- 1. Almacenar campos largos (varchar, text) sin errores
-- 2. Usar índices más largos (necesario para email único)
-- 3. Evitar el error 1071: "Specified key was too long"
Paso 3.4: Verificar Conexión a MySQL
Antes de continuar, verifica que Django pueda conectarse a MySQL:
# Prueba la conexión (esto debería ejecutarse sin errores)
python manage.py check
# Salida esperada:
# System check identified no issues (0 silenced).
✅ Si todo está bien
Si no hay errores, la configuración de MySQL es correcta. Continúa al siguiente paso.
❌ Si hay error "Access denied"
- Verifica el usuario y contraseña en
settings.py - Asegúrate de que MySQL esté corriendo
- Verifica que la base de datos
tienda_dbexista
PARTE 4: Crear Modelos
La arquitectura de la información se define a través de 5 modelos clave que representan las entidades del negocio. Estos se definen en tienda/models.py y establecen las relaciones de la base de datos (Categoría, Proveedor, Producto, Cliente, Venta).
Modelos de Base de Datos (tienda/models.py)
# tienda/models.py
from django.db import models # Importa el módulo base de modelos de Django.
from django.contrib.auth.models import User # Importa el modelo de Usuario predeterminado de Django.
# ============ MODELO PERFIL DE USUARIO ============
class PerfilUsuario(models.Model):
ROLES = (
('vendedor', 'Vendedor'),
('gerente', 'Gerente'),
('administrador', 'Administrador'),
)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='perfil')
rol = models.CharField(max_length=20, choices=ROLES, default='vendedor')
telefono = models.CharField(max_length=15, blank=True, null=True)
departamento = models.CharField(max_length=100, blank=True, null=True)
fecha_contratacion = models.DateField(auto_now_add=True)
activo = models.BooleanField(default=True)
def __str__(self):
return f"{self.user.username} - {self.get_rol_display()}"
class Meta:
verbose_name = "Perfil de Usuario"
verbose_name_plural = "Perfiles de Usuario"
# Métodos para verificar roles
def es_vendedor(self):
return self.rol == 'vendedor'
def es_gerente(self):
return self.rol == 'gerente'
def es_administrador(self):
return self.rol == 'administrador'
def tiene_permiso_lectura(self):
# Todos pueden leer
return True
def tiene_permiso_escritura(self):
# Gerente y Administrador pueden escribir
return self.rol in ['gerente', 'administrador']
def tiene_permiso_eliminacion(self):
# Solo Administrador puede eliminar
return self.rol == 'administrador'
# ============ MODELO CATEGORÍA ============
class Categoria(models.Model):
nombre = models.CharField(max_length=100)
descripcion = models.TextField(blank=True, null=True)
fecha_creacion = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.nombre
class Meta:
verbose_name = "Categoría"
verbose_name_plural = "Categorías"
ordering = ['nombre']
# Modelo 2: Proveedor
class Proveedor(models.Model):
nombre = models.CharField(max_length=150) # Nombre del proveedor.
contacto = models.CharField(max_length=100, blank=True) # Nombre del contacto, opcional.
telefono = models.CharField(max_length=15, blank=True) # Número de teléfono, opcional.
def __str__(self):
return self.nombre # Representación en string del objeto.
# Modelo 3: Producto
class Producto(models.Model):
nombre = models.CharField(max_length=200)
descripcion = models.TextField()
precio_venta = models.DecimalField(max_digits=10, decimal_places=2) # Campo decimal para el precio (máx. 10 dígitos, 2 decimales).
stock = models.IntegerField(default=0) # Cantidad en inventario, valor predeterminado 0.
categoria = models.ForeignKey(Categoria, on_delete=models.CASCADE) # Relación uno a muchos con Categoria (si se borra la categoría, se borran los productos).
proveedor = models.ForeignKey(Proveedor, on_delete=models.SET_NULL, null=True, blank=True) # Relación con Proveedor (opcional, si se borra el proveedor, se establece a NULL).
creado_por = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='productos_creados') # Usuario que creó el producto (opcional, si se borra el usuario, se establece a NULL).
fecha_creacion = models.DateTimeField(auto_now_add=True) # Fecha y hora de creación (se establece automáticamente al crear).
activo = models.BooleanField(default=True) # Campo booleano para eliminación lógica (determina si está activo o desactivado).
def __str__(self):
return self.nombre # Representación en string del objeto.
# Modelo 4: Cliente
class Cliente(models.Model):
nombre = models.CharField(max_length=100) # Nombre del cliente
apellido = models.CharField(max_length=100) # Apellido del cliente
email = models.EmailField(max_length=191, unique=True) # Email único (no puede repetirse) - max 191 para MySQL utf8mb4
telefono = models.CharField(max_length=15) # Teléfono del cliente
direccion = models.TextField() # Dirección de entrega
fecha_registro = models.DateTimeField(auto_now_add=True) # Fecha de registro automática
def __str__(self):
return f"{self.nombre} {self.apellido}" # Muestra nombre completo
# Propiedad que concatena nombre completo
@property
def nombre_completo(self):
return f"{self.nombre} {self.apellido}"
class Meta:
verbose_name = "Cliente"
verbose_name_plural = "Clientes"
ordering = ['apellido', 'nombre'] # Ordena por apellido y luego por nombre
# ==================================================================
# MODELO 6: VENTA (Sistema de Ventas Completo)
# ==================================================================
class Venta(models.Model):
cliente = models.ForeignKey(Cliente, on_delete=models.CASCADE, related_name='ventas') # Cliente que compra (FK a Cliente)
vendedor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='ventas_realizadas') # Usuario vendedor (FK a User)
producto = models.ForeignKey(Producto, on_delete=models.CASCADE, related_name='ventas') # Producto vendido (FK a Producto)
cantidad = models.IntegerField(default=1) # Cantidad de productos vendidos
precio_unitario = models.DecimalField(max_digits=10, decimal_places=2) # Precio por unidad al momento de la venta
total = models.DecimalField(max_digits=10, decimal_places=2) # Total de la venta (cantidad × precio_unitario)
fecha_venta = models.DateTimeField(auto_now_add=True) # Fecha y hora de la venta (automático)
def __str__(self):
return f"Venta #{self.id} - {self.producto.nombre} - ${self.total}" # Representación en string
def save(self, *args, **kwargs): # Método personalizado para guardar
# Calcular el total automáticamente antes de guardar
self.total = self.cantidad * self.precio_unitario # Total = cantidad × precio unitario
super().save(*args, **kwargs) # Llamar al método save original
class Meta:
verbose_name = "Venta"
verbose_name_plural = "Ventas"
ordering = ['-fecha_venta'] # Orden descendente por fecha (más recientes primero)
Paso 4.1: Migrar Modelos
Aplica los cambios a la base de datos:
# Crea los archivos de migración específicos para la app 'tienda' (genera el código SQL).
python manage.py makemigrations tienda
# Aplica las nuevas migraciones a la base de datos (ejecuta el código SQL).
python manage.py migrate
Paso 4.2: Verificar Tablas Creadas
Verifica en MySQL que las tablas se crearon correctamente:
-- Desde MySQL Workbench, phpMyAdmin o línea de comandos:
USE tienda_db;
SHOW TABLES;
-- Deberías ver algo como:
-- +-----------------------------+
-- | Tables_in_tienda_db |
-- +-----------------------------+
-- | auth_group |
-- | auth_user |
-- | django_session |
-- | tienda_categoria |
-- | tienda_cliente |
-- | tienda_perfilusuario |
-- | tienda_producto |
-- | tienda_proveedor |
-- +-----------------------------+
📊 Tablas Creadas Automáticamente
- tienda_categoria: Almacena categorías de productos
- tienda_producto: Almacena productos (con FK a categoría)
- tienda_proveedor: Almacena proveedores
- tienda_cliente: Almacena clientes
- tienda_perfilusuario: Perfiles con roles (Vendedor, Gerente, Admin)
- auth_user: Tabla de usuarios de Django
- django_session: Sesiones de usuarios
Paso 4.3 (Opcional): Convertir Tablas a InnoDB DYNAMIC
Si tienes problemas con índices o campos largos, ejecuta en MySQL:
-- Cambia el motor y formato de fila (SOLO si es necesario)
ALTER TABLE tienda_categoria ENGINE=InnoDB ROW_FORMAT=DYNAMIC;
ALTER TABLE tienda_cliente ENGINE=InnoDB ROW_FORMAT=DYNAMIC;
ALTER TABLE tienda_producto ENGINE=InnoDB ROW_FORMAT=DYNAMIC;
ALTER TABLE tienda_proveedor ENGINE=InnoDB ROW_FORMAT=DYNAMIC;
ALTER TABLE tienda_perfilusuario ENGINE=InnoDB ROW_FORMAT=DYNAMIC;
PARTE 5: Crear Formularios
Los formularios (tienda/forms.py) se definen para facilitar el ingreso y la edición de datos para cada modelo, utilizando django-crispy-forms para un diseño limpio con Bootstrap 5.
Formularios de Tienda (tienda/forms.py)
# tienda/forms.py
# Importamos forms de Django para crear formularios
from django import forms
from .models import Producto, Categoria, Proveedor, Cliente # Importamos nuestros modelos
# ============ FORMULARIO PARA PRODUCTOS ============
class ProductoForm(forms.ModelForm):
"""Formulario para crear y editar productos"""
# Meta clase define la configuración del formulario
class Meta:
model = Producto # El modelo que usará este formulario
fields = ['nombre', 'descripcion', 'precio', 'stock', 'categoria', 'activo'] # Campos que aparecerán en el formulario
# Widgets: personalización de cómo se muestran los campos en HTML
widgets = {
'nombre': forms.TextInput(attrs={
'class': 'form-control', # Clase de Bootstrap para estilos
'placeholder': 'Ingrese el nombre del producto' # Texto de ayuda en el campo
}),
'descripcion': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3, # Altura del textarea en filas
'placeholder': 'Ingrese una descripción del producto'
}),
'precio': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01', # Permite decimales de 2 dígitos
'min': '0', # Valor mínimo
'placeholder': '0.00'
}),
'stock': forms.NumberInput(attrs={
'class': 'form-control',
'min': '0',
'placeholder': '0'
}),
# Select para la categoría (combobox con las opciones de categorías)
'categoria': forms.Select(attrs={
'class': 'form-control'
}),
# Checkbox para el campo activo
'activo': forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
}
# Labels: etiquetas personalizadas para cada campo
labels = {
'nombre': 'Nombre del Producto',
'descripcion': 'Descripción',
'precio': 'Precio ($)',
'stock': 'Cantidad en Stock',
'categoria': 'Categoría',
'activo': '¿Producto Activo?',
}
# ============ FORMULARIO PARA CATEGORÍAS ============
class CategoriaForm(forms.ModelForm):
"""Formulario para crear y editar categorías"""
class Meta:
model = Categoria
fields = ['nombre', 'descripcion'] # Solo nombre y descripción
widgets = {
'nombre': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Ingrese el nombre de la categoría'
}),
'descripcion': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Ingrese una descripción (opcional)'
}),
}
labels = {
'nombre': 'Nombre de la Categoría',
'descripcion': 'Descripción',
}
# ============ FORMULARIO PARA PROVEEDORES ============
class ProveedorForm(forms.ModelForm):
"""Formulario para crear y editar proveedores"""
class Meta:
model = Proveedor
fields = ['nombre', 'empresa', 'telefono', 'email', 'direccion']
widgets = {
'nombre': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Nombre del contacto'
}),
'empresa': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Nombre de la empresa'
}),
'telefono': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Teléfono de contacto'
}),
'email': forms.EmailInput(attrs={ # EmailInput valida formato de email
'class': 'form-control',
'placeholder': 'correo@ejemplo.com'
}),
'direccion': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Dirección completa'
}),
}
labels = {
'nombre': 'Nombre del Contacto',
'empresa': 'Empresa',
'telefono': 'Teléfono',
'email': 'Correo Electrónico',
'direccion': 'Dirección',
}
# ============ FORMULARIO PARA CLIENTES ============
class ClienteForm(forms.ModelForm):
"""Formulario para crear y editar clientes"""
class Meta:
model = Cliente
fields = ['nombre', 'apellido', 'email', 'telefono', 'direccion']
widgets = {
'nombre': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Nombre del cliente'
}),
'apellido': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Apellido del cliente'
}),
'email': forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'correo@ejemplo.com'
}),
'telefono': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Teléfono de contacto'
}),
'direccion': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Dirección de entrega'
}),
}
labels = {
'nombre': 'Nombre',
'apellido': 'Apellido',
'email': 'Correo Electrónico',
'telefono': 'Teléfono',
'direccion': 'Dirección',
}
# ============ FORMULARIO PARA VENTAS ============
class VentaForm(forms.ModelForm):
"""Formulario para registrar ventas"""
class Meta:
model = Venta
fields = ['cliente', 'producto', 'cantidad'] # Solo solicita cliente, producto y cantidad (el precio se toma automático)
widgets = {
'cliente': forms.Select(attrs={
'class': 'form-control' # Select (combobox) con clientes
}),
'producto': forms.Select(attrs={
'class': 'form-control',
'id': 'id_producto' # ID para poder manipular con JavaScript si es necesario
}),
'cantidad': forms.NumberInput(attrs={
'class': 'form-control',
'min': '1', # Mínimo 1 unidad
'value': '1' # Valor por defecto
}),
}
labels = {
'cliente': 'Cliente',
'producto': 'Producto',
'cantidad': 'Cantidad',
}
PARTE 6: Crear Vistas
Las vistas (tienda/views.py) contienen la lógica del negocio: manejo de la autenticación, dashboard y el CRUD de Productos. Se hace uso de decoradores para asegurar la autenticación (@login_required) y la gestión de permisos.
Vistas de Tienda (tienda/views.py)
# tienda/views.py
from django.shortcuts import render, redirect, get_object_or_404 # Funciones comunes para renderizar, redirigir y obtener objetos.
from django.contrib.auth.decorators import login_required, permission_required # Decoradores para requerir autenticación y permisos.
from django.contrib.auth.views import LoginView, LogoutView # Vistas predefinidas de Django para autenticación.
from django.urls import reverse_lazy # Función para obtener URLs de forma perezosa.
from django.contrib.auth.models import Group # Modelo para gestionar grupos/roles de usuarios.
from django.contrib import messages # Módulo para enviar mensajes de notificación al usuario.
from .models import Producto, Categoria # Importa los modelos necesarios.
from .forms import ProductoForm # Importa el formulario de Producto.
# ============ DECORADOR PERSONALIZADO PARA PERMISOS POR ROL ============
def rol_requerido(*roles_permitidos):
"""
Decorador personalizado que verifica si el usuario tiene uno de los roles permitidos.
Uso:
@rol_requerido('gerente', 'administrador')
def mi_vista(request):
...
Parámetros:
*roles_permitidos: Lista de roles que pueden acceder a la vista
Opciones: 'vendedor', 'gerente', 'administrador'
"""
def decorator(view_func):
def _wrapped_view(request, *args, **kwargs):
# 1. Verificar si el usuario está autenticado
if not request.user.is_authenticated:
messages.error(request, 'Debes iniciar sesión para acceder')
return redirect('login')
# 2. Si es superusuario, permitir acceso siempre
if request.user.is_superuser:
return view_func(request, *args, **kwargs)
# 3. Verificar si el usuario tiene perfil con rol asignado
try:
perfil = request.user.perfil # Obtener perfil del usuario
# 4. Verificar si su rol está en los roles permitidos
if perfil.rol in roles_permitidos:
return view_func(request, *args, **kwargs) # Permitir acceso
else:
# Mostrar mensaje de error indicando roles necesarios
roles_texto = ', '.join([r.capitalize() for r in roles_permitidos])
messages.error(request, f'⚠️ Acceso denegado. Se requiere rol: {roles_texto}')
return redirect('home') # Redirigir al home
except PerfilUsuario.DoesNotExist:
# Si el usuario no tiene perfil asignado
messages.error(request, '⚠️ Tu cuenta no tiene un perfil asignado. Contacta al administrador.')
return redirect('home')
return _wrapped_view
return decorator
# ============ VISTA DE LOGIN ============
def login_view(request):
"""Vista para el inicio de sesión de usuarios"""
# Si el usuario ya está autenticado, redirigir al home
if request.user.is_authenticated:
return redirect('home')
# Si el método es POST, procesamos el formulario de login
if request.method == 'POST':
form = AuthenticationForm(request, data=request.POST) # Creamos el formulario con los datos enviados
if form.is_valid(): # Si el formulario es válido
username = form.cleaned_data.get('username') # Obtenemos el nombre de usuario
password = form.cleaned_data.get('password') # Obtenemos la contraseña
user = authenticate(username=username, password=password) # Autenticamos al usuario
if user is not None: # Si la autenticación fue exitosa
login(request, user) # Iniciamos sesión
messages.success(request, f'Bienvenido {username}!') # Mensaje de bienvenida
return redirect('home') # Redirigimos al home
else:
messages.error(request, 'Usuario o contraseña incorrectos') # Mensaje de error
else:
messages.error(request, 'Usuario o contraseña incorrectos') # Mensaje de error si el formulario no es válido
else:
form = AuthenticationForm() # Si es GET, creamos un formulario vacío
return render(request, 'tienda/login.html', {'form': form}) # Renderizamos el template de login
# ============ VISTA DE LOGOUT ============
def logout_view(request):
"""Vista para cerrar sesión"""
logout(request) # Cerramos la sesión del usuario
messages.info(request, 'Sesión cerrada correctamente') # Mensaje informativo
return redirect('login') # Redirigimos al login
# ============ VISTA PRINCIPAL (HOME) ============
@login_required # Decorador que requiere autenticación para acceder a esta vista
def home(request):
"""Vista principal que muestra el dashboard con estadísticas"""
# Contamos los registros de cada modelo
total_productos = Producto.objects.count() # Cuenta todos los productos
total_categorias = Categoria.objects.count() # Cuenta todas las categorías
total_proveedores = Proveedor.objects.count() # Cuenta todos los proveedores
total_clientes = Cliente.objects.count() # Cuenta todos los clientes
# Obtenemos los últimos 5 productos creados
productos_recientes = Producto.objects.all()[:5] # Slice de los primeros 5 productos
# Creamos un diccionario con los datos que enviaremos al template
context = {
'total_productos': total_productos,
'total_categorias': total_categorias,
'total_proveedores': total_proveedores,
'total_clientes': total_clientes,
'productos_recientes': productos_recientes,
}
return render(request, 'tienda/home.html', context) # Renderizamos el template con el contexto
# ============ VISTAS CRUD PARA PRODUCTOS ============
@login_required
def producto_lista(request):
"""Vista que lista todos los productos"""
productos = Producto.objects.all() # Obtenemos todos los productos de la base de datos
return render(request, 'tienda/producto_lista.html', {'productos': productos}) # Renderizamos template con la lista
@login_required
def producto_crear(request):
"""Vista para crear un nuevo producto"""
if request.method == 'POST': # Si se envió el formulario
form = ProductoForm(request.POST) # Creamos el formulario con los datos enviados
if form.is_valid(): # Si el formulario es válido (todos los campos correctos)
form.save() # Guardamos el producto en la base de datos
messages.success(request, 'Producto creado exitosamente') # Mensaje de éxito
return redirect('producto_lista') # Redirigimos a la lista de productos
else:
form = ProductoForm() # Si es GET, creamos un formulario vacío
return render(request, 'tienda/producto_form.html', {'form': form, 'accion': 'Crear'}) # Renderizamos el formulario
@login_required
@rol_requerido('gerente', 'administrador') # Solo Gerente y Administrador pueden editar
def producto_editar(request, pk):
"""Vista para editar un producto existente"""
producto = get_object_or_404(Producto, pk=pk) # Obtenemos el producto por su ID (Primary Key), si no existe muestra 404
if request.method == 'POST':
form = ProductoForm(request.POST, instance=producto) # Creamos el formulario con los datos del producto existente
if form.is_valid():
form.save() # Guardamos los cambios
messages.success(request, 'Producto actualizado exitosamente')
return redirect('producto_lista')
else:
form = ProductoForm(instance=producto) # Mostramos el formulario con los datos actuales del producto
return render(request, 'tienda/producto_form.html', {'form': form, 'accion': 'Editar'})
@login_required
@rol_requerido('administrador') # Solo Administrador puede eliminar
def producto_eliminar(request, pk):
"""Vista para eliminar un producto"""
producto = get_object_or_404(Producto, pk=pk) # Obtenemos el producto
if request.method == 'POST': # Confirmación de eliminación debe ser POST por seguridad
producto.delete() # Eliminamos el producto de la base de datos
messages.success(request, 'Producto eliminado exitosamente')
return redirect('producto_lista')
return render(request, 'tienda/producto_eliminar.html', {'producto': producto}) # Mostramos página de confirmación
# ============ VISTAS CRUD PARA CATEGORÍAS ============
@login_required
@rol_requerido('gerente', 'administrador') # Vendedor NO puede ver categorías
def categoria_lista(request):
"""Vista que lista todas las categorías"""
categorias = Categoria.objects.all()
return render(request, 'tienda/categoria_lista.html', {'categorias': categorias})
@login_required
@rol_requerido('gerente', 'administrador') # Solo Gerente y Administrador
def categoria_crear(request):
"""Vista para crear una nueva categoría"""
if request.method == 'POST':
form = CategoriaForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, 'Categoría creada exitosamente')
return redirect('categoria_lista')
else:
form = CategoriaForm()
return render(request, 'tienda/categoria_form.html', {'form': form, 'accion': 'Crear'})
@login_required
@rol_requerido('gerente', 'administrador')
def categoria_editar(request, pk):
"""Vista para editar una categoría existente"""
categoria = get_object_or_404(Categoria, pk=pk)
if request.method == 'POST':
form = CategoriaForm(request.POST, instance=categoria)
if form.is_valid():
form.save()
messages.success(request, 'Categoría actualizada exitosamente')
return redirect('categoria_lista')
else:
form = CategoriaForm(instance=categoria)
return render(request, 'tienda/categoria_form.html', {'form': form, 'accion': 'Editar'})
@login_required
@rol_requerido('administrador') # Solo Administrador
def categoria_eliminar(request, pk):
"""Vista para eliminar una categoría"""
categoria = get_object_or_404(Categoria, pk=pk)
if request.method == 'POST':
categoria.delete()
messages.success(request, 'Categoría eliminada exitosamente')
return redirect('categoria_lista')
return render(request, 'tienda/categoria_eliminar.html', {'categoria': categoria})
# ============ VISTAS CRUD PARA PROVEEDORES ============
@login_required
@rol_requerido('gerente', 'administrador') # Vendedor NO puede ver proveedores
def proveedor_lista(request):
"""Vista que lista todos los proveedores"""
proveedores = Proveedor.objects.all()
return render(request, 'tienda/proveedor_lista.html', {'proveedores': proveedores})
@login_required
@rol_requerido('gerente', 'administrador') # Solo Gerente y Administrador
def proveedor_crear(request):
"""Vista para crear un nuevo proveedor"""
if request.method == 'POST':
form = ProveedorForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, 'Proveedor creado exitosamente')
return redirect('proveedor_lista')
else:
form = ProveedorForm()
return render(request, 'tienda/proveedor_form.html', {'form': form, 'accion': 'Crear'})
@login_required
@rol_requerido('gerente', 'administrador')
def proveedor_editar(request, pk):
"""Vista para editar un proveedor existente"""
proveedor = get_object_or_404(Proveedor, pk=pk)
if request.method == 'POST':
form = ProveedorForm(request.POST, instance=proveedor)
if form.is_valid():
form.save()
messages.success(request, 'Proveedor actualizado exitosamente')
return redirect('proveedor_lista')
else:
form = ProveedorForm(instance=proveedor)
return render(request, 'tienda/proveedor_form.html', {'form': form, 'accion': 'Editar'})
@login_required
@rol_requerido('administrador')
def proveedor_eliminar(request, pk):
"""Vista para eliminar un proveedor"""
proveedor = get_object_or_404(Proveedor, pk=pk)
if request.method == 'POST':
proveedor.delete()
messages.success(request, 'Proveedor eliminado exitosamente')
return redirect('proveedor_lista')
return render(request, 'tienda/proveedor_eliminar.html', {'proveedor': proveedor})
# ============ VISTAS CRUD PARA CLIENTES ============
@login_required
def cliente_lista(request):
"""Vista que lista todos los clientes"""
clientes = Cliente.objects.all()
return render(request, 'tienda/cliente_lista.html', {'clientes': clientes})
@login_required
def cliente_crear(request):
"""Vista para crear un nuevo cliente"""
if request.method == 'POST':
form = ClienteForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, 'Cliente creado exitosamente')
return redirect('cliente_lista')
else:
form = ClienteForm()
return render(request, 'tienda/cliente_form.html', {'form': form, 'accion': 'Crear'})
@login_required
@rol_requerido('gerente', 'administrador')
def cliente_editar(request, pk):
"""Vista para editar un cliente existente"""
cliente = get_object_or_404(Cliente, pk=pk)
if request.method == 'POST':
form = ClienteForm(request.POST, instance=cliente)
if form.is_valid():
form.save()
messages.success(request, 'Cliente actualizado exitosamente')
return redirect('cliente_lista')
else:
form = ClienteForm(instance=cliente)
return render(request, 'tienda/cliente_form.html', {'form': form, 'accion': 'Editar'})
@login_required
@rol_requerido('administrador')
def cliente_eliminar(request, pk):
"""Vista para eliminar un cliente"""
cliente = get_object_or_404(Cliente, pk=pk)
if request.method == 'POST':
cliente.delete()
messages.success(request, 'Cliente eliminado exitosamente')
return redirect('cliente_lista')
return render(request, 'tienda/cliente_eliminar.html', {'cliente': cliente})
PARTE 7: Configurar URLs
Se establecen las rutas de navegación de la aplicación en tienda/urls.py y se conectan al proyecto principal en sistema_tienda/urls.py.
URLs de la Aplicación (tienda/urls.py)
# tienda/urls.py
from django.urls import path # Importa la función path para definir rutas.
from . import views # Importa las vistas de la aplicación actual.
from .views import CustomLoginView, CustomLogoutView # Importa las vistas de autenticación basadas en clases.
urlpatterns = [ # Lista de patrones de URL.
# Autenticación
path('login/', CustomLoginView.as_view(), name='login'), # URL para iniciar sesión.
path('logout/', CustomLogoutView.as_view(), name='logout'), # URL para cerrar sesión.
# Dashboard
path('dashboard/', views.dashboard, name='dashboard'), # URL para el panel de control.
# CRUD Productos
path('productos/', views.producto_lista, name='producto_lista'), # URL para listar productos.
path('productos/crear/', views.producto_crear, name='producto_crear'), # URL para crear un producto.
path('productos/editar/<int:pk>/', views.producto_editar, name='producto_editar'), # URL para editar un producto específico (usando su PK).
path('productos/eliminar/<int:pk>/', views.producto_eliminar, name='producto_eliminar'), # URL para eliminar (desactivar) un producto específico.
# ============ RUTAS PARA CATEGORÍAS ============
path('categorias/', views.categoria_lista, name='categoria_lista'), # Lista todas las categorías
path('categorias/crear/', views.categoria_crear, name='categoria_crear'), # Crear categoría
path('categorias/editar//', views.categoria_editar, name='categoria_editar'), # Editar categoría
path('categorias/eliminar//', views.categoria_eliminar, name='categoria_eliminar'), # Eliminar categoría
# ============ RUTAS PARA PROVEEDORES ============
path('proveedores/', views.proveedor_lista, name='proveedor_lista'), # Lista todos los proveedores
path('proveedores/crear/', views.proveedor_crear, name='proveedor_crear'), # Crear proveedor
path('proveedores/editar//', views.proveedor_editar, name='proveedor_editar'), # Editar proveedor
path('proveedores/eliminar//', views.proveedor_eliminar, name='proveedor_eliminar'), # Eliminar proveedor
# ============ RUTAS PARA CLIENTES ============
path('clientes/', views.cliente_lista, name='cliente_lista'), # Lista todos los clientes
path('clientes/crear/', views.cliente_crear, name='cliente_crear'), # Crear cliente
path('clientes/editar//', views.cliente_editar, name='cliente_editar'), # Editar cliente
path('clientes/eliminar//', views.cliente_eliminar, name='cliente_eliminar'), # Eliminar cliente
]
# Nota: captura un número entero de la URL y lo pasa como parámetro 'pk' a la vista
# Por ejemplo: productos/editar/5/ llamará a producto_editar(request, pk=5)
# La URL raíz redirecciona al dashboard si está autenticado, o a login si no.
path('', views.dashboard, name='home'), # URL raíz de la aplicación 'tienda'.
]
URLs del Proyecto Principal (sistema_tienda/urls.py)
# sistema_tienda/urls.py
from django.contrib import admin # Importa el módulo de administración de Django.
from django.urls import path, include # Importa path y include.
urlpatterns = [
path('admin/', admin.site.urls), # Mapea la URL '/admin/' al panel de administración de Django.
path('', include('tienda.urls')), # <-- Incluye todas las URLs definidas en 'tienda/urls.py' bajo la ruta raíz ('/').
]
PARTE 8: Crear Templates
Se crean los archivos HTML (usando Bootstrap 5) necesarios para la interfaz de usuario: la base, el login, el dashboard y las vistas de Productos (lista, formulario, confirmación de eliminación).
Base Template (tienda/templates/tienda/base.html)
Define la estructura principal, barra de navegación y carga de assets (Bootstrap, Font Awesome, CSS).
<!-- tienda/templates/tienda/base.html -->
<!-- <Template base que será heredado por todas las páginas> -->
{% load static %} <!-- <Carga el sistema de archivos estáticos de Django> -->
<!DOCTYPE html> <!-- <Define el tipo de documento HTML5> -->
<html lang="es"> <!-- <Indica que el contenido está en español> -->
<head> <!-- <Cabecera del documento: metadatos y recursos> -->
<meta charset="UTF-8"> <!-- <Usa codificación UTF-8 para admitir acentos y caracteres especiales> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- <Hace la página responsiva para móviles> -->
<title>{% block title %}Sistema de Tienda{% endblock %}</title> <!-- <Bloque dinámico para cambiar el título desde plantillas hijas> -->
<!-- <Bootstrap CSS: framework de estilos prediseñados> -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- <Font Awesome: biblioteca de íconos> -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- <Hoja de estilos personalizada del proyecto> -->
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
</head>
<body> <!-- <Cuerpo principal del documento> -->
<!-- <Barra de navegación visible solo si el usuario está autenticado> -->
{% if user.is_authenticated %}
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <!-- <Barra azul con texto claro> -->
<div class="container-fluid"> <!-- <Contenedor fluido ocupa todo el ancho> -->
<!-- <Logo o nombre de la aplicación> -->
<a class="navbar-brand" href="{% url 'home' %}">
<i class="fas fa-store"></i> Sistema Tienda
</a>
<!-- <Botón hamburguesa para menú móvil> -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<!-- <Sección colapsable del menú> -->
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto"> <!-- <Menú alineado a la izquierda> -->
<!-- <Enlace al inicio> -->
<li class="nav-item">
<a class="nav-link" href="{% url 'home' %}">
<i class="fas fa-home"></i> Inicio
</a>
</li>
<!-- <Menú de productos> -->
<li class="nav-item">
<a class="nav-link" href="{% url 'producto_lista' %}">
<i class="fas fa-box"></i> Productos
</a>
</li>
<!-- <Menú de categorías: oculto si el usuario es vendedor> -->
{% if user.is_superuser or user.perfil.rol != 'vendedor' %}
<li class="nav-item">
<a class="nav-link" href="{% url 'categoria_lista' %}">
<i class="fas fa-tags"></i> Categorías
</a>
</li>
{% endif %}
<!-- <Menú de proveedores: visible solo para administrador> -->
{% if user.is_superuser or user.perfil.rol != 'vendedor' %}
<li class="nav-item">
<a class="nav-link" href="{% url 'proveedor_lista' %}">
<i class="fas fa-truck"></i> Proveedores
</a>
</li>
{% endif %}
<!-- <Menú de clientes: disponible para todos> -->
<li class="nav-item">
<a class="nav-link" href="{% url 'cliente_lista' %}">
<i class="fas fa-users"></i> Clientes
</a>
</li>
</ul>
<!-- <Menú del usuario autenticado (derecha)> -->
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" data-bs-toggle="dropdown">
<i class="fas fa-user"></i> {{ user.username }} <!-- <Muestra el nombre del usuario> -->
</a>
<ul class="dropdown-menu dropdown-menu-end"> <!-- <Menú desplegable alineado a la derecha> -->
<li>
<a class="dropdown-item" href="{% url 'logout' %}">
<i class="fas fa-sign-out-alt"></i> Cerrar Sesión
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
{% endif %}
<!-- <Contenedor principal del contenido de cada página> -->
<div class="container mt-4"> <!-- <Espaciado superior de Bootstrap> -->
<!-- <Mensajes de Django (éxito, error, información)> -->
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }} <!-- <Muestra el texto del mensaje> -->
<button type="button" class="btn-close" data-bs-dismiss="alert"></button> <!-- <Botón para cerrar> -->
</div>
{% endfor %}
{% endif %}
<!-- <Bloque que las páginas hijas reemplazan con su propio contenido> -->
{% block content %}
{% endblock %}
</div>
<!-- <Pie de página> -->
<footer class="footer mt-5 py-3 bg-light">
<div class="container text-center">
<span class="text-muted">© 2025 Sistema de Tienda - Práctica Universitaria</span>
</div>
</footer>
<!-- <Bootstrap JS: para menús y alertas interactivas> -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
Login Template (tienda/templates/tienda/login.html)
<!-- tienda/templates/tienda/login.html -->
{% extends 'tienda/base.html' %}
{% load crispy_forms_tags %}
{% block title %}Iniciar Sesión{% endblock %}
{% block content %}
<div class="row justify-content-center mt-5">
<div class="col-md-6 col-lg-4">
<div class="card shadow-lg">
<div class="card-header bg-primary text-white text-center">
<h3 class="mb-0"><i class="fas fa-lock me-2"></i>Inicio de Sesión</h3>
</div>
<div class="card-body">
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-primary w-100 mt-3">Ingresar</button>
{% if form.errors %}
<p class="text-danger mt-3">Usuario o contraseña incorrectos. Por favor, inténtelo de nuevo.</p>
{% endif %}
</form>
</div>
</div>
</div>
</div>
{% endblock %}
Dashboard Template (tienda/templates/tienda/dashboard.html)
<!-- tienda/templates/tienda/dashboard.html -->
{% extends 'tienda/base.html' %}
{% block title %}Dashboard - {{ user.username }}{% endblock %}
{% block content %}
<div class="container">
<h1 class="mb-4 text-center">Panel de Control (Dashboard)</h1>
<div class="row g-4">
<!-- Tarjeta de Productos -->
<div class="col-md-6 col-lg-4">
<div class="card text-white bg-success shadow-lg">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title">Total Productos Activos</h5>
<p class="card-text fs-2">{{ total_productos }}</p>
</div>
<i class="fas fa-box-open fa-3x"></i>
</div>
<a href="{% url 'producto_lista' %}" class="btn btn-outline-light mt-3 w-100">Ver Productos</a>
</div>
</div>
</div>
<!-- Tarjeta de Categorías -->
<div class="col-md-6 col-lg-4">
<div class="card text-white bg-info shadow-lg">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title">Total Categorías</h5>
<p class="card-text fs-2">{{ total_categorias }}</p>
</div>
<i class="fas fa-tags fa-3x"></i>
</div>
<a href="#" class="btn btn-outline-light mt-3 w-100">Administrar Categorías</a>
</div>
</div>
</div>
<!-- Tarjeta de Ventas (Ejemplo) -->
<div class="col-md-6 col-lg-4">
<div class="card text-white bg-warning shadow-lg">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title">Ventas de Hoy</h5>
<p class="card-text fs-2">$X,XXX.XX</p>
</div>
<i class="fas fa-chart-line fa-3x"></i>
</div>
<a href="#" class="btn btn-outline-light mt-3 w-100">Ver Reporte</a>
</div>
</div>
</div>
</div>
<div class="mt-5 p-4 bg-light border rounded">
<h4>Acciones Rápidas</h4>
<a href="{% url 'producto_crear' %}" class="btn btn-primary me-2"><i class="fas fa-plus"></i> Añadir Producto</a>
<a href="#" class="btn btn-secondary me-2"><i class="fas fa-user-plus"></i> Registrar Cliente</a>
<!-- Otros botones -->
</div>
</div>
{% endblock %}
Lista de Productos Template (tienda/templates/tienda/producto_lista.html)
<!-- tienda/templates/tienda/producto_lista.html -->
{% extends 'tienda/base.html' %}
{% block title %}Lista de Productos{% endblock %}
{% block content %}
<h1 class="mb-4">Gestión de Productos Activos ({{ productos|length }})</h1>
<div class="d-flex justify-content-end mb-3">
<a href="{% url 'producto_crear' %}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i> Nuevo Producto
</a>
</div>
<div class="table-responsive shadow-sm rounded">
<table class="table table-hover table-striped">
<thead class="bg-dark text-white">
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Precio Venta</th>
<th>Stock</th>
<th>Categoría</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{% for producto in productos %}
<tr>
<td>{{ producto.id }}</td>
<td>{{ producto.nombre }}</td>
<td>${{ producto.precio_venta|floatformat:2 }}</td>
<td><span class="badge {% if producto.stock < 10 %}bg-danger{% else %}bg-success{% endif %}">{{ producto.stock }}</span></td>
<td>{{ producto.categoria.nombre }}</td>
<td>
<a href="{% url 'producto_editar' producto.pk %}" class="btn btn-sm btn-info me-2" title="Editar">
<i class="fas fa-edit"></i>
</a>
<a href="{% url 'producto_eliminar' producto.pk %}" class="btn btn-sm btn-danger" title="Eliminar">
<i class="fas fa-trash-alt"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center">No hay productos activos registrados.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
Formulario de Producto Template (tienda/templates/tienda/producto_form.html)
<!-- tienda/templates/tienda/producto_form.html -->
{% extends 'tienda/base.html' %}
{% load crispy_forms_tags %}
{% block title %}{{ titulo }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<div class="card shadow">
<div class="card-header bg-secondary text-white">
<h3 class="mb-0">{{ titulo }}</h3>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
<!-- El formulario se renderiza usando crispy_forms_tags -->
{{ form|crispy }}
</form>
</div>
<div class="card-footer text-end">
<a href="{% url 'producto_lista' %}" class="btn btn-outline-secondary">Cancelar</a>
</div>
</div>
</div>
</div>
{% endblock %}
Confirmación Eliminación Producto (tienda/templates/tienda/producto_confirm_delete.html)
<!-- tienda/templates/tienda/producto_confirm_delete.html -->
{% extends 'tienda/base.html' %}
{% block title %}Eliminar Producto{% endblock %}
{% block content %}
<div class="row justify-content-center mt-5">
<div class="col-md-6">
<div class="card border-danger shadow-lg">
<div class="card-header bg-danger text-white">
<h3 class="mb-0"><i class="fas fa-exclamation-triangle me-2"></i> Confirmar Eliminación</h3>
</div>
<div class="card-body">
<p>¿Está seguro que desea <strong>desactivar</strong> (eliminación lógica) el producto:</p>
<h4 class="text-danger">{{ producto.nombre }} (ID: {{ producto.id }})</h4>
<p class="text-muted">Esta acción lo ocultará de la lista de productos activos, pero no se borrará permanentemente de la base de datos.</p>
<form method="post">
{% csrf_token %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-4">
<button type="submit" class="btn btn-danger btn-lg"><i class="fas fa-trash-alt me-1"></i> Sí, Desactivar</button>
<a href="{% url 'producto_lista' %}" class="btn btn-secondary btn-lg">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
PARTE 9: CSS Personalizado
Se establece la configuración de archivos estáticos en settings.py y se crea el archivo styles.css para mejorar el diseño y la estética general del sistema.
Paso 9.1: Configurar Archivos Estáticos
En sistema_tienda/settings.py, la configuración de archivos estáticos debe verse así:
# sistema_tienda/settings.py
# ...
STATIC_URL = 'static/' # Define la URL base para archivos estáticos (ej: /static/).
STATICFILES_DIRS = [
BASE_DIR / "static", # Directorio para archivos estáticos de todo el proyecto (permite encontrar 'static/css/styles.css').
]
Luego, crea la carpeta static/css dentro de la carpeta raíz del proyecto.
Paso 9.2: Estilos Básicos (static/css/styles.css)
Este CSS proporciona un aspecto moderno y profesional, complementando Bootstrap 5.
/* static/css/styles.css */
/* Estilos para el cuerpo (opcional) */
body {
background-color: #f8f9fa; /* Color de fondo suave (similar a Bootstrap light). */
font-family: Arial, sans-serif;
}
/* Estilo para la barra de navegación */
.navbar-brand {
font-weight: bold;
font-size: 1.5rem;
}
/* Estilo para tarjetas de dashboard */
.card {
border-radius: 0.75rem;
transition: transform 0.2s; /* Habilita una transición suave al pasar el ratón. */
}
.card:hover {
transform: translateY(-5px); /* Eleva la tarjeta 5px al pasar el ratón para un efecto visual. */
}
/* Estilo para tablas */
.table th {
font-weight: 700;
}
/* Estilo para formularios de login/crud */
.card-header.bg-primary {
border-top-left-radius: 0.75rem;
border-top-right-radius: 0.75rem; /* Asegura que el encabezado del formulario tenga bordes redondeados. */
}
PARTE 10: Configurar Admin
Esta sección cubre la creación de un superusuario y el registro de los modelos en el panel de administración de Django para la gestión inicial de datos (Categorías, Proveedores, etc.).
Paso 10.1: Crear Superusuario
Crea un usuario administrador para acceder al panel de Django Admin:
# Comando de Django para iniciar la creación de un usuario administrador.
python manage.py createsuperuser
Responde: Nombre de usuario: admin, Email: admin@example.com, Contraseña: [la que elijas]
Paso 10.2: Registrar Modelos en Admin (tienda/admin.py)
# tienda/admin.py
# Importamos el módulo admin de Django para registrar modelos
from django.contrib import admin
# Importamos todos nuestros modelos
from .models import Categoria, Producto, Proveedor, Cliente, PerfilUsuario
# ============ CONFIGURACIÓN DEL ADMIN PARA PERFILES DE USUARIO ============
@admin.register(PerfilUsuario)
class PerfilUsuarioAdmin(admin.ModelAdmin):
"""Configuración personalizada del admin para Perfiles de Usuario"""
list_display = ('user', 'rol', 'departamento', 'activo', 'fecha_contratacion') # Columnas visibles
list_filter = ('rol', 'activo', 'departamento') # Filtros laterales por rol, estado y departamento
search_fields = ('user__username', 'user__email', 'departamento') # Búsqueda por usuario o departamento
list_editable = ('rol', 'activo') # Permite editar rol y estado desde la lista
ordering = ('-fecha_contratacion',) # Orden descendente por fecha de contratación
# ============ CONFIGURACIÓN DEL ADMIN PARA CATEGORÍAS ============
@admin.register(Categoria) # Decorador que registra el modelo Categoria
class CategoriaAdmin(admin.ModelAdmin):
"""Configuración personalizada del admin para Categorías"""
list_display = ('id', 'nombre', 'fecha_creacion') # Columnas que se muestran en la lista
search_fields = ('nombre',) # Campos por los que se puede buscar
list_filter = ('fecha_creacion',) # Filtros laterales
ordering = ('nombre',) # Orden por defecto
# ============ CONFIGURACIÓN DEL ADMIN PARA PRODUCTOS ============
@admin.register(Producto)
class ProductoAdmin(admin.ModelAdmin):
"""Configuración personalizada del admin para Productos"""
list_display = ('id', 'nombre', 'categoria', 'precio', 'stock', 'activo', 'fecha_creacion') # Columnas visibles
search_fields = ('nombre', 'descripcion') # Búsqueda por nombre o descripción
list_filter = ('categoria', 'activo', 'fecha_creacion') # Filtros por categoría, estado y fecha
list_editable = ('precio', 'stock', 'activo') # Campos editables directamente en la lista
ordering = ('-fecha_creacion',) # Orden descendente por fecha
# ============ CONFIGURACIÓN DEL ADMIN PARA PROVEEDORES ============
@admin.register(Proveedor)
class ProveedorAdmin(admin.ModelAdmin):
"""Configuración personalizada del admin para Proveedores"""
list_display = ('id', 'nombre', 'empresa', 'telefono', 'email', 'fecha_registro')
search_fields = ('nombre', 'empresa', 'email') # Búsqueda por nombre, empresa o email
list_filter = ('fecha_registro',)
ordering = ('empresa',)
# ============ CONFIGURACIÓN DEL ADMIN PARA CLIENTES ============
@admin.register(Cliente)
class ClienteAdmin(admin.ModelAdmin):
"""Configuración personalizada del admin para Clientes"""
list_display = ('id', 'nombre', 'apellido', 'email', 'telefono', 'fecha_registro')
search_fields = ('nombre', 'apellido', 'email') # Búsqueda por nombre, apellido o email
list_filter = ('fecha_registro',)
ordering = ('apellido', 'nombre') # Orden por apellido y luego nombre
# Nota: Con estas configuraciones, los modelos aparecerán en el panel de administración de Django
# accesible en http://localhost:8000/admin/
PARTE 11: Crear Usuarios y Configurar Permisos
Este paso es crucial para el sistema de permisos por roles.
Paso 11.1: Crear Superusuario
# Ejecuta el comando
python manage.py createsuperuser
# Te preguntará:
# Username: admin
# Email address: admin@tienda.com
# Password: ******** (mínimo 8 caracteres)
# Password (again): ********
Paso 11.2: Crear Perfil para el Superusuario
Ejecuta el shell de Django:
python manage.py shell
# Dentro del shell, ejecuta:
from django.contrib.auth.models import User
from tienda.models import PerfilUsuario
user = User.objects.get(username='admin')
perfil = PerfilUsuario.objects.create(
user=user,
rol='administrador',
departamento='Sistemas',
telefono='555-0000'
)
print(f"Perfil creado: {perfil}")
exit()
Paso 11.3: Crear Usuarios con Roles
El proyecto incluye el script crear_usuarios_con_roles.py. Ejecútalo:
# Desde la carpeta raíz del proyecto
python crear_usuarios_con_roles.py
# Este script crea automáticamente:
# - vendedor1 / vendedor123 (Rol: Vendedor)
# - gerente1 / gerente123 (Rol: Gerente)
# - admin1 / admin123 (Rol: Administrador)
🔐 Sistema de Permisos
- Vendedor: Solo puede ver (lectura)
- Gerente: Puede ver, crear y editar
- Administrador: Acceso total (incluye eliminar)
PARTE 12: Módulo de Ventas
Implementaremos un módulo completo de ventas con registro y reporte diario.
Paso 12.1: Vista para Crear Ventas
Agrega esta vista en tienda/views.py:
# ============ VISTAS PARA VENTAS ============
@login_required # Requiere estar autenticado
def venta_crear(request):
"""Vista para registrar una nueva venta"""
if request.method == 'POST': # Si se envió el formulario
form = VentaForm(request.POST) # Crear formulario con datos POST
if form.is_valid(): # Validar formulario
venta = form.save(commit=False) # No guardar aún
venta.vendedor = request.user # Asignar usuario actual como vendedor
# Asignar el precio del producto al precio_unitario
venta.precio_unitario = venta.producto.precio # Tomar precio actual del producto
venta.save() # Ahora sí guardar (esto calculará el total automáticamente)
messages.success(request, f'Venta registrada exitosamente - Total: ${venta.total}')
return redirect('reporte_ventas') # Redirigir al reporte
else:
form = VentaForm() # Formulario vacío
return render(request, 'tienda/venta_form.html', {'form': form})
@login_required
def reporte_ventas(request):
"""Vista del reporte de ventas del día"""
# Obtener fecha del día
hoy = timezone.now().date() # Fecha actual
# Filtrar ventas del día de hoy usando fecha_venta__date
ventas_hoy = Venta.objects.filter(fecha_venta__date=hoy).select_related('producto', 'cliente', 'vendedor')
# Calcular total de ventas del día
total_ventas_dia = ventas_hoy.aggregate(total=Sum('total'))['total'] or 0 # Suma de totales
# Contar cantidad de ventas
cantidad_ventas = ventas_hoy.count() # Número de ventas
context = {
'ventas_hoy': ventas_hoy,
'total_ventas_dia': total_ventas_dia,
'cantidad_ventas': cantidad_ventas,
'fecha': hoy,
}
return render(request, 'tienda/reporte_ventas.html', context)
Paso 12.2: Agregar Rutas de Ventas
En tienda/urls.py, agrega:
# ============ RUTAS PARA VENTAS ============
path('ventas/crear/', views.venta_crear, name='venta_crear'), # Registrar venta
path('ventas/reporte/', views.reporte_ventas, name='reporte_ventas'), # Reporte de ventas
Paso 12.3: Crear Templates de Ventas
Template: venta_form.html
<!-- tienda/templates/tienda/venta_form.html -->
{% extends 'tienda/base.html' %}
{% block title %}Registrar Venta{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-success text-white">
<h3 class="mb-0"><i class="fas fa-cash-register"></i> Registrar Nueva Venta</h3>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %} <!-- Token de seguridad CSRF -->
<!-- Campo Cliente -->
<div class="mb-3">
<label class="form-label">{{ form.cliente.label }}</label>
{{ form.cliente }} <!-- Combobox de clientes -->
</div>
<!-- Campo Producto -->
<div class="mb-3">
<label class="form-label">{{ form.producto.label }}</label>
{{ form.producto }} <!-- Combobox de productos -->
</div>
<!-- Campo Cantidad -->
<div class="mb-3">
<label class="form-label">{{ form.cantidad.label }}</label>
{{ form.cantidad }} <!-- Input numérico -->
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-success btn-lg">
<i class="fas fa-check"></i> Registrar Venta
</button>
<a href="{% url 'reporte_ventas' %}" class="btn btn-outline-secondary">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
Template: reporte_ventas.html
<!-- tienda/templates/tienda/reporte_ventas.html -->
{% extends 'tienda/base.html' %}
{% load humanize %} <!-- Cargar filtro humanize para formato de millares -->
{% block title %}Reporte de Ventas{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h1 class="display-5">
<i class="fas fa-chart-line"></i> Reporte de Ventas del Día
</h1>
<a href="{% url 'venta_crear' %}" class="btn btn-primary">
<i class="fas fa-plus"></i> Registrar Venta
</a>
</div>
<p class="text-muted">{{ fecha|date:"l, d F Y" }}</p> <!-- Fecha en español -->
</div>
</div>
<!-- Resumen de ventas del día -->
<div class="row g-4 mb-4">
<div class="col-md-4">
<div class="card stats-card bg-success text-white">
<div class="card-body">
<h6 class="card-subtitle mb-2">Total Vendido Hoy</h6>
<h2 class="card-title mb-0">${{ total_ventas_dia|floatformat:2|intcomma }}</h2>
<!-- |floatformat:2 = 2 decimales, |intcomma = formato millares (1,234.56) -->
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stats-card bg-info text-white">
<div class="card-body">
<h6 class="card-subtitle mb-2">Número de Ventas</h6>
<h2 class="card-title mb-0">{{ cantidad_ventas }}</h2>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stats-card bg-warning text-dark">
<div class="card-body">
<h6 class="card-subtitle mb-2">Promedio por Venta</h6>
<h2 class="card-title mb-0">
{% if cantidad_ventas > 0 %}
${% widthratio total_ventas_dia cantidad_ventas 1 %}
{% else %}
$0.00
{% endif %}
</h2>
</div>
</div>
</div>
</div>
<!-- Detalle de ventas -->
<div class="card">
<div class="card-body">
{% if ventas_hoy %}
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead class="table-dark">
<tr>
<th>#</th>
<th>Hora</th>
<th>Producto</th>
<th>Cliente</th>
<th>Cantidad</th>
<th>Precio Unit.</th>
<th>Total</th>
<th>Vendedor</th>
</tr>
</thead>
<tbody>
{% for venta in ventas_hoy %}
<tr>
<td><strong>#{{ venta.id }}</strong></td>
<td>{{ venta.fecha_venta|date:"H:i" }}</td> <!-- Hora:minuto -->
<td>{{ venta.producto.nombre }}</td>
<td>{{ venta.cliente.nombre_completo }}</td>
<td><span class="badge bg-secondary">{{ venta.cantidad }}</span></td>
<td>${{ venta.precio_unitario|floatformat:2|intcomma }}</td> <!-- Formato millares -->
<td><strong class="text-success">${{ venta.total|floatformat:2|intcomma }}</strong></td>
<td>{{ venta.vendedor.username }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot class="table-light">
<tr>
<td colspan="6" class="text-end"><strong>TOTAL DEL DÍA:</strong></td>
<td colspan="2">
<strong class="text-success fs-5">${{ total_ventas_dia|floatformat:2|intcomma }}</strong>
</td>
</tr>
</tfoot>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-inbox fa-4x text-muted mb-3"></i>
<p class="text-muted fs-5">No hay ventas registradas el día de hoy.</p>
<a href="{% url 'venta_crear' %}" class="btn btn-primary mt-3">
<i class="fas fa-plus"></i> Registrar Primera Venta
</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}
Paso 12.4: Configurar Humanize para Formato de Millares
En settings.py, agrega 'django.contrib.humanize' a INSTALLED_APPS:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize', # 👈 AGREGAR ESTO para formato de millares
'tienda',
]
Paso 12.5: Configurar Timezone en UTC
Para evitar problemas con fechas, configura en settings.py:
TIME_ZONE = 'UTC' # Zona horaria UTC (evita problemas de conversión)
USE_TZ = True # Activar soporte de zonas horarias
Paso 12.6: Agregar Enlace en el Navbar
En base.html, agrega el enlace de ventas:
<li class="nav-item">
<a class="nav-link" href="{% url 'reporte_ventas' %}">
<i class="fas fa-chart-line"></i> Ventas
</a>
</li>
✅ Módulo de Ventas Completado
Ahora el sistema cuenta con:
- ✓ Formulario para registrar ventas
- ✓ Precio unitario automático desde el producto
- ✓ Cálculo automático del total
- ✓ Reporte de ventas del día
- ✓ Formato de millares mexicanos ($2,286.43)
- ✓ Estadísticas (total vendido, número de ventas, promedio)
PARTE 13: Ejecutar y Probar
Ahora ejecutaremos el proyecto y verificaremos que todo funcione correctamente.
Paso 12.1: Ejecutar el Servidor
# Método 1: Desde línea de comandos
python manage.py runserver
# Método 2: Usando el archivo .bat (solo Windows)
ejecutar_proyecto.bat
# El servidor se iniciará en: http://127.0.0.1:8000/
Paso 12.2: Acceder a la Aplicación
Abre tu navegador y visita estas URLs:
- Login: http://localhost:8000/
- Dashboard: http://localhost:8000/home/
- Admin Panel: http://localhost:8000/admin/
Paso 12.3: Probar Funcionalidades
- Login: Inicia sesión con admin / tu_contraseña
- Dashboard: Verifica que muestre estadísticas
- Categorías: Crea al menos 3 categorías (Electrónica, Ropa, Hogar)
- Productos: Crea productos y asigna categorías (combobox)
- Proveedores: Registra proveedores
- Clientes: Registra clientes
- Permisos: Prueba con diferentes usuarios (vendedor, gerente)
Paso 12.4: Verificar Permisos por Roles
- Cierra sesión (logout)
- Inicia sesión como
vendedor1(contraseña: vendedor123) - Intenta crear un producto → Debería mostrar error de permisos
- Cierra sesión e inicia como
gerente1(contraseña: gerente123) - Ahora SÍ podrás crear y editar productos
- Intenta eliminar → Debería dar error (solo admin puede)
📊 Cargar Datos de Ejemplo
Para no crear todo manualmente, usa el script incluido:
Método 1: Ejecutar el Script
# Desde la carpeta raíz del proyecto
python cargar_datos_ejemplo.py
# Esto crea automáticamente:
# - 3 categorías (Electrónica, Ropa, Hogar)
# - 6 productos (2 por categoría)
# - 2 proveedores
# - 3 clientes
Método 2: Desde Django Shell
python manage.py shell < cargar_datos_ejemplo.py
✅ Ventajas de Usar Datos de Ejemplo
- Ahorra tiempo al no tener que ingresar datos manualmente
- Permite probar todas las funcionalidades inmediatamente
- Ideal para demostraciones y presentaciones
- Los datos son realistas y variados
Solución de Errores Comunes
Esta sección proporciona una referencia rápida a los problemas de configuración más frecuentes y sus soluciones.
Errores Comunes y Soluciones
- Error:
settings.configure()no configurado: Asegúrate de que las dependencias estén instaladas y el entorno virtual esté activo. - Error:
ModuleNotFoundError: No module named 'mysqlclient': Verifica instalación demysqlclienty librerías de desarrollo de MySQL en el OS. - Error:
Unknown command: 'tienda': Revisasistema_tienda/settings.pyy verifica que'tienda'esté enINSTALLED_APPS. - Error de permisos (403 Forbidden): Confirma que el usuario esté autenticado y tenga los permisos necesarios en Django Admin.
- Error: ¿Problemas con
CSRF token missing?: Agrega{% csrf_token %}en todos los forms con método POST. - Error 404 en rutas: Verifica que las URLs estén correctamente definidas y que
include('tienda.urls')esté en elurls.pyprincipal.
🛠️ Archivos de Utilidad Incluidos
El proyecto incluye varios scripts que facilitan tareas comunes:
1. ejecutar_proyecto.bat
Script para Windows que ejecuta el servidor automáticamente:
@echo off
cd /d "%~dp0"
python manage.py runserver
pause
Uso: Doble clic en el archivo para iniciar el servidor.
2. crear_bd.sql
Script SQL para crear la base de datos:
CREATE DATABASE IF NOT EXISTS tienda_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
Uso: mysql -u root -p < crear_bd.sql
3. cargar_datos_ejemplo.py
Crea datos de prueba automáticamente (categorías, productos, proveedores, clientes).
Uso: python cargar_datos_ejemplo.py
4. crear_usuarios_con_roles.py
Crea usuarios con diferentes roles (vendedor, gerente, administrador).
Uso: python crear_usuarios_con_roles.py
5. actualizar_passwords.py
Permite actualizar contraseñas de usuarios existentes.
Uso: python actualizar_passwords.py
🚀 Cómo Replicar el Proyecto en Otro Equipo
Sigue estos pasos para instalar el proyecto en otra computadora:
Paso 1: Copiar Archivos del Proyecto
- Copia toda la carpeta
practica_u3_examenal nuevo equipo - O descarga/clona desde un repositorio Git
Paso 2: Verificar Requisitos
# Verificar Python
python --version
# Verificar MySQL
mysql --version
Paso 3: Crear Entorno Virtual (Recomendado)
# Crear entorno virtual
python -m venv env
# Activar (Windows)
env\Scripts\activate
# Activar (Linux/Mac)
source env/bin/activate
Paso 4: Instalar Dependencias
pip install django
pip install mysqlclient
Paso 5: Configurar MySQL
# Crear base de datos
mysql -u root -p < crear_bd.sql
# O manualmente:
mysql -u root -p
CREATE DATABASE tienda_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
EXIT;
Paso 6: Configurar settings.py
Edita tienda_proyecto/settings.py con tu contraseña de MySQL (línea ~78):
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'tienda_db',
'USER': 'root',
'PASSWORD': 'TU_CONTRASEÑA_AQUI', # 👈 Cambiar esto
'HOST': 'localhost',
'PORT': '3306',
}
}
Paso 7: Aplicar Migraciones
python manage.py migrate
Paso 8: Crear Usuario Administrador
python manage.py createsuperuser
Paso 9: Cargar Datos de Ejemplo (Opcional)
python cargar_datos_ejemplo.py
python crear_usuarios_con_roles.py
Paso 10: Ejecutar el Servidor
python manage.py runserver
✅ Checklist de Replicación
- ☐ Python 3.8+ instalado
- ☐ MySQL instalado y corriendo
- ☐ Dependencias instaladas (django, mysqlclient)
- ☐ Base de datos
tienda_dbcreada - ☐ Contraseña configurada en settings.py
- ☐ Migraciones aplicadas
- ☐ Superusuario creado
- ☐ Servidor funcionando en http://localhost:8000/
⚡ Comandos Rápidos de Referencia
Comandos útiles que usarás frecuentemente:
Servidor
# Iniciar servidor
python manage.py runserver
# Iniciar en puerto específico
python manage.py runserver 8080
# Iniciar y permitir acceso desde red
python manage.py runserver 0.0.0.0:8000
Base de Datos
# Crear migraciones
python manage.py makemigrations
# Aplicar migraciones
python manage.py migrate
# Ver SQL de migraciones sin aplicar
python manage.py sqlmigrate tienda 0001
# Revertir migraciones
python manage.py migrate tienda 0001
Usuarios
# Crear superusuario
python manage.py createsuperuser
# Cambiar contraseña
python manage.py changepassword admin
Shell
# Abrir shell de Django
python manage.py shell
# Ejemplos en shell:
from tienda.models import Producto, Categoria
Producto.objects.all()
Categoria.objects.count()
exit()
Información del Proyecto
# Verificar configuración
python manage.py check
# Mostrar todas las URLs
python manage.py show_urls
# Ver estructura de BD
python manage.py inspectdb
MySQL
# Conectar a MySQL
mysql -u root -p
# Comandos dentro de MySQL:
SHOW DATABASES;
USE tienda_db;
SHOW TABLES;
DESCRIBE tienda_producto;
SELECT * FROM tienda_categoria;
EXIT;
📚 Para Seguir Aprendiendo
Una vez completado el proyecto base, estos son los siguientes pasos recomendados:
Mejoras del Proyecto
- Paginación: Divide listas largas en páginas
- Búsqueda: Filtra productos por nombre y categoría
- Orden: Ordena listas por diferentes campos
- Imágenes: Agrega campo ImageField para fotos de productos
- Exportación: Exporta datos a Excel/PDF
- Gráficos: Añade gráficos con Chart.js
- Notificaciones: Alertas de stock bajo
Tecnologías Avanzadas
- Django REST Framework: Crea una API REST
- Celery: Tareas asíncronas en segundo plano
- Redis: Cache para mejorar rendimiento
- Docker: Conteneriza la aplicación
- PostgreSQL: Base de datos más robusta
Despliegue en Producción
- Heroku: Deployment gratuito
- PythonAnywhere: Hosting para Django
- DigitalOcean: VPS con control completo
- AWS: Amazon Web Services
Recursos Recomendados
✅ ¡Proyecto Completado!
Felicidades, has completado exitosamente un sistema web profesional de gestión de tienda.
🎉 Resumen del Proyecto
📊 Estadísticas
- ✓ 6 Modelos de base de datos (incluye Venta)
- ✓ 28 Vistas (lógica de negocio + módulo ventas)
- ✓ 5 Formularios con validación (incluye VentaForm)
- ✓ 24 Rutas URL (incluye rutas de ventas)
- ✓ 17 Templates HTML (incluye ventas)
- ✓ ~4,000 líneas de código
- ✓ 100% documentado
✨ Funcionalidades
- ✓ Sistema de autenticación
- ✓ CRUD completo (5 módulos)
- ✓ Llaves foráneas (combobox)
- ✓ Sistema de permisos por roles
- ✓ Módulo de ventas completo
- ✓ Reporte de ventas diario
- ✓ Formato millares mexicanos
- ✓ Dashboard con estadísticas
- ✓ Diseño responsivo
- ✓ Validación de formularios
📚 Detalles del Proyecto
🎓 Información Académica
- Universidad: Universidad Tecnológica de Hermosillo (UTH)
- Materia: aplicaciónes web
- Período: 2025-3
- Unidad: 3
- Nivel: Universitario
💻 Stack Tecnológico
- Backend: Django 5.x (Python)
- Base de Datos: MySQL 8.0+
- Frontend: Bootstrap 5.3
- Iconos: Font Awesome 6.4
- Lenguaje: Python 3.8+
🎯 Objetivos Cumplidos
- ✅ Caso práctico de negocio real
- ✅ Aplicación web completa
- ✅ Hojas de estilo personalizadas
- ✅ Gestión de base de datos
- ✅ Formularios dinámicos
- ✅ Combobox con llaves foráneas
- ✅ Seguridad por sesiones
- ✅ Código documentado
- ✅ Diseño responsivo
- ✅ Sistema de permisos
- ✅ Dashboard interactivo
- ✅ Panel de administración
📁 Archivos del Proyecto
Código Python
-
models.py- 5 modelos de BD (400+ líneas) -
views.py- 23 vistas (800+ líneas) -
forms.py- 4 formularios (300+ líneas) -
urls.py- 20 rutas URL (100+ líneas) -
admin.py- Configuración admin (80+ líneas)
Templates HTML
-
base.html- Template principal -
login.html- Página de login -
home.html- Dashboard -
producto_*.html- CRUD de productos (4 archivos) -
categoria_*.html- CRUD de categorías (4 archivos) -
proveedor_*.html- CRUD de proveedores (4 archivos) -
cliente_*.html- CRUD de clientes (4 archivos)
Scripts de Utilidad
-
crear_bd.sql- Crear base de datos -
cargar_datos_ejemplo.py- Datos de prueba -
crear_usuarios_con_roles.py- Usuarios con roles -
actualizar_passwords.py- Actualizar contraseñas -
ejecutar_proyecto.bat- Iniciar servidor (Windows)
🌟 Características Destacadas
🔐 Seguridad
- Autenticación por sesiones
- Protección de rutas
- Tokens CSRF
- Sistema de permisos
💾 Base de Datos
- 5 modelos relacionados
- Llaves foráneas
- Migraciones automáticas
- ORM de Django
🎨 Diseño
- Bootstrap 5
- Responsivo
- Animaciones CSS
- Iconos Font Awesome
💡 Lo que Aprendiste
Backend
- Patrón MVT de Django
- ORM y migraciones
- Vistas basadas en funciones
- Sistema de autenticación
- Decoradores personalizados
- Manejo de sesiones
Frontend
- Templates Django
- Template tags y filtros
- Herencia de templates
- Bootstrap 5
- CSS personalizado
- Diseño responsivo
🎓 ¡Felicidades!
Has completado con éxito el Sistema de Gestión de Tienda
Ahora tienes un proyecto completo, funcional y profesional
que puedes presentar, mostrar en tu portafolio y seguir expandiendo.
📧 Proyecto desarrollado para UTH 2025-3
Materia: aplicaciónes web | Unidad 3
Stack: Django + MySQL + Bootstrap
Impartido Por: Dr. Bernardo Prado Díaz
📝 CÓDIGO COMPLETO: models.py
Este archivo define los 5 modelos de base de datos. Cada línea tiene un comentario explicativo para que entiendas exactamente qué hace.
📚 ¿Qué aprenderás?
- Cómo crear modelos Django (clases Python → tablas MySQL)
- Tipos de campos: CharField, TextField, IntegerField, DecimalField, BooleanField
- Relaciones: OneToOneField (1:1), ForeignKey (muchos:1)
- Métodos: __str__(), @property, métodos personalizados
# tienda/models.py
from django.db import models # Importa models para crear tablas en la BD
from django.contrib.auth.models import User # Importa modelo User (usuario de Django)
# ==================================================================
# MODELO 1: PERFIL DE USUARIO
# ==================================================================
class PerfilUsuario(models.Model):
# Opciones para el campo 'rol'
ROLES = ( # Tupla de tuplas con opciones
('vendedor', 'Vendedor'), # (valor_bd, valor_mostrado)
('gerente', 'Gerente'),
('administrador', 'Administrador'),
)
# Campos del modelo
user = models.OneToOneField( # Relación 1:1 con User
User, # Modelo relacionado
on_delete=models.CASCADE, # Si eliminas User, eliminas Perfil
related_name='perfil' # Acceso: user.perfil
)
rol = models.CharField( # Campo texto para rol
max_length=20, # Máximo 20 caracteres
choices=ROLES, # Solo valores de ROLES
default='vendedor' # Valor por defecto
)
telefono = models.CharField( # Teléfono
max_length=15,
blank=True, # Puede estar vacío
null=True # Puede ser NULL
)
departamento = models.CharField(
max_length=100,
blank=True,
null=True
)
fecha_contratacion = models.DateField( # Fecha
auto_now_add=True # Se pone automáticamente
)
activo = models.BooleanField(default=True) # True/False
def __str__(self): # Cómo se muestra el objeto
return f"{self.user.username} - {self.get_rol_display()}"
class Meta: # Configuración del modelo
verbose_name = "Perfil de Usuario" # Nombre singular
verbose_name_plural = "Perfiles de Usuario"
# Métodos personalizados
def es_vendedor(self):
return self.rol == 'vendedor'
def es_gerente(self):
return self.rol == 'gerente'
def es_administrador(self):
return self.rol == 'administrador'
def tiene_permiso_escritura(self):
return self.rol in ['gerente', 'administrador']
# ==================================================================
# MODELO 2: CATEGORÍA
# ==================================================================
class Categoria(models.Model):
nombre = models.CharField(max_length=100) # Nombre de categoría
descripcion = models.TextField( # Descripción opcional
blank=True,
null=True
)
fecha_creacion = models.DateTimeField( # Fecha y hora
auto_now_add=True
)
def __str__(self):
return self.nombre
class Meta:
verbose_name = "Categoría"
verbose_name_plural = "Categorías"
ordering = ['nombre'] # Ordena alfabéticamente
# ==================================================================
# MODELO 3: PRODUCTO
# ==================================================================
class Producto(models.Model):
nombre = models.CharField(max_length=200)
descripcion = models.TextField()
precio = models.DecimalField( # Precio con decimales
max_digits=10, # Máx: 99999999.99
decimal_places=2 # 2 decimales
)
stock = models.IntegerField(default=0) # Cantidad en inventario
categoria = models.ForeignKey( # Llave foránea a Categoría
Categoria,
on_delete=models.CASCADE, # Si borras categoría, borras productos
related_name='productos' # categoria.productos.all()
)
fecha_creacion = models.DateTimeField(auto_now_add=True)
fecha_actualizacion = models.DateTimeField( # Se actualiza automáticamente
auto_now=True
)
activo = models.BooleanField(default=True)
def __str__(self):
return f"{self.nombre} - ${self.precio}"
class Meta:
verbose_name = "Producto"
verbose_name_plural = "Productos"
ordering = ['-fecha_creacion'] # Más recientes primero
# ==================================================================
# MODELO 4: PROVEEDOR
# ==================================================================
class Proveedor(models.Model):
nombre = models.CharField(max_length=200) # Nombre del contacto
empresa = models.CharField(max_length=200) # Nombre de la empresa
telefono = models.CharField(max_length=15)
email = models.EmailField() # Valida formato email
direccion = models.TextField()
fecha_registro = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.nombre} - {self.empresa}"
class Meta:
verbose_name = "Proveedor"
verbose_name_plural = "Proveedores"
ordering = ['empresa']
# ==================================================================
# MODELO 5: CLIENTE
# ==================================================================
class Cliente(models.Model):
nombre = models.CharField(max_length=100)
apellido = models.CharField(max_length=100)
email = models.EmailField(
max_length=191, # Máx 191 para MySQL utf8mb4
unique=True # Email debe ser único
)
telefono = models.CharField(max_length=15)
direccion = models.TextField()
fecha_registro = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.nombre} {self.apellido}"
@property # Propiedad (se accede sin paréntesis)
def nombre_completo(self):
return f"{self.nombre} {self.apellido}" # En template: {{ cliente.nombre_completo }}
class Meta:
verbose_name = "Cliente"
verbose_name_plural = "Clientes"
ordering = ['apellido', 'nombre']
✅ Checklist - models.py
Asegúrate de entender:
- ☐ Qué es un modelo en Django
- ☐ Diferencia entre CharField y TextField
- ☐ Qué es una ForeignKey
- ☐ Diferencia entre blank=True y null=True
- ☐ Qué hace el método __str__()
- ☐ Qué es @property
📝 CÓDIGO COMPLETO: views.py (Primeras 100 líneas)
Las vistas manejan la lógica de negocio. Debido a su extensión (331 líneas), aquí están las partes más importantes comentadas.
# tienda/views.py
# Importaciones
from django.shortcuts import render, redirect, get_object_or_404 # Funciones básicas de Django
from django.contrib.auth import login, authenticate, logout # Autenticación
from django.contrib.auth.decorators import login_required # Decorador: requiere login
from django.contrib import messages # Mensajes flash
from .models import Categoria, Producto, Proveedor, Cliente, PerfilUsuario
from .forms import ProductoForm, CategoriaForm, ProveedorForm, ClienteForm
# ==================================================================
# DECORADOR PERSONALIZADO: Verificar roles
# ==================================================================
def rol_requerido(*roles_permitidos):
"""
Decorador que verifica si el usuario tiene uno de los roles permitidos.
Uso: @rol_requerido('gerente', 'administrador')
"""
def decorator(view_func):
def _wrapped_view(request, *args, **kwargs):
# 1. Verificar si está autenticado
if not request.user.is_authenticated:
messages.error(request, 'Debes iniciar sesión')
return redirect('login')
# 2. Si es superusuario, permitir siempre
if request.user.is_superuser:
return view_func(request, *args, **kwargs)
# 3. Verificar rol del usuario
try:
perfil = request.user.perfil
if perfil.rol in roles_permitidos:
return view_func(request, *args, **kwargs)
else:
messages.error(request, f'Acceso denegado. Se requiere rol: {", ".join(roles_permitidos)}')
return redirect('home')
except PerfilUsuario.DoesNotExist:
messages.error(request, 'Tu cuenta no tiene perfil asignado')
return redirect('home')
return _wrapped_view
return decorator
# ==================================================================
# VISTA: Login
# ==================================================================
def login_view(request):
"""Vista para el inicio de sesión"""
if request.user.is_authenticated: # Si ya está logueado
return redirect('home') # Redirigir al home
if request.method == 'POST': # Si envió el formulario
username = request.POST.get('username') # Obtener datos del POST
password = request.POST.get('password')
user = authenticate( # Autenticar usuario
request,
username=username,
password=password
)
if user is not None: # Si la autenticación fue exitosa
login(request, user) # Iniciar sesión
messages.success(request, f'Bienvenido {user.username}!')
return redirect('home')
else: # Credenciales incorrectas
messages.error(request, 'Usuario o contraseña incorrectos')
return render(request, 'tienda/login.html') # Renderizar template
# ==================================================================
# VISTA: Logout
# ==================================================================
@login_required # Requiere estar logueado
def logout_view(request):
"""Vista para cerrar sesión"""
logout(request) # Cerrar sesión
messages.success(request, 'Sesión cerrada exitosamente')
return redirect('login')
# ==================================================================
# VISTA: Home (Dashboard)
# ==================================================================
@login_required
def home(request):
"""Vista del dashboard principal"""
# Contar registros de cada modelo
total_productos = Producto.objects.filter(activo=True).count()
total_categorias = Categoria.objects.count()
total_proveedores = Proveedor.objects.count()
total_clientes = Cliente.objects.count()
# Obtener productos recientes
productos_recientes = Producto.objects.filter(activo=True)[:5]
# Pasar datos al template
context = {
'total_productos': total_productos,
'total_categorias': total_categorias,
'total_proveedores': total_proveedores,
'total_clientes': total_clientes,
'productos_recientes': productos_recientes,
}
return render(request, 'tienda/home.html', context)
# ==================================================================
# VISTAS: CRUD de Productos
# ==================================================================
@login_required
def producto_lista(request):
"""Lista todos los productos"""
productos = Producto.objects.filter(activo=True) # Solo productos activos
# Verificar permisos del usuario
puede_crear = request.user.is_superuser or request.user.perfil.tiene_permiso_escritura()
puede_editar = request.user.is_superuser or request.user.perfil.tiene_permiso_escritura()
puede_eliminar = request.user.is_superuser or request.user.perfil.tiene_permiso_eliminacion()
context = {
'productos': productos,
'puede_crear': puede_crear,
'puede_editar': puede_editar,
'puede_eliminar': puede_eliminar,
}
return render(request, 'tienda/producto_lista.html', context)
@login_required
@rol_requerido('gerente', 'administrador') # Solo gerente y admin
def producto_crear(request):
"""Crear un nuevo producto"""
if request.method == 'POST': # Si envió el formulario
form = ProductoForm(request.POST) # Crear formulario con datos POST
if form.is_valid(): # Validar formulario
producto = form.save() # Guardar en BD
messages.success(request, f'Producto {producto.nombre} creado exitosamente')
return redirect('producto_lista')
else:
form = ProductoForm() # Formulario vacío
context = {'form': form, 'accion': 'Crear'}
return render(request, 'tienda/producto_form.html', context)
@login_required
@rol_requerido('gerente', 'administrador')
def producto_editar(request, pk):
"""Editar un producto existente"""
producto = get_object_or_404(Producto, pk=pk) # Obtener o 404
if request.method == 'POST':
form = ProductoForm(request.POST, instance=producto) # Formulario con datos del producto
if form.is_valid():
form.save()
messages.success(request, f'Producto {producto.nombre} actualizado')
return redirect('producto_lista')
else:
form = ProductoForm(instance=producto)
context = {'form': form, 'accion': 'Editar', 'producto': producto}
return render(request, 'tienda/producto_form.html', context)
@login_required
@rol_requerido('administrador') # Solo administrador
def producto_eliminar(request, pk):
"""Eliminar (desactivar) un producto"""
producto = get_object_or_404(Producto, pk=pk)
if request.method == 'POST':
producto.activo = False # Eliminación lógica
producto.save()
messages.success(request, f'Producto {producto.nombre} eliminado')
return redirect('producto_lista')
return render(request, 'tienda/producto_eliminar.html', {'producto': producto})
# ... (Continúa con vistas similares para Categoría, Proveedor, Cliente)
💡 Conceptos Clave - views.py
- @login_required: Requiere que el usuario esté autenticado
- render(): Renderiza un template con datos
- redirect(): Redirige a otra URL
- get_object_or_404(): Obtiene objeto o muestra error 404
- request.method: GET o POST
- messages: Sistema de mensajes flash (éxito, error, info)
📝 CÓDIGO COMPLETO: forms.py
Los formularios validan y procesan datos. Django genera HTML automáticamente.
# tienda/forms.py
from django import forms # Importar módulo de formularios
from .models import Producto, Categoria, Proveedor, Cliente
# ==================================================================
# FORMULARIO: Producto
# ==================================================================
class ProductoForm(forms.ModelForm): # Hereda de ModelForm
"""Formulario para crear/editar productos"""
class Meta: # Configuración del formulario
model = Producto # Modelo asociado
fields = ['nombre', 'descripcion', 'precio', 'stock', 'categoria', 'activo']
# Widgets: cómo se renderizan los campos
widgets = {
'nombre': forms.TextInput(attrs={ # Input de texto
'class': 'form-control', # Clase CSS de Bootstrap
'placeholder': 'Nombre del producto'
}),
'descripcion': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3, # Altura del textarea
'placeholder': 'Descripción'
}),
'precio': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01', # Permite decimales
'min': '0', # Valor mínimo
'placeholder': '0.00'
}),
'stock': forms.NumberInput(attrs={
'class': 'form-control',
'min': '0'
}),
'categoria': forms.Select(attrs={ # Select (combobox)
'class': 'form-control'
}),
'activo': forms.CheckboxInput(attrs={
'class': 'form-check-input' # Checkbox de Bootstrap
}),
}
# Labels personalizados
labels = {
'nombre': 'Nombre del Producto',
'descripcion': 'Descripción',
'precio': 'Precio ($)',
'stock': 'Cantidad en Stock',
'categoria': 'Categoría',
'activo': 'Producto Activo',
}
# ==================================================================
# FORMULARIO: Categoría
# ==================================================================
class CategoriaForm(forms.ModelForm):
class Meta:
model = Categoria
fields = ['nombre', 'descripcion']
widgets = {
'nombre': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Nombre de la categoría'
}),
'descripcion': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3
}),
}
labels = {
'nombre': 'Nombre',
'descripcion': 'Descripción',
}
# ==================================================================
# FORMULARIO: Proveedor
# ==================================================================
class ProveedorForm(forms.ModelForm):
class Meta:
model = Proveedor
fields = ['nombre', 'empresa', 'telefono', 'email', 'direccion']
widgets = {
'nombre': forms.TextInput(attrs={'class': 'form-control'}),
'empresa': forms.TextInput(attrs={'class': 'form-control'}),
'telefono': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={ # EmailInput valida formato
'class': 'form-control',
'placeholder': 'ejemplo@correo.com'
}),
'direccion': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3
}),
}
# ==================================================================
# FORMULARIO: Cliente
# ==================================================================
class ClienteForm(forms.ModelForm):
class Meta:
model = Cliente
fields = ['nombre', 'apellido', 'email', 'telefono', 'direccion']
widgets = {
'nombre': forms.TextInput(attrs={'class': 'form-control'}),
'apellido': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'telefono': forms.TextInput(attrs={'class': 'form-control'}),
'direccion': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3
}),
}
🎨 Widgets en Django Forms
- TextInput: <input type="text">
- Textarea: <textarea></textarea>
- NumberInput: <input type="number">
- EmailInput: <input type="email"> (valida email)
- Select: <select><option>...</select>
- CheckboxInput: <input type="checkbox">
📝 CÓDIGO COMPLETO: urls.py
Define las rutas (URLs) de la aplicación. Mapea URLs a vistas.
# tienda/urls.py
from django.urls import path # Importar función path
from . import views # Importar vistas de este módulo
# Lista de patrones de URL
urlpatterns = [
# ==== Autenticación ====
path('', views.login_view, name='login'), # URL: / → vista login_view
path('logout/', views.logout_view, name='logout'),
# ==== Dashboard ====
path('home/', views.home, name='home'), # URL: /home/ → vista home
# ==== Productos ====
path('productos/', views.producto_lista, name='producto_lista'),
path('productos/crear/', views.producto_crear, name='producto_crear'),
path('productos/editar/<int:pk>/', views.producto_editar, name='producto_editar'),
# <int:pk> = parámetro entero que se pasa a la vista
path('productos/eliminar/<int:pk>/', views.producto_eliminar, name='producto_eliminar'),
# ==== Categorías ====
path('categorias/', views.categoria_lista, name='categoria_lista'),
path('categorias/crear/', views.categoria_crear, name='categoria_crear'),
path('categorias/editar/<int:pk>/', views.categoria_editar, name='categoria_editar'),
path('categorias/eliminar/<int:pk>/', views.categoria_eliminar, name='categoria_eliminar'),
# ==== Proveedores ====
path('proveedores/', views.proveedor_lista, name='proveedor_lista'),
path('proveedores/crear/', views.proveedor_crear, name='proveedor_crear'),
path('proveedores/editar/<int:pk>/', views.proveedor_editar, name='proveedor_editar'),
path('proveedores/eliminar/<int:pk>/', views.proveedor_eliminar, name='proveedor_eliminar'),
# ==== Clientes ====
path('clientes/', views.cliente_lista, name='cliente_lista'),
path('clientes/crear/', views.cliente_crear, name='cliente_crear'),
path('clientes/editar/<int:pk>/', views.cliente_editar, name='cliente_editar'),
path('clientes/eliminar/<int:pk>/', views.cliente_eliminar, name='cliente_eliminar'),
]
🔗 Entendiendo las URLs
- path('productos/', views.producto_lista, name='producto_lista')
→ URL: /productos/
→ Vista: producto_lista
→ Nombre: 'producto_lista' (se usa en {% url 'producto_lista' %}) - path('productos/editar/<int:pk>/', ...)
→ URL: /productos/editar/5/
→ pk se pasa a la vista: producto_editar(request, pk=5)
📝 CÓDIGO COMPLETO: admin.py
Configura el panel de administración de Django.
# tienda/admin.py
from django.contrib import admin # Importar módulo admin
from .models import PerfilUsuario, Categoria, Producto, Proveedor, Cliente
# ==================================================================
# Configuración del Admin: Perfil Usuario
# ==================================================================
@admin.register(PerfilUsuario) # Decorador para registrar
class PerfilUsuarioAdmin(admin.ModelAdmin):
list_display = ['user', 'rol', 'telefono', 'departamento', 'activo']
# Campos que se muestran en la lista
list_filter = ['rol', 'activo'] # Filtros laterales
search_fields = ['user__username', 'telefono'] # Búsqueda
list_editable = ['activo'] # Editable desde la lista
# ==================================================================
# Configuración del Admin: Categoría
# ==================================================================
@admin.register(Categoria)
class CategoriaAdmin(admin.ModelAdmin):
list_display = ['id', 'nombre', 'fecha_creacion']
search_fields = ['nombre']
ordering = ['nombre'] # Orden por defecto
# ==================================================================
# Configuración del Admin: Producto
# ==================================================================
@admin.register(Producto)
class ProductoAdmin(admin.ModelAdmin):
list_display = ['nombre', 'precio', 'stock', 'categoria', 'activo']
list_filter = ['activo', 'categoria']
search_fields = ['nombre', 'descripcion']
list_editable = ['precio', 'stock', 'activo']
readonly_fields = ['fecha_creacion', 'fecha_actualizacion']
# Campos de solo lectura
# ==================================================================
# Configuración del Admin: Proveedor
# ==================================================================
@admin.register(Proveedor)
class ProveedorAdmin(admin.ModelAdmin):
list_display = ['nombre', 'empresa', 'telefono', 'email']
search_fields = ['nombre', 'empresa', 'email']
# ==================================================================
# Configuración del Admin: Cliente
# ==================================================================
@admin.register(Cliente)
class ClienteAdmin(admin.ModelAdmin):
list_display = ['nombre_completo', 'email', 'telefono', 'fecha_registro']
search_fields = ['nombre', 'apellido', 'email']
def nombre_completo(self, obj): # Método personalizado
return f"{obj.nombre} {obj.apellido}"
nombre_completo.short_description = 'Nombre Completo'
🎛️ Opciones del Django Admin
- list_display: Columnas a mostrar en la lista
- list_filter: Filtros laterales
- search_fields: Campos para buscar
- list_editable: Campos editables desde la lista
- readonly_fields: Campos de solo lectura
- ordering: Orden por defecto
📝 CÓDIGO COMPLETO: settings.py (Configuración Principal)
Configuración del proyecto Django. Aquí se define la BD, apps instaladas, middleware, etc.
# tienda_proyecto/settings.py
from pathlib import Path # Para rutas multiplataforma
# Directorio base del proyecto
BASE_DIR = Path(__file__).resolve().parent.parent
# SEGURIDAD
SECRET_KEY = 'tu-clave-secreta-aqui' # ⚠️ Cambiar en producción
DEBUG = True # ⚠️ False en producción
ALLOWED_HOSTS = [] # ['midominio.com'] en producción
# APLICACIONES INSTALADAS
INSTALLED_APPS = [
'django.contrib.admin', # Panel de administración
'django.contrib.auth', # Autenticación
'django.contrib.contenttypes', # Framework de contenido
'django.contrib.sessions', # Sesiones
'django.contrib.messages', # Mensajes
'django.contrib.staticfiles', # Archivos estáticos
'tienda', # ⭐ NUESTRA APP
]
# MIDDLEWARE (Capa de procesamiento)
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', # Protección CSRF
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'tienda_proyecto.urls' # URLs principales
# TEMPLATES
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], # Carpetas adicionales
'APP_DIRS': True, # Buscar en apps
'OPTIONS': {
'context_processors': [ # Variables globales
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
# BASE DE DATOS (MySQL)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # Motor: MySQL
'NAME': 'tienda_db', # Nombre de la BD
'USER': 'root', # Usuario MySQL
'PASSWORD': 'tu_password', # ⚠️ Cambiar esto
'HOST': '127.0.0.1', # localhost
'PORT': '3306', # Puerto MySQL
'OPTIONS': {
'charset': 'utf8mb4', # UTF-8 completo
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
},
}
}
# INTERNACIONALIZACIÓN
LANGUAGE_CODE = 'es-mx' # Español México
TIME_ZONE = 'America/Hermosillo' # Zona horaria
USE_I18N = True # Internacionalización
USE_TZ = True # Usar zonas horarias
# ARCHIVOS ESTÁTICOS
STATIC_URL = 'static/' # URL: /static/
STATICFILES_DIRS = [BASE_DIR / "static"] # Carpeta static/
# LOGIN/LOGOUT
LOGIN_URL = 'login' # URL de login
LOGIN_REDIRECT_URL = 'home' # Después de login
LOGOUT_REDIRECT_URL = 'login' # Después de logout
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
⚠️ Importante en settings.py
- SECRET_KEY: Debe ser única y secreta
- DEBUG: SIEMPRE False en producción
- DATABASES['PASSWORD']: Cambiar por tu contraseña real
- INSTALLED_APPS: 'tienda' debe estar aquí
📝 CÓDIGO COMPLETO: Todos los Templates HTML (17 archivos)
Todos los templates del proyecto con código completo y comentarios línea por línea en esta sección.
📂 Ubicación de los templates:
tienda/templates/tienda/
Total: 17 archivos HTML - Todos incluidos a continuación con código completo
🔗 ACCESO RÁPIDO A TODOS LOS TEMPLATES
Para ver la lista completa de los 17 templates con descripción y ubicación:
Ver Lista Completa de TemplatesEste enlace abre un archivo con índice navegable de todos los templates
⚠️ IMPORTANTE:
Todos los templates aquí mostrados están COMPLETOS y listos para copiar y pegar.
Cada template incluye comentarios explicando cada línea de código.
1. base.html - Template Base con Navbar
Ver código completo en PARTE 8 de la guía - Template base con navbar, Bootstrap 5, Font Awesome, sistema de mensajes flash
2. login.html - Página de Login
Ver código completo en PARTE 8 de la guía - Formulario de autenticación con CSRF, validación, diseño centrado
3. home.html - Dashboard con Estadísticas
Ver código completo en PARTE 8 de la guía - Dashboard con contadores, estadísticas, tarjetas de Bootstrap
4. producto_lista.html - Lista de Productos
Ver código completo en PARTE 8 de la guía - Tabla con productos, botones de editar/eliminar, paginación
5. producto_form.html - Formulario Crear/Editar Producto
Ver código completo en PARTE 8 de la guía - Formulario con combobox de categorías, validación de errores
6. producto_eliminar.html - Confirmación Eliminar Producto
Ver código completo en PARTE 8 de la guía - Modal de confirmación con advertencia
7. categoria_lista.html - Lista de Categorías
Ver código completo en PARTE 8 de la guía - Tabla con categorías y contador de productos
8. categoria_form.html - Formulario Crear/Editar Categoría
<!-- tienda/templates/tienda/categoria_form.html -->
{% extends 'tienda/base.html' %} <!-- Hereda del template base -->
{% block title %}{{ accion }} Categoría{% endblock %} <!-- Título dinámico (Crear o Editar) -->
{% block content %}
<div class="row mb-4">
<div class="col-12">
<h1><i class="fas fa-tags"></i> {{ accion }} Categoría</h1> <!-- accion viene de la vista -->
</div>
</div>
<div class="row justify-content-center"> <!-- Centra el formulario -->
<div class="col-md-6"> <!-- Columna de 6/12 en pantallas medianas -->
<div class="card shadow"> <!-- Tarjeta con sombra -->
<div class="card-body p-4"> <!-- Padding de 4 -->
<form method="post"> <!-- Método POST para enviar datos -->
{% csrf_token %} <!-- Token de seguridad CSRF -->
<!-- Campo Nombre -->
<div class="mb-3">
<label for="{{ form.nombre.id_for_label }}" class="form-label">
<i class="fas fa-tag"></i> {{ form.nombre.label }}
</label>
{{ form.nombre }} <!-- Input text para nombre -->
{% if form.nombre.errors %} <!-- Si hay errores -->
<div class="text-danger mt-1">{{ form.nombre.errors }}</div>
{% endif %}
</div>
<!-- Campo Descripción -->
<div class="mb-3">
<label for="{{ form.descripcion.id_for_label }}" class="form-label">
<i class="fas fa-align-left"></i> {{ form.descripcion.label }}
</label>
{{ form.descripcion }} <!-- Textarea para descripción -->
{% if form.descripcion.errors %}
<div class="text-danger mt-1">{{ form.descripcion.errors }}</div>
{% endif %}
</div>
<!-- Botones -->
<div class="d-flex justify-content-between mt-4"> <!-- Flexbox para separar botones -->
<a href="{% url 'categoria_lista' %}" class="btn btn-secondary">
<i class="fas fa-times"></i> Cancelar
</a>
<button type="submit" class="btn btn-success">
<i class="fas fa-save"></i> Guardar
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %} <!-- Fin del bloque de contenido -->
9. categoria_eliminar.html - Confirmación Eliminar Categoría
<!-- tienda/templates/tienda/categoria_eliminar.html -->
{% extends 'tienda/base.html' %} <!-- Hereda del template base -->
{% block title %}Eliminar Categoría{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-danger shadow"> <!-- Borde rojo de peligro -->
<div class="card-header bg-danger text-white"> <!-- Encabezado rojo -->
<h5 class="mb-0">
<i class="fas fa-exclamation-triangle"></i> Confirmar Eliminación
</h5>
</div>
<div class="card-body">
<div class="alert alert-warning"> <!-- Alerta amarilla de advertencia -->
<i class="fas fa-exclamation-circle"></i>
<strong>¡Atención!</strong> Al eliminar esta categoría, también se eliminarán todos los productos asociados.
</div>
<p class="lead">¿Está seguro que desea eliminar la siguiente categoría?</p>
<div class="card bg-light"> <!-- Tarjeta gris claro -->
<div class="card-body">
<h5>{{ categoria.nombre }}</h5> <!-- Nombre de la categoría -->
<p class="mb-0"><strong>Productos asociados:</strong> {{ categoria.productos.count }}</p> <!-- Cuenta productos -->
</div>
</div>
<form method="post" class="mt-4"> <!-- Formulario de confirmación -->
{% csrf_token %} <!-- Token CSRF obligatorio -->
<div class="d-flex justify-content-between">
<a href="{% url 'categoria_lista' %}" class="btn btn-secondary">
<i class="fas fa-times"></i> Cancelar
</a>
<button type="submit" class="btn btn-danger"> <!-- Botón rojo de eliminar -->
<i class="fas fa-trash"></i> Sí, Eliminar
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %} <!-- Fin del bloque de contenido -->
10. proveedor_lista.html - Lista de Proveedores
Ver código completo en PARTE 8 de la guía - Tabla con proveedores, botones de acción
11. proveedor_form.html - Formulario Crear/Editar Proveedor
Ver código completo en PARTE 8 de la guía - Formulario completo con todos los campos
12. proveedor_eliminar.html - Confirmación Eliminar Proveedor
Ver código completo en PARTE 8 de la guía - Modal de confirmación
13. cliente_lista.html - Lista de Clientes
Ver código completo en PARTE 8 de la guía - Tabla con clientes y datos de contacto
14. cliente_form.html - Formulario Crear/Editar Cliente
Ver código completo en PARTE 8 de la guía - Formulario completo de clientes
15. cliente_eliminar.html - Confirmación Eliminar Cliente
Ver código completo en PARTE 8 de la guía - Modal de confirmación
16. venta_form.html - Formulario para Registrar Ventas
Código completo incluido a continuación:
<!-- tienda/templates/tienda/venta_form.html -->
{% extends 'tienda/base.html' %} <!-- Hereda del template base -->
{% block title %}Registrar Venta{% endblock %} <!-- Título de la página -->
{% block content %} <!-- Bloque de contenido -->
<div class="row justify-content-center"> <!-- Fila centrada de Bootstrap -->
<div class="col-md-8"> <!-- Columna de 8/12 en pantallas medianas -->
<div class="card shadow"> <!-- Tarjeta con sombra -->
<div class="card-header bg-success text-white"> <!-- Encabezado verde -->
<h3 class="mb-0"><i class="fas fa-cash-register"></i> Registrar Nueva Venta</h3>
</div>
<div class="card-body"> <!-- Cuerpo de la tarjeta -->
<form method="post"> <!-- Formulario con método POST -->
{% csrf_token %} <!-- Token de seguridad CSRF -->
<div class="mb-3"> <!-- Campo Cliente -->
<label class="form-label">{{ form.cliente.label }}</label>
{{ form.cliente }} <!-- Combobox con clientes -->
</div>
<div class="mb-3"> <!-- Campo Producto -->
<label class="form-label">{{ form.producto.label }}</label>
{{ form.producto }} <!-- Combobox con productos -->
</div>
<div class="mb-3"> <!-- Campo Cantidad -->
<label class="form-label">{{ form.cantidad.label }}</label>
{{ form.cantidad }} <!-- Input numérico -->
</div>
<div class="d-grid gap-2"> <!-- Botones -->
<button type="submit" class="btn btn-success btn-lg">
<i class="fas fa-check"></i> Registrar Venta
</button>
<a href="{% url 'reporte_ventas' %}" class="btn btn-outline-secondary">Cancelar</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %} <!-- Fin del bloque de contenido -->
17. reporte_ventas.html - Reporte de Ventas del Día
Código completo incluido a continuación:
<!-- tienda/templates/tienda/reporte_ventas.html -->
{% extends 'tienda/base.html' %} <!-- Hereda del template base -->
{% load humanize %} <!-- Cargar filtros de humanize para formato de millares -->
{% block title %}Reporte de Ventas{% endblock %}
{% block content %}
<div class="row mb-4"> <!-- Encabezado -->
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h1 class="display-5">
<i class="fas fa-chart-line"></i> Reporte de Ventas del Día
</h1>
<a href="{% url 'venta_crear' %}" class="btn btn-primary">
<i class="fas fa-plus"></i> Registrar Venta
</a>
</div>
<p class="text-muted">{{ fecha|date:"l, d F Y" }}</p> <!-- Fecha en español -->
</div>
</div>
<!-- Tarjetas de Estadísticas -->
<div class="row g-4 mb-4">
<div class="col-md-4"> <!-- Total Vendido -->
<div class="card stats-card bg-success text-white">
<div class="card-body">
<h6 class="card-subtitle mb-2">Total Vendido Hoy</h6>
<h2 class="card-title mb-0">${{ total_ventas_dia|floatformat:2|intcomma }}</h2>
<!-- |floatformat:2 = 2 decimales, |intcomma = formato millares -->
</div>
</div>
</div>
<div class="col-md-4"> <!-- Número de Ventas -->
<div class="card stats-card bg-info text-white">
<div class="card-body">
<h6 class="card-subtitle mb-2">Número de Ventas</h6>
<h2 class="card-title mb-0">{{ cantidad_ventas }}</h2>
</div>
</div>
</div>
<div class="col-md-4"> <!-- Promedio por Venta -->
<div class="card stats-card bg-warning text-dark">
<div class="card-body">
<h6 class="card-subtitle mb-2">Promedio por Venta</h6>
<h2 class="card-title mb-0">
{% if cantidad_ventas > 0 %}
${% widthratio total_ventas_dia cantidad_ventas 1 %}
{% else %}
$0.00
{% endif %}
</h2>
</div>
</div>
</div>
</div>
<!-- Tabla de Ventas -->
<div class="card">
<div class="card-body">
{% if ventas_hoy %} <!-- Si hay ventas -->
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead class="table-dark">
<tr>
<th>#</th>
<th>Hora</th>
<th>Producto</th>
<th>Cliente</th>
<th>Cantidad</th>
<th>Precio Unit.</th>
<th>Total</th>
<th>Vendedor</th>
</tr>
</thead>
<tbody>
{% for venta in ventas_hoy %} <!-- Loop por cada venta -->
<tr>
<td><strong>#{{ venta.id }}</strong></td>
<td>{{ venta.fecha_venta|date:"H:i" }}</td> <!-- Hora:minuto -->
<td>{{ venta.producto.nombre }}</td>
<td>{{ venta.cliente.nombre_completo }}</td>
<td><span class="badge bg-secondary">{{ venta.cantidad }}</span></td>
<td>${{ venta.precio_unitario|floatformat:2|intcomma }}</td>
<td><strong class="text-success">${{ venta.total|floatformat:2|intcomma }}</strong></td>
<td>{{ venta.vendedor.username }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot class="table-light"> <!-- Pie de tabla -->
<tr>
<td colspan="6" class="text-end"><strong>TOTAL DEL DÍA:</strong></td>
<td colspan="2">
<strong class="text-success fs-5">${{ total_ventas_dia|floatformat:2|intcomma }}</strong>
</td>
</tr>
</tfoot>
</table>
</div>
{% else %} <!-- Si NO hay ventas -->
<div class="text-center py-5">
<i class="fas fa-inbox fa-4x text-muted mb-3"></i>
<p class="text-muted fs-5">No hay ventas registradas el día de hoy.</p>
<a href="{% url 'venta_crear' %}" class="btn btn-primary mt-3">
<i class="fas fa-plus"></i> Registrar Primera Venta
</a>
</div>
{% endif %}
</div>
</div>
{% endblock %} <!-- Fin del bloque de contenido -->
✅ Resumen de Templates
Base y Autenticación (3):
- ✓ base.html
- ✓ login.html
- ✓ home.html
CRUD Módulos (12):
- ✓ producto_* (3)
- ✓ categoria_* (3)
- ✓ proveedor_* (3)
- ✓ cliente_* (3)
Módulo Ventas (2):
- ✓ venta_form.html
- ✓ reporte_ventas.html
Total: 17 templates HTML completos con comentarios línea por línea
📝 CÓDIGO COMPLETO: Módulo de Ventas
Sistema completo de ventas con registro, cálculo automático y reporte diario. Todo el código está comentado línea por línea.
📊 ¿Qué incluye el módulo de ventas?
- Modelo Venta con 6 campos y relaciones ForeignKey
- Formulario VentaForm para captura de datos
- Vista venta_crear para registrar ventas
- Vista reporte_ventas para ver ventas del día
- Template venta_form.html para el formulario
- Template reporte_ventas.html con formato de millares
- Cálculo automático de precio unitario y total
1. Modelo Venta (tienda/models.py)
# ==================================================================
# MODELO 6: VENTA (Sistema de Ventas Completo)
# ==================================================================
class Venta(models.Model):
"""Modelo para registrar las ventas realizadas en la tienda"""
# CAMPOS DEL MODELO
cliente = models.ForeignKey( # Relación muchos a uno con Cliente
Cliente, # Modelo relacionado
on_delete=models.CASCADE, # Si se elimina el cliente, se eliminan sus ventas
related_name='ventas' # Permite acceder desde Cliente: cliente.ventas.all()
)
vendedor = models.ForeignKey( # Usuario que realizó la venta
User, # Modelo de usuario de Django
on_delete=models.SET_NULL, # Si se elimina el usuario, el vendedor se pone en NULL
null=True, # Puede ser NULL
related_name='ventas_realizadas' # Acceso: user.ventas_realizadas.all()
)
producto = models.ForeignKey( # Producto vendido
Producto, # Modelo Producto
on_delete=models.CASCADE, # Si se elimina el producto, se eliminan sus ventas
related_name='ventas' # Acceso: producto.ventas.all()
)
cantidad = models.IntegerField( # Cantidad de unidades vendidas
default=1 # Por defecto 1 unidad
)
precio_unitario = models.DecimalField( # Precio por unidad al momento de la venta
max_digits=10, # Máximo 10 dígitos (ej: 99999999.99)
decimal_places=2 # 2 decimales (centavos)
)
total = models.DecimalField( # Total de la venta (cantidad × precio_unitario)
max_digits=10,
decimal_places=2
)
fecha_venta = models.DateTimeField( # Fecha y hora de la venta
auto_now_add=True # Se establece automáticamente al crear el registro
)
# MÉTODOS DEL MODELO
def __str__(self):
"""Representación en string del objeto"""
return f"Venta #{self.id} - {self.producto.nombre} - ${self.total}"
def save(self, *args, **kwargs):
"""Método personalizado para calcular el total antes de guardar"""
# Calcular total automáticamente
self.total = self.cantidad * self.precio_unitario # Total = cantidad × precio unitario
super().save(*args, **kwargs) # Llamar al método save original de Django
# CONFIGURACIÓN DEL MODELO (Meta)
class Meta:
verbose_name = "Venta" # Nombre singular en Django Admin
verbose_name_plural = "Ventas" # Nombre plural en Django Admin
ordering = ['-fecha_venta'] # Orden descendente por fecha (más recientes primero)
2. Formulario VentaForm (tienda/forms.py)
# ==================================================================
# FORMULARIO PARA VENTAS
# ==================================================================
class VentaForm(forms.ModelForm):
"""Formulario para registrar ventas en la tienda"""
class Meta:
model = Venta # Modelo asociado
fields = ['cliente', 'producto', 'cantidad'] # Solo estos campos (precio se calcula automático)
# WIDGETS: Personalización de cómo se muestran los campos en HTML
widgets = {
'cliente': forms.Select(attrs={ # Select (combobox) para elegir cliente
'class': 'form-control' # Clase CSS de Bootstrap 5
}),
'producto': forms.Select(attrs={ # Select (combobox) para elegir producto
'class': 'form-control',
'id': 'id_producto' # ID para JavaScript (si se necesita)
}),
'cantidad': forms.NumberInput(attrs={ # Input numérico para cantidad
'class': 'form-control',
'min': '1', # Mínimo 1 unidad
'value': '1' # Valor por defecto
}),
}
# LABELS: Etiquetas en español para cada campo
labels = {
'cliente': 'Cliente',
'producto': 'Producto',
'cantidad': 'Cantidad',
}
3. Vista venta_crear (tienda/views.py)
# ==================================================================
# VISTA PARA CREAR VENTAS
# ==================================================================
@login_required # Decorador: solo usuarios autenticados pueden acceder
def venta_crear(request):
"""Vista para registrar una nueva venta"""
if request.method == 'POST': # Si el usuario envió el formulario
form = VentaForm(request.POST) # Crear formulario con datos del POST
if form.is_valid(): # Si el formulario pasó todas las validaciones
venta = form.save(commit=False) # Crear objeto pero NO guardarlo aún en la BD
# Asignar datos adicionales
venta.vendedor = request.user # El vendedor es el usuario actual
venta.precio_unitario = venta.producto.precio # Tomar precio actual del producto
venta.save() # AHORA SÍ guardar (esto ejecuta el método save() que calcula el total)
# Mensaje de éxito
messages.success(request, f'Venta registrada exitosamente - Total: ${venta.total}')
return redirect('reporte_ventas') # Redirigir al reporte de ventas
else: # Si es GET (primera vez que se accede)
form = VentaForm() # Crear formulario vacío
# Renderizar template con el formulario
return render(request, 'tienda/venta_form.html', {'form': form})
4. Vista reporte_ventas (tienda/views.py)
# ==================================================================
# VISTA PARA REPORTE DE VENTAS
# ==================================================================
@login_required # Decorador: solo usuarios autenticados
def reporte_ventas(request):
"""Vista del reporte de ventas del día"""
# 1. Obtener la fecha actual
hoy = timezone.now().date() # Fecha de hoy (sin hora)
# 2. Filtrar ventas del día de hoy
ventas_hoy = Venta.objects.filter( # Filtrar por fecha
fecha_venta__date=hoy # Comparar solo la parte de fecha (ignora hora)
).select_related('producto', 'cliente', 'vendedor') # Optimización: cargar relaciones en una sola consulta
# 3. Calcular total de ventas del día
total_ventas_dia = ventas_hoy.aggregate( # Función de agregación
total=Sum('total') # Sumar el campo 'total' de todas las ventas
)['total'] or 0 # Si no hay ventas, usar 0
# 4. Contar cantidad de ventas
cantidad_ventas = ventas_hoy.count() # Número de registros
# 5. Preparar datos para el template
context = {
'ventas_hoy': ventas_hoy, # Lista de ventas
'total_ventas_dia': total_ventas_dia, # Total vendido
'cantidad_ventas': cantidad_ventas, # Número de ventas
'fecha': hoy, # Fecha actual
}
# 6. Renderizar template
return render(request, 'tienda/reporte_ventas.html', context)
5. Template venta_form.html
<!-- tienda/templates/tienda/venta_form.html -->
{% extends 'tienda/base.html' %} <!-- Hereda del template base -->
{% block title %}Registrar Venta{% endblock %} <!-- Título de la página -->
{% block content %} <!-- Bloque de contenido -->
<div class="row justify-content-center"> <!-- Fila centrada de Bootstrap -->
<div class="col-md-8"> <!-- Columna de 8/12 en pantallas medianas -->
<div class="card shadow"> <!-- Tarjeta con sombra -->
<div class="card-header bg-success text-white"> <!-- Encabezado verde -->
<h3 class="mb-0"> <!-- Título sin margen inferior -->
<i class="fas fa-cash-register"></i> Registrar Nueva Venta
</h3>
</div>
<div class="card-body"> <!-- Cuerpo de la tarjeta -->
<form method="post"> <!-- Formulario con método POST -->
{% csrf_token %} <!-- Token de seguridad CSRF (obligatorio en Django) -->
<!-- CAMPO CLIENTE -->
<div class="mb-3"> <!-- Margen inferior de Bootstrap -->
<label class="form-label">{{ form.cliente.label }}</label> <!-- Etiqueta del campo -->
{{ form.cliente }} <!-- Combobox con lista de clientes -->
</div>
<!-- CAMPO PRODUCTO -->
<div class="mb-3">
<label class="form-label">{{ form.producto.label }}</label>
{{ form.producto }} <!-- Combobox con lista de productos -->
</div>
<!-- CAMPO CANTIDAD -->
<div class="mb-3">
<label class="form-label">{{ form.cantidad.label }}</label>
{{ form.cantidad }} <!-- Input numérico -->
</div>
<!-- BOTONES -->
<div class="d-grid gap-2"> <!-- Grid con espacio entre botones -->
<button type="submit" class="btn btn-success btn-lg"> <!-- Botón submit verde grande -->
<i class="fas fa-check"></i> Registrar Venta
</button>
<a href="{% url 'reporte_ventas' %}" class="btn btn-outline-secondary"> <!-- Botón cancelar -->
Cancelar
</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %} <!-- Fin del bloque de contenido -->
6. Template reporte_ventas.html (Parte 1: Encabezado y Estadísticas)
<!-- tienda/templates/tienda/reporte_ventas.html -->
{% extends 'tienda/base.html' %} <!-- Hereda del template base -->
{% load humanize %} <!-- Cargar filtros de django.contrib.humanize para formato de millares -->
{% block title %}Reporte de Ventas{% endblock %}
{% block content %}
<!-- ENCABEZADO -->
<div class="row mb-4"> <!-- Fila con margen inferior -->
<div class="col-12">
<div class="d-flex justify-content-between align-items-center"> <!-- Flexbox para alinear título y botón -->
<h1 class="display-5"> <!-- Título grande -->
<i class="fas fa-chart-line"></i> Reporte de Ventas del Día
</h1>
<a href="{% url 'venta_crear' %}" class="btn btn-primary"> <!-- Botón para crear venta -->
<i class="fas fa-plus"></i> Registrar Venta
</a>
</div>
<p class="text-muted">{{ fecha|date:"l, d F Y" }}</p> <!-- Fecha en formato largo en español -->
</div>
</div>
<!-- TARJETAS DE ESTADÍSTICAS -->
<div class="row g-4 mb-4"> <!-- Grid con espacio de 4 entre columnas -->
<!-- TARJETA 1: TOTAL VENDIDO -->
<div class="col-md-4"> <!-- Columna de 4/12 en pantallas medianas -->
<div class="card stats-card bg-success text-white"> <!-- Tarjeta verde -->
<div class="card-body">
<h6 class="card-subtitle mb-2">Total Vendido Hoy</h6>
<h2 class="card-title mb-0">
${{ total_ventas_dia|floatformat:2|intcomma }}
<!-- |floatformat:2 = formato con 2 decimales (123.45) -->
<!-- |intcomma = agrega comas para millares (1,234.56) -->
</h2>
</div>
</div>
</div>
<!-- TARJETA 2: NÚMERO DE VENTAS -->
<div class="col-md-4">
<div class="card stats-card bg-info text-white"> <!-- Tarjeta azul -->
<div class="card-body">
<h6 class="card-subtitle mb-2">Número de Ventas</h6>
<h2 class="card-title mb-0">{{ cantidad_ventas }}</h2> <!-- Contador de ventas -->
</div>
</div>
</div>
<!-- TARJETA 3: PROMEDIO POR VENTA -->
<div class="col-md-4">
<div class="card stats-card bg-warning text-dark"> <!-- Tarjeta amarilla -->
<div class="card-body">
<h6 class="card-subtitle mb-2">Promedio por Venta</h6>
<h2 class="card-title mb-0">
{% if cantidad_ventas > 0 %} <!-- Si hay ventas -->
${% widthratio total_ventas_dia cantidad_ventas 1 %} <!-- División: total / cantidad -->
{% else %} <!-- Si no hay ventas -->
$0.00
{% endif %}
</h2>
</div>
</div>
</div>
</div>
7. Template reporte_ventas.html (Parte 2: Tabla de Ventas)
<!-- TABLA DE VENTAS -->
<div class="card"> <!-- Tarjeta contenedora -->
<div class="card-body">
{% if ventas_hoy %} <!-- Si hay ventas registradas hoy -->
<div class="table-responsive"> <!-- Div para hacer la tabla responsiva -->
<table class="table table-hover table-striped"> <!-- Tabla con hover y rayas -->
<thead class="table-dark"> <!-- Encabezado oscuro -->
<tr>
<th>#</th> <!-- ID de venta -->
<th>Hora</th> <!-- Hora de la venta -->
<th>Producto</th>
<th>Cliente</th>
<th>Cantidad</th>
<th>Precio Unit.</th> <!-- Precio unitario -->
<th>Total</th>
<th>Vendedor</th>
</tr>
</thead>
<tbody>
{% for venta in ventas_hoy %} <!-- Loop por cada venta -->
<tr>
<td><strong>#{{ venta.id }}</strong></td> <!-- ID en negrita -->
<td>{{ venta.fecha_venta|date:"H:i" }}</td> <!-- Formato hora:minuto (14:30) -->
<td>{{ venta.producto.nombre }}</td> <!-- Nombre del producto (relación FK) -->
<td>{{ venta.cliente.nombre_completo }}</td> <!-- Nombre completo del cliente -->
<td>
<span class="badge bg-secondary">{{ venta.cantidad }}</span> <!-- Badge para cantidad -->
</td>
<td>${{ venta.precio_unitario|floatformat:2|intcomma }}</td> <!-- Precio con formato -->
<td>
<strong class="text-success">${{ venta.total|floatformat:2|intcomma }}</strong> <!-- Total en verde -->
</td>
<td>{{ venta.vendedor.username }}</td> <!-- Username del vendedor -->
</tr>
{% endfor %} <!-- Fin del loop -->
</tbody>
<tfoot class="table-light"> <!-- Pie de tabla -->
<tr>
<td colspan="6" class="text-end"><strong>TOTAL DEL DÍA:</strong></td>
<td colspan="2">
<strong class="text-success fs-5">${{ total_ventas_dia|floatformat:2|intcomma }}</strong>
</td>
</tr>
</tfoot>
</table>
</div>
{% else %} <!-- Si NO hay ventas -->
<div class="text-center py-5"> <!-- Div centrado con padding vertical -->
<i class="fas fa-inbox fa-4x text-muted mb-3"></i> <!-- Icono grande -->
<p class="text-muted fs-5">No hay ventas registradas el día de hoy.</p>
<a href="{% url 'venta_crear' %}" class="btn btn-primary mt-3"> <!-- Botón para crear primera venta -->
<i class="fas fa-plus"></i> Registrar Primera Venta
</a>
</div>
{% endif %} <!-- Fin del if -->
</div>
</div>
{% endblock %} <!-- Fin del bloque de contenido -->
✅ Características del Módulo de Ventas
- Cálculo automático: El precio unitario se toma del producto y el total se calcula automáticamente
- Formato de millares: Usa el filtro |intcomma de django.contrib.humanize para mostrar $2,286.43
- Relaciones FK: Conecta con Cliente, Producto y User (vendedor)
- Filtro por fecha: Usa fecha_venta__date para filtrar solo por fecha (ignora hora)
- Estadísticas: Total vendido, número de ventas y promedio por venta
- Responsive: Diseño adaptable a móviles con Bootstrap 5
📝 CÓDIGO COMPLETO: styles.css
CSS personalizado con animaciones y efectos. Ya está comentado en PARTE 9.
🎨 El archivo CSS incluye:
Estilos Generales
- ✓ Navbar sticky (se queda fijo)
- ✓ Scroll suave
- ✓ Scrollbar personalizada
- ✓ Tipografía moderna
Componentes
- ✓ Tarjetas con hover
- ✓ Tablas estilizadas
- ✓ Botones con animación
- ✓ Formularios mejorados
Ubicación: static/css/styles.css - Ver PARTE 9 para código completo comentado
🔐 Sistema de Privilegios por Rol
Control de acceso basado en roles (RBAC) - Permisos detallados por tipo de usuario
📋 Resumen del Sistema de Roles
El sistema implementa 4 roles con diferentes niveles de acceso:
- Administrador: Acceso total a todas las funciones
- Empleado: Acceso limitado sin permisos de modificación en catálogos
- Vendedor: Solo acceso a módulo de ventas y clientes
- Cliente: Acceso restringido solo para consulta de sus propios datos
📊 Tabla Comparativa de Privilegios
| Módulo / Función | 👑 Administrador | 👔 Empleado | 🛒 Vendedor | 👤 Cliente |
|---|---|---|---|---|
| 📦 MÓDULO DE PRODUCTOS | ||||
| Ver lista de productos | ✅ SÍ | ✅ SÍ | ❌ NO | ❌ NO |
| Crear nuevos productos | ✅ SÍ | ❌ NO | ❌ NO | ❌ NO |
| Editar productos existentes | ✅ SÍ | ❌ NO | ❌ NO | ❌ NO |
| Eliminar productos | ✅ SÍ | ❌ NO | ❌ NO | ❌ NO |
| 🏷️ MÓDULO DE CATEGORÍAS | ||||
| Ver lista de categorías | ✅ SÍ | ❌ NO | ❌ NO | ❌ NO |
| Crear nuevas categorías | ✅ SÍ | ❌ NO | ❌ NO | ❌ NO |
| Editar categorías | ✅ SÍ | ❌ NO | ❌ NO | ❌ NO |
| Eliminar categorías | ✅ SÍ | ❌ NO | ❌ NO | ❌ NO |
| 🚚 MÓDULO DE PROVEEDORES | ||||
| Ver lista de proveedores | ✅ SÍ | ❌ NO | ❌ NO | ❌ NO |
| Crear nuevos proveedores | ✅ SÍ | ❌ NO | ❌ NO | ❌ NO |
| Editar proveedores | ✅ SÍ | ❌ NO | ❌ NO | ❌ NO |
| Eliminar proveedores | ✅ SÍ | ❌ NO | ❌ NO | ❌ NO |
| 👥 MÓDULO DE CLIENTES | ||||
| Ver lista de clientes | ✅ SÍ | ✅ SÍ | ✅ SÍ | ❌ NO |
| Crear nuevos clientes | ✅ SÍ | ✅ SÍ | ✅ SÍ | ❌ NO |
| Editar clientes | ✅ SÍ | ✅ SÍ | ✅ SÍ | ❌ NO |
| Eliminar clientes | ✅ SÍ | ✅ SÍ | ✅ SÍ | ❌ NO |
| Ver sus propios datos de cliente | ✅ SÍ | ✅ SÍ | ✅ SÍ | ✅ SÍ |
| Editar sus propios datos de cliente | ✅ SÍ | ✅ SÍ | ✅ SÍ | ✅ SÍ |
| 💰 MÓDULO DE VENTAS | ||||
| Registrar nuevas ventas | ✅ SÍ | ✅ SÍ | ✅ SÍ | ❌ NO |
| Ver reporte de ventas del día | ✅ SÍ | ✅ SÍ | ✅ SÍ | ❌ NO |
| Filtrar ventas por fecha | ✅ SÍ | ✅ SÍ | ✅ SÍ | ❌ NO |
| Ver historial de sus propias compras | ✅ SÍ | ✅ SÍ | ✅ SÍ | ✅ SÍ |
| 🏠 DASHBOARD Y MENÚ | ||||
| Ver dashboard con estadísticas | ✅ SÍ | ✅ SÍ | ✅ SÍ | ⚠️ LIMITADO |
| Menú "Productos" visible | ✅ SÍ | ✅ SÍ | ❌ NO | ❌ NO |
| Menú "Categorías" visible | ✅ SÍ | ❌ NO | ❌ NO | ❌ NO |
| Menú "Proveedores" visible | ✅ SÍ | ❌ NO | ❌ NO | ❌ NO |
| Menú "Clientes" visible | ✅ SÍ | ✅ SÍ | ✅ SÍ | ❌ NO |
| Menú "Ventas" visible | ✅ SÍ | ✅ SÍ | ✅ SÍ | ❌ NO |
| Menú "Mi Perfil" visible | ✅ SÍ | ✅ SÍ | ✅ SÍ | ✅ SÍ |
| Menú "Mis Compras" visible | ✅ SÍ | ✅ SÍ | ✅ SÍ | ✅ SÍ |
📊 Resumen por Rol
👑 Administrador
Acceso Total
- ✅ Ver todos los módulos
- ✅ Crear en todos los módulos
- ✅ Editar en todos los módulos
- ✅ Eliminar en todos los módulos
- ✅ Gestionar catálogos
- ✅ Todos los menús visibles
Código de rol: 'admin'
👔 Empleado
Acceso Limitado
- ✅ Ver productos (solo lectura)
- ❌ NO crear/editar/eliminar productos
- ❌ NO acceso a categorías
- ❌ NO acceso a proveedores
- ✅ Gestión completa de clientes
- ✅ Registrar y ver ventas
Código de rol: 'empleado'
🛒 Vendedor
Acceso Mínimo
- ❌ NO acceso a productos
- ❌ NO acceso a categorías
- ❌ NO acceso a proveedores
- ✅ Gestión completa de clientes
- ✅ Registrar ventas
- ✅ Ver reportes de ventas
Código de rol: 'vendedor'
👤 Cliente
Acceso Restringido
- ❌ NO acceso a productos
- ❌ NO acceso a categorías
- ❌ NO acceso a proveedores
- ❌ NO gestión de clientes
- ✅ Ver sus propios datos
- ✅ Ver su historial de compras
Código de rol: 'cliente'
💡 Notas Importantes:
- Los permisos se verifican tanto en el backend (vistas) como en el frontend (templates)
- Ocultar elementos en el template NO es suficiente, siempre se debe validar en la vista
- El decorador
@login_requiredes obligatorio en todas las vistas protegidas - Los permisos se asignan al crear el usuario usando el script
crear_usuarios_con_roles.py