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

InvoiceDraft

Public 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_vision

On-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: BaseModel

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.

  • 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), or None.

  • 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)

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 InvoiceDraft extracted 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:

InvoiceDraft

Returns:

InvoiceDraft carrying every field the heuristics could ground, with raw_text_length recording 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 a purchase_invoice_evidence id (looked up through PurchaseInvoiceEvidenceService) or a linked attachment_id, reads the evidence’s bytes from secure storage into memory (the private evidence-input resolvers resolve_purchase_invoice_evidence_input() and resolve_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) – A purchase_invoice_evidence record id, or None.

  • attachment_id (str | None) – A linked attachment id, or None.

  • settings (Settings | None) – Resolved Settings. When None, load_settings() is used so test overrides via override_settings() are honoured.

Returns:

The best-effort extracted fields, for operator review. Never itself persisted as an Invoice.

Return type:

InvoiceDraft

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: BaseModel

Outcome of confirming a reviewed InvoiceDraft into an Invoice.

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.

  • createdTrue when this call minted a new catalogue row; False when 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
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 to create_catalogue_invoice(), the single sanctioned Invoice writer (composition-service-no-parallel-write-path); this function never writes the catalogue itself.

Idempotent-guarded (single-subject-mutation-is-idempotent-guarded): the persisted invoice_id is 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 (issued or received) — 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) – A purchase_invoice_evidence record id, or None.

  • attachment_id (str | None) – A linked attachment id, or None. 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 (None resolves to the EXEMPT slot, matching build_catalogue_invoice()).

  • currency (str) – ISO-4217 currency code. Defaults to "EUR".

  • notes (str) – Free-text operator notes carried onto the invoice.

  • settings (Settings | None) – Resolved Settings; load_settings() when None.

  • invoice_repository (InvoiceCatalogueRepositoryProtocol | None) – Optional injected InvoiceCatalogueRepositoryProtocol (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:

InvoiceConfirmationResult

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 None after 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).