anishfyi.com Projects All writings

December 1, 20257 min readDjango, PDF, Python

Generating PDF Documents in Django with ReportLab

Building clean, repeatable PDF documents like invoices and purchase orders directly from Django with ReportLab.

Invoices, purchase orders, packing slips. A backend that handles commerce ends up generating documents, and they need to look the same every time. ReportLab generates PDFs straight from Python, with no headless browser and no external service to keep alive.

Two ways to build a PDF

ReportLab offers a low-level canvas, where you place text at exact coordinates, and a higher-level layout engine called Platypus, where you assemble flowable elements and let it handle the page. For structured documents like a purchase order, Platypus is the right tool.

A purchase order with Platypus

from io import BytesIO
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table


def build_purchase_order_pdf(po):
    buffer = BytesIO()
    doc = SimpleDocTemplate(buffer, pagesize=A4)
    styles = getSampleStyleSheet()

    story = [
        Paragraph(f"Purchase Order {po.reference}", styles["Title"]),
        Spacer(1, 12),
    ]
    rows = [["SKU", "Quantity", "Price"]]
    rows += [[item.sku, item.quantity, item.price] for item in po.items.all()]
    story.append(Table(rows))

    doc.build(story)
    buffer.seek(0)
    return buffer

Serve it or store it

The function returns an in-memory buffer, so the caller decides what to do with it: stream it back as a download, or upload it to object storage and hand out a link.

from django.http import FileResponse
from django.shortcuts import get_object_or_404


def download_purchase_order(request, po_id):
    po = get_object_or_404(PurchaseOrder, pk=po_id)
    pdf = build_purchase_order_pdf(po)
    return FileResponse(
        pdf, as_attachment=True, filename=f"po-{po.reference}.pdf"
    )

Generate off the request path

A small receipt is fine to build inline. A long, branded, multi-page document is not. Push that to a Celery task, store the result, and notify the user when it is ready. PDF generation is CPU work, and CPU work does not belong in a web worker.

Make it deterministic

The same input should produce the same bytes. Avoid embedding the current time inside the document body. A deterministic generator is easy to test, easy to cache, and easy to compare when something looks wrong.

Factor out the repeated parts

The header, the footer, the address block, the totals table: pull each into its own function. Every document type then shares the same building blocks, and a branding change happens in one place instead of five.

Working on something in this territory? Book thirty minutes.