Introduction
Freelancers lose hours to invoicing — formatting spreadsheets, copying client details, computing tax, keeping numbers unique. This script generates a professional PDF invoice from a line-item list and a small client database, with automatic totals, tax math, and sequential invoice numbers. It uses fpdf2, a pure-Python PDF library with no external dependencies — the same spirit as the PDF merger, but creating documents instead of combining them.
Features
- Client database — repeat clients stored in JSON, referenced by key.
- Line items — description, quantity, rate; totals computed for you.
- Tax handling — configurable percentage, added at the end.
- Sequential numbering — INV-2026-001, 002, … tracked automatically.
- Professional layout — header, itemized table, totals block, footer.
Prerequisites
- Python 3.8+ — from python.org.
- Dependencies:
pip install fpdf2
Step 1: Create the Client Database
Save as clients.json:
{
"acme": {
"name": "Acme Corporation",
"address": "42 Enterprise Way\nSpringfield, IL 62701",
"email": "[email protected]"
},
"globex": {
"name": "Globex Industries",
"address": "7 Market Street\nRiverside, CA 92501",
"email": "[email protected]"
}
}
Step 2: Create the Script
Save as invoice.py:
import json
import sys
from datetime import date
from pathlib import Path
from fpdf import FPDF
MY_DETAILS = {
"name": "Galvan",
"address": "123 Developer Lane\nAhmedabad, India",
"email": "[email protected]",
}
TAX_RATE = 0.18
COUNTER_FILE = "invoice_counter.txt"
def next_invoice_number():
year = str(date.today().year)
count = int(Path(COUNTER_FILE).read_text()) if Path(COUNTER_FILE).exists() else 0
count += 1
Path(COUNTER_FILE).write_text(str(count))
return f"INV-{year}-{count:03d}"
class InvoicePDF(FPDF):
def footer(self):
self.set_y(-15)
self.set_font("helvetica", "I", 8)
self.set_text_color(150)
self.cell(0, 10, f"Invoice {self.invoice_no} — Thank you for your business!", align="C")
def generate_invoice(client_key, items, notes=""):
clients = json.loads(Path("clients.json").read_text())
client = clients[client_key]
pdf = InvoicePDF()
pdf.invoice_no = next_invoice_number()
pdf.add_page()
# Header
pdf.set_font("helvetica", "B", 22)
pdf.set_text_color(15, 118, 110)
pdf.cell(0, 12, "INVOICE", new_x="LMARGIN", new_y="NEXT")
pdf.set_font("helvetica", "", 10)
pdf.set_text_color(60)
pdf.cell(0, 6, pdf.invoice_no, new_x="LMARGIN", new_y="NEXT")
pdf.ln(4)
# From / To
pdf.set_font("helvetica", "B", 10)
pdf.cell(95, 6, "From:", new_x="LMARGIN", new_y="NEXT")
pdf.cell(95, 6, "Bill To:", new_x="LMARGIN", new_y="NEXT")
pdf.set_font("helvetica", "", 10)
pdf.multi_cell(95, 5, f"{MY_DETAILS['name']}\n{MY_DETAILS['address']}\n{MY_DETAILS['email']}")
pdf.set_xy(105, pdf.get_y() - 15)
pdf.multi_cell(95, 5, f"{client['name']}\n{client['address']}\n{client['email']}")
pdf.ln(6)
# Items table
pdf.set_font("helvetica", "B", 10)
pdf.set_fill_color(15, 118, 110)
pdf.set_text_color(255)
pdf.cell(105, 8, "Description", fill=True)
pdf.cell(25, 8, "Qty", fill=True, align="C")
pdf.cell(30, 8, "Rate", fill=True, align="R")
pdf.cell(30, 8, "Amount", fill=True, align="R", new_x="LMARGIN", new_y="NEXT")
pdf.set_font("helvetica", "", 10)
pdf.set_text_color(30)
subtotal = 0
for desc, qty, rate in items:
amount = qty * rate
subtotal += amount
pdf.cell(105, 7, desc)
pdf.cell(25, 7, str(qty), align="C")
pdf.cell(30, 7, f"{rate:,.2f}", align="R")
pdf.cell(30, 7, f"{amount:,.2f}", align="R", new_x="LMARGIN", new_y="NEXT")
# Totals
tax = subtotal * TAX_RATE
pdf.ln(4)
pdf.set_x(130)
pdf.cell(40, 6, "Subtotal:", align="R")
pdf.cell(20, 6, f"{subtotal:,.2f}", align="R", new_x="LMARGIN", new_y="NEXT")
pdf.set_x(130)
pdf.cell(40, 6, f"Tax ({TAX_RATE:.0%}):", align="R")
pdf.cell(20, 6, f"{tax:,.2f}", align="R", new_x="LMARGIN", new_y="NEXT")
pdf.set_font("helvetica", "B", 11)
pdf.set_x(130)
pdf.cell(40, 7, "Total:", align="R")
pdf.cell(20, 7, f"{subtotal + tax:,.2f}", align="R", new_x="LMARGIN", new_y="NEXT")
if notes:
pdf.ln(8)
pdf.set_font("helvetica", "I", 9)
pdf.multi_cell(0, 5, notes)
out = f"{pdf.invoice_no}.pdf"
pdf.output(out)
print(f"Generated {out} — total {subtotal + tax:,.2f}")
if __name__ == "__main__":
generate_invoice(
"acme",
items=[
("Website development — landing page", 1, 45000),
("SEO optimization — monthly retainer", 1, 12000),
("Consultation hours", 6, 1500),
],
notes="Payment due within 15 days. UPI: galvan@upi",
)
Step 3: Run the Generator
python invoice.py
INV-2026-001.pdf appears — open it: branded header, client block, itemized table, tax math, footer with the invoice number.
How It Works
fpdf2 is a cursor-based canvas: cell() places text at the cursor and advances it; new_x/new_y control where the cursor lands afterward. The item table is nothing but cells in a row with fixed widths, each row ending with new_y="NEXT" — tables in fpdf2 are loops you write, not widgets you configure, which makes them completely predictable.
The totals math is deliberately boring: subtotal from the items loop, tax as subtotal * rate, total as the sum. The invoice’s credibility comes from consistent alignment (right-aligned numbers in fixed-width cells) rather than from fancy computation.
Sequential numbering uses a one-line counter file — read, increment, write. It’s not crash-proof (two invoices in the same millisecond could race), but for a single-user freelancer tool it’s the right amount of engineering, the same minimalism as the habit tracker’s CSV log.
The footer override shows fpdf2’s class-based design: subclass FPDF, override footer(), and every page gets the branding automatically — the method runs at page-finalization time with access to your custom attributes like invoice_no.
Common Errors & Fixes
FPDF error: Missing glyph— default fonts (helvetica) are Latin-only; for ₹ or Devanagari, add a Unicode TTF font withpdf.add_font("Noto", "", "NotoSans-Regular.ttf").- Numbers misalign in columns — cells have different widths; keep every column’s width identical across header and item rows.
KeyError: 'acme'— the client key must matchclients.jsonexactly; validate with a friendly error listing available keys.- Counter file lost — invoice numbers restart; back up
invoice_counter.txtwith your accounts data.
Key Concepts
- Cursor-based layout — cells, cursor movement, and
new_x/new_y. - Class overrides — footer/header hooks that run per page.
- Counter files — minimal sequential state for single-user tools.
- Separation of data — clients in JSON, items as parameters, layout in code.
What to Try Next
- Add a logo image in the header with
pdf.image("logo.png", x, y, w). - Add multi-currency support — a currency parameter formatting the totals block.
- Add a Tkinter front-end — pick a client, add items in a table, generate; the paint app’s window scaffolding fits.
- Email the invoice automatically — the email automation mailer attaches the PDF in one line.
FAQ
Is fpdf2 good enough for real invoices?
Yes — it’s actively maintained, pure Python, and handles tables, images, and Unicode (with font files). Thousands of small businesses run on it.
How do I add my logo and brand colors?
pdf.image() in the header section, and swap the set_fill_color/set_text_color values — the teal (15, 118, 110) is a placeholder for your palette, like the color palette generator would suggest.
Can I store invoice history?
Keep the generated PDFs in a dated folder and append a JSON line per invoice (number, client, total, date) — a searchable ledger in ten lines, in the expense tracker’s storage style.