Extract structured data from PDF invoices with an LLM, then run a battery of validation checks that flag anything uncertain for human review. Built for a vendor / accounts-payable workflow, with the extracted data destined for the SaldeoSMART accounting system.
Given a PDF invoice, the pipeline:
- Extracts the vendor/seller fields with an LLM (OpenAI, strict JSON schema)
— company details, invoice number, dates, gross amount, currency, tax ID, and
bank account (IBAN or local account number). The prompt normalizes
locale-specific formats to canonical machine values: dates to ISO 8601
(
YYYY-MM-DD), the amount to a plain dot-decimal (no thousands separators), and the currency to its ISO 4217 code. - Reads the PDF's raw text separately (
pdfplumber) as ground truth. - Validates the extraction and produces a
ValidatedInvoicewith a list of issues and aflagged_for_reviewflag.
The validation layer is the core value-add — it catches LLM hallucinations and malformed data so only invoices that genuinely need attention are surfaced:
- Grounding — every extracted string/amount must appear verbatim in the PDF
text, tolerant of the whitespace, thousands separators and stray characters
(stray spaces, underscores from underlined total fields) that
pdfplumberscatters through numbers — guarding against hallucinated values. - Account — a captured IBAN is fully validated (structural format, country-specific length, ISO 7064 MOD-97 checksum); a value that only looks like an IBAN but fails is flagged as a likely typo, while a plain domestic account number is accepted as-is (bank/clearing codes aren't modelled).
- Currency — validated against the ISO 4217 code set.
- Dates — issue date not in the future, payment date not before issue date,
and payment date consistent with
issue_date + payment_terms_days. - Scanned / image-only PDFs — when a PDF has no extractable text layer, grounding can't run, so it's skipped and a single warning is emitted instead of flagging every field as "not found". (OCR is a planned follow-up.)
Each issue carries a severity (error / warning); any error sets
flagged_for_review = True.
src/
extraction/ LLM extraction (llm_extract) + raw text (text_extract)
validation/ validate_invoice() + individual check functions (checks.py)
models/ Pydantic models: ExtractedInvoice, ValidationIssue, ValidatedInvoice
pipeline/ process_invoice() — orchestrates extraction + validation
ui/ Tkinter review app: single/batch, PDF preview, tooltips (see below)
saldeo/ (planned) send processed invoices via the Saldeo API
main.py CLI entry point for a single invoice
tests/ pytest suite (extraction mocked — no live LLM calls)
sample_invoices/ drop your PDFs here (gitignored contents)
The project runs in a conda environment named invoice-reader (Python 3.13).
Dependencies are listed in requirements.txt: openai,
pdfplumber, pydantic, python-dotenv, plus Pillow and pypdfium2 for the
UI's PDF preview.
conda create -n invoice-reader python=3.13
conda activate invoice-reader
pip install -r requirements.txtCreate a .env file in the project root with your OpenAI key:
OPENAI_API_KEY=sk-...
The model is set in src/extraction/config.py.
conda run -n invoice-reader python src/main.py path/to/invoice.pdfPrints the extracted fields as JSON plus the validation result (flagged status and any issues).
A Tkinter desktop app for reviewing extractions. Pick a single PDF or a whole folder, press Process, then page through the results:
- Invoice list with a status glyph per file (
✓ok /⚠flagged /✗error). Processing more files appends to the list rather than clearing it. - Fields table — every extracted field, flagged ones highlighted (red = error, amber = warning). Hover a truncated cell to see its full value.
- Issues list — every validation issue with its severity, field and message.
- PDF preview beside the data: pages rasterized with
pypdfium2to fit the pane width, scrollable, multi-page, re-rendered sharp on resize. The app is DPI-aware, so the preview is crisp on scaled / high-DPI displays. - Open in PDF reader — open the selected invoice in the OS default viewer.
conda run -n invoice-reader python src/ui/__main__.pyProcessing calls the LLM (spends tokens). To explore the whole interface — including loading real PDFs into the viewer — without any API calls, run the demo, where processing is stubbed out and a few sample results are preloaded:
conda run -n invoice-reader python src/ui/demo.py
conda run -n invoice-reader python -m pytest -qThe extraction layer is stubbed in tests, so the suite never makes live LLM calls.
Note on cost:
process_invoicesends each PDF to the OpenAI API, which costs tokens. Everything except actually processing real invoices (launching the UI, the demo, rendering previews, running tests) is free.
saldeo— submit processed/approved invoices via the Saldeo API.- OCR fallback for scanned / image-only PDFs (currently warned and skipped for grounding).
- UI — draw highlight boxes over flagged field values directly on the
rendered PDF page (groundwork:
pdfplumberword boxes +PageImage.draw_rect()).