Creating PDF Invoices in Python with pText - Stack Abuse

Creating PDF Invoices in Python with pText

Introduction

The Portable Document Format (PDF) is not a WYSIWYG (What You See is What You Get) format. It was developed to be platform-agnostic, independent of the underlying operating system and rendering engines.

To achieve this, PDF was constructed to be interacted with via something more like a programming language, and relies on a series of instructions and operations to achieve a result. In fact, PDF is based on a scripting language - PostScript, which was the first device-independent Page Description Language.

In this guide, we'll be using pText - a Python library dedicated to reading, manipulating and generating PDF documents. It offers both a low-level model (allowing you access to the exact coordinates and layout if you choose to use those) and a high-level model (where you can delegate the precise calculations of margins, positions, etc to a layout manager).

We'll take a look at how to create a PDF invoice in Python using pText.

Installing pText

pText can be downloaded from source on GitHub, or installed via pip:

$ pip install ptext-joris-schellekens

Creating a PDF Invoice in Python with pText

pText has two intuitive key classes - Document and Page, which represent a document and the pages within it. Additionally, the PDF class represents an API for loading and saving the Documents we create.

Let's create a Document() and Page() as a blank canvas that we can add the invoice to:

from ptext.pdf.document import Document
from ptext.pdf.page.page import Page

# Create document
pdf = Document()

# Add page
page = Page()
pdf.append_page(page)

Since we don't want to deal with calculating coordinates - we can delegate this to a PageLayout which manages all of the content and its positions:

# New imports
from ptext.pdf.canvas.layout.page_layout import SingleColumnLayout
from ptext.io.read.types import Decimal

page_layout = SingleColumnLayout(page)
page_layout.vertical_margin = page.get_page_info().get_height() * Decimal(0.02)

Here, we're using a SingleColumnLayout since all of the content should be in a single column - we won't have a left and right side of the invoice. We're also making the vertical margin smaller here. The default value is to trim the top 10% of the page height as the margin, and we're reducing it down to 2%, since we'll want to use this space for the company logo/name.

Speaking of which, let's add the company logo to the layout:

# New import
from ptext.pdf.canvas.layout.image import Image

page_layout.add(    
        Image(        
        "https://s3.amazonaws.com/s3.stackabuse.com/media/articles/creating-an-invoice-in-python-with-ptext-1.png",        
        width=Decimal(128),        
        height=Decimal(128),    
        ))

Here, we're adding an element to the layout - an Image(). Through its constructor, we're adding a URL pointing to the image resource and setting its width and height.

Beneath the image, we'll want to add our imaginary company info (name, address, website, phone) as well as the invoice information (invoice number, date, due date). A common format for brevity (which incidentally also makes the code cleaner) is to use a table to store invoice data. Let's create a separate helper method to build the invoice information in a table, which we can then use to simply add a table to the invoice in our main method:

# New imports
from ptext.pdf.canvas.layout.table import Table
from ptext.pdf.canvas.layout.paragraph import Paragraph, Alignment
from datetime import datetime
import random

def _build_invoice_information():    
    table_001 = Table(number_of_rows=5, number_of_columns=3)
	
    table_001.add(Paragraph("[Street Address]"))    
    table_001.add(Paragraph("Date", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT))    
    now = datetime.now()    
    table_001.add(Paragraph("%d/%d/%d" % (now.day, now.month, now.year)))
	
    table_001.add(Paragraph("[City, State, ZIP Code]"))    
    table_001.add(Paragraph("Invoice #", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT))
    table_001.add(Paragraph("%d" % random.randint(1000, 10000)))   
	
    table_001.add(Paragraph("[Phone]"))    
    table_001.add(Paragraph("Due Date", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT))
    table_001.add(Paragraph("%d/%d/%d" % (now.day, now.month, now.year))) 
	
    table_001.add(Paragraph("[Email Address]"))    
    table_001.add(Paragraph(" "))
    table_001.add(Paragraph(" "))

    table_001.add(Paragraph("[Company Website]"))
    table_001.add(Paragraph(" "))
    table_001.add(Paragraph(" "))

    table_001.set_padding_on_all_cells(Decimal(2), Decimal(2), Decimal(2), Decimal(2))    		
    table_001.no_borders()
    return table_001

Here, we're making a simple Table with 5 rows and 3 columns. The rows correspond to the street address, city/state, phone, email address and company website. Each row will have 0..3 values (columns). Each text element is added as a Paragraph, which we've aligned to the right via Alignment.RIGHT, and accept styling arguments such as font.

Finally, we've added padding to all the cells to make sure we don't place the text awkwardly near the confounds of the cells.

Now, back in our main method, we can call _build_invoice_information() to populate a table and add it to our layout:

page_layout = SingleColumnLayout(page)
page_layout.vertical_margin = page.get_page_info().get_height() * Decimal(0.02)
page_layout.add(    
    Image(        
        "https://s3.amazonaws.com/s3.stackabuse.com/media/articles/creating-an-invoice-in-python-with-ptext-1.png",        
        width=Decimal(128),        
        height=Decimal(128),    
        ))

# Invoice information table  
page_layout.add(_build_invoice_information())  
  
# Empty paragraph for spacing  
page_layout.add(Paragraph(" "))

Now, let's build this PDF document real quick to see what it looks like. For this, we'll use the PDF module:

# New import
from ptext.pdf.pdf import PDF

with open("output.pdf", "wb") as pdf_file_handle:
    PDF.dumps(pdf_file_handle, document)

ptext invoice 1

Great! Now we'll want to add the billing and shipping information as well. It'll conveniently be placed in a table, just like the company information. For brevity's sake, we'll also opt to make a separate helper function to build this info, and then we can simply add it in our main method:

# New imports
from ptext.pdf.canvas.color.color import HexColor, X11Color

def _build_billing_and_shipping_information():  
    table_001 = Table(number_of_rows=6, number_of_columns=2)  
    table_001.add(  
        Paragraph(  
            "BILL TO",  
            background_color=HexColor("263238"),  
            font_color=X11Color("White"),  
        )  
    )  
    table_001.add(  
        Paragraph(  
            "SHIP TO",  
            background_color=HexColor("263238"),  
            font_color=X11Color("White"),  
        )  
    )  
    table_001.add(Paragraph("[Recipient Name]"))        # BILLING  
    table_001.add(Paragraph("[Recipient Name]"))        # SHIPPING  
    table_001.add(Paragraph("[Company Name]"))          # BILLING  
    table_001.add(Paragraph("[Company Name]"))          # SHIPPING  
    table_001.add(Paragraph("[Street Address]"))        # BILLING  
    table_001.add(Paragraph("[Street Address]"))        # SHIPPING  
    table_001.add(Paragraph("[City, State, ZIP Code]")) # BILLING  
    table_001.add(Paragraph("[City, State, ZIP Code]")) # SHIPPING  
    table_001.add(Paragraph("[Phone]"))                 # BILLING  
    table_001.add(Paragraph("[Phone]"))                 # SHIPPING  
    table_001.set_padding_on_all_cells(Decimal(2), Decimal(2), Decimal(2), Decimal(2))  
    table_001.no_borders()  
    return table_001

We've set the background_color of the initial paragraphs to #263238 (grey-blue) to match the color of the logo, and the font_color to White.

Let's call this in the main method as well:

# Invoice information table
page_layout.add(_build_invoice_information())

# Empty paragraph for spacing
page_layout.add(Paragraph(" "))

# Billing and shipping information table
page_layout.add(_build_billing_and_shipping_information())

Better understand your data with visualizations

  •  30-day no-questions refunds
  •  Beginner to Advanced
  •  Updated regularly (update June 2021)
  •  New bonus resources and guides

Once we run the script again, this results in a new PDF file that contains more information:

ptext invoice 2

With our basic information sorted out (company info and billing/shipping info) - we'll want to add an itemized description. These will be the goods/services that our supposed company offered to someone and are also typically done in a table-like fashion beneath the information we've already added.

Again, let's create a helper function that generates a table and populates it with data, which we can simply add to our layout later on:

# New import
from ptext.pdf.canvas.layout.table import Table, TableCell

def _build_itemized_description_table(self):  
    table_001 = Table(number_of_rows=15, number_of_columns=4)  
    for h in ["DESCRIPTION", "QTY", "UNIT PRICE", "AMOUNT"]:  
        table_001.add(  
            TableCell(  
                Paragraph(h, font_color=X11Color("White")),  
                background_color=HexColor("016934"),  
            )  
        )  
  
    odd_color = HexColor("BBBBBB")  
    even_color = HexColor("FFFFFF")  
    for row_number, item in enumerate([("Product 1", 2, 50), ("Product 2", 4, 60), ("Labor", 14, 60)]):  
        c = even_color if row_number % 2 == 0 else odd_color  
        table_001.add(TableCell(Paragraph(item[0]), background_color=c))  
        table_001.add(TableCell(Paragraph(str(item[1])), background_color=c))  
        table_001.add(TableCell(Paragraph("$ " + str(item[2])), background_color=c))  
        table_001.add(TableCell(Paragraph("$ " + str(item[1] * item[2])), background_color=c))  
	  
	# Optionally add some empty rows to have a fixed number of rows for styling purposes
    for row_number in range(3, 10):  
        c = even_color if row_number % 2 == 0 else odd_color  
        for _ in range(0, 4):  
            table_001.add(TableCell(Paragraph(" "), background_color=c))  
  
    table_001.add(TableCell(Paragraph("Subtotal", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT,), col_span=3,))  
    table_001.add(TableCell(Paragraph("$ 1,180.00", horizontal_alignment=Alignment.RIGHT)))  
    table_001.add(TableCell(Paragraph("Discounts", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT,),col_span=3,))  
    table_001.add(TableCell(Paragraph("$ 177.00", horizontal_alignment=Alignment.RIGHT)))  
    table_001.add(TableCell(Paragraph("Taxes", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT), col_span=3,))  
    table_001.add(TableCell(Paragraph("$ 100.30", horizontal_alignment=Alignment.RIGHT)))  
    table_001.add(TableCell(Paragraph("Total", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT  ), col_span=3,))  
    table_001.add(TableCell(Paragraph("$ 1163.30", horizontal_alignment=Alignment.RIGHT)))  
    table_001.set_padding_on_all_cells(Decimal(2), Decimal(2), Decimal(2), Decimal(2))  
    table_001.no_borders()  
    return table_001

In practice, you'd substitute the hard-coded strings related to the subtotal, taxes and total prices with calculations of the actual prices - though, this heavily depends on the underlying implementation of your Product models, so we've added a stand-in for abstraction. Once we add this table to the document as well - we can rebuild it and take a look.

The entire main method should now look something along the lines of:

# Create document
pdf = Document()

# Add page
page = Page()
pdf.append_page(page)

page_layout = SingleColumnLayout(page)
page_layout.vertical_margin = page.get_page_info().get_height() * Decimal(0.02)

page_layout.add(
        Image(
        "https://s3.amazonaws.com/s3.stackabuse.com/media/articles/creating-an-invoice-in-python-with-ptext-1.png",
        width=Decimal(128),
        height=Decimal(128),
        ))


# Invoice information table
page_layout.add(_build_invoice_information())

# Empty paragraph for spacing
page_layout.add(Paragraph(" "))

# Billing and shipping information table
page_layout.add(_build_billing_and_shipping_information())

# Itemized description
page_layout.add(_build_itemized_description_table())

with open("output2.pdf", "wb") as pdf_file_handle:
    PDF.dumps(pdf_file_handle, pdf)

Running this piece of code results in:

ptext invoice 3

Creating an Outline

Our PDF is done and ready to be served - though, we can take it up a notch with two little additions. First, we can add an Outline, which helps readers like Adobe navigate and generate a menu for your PDFs:

# New import
from ptext.pdf.page.page import DestinationType

# Outline  
pdf.add_outline("Your Invoice", 0, DestinationType.FIT, page_nr=0)

The add_outline() function accepts a few arguments:

  • title : the title that will be displayed in the side menu
  • level : how deep down the tree something will be. Level 0 is root-level.
  • Several arguments that make up a "destination"

Destinations can be thought of as targets for hyperlinks. You can link to an entire page (which is what we are doing in this example), but you can also link to specific parts of a page (for instance - exactly at y-coordinate 350).

Furthermore, you need to specify how the reader should present that page - for instance, do you want to simply scroll to that page and not zoom? Do you want to display only a target area, with the reader completely zoomed into that particular area?

In this line of code, we are asking the reader to display page 0 (the first page) and ensure it fits the reader window (zooming in/out if needed).

Once you've added the outline, you should see it appear in the reader of your choice:

ptext invoice 4

With multiple pages - you can create a more complex outline and link to them via add_outline() for easier navigation.

Embedding JSON Documents in PDF Invoices

Since PDFs aren't very computer-friendly (in terms of reading and unambiguously decoding) - sometimes, we might want to add more computer-friendly formats as well if someone would like to process invoices automatically.

A Germany-originating invoice standard called ZUGFeRD (later adopted by the EU) enables us to make PDF invoices with more computer-legible file formats such as XML - which describes the invoice and is easily parsable. In addition to these, you can also embed other documents related to your invoice such as terms and agreements, a refund policy, etc.

To embed any sort of additional file in a PDF file, using pText - we can use the append_embedded_file() function.

Let's first go ahead and create a dictionary to store our invoice data in JSON, which we'll then save into an invoice_json file:

import json

# Creating a JSON file
invoice_json = {  
"items": [  
    {  
        "Description": "Product1",  
        "Quantity": 2,  
        "Unit Price": 50,  
        "Amount": 100,  
    },  
    {  
        "Description": "Product2",  
        "Quantity": 4,  
        "Unit Price": 60,  
        "Amount": 100,  
    },  
    {  
        "Description": "Labor",  
        "Quantity": 14,  
        "Unit Price": 60,  
        "Amount": 100,  
    },  
],  
"Subtotal": 1180,  
"Discounts": 177,  
"Taxes": 100.30,  
"Total": 1163.30,  
}  
invoice_json_bytes = bytes(json.dumps(invoice_json, indent=4), encoding="latin1")

Now, we can simply embed this file into our PDF invoice:

pdf.append_embedded_file("invoice.json", invoice_json_bytes, apply_compression=False)

Once we run the script again and store the document, we've go:

ptext invoice 5

Conclusion

In this guide, we've taken a look at how to create an invoice in Python using pText. We've then added an outline to the PDF file for ease of navigation and taken a look at how to add attachments/embedded files for programmatic access to the contents of the PDF.

Last Updated: May 6th, 2021

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

Joris SchellekensAuthor

I'm a software architect from Belgium, with a passion for machine learning, knowledge-based systems and graph algorithms. I'm also the author of pText, the pure python PDF library.

Want a remote job?

    Prepping for an interview?

    • Improve your skills by solving one coding problem every day
    • Get the solutions the next morning via email
    • Practice on actual problems asked by top companies, like:
     
     
     

    Better understand your data with visualizations

    •  30-day no-questions refunds
    •  Beginner to Advanced
    •  Updated regularly (update June 2021)
    •  New bonus resources and guides

    © 2013-2021 Stack Abuse. All rights reserved.