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)  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 money-back guarantee • Beginner to Advanced • Updated regularly (latest update June 2021) • Updated with bonus resources and guides Once we run the script again, this results in a new PDF file that contains more information: 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("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.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()

page = Page()
pdf.append_page(page)

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

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

# Empty paragraph for spacing

# Billing and shipping information table

# Itemized description

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


Running this piece of code results in:

### 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


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).

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:

### 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 5th, 2021

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

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.

# 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: