aeat.application.ledger._evidence_draft module¶
On-host invoice-PDF field extraction into a typed draft.
Given an EvidenceInput already resolved from secure storage (see
_evidence_input),
extract_invoice_fields() runs the in-tree on-host
text-layer extractor
(extract_evidence_text()) and
applies grounded heuristics – the shared Spanish tax-id validator
(validate_spanish_tax_id()), the shared day-first date
parser (parse_date()), and the shared European decimal
separator normaliser (normalize_decimal_separators()) – to
recover a supplier NIF/NIE/CIF, invoice number, invoice date, taxable base, IVA
rate, IVA amount, and grand total.
This is the extraction PRIMITIVE only: it returns an InvoiceDraft the
operator reviews and confirms. It never persists an
Invoice and never guesses a value it cannot ground in
the extracted text – every field it cannot recover is left None rather than
fabricated
(no-silent-under-declaration in spirit: an unconfident field is absent, not
invented).
Everything here runs on-host and in-memory only. The evidence bytes and the
extracted text never touch disk and are never sent to a cloud provider or an
LLM (sensitive-financial-data-secure-storage-only,
2026-06-10-llm-evidence-classification-adr). This module makes no network
call and performs no filesystem write.
A scan-only PDF (no embedded text layer) or an image attachment has nothing for
extract_invoice_fields() to read, so
extract_invoice_draft_from_evidence() falls back to the
on-host LOCAL vision reader (_evidence_draft_vision)
– the same rasterise-then-read-with-Ollama transport
LocalVisionLLMClassifier already
uses for classification, gated by LLM_VISION and
never a cloud call. When on-host vision reading is disabled for the profile, or
the local Ollama runtime is unreachable, the caller gets a typed, instructive
refusal – never a silent empty draft.
extract_invoice_draft_from_evidence() is the CLI-facing
wiring layer: it resolves an already-stored purchase_invoice_evidence record
or a linked attachment_id to its in-memory bytes (through the private
evidence-input resolvers
resolve_purchase_invoice_evidence_input()
and
resolve_attachment_evidence_input()) and
runs extract_invoice_fields() over them, falling back
to the on-host vision reader for scan-only PDFs and images, so
aeat app ledger evidence extract needs only a bucket id plus one of the two
reference ids.
confirm_invoice_draft_from_evidence() is the
non-interactive CONFIRM step that closes the review loop: it re-runs the on-host
extraction, applies any operator-supplied field overrides (extraction is
best-effort – every field may be corrected), and delegates the actual write to
create_catalogue_invoice() – the sole sanctioned
Invoice writer
(composition-service-no-parallel-write-path). A confirm keyed on the same
evidence/attachment reference and the same resolved fields is a guarded no-op
that returns the existing invoice rather than raising or duplicating
(single-subject-mutation-is-idempotent-guarded); a same-reference confirm
whose resolved fields genuinely differ from the already-stored invoice mints a
second, distinct invoice record (a different content-derived
invoice_id) rather than silently
overwriting one filer’s data with another’s.
Confirming also auto-links the source evidence to the resulting invoice:
link_attachment_invoice() appends the invoice’s id
to the backing Attachment’s
linked_invoice_ids, closing the
provenance loop in both directions (the invoice is discoverable from the
evidence, and the evidence is the invoice’s traceable source). The link is
re-asserted on a guarded no-op confirm too, so a re-confirm never regresses a
provenance link that was never wired for older evidence, and the append itself
is idempotent (dedup on the linked-ids tuple).
See also
InvoiceDraftPublic draft record returned before an invoice is persisted.
extract_invoice_fields()Text-layer extraction primitive used before any evidence reference resolution or confirm write.
extract_invoice_draft_from_evidence()CLI-facing resolver that loads stored evidence bytes and chooses the text-layer or on-host vision path.
confirm_invoice_draft_from_evidence()Non-interactive confirm step that re-extracts, applies overrides, and delegates the catalogue write.
_evidence_draft_visionOn-host vision fallback for scan-only PDFs and image attachments.
create_catalogue_invoice()Sole sanctioned writer for the resulting catalogue invoice.
- class InvoiceDraft(**data)[source]¶
Bases:
BaseModelBest-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
Nonerather than guessed. The operator reviews this draft and supplies or corrects fields before anyInvoiceis 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
Nonewhen no valid tax identifier was found.invoice_number – Invoice number recovered from a labelled line, or
None.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), orNone.iva_amount – Labelled IVA cuota amount, or
None.grand_total – Labelled invoice total amount, or
None.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: str | None¶
- invoice_number: str | None¶
- invoice_date: str | None¶
- taxable_base: Decimal | None¶
- iva_rate: Decimal | None¶
- iva_amount: Decimal | None¶
- grand_total: Decimal | None¶
- raw_text_length: int¶
- extract_invoice_fields(evidence)[source]¶
Return a best-effort
InvoiceDraftextracted from evidence.Runs the on-host pdfplumber text-layer extractor over the evidence’s in-memory bytes and applies grounded regex heuristics scoped to Spanish invoice layouts. A field the heuristics cannot ground in the extracted text is left
None; nothing is fabricated.- Parameters:
evidence (
EvidenceInput) – Resolved in-memory evidence bytes (already read from secure storage by the caller).- Return type:
- Returns:
InvoiceDraftcarrying every field the heuristics could ground, withraw_text_lengthrecording how much text the on-host extractor recovered.- Raises:
PurchaseInvoiceEvidenceInputError – When the evidence is not a PDF, or the PDF has no usable text layer (scan-only / XFA) – the caller should fall back to the on-host vision reader in that case.
- extract_invoice_draft_from_evidence(*, bucket_id, evidence_id=None, attachment_id=None, settings=None)[source]¶
Resolve one stored evidence reference to bytes and extract its
InvoiceDraft.The CLI-facing wiring layer over
extract_invoice_fields(): given either apurchase_invoice_evidenceid (looked up throughPurchaseInvoiceEvidenceService) or a linkedattachment_id, reads the evidence’s bytes from secure storage into memory (the private evidence-input resolversresolve_purchase_invoice_evidence_input()andresolve_attachment_evidence_input()) and runs the on-host extractor over them. Exactly one of evidence_id / attachment_id must be supplied.Nothing is written to disk and nothing leaves the host: the resolved bytes and the extracted text stay in process memory for the duration of this call (
sensitive-financial-data-secure-storage-only).- Parameters:
bucket_id (
str) – Active ledger bucket the evidence or attachment belongs to.evidence_id (
str|None) – Apurchase_invoice_evidencerecord id, orNone.attachment_id (
str|None) – A linked attachment id, orNone.settings (
Settings|None) – ResolvedSettings. WhenNone,load_settings()is used so test overrides viaoverride_settings()are honoured.
- Returns:
The best-effort extracted fields, for operator review. Never itself persisted as an
Invoice.- Return type:
- Raises:
PurchaseInvoiceEvidenceInputError – When neither or both of evidence_id / attachment_id are supplied, when the resolved evidence’s media type is unsupported, or when a scan-only PDF / image falls back to the on-host vision reader and that reader is disabled for the profile or the local Ollama runtime is unreachable.
PurchaseInvoiceEvidenceNotFoundError – When evidence_id names no record in bucket_id.
- class InvoiceConfirmationResult(**data)[source]¶
Bases:
BaseModelOutcome of confirming a reviewed
InvoiceDraftinto anInvoice.- Variables:
invoice – The persisted (or already-existing, on a guarded no-op)
Invoice.draft – The re-run on-host extraction the confirmation was based on (before overrides were applied), kept so the operator can see what was actually read from the document versus what they overrode.
created –
Truewhen this call minted a new catalogue row;Falsewhen an invoice with the identical derived identity already existed and this call returned it unchanged (the guarded idempotent-retry no-op).
- Parameters:
invoice (Invoice)
draft (InvoiceDraft)
created (bool)
- invoice: Invoice¶
- draft: InvoiceDraft¶
- created: bool¶
- confirm_invoice_draft_from_evidence(*, bucket_id, kind, counterparty_country='ES', evidence_id=None, attachment_id=None, counterparty_tax_id=None, counterparty_name=None, invoice_number=None, invoice_date=None, taxable_base=None, iva_rate=None, currency='EUR', notes='', settings=None, invoice_repository=None)[source]¶
Re-extract one evidence reference and confirm it into a real
Invoice.Re-runs
extract_invoice_draft_from_evidence()on-host (bytes and text stay in memory only), then layers any operator-supplied override on top of each extracted field – extraction is best-effort, so every field may be corrected before the record is minted. The resulting identity fields are handed tocreate_catalogue_invoice(), the single sanctionedInvoicewriter (composition-service-no-parallel-write-path); this function never writes the catalogue itself.Idempotent-guarded (
single-subject-mutation-is-idempotent-guarded): the persistedinvoice_idis a stable hash of(kind, invoice_number, issued_at, counterparty_tax_id, currency, grand_total)— a confirm carrying identical resolved fields to an already-persisted invoice returns that invoice unchanged (created=False, no new bucket write); a confirm whose resolved fields genuinely differ mints a distinct invoice record rather than overwriting.- Parameters:
bucket_id (
str) – Active ledger bucket the evidence belongs to.kind (
InvoiceKind) – Invoice direction (issuedorreceived) — extraction cannot infer this; the operator must state it.counterparty_country (
str) – ISO 3166-1 alpha-2 counterparty country code. Defaults to"ES"; override for a non-Spanish counterparty.evidence_id (
str|None) – Apurchase_invoice_evidencerecord id, orNone.attachment_id (
str|None) – A linked attachment id, orNone. Exactly one of evidence_id / attachment_id must be supplied.counterparty_tax_id (
str|None) – Override for the extracted supplier tax id.counterparty_name (
str|None) – Override (there is no extraction heuristic for the counterparty’s display name yet, so this is normally required).invoice_number (
str|None) – Override for the extracted invoice number.invoice_date (
date|None) – Override for the extracted invoice date.taxable_base (
Decimal|None) – Override for the extracted taxable base.iva_rate (
Decimal|None) – Override for the extracted IVA rate (Noneresolves to the EXEMPT slot, matchingbuild_catalogue_invoice()).currency (
str) – ISO-4217 currency code. Defaults to"EUR".notes (
str) – Free-text operator notes carried onto the invoice.settings (
Settings|None) – ResolvedSettings;load_settings()whenNone.invoice_repository (
InvoiceCatalogueRepositoryProtocol|None) – Optional injectedInvoiceCatalogueRepositoryProtocol(testing seam).
- Returns:
The persisted (or pre-existing) invoice, the re-run draft it was checked against, and whether this call minted a new record.
- Return type:
- Raises:
PurchaseInvoiceEvidenceInputError – When neither or both of evidence_id / attachment_id are supplied, when the resolved evidence has no usable text layer, or when a required field is
Noneafter overrides (extraction found nothing and the operator supplied no override).PurchaseInvoiceEvidenceNotFoundError – When evidence_id names no record in bucket_id.
InvoiceValidationError – When the resolved fields fail invoice-model validation (e.g. an invalid counterparty tax id or IVA rate).