PDF facturen met Python
Rapportages en gegevens worden in veel gevallen verstuurd als PDF, in deze blog wordt uiteengezet hoe PDF-facturen automatisch gegenereerd kunnen worden met behulp van Python.
Facturen worden in veel gevallen verstuurd als PDF, en hierbij is vaak vraag naar manieren om deze bestanden te personaliseren en het creëren ervan te automatiseren. Veel boekhoudprogramma’s bieden ingebouwde functionaliteiten om facturen te genereren en deze vervolgens als PDF’s naar klanten te sturen. Deze functionaliteit biedt vaak geringe flexibiliteit als het gaat om opmaak, op maat gemaakte informatieonderdelen en/of het versturen van de betreffende facturen. In deze post richten wij ons op het opmaken van de PDF door middel van Python (ReportLab) en de mogelijkheden tot personalisatie en automatisering die deze biedt.
De PDF-bestandsextensie is een van de meest gebruikte extensies. PDF staat voor portable document format en is de standaard voor documenten waarbij de complete opmaak hetzelfde dient te blijven, onafhankelijk van het platform waarmee deze gemaakt wordt. Bij Zypp zijn wij groot fan van platformonafhankelijke toepassingen, daarmee is PDF geen uitzondering.
PDF’s bestaan al sinds de jaren negentig en zijn sindsdien voor tal van verschillende zaken gebruikt. Een van de meest bekende toepassingen is voor het maken van facturen. Waarom? Je wil als verstuurder van een factuur zeker zijn dat de ontvanger exact dezelfde informatie ontvangt zoals jij die ook verstuurd hebt, om zo misverstanden te voorkomen (Word documenten met Google Docs openen anyone?). Daarnaast is een factuur ook een contactmoment met je klant, het is dus belangrijk om hier ook een goede indruk achter te kunnen laten!
In deze blog wordt uiteengezet hoe deze goede indruk mogelijk gemaakt kan worden door het gebruik van Python. Eerst zullen de basis functionaliteiten worden beschreven om tot een bestand te kunnen komen. En daarna zal er een korte inkijk gegeven worden in de mogelijkheden om de opmaak aan te passen. Veel leesplezier!
Benodigdheden
-
Python packages
-
Bestanden
Maak een factuur
Er wordt gestart met het opzetten van de basisfunctionaliteiten: het inladen van Python packages, initialiseren van de ReportLab class en het inladen van het bestand. Er zal niet overal in detail worden getreden van specifieke functies van de betreffende packages, hiervoor wordt verwezen naar de documentatie van de packages zelf (zie links hierboven).
Er zal een korte beschrijving van de onderdelen in de code worden gegeven, dan de code zelf en uiteindelijk hoe de PDF er uitziet als de code gedraaid wordt. Hierdoor kan je makkelijk controleren of er dezelfde PDF geproduceerd wordt zodra je hiermee gaat experimenteren.
Uitleg
- Importeer de packages, hiervoor wordt in de basis gebruik gemaakt van pandas en een aantal reportlab modules.
- Creëer een class die de basis ReportLab class BaseDocTemplate erft.
De grootte van de pagina’s (A4) wordt aangegeven samen met de naam van de PDF die gemaakt gaat worden.
Nadat de basisonderdelen van de PDF zijn gedefinieerd (Frame = vlak waarin de data terecht komt,
PageTemplate = indeling van de pagina), wordt de Table aangemaakt om de data in te zetten. - Bij het runnen van het script wordt de data ingeladen, en wordt dit als input gebruikt om de PDF te maken.
Code
import pandas as pd
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, Table
class GeneratePDF(BaseDocTemplate):
def __init__(self, df: pd.DataFrame):
self.df = df
# Initialise Reportlab class
super().__init__(filename="sample_invoice_simple.pdf", page_size=A4)
# Create the frame in which the data should be placed
table_frame = Frame(x1=0, y1=0, width=20 * cm, height=20 * cm)
# Creating the page template
invoice_page = PageTemplate(frames=[table_frame])
self.addPageTemplates([invoice_page])
# Create the Reportlab table in which we will place the main invoice data
self.set_table()
def set_table(self):
"""
Create table format for reportlab
"""
# Transform dataframe to format for Reportlab
data = [self.df.columns.astype(str).tolist()] + self.df.to_numpy().tolist()
rl_table = Table(data)
self.build(flowables=[rl_table])
if __name__ == "__main__":
sample_data = pd.read_csv("sample_data.csv", sep=";")
GeneratePDF(df=sample_data)
Uitkomst
Het resultaat van de simpelete PDF.
Het is te zien dat hoofdzakelijk de data zonder layout op een specifieke plek van de A4 geplaatst is (zie de argumenten in de Frame method). Dit lijkt nog ver van een factuur, maar inmiddels is er wel een PDF gecreëerd door middel van Python (#winnen). Lees snel door om te kijken hoe hier meer structuur aan toegevoegd kan worden.
Indeling factuur aanpassen
De volgende stap om de PDF meer op een factuur te laten lijken is om verschillende onderdelen op een natuurlijke plek neer te zetten. Hierbij zullen er meer elementen worden toegevoegd ten opzichte van de versie hierboven. Er worden onder andere adresgegevens, datum, titels, factuurnummers en een footer toegevoegd.
Uitleg
De vorige codebase is uitgebreid met de volgende zaken als meest belangrijke elementen:
- De locatie en grootte van het vlak met de data is aangepast. Het Frame element heeft nu de breedte van het document (self.width) en hierdoor is de x1=0 positie gelijk gekomen met de linkerkant van het document. Om nu de tabel een bepaalde afstand van de rand te printen wordt er een waarde groter dan 0 meegegeven voor het x1 argument (y1 idem dito).
- Voor de pagina-indeling wordt nu een methode meegegeven aan het onPage argument. Hiermee kan er door middel van verschillende methodes (gegroepeerd in on_first_page()) verschillende elementen worden toegevoegd aan de PDF. In deze blog worden alleen PDF’s met één pagina getoond, daarom dat de naam van de methode misschien overbodig lijkt. In het geval van meerdere pagina’s wil je vaak een andere pagina-indeling op de 1e pagina ten opzichte van de rest, en heb je dus verschillende methodes om je pagina-elementen te groeperen.
- add_default_info() wordt gebruikt om algemene informatie op de pagina te krijgen. In dit geval is dit alleen de footer die door middel van een nieuw Frame en de Paragraph methode wordt aangemaakt. Hier kan de tekst naar wens worden ingevuld, en worden uitgebreid met meerdere elementen aan de footer_list.
- Verschillende methodes printen een specifieke string op een bepaalde positie. Hier kan jezelf ook extra elementen aan toevoegen, namen van methodes aanpassen of door middel van een iteratief proces meerdere facturen aanmaken (met bijvoorbeeld verschillende factuurnummers) door de string niet hard te coderen.
Code
import pandas as pd
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, Table, Paragraph, TableStyle
class GeneratePDF(BaseDocTemplate):
def __init__(self, df: pd.DataFrame):
self.df = df
# Initialise Reportlab class
super().__init__(filename="sample_invoice_structuur.pdf", page_size=A4)
# start posities
self.x = 50
self.y = 740
# Create the frame in which the data should be placed
table_frame = Frame(x1=self.x, y1=self.bottomMargin, width=self.width, height=self.height - 9 * cm)
# Creating the page template
invoice_page = PageTemplate(frames=[table_frame], onPage=self.on_first_page)
self.addPageTemplates([invoice_page])
# Create the Reportlab table in which we will place the main invoice data
self.set_table()
def set_table(self):
"""
Create table format for reportlab
"""
# Transform dataframe to format for Reportlab
data = [self.df.columns.astype(str).tolist()] + self.df.to_numpy().tolist()
rl_table = Table(data,
colWidths=[self.width * (2 / 3), self.width * (1 / 3)],
)
table_style_list = [
("LINEABOVE", (0, 0), (-1, 0), 1, colors.black),
("LINEABOVE", (0, 0), (1, 1), 1, colors.black),
("LINEBELOW", (0, -1), (-1, -1), 1, colors.black),
("LINEBEFORE", (0, 0), (-1, -1), 1, colors.black),
("LINEAFTER", (-1, 0), (-1, -1), 1, colors.black),
]
rl_table.setStyle(TableStyle(table_style_list))
self.build(flowables=[rl_table])
def on_first_page(self, canvas, doc):
# Add the logo and other default stuff
self.add_default_info(canvas)
self.set_datum(canvas)
self.set_subtitle(canvas)
self.set_address_info(canvas)
self.set_title(canvas)
def add_default_info(self, canvas):
self.set_footer(canvas)
def set_address_info(self, canvas):
address_list = list()
address_list.append(Paragraph("Zypp"))
address_list.append(Paragraph("Stationsplein 45, A4.004"))
address_list.append(Paragraph("3013 AK, Rotterdam"))
f = Frame(x1=self.x - 5, y1=self.y - 140, width=200, height=90)
f.addFromList(address_list, canvas)
def set_title(self, canvas):
title = "PDF PYTHON INVOICE"
canvas.drawString(x=self.x, y=self.y - 175, text=title)
def set_datum(self, canvas):
datum = "Rotterdam, 24-04-2022"
canvas.drawString(x=self.x, y=self.y - 155, text=datum)
def set_subtitle(self, canvas):
subtitle = "Invoice number: 12345678-001"
# Draw the title
canvas.drawString(x=self.x, y=self.y - 195, text=subtitle)
def set_footer(self, canvas):
footer_list = list()
footer_list.append(Paragraph("Zypp | Cool PDF business | Footer Inc."))
f = Frame(x1=self.width / 2, y1=-20, width=self.width, height=100)
f.addFromList(footer_list, canvas)
if __name__ == "__main__":
sample_data = pd.read_csv("sample_data.csv", sep=";")
GeneratePDF(df=sample_data)
De code voor de PDF met structuur
Uitkomst
Het resultaat van de PDF met structuur.
Nu zijn de eerste contouren van een factuur zichtbaar door het toevoegen van een aantal structurele elementen aan de PDF. Zaken als adressering, (sub)titels en footers zijn vaak essentiële onderdelen van een factuur. Door de juiste gegevens in te vullen, kan dit zelfs al voldoen aan de minimale voorwaarden voor een geldige factuur. Denk hierbij aan het invullen van BTW-nummer, KvK-nummer en uw adressering ( zie ook hier).
Opmaak factuur aanpassen
De laatste stap die wordt behandeld is het verbeteren van de opmaak. Hierin worden de mogelijkheden beschreven om kleuren, lettertypes en afbeeldingen aan te passen of toe te voegen.
Uitleg
In deze stap wordt het Babel package toegevoegd om de getallen netjes als valuta te kunnen tonen. Er zijn talloze andere packages die gebruikt kunnen worden om opmaak te verbeteren, in dit geval is er voor gekozen om de verscheidenheid te minimaliseren.
-
Er wordt gestart met het formatteren van de data. De bedragen worden opgemaakt als valuta, kolomnamen met hoofdletters neergezet en een witregel toegevoegd boven de regel met het totaalbedrag.
-
Er worden hier kleuren gedefinieerd die op verschillende plekken in het document gebruikt kunnen worden. Door de mogelijkheid van hex-kleurcodes is er volledige vrijheid om company_color en secondary_color te bepalen. Hiermee kan al een groot deel van het document gekoppeld worden aan de merkidentiteit van het bedrijf.
-
Door middel van de ParagraphStyle methode kan er opmaak worden meegegeven aan tekst die met Paragraph aangemaakt wordt. In dit geval worden er twee stijl-sets aangemaakt waarin lettertype, lettergrootte en tekstkleur worden gedefinieerd. Ook kan de uitlijning met alignment worden bepaald (links, centraal, rechts, spreiden = 0, 1, 2, 4).
-
In set_table() worden nu ook de kolombreedte en rijhoogte bepaald door dit als argument mee te geven aan Table(). Er worden drie verschillende rijhoogtes aangegeven, de 1e is voor de rij met kolomnamen, daarna de rest van de inhoud en als laatste wordt er meer ruimte gegeven voor de rij met het totaal.
De tabel wordt verder opgemaakt door lijnen, kleuren, lettergrootte en uitlijning te definiëren over de verschillende cellen van de tabel.
Er wordt onder de tabel nog een footnote toegevoegd om hier informatie voor de ontvanger neer te zetten.
-
Om de merkidentiteit nog duidelijker te maken kunnen er afbeeldingen van bijvoorbeeld het logo worden toegevoegd. In dit geval wordt dit gedaan bovenaan het document en in de footer. In de drawImage methode wordt de bronlocatie van de afbeelding en de locatie en afmetingen op de pagina aangegeven.
-
De verschillende tekst elementen worden opgemaakt met kleur, lettertypegrootte en lettertype. Om de opmaak specifiek voor die elementen aan te geven wordt er gebruik gemaakt van de saveState() en restoreState() methodes.
-
Naast afbeeldingen kunnen er ook (gekleurde) vlakken worden toegevoegd. Hier is dit gedaan om de footer een andere kleur te geven. Het vlak is hier als een rechthoek gedefinieerd met rect().
-
Door het paginanummer te onttrekken met de getPageNumber() methode van ReportLab is het mogelijk om deze vervolgens op de pagina te printen op de gewenste positie.
Code
from babel.numbers import format_currency
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.units import cm
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, Table, Paragraph, TableStyle
class GeneratePDF(BaseDocTemplate):
def __init__(self, df: pd.DataFrame):
self.df = self.format_data(df=df)
# Initialise Reportlab class
super().__init__(filename="sample_invoice_opmaak.pdf", page_size=A4)
# Define f.e. a company color
self.company_color = colors.HexColor("#F0B62E")
self.secondary_color = colors.HexColor("#008375")
# Define our paragraph style
self.p_style = ParagraphStyle("default_style", fontName="Helvetica", fontSize=9,
textColor=self.company_color)
# Define the footer style
self.f_style = ParagraphStyle(
"default_style", fontName="Helvetica", fontSize=12, textColor=self.secondary_color,
alignment=1
)
# start posities
self.x = 50
self.y = 740
# Create the frame in which the data should be placed
table_frame = Frame(x1=self.x, y1=self.bottomMargin, width=self.width, height=self.height - 9 * cm)
# Creating the page template
invoice_page = PageTemplate(frames=[table_frame], onPage=self.on_first_page)
self.addPageTemplates([invoice_page])
# Create the Reportlab table in which we will place the main invoice data
self.set_table()
@staticmethod
def format_data(df) -> pd.DataFrame:
"""
Format the data to show nicely on the invoice
:param df:
:return:
"""
df_format = df.copy()
# Format to euro format
df_format["amount"] = df_format["amount"].str.replace(",", ".").apply(format_currency, currency="EUR", locale="nl_NL")
# Add enter at the total row
df_format.loc[df_format["items"] == "total"] = "\n" + df_format.loc[df_format["items"] == "total"]
# Capitalize header
df_format.columns = df_format.columns.str.capitalize()
return df_format
def set_table(self):
"""
Create table format for reportlab
"""
# Transform dataframe to format for Reportlab
data = [self.df.columns.astype(str).tolist()] + self.df.to_numpy().tolist()
rl_table = Table(data,
colWidths=[self.width * (2 / 3), self.width * (1 / 3)],
# First the header of the table, the rest, and the total row
rowHeights=[1 * cm] + [0.6 * cm] * (self.df.shape[0] - 1) + [0.8 * cm],
)
background_color = colors.grey
table_style_list = [
# (x1, y1), (x2, y2), -1 is the last one
# Background color in first row
("BACKGROUND", (0, 0), (-1, 0), background_color),
# Color of text in first rows
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
# Color oof text of the rest
("TEXTCOLOR", (0, 1), (-1, -1), self.secondary_color),
("LINEABOVE", (0, 0), (-1, 0), 1, colors.black),
("LINEABOVE", (0, 0), (1, 1), 1, colors.black),
# Line above the total row
("LINEABOVE", (0, -1), (-1, -1), 1, colors.black),
("LINEBELOW", (0, -1), (-1, -1), 1, colors.black),
("LINEBEFORE", (0, 0), (-1, -1), 1, colors.black),
("LINEAFTER", (-1, 0), (-1, -1), 1, colors.black),
# Adjust the fontsize of the first row
("FONTSIZE", (0, 0), (-1, 0), 13),
# Align the last column to the right
("ALIGN", (-1, 0), (-1, -1), "RIGHT"),
# Vertical align all cells in the middle
("VALIGN", (0, 0), (-1, -2), "MIDDLE"),
]
rl_table.setStyle(TableStyle(table_style_list))
# Create extra information at the foot of the table
df_footer = pd.DataFrame({"tekst": "Please pay the amount in 60 days to the bank account number: "
"NL99 BANK 1234 5678 90"}, index=[0])
footer_table = Table(df_footer.to_numpy().tolist(), colWidths=[self.width])
footer_style = [
("TEXTCOLOR", (0, 0), (0, 0), self.secondary_color),
("FONTSIZE", (0, 0), (0, 0), 7),
]
footer_table.setStyle(TableStyle(footer_style))
self.build(flowables=[rl_table, footer_table])
def on_first_page(self, canvas, doc):
# Add the logo and other default stuff
self.add_default_info(canvas)
self.set_logo(canvas)
self.set_date(canvas)
self.set_subtitle(canvas)
self.set_address_info(canvas)
self.set_title(canvas)
def add_default_info(self, canvas):
canvas.saveState()
self.set_footer(canvas)
self.set_page_number(canvas)
canvas.restoreState()
def set_logo(self, canvas):
"""
Set the logo at the top of the document
"""
canvas.drawImage(image="https://zypp.io/static/assets/img/logos/zypp/black/500px.png",
x=self.x, y=self.pagesize[1] - 100, width=100, height=40, mask="auto")
def set_address_info(self, canvas):
"""
Set the address info of the document
"""
address_list = list()
address_list.append(Paragraph("Zypp"))
address_list.append(Paragraph("Stationsplein 45, A4.004"))
address_list.append(Paragraph("3013 AK, Rotterdam"))
f = Frame(x1=self.x - 5, y1=self.y - 140, width=200, height=90)
f.addFromList(address_list, canvas)
def set_title(self, canvas):
"""
Set the title of the document and style it.
"""
canvas.saveState()
canvas.setFillColor(self.company_color)
canvas.setFont("Helvetica-Bold", size=14)
title = "PDF PYTHON INVOICE"
canvas.drawString(x=self.x, y=self.y - 175, text=title)
canvas.restoreState()
def set_date(self, canvas):
"""
Set the date of the document and style it.
"""
canvas.saveState()
canvas.setFillColor(colors.darkblue)
canvas.setFont("Helvetica", size=9)
date = "Rotterdam, 24-04-2022"
canvas.drawString(x=self.x, y=self.y - 155, text=date)
canvas.restoreState()
def set_subtitle(self, canvas):
"""
Set the subtitle of the document and style it.
"""
canvas.saveState()
canvas.setFillColor(self.secondary_color)
canvas.setFont("Helvetica", size=10)
subtitle = "Invoice number: 12345678-001"
# Draw the title
canvas.drawString(x=self.x, y=self.y - 195, text=subtitle)
canvas.restoreState()
def set_footer(self, canvas):
"""
Set the footer by creating a coloured rectangle based on the width of the page (self.pagesize[0]) and adding
a logo.
"""
footer_list = list()
canvas.saveState()
canvas.setFillColor(self.secondary_color)
canvas.rect(
0, 0, self.pagesize[0], 120,
stroke=0, fill=1
)
canvas.restoreState()
footer_list.append(Paragraph("Cool PDF business | Footer Inc."))
canvas.drawImage(image="https://zypp.io/static/assets/img/logos/zypp/black/500px.png",
x=self.x, y=55, width=50, height=20, mask="auto")
f = Frame(x1=self.x + 60, y1=-20, width=self.width, height=100)
f.addFromList(footer_list, canvas)
def set_page_number(self, canvas):
"""
Set the page number with self.x (left indent) and self.width (width of the main table)
"""
page_number = canvas.getPageNumber()
canvas.drawString(x=self.width + self.x, y=130, text=f"{page_number}")
if __name__ == "__main__":
sample_data = pd.read_csv("sample_data.csv", sep=";")
GeneratePDF(df=sample_data)
De code voor de PDF met opmaak
Uitkomst
Het resultaat van de PDF met personalisatie.
Zie hier: een opgemaakte factuur! Er zijn veel verschillen in bijvoorbeeld kleuren en lettergrootte gebruikt om de verschillende mogelijkheden aan te duiden. De afbeeldingen en bedrijfskleuren kunnen de factuur een herkenbaar document maken, zodat klanten direct weten met wie ze te maken hebben.
Ten slotte
In deze blog is er beschreven hoe een gepersonaliseerde factuur gemaakt kan worden d.m.v. Python. Er is geprobeerd een tipje van de sluier te lichten in deze wondere wereld. Dit is slechts het begin van de mogelijkheden van PDF’s met Python en we hopen je hiermee geïnspireerd en/of geholpen te hebben. Bedankt voor het lezen en als je nog tips of vragen hebt laat het ons gerust weten!
✅ Make sure to follow us on LinkedIn for new content!

