Invoice Generation skill for Codex
Invoices whose totals add up to the cent. Codex treats invoices as financial records, not just PDFs: money math in Decimal with explicit rounding per currency, tax grouped by rate, numbers issued from a locked counter inside the issuing transaction, and an immutable snapshot plus hash stored for every issued invoice. Templates render with WeasyPrint as PDF/A-3, ready for embedded e-invoice XML when a jurisdiction requires it.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- invoice-generation Installs to ~/.agents/skills/invoice-generation/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install invoice-generation The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project invoice-generation Installs to .agents/skills/invoice-generation/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/invoice-generation.zipunzip -o invoice-generation.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) invoice-generation PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $invoice-generation, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Generates invoices, credit notes and receipts as PDF from order or billing data - Decimal money math with per-currency rounding, tax breakdown by rate, gap-free invoice numbering, HTML templates rendered with WeasyPrint (or ReportLab/Typst), immutable snapshots, and optional Factur-X/ZUGFeRD e-invoice XML. Use when the user asks to add invoice or receipt PDFs to an app, fix wrong totals or rounding on invoices, design an invoice template, number invoices correctly, or batch-generate invoices from a CSV or database.
Use it when
- An app needs invoice, credit note or receipt PDFs
- Invoice totals, tax lines or rounding are wrong
- Invoice numbering must be unique and sequential
- A customer or country requires Factur-X, ZUGFeRD or another e-invoice format
Use something else when
- You are extracting data from invoices you received: use form-data-extraction
- The PDF is a general report rather than a billing document: use pdf-generation
How to check the result
Unit-test the calculation across mixed tax rates, discounts and zero-decimal currencies, assert lines sum to totals, and render 1-line and 40-line fixtures to check layout and searchable text.
The mistake it prevents
Re-rendering an already issued invoice from live customer data, so a later address or price change silently rewrites history.
Done well means: every total can be recomputed from the lines to the cent, tax is shown per rate, the invoice number is unique and issued once, an issued invoice is never regenerated with different content, and the PDF carries everything the recipient’s accounts team needs to pay it.
Ground rules
- Money is
Decimal(or integer minor units), never float. Rounding mode and the point where rounding happens (per line or per tax group) are explicit and tested. - An issued invoice is immutable. Corrections are credit notes referencing the original.
- Invoice numbers come from an atomic sequence at issue time, not when a draft is created.
- Legal content (required fields, tax wording, e-invoicing mandates) depends on the seller’s jurisdiction. Ask the user which countries apply and what their accountant requires; do not invent tax rules or give tax advice.
- Emailing invoices to customers is a send action: build and preview, then get explicit confirmation before any batch send.
Workflow
- [ ] 1. Read AGENTS.md and existing billing code: where orders, customers and taxes live
- [ ] 2. Define the invoice data model (seller, buyer, lines, tax groups, totals, payment)
- [ ] 3. Implement calculation as a pure function with unit tests
- [ ] 4. Implement numbering (DB sequence/row lock) and the issue step
- [ ] 5. Build the template (HTML + WeasyPrint by default) and render a PDF
- [ ] 6. Store the data snapshot + PDF + hash; never re-render issued invoices from live data
- [ ] 7. Verify totals, required fields and layout with edge-case fixtures
Data model and calculation
from dataclasses import dataclass, field
from decimal import ROUND_HALF_UP, Decimal
from collections import defaultdict
MINOR_UNITS = {"EUR": 2, "USD": 2, "GBP": 2, "JPY": 0, "KWD": 3}
def q(amount: Decimal, currency: str) -> Decimal:
exp = Decimal(1).scaleb(-MINOR_UNITS[currency]) # 0.01, 1, 0.001
return amount.quantize(exp, rounding=ROUND_HALF_UP)
@dataclass(frozen=True)
class Line:
description: str
quantity: Decimal
unit_price: Decimal # net, excluding tax
tax_rate: Decimal # 0.19 for 19 %
discount_pct: Decimal = Decimal("0")
@dataclass
class Totals:
net_by_rate: dict = field(default_factory=dict)
tax_by_rate: dict = field(default_factory=dict)
net: Decimal = Decimal("0")
tax: Decimal = Decimal("0")
gross: Decimal = Decimal("0")
def compute(lines: list[Line], currency: str) -> tuple[list[Decimal], Totals]:
"""Round each line net, then compute tax once per rate group (common EU practice)."""
line_nets = []
groups: dict[Decimal, Decimal] = defaultdict(Decimal)
for ln in lines:
net = q(ln.quantity * ln.unit_price * (1 - ln.discount_pct / 100), currency)
line_nets.append(net)
groups[ln.tax_rate] += net
t = Totals()
for rate, net in sorted(groups.items()):
t.net_by_rate[rate] = net
t.tax_by_rate[rate] = q(net * rate, currency)
t.net = sum(t.net_by_rate.values(), Decimal("0"))
t.tax = sum(t.tax_by_rate.values(), Decimal("0"))
t.gross = t.net + t.tax
return line_nets, t
- Construct Decimals from strings (
Decimal("19.99")), never from floats. - Per-line versus per-group tax rounding can differ by a cent; match what the existing accounting system does and encode it in a test.
- Prices entered gross (B2C shops) need the reverse calculation; keep net and gross logic in one module so both directions use the same rounding.
Numbering
-- One row per series; lock it inside the issuing transaction
UPDATE invoice_series
SET next_number = next_number + 1
WHERE series = 'INV-2025'
RETURNING next_number - 1 AS issued_number;
Format at display time (f"INV-2025-{n:06d}"). Run the update, the invoice insert and
the snapshot write in one transaction so a failure does not burn a number. Database
sequences (nextval) can skip values on rollback; if the jurisdiction requires gap-free
numbering, use the locked counter row above.
Template and rendering
from pathlib import Path
from jinja2 import Environment, FileSystemLoader, select_autoescape
from weasyprint import HTML
env = Environment(loader=FileSystemLoader("templates"), autoescape=select_autoescape())
env.filters["money"] = lambda v, cur: f"{cur} {v:,.{MINOR_UNITS[cur]}f}"
def render_invoice(snapshot: dict, out: Path) -> bytes:
html = env.get_template("invoice.html").render(**snapshot)
pdf = HTML(string=html, base_url="templates/").write_pdf(pdf_variant="pdf/a-3b")
out.write_bytes(pdf)
return pdf
<!-- templates/invoice.html (excerpt) -->
<header>
<img src="logo.svg" alt="{{ seller.name }}" class="logo">
<h1>Invoice {{ number }}</h1>
<dl>
<dt>Issue date</dt><dd>{{ issue_date }}</dd>
<dt>Due date</dt><dd>{{ due_date }}</dd>
</dl>
</header>
<section class="parties">
<address><strong>{{ seller.name }}</strong><br>
{% for l in seller.address %}{{ l }}<br>{% endfor %}VAT {{ seller.vat_id }}</address>
<address><strong>{{ buyer.name }}</strong><br>
{% for l in buyer.address %}{{ l }}<br>{% endfor %}</address>
</section>
<table class="lines">
<thead><tr><th>Description</th><th>Qty</th><th>Unit price</th><th>Tax</th><th>Amount</th></tr></thead>
<tbody>
{% for ln in lines %}
<tr><td>{{ ln.description }}</td><td class="num">{{ ln.quantity }}</td>
<td class="num">{{ ln.unit_price | money(currency) }}</td>
<td class="num">{{ (ln.tax_rate * 100) | round(1) }}%</td>
<td class="num">{{ ln.net | money(currency) }}</td></tr>
{% endfor %}
</tbody>
</table>
<table class="totals">
{% for rate, net in totals.net_by_rate.items() %}
<tr><th>Net at {{ (rate * 100) | round(1) }}%</th><td>{{ net | money(currency) }}</td></tr>
<tr><th>Tax {{ (rate * 100) | round(1) }}%</th><td>{{ totals.tax_by_rate[rate] | money(currency) }}</td></tr>
{% endfor %}
<tr class="grand"><th>Total due</th><td>{{ totals.gross | money(currency) }}</td></tr>
</table>
<footer>Pay to IBAN {{ seller.iban }} - reference {{ number }}</footer>
Never mark customer-supplied text | safe; autoescape protects the PDF from injected
markup and remote URLs. Keep CSS in a file: thead { display: table-header-group; },
tr { break-inside: avoid; }, numeric columns text-align: right; font-variant-numeric: tabular-nums;. For engine trade-offs (ReportLab, Typst) see
$pdf-generation.
Receipts for thermal printers: set @page { size: 80mm 297mm; margin: 4mm; } (or
the printer’s roll width) and a single-column layout; test on the device.
Snapshot and immutability
Store at issue time: the full JSON used to render (seller, buyer, lines, totals, template version), the PDF bytes or object-storage key, and a SHA-256 of the PDF. Re-downloads serve the stored PDF. Changing a customer’s address later must not change old invoices.
E-invoicing (Factur-X / ZUGFeRD, XRechnung, Peppol)
Several countries now require or are phasing in structured e-invoices for B2B. When the user needs one:
- Generate the structured XML (CII or UBL, per the required profile) from the same snapshot as the PDF so they cannot disagree.
- For Factur-X/ZUGFeRD, embed the XML into a PDF/A-3 (
pdf_variant="pdf/a-3b"above) with a library such as thefactur-xPython package, then validate the result with an official validator for that profile before going live. - Pure-XML formats (XRechnung, Peppol BIS) are sent as XML; the PDF becomes a human-readable copy only.
Verification
- Unit tests for
compute: mixed tax rates, 100% discount, quantity with decimals, JPY (0 decimals), a case where per-line versus per-group rounding differs. - Independent recompute in the test:
sum(line_nets) == totals.netandtotals.net + totals.tax == totals.gross. - Render fixtures with 1 line, 40 lines (page break, header repeats) and very long
descriptions; inspect the PNGs (
pdftoppm -r 60 -png). pdftotext invoice.pdf - | grep -E "INV-2025-0000|Total due"confirms text is real, searchable text, not an image.- Required fields checklist signed off with the user for their jurisdiction.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Invoice Generation"
short_description: "Invoices whose totals add up to the cent"
default_prompt: "$invoice-generation Add downloadable PDF invoices to our Django billing app" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.