Generar archivos Excel en Django – Guía Completa con openpyxl y XlsxWriter (2026)

🔗 Actualización de: blog.silverit.co/como-generar-archivos-excel-en-django-con-openpyxl/

Generar reportes en Excel desde Django es una de las funcionalidades más solicitadas en proyectos reales: exportar listados de clientes, generar facturas, producir informes financieros, descargar datos de inventario. Si llegaste aquí desde nuestro post anterior sobre openpyxl, esta es la versión completa y actualizada para 2026.

En esta guía cubrimos las dos librerías más usadas — openpyxl y XlsxWriter — con código real listo para usar, desde lo más básico hasta casos avanzados: múltiples hojas, estilos profesionales, gráficas, streaming para archivos grandes, y descarga directa como HttpResponse en Django.

📋 Lo que vas a aprender: Instalar y configurar openpyxl y XlsxWriter en Django · Generar un Excel básico y descargarlo · Aplicar estilos: colores, fuentes, bordes, anchos de columna · Trabajar con múltiples hojas · Agregar gráficas · Manejar archivos grandes con streaming · Decidir cuál librería usar según tu caso

openpyxl vs XlsxWriter: ¿cuál usar en tu proyecto Django?

Antes de escribir código, la decisión más importante es elegir la librería correcta. Ambas son gratuitas, open-source y ampliamente usadas — pero tienen filosofías muy distintas.

Tipo de operaciónopenpyxlXlsxWriter
Leer archivos .xlsx✅ Sí❌ No (solo escritura)
Crear archivos nuevos✅ Sí✅ Sí (más rápido)
Modificar archivos existentes✅ Sí❌ No
Velocidad en archivos grandes⚠️ Más lento✅ Más rápido (streaming)
Formateo de celdas✅ Completo✅ Completo + más opciones
Gráficas en Excel⚠️ Básico✅ Avanzado
Fórmulas Excel✅ Lectura + escritura✅ Solo escritura
Múltiples hojas✅ Sí✅ Sí
Integración con pandas✅ Sí✅ Sí (engine=’xlsxwriter’)
Licencia✅ MIT (gratuito)✅ BSD (gratuito)
Caso de uso ideal en DjangoReportes que leen/modifican plantillasReportes generados desde cero
Usa openpyxl cuando…Usa XlsxWriter cuando…
Necesitas leer un archivo Excel existenteSolo necesitas generar archivos nuevos
Trabajas con plantillas (.xlsx) prediseñadasPrioridad en velocidad con datasets grandes
Necesitas modificar celdas específicasNecesitas gráficas avanzadas en el reporte
Tu reporte es relativamente pequeño (<10k filas)Generas reportes de +50.000 filas
Quieres una sola librería para todoUsas pandas y quieres mejor integración
💡 Regla práctica para Django: Si necesitas leer o modificar plantillas Excel existentes → openpyxl. Si solo generas reportes desde datos de tu base de datos → XlsxWriter es más rápido y produce mejor output visual. En muchos proyectos reales terminas usando ambas.

Parte 1: Generando Excel en Django con openpyxl

1.1 Instalación

openpyxl no viene incluido con Django. Instálalo con pip y agrégalo a tu requirements.txt:

pip install openpyxl

# Agrega a tu requirements.txt:
openpyxl==3.1.5

1.2 Ejemplo básico: Excel simple con HttpResponse

Este es el caso de uso más común en Django: un endpoint que genera y devuelve un archivo Excel directamente al navegador para descarga.

# views.py
from django.http import HttpResponse
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from .models import Cliente

class ExportarClientesView(View):
    def get(self, request, *args, **kwargs):
        # Crear workbook y hoja activa
        wb = Workbook()
        ws = wb.active
        ws.title = 'Clientes'

        # Encabezados
        headers = ['ID', 'Nombre', 'Email', 'Teléfono', 'Fecha registro']
        ws.append(headers)

        # Datos desde base de datos
        for cliente in Cliente.objects.all().values_list(
            'id', 'nombre', 'email', 'telefono', 'fecha_registro'
        ):
            ws.append(list(cliente))

        # Configurar respuesta HTTP
        response = HttpResponse(
            content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        )
        response['Content-Disposition'] = 'attachment; filename=clientes.xlsx'
        wb.save(response)
        return response
⚠️ Nota importante: Usa el content_type correcto: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet para .xlsx. El antiguo application/ms-excel es para el formato .xls (Excel 97-2003) y puede causar problemas en navegadores modernos.

1.3 Aplicando estilos profesionales: colores, fuentes y bordes

Un reporte sin formato es funcional pero poco profesional. openpyxl permite controlar prácticamente cualquier aspecto visual de una celda. Aquí un ejemplo completo con encabezados estilizados, filas alternadas y bordes:

# views.py — reporte con estilos completos
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter

def get(self, request, *args, **kwargs):
    wb = Workbook()
    ws = wb.active
    ws.title = 'Reporte'

    # ── Estilos reutilizables ──
    estilo_header = Font(bold=True, color='FFFFFF', size=12)
    fill_header = PatternFill('solid', fgColor='1A5276')  # Azul SilverIT
    fill_par   = PatternFill('solid', fgColor='EAF2F8')   # Azul claro
    fill_impar = PatternFill('solid', fgColor='FFFFFF')   # Blanco
    centrado   = Alignment(horizontal='center', vertical='center')
    lado = Side(style='thin', color='CCCCCC')
    borde = Border(left=lado, right=lado, top=lado, bottom=lado)

    # ── Encabezados ──
    headers = ['#', 'Cliente', 'Email', 'Estado', 'Monto', 'Fecha']
    ws.append(headers)
    for col_num, header in enumerate(headers, 1):
        celda = ws.cell(row=1, column=col_num)
        celda.font    = estilo_header
        celda.fill    = fill_header
        celda.alignment = centrado
        celda.border  = borde

    # ── Datos con filas alternadas ──
    for row_num, obj in enumerate(MiModelo.objects.all(), 2):
        fila = [obj.id, obj.nombre, obj.email,
                obj.get_estado_display(), obj.monto, obj.fecha]
        ws.append(fila)
        fill = fill_par if row_num % 2 == 0 else fill_impar
        for col_num in range(1, len(fila) + 1):
            celda = ws.cell(row=row_num, column=col_num)
            celda.fill   = fill
            celda.border = borde
            celda.alignment = centrado

    # ── Ancho de columnas automático ──
    for col in ws.columns:
        max_len = max(len(str(c.value or '')) for c in col) + 4
        ws.column_dimensions[get_column_letter(col[0].column)].width = min(max_len, 40)

    # ── Fijar fila de encabezado ──
    ws.freeze_panes = 'A2'

    response = HttpResponse(
        content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    )
    response['Content-Disposition'] = 'attachment; filename=reporte.xlsx'
    wb.save(response)
    return response

1.4 Múltiples hojas en un solo archivo

Uno de los casos más útiles en proyectos reales: generar un Excel con varias pestañas — por ejemplo, un reporte mensual donde cada hoja es un mes, o un informe con resumen + detalle:

# Múltiples hojas en un workbook
wb = Workbook()

# Hoja 1 — Resumen
ws_resumen = wb.active
ws_resumen.title = 'Resumen'
ws_resumen.append(['Métrica', 'Valor'])
ws_resumen.append(['Total clientes', Cliente.objects.count()])
ws_resumen.append(['Total ventas', Venta.objects.count()])
ws_resumen.append(['Monto total', Venta.objects.aggregate(
    total=Sum('monto'))['total'] or 0])

# Hoja 2 — Detalle de clientes
ws_clientes = wb.create_sheet(title='Clientes')
ws_clientes.append(['ID', 'Nombre', 'Email', 'Fecha'])
for c in Cliente.objects.all():
    ws_clientes.append([c.id, c.nombre, c.email, c.fecha_registro])

# Hoja 3 — Detalle de ventas
ws_ventas = wb.create_sheet(title='Ventas')
ws_ventas.append(['ID', 'Cliente', 'Monto', 'Estado', 'Fecha'])
for v in Venta.objects.select_related('cliente').all():
    ws_ventas.append([v.id, v.cliente.nombre, v.monto,
                      v.get_estado_display(), v.fecha])

# Posicionar en la primera hoja al abrir
wb.active = ws_resumen

1.5 Formatos de número, fechas y moneda

openpyxl permite aplicar formatos de número nativos de Excel — esencial para reportes financieros:

from openpyxl.styles import numbers

# Formato de moneda (COP)
celda_monto = ws['E2']
celda_monto.value = 1500000
celda_monto.number_format = '"$"#,##0.00'

# Formato de fecha legible
celda_fecha = ws['F2']
celda_fecha.value = datetime.date.today()
celda_fecha.number_format = 'DD/MM/YYYY'

# Porcentaje
celda_pct = ws['G2']
celda_pct.value = 0.1534
celda_pct.number_format = '0.00%'

# Fórmula de suma (se evalúa al abrir en Excel)
ws['E100'] = '=SUM(E2:E99)'

Parte 2: Generando Excel en Django con XlsxWriter

2.1 Instalación

pip install XlsxWriter

# requirements.txt:
XlsxWriter==3.2.0

2.2 Ejemplo básico con XlsxWriter en Django

XlsxWriter trabaja con un objeto de tipo BytesIO (en memoria) que luego envías como HttpResponse. A diferencia de openpyxl, no puede escribir directamente al response — necesitas el paso intermedio de io.BytesIO:

# views.py
import io
import xlsxwriter
from django.http import HttpResponse
from .models import Producto

class ExportarProductosView(View):
    def get(self, request, *args, **kwargs):
        # Buffer en memoria
        output = io.BytesIO()
        workbook  = xlsxwriter.Workbook(output)
        worksheet = workbook.add_worksheet('Productos')

        # Formatos
        fmt_header = workbook.add_format({
            'bold': True, 'font_color': 'white',
            'bg_color': '#1A5276', 'align': 'center',
            'border': 1,
        })
        fmt_normal = workbook.add_format({'border': 1})
        fmt_moneda = workbook.add_format({
            'num_format': '$#,##0.00', 'border': 1
        })

        # Encabezados (fila 0 = fila 1 en Excel)
        headers = ['SKU', 'Nombre', 'Categoría', 'Stock', 'Precio']
        for col, header in enumerate(headers):
            worksheet.write(0, col, header, fmt_header)

        # Ancho de columnas
        worksheet.set_column('A:A', 12)
        worksheet.set_column('B:B', 35)
        worksheet.set_column('C:C', 20)
        worksheet.set_column('D:D', 10)
        worksheet.set_column('E:E', 15)

        # Datos
        for row, prod in enumerate(Producto.objects.all(), 1):
            worksheet.write(row, 0, prod.sku, fmt_normal)
            worksheet.write(row, 1, prod.nombre, fmt_normal)
            worksheet.write(row, 2, prod.categoria.nombre, fmt_normal)
            worksheet.write(row, 3, prod.stock, fmt_normal)
            worksheet.write(row, 4, prod.precio, fmt_moneda)

        # Fijar encabezado
        worksheet.freeze_panes(1, 0)

        workbook.close()
        output.seek(0)

        response = HttpResponse(
            output.read(),
            content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        )
        response['Content-Disposition'] = 'attachment; filename=productos.xlsx'
        return response

2.3 Gráficas en Excel con XlsxWriter

Esta es la gran ventaja de XlsxWriter sobre openpyxl: soporte nativo para gráficas de alta calidad. Ideal para reportes ejecutivos o dashboards exportables:

# Continuando desde el workbook anterior...

# Hoja de datos
ws_data = workbook.add_worksheet('Datos')
ws_data.write_column('A1', ['Mes', 'Enero', 'Febrero', 'Marzo',
                            'Abril', 'Mayo', 'Junio'])
ws_data.write_column('B1', ['Ventas', 1200000, 1450000, 980000,
                             1700000, 2100000, 1850000])
ws_data.write_column('C1', ['Meta', 1500000, 1500000, 1500000,
                             1500000, 1500000, 1500000])

# Crear gráfica de barras
chart = workbook.add_chart({'type': 'column'})

# Serie: Ventas reales
chart.add_series({
    'name':       '=Datos!$B$1',
    'categories': '=Datos!$A$2:$A$7',
    'values':     '=Datos!$B$2:$B$7',
    'fill':       {'color': '#2874A6'},
})

# Serie: Meta
chart.add_series({
    'name':       '=Datos!$C$1',
    'categories': '=Datos!$A$2:$A$7',
    'values':     '=Datos!$C$2:$C$7',
    'fill':       {'color': '#E74C3C'},
    'type':       'line',
})

# Configurar título y ejes
chart.set_title({'name': 'Ventas vs Meta — 2026'})
chart.set_x_axis({'name': 'Mes'})
chart.set_y_axis({'name': 'COP', 'num_format': '$#,##0'})
chart.set_size({'width': 480, 'height': 300})

# Hoja del reporte: insertar la gráfica
ws_reporte = workbook.add_worksheet('Reporte')
ws_reporte.insert_chart('B2', chart)

2.4 Streaming para archivos grandes (50.000+ filas)

Si tu reporte tiene muchas filas, cargar todo en memoria puede ser lento o incluso agotar el RAM del servidor. openpyxl tiene modo write-only y XlsxWriter usa streaming nativo por defecto. Aquí el patrón correcto para ambos:

# openpyxl — modo write-only para datasets grandes
from openpyxl import Workbook

wb = Workbook(write_only=True)  # No carga nada en memoria hasta el final
ws = wb.create_sheet()

# Encabezados
ws.append(['ID', 'Campo1', 'Campo2', 'Campo3'])

# Iterar con iterator() para no cargar todo el queryset en RAM
for obj in MiModelo.objects.all().iterator(chunk_size=500):
    ws.append([obj.id, obj.campo1, obj.campo2, obj.campo3])

# XlsxWriter — streaming es el modo por defecto
# Simplemente itera el queryset con .iterator()
for row_num, obj in enumerate(MiModelo.objects.all().iterator(), 1):
    worksheet.write(row_num, 0, obj.id)
    worksheet.write(row_num, 1, obj.campo1)
    worksheet.write(row_num, 2, obj.campo2)
🚀 Optimización importante: Siempre usa .iterator() en QuerySets grandes en lugar de .all(). Sin iterator(), Django carga todos los objetos en memoria de una vez. Con iterator(), los procesa en lotes y el consumo de RAM es mínimo, sin importar cuántos registros tenga la tabla.

Parte 3: Casos de uso avanzados en Django

3.1 Proteger la descarga con login requerido

En la mayoría de proyectos reales, los reportes son privados. Django hace esto muy fácil:

# Opción 1 — Decorador (vistas basadas en función)
from django.contrib.auth.decorators import login_required

@login_required(login_url='/login/')
def exportar_reporte(request):
    # ... tu código de generación

# Opción 2 — Mixin (vistas basadas en clase)
from django.contrib.auth.mixins import LoginRequiredMixin

class ExportarView(LoginRequiredMixin, View):
    login_url = '/login/'

    def get(self, request):
        # Solo usuarios autenticados llegan aquí
        ...

# Opción 3 — Permisos por grupo/rol
from django.contrib.auth.mixins import PermissionRequiredMixin

class ExportarReporteFinanciero(PermissionRequiredMixin, View):
    permission_required = 'app.can_export_reports'

3.2 Generar reportes grandes en background con Celery

Para archivos que tardan más de 5-10 segundos en generarse, es mejor procesarlos de forma asíncrona con Celery y notificar al usuario cuando estén listos:

# tasks.py
from celery import shared_task
from django.core.files.base import ContentFile
from .models import ReporteGenerado

@shared_task
def generar_reporte_async(usuario_id, filtros):
    import io
    from openpyxl import Workbook

    wb = Workbook(write_only=True)
    ws = wb.create_sheet('Datos')
    ws.append(['ID', 'Nombre', 'Valor'])

    # Aquí va la lógica de generación (puede ser lenta)
    for obj in MiModelo.objects.filter(**filtros).iterator():
        ws.append([obj.id, obj.nombre, obj.valor])

    # Guardar en un campo FileField del modelo
    output = io.BytesIO()
    wb.save(output)
    output.seek(0)

    reporte = ReporteGenerado.objects.get_or_create(
        usuario_id=usuario_id)[0]
    reporte.archivo.save('reporte.xlsx', ContentFile(output.read()))
    reporte.estado = 'listo'
    reporte.save()

# views.py — disparar la tarea
def solicitar_reporte(request):
    generar_reporte_async.delay(request.user.id, {'activo': True})
    return JsonResponse({'status': 'procesando'})

3.3 Usando pandas como intermediario

Si ya usas pandas en tu proyecto para análisis de datos, puedes aprovechar su integración nativa con ambas librerías para exportar DataFrames directamente a Excel:

import pandas as pd
import io
from django.http import HttpResponse

def exportar_con_pandas(request):
    # Convertir QuerySet a DataFrame
    qs = MiModelo.objects.values('id', 'nombre', 'email', 'fecha', 'monto')
    df = pd.DataFrame(list(qs))

    # Renombrar columnas al español
    df.columns = ['ID', 'Nombre', 'Email', 'Fecha', 'Monto']

    # Formatear tipos
    df['Fecha'] = pd.to_datetime(df['Fecha']).dt.strftime('%d/%m/%Y')
    df['Monto'] = df['Monto'].map('${:,.2f}'.format)

    output = io.BytesIO()

    # Exportar con openpyxl como engine
    with pd.ExcelWriter(output, engine='openpyxl') as writer:
        df.to_excel(writer, sheet_name='Datos', index=False)

    # O con XlsxWriter para mejor formato:
    # with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
    #     df.to_excel(writer, sheet_name='Datos', index=False)

    output.seek(0)
    response = HttpResponse(
        output.read(),
        content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    )
    response['Content-Disposition'] = 'attachment; filename=reporte.xlsx'
    return response

Errores comunes y cómo solucionarlos

❌ Error: ‘cannot import name Workbook’

# Causa: openpyxl no está instalado en el entorno virtual activo
# Solución:
pip install openpyxl
# Verifica que estás en el entorno correcto:
which python  # debe apuntar a tu venv

❌ El archivo se descarga corrupto o no abre

# Causa más común: content_type incorrecto
# ❌ Incorrecto (formato antiguo .xls):
response = HttpResponse(content_type='application/ms-excel')

# ✅ Correcto para .xlsx:
response = HttpResponse(
    content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)

# Otra causa: olvidar output.seek(0) con BytesIO en XlsxWriter
workbook.close()
output.seek(0)  # ← Obligatorio antes de leer el buffer
response = HttpResponse(output.read(), content_type='...')

❌ Django sirve el archivo como texto plano en lugar de descargarlo

# Causa: falta el header Content-Disposition
# ✅ Correcto:
response['Content-Disposition'] = 'attachment; filename="reporte.xlsx"'
#                                  ^^^^^^^^^^
#                 'attachment' fuerza la descarga.
#                 'inline' intentaría mostrarlo en el navegador.

❌ Timeout en reportes grandes

# Causa: QuerySet sin .iterator() carga todo en memoria
# ❌ Lento para tablas grandes:
for obj in MiModelo.objects.all():
    ...

# ✅ Eficiente:
for obj in MiModelo.objects.all().iterator(chunk_size=500):
    ...

# También: agrega select_related/prefetch_related para evitar N+1
MiModelo.objects.select_related('cliente', 'categoria').iterator()

¿Necesitas reportes Excel personalizados en tu sistema?

Los ejemplos de esta guía cubren los casos más comunes, pero los proyectos reales suelen tener requerimientos específicos: reportes con lógica de negocio compleja, múltiples fuentes de datos, formatos muy específicos para cumplimiento regulatorio, o integración con sistemas legacy.

En SilverIT desarrollamos software a medida con Django y llevamos años construyendo sistemas de información con reportería avanzada para empresas en Colombia y Latinoamérica. Si tienes un proyecto donde necesitas reportes Excel automatizados como parte de un sistema más grande, podemos ayudarte.

🚀 En SilverIT diseñamos e implementamos sistemas de información personalizados, incluyendo módulos de reportería en Excel, PDF y dashboards. Escríbenos en silverit.co o a info@silverit.co — el diagnóstico inicial es sin costo.

Preguntas frecuentes

¿Cuál es mejor para Django: openpyxl o XlsxWriter?

Depende del caso. Si necesitas leer o modificar archivos Excel existentes, openpyxl es la única opción. Si solo generas archivos nuevos desde datos de tu base de datos, XlsxWriter es más rápido y tiene mejor soporte para gráficas. Para reportes estándar con Django, openpyxl es más versátil y es la elección más común.

¿Se puede generar Excel sin instalar Microsoft Office en el servidor?

Sí, completamente. Tanto openpyxl como XlsxWriter generan archivos .xlsx puros sin necesitar que Excel esté instalado en el servidor. El archivo generado se puede abrir en Excel, Google Sheets, LibreOffice o cualquier aplicación compatible con el formato .xlsx.

¿Cómo manejar archivos de más de 1 millón de filas?

Para datasets de ese tamaño, usa openpyxl en modo write_only=True con .iterator() en el QuerySet, o XlsxWriter con la opción constant_memory=True. En casos extremos, considera dividir el reporte en múltiples archivos o usar un formato más eficiente como CSV o parquet para esos volúmenes.

¿Es compatible con Django REST Framework?

Sí. Puedes devolver el Excel como un FileResponse o HttpResponse desde cualquier vista de DRF. Para APIs que deben soportar tanto JSON como Excel en el mismo endpoint, implementa content negotiation revisando el header Accept de la petición.

¿Cómo agregar una imagen o logo de la empresa al Excel?

# Con openpyxl:
from openpyxl.drawing.image import Image

img = Image('ruta/al/logo.png')
img.width  = 150  # píxeles
img.height = 60
ws.add_image(img, 'A1')

# Con XlsxWriter:
worksheet.insert_image('A1', 'ruta/al/logo.png',
    {'x_scale': 0.5, 'y_scale': 0.5})