cadrumo.application.ledger.invoice_draft_records module

The invoice-draft record family, apart from the service that fills it.

These records are read by the on-host inference package, which holds no repository handle of its own. Their previous home, evidence_draft, also wires InvoiceCatalogueRepository and AttachmentStore, so importing a draft record pulled the whole persistence subtree into every consumer that only wanted the shape – including the outbound LLM adapter, whose distance from persistence is what the operator’s in-memory inference exemption rests on.

This module therefore imports neither persistence nor the inference package, and must keep it that way. Every dependency below was checked to reach neither before the split.

class InvoiceDraftLine(**data)[source]

Bases: BaseModel

One line item recovered from a structured invoice document.

Only a structured reader can populate this: a text or vision reader recovers printed totals, not the document’s own line decomposition. The rate is carried as a bare percentage Decimal (21, not 0.21) because the draft is pre-confirm operator-facing data; mapping it onto the closed IvaRate slot enum happens at the parse boundary, which refuses loudly rather than rounding to the nearest slot.

Variables:
  • description – Line description as printed, or None.

  • quantity – Billed quantity, or None when the document omits it.

  • unit_price – Price per unit before tax, or None.

  • taxable_base – Line taxable base before IVA.

  • iva_rate – Line IVA percentage as a whole-number Decimal.

  • iva_amount – Line IVA cuota, or None when the document states only the rate and lets the total carry the cuota.

  • recargo_rate – Recargo de equivalencia percentage, or None.

  • recargo_amount – Recargo de equivalencia cuota, or None.

Parameters:

data (Any)

description: str | None
quantity: Decimal | None
unit_price: Decimal | None
taxable_base: Decimal | None
iva_rate: Decimal | None
iva_amount: Decimal | None
recargo_rate: Decimal | None
recargo_amount: Decimal | None
class InvoiceDraftRateBreakdown(**data)[source]

Bases: BaseModel

Per-rate tax subtotal as the document itself declares it.

This is the field that makes the multi-rate silent collapse detectable. A draft carrying only the flat taxable_base / iva_rate / iva_amount triple structurally cannot represent an invoice charging two rates: the two bases sum into one figure and one of the rates is simply lost, producing an invoice whose printed total no longer reconciles with its declared cuota.

Variables:
  • iva_rate – The rate this subtotal is charged at, as a whole-number percentage Decimal.

  • taxable_base – Base charged at this rate.

  • iva_amount – Cuota charged at this rate.

  • recargo_rate – Recargo de equivalencia percentage for this rate.

  • recargo_amount – Recargo de equivalencia cuota for this rate.

Parameters:

data (Any)

iva_rate: Decimal | None
taxable_base: Decimal | None
iva_amount: Decimal | None
recargo_rate: Decimal | None
recargo_amount: Decimal | None
class FieldAmbiguityCandidate(**data)[source]

Bases: BaseModel

One competing reading a grounding pass could not decide between.

Recorded rather than resolved. An ambiguity collapsed by taking the first match is indistinguishable downstream from a fact, so the candidates travel to the operator, who is the only party with the document in front of them.

Variables:
  • value – The candidate reading, as a string. Deliberately not the field’s own type: a candidate exists precisely because the value could not be established, and coercing an undecided reading into a Decimal or a date would assert the parse that was in question.

  • anchor – The verbatim printed form this candidate was read from, or None when the candidate is a normalisation of another reading rather than a distinct occurrence.

  • note – Why this candidate competed, in operator-facing terms.

Parameters:

data (Any)

value: str
anchor: str | None
note: str
class FieldProvenance(**data)[source]

Bases: BaseModel

The provenance envelope for exactly one field of a draft.

Per FIELD, not per document. A draft is routinely assembled from several readers – a structured record for the parties, an arithmetic derivation for a cuota, an operator correction at confirm – so a single document-level stamp would claim one origin for values that did not share one. That is the laundering this record exists to prevent: an exactly-read value must stay distinguishable from a model-read one all the way to the operator’s screen.

Carries no numeric confidence, deliberately and permanently. See FieldOrigin and FieldGroundingOutcome for the two axes that are facts: how the value was obtained, and what checking it survived.

Variables:
  • field – Name of the InvoiceDraft field this envelope describes. Validated against the draft’s own fields rather than a hand-listed enum, so a renamed or removed draft field invalidates its stale envelopes instead of leaving them pointing at nothing.

  • origin – How the value was obtained.

  • grounding – What verification the value passed, or failed.

  • anchor – The verbatim printed form the value was read from, exactly as it appears in the source – "1.234,56 €", not 1234.56. This is the whole anti-fabrication mechanism: a value nobody can point at in the document has None here and an UNANCHORED outcome, and the operator sees both.

  • candidates – Competing readings, when the grounding outcome is AMBIGUOUS. Empty otherwise.

  • refused_anchor – The printed form the reader DID offer, kept here when the check looked for it and the document does not carry it. The anchor field is cleared on that outcome, deliberately – a form the document does not print is not evidence, and every consumer reading anchor reads it as evidence. Clearing it alone, though, leaves a refused claim indistinguishable from an absent one, and the two are different operator situations: a reader that offered nothing is a reader limitation, while a reader that offered something the document does not carry is a possible misread or the wrong document entirely. So the refused form is preserved HERE, where nothing can mistake it for a corroborated one, and the operator surface can say which of the two happened. None whenever no check ran or the anchor was found.

  • anchor_self_reported – True when the anchor was asserted by the same reader that produced the value, with nothing independent to check it against – the vision lane, which reads image to fields in one call and has no transcription. Such an anchor is recorded because it is still useful to an operator, but it can never carry an ANCHORED outcome; see the validator below.

  • role_evidence – For an identity field, the printed context the reader copied that assigns this value to its party role – a heading, a label, the line the identifier sits under. None where the document showed nothing that does, and on every field that names no party. This is a SECOND anchor rather than a note: it is a printed excerpt, so it is checkable against the transcription exactly the way anchor is, and it is only trusted once that check has run. The reader’s own account of what it did (“I assigned this to supplier_tax_id”) is not role evidence and must never be recorded here – always-truthy text in this slot permanently satisfies the guard that exists to refuse an unevidenced identity, which is a measured defect rather than a hypothetical one.

  • attribution_unverified – True when nothing checked WHICH PARTY this value belongs to. Party attribution is its own axis, distinct from whether the value was read correctly: a postal code can be copied perfectly off the page and still be filed under the wrong party, and every anchor check in this record would pass. Role evidence answers the attribution question for the identity fields, and the document’s own record answers it for a structured read, where the element path names the party. For a model-read address value there is no such answer today – the reader’s assignment is final and unchecked – so the value is stamped here and the operator is told. Per FIELD for the same reason everything else on this envelope is: one party’s postal code may be attributed while their country is not, and a draft-level or party-level flag could not say so. When deterministic co-location lands, an attributed value simply stops carrying the stamp; nothing here changes shape.

  • note – Operator-facing explanation, e.g. which identity contradicted the value.

Parameters:

data (Any)

field: str
origin: FieldOrigin
grounding: FieldGroundingOutcome
anchor: str | None
refused_anchor: str | None
candidates: tuple[FieldAmbiguityCandidate, ...]
anchor_self_reported: bool
derived_from: tuple[str, ...]
role_evidence: str | None
attribution_unverified: bool
note: str
class DraftDiscrepancyFinding(**data)[source]

Bases: BaseModel

One deterministic check the read document failed.

Distinct from PrintedTotalDiscrepancy, which compares the document against the invoice that was actually WRITTEN and therefore only exists at confirm. This record is a finding about the document alone, available the moment it is read, so the operator meets it during review rather than after a record has been minted from it.

Variables:
  • kind – Which identity failed.

  • field – The draft field the finding is about, or None when the finding is about a relationship between several.

  • detail – Operator-facing explanation naming the figures involved.

  • expected – What the identity required, when the check is arithmetic.

  • observed – What the document stated instead.

Parameters:

data (Any)

kind: DraftDiscrepancyKind
field: str | None
detail: str
expected: Decimal | None
observed: Decimal | None
facturae_invoice_class_findings(*, declared, rectifies_invoice_number)[source]

Report Facturae class gaps and contradictions without choosing a side.

Return type:

tuple[DraftDiscrepancyFinding, ...]

Parameters:
class InvoiceDraftIdentityDocumentFields(**data)[source]

Bases: BaseModel

Identity and printed-document fields shared by draft projections.

Parameters:
  • supplier_tax_id (TaxIdIdentityToken | None)

  • supplier_name (str | None)

  • customer_tax_id (TaxIdIdentityToken | None)

  • customer_name (str | None)

  • supplier_postal_code (str | None)

  • customer_postal_code (str | None)

  • supplier_country (str | None)

  • customer_country (str | None)

  • supplier_country_code (str | None)

  • customer_country_code (str | None)

  • supplier_stated_country_code (str | None)

  • customer_stated_country_code (str | None)

  • invoice_number (str | None)

  • invoice_series (str | None)

  • rectifies_invoice_number (str | None)

supplier_tax_id: TaxIdIdentityToken | None
supplier_name: str | None
customer_tax_id: TaxIdIdentityToken | None
customer_name: str | None
supplier_postal_code: str | None
customer_postal_code: str | None
supplier_country: str | None
customer_country: str | None
supplier_country_code: str | None
customer_country_code: str | None
supplier_stated_country_code: str | None
customer_stated_country_code: str | None
invoice_number: str | None
invoice_series: str | None
rectifies_invoice_number: str | None
class InvoiceDraft(**data)[source]

Bases: InvoiceDraftIdentityDocumentFields

Best-effort invoice fields extracted from an on-host PDF text layer.

Every field is optional: a field the extractor cannot ground in the document’s text is left None rather than guessed. The operator reviews this draft and supplies or corrects fields before any Invoice is minted from it – this model is never itself persisted as a filing-grade record.

Variables:
  • supplier_tax_id – Canonical Spanish NIF / NIE / CIF recovered from the text, or None when no valid tax identifier was found.

  • supplier_name – The issuing party’s stated name, or None.

  • customer_tax_id – The receiving party’s tax identifier, or None. Populated only by a structured reader, which is the only one that can tell the two parties apart; a text or vision reader recovers a single identifier and cannot say whose it is.

  • customer_name – The receiving party’s stated name, or None.

  • supplier_postal_code – The postal code printed in the issuing party’s address, copied verbatim, or None. Transcriptive like every other copied field: it carries the printed code and never the territory read off it. That reading belongs to territorial_scope_for_spanish_postal_code(), which is the deterministic evidence separating the three Spanish IVA territories – the first two digits of a Spanish code are the province – and which refuses an unreadable code rather than defaulting it to the peninsula.

  • customer_postal_code – The same for the party billed by the invoice. Carried separately because establishment is asked of each party independently: an issuer in Las Palmas invoicing a customer in Madrid crosses a territorial boundary that one shared code could not express.

  • supplier_country – The country NAME printed in the issuing party’s address, copied verbatim in whatever language the document set it, or None. Never an alpha-2 code: a country prints as “Alemania”, “Deutschland” or “Allemagne”, so recording a code would mean the reading stage translated, and translation is inference. The match against the bounded vocabulary belongs to country_code_for_printed_country_name(), which is a deterministic lookup rather than a judgement.

  • customer_country – The same for the party billed by the invoice, carried separately for the reason the postal codes are: an issuer in Las Palmas billing a customer in Berlin cannot be expressed by one shared field.

  • supplier_country_code –

    The country code the issuing party’s address states, as an ISO 3166-1 alpha-2 code, or None. Populated only by the structured reader: a machine-readable record states the country as a CODE, where a printed document states it as a name and asking a reader for the code would be asking it to translate. Carried beside the postal code because the two answer different halves of one question – the country says which State, the postal code separates the three Spanish IVA territories inside it.

    Alpha-2 even where the document stated alpha-3. Facturae states ESP and UBL states ES; both arrive here in the single form every country surface downstream is keyed by, resolved through the registry correspondence in country_code_for_stated_country_code(). Normalising at the boundary rather than downstream is what keeps a Facturae document from failing an alpha-2 shape check in silence, with its country element present, read, and establishing nothing.

    The translated form is deliberately NOT anchorable in a Facturae record, and that falls out rather than being asserted: the anchor check looks for the value as a whole token in the document’s own text, and ES is not a token in a record that states ESP. So a stated alpha-2 arrives ANCHORED and a translated one UNANCHORED, which is the honest distinction between what a document said and what was derived from it.

  • customer_country_code – The same for the party billed by the invoice, carried separately for the reason the postal codes are: a supplier in Barcelona invoicing a customer in Lisbon states two countries.

  • supplier_stated_country_code –

    The country token the issuing party’s address element carries, EXACTLY as the record states it – ESP from Facturae, ES from UBL, THA from a Thai supplier’s Facturae invoice – or None where the record’s country element was absent or empty.

    This is the field that keeps “stated something we cannot place” distinguishable from “stated nothing”. The resolved code beside it is contracted alpha-2 and resolves only through the bundled vocabulary, so a token that vocabulary does not carry leaves it None – byte-identical to a document with no country element at all. That collapse reached the operator: no value, no provenance envelope, and the country advisory reading an empty field and staying silent. A genuine Thai export therefore arrived carrying no country and nothing said so.

    Carried verbatim rather than normalised, and never coerced towards the alpha-2 contract: THA is not an alpha-2 code and putting it in a field typed as one would trade a silent absence for a silent lie. Consumers that need a country ask the resolved field; consumers that need to know what the DOCUMENT said ask this one.

  • customer_stated_country_code – The same for the party billed by the invoice, carried separately for the reason every other party field is: each side states its own country and either may be the one the vocabulary cannot place.

  • invoice_number – Invoice number recovered from a labelled line, or None.

  • invoice_series – The series half of the invoice’s identity, stated separately by Facturae as InvoiceSeriesCode, or None. Kept beside the number rather than concatenated into it: composing the printed reference from the two is always possible, while splitting a composed string back into them is not.

  • invoice_date – Day-first invoice date recovered from the text, or None.

  • taxable_base – Labelled “base imponible” amount, or None.

  • iva_rate – IVA percentage recovered from a “IVA NN%” label, expressed as a whole-number Decimal (e.g. 21), or None.

  • iva_amount – Labelled IVA cuota amount, or None.

  • grand_total – Labelled invoice total amount, or None.

  • regime_legend – The statutory mention the document prints to state that a special regime applies (RD 1619/2012 art. 6.1), copied verbatim, or None when the document prints none. Transcriptive and anchorable like every other copied field: it carries what the paper says, never a category derived from it. The derivation belongs to the deterministic classifier downstream, because an IvaCategory token is printed on no invoice and a reading stage asked for one would have to infer.

  • currency – ISO-4217 code for the currency the amounts are printed in, or None when the document shows no currency marker. Left None rather than defaulted to euro: a foreign-currency invoice silently read as euro would carry its face value into a filing unconverted, so an absent marker must stay absent and be resolved by the operator.

  • retencion_rate – IRPF retención percentage as a whole-number Decimal, or None. Carried beside the amount rather than derived from it: a document may print either, and deriving the missing half would manufacture a figure the document does not state.

  • retencion_amount – IRPF withheld at source. Subtracted from the total to reach the cash actually paid, so a draft that drops it reconciles against the wrong figure.

  • suplidos_amount – Sums advanced on the customer’s behalf. Outside the base imponible by law, so folding them into the base over-declares IVA on money that was never the issuer’s revenue.

  • suggested_kind – Which side of the invoice the filer is on, as the reading path SUGGESTS it. Never the decision: the draft is deliberately pre-direction data and the direction is decided by the operator at confirm, where --kind selects the counterparty side. A suggestion the operator does not act on has no effect.

  • transcription_sha256 – Content address of the stage-one transcription this draft was read from, tying the draft to the exact artefact that produced it. The address, never the text: a transcription holds the document’s readable contents and has exactly one sanctioned durable route (DocumentTranscription), so embedding it here would open a second one.

  • provenance – One FieldProvenance envelope per field the reading path established. Absent for a field means no envelope was recorded, which is itself reviewable – it never means the value was exact.

  • discrepancies – Deterministic checks the document failed, available at read time rather than at confirm.

  • raw_text_length – Length of the on-host extracted text, kept as an honest signal of how much source material the heuristics had to work with (zero means the PDF carried no usable text layer for this evidence and the operator should route to the on-host vision reader instead).

Parameters:
  • supplier_tax_id (TaxIdIdentityToken | None)

  • supplier_name (str | None)

  • customer_tax_id (TaxIdIdentityToken | None)

  • customer_name (str | None)

  • supplier_postal_code (str | None)

  • customer_postal_code (str | None)

  • supplier_country (str | None)

  • customer_country (str | None)

  • supplier_country_code (str | None)

  • customer_country_code (str | None)

  • supplier_stated_country_code (str | None)

  • customer_stated_country_code (str | None)

  • invoice_number (str | None)

  • invoice_series (str | None)

  • rectifies_invoice_number (str | None)

proposed_supply_nature: SupplyNature | None

A model’s PROPOSAL about goods-or-services, for a person to accept or discard.

Carried on the draft rather than inside the extracted fields because it is a JUDGEMENT and not a transcription: it has no printed form to anchor to, and the anchor check the extraction contract rests on would have nothing to point at. Folding it in would put an unanchorable value inside the model whose whole guarantee is that values are copied.

None is the ordinary state. It is populated only when the operator asked for a proposal, and never reaches the classifier by itself – the value that does is the one they state at confirm, which is why the classifier’s inputs stay facts.

invoice_date: str | None
taxable_base: Decimal | None
iva_rate: Decimal | None
iva_amount: Decimal | None
grand_total: Decimal | None
currency: str | None
regime_legend: str | None
recargo_amount: Decimal | None
retencion_rate: Decimal | None
retencion_amount: Decimal | None
suplidos_amount: Decimal | None
lines: tuple[InvoiceDraftLine, ...]
iva_breakdown: tuple[InvoiceDraftRateBreakdown, ...]
iva_category: str | None
suggested_kind: InvoiceKind | None
transcription_sha256: ContentDigest | None
provenance: tuple[FieldProvenance, ...]
discrepancies: tuple[DraftDiscrepancyFinding, ...]
raw_text_length: int
property facturae_invoice_class: StructuredInvoiceClassification | None

Return the structured reader’s document-class fact, when present.

set_facturae_invoice_class(value)[source]

Attach the structured reader’s document-class fact to this draft.

Return type:

None

Parameters:

value (StructuredInvoiceClassification | None)