cadrumo.application.ledger.invoice_draft_extraction module

On-host routing of one stored evidence reference to a typed invoice draft.

Holds the reading layer that decides which reader a given document is handed to, and the three readers’ wiring. It never persists an Invoice and never guesses a value no reader could ground in the document: every field a reader could not recover is left None rather than fabricated (no-silent-under-declaration in spirit: an unconfident field is absent, not invented).

Three reading paths, chosen on the document’s own shape rather than on its stored MIME type:

  • A structured e-invoice (Facturae, CII, UBL) is read exactly through the application-owned StructuredInvoiceReader. It reaches no model at all, so prompt injection is categorically impossible for that document rather than merely mitigated, and it is the only path that can recover the document’s own line decomposition and per-rate breakdown.

  • A text-native PDF is transcribed by transcribe_text_layer() and read first by the fixed label rules of read_invoice_fields_by_labels(). When those rules read every required field no model runs; otherwise the fields they could not read come from extract_invoice_fields_from_text(), and an unreachable model leaves the rule reading standing. Either way the draft is grounded against that same transcription by ground_draft_against_transcription(). The transcription is produced by a DIFFERENT reader than the one that proposes values, which is what makes the anchor check an external check rather than a model confirming itself.

  • A scan-only PDF or image has nothing to transcribe, so it 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.

The escalation is one-directional and the asymmetry is deliberate. A document with no text layer genuinely needs a reader that works on pixels, so it escalates. A readable document whose semantic reader is absent or unreachable is an ENVIRONMENT failure, so it REFUSES in the operator’s face rather than silently running a heavier engine they did not ask for – see _refuse_a_text_read_with_no_reader(). A profile that has opted out of vision reading likewise gets a typed, instructive refusal, never a silent empty draft.

Everything here runs on-host and in-memory only. The evidence bytes, the transcription and the draft never touch disk and are never sent to a cloud provider. This module performs no filesystem write.

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 routes them to one of the three readers above, so aeat app ledger evidence extract needs only a bucket id plus one of the two reference ids.

Confirming a read draft into a catalogue record is a separate responsibility and lives in invoice_confirmation; the draft’s own direction-resolved projections live in evidence_draft.

See also

InvoiceDraft

Public draft record returned before an invoice is persisted.

transcribe_text_layer()

Acquisition-stage primitive that turns a text-native PDF into the reading-order transcription the semantic reader is handed.

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.

extract_invoice_draft_from_evidence(*, bucket_id, evidence_id=None, attachment_id=None, settings=None, off_host_provider=None, consent_token=None, ports, operation, legends)[source]

Resolve one stored evidence reference to bytes and extract its InvoiceDraft.

The CLI-facing wiring layer over the three readers: 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 routes them to the reader the document’s shape selects. 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.

  • off_host_provider (LLMProvider | None) – Provider to route the MODEL-BEARING stages at, or None for the on-host default. Widening this signature does not widen the boundary: the refusal lives below this function, at the client’s single dispatch point, so a caller naming a cloud provider without a token is refused there rather than trusted here.

  • consent_token (EvidenceConsentProof | None) – Per-invocation off-host consent proof, minted through mint_evidence_consent_token(). None is correct for every on-host read.

  • operation (PinnedAuthorityOperation) – Caller-owned pinned authority operation used to resolve the invoice extraction authority values. The lease must remain open for the complete extraction operation.

  • legends (tuple[RegimeLegend, ...]) – The dated regime declarations resolved from operation by the enclosing composition boundary. The same tuple is used by the structured and grounded deterministic checks.

  • ports (InvoiceDraftExtractionPorts) – Application-owned evidence and reader capabilities supplied by the enclosing composition root.

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 evidence_id resolves outside the bytes-bearing evidence-record id space (a catalogue-invoice id carries fiscal totals, not a document), 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.