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!