cadrumo.domain.invoices.models module

Strict immutable models for the invoice catalogue.

Defines the pydantic v2 records that back cadrumo.domain.invoices: InvoiceLine, Invoice, and the keyed InvoiceCatalogue. Every model is strict, frozen, and forbids extra fields; identity-bearing fields on Invoice are canonicalised in a model_validator and the stable Invoice.invoice_id is derived via derive_invoice_id(). Counterparty identity validation is delegated to cadrumo.domain.invoices.validators.

derive_invoice_id(*, kind, invoice_number, issued_at, counterparty_tax_id, currency, grand_total)[source]

Return the stable invoice hash for one invoice record.

The digest is computed over a canonical JSON payload so that two invoices with equal logical identity produce identical IDs regardless of whitespace or numeric formatting.

Parameters:
  • kind (InvoiceKind) – Invoice direction (issued / received).

  • invoice_number (str) – AEAT-significant invoice number as printed on the document.

  • issued_at (date) – ISO calendar date printed on the invoice.

  • counterparty_tax_id (str | None) – Counterparty NIF / NIE / CIF / IVA number already validated and uppercased, or None for a factura simplificada issued outside the RD 1619/2012 art. 6.1.d cases that make the counterparty’s tax id mandatory.

  • currency (str) – ISO-4217 currency code already uppercased.

  • grand_total (Decimal) – Invoice grand total.

Return type:

str

Returns:

A lowercase SHA-256 digest that uniquely identifies the invoice.

class InvoiceLine(**data)[source]

Bases: BaseModel

Immutable line item on an invoice.

Parameters:

data (Any)

description: str
quantity: Decimal
unit_price: Decimal
subtotal: Decimal
iva_rate: IvaRate
iva_amount: Decimal
spending_category_id: str | None
oss_rate_kind: IvaRateKind | None
class Invoice(**data)[source]

Bases: BaseModel

Strict frozen record for one issued or received invoice.

Parameters:

data (Any)

invoice_id: InvoiceId
bucket_id: BucketId | None
kind: InvoiceKind
invoice_class: InvoiceClass
series: str | None
invoice_number: str
issued_at: date
operation_date: date | None
operation_date_role: InvoiceOperationDateRole | None
counterparty_name: str
counterparty_tax_id: TaxIdIdentityToken | None
counterparty_country: CountryCodeAlpha2
counterparty_identification_state: EUMemberState | None
issuer_address: str | None
recipient_address: str | None
exemption_reference: str | None
legal_mentions: tuple[InvoiceLegalMention, ...]
base_total: Decimal
iva_total: Decimal
grand_total: Decimal
currency: IsoCurrencyCode
lines: tuple[InvoiceLine, ...]
payment_status: PaymentStatus
linked_transaction_ids: tuple[str, ...]
notes: str
iva_category: IvaCategory | None
operation_type: IntracomOperationType | None
travel_agency_mediation: TravelAgencyMediationType | None
collected_on_behalf_of_tax_id: TaxIdIdentityToken | None
collected_on_behalf_of_name: str | None
is_subvencion_ayuda: bool | None
outside_economic_activity: bool | None
oss_ioss_regime: OssIossRegime | None
oss_transaction_kind: TransactionKind | None
retention_rate: Decimal | None
retention_amount: Decimal | None
recargo_amount: Decimal | None
suplido_amount: Decimal | None
rectifies_invoice_number: str | None
payment_id: str | None
fx_rate: Decimal | None
fx_rate_date: date | None
fx_rate_source: str | None
created_at: UtcInstant | None
updated_at: UtcInstant | None
property base_total_eur: Decimal | None

base_total in euro, or None when the invoice is unconverted.

property iva_total_eur: Decimal | None

iva_total in euro, or None when the invoice is unconverted.

property grand_total_eur: Decimal | None

grand_total in euro, or None when the invoice is unconverted.

property retention_amount_eur: Decimal | None

The declared retención in euro, or None when there is none to convert.

Returns None both when no retención was declared and when the invoice is unconverted, so a caller that needs to tell those apart must read retention_amount alongside it – the same shape the three total accessors already have.

property recargo_amount_eur: Decimal | None

The recargo de equivalencia in euro, or None when there is none to convert.

Same shape as retention_amount_eur: None covers both “no recargo was declared” and “the invoice is unconverted”, so a caller needing to tell those apart reads recargo_amount alongside it.

property suplido_amount_eur: Decimal | None

The suplido (LIVA art. 78.Tres.3.º) in euro, or None when there is none to convert.

Same shape as recargo_amount_eur and retention_amount_eur: None covers both “no suplido was declared” and “the invoice is unconverted”.

line_amount_eur(amount)[source]

Convert a LINE-level native-currency amount to euro, or None if unconverted.

InvoiceLine carries no currency of its own – every line on an invoice is denominated in that invoice’s own currency (a single document states one currency for all its lines), so a line amount (line.subtotal, line.iva_amount) converts through the SAME rate resolution as the invoice-level totals (base_total_eur and siblings), not a separate per-line mechanism. Delegating to _in_eur() keeps that arithmetic consistent: summing line_amount_eur(line.subtotal) over every line equals base_total_eur exactly, the same way summing line.subtotal natively already equals base_total.

Parameters:

amount (Decimal) – A native-currency line amount, e.g. line.subtotal or line.iva_amount for one of this invoice’s own lines.

Return type:

Decimal | None

Returns:

The euro-equivalent amount, or None when this invoice is foreign-currency with no resolved fx_rate.

property counterparty_eu_member_state: EUMemberState | None

Return the substrate-typed EUMemberState for the counterparty, or None for non-EU.

counterparty_country carries the raw uppercase ISO-3166-1 alpha-2 code (validated at construction time). This typed accessor lets downstream consumers (Modelo 369 OSS bindings, intra-community classification, OSS classifier dispatch) work with a registry-projected token without a per-call lowercase / membership check. It is anchored to the dated EU member-state fact 0131.

class InvoiceCatalogue(**data)[source]

Bases: BaseModel

Immutable invoice catalogue keyed by invoice_id.

Parameters:

data (Any)

invoices: Mapping[str, Invoice]
get(invoice_id)[source]

Fetch one invoice by ID if present.

Parameters:

invoice_id (str) – Stable invoice identifier.

Return type:

Invoice | None

Returns:

The matching Invoice, or None when absent.

values()[source]

Iterate over catalogue Invoice records.

Return type:

Iterator[Invoice]