Tech With Galvan
Back to Articles
Development12/15/2025 • 4 min read

PDF Invoice Generator using Python

Generate professional PDF invoices with Python — line items, totals with tax, invoice numbering, and a client database in JSON.

Galvan
Galvan

Founder & Creator

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

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 with pdf.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 match clients.json exactly; validate with a friendly error listing available keys.
  • Counter file lost — invoice numbers restart; back up invoice_counter.txt with 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.