TabularInline
TabularInline muestra los modelos relacionados en formato de tabla, compacto y eficiente para muchos campos:
from django.contrib import admin
from .models import Pedido, LineaPedido, ImagenProducto, Producto
class LineaPedidoInline(admin.TabularInline):
model = LineaPedido
fields = ['producto', 'cantidad', 'precio_unitario', 'subtotal']
readonly_fields = ['subtotal']
extra = 1 # Número de formularios vacíos adicionales
min_num = 1 # Mínimo requerido
max_num = 50 # Máximo permitido
def subtotal(self, obj):
return f'{obj.precio_unitario * obj.cantidad:.2f} €'
subtotal.short_description = 'Subtotal'
@admin.register(Pedido)
class PedidoAdmin(admin.ModelAdmin):
list_display = ['id', 'cliente', 'estado', 'total', 'fecha_creacion']
inlines = [LineaPedidoInline]

StackedInline
StackedInline muestra cada modelo relacionado como un formulario apilado, más espacioso y adecuado para modelos con muchos campos:
class ImagenProductoInline(admin.StackedInline):
model = ImagenProducto
fields = ['imagen', 'alt_text', 'es_principal', 'orden']
extra = 0
max_num = 10
can_delete = True
class Media:
css = {'all': ['admin/css/inlines.css']}
@admin.register(Producto)
class ProductoAdmin(admin.ModelAdmin):
inlines = [ImagenProductoInline]
Inline con validación personalizada
from django.forms import BaseInlineFormSet
class LineaPedidoFormSet(BaseInlineFormSet):
def clean(self):
super().clean()
total = sum(
form.cleaned_data.get('precio_unitario', 0) * form.cleaned_data.get('cantidad', 0)
for form in self.forms
if form.cleaned_data and not form.cleaned_data.get('DELETE')
)
if total > 50000:
from django.core.exceptions import ValidationError
raise ValidationError('El importe total del pedido no puede superar 50.000 €.')
class LineaPedidoInline(admin.TabularInline):
model = LineaPedido
formset = LineaPedidoFormSet
extra = 1
Inline con relación ManyToMany mediante through
Cuando una relación ManyToMany tiene un modelo intermedio (through), se puede editar ese modelo intermedio como inline:
class ProductoEtiquetaInline(admin.TabularInline):
model = Producto.etiquetas.through # Modelo intermedio
extra = 1
verbose_name = 'Etiqueta'
verbose_name_plural = 'Etiquetas'
@admin.register(Producto)
class ProductoAdmin(admin.ModelAdmin):
exclude = ['etiquetas'] # Excluir el widget M2M por defecto
inlines = [ProductoEtiquetaInline]
ReadOnlyInline (Django 5.0+)
Desde Django 5.0 existe ReadOnlyTabularInline y ReadOnlyStackedInline para mostrar modelos relacionados sin posibilidad de edición:
class HistorialPrecioInline(admin.TabularInline):
model = HistorialPrecio
fields = ['precio_anterior', 'precio_nuevo', 'fecha_cambio', 'modificado_por']
readonly_fields = ['precio_anterior', 'precio_nuevo', 'fecha_cambio', 'modificado_por']
extra = 0
can_delete = False
def has_add_permission(self, request, obj=None):
return False # No permitir añadir registros de historial manualmente
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Django es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Django
Explora más contenido relacionado con Django y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Crear inlines con TabularInline para mostrar modelos relacionados en formato tabla. Usar StackedInline para mostrar modelos relacionados como formularios apilados. Configurar extra, min_num y max_num para controlar el número de formularios inline. Definir readonly_fields y fields en inlines para controlar qué se muestra. Usar inlines con relaciones ManyToMany mediante through models.