Business reporting in Django 5 using ReportLab

In this tutorial we’ll see various business reporting and documents generated by utilizing ReportLab library from within a Django 5 application.

Prerequisites: A Unix-like system (Linux, macOS, or WSL) and Python 3 installed.

Let’s open the terminal and execute following command to create a new folder:

mkdir myapp

Go inside this newly created folder:

cd myapp

Then create virtual environment by executing following command:

python3 -m venv .env

Activate virtual environment:

source .env/bin/activate

(Once the virtual environment is activated, your terminal defaults to the local sandbox, meaning you can simply use python and pip instead of python3 or pip3.)

Now, install Django 5 along with ReportLab library by executing following command:

pip install django==5 reportlab num2words

Then create the project:

django-admin startproject project .

Next, create an app named reports:

python manage.py startapp reports

In order to define routes for our reports app, create urls.py inside reports folder by executing:

touch reports/urls.py

Then, open reports/urls.py and put following code inside it:

from django.urls import path
from . import views

app_name = "reports"
urlpatterns = [
    path('', views.reports, name='reports'),
    path('invoice', views.invoice, name='invoice'),
    path('ledger', views.ledger, name='ledger'),
    path('doc', views.doc, name='doc'),
    path('multi', views.multi, name='multi'),
]

Now, let’s propagate our app’s routes by amending project/urls.py as follows:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("reports/", include("reports.urls")),
    path("admin/", admin.site.urls),
]

Next, amend project/settings.py as follows, in order to add reports app into project:

INSTALLED_APPS = [
    'reports.apps.ReportsConfig',
    # ... default django apps
]

Now, let’s have our views’ logic by amending reports/views.py as follows:

from django.shortcuts import render
from django.http import FileResponse, HttpResponse
from . import utils

def reports(request):
	return render(request, 'reports/index.html', context={})

def invoice(request):
	buffer = utils.invoice()
	return FileResponse(buffer, as_attachment=True, filename="invoice.pdf")

def ledger(request):
	utils.ledger()
	return HttpResponse('Ledger generated...see app root folder')

def doc(request):
	utils.doc()
	return HttpResponse('PDFdoc generated...see app root folder')

def multi(request):
	utils.multi()
	return HttpResponse('Multipage doc generated...see app root folder')

Create another file named utils.py inside reports folder, by entering following command:

touch reports/utils.py

We’ve created the utils.py file just to keep our views.py clean. Now, insert PDF generating logic inside utils.py file:

import io
import time
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from reportlab.lib.enums import TA_RIGHT, TA_JUSTIFY
from reportlab.rl_config import defaultPageSize
from reportlab.lib.units import inch
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import Paragraph, Table, TableStyle, SimpleDocTemplate, Spacer
from reportlab.lib import colors
from num2words import num2words

def invoice():

    company_name = "My Company Name"
    company_address = "XYZ, Executive Centre"
    company_contact = "Phone: 123-456-7890, Email: info@domain.com"
    customer_name = "ABC Private Limited"
    customer_address = "Client Address"
    invoice_number = "INV1234"
    invoice_date = "2024-02-09"
    products = [
		{"name": "Product 1", "quantity": 2, "price": 10.00, "discount": 10},
		{"name": "Product 2", "quantity": 1, "price": 25.00, "discount": 5},
	]

    buffer = io.BytesIO()
    c = canvas.Canvas(buffer, pagesize=letter)

    styles = getSampleStyleSheet()
    heading_style2 = styles["h3"]
    data_style = ParagraphStyle(name="Data", fontSize=10)
    # To line wrap large sentences
    normal = styles["BodyText"]
    normal.wordWrap = 'CJK'

    c.drawString(30, 750, company_name)
    c.drawString(30, 730, company_address)
    c.drawString(30, 710, company_contact)
    c.drawString(300, 750, "Bill To:")
    c.drawString(300, 730, customer_name)
    c.drawString(300, 710, customer_address)
    c.drawString(30, 680, invoice_number)
    c.drawString(30, 660, invoice_date)

    data = [[Paragraph("Product", heading_style2), Paragraph("Quantity", heading_style2), Paragraph("Price", heading_style2), Paragraph("Discount", heading_style2), Paragraph("Amount", heading_style2)]]
    data.extend([[Paragraph(p["name"], normal), Paragraph(str(p["quantity"]), normal), Paragraph(f"Rs.{p['price']:.2f}", data_style), Paragraph(f"{p['discount']}%", data_style), Paragraph(f"Rs.{p['quantity'] * p['price'] * (1 - p['discount']/100):.2f}", data_style)] for p in products])

    table = Table(data, colWidths=[100, 100, 70, 100, 100], style=[('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('VALIGN', (0, 0), (-1, -1), 'MIDDLE')])
    style = TableStyle([
                        ('VALIGN',(0,0),(-1,-1),'MIDDLE'),
                        ('GRID', (0,0), (-1,-1), 0.25, colors.black),
                        ])
    table.setStyle(style)    
    table.wrapOn(c, 100, 520)
    table.drawOn(c, 100, 520)

	# total amount
    total_amount = sum([p["quantity"] * p["price"] * (1 - p["discount"]/100) for p in products])
    c.drawString(100, 300, "Total Amount:")
    c.drawString(520, 300, f"Rs.{total_amount:.2f}")
    c.drawString(100, 280, "Rupees ")
    c.drawString(145, 280, num2words(f"{total_amount:.2f}"))

    c.showPage()
    c.save()
    buffer.seek(0)
    return buffer

data = [
    {"date": "2021-01-01", "description": "Sales", "debit": 1000, "credit": 0},
    {"date": "2021-01-02", "description": "Rent", "debit": 0, "credit": 500},
    {"date": "2021-01-03", "description": "Salary", "debit": 1000, "credit": 0},
    {"date": "2021-01-04", "description": "Utilities", "debit": 0, "credit": 200},
]

def ledger():
    c = canvas.Canvas("ledger.pdf")
    c.setFont("Helvetica", 12)
    x = 50
    y = 700
    headers = ["Date", "Description", "Debit", "Credit", "Running Balance"]
    for header in headers:
        c.drawString(x, y, header)
        x += 100
    y -= 20
    running_balance = 0
    for entry in data:
        date = entry["date"]
        description = entry["description"]
        debit = entry["debit"]
        credit = entry["credit"]

        running_balance += debit - credit

        x = 50

        c.drawString(x, y, str(date))
        x += 100
        c.drawString(x, y, description)
        x += 100
        c.drawString(x, y, str(debit))
        x += 100
        c.drawString(x, y, str(credit))
        x += 100
        c.drawString(x, y, str(running_balance))

        y -= 20
    c.showPage()
    c.save()


def doc():

	doc = SimpleDocTemplate("doc.pdf",pagesize=letter,
							rightMargin=72,leftMargin=72,
							topMargin=72,bottomMargin=18)
	Story=[]

	magName = "Fantasia"
	issueNum = 12
	subPrice = "99.00"
	limitedDate = "03/05/2026"
	freeGift = "toolbox"

	formatted_time = time.ctime()
	full_name = "John Doe"
	address_parts = ["577 Magnum Street", "Capetown, CA 50158"]

	styles=getSampleStyleSheet()
	styles.add(ParagraphStyle(name='Justify', alignment=TA_JUSTIFY))
	ptext = '%s' % formatted_time

	Story.append(Paragraph(ptext, styles["Normal"]))
	Story.append(Spacer(1, 12))


	Story.append(Spacer(1, 12))
	ptext = 'Dear %s:' % full_name.split()[0].strip()
	Story.append(Paragraph(ptext, styles["Normal"]))
	Story.append(Spacer(1, 12))

	ptext = 'We would like to welcome you to our subscriber base for %s Magazine! \
			You will receive %s issues at the excellent introductory price of $%s. Please respond by\
			%s to start receiving your subscription and get the following free gift: %s.' % (magName, 
																									issueNum,
																									subPrice,
																									limitedDate,
																									freeGift)
	Story.append(Paragraph(ptext, styles["Justify"]))
	Story.append(Spacer(1, 12))


	ptext = 'Thank you very much and we look forward to serving you.'
	Story.append(Paragraph(ptext, styles["Justify"]))
	Story.append(Spacer(1, 12))
	ptext = 'Sincerely,'
	Story.append(Paragraph(ptext, styles["Normal"]))
	Story.append(Spacer(1, 48))
	ptext = 'Jane Doe'
	Story.append(Paragraph(ptext, styles["Normal"]))
	Story.append(Spacer(1, 12))
	doc.build(Story)


Title = "Hello world"
pageinfo = "platypus example"
PAGE_HEIGHT=defaultPageSize[1]; PAGE_WIDTH=defaultPageSize[0]
styles = getSampleStyleSheet()

def myFirstPage(canv, doc):
	canv.saveState()
	canv.setFont('Times-Bold',16)
	canv.drawCentredString(PAGE_WIDTH/2.0, PAGE_HEIGHT-108, Title)
	canv.setFont('Times-Roman',9)
	canv.drawString(inch, 0.75 * inch, "First Page / %s" % pageinfo)
	canv.restoreState()

def myLaterPages(canv, doc):
	canv.saveState()
	canv.setFont('Times-Roman',9)
	canv.drawString(inch, 0.75 * inch, "Page %d %s" % (doc.page, pageinfo))
	canv.restoreState()

def multi():
	doc = SimpleDocTemplate("multi.pdf")
	Story = [Spacer(1,2*inch)]
	style = styles["Normal"]
	for i in range(100):
		bogustext = ("This is Paragraph number %s. " % i) *20
		p = Paragraph(bogustext, style)
		Story.append(p)
		Story.append(Spacer(1,0.2*inch))
	doc.build(Story, onFirstPage=myFirstPage, onLaterPages=myLaterPages)

The above code contains 4 functions corresponding the functions in views.py for generating an invoice, a ledger, a letter and a multipage document covering examples for business reporting.

Next, run the migrations:

python manage.py migrate

Now, let’s move on to client-side. So, create template folder reports/templates/reports by executing following command:

mkdir -p reports/templates/reports

Create base.html and index.html inside the above folder. You may execute following command to create these files in one go:

touch reports/templates/reports/{base,index}.html

Put following code inside reports/templates/reports/base.html:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="">
    <title>{% block title %}My App{% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>
  <body>

    <div class="container-fluid">
      <div class="row">

        <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">

          {% block content %}
          {% endblock %}

        </main>
      </div>
    </div>
  
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
  </body>
</html>

Then, put following code inside reports/templates/reports/index.html:

{% extends "reports/base.html" %}
{% block content %}

<div class="m-4">
  <a href="{% url 'reports:invoice' %}" class="btn btn-primary btn-sm">Generate Invoice</a>
</div>
<div class="m-4">
  <a href="{% url 'reports:ledger' %}" class="btn btn-primary btn-sm">Generate Ledger</a>
</div>
<div class="m-4">
  <a href="{% url 'reports:doc' %}" class="btn btn-primary btn-sm">Generate a document</a>
</div>
<div class="m-4">
  <a href="{% url 'reports:multi' %}" class="btn btn-primary btn-sm">Generate a multipage document</a>
</div>

{% endblock %}

Follow the prompts to configure credentials, then boot the local engine:

python manage.py runserver

Next, go to localhost:8000/reports. You can generate related PDF by pressing a button.

Leave a Comment