English |
German
An agentic Python library for extracting structured data from receipts and invoices and preparing essential German tax return statements.
- German Tax Alignment — Category taxonomy and VAT handling aligned with German fiscal practice managing receipts
- Local-First — Everything runs completely offline, with data stored in a local database
- 4-Agent Pipeline — Sequential specialised agents for metadata, counterparty, amounts, and line items; short focused prompts for reliable local model performance
- Web UI — Full browser interface for uploading, reviewing, editing, and managing receipts and invoices and preparing tax returns
Backend
Python — package language
FastAPI — backend for the web UI
PaddleOCR — OCR for scanned PDFs
Tesseract — OCR for scanned PDFs and images when PaddleOCR fails or times out
Ollama — local LLMs for structured extraction of information from receipts and invoices
Qwen – laptop-compatible LLMs with qwen2.5:7b-instruct-q4_K_M currently as preferred default for text-based extraction
SQLite – local database for original receipts and extracted data
Frontend
React — interactive frontend
Vite — fast dev server and production bundler
Tailwind CSS — utility-first styling
TypeScript — type-safe component and API code
CLI
Typer — CLI with coloured progress output
Packaging
PyPI — distributed as an installable Python package
pip install finamtFor CLI usage, installing via pipx is recommended — it places finamt into its own dedicated virtual environment, ensuring its dependencies never interfere with your other projects, while still exposing the finamt command globally without requiring you to activate a virtualenv:
pipx install finamtNote for Python 3.14+ users:
finamtcurrently requires Python 3.13. If your system Python is 3.14 or newer, install uv to manage Python versions and pass the resolved path to pipx:uv python install 3.13 pipx install finamt --python $(uv python find 3.13)
- Python 3.10+
- Ollama running locally with a supported model pulled
- Tesseract OCR (optional fallback when PaddleOCR times out)
# Install Ollama
curl -fsSL https://ollama.ai/install.sh | sh
# Pull a model — qwen2.5 7B is the recommended default
ollama pull qwen2.5:7b-instruct-q4_K_MOther models that work well: qwen3:8b, llama3.2, llama3.1.
Ubuntu / Debian
sudo apt-get install tesseract-ocr tesseract-ocr-deumacOS
brew install tesseract tesseract-langWindows
Download the installer from https://github.com/UB-Mannheim/tesseract/wiki and add it to your PATH.
finamt serve
Interactive UI to upload receipts and manage tax statements
from finamt import FinanceAgent
agent = FinanceAgent()
result = agent.process_receipt("receipt.pdf")
if result.success:
data = result.data
print(f"Counterparty: {data.vendor}")
print(f"Date: {data.receipt_date}")
print(f"Total: {data.total_amount} EUR")
print(f"VAT: {data.vat_percentage}% ({data.vat_amount} EUR)")
print(f"Net: {data.net_amount} EUR")
print(f"Category: {data.category}")
print(f"Items: {len(data.items)}")
# Serialise to JSON
with open("extracted.json", "w", encoding="utf-8") as f:
f.write(data.to_json())
else:
print(f"Extraction failed: {result.error_message}")result = agent.process_receipt("invoice_to_client.pdf", receipt_type="sale")from pathlib import Path
from finamt import FinanceAgent
agent = FinanceAgent()
results = agent.batch_process(list(Path("receipts/").glob("*.pdf")))
for path, result in results.items():
if result.success:
print(f"{path}: {result.data.total_amount} EUR")
else:
print(f"{path}: ERROR — {result.error_message}")Settings are read in priority order from: environment variables → .env file → built-in defaults.
# .env
# OCR and general settings
FINAMT_OLLAMA_BASE_URL=http://localhost:11434
FINAMT_OCR_LANGUAGE=german
FINAMT_OCR_TIMEOUT=60
FINAMT_TESSERACT_CMD=tesseract
FINAMT_OCR_PREPROCESS=true
FINAMT_PDF_DPI=150
# Extraction agents — all 4 agents use this model
FINAMT_AGENT_MODEL=qwen2.5:7b-instruct-q4_K_M
FINAMT_AGENT_TIMEOUT=60
FINAMT_AGENT_NUM_CTX=4096
FINAMT_AGENT_MAX_RETRIES=2You can also pass config objects directly:
from finamt import FinanceAgent
from finamt.agents.config import Config, AgentsConfig
agent = FinanceAgent(
config=Config(ocr_language="deu+eng", pdf_dpi=150),
agents_cfg=AgentsConfig(agent_model="qwen3:8b"),
)class FinanceAgent:
def __init__(
self,
config: Config | None = None,
db_path: str | Path | None = "~/.finamt/default/finamt.db",
agents_cfg: AgentsConfig | None = None,
) -> None: ...
def process_receipt(
self,
pdf_path: str | Path | bytes,
receipt_type: str = "purchase", # "purchase" or "sale"
) -> ExtractionResult: ...
def batch_process(
self,
pdf_paths: list[str | Path],
receipt_type: str = "purchase",
) -> dict[str, ExtractionResult]: ...Always check success before accessing data.
@dataclass
class ExtractionResult:
success: bool
data: ReceiptData | None
error_message: str | None
duplicate: bool # True if already in the database
existing_id: str | None # ID of the original if duplicate
processing_time: float | None # seconds
def to_dict(self) -> dict: ...@dataclass
class ReceiptData:
id: str # SHA-256 of OCR text — stable dedup key
receipt_type: ReceiptType # "purchase" or "sale"
counterparty: Counterparty | None # vendor (purchase) or client (sale)
receipt_number: str | None
receipt_date: datetime | None
total_amount: Decimal | None
currency: str | "EUR"
vat_percentage: Decimal | None # e.g. Decimal("19.0")
vat_amount: Decimal | None
net_amount: Decimal | None # computed: total - vat
category: ReceiptCategory
items: list[ReceiptItem]
vat_splits: list[dict] # for mixed-rate invoices
vendor: str | None # alias for counterparty.name
def to_dict(self) -> dict: ...
def to_json(self) -> str: ...@dataclass
class Counterparty:
id: str # UUID assigned by the database
name: str | None
vat_id: str | None # EU format, e.g. DE123456789
tax_number: str | None # German Steuernummer, e.g. 123/456/78901
address: Address
verified: bool # manually confirmed in the UI@dataclass
class ReceiptItem:
position: int | None
description: str
quantity: Decimal | None
unit_price: Decimal | None
total_price: Decimal | None
vat_rate: Decimal | None
vat_amount: Decimal | None
category: ReceiptCategory
def to_dict(self) -> dict: ...A validated string subclass. Invalid values are silently normalised to "other".
from finamt.agents.prompts import RECEIPT_CATEGORIES # list[str]
from finamt.models import ReceiptCategory
cat = ReceiptCategory("software") # valid
cat = ReceiptCategory("unknown_value") # normalised to "other"
cat = ReceiptCategory.other() # explicit fallbackAll exceptions inherit from FinanceAgentError.
| Exception | Raised when |
|---|---|
OCRProcessingError |
PDF cannot be opened or text extraction fails |
LLMExtractionError |
Ollama is unreachable or returns invalid JSON after all retries |
InvalidReceiptError |
Extracted data fails business-logic validation |
from finamt.exceptions import FinanceAgentError, OCRProcessingError
try:
result = agent.process_receipt("scan.pdf")
except OCRProcessingError as e:
print(e)Each receipt goes through four sequential LLM calls, each with a short focused prompt:
| Agent | Extracts |
|---|---|
| Agent 1 | Receipt number, date, category |
| Agent 2 | Counterparty name, VAT ID, Steuernummer, address |
| Agent 3 | Total amount, VAT percentage, VAT amount |
| Agent 4 | Line items (description, VAT rate, VAT amount, price) |
Results are merged in Python — no additional LLM validation step. Debug output for every agent (prompt, raw response, parsed JSON) is saved to ~/.finamt/debug/<receipt_id>/.
Every receipt is tagged with a category and optional subcategory. Categories map directly to line items in the German ELSTER tax forms (EÜR / UStVA), so the correct totals land in the right fields without manual re-sorting.
Receipt processing
- OCR pipeline (PaddleOCR + Tesseract fallback)
- 4-agent extraction (metadata, counterparty, amounts, line items)
- Deduplication, database storage, batch processing
Tax calculation
- UStVA — VAT pre-return (monthly / quarterly)
- UStE — annual VAT return
- EÜR — income-surplus statement
- KSt 1 — corporate income tax return
- GewSt — trade tax return
- Jahresabschluss — annual accounts (Bilanz + GuV, § 267a HGB)
ELSTER transmission
- UStVA — ELSTER XML builder + Kennzahlen mapper + RSA signing + HTTP submission
- E-Bilanz — XBRL instance document (HGB taxonomy v6, MicroBilG schema)
- E-Bilanz — ERiC ctypes bridge for actual transmission
- EÜR — ELSTER XML builder
- KSt / GewSt — ELSTER XML builder
Validation
- XSD validation of generated XBRL against HGB taxonomy
- ELSTER dry-run / test-server validation before live submission
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-change) - Make your changes
- Run the test suite:
pytest --cov=src --cov-report=term-missing - Submit a pull request
MIT — see LICENSE for details.