6. Paso a Paso: Sistema de Boletas y Migraciones Evolutivas 🧾
1. El Reto Real: Construir una Base de Datos que Evoluciona
En los tutoriales introductorios es común ver que todos los modelos se escriben de una sola vez en un único archivo models.py y se ejecuta una única migración 0001_initial. En el mundo laboral real, ningún software se construye así.
Los sistemas crecen incrementalmente: primero lanzas un catálogo básico, luego añades el módulo de ventas, más adelante incorporas la gestión de clientes sin romper las ventas anteriores, y finalmente blindas la base de datos con restricciones de integridad y migraciones de datos.
A lo largo de esta lección construiremos el núcleo de datos de un Sistema de Punto de Venta (POS) y Facturación a través de 5 migraciones sucesivas:
- Fase 1 (`0001`): Catálogo maestro con
CategoriayProducto. - Fase 2 (`0002`): Transacciones de venta con
BoletayDetalleBoleta(congelando el precio histórico). - Fase 3 (`0003`): Incorporación de
Clientecomo relación opcional (null=True, blank=True) sobre tablas con datos previos. - Fase 4 (`0004`): Restricciones a nivel de base de datos con
CheckConstrainty cálculos automáticos ensave(). - Fase 5 (`0005`): Migración de datos idempotente con
RunPythonpara sembrar categorías maestras.
2. Línea de Tiempo Interactiva de la Base de Datos
Explora visualmente cómo cambia el código en Python, qué comando se ejecuta en la terminal y qué tablas se generan en la base de datos en cada una de las 5 fases:
Evolución de la Base de Datos: Sistema de Boletas POS
Avanza paso a paso viendo cómo nacen las tablas, se añaden relaciones y se aplican migraciones seguras sin pérdida de datos.
Creación de Categoría y Producto
Establecer las entidades maestras de nuestro inventario con una relación 1 a Muchos (1 Categoría tiene N Productos).
(venv) C:\proyectos\django\ventas> python manage.py makemigrations ventas
Migrations for 'ventas':
ventas/migrations/0001_initial.py
- Create model Categoria
- Create model Producto
(venv) C:\proyectos\django\ventas> python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, ventas
Running migrations:
Applying ventas.0001_initial... OKventas/models.py)Django Models# ventas/models.py (Fase 1)
from django.db import models
class Categoria(models.Model):
nombre = models.CharField(max_length=50, unique=True)
descripcion = models.TextField(blank=True)
def __str__(self):
return self.nombre
class Producto(models.Model):
nombre = models.CharField(max_length=150, db_index=True)
codigo_barra = models.CharField(max_length=30, unique=True)
precio = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField(default=0)
categoria = models.ForeignKey(
Categoria,
on_delete=models.PROTECT,
related_name='productos'
)
def __str__(self):
return f"{self.nombre} (${self.precio})"sqlmigrate ventas 0001)CREATE TABLE "ventas_categoria" (
"id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
"nombre" varchar(50) NOT NULL UNIQUE,
"descripcion" text NOT NULL
);
CREATE TABLE "ventas_producto" (
"id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
"nombre" varchar(150) NOT NULL,
"codigo_barra" varchar(30) NOT NULL UNIQUE,
"precio" numeric(10, 2) NOT NULL,
"stock" integer NOT NULL CHECK ("stock" >= 0),
"categoria_id" bigint NOT NULL REFERENCES "ventas_categoria" ("id")
);Tabla maestra de familias de productos
Catálogo de artículos vendibles
Usamos on_delete=models.PROTECT en la relación con Categoria para evitar que un administrador borre una categoría si todavía existen productos asignados a ella, protegiendo la integridad referencial.
3. Desglose Técnico Paso a Paso
Paso 1: Catálogo Base (Migración `0001_initial.py`)
Comenzamos en la aplicación ventas creando los dos modelos fundacionales de nuestro inventario:
Si un empleado intenta eliminar la categoría "Bebidas", pero ya existen 20 productos registrados en ella, la base de datos lanzará un error
ProtectedError impidiendo el borrado. Esto evita que los productos queden huérfanos o se borren por accidente.Paso 2: Transacciones de Venta (Migración `0002_boleta_detalleboleta.py`)
Una venta requiere un patrón Maestro-Detalle (Master-Detail):
- Boleta (Maestro): Representa la transacción global (folio único, fecha, hora, total).
- DetalleBoleta (Detalle): Representa cada ítem vendido dentro de esa boleta (qué producto, cuántas unidades y a qué precio).
Muchos desarrolladores novatos omiten el campo
precio_unitario en DetalleBoleta pensando: "¿Para qué guardarlo si puedo hacer detalle.producto.precio?".¡Esto es un error financiero grave! Si vendes un café a $2.00 hoy, y dentro de 4 meses subes el precio a $3.50, al consultar las boletas del año pasado verías que todas aumentaron su valor mágicamente. En cualquier sistema de facturación, el detalle debe congelar el precio exacto al que se vendió en ese milisegundo.
Paso 3: Incorporación de Cliente Opcional (Migración `0003_cliente_boleta_cliente.py`)
Ahora la empresa decide implementar un programa de fidelización y necesita asociar boletas a clientes registrados. Sin embargo, en una cafetería o tienda muchas ventas son anónimas (público general que no entrega sus datos).
Si ejecutas
python manage.py makemigrations sobre una tabla que ya tiene registros reales:- Si el campo no permite nulos (
null=False), Django te detendrá en seco pidiéndote un valor por defecto para las filas anteriores. - Al declarar
null=True, la migración ejecuta unALTER TABLE "ventas_boleta" ADD COLUMN "cliente_id" bigint NULL;limpio e inmediato, manteniendo todas las ventas anteriores como compras anónimas sin romper el sistema.
Paso 4: Blindaje con Restricciones CheckConstraint y `save()` Automatizado
Para que el sistema sea confiable, no podemos permitir que nadie inserte un detalle con cantidad 0 o negativa, ni precios negativos. Agregamos restricciones a nivel de motor SQL en Meta.constraints:
Paso 5: Migración de Datos con `RunPython` (`0005_sembrar_categorias_iniciales.py`)
¿Cómo garantizamos que cuando un nuevo desarrollador o un servidor de despliegue clone el proyecto, existan categorías listas para usar? Creamos una migración de datos con --empty:
Luego editamos el archivo generado usando apps.get_model():
Ponte a prueba
Comprueba que comprendes el ciclo de vida de migraciones progresivas, snapshots de precios y claves foráneas opcionales.


