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.
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: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'))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
(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 ColorNow 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()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:
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:Final Wrap-upIn 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
Post a Comment