Generate PDF reports in Django

Generate PDF reports in Django

Introduction

I have seen many people asking about how to generate pdf reports from django, and i remember i did it few years ago to generate a complex table as a report for one of my projects.

so i decided to revisit my old code and put together a short guide on how to do it

Dependencies

First we need to add a python package to our project xhtml2pdf, and don't forget to include it in your requirements.txt

pip install "xhtml2pdf>=0.2.16"

The Template

Next build your report in your template folder, but bear in mind to not use any external css or tailwind, you can use only inline styling and <style> tag in the html head also modern css like flexbox or grid will not work

here is sample of a report for an invoice i created for this guide

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Invoice #{{ invoice.id }}</title>
        <style>
        @page {
            size: letter portrait;
            margin: 2cm;
        }
        body {
            font-family: Arial, sans-serif;
            font-size: 12px;
            line-height: 1.4;
            color: #333;
        }
        .company-header {
            margin-bottom: 30px;
        }
        .company-logo {
            width: 150px;
            height: auto;
        }
        .invoice-title {
            font-size: 24px;
            font-weight: bold;
            margin-bottom: 20px;
            color: #2c3e50;
        }
        .info-grid {
            display: table;
            width: 100%;
            margin-bottom: 30px;
        }
        .info-section {
            display: table-cell;
            width: 50%;
            vertical-align: top;
        }
        .info-section.right {
            text-align: right;
        }
        .info-title {
            font-size: 14px;
            font-weight: bold;
            margin-bottom: 10px;
            color: #2c3e50;
        }
        .items-table {
            width: 100%;
            border-collapse: collapse;
            margin-bottom: 30px;
        }
        .items-table th {
            background-color: #f8f9fa;
            border: 1px solid #dee2e6;
            padding: 10px;
            text-align: left;
            font-weight: bold;
        }
        .items-table td {
            border: 1px solid #dee2e6;
            padding: 10px;
        }
        .items-table .text-right {
            text-align: right;
        }
        .totals-section {
            width: 100%;
        }
        .totals-table {
            width: 300px;
            margin-left: auto;
        }
        .totals-table td {
            padding: 5px 10px;
        }
        .totals-table .total-row {
            font-weight: bold;
            font-size: 14px;
            border-top: 2px solid #dee2e6;
        }
        .footer {
            margin-top: 50px;
            padding-top: 20px;
            border-top: 1px solid #dee2e6;
            font-size: 10px;
            color: #6c757d;
        }
        </style>
    </head>
    <body>
        <div class="company-header">
            <img src="{% static 'img/logo.png' %}"
                 alt="Company Logo"
                 class="company-logo">
        </div>
        <div class="invoice-title">INVOICE</div>
        <div class="info-grid">
            <div class="info-section">
                <div class="info-title">Bill To:</div>
                <div>{{ invoice.customer_name }}</div>
                <div>{{ invoice.customer_email }}</div>
                <div style="white-space: pre-line">{{ invoice.customer_address }}</div>
            </div>
            <div class="info-section right">
                <div>
                    <strong>Invoice #:</strong> {{ invoice.id }}
                </div>
                <div>
                    <strong>Date:</strong> {{ invoice.date }}
                </div>
                <div>
                    <strong>Created:</strong> {{ invoice.created_at|date:"F j, Y" }}
                </div>
            </div>
        </div>
        <table class="items-table">
            <thead>
                <tr>
                    <th style="width: 40%">Item</th>
                    <th style="width: 20%">Quantity</th>
                    <th style="width: 20%">Unit Price</th>
                    <th style="width: 20%">Total</th>
                </tr>
            </thead>
            <tbody>
                {% for item in invoice.items.all %}
                    <tr>
                        <td>{{ item.name }}</td>
                        <td>{{ item.quantity }}</td>
                        <td>${{ item.unit_price }}</td>
                        <td>${{ item.total_price }}</td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
        <div class="totals-section">
            <table class="totals-table">
                <tr>
                    <td>Subtotal:</td>
                    <td>${{ invoice.subtotal }}</td>
                </tr>
                <tr class="total-row">
                    <td>Total:</td>
                    <td>${{ invoice.total }}</td>
                </tr>
            </table>
        </div>
        <div class="footer">
            <p>Thank you for your business!</p>
        </div>
    </body>
</html>

The view

Now let us assume you are in the invoice details page in your we application and you want to print that invoice, you create a button link that is connected to the following function view

from datetime import datetime as dt
from io import BytesIO

from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.template.loader import get_template
import xhtml2pdf.pisa as pisa

from .models import Invoice

@login_required
def print_invoice(request, pk):
    invoice = get_object_or_404(Invoice, pk=pk, created_by=request.user)
    template = get_template("reports/invoice.html")
    html = template.render({"invoice": invoice})
    response = BytesIO()
    pdf = pisa.pisaDocument(BytesIO(html.encode("UTF-8")), response)
    if not pdf.err:
        http_response = HttpResponse(
            response.getvalue(), content_type="application/pdf"
        )
        return http_response
    else:
        return HttpResponse("Error Rendering PDF", status=400)

once you click that link, the browser will open your pdf in a new tab where you can review and download

if you want the link to download immediately you add an extra line before return thr http_response, you can also customize the downloaded filename as needed

# ...
# ...
if not pdf.err:
    # ...
    # ...
    http_response["Content-Disposition"] = (
        f"attachment; filename=invoice#{invoice.id}_{dt.today().strftime('%d-%m-%Y')}.pdf"
    )
    return http_response

Enjoy!

s