Skip to main content

How to Create PDF Invoices with Python and ReportLab?

 

Sales invoice

In this second part of the series, we’ll build on the Google Sheets integration from Part 1 and use Python to turn raw form responses into professional PDF invoices. You’ll learn how to design a clean invoice template with ReportLab, dynamically insert customer and product data, calculate totals, and generate a downloadable PDF file.

Prerequisite:

This tutorial is part of the Automated Sales Invoice Series.

📚 View the Complete Automated Sales Invoice Series

⬅ Previous Part                                                                                                                     ➡ Next Part

Preliminary 
Before I begin, it is recommended to activate the virtual environment before installing the relevant dependencies.
python -m venv venv
venv\Scripts\activate
pip install reportlab
The file structure is as shown in the diagram below:

Gsheet

In this tutorial, I will create a new media folder to store the logo and selected fonts under the parent folder of sales_invoice. Meanwhile, pdf.py is the main working file, and the rest of the file structure remains the same as in part 1

I have selected both Roboto-Bold.ttf and Roboto-Regular.ttf as my default font, and you may download them from my GitHub. If you need a different font, you may download it from Google Fonts. I also created my_logo.png using Canva Designer and placed it under the media folder, and you can create and add your own logo design. 

Step 1: Import the Reportlab dependencies and the gsheet module
Since I had installed ReportLab above, I could now import it. The following are my imported dependencies and modules:
# From ReportLab module
from reportlab.lib.units import inch
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase import pdfmetrics

# From Gsheet Python module (Part 1)
from gsheet import get_gsheet_data

# Register Roboto fonts
pdfmetrics.registerFont(TTFont('Roboto', 'sales_invoice/media/Roboto-Regular.ttf'))
pdfmetrics.registerFont(TTFont('Roboto-Bold', 'sales_invoice/media/Roboto-Bold.ttf'))

Step 2: Set up the Sales Invoice templates
This section builds the layout of the sales invoice. Therefore, my measurement is in "inches," and the page size is a letter. I divided them into the following sections:
    a) Logo 
    b) Company info 
    c) Bill info and invoice date 
    d) Invoice title 
    e) Column header 
    f) All horizontal and vertical lines 
    g) Signature

The builder elements are as follows:
    (i)   drawstring(x, y, text)—Draw Text,
    (ii)  drawImage(path, x, y, width, height)Insert Image,
    (iii) line(x1, y1, x2, y2)—Draw Line, 
    (iv) setFont(font_name, size)Set Font Style, 
    (v)  setFillColorRGB(r, g, b)Set Text / Fill Color,
    (vi) setStrokeColorCMYK(c, m, y, k)Set Line Color

Now I can use the builder elements above to set up my sales invoice template in each of the sections.

Please take note that, to make the sales invoice more presentable, I had to make an adjustment to the position of some of the sections above, such as rows_down, shift_up, and rows_to_shorten. However, you are free to make your own adjustment as appropriate.

The invoice date was extracted from the Google Sheet file and imported from the get_gsheet_data function (Part 1). Below is my code:
def draw_invoice_template(c, invoice_data):
    c.translate(inch, inch)

    # =============================
    # Logo
    # =============================
    logo_width = 1*inch
    logo_height = 1*inch
    logo_x = -0.05*inch
    logo_y = 8.5*inch

    c.drawImage('sales_invoice/media/my_logo.png', logo_x, logo_y,
                width=logo_width, height=logo_height, mask='auto')

    info_x = logo_x + logo_width + 0.3*inch
    
    # =============================
    # Company Info
    # =============================
    c.setFillColorRGB(0,0,0)

    c.setFont("Roboto-Bold", 16)
    c.drawString(info_x, logo_y + 0.8*inch, "How App & Web")

    c.setFont("Roboto", 14)
    c.drawString(info_x, logo_y + 0.55*inch, "1234, ABCD Road")
    c.drawString(info_x, logo_y + 0.3*inch, "Mycity, ZIP : 12345")

    # =============================
    # Bill Info
    # =============================
    c.setFillColorRGB(0,0,1)
    c.drawString(5.6*inch, 8.7*inch, 'Bill No :# 1234')
    
    # =============================
    # Invoice Date from row[0]
    # =============================
    if invoice_data and len(invoice_data[0]) > 0:
        raw_date = str(invoice_data[0][0])

        # If Google Sheet includes time (2026-02-23 00:00:00)
        clean_date = raw_date.split(" ")[0]

        c.setFillColorRGB(0,0,1)
        c.drawString(5.6*inch, 8.5*inch, clean_date)

    # =============================
    # INVOICE TITLE 
    # =============================
    row_height = 0.25 * inch
    rows_down = 5
    move_down = rows_down * row_height

    invoice_y = 8.3*inch - move_down
    line_y = 8.1*inch - move_down

    c.setFillColorRGB(1,0,0)
    c.setFont("Roboto-Bold", 40)
    c.drawString(4.3*inch, invoice_y, 'INVOICE')

    c.setFillColorRGB(0,0,0)
    c.line(0, line_y, 6.8*inch, line_y)

    shift_up = 2 * row_height  # 0.5 inch

    # =============================
    # Column Headers 
    # =============================
    c.setFont("Roboto", 22)

    column_headers_y = (7.3*inch - move_down) + shift_up

    c.drawString(0.5*inch, column_headers_y, 'Products')
    c.drawString(4*inch, column_headers_y, 'Price')
    c.drawString(5*inch, column_headers_y, 'Quantity')
    c.drawString(6.1*inch, column_headers_y, 'Total')

    # =============================
    # Vertical Lines 
    # =============================
    c.setStrokeColorCMYK(0,0,0,1)

    original_bottom = (1.9*inch - move_down)
    rows_to_shorten = 3
    new_bottom = original_bottom + (rows_to_shorten * row_height) + shift_up

    c.line(3.9*inch, column_headers_y, 3.9*inch, new_bottom)
    c.line(4.9*inch, column_headers_y, 4.9*inch, new_bottom)
    c.line(6.1*inch, column_headers_y, 6.1*inch, new_bottom)

    # =============================
    # Bottom Horizontal Line 
    # =============================
    horizontal_line_y = new_bottom - 0.2*inch
    c.line(0.01*inch, horizontal_line_y, 7*inch, horizontal_line_y)

    # =============================
    # Total 
    # =============================
    total_y = horizontal_line_y - 0.25*inch

    c.setFont("Roboto-Bold", 22)
    # c.drawString(2*inch, total_y, 'Total') 

    # =============================
    # Signature 
    # =============================
    signature_y = total_y - (3 * 0.3*inch)

    c.setFont("Roboto", 22)
    c.drawString(5.6*inch, signature_y, 'Signature')

    # =============================
    # Store positions
    # =============================
    positions = {
        "data_start_y": column_headers_y - 0.4*inch,
        "data_row_height": 0.3*inch,
        "new_bottom": new_bottom
    }

    return c, positions

# Set the output PDF file path
my_path = 'sales_invoice/my_invoice.pdf'

# Create a new PDF canvas object
c = canvas.Canvas(my_path, pagesize=letter)

# Fetch invoice data from Google Sheets
invoice_data = get_gsheet_data()  

# Draw the static invoice template
# This draws logo, company info, table headers, lines, etc.
c, positions = draw_invoice_template(c, invoice_data) 

# Finish the current page
c.showPage()

# Save the PDF file to disk
c.save()
When you run pdf.py, here is the result of the above function.
Invoice template

Step 3: Fill out the customer information.
After the template is ready, I need to loop over the "invoice_data" to retrieve rows [1]-[3], which contain the "invoice_data" details, such as customer name, address, and email, as I mentioned above in part 1. Finally, the data was drawn on the above template, and I will set their required position, font type, and colour.

Here is my code.
def fill_customer_info(c, invoice_data, positions):
    data_start_y = positions['data_start_y']
   
    c.setFillColorRGB(0,0,0)

    # Assuming customer info is in the first row only
    if invoice_data and len(invoice_data) > 0:
        row = invoice_data[0]

        name_str = row[1] if len(row) > 1 else ""
        address_str = row[2] if len(row) > 2 else ""
        email_str = row[3] if len(row) > 3 else ""

        y_position = data_start_y + 2*inch  # move above table

        # Customer Name (Bold)
        c.setFont("Roboto-Bold", 12)
        c.drawString(0.1*inch, y_position, name_str)
        
        # Address (Regular)
        c.setFont("Roboto", 12)
        c.drawString(0.1*inch, y_position - 0.2*inch, address_str)

        # Email (Regular)
        c.drawString(0.1*inch, y_position - 0.4*inch, email_str)

    return c
    
# Set the output PDF file path
my_path = 'sales_invoice/my_invoice.pdf'

# Create a new PDF canvas object
c = canvas.Canvas(my_path, pagesize=letter)

# Fetch invoice data from Google Sheets
invoice_data = get_gsheet_data()  

# Draw the static invoice template
# This draws logo, company info, table headers, lines, etc.
c, positions = draw_invoice_template(c, invoice_data) 

# Fill the customer information section on the invoice
c = fill_customer_info(c, invoice_data, positions)

# Finish the current page
c.showPage()

# Save the PDF file to disk
c.save()
The result of the above step 2 was set out as follows:
Empty invoice

Step 4: Fill out the other invoice details
At the beginning, I set the "grand_total" to zero and defined the data starting position. Then I loop over the "invoice_data" from rows [4] to [6] to retrieve the other invoice information, such as service name, quantity, and price. The "grand_total" is amended by multiplying the price by the quantity, and both the price and "grand_total" value are formatted with a $ sign and a comma with 2 decimal places, respectively. Similar to step 3 above, I will set all elements' required positions, font types, and colours for each element before drawing to the template.

Below is my code.
def fill_invoice_data(c, invoice_data, positions):
    data_start_y = positions['data_start_y']
    data_row_height = positions['data_row_height']
    new_bottom = positions['new_bottom']

    c.setFont("Roboto", 12)
    c.setFillColorRGB(0,0,0)

    grand_total = 0
    
    # ================================
    # Draw the invoice table for each row
    # ================================
    for i, row in enumerate(invoice_data):
        if any(str(cell).strip() for cell in row if cell):

            y_position = data_start_y - (i * data_row_height)

            product = row[4] if len(row) > 3 else ""
            price_str = row[5] if len(row) > 4 else "0"
            quantity_str = row[6] if len(row) > 5 else "0"

            try:
                clean_price = str(price_str).replace('$', '').replace(',', '').strip()
                price = int(float(clean_price)) if clean_price else 0
            except ValueError:
                price = 0

            try:
                quantity = int(float(quantity_str)) if quantity_str else 0
            except ValueError:
                quantity = 0

            line_total = price * quantity
            grand_total += line_total

            price_display = f"${price:,.2f}" if price > 0 else ""
            quantity_display = str(quantity) if quantity > 0 else ""
            total_display = f"${line_total:,.2f}" if line_total > 0 else ""

            # Draw values
            c.drawString(0.5*inch, y_position, str(product))
            c.drawString(4*inch, y_position, price_display)
            c.drawString(5*inch, y_position, quantity_display)
            c.drawString(6.1*inch, y_position, total_display)

    # ================================
    # Draw only one "Total" at the bottom of the table
    # ================================
    row_height = 0.25 * inch
    total_row_y = new_bottom - 0.2*inch - row_height  

    c.setFont("Roboto-Bold", 22)
    c.setFillColorRGB(0,0,0)  
    c.drawString(2*inch, total_row_y, "Total")  
    c.drawString(6.1*inch, total_row_y, f"${grand_total:,.2f}"
    
    return c 
    
# Set the output PDF file path
my_path = 'sales_invoice/my_invoice.pdf'

# Create a new PDF canvas object
c = canvas.Canvas(my_path, pagesize=letter)

# Fetch invoice data from Google Sheets
invoice_data = get_gsheet_data()  

# Draw the static invoice template
# This draws logo, company info, table headers, lines, etc.
c, positions = draw_invoice_template(c, invoice_data) 

# Fill in the other invoice details section on the invoice
c = fill_invoice_data(c, invoice_data, positions)

# Finish the current page
c.showPage()

# Save the PDF file to disk
c.save()
The completed view for a sales invoice from steps 1 to 4 is shown below:
Complete PDF
Final Wrap-up
In this tutorial, we learned how to generate a professional sales invoice PDF using Python and ReportLab, from designing a clean template to inserting dynamic data from Google Sheets. We separated the layout from the data logic, automatically calculated the line totals, and made a complete invoice file that was ready to use. This process demonstrated how automation can reduce manual work and improve accuracy in business operations. In the next tutorial, we will take it one step further by automatically emailing the generated sales invoice to customers, completing the end-to-end invoicing workflow.

Published: Feb 2026
Last Updated: Feb 2026

About the Author

Kelvin Loh is a Python developer focused on Flask, desktop applications, and business automation solutions. He shares practical tutorials and real-world coding projects to help developers and small businesses build useful applications.

Comments

Popular Posts

How to Design a Location Tracking Module for Desktop Business Systems Using Python?

This tutorial will show you how to create an interactive map application that allows users to input geographic coordinates and visualize locations on an interactive map using Python's Tkinter GUI framework enhanced with TTKBootstrap styling and the TkinterMapView widget.  Prerequisite: This tutorial is part of the standalone tutorial. 📚 View the  standalone tutorial Preliminary   Before I begin, it is recommended to activate the virtual environment before installing the relevant dependencies. python -m venv venv venv\Scripts\activate pip install ttkbootstrap tkintermapview As usual, we need to import the relevant module and set the root values from tkinter import * import ttkbootstrap as tb import tkintermapview from ttkbootstrap.dialogs import Messagebox root = tb.Window(themename='darkly') root.title('Find My Map') root.geometry("1200x1200") I would like to divide it into three sections.  a) Input Panel Layout,  The headers are mainly a label...

How to Create Flask Forms with CKEditor and Flask-WTF?

In this tutorial, we will be integrating Flask-WTF and Flask-CKEditor into your Flask application! You'll learn how to set up the editor, securely handle formatted HTML content, and create a seamless user experience that enhances any project that requires user-generated content. Let's get started! Prerequisite: This tutorial is part of the Flask CKEditor Project Series. 📚 View the Complete Flask CKEditor Series                                                                                                                                                  ➡ Next Part Preliminary Before I begin, it is recommended to activate the v...

How to Store Application Data with SQLite in Python?

  In the previous tutorial, I used TinyDB as our storage solution to keep application data in a simple JSON-based format. While TinyDB is lightweight and easy to use, many web applications require a more structured and scalable database system. In this tutorial, we will switch to SQLite , a powerful relational database that integrates well with Python and web frameworks. By using SQLite, you will learn how to store, manage, and query data in a more structured way for real-world applications. Prerequisite: This tutorial is part of the Flask CKEditor Project Series. 📚 View the Complete Flask CKEditor Series ⬅ Previous Part                                                                                                         ...