Saltar al contenido
jesusprodriguez.com

pdf-report

Exportar un informe a PDF

Informes en PDF reproducibles con ReportLab: tablas que no se cortan, tipografías embebidas y la misma salida en cada ejecución.

stack:
Python
versión:
v1.0.0
actualizada:
tamaño:
4.8 KB
lectura:
3 min
licencia:
CC-BY-4.0

Cuándo se activa

Al exportar a PDF un informe de estado, una auditoría o cualquier documento que se envíe fuera.

description: Genera informes en PDF reproducibles desde Python con ReportLab - portada, tablas que no se cortan, tipografías embebidas y salida idéntica en cada ejecución. Úsala al exportar a PDF un informe de estado, una auditoría, un CV o cualquier documento que se vaya a enviar fuera.

  • Platypus vs canvas
  • Tablas con repeatRows
  • Fuentes TTF y acentos
  • Checklist de envío

Cómo se le pide

> Exporta este informe de auditoría a PDF con las tablas sin cortarse entre páginas.

Escríbeselo tal cual al agente: la skill se carga sola por la descripción, no hay que nombrarla.

Cómo se instala

/plugin marketplace add https://jesusprodriguez.com/skills/marketplace.json
/plugin install ingenieria@jprodriguez-toolkit

La vía nativa, y la única que se actualiza sola: el marketplace se añade una vez y `/plugin marketplace update` trae las versiones nuevas. Las skills quedan con espacio de nombres propio (`azure-devops:azure-pr-review`).

El fichero, entero

Esto es exactamente lo que descargas: sin resúmenes ni recortes.

Exportar un informe a PDF

Un PDF que sale de un script se envía a gente que no va a poder pedir correcciones. Se genera entero desde el dato, no a mano, y dos ejecuciones con los mismos datos producen el mismo fichero.

Elegir la herramienta

CasoHerramienta
Informe con tablas y texto fluidoReportLab Platypus
Maquetación pixel a pixel (tarjeta, diploma, CV)ReportLab canvas
Ya existe una versión HTML fielPlaywright → page.pdf()

No mezcles: elegir canvas para un informe de 12 páginas significa calcular saltos de página a mano, y ese cálculo siempre acaba mal.

Informe con Platypus

from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib import colors
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak,
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle

doc = SimpleDocTemplate(
    str(salida), pagesize=A4,
    leftMargin=20 * mm, rightMargin=20 * mm,
    topMargin=18 * mm, bottomMargin=18 * mm,
    title="Estado del proyecto", author="…",   # metadatos: se ven en el visor
)

estilos = getSampleStyleSheet()
cuerpo = ParagraphStyle('cuerpo', parent=estilos['BodyText'],
                        fontName='Inter', fontSize=9.5, leading=13.5)

historia = [
    Paragraph("Estado del proyecto", estilos['Title']),
    Spacer(1, 6 * mm),
    Paragraph(resumen, cuerpo),
]
doc.build(historia, onFirstPage=portada, onLaterPages=pie)

El texto va siempre en Paragraph, nunca como cadena suelta: solo el párrafo sabe partir líneas. Y Paragraph interpreta un subconjunto de HTML (<b>, <i>, <font>), así que cualquier dato que venga de fuera hay que escaparlo: un & sin escapar rompe la generación entera.

Tablas que sobreviven al salto de página

tabla = Table(filas, colWidths=[70*mm, 30*mm, 40*mm, 30*mm], repeatRows=1)
tabla.setStyle(TableStyle([
    ('FONTNAME',   (0, 0), (-1, 0), 'Inter-Bold'),
    ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f1efe8')),
    ('ALIGN',      (1, 1), (-1, -1), 'RIGHT'),     # números a la derecha
    ('VALIGN',     (0, 0), (-1, -1), 'TOP'),
    ('LINEBELOW',  (0, 0), (-1, 0), 0.5, colors.HexColor('#e0dfd8')),
    ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#faf9f6')]),
]))
  • repeatRows=1 repite la cabecera en cada página. Sin ella, la página 3 es una tabla de números sin significado.
  • Anchos de columna explícitos. El automático reparte mal en cuanto una celda es larga.
  • Las celdas con texto largo también van en Paragraph, o se salen de la celda sin avisar.
  • Números alineados a la derecha y con el mismo número de decimales. Una columna de importes desalineada es el detalle que hace dudar del resto.

Tipografías y colores

from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

pdfmetrics.registerFont(TTFont('Inter', ruta / 'Inter-Regular.ttf'))
pdfmetrics.registerFont(TTFont('Inter-Bold', ruta / 'Inter-Bold.ttf'))

Registra las TTF del proyecto: las Type1 por defecto (Helvetica) no llevan acentos completos según la codificación y el PDF sale con huecos. Y usa la misma paleta que el resto de la marca en colors.HexColor, para que el PDF no parezca de otra empresa.

Encabezado, pie y numeración

def pie(canvas, doc):
    canvas.saveState()
    canvas.setFont('Inter', 7.5)
    canvas.setFillColor(colors.HexColor('#4a4943'))
    canvas.drawString(20 * mm, 12 * mm, f"{titulo} · {fecha:%d/%m/%Y}")
    canvas.drawRightString(A4[0] - 20 * mm, 12 * mm, f"{doc.page}")
    canvas.restoreState()

saveState/restoreState siempre: sin ellos, la fuente del pie se filtra a la página siguiente. Para “página X de Y” hace falta una pasada doble (doc.multiBuild con un marcador), porque el total no se conoce hasta el final.

Checklist antes de enviar

  • Fecha de generación y origen del dato visibles en el documento.
  • Acentos, ñ y € correctos —abre el PDF, no confíes en que no falló—.
  • Ninguna tabla cortada a mitad de fila ni texto fuera de márgenes.
  • Metadatos (title, author) puestos: es lo que se ve en la pestaña del visor y al adjuntarlo.
  • Salida a un directorio de build, nunca sobreescribiendo la fuente.
  • Script idempotente: reejecutarlo no cambia nada más que el fichero.
  • Sin datos personales o confidenciales que no correspondan al destinatario. Un PDF se reenvía solo.