🛍️ Sistema de Gestión de Tienda - Django + MySQL (ACTUALIZADO 2025)

🎯 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

  1. Python 3.8+ (Recomendado: Python 3.13)
  2. MySQL Server 8.0+ (o WAMP/XAMPP con MySQL)
  3. Editor de código (VS Code, PyCharm, Sublime Text)
  4. 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

  1. Descarga Python 3.8 o superior desde https://www.python.org/downloads/
  2. Durante la instalación:
    • Marca "Add Python to PATH"
    • Instala para todos los usuarios
  3. 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

  1. Navega a la carpeta de tu proyecto.
  2. Crea el entorno virtual (venv):
# Comando de Python para crear un entorno virtual llamado 'venv'.
python -m venv venv
  1. Activa el entorno virtual:
    • Windows (CMD): venv\Scripts\activate
    • Windows (PowerShell): .\venv\Scripts\Activate.ps1
    • Linux/macOS: source venv/bin/activate

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:

  1. Ve a: https://www.lfd.uci.edu/~gohlke/pythonlibs/#mysqlclient
  2. Descarga el archivo .whl correcto para tu versión de Python
  3. Ejemplo: mysqlclient-2.1.1-cp313-cp313-win_amd64.whl (para Python 3.13 de 64 bits)
  4. 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_db exista

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:

Paso 12.3: Probar Funcionalidades

  1. Login: Inicia sesión con admin / tu_contraseña
  2. Dashboard: Verifica que muestre estadísticas
  3. Categorías: Crea al menos 3 categorías (Electrónica, Ropa, Hogar)
  4. Productos: Crea productos y asigna categorías (combobox)
  5. Proveedores: Registra proveedores
  6. Clientes: Registra clientes
  7. Permisos: Prueba con diferentes usuarios (vendedor, gerente)

Paso 12.4: Verificar Permisos por Roles

  1. Cierra sesión (logout)
  2. Inicia sesión como vendedor1 (contraseña: vendedor123)
  3. Intenta crear un producto → Debería mostrar error de permisos
  4. Cierra sesión e inicia como gerente1 (contraseña: gerente123)
  5. Ahora SÍ podrás crear y editar productos
  6. 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 de mysqlclient y librerías de desarrollo de MySQL en el OS.
  • Error: Unknown command: 'tienda': Revisa sistema_tienda/settings.py y verifica que 'tienda' esté en INSTALLED_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 el urls.py principal.

🛠️ 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

  1. Copia toda la carpeta practica_u3_examen al nuevo equipo
  2. 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_db creada
  • ☐ 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 Templates

Este 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_required es obligatorio en todas las vistas protegidas
  • Los permisos se asignan al crear el usuario usando el script crear_usuarios_con_roles.py