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.

  • ReceiptType distinguishes purchase invoices (Eingangsrechnung — input tax you reclaim) from sales invoices (Ausgangsrechnung — output tax you remit).

  • Counterparty replaces the old vendor field and covers both vendors (on purchase invoices) and clients (on sales invoices).

  • ReceiptCategory remains 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: str

Direction of a double-entry posting — 'debit' or 'credit'.

class finamt.models.PostingType(value: str)[source]

Bases: str

Account type for a double-entry posting.

expense — Betriebsausgabe input_vat — Vorsteuer accounts_payable — Verbindlichkeiten Lieferanten revenue — Betriebseinnahme output_vat — Umsatzsteuer accounts_receivable — Forderungen Kunden private_withdrawal — Privatentnahme / geldwerter Vorteil

class finamt.models.Posting(receipt_id: str, posting_type: PostingType, direction: PostingDirection, amount: Decimal, description: str = '')[source]

Bases: object

A single double-entry journal posting generated from a receipt.

For each receipt ReceiptData.generate_postings() returns a balanced list of debits and credits. When private_use_share > 0 the 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

receipt_id: str
posting_type: PostingType
direction: PostingDirection
amount: Decimal
description: str = ''
to_dict() dict[source]
class finamt.models.ReceiptCategory(value: str = 'other')[source]

Bases: str

A 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: str

Whether 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: object

Structured postal address.

All fields are optional because OCR and LLM extraction may not find them. address_supplement captures a secondary address line (e.g. building name, campus, suite) that appears separately from the street and number.

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
to_dict() dict[source]
classmethod from_dict(d: dict) Address[source]
classmethod empty() Address[source]
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: object

The other party on a receipt — a vendor (on purchases) or a client (on sales).

id is a UUID assigned by the database layer. Two counterparties are considered the same entity if their vat_id matches, or failing that, if their name matches case-insensitively.

id: str
name: str | None = None
address: Address
tax_number: str | None = None
vat_id: str | None = None
verified: bool = False
to_dict() dict[source]
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: object

A single line item within a receipt.

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
to_dict() dict[source]
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: object

Structured data extracted from a single receipt or invoice.

The id is derived from the content hash of raw_text — pass raw_text first so the default factory can compute it. Alternatively, set id explicitly (e.g. when loading from DB).

receipt_type controls how VAT is treated for tax purposes: - "purchase" → Vorsteuer (input tax, you reclaim) - "sale" → Umsatzsteuer (output tax, you remit)

raw_text: str = ''
id: str
receipt_type: ReceiptType
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
subcategory: str | None = None
description: str = ''
items: list[ReceiptItem]
vat_splits: list[dict]
validation_warnings: list[str]
private_use_share: Decimal
einfuhr_vat: Decimal | None = None
property vendor: str | None

Backward-compatible alias for counterparty.name.

property net_amount: Decimal | None
property is_purchase: bool
property is_sale: bool
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 only vat*(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).

property business_vat: Decimal | None

Reclaimable / remittable VAT for the business portion only.

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.

to_dict() dict[source]
to_json() str[source]
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: object

Top-level result returned by FinanceAgent.process_receipt().

Always check success before accessing data. When duplicate is True, the receipt was not re-saved — see existing_id for the ID of the original.

success: bool
data: ReceiptData | None = None
error_message: str | None = None
processing_time: float | None = None
duplicate: bool = False
existing_id: str | None = None
to_dict() dict[source]