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.