Models
Pydantic models that represent extracted receipt data and enumeration types used throughout the library.
finamt.models
Data models for extracted receipt information.
Key design decisions
Receipt ID is a SHA-256 hash of the normalised raw OCR text. Identical content → identical ID → automatic duplicate detection.
ReceiptTypedistinguishes purchase invoices (Eingangsrechnung — input tax you reclaim) from sales invoices (Ausgangsrechnung — output tax you remit).Counterpartyreplaces the oldvendorfield and covers both vendors (on purchase invoices) and clients (on sales invoices).ReceiptCategoryremains a thin string wrapper keyed to the same list the LLM is prompted with — single source of truth.
- class finamt.models.PostingDirection(value: str)[source]
Bases:
strDirection of a double-entry posting —
'debit'or'credit'.
- class finamt.models.PostingType(value: str)[source]
Bases:
strAccount type for a double-entry posting.
expense— Betriebsausgabeinput_vat— Vorsteueraccounts_payable— Verbindlichkeiten Lieferantenrevenue— Betriebseinnahmeoutput_vat— Umsatzsteueraccounts_receivable— Forderungen Kundenprivate_withdrawal— Privatentnahme / geldwerter Vorteil
- class finamt.models.Posting(receipt_id: str, posting_type: PostingType, direction: PostingDirection, amount: Decimal, description: str = '')[source]
Bases:
objectA single double-entry journal posting generated from a receipt.
For each receipt
ReceiptData.generate_postings()returns a balanced list of debits and credits. Whenprivate_use_share > 0the list includes correction postings that isolate the non-deductible private portion so that:VAT is only claimed on the business portion.
The full gross amount is still preserved as accounts payable.
A private withdrawal posting captures the owner’s benefit in kind.
An EÜR can be derived by aggregating only the net expense/revenue postings (after corrections).
Fields
receipt_id : back-reference to the parent receipt posting_type : account type (expense, input_vat, …) direction : ‘debit’ or ‘credit’ amount : always positive description : human-readable general-ledger label
- posting_type: PostingType
- direction: PostingDirection
- class finamt.models.ReceiptCategory(value: str = 'other')[source]
Bases:
strA validated receipt category string.
Unknown values are silently normalised to
"other"so that LLM hallucinations never break model construction.- VALID: frozenset = frozenset({'capital_movement', 'car', 'donations', 'education', 'equipment', 'financial', 'insurance', 'licensing', 'marketing', 'material', 'office', 'other', 'products', 'public_fees', 'services', 'software', 'tax_settlement', 'telecommunication', 'travel', 'utilities'})
- classmethod other() ReceiptCategory[source]
- class finamt.models.ReceiptType(value: str = 'purchase')[source]
Bases:
strWhether this is a purchase or a sales invoice.
"purchase"— Eingangsrechnung. You paid a vendor.VAT = Vorsteuer (input tax) → you reclaim it.
"sale"— Ausgangsrechnung. A client paid you.VAT = Umsatzsteuer (output tax) → you remit it to the state.
- classmethod purchase() ReceiptType[source]
- classmethod sale() ReceiptType[source]
- class finamt.models.Address(street_and_number: str | None = None, address_supplement: str | None = None, postcode: str | None = None, city: str | None = None, state: str | None = None, country: str | None = None)[source]
Bases:
objectStructured postal address.
All fields are optional because OCR and LLM extraction may not find them.
address_supplementcaptures a secondary address line (e.g. building name, campus, suite) that appears separately from the street and number.
- class finamt.models.Counterparty(id: str = <factory>, name: str | None = None, address: Address = <factory>, tax_number: str | None = None, vat_id: str | None = None, verified: bool = False)[source]
Bases:
objectThe other party on a receipt — a vendor (on purchases) or a client (on sales).
idis a UUID assigned by the database layer. Two counterparties are considered the same entity if theirvat_idmatches, or failing that, if theirnamematches case-insensitively.
- class finamt.models.ReceiptItem(description: str = '', position: int | None = None, quantity: Decimal | None = None, unit_price: Decimal | None = None, total_price: Decimal | None = None, vat_rate: Decimal | None = None, vat_amount: Decimal | None = None, category: ReceiptCategory = <factory>)[source]
Bases:
objectA single line item within a receipt.
- category: ReceiptCategory
- class finamt.models.ReceiptData(raw_text: str = '', receipt_type: ReceiptType = <factory>, counterparty: Counterparty | None = None, receipt_number: str | None = None, receipt_date: datetime | None = None, total_amount: Decimal | None = None, vat_percentage: Decimal | None = None, vat_amount: Decimal | None = None, currency: str = 'EUR', category: ReceiptCategory = <factory>, subcategory: str | None = None, description: str = '', items: list[ReceiptItem] = <factory>, vat_splits: list[dict] = <factory>, validation_warnings: list[str] = <factory>, private_use_share: Decimal = <factory>, einfuhr_vat: Decimal | None = None)[source]
Bases:
objectStructured data extracted from a single receipt or invoice.
The
idis derived from the content hash ofraw_text— passraw_textfirst so the default factory can compute it. Alternatively, setidexplicitly (e.g. when loading from DB).receipt_typecontrols how VAT is treated for tax purposes: -"purchase"→ Vorsteuer (input tax, you reclaim) -"sale"→ Umsatzsteuer (output tax, you remit)- receipt_type: ReceiptType
- counterparty: Counterparty | None = None
- category: ReceiptCategory
- items: list[ReceiptItem]
- generate_postings() list[Posting][source]
Generate a balanced list of double-entry postings for this receipt.
Purchase (
private_use_share = 0):DEBIT expense net — Betriebsausgabe (gesamt) DEBIT input_vat vat — Vorsteuer (gesamt) CREDIT accounts_payable gross — Verbindlichkeit Lieferant
Purchase (
private_use_share = p > 0) — additional corrections:CREDIT expense net*p — Privatanteil Korrektur (Netto) CREDIT input_vat vat*p — Privatanteil Vorsteuerkorrektur DEBIT private_withdrawal gross*p — Privatentnahme / geldwerter Vorteil
Net effect: only
net*(1-p)flows through the expense account and onlyvat*(1-p)remains as reclaimable input VAT.Sale:
DEBIT accounts_receivable gross — Forderung Kunde CREDIT revenue net — Betriebseinnahme (netto) CREDIT output_vat vat — Umsatzsteuer
Returns an empty list when amounts are not yet available.
- property business_net: Decimal | None
Net amount attributable to the business (after private-use deduction).
- validate() bool[source]
Collect business-rule warnings into
self.validation_warnings.Returns True when there are no warnings (clean receipt). Returns False when at least one rule is violated.
Regardless of the return value, receipts are always saved — the caller must not block on a False return. Warnings are stored in the DB and shown to the user, who decides to correct or delete.
- class finamt.models.ExtractionResult(success: bool, data: ReceiptData | None = None, error_message: str | None = None, processing_time: float | None = None, duplicate: bool = False, existing_id: str | None = None)[source]
Bases:
objectTop-level result returned by
FinanceAgent.process_receipt().Always check
successbefore accessingdata. Whenduplicateis True, the receipt was not re-saved — seeexisting_idfor the ID of the original.- data: ReceiptData | None = None