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.

  • Documents & Office
  • $invoice-generation
  • 198 lines
  • 2 files
  • ~2,050 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- invoice-generation

Installs to ~/.agents/skills/invoice-generation/ for every project on this machine.

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

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.

~/.agents/skills/invoice-generation/SKILL.md

--- name: invoice-generation description: Generates invoices, credit notes and receipts as PDF from order or billing data - Decimal money math with per-currency rounding, tax breakdown by… license: MIT ---

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 the factur-x Python 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.net and totals.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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem