cadrumo.adapters.inbound.einvoice.parsers module

Deterministic parsers for EN16931 (CII and UBL) and Facturae 3.2.x.

All three map onto ONE line-carrying extraction draft, so a downstream consumer never branches on which syntax a document happened to arrive in. Every parser runs through parse_hardened_xml(), so entity resolution and external DTD loading are off and size and depth are bounded before any field is read.

Three behaviours here are deliberate and each closes a diagnosed defect.

The IVA number is selected as the party tax identifier, not the first identifier in the tree. ZUGFeRD is a Franco-German format and its supplier block routinely carries a French SIRET or a German Steuernummer alongside the IVA id; taking the first one produced 22 of the 34 wrong fields measured corpus-wide, with zero missing fields – the parser was finding every field and choosing the wrong one.

Emisor and destinatario are mapped by role, never by position. A received-invoice record whose supplier and customer are swapped reports the taxpayer as the issuer, which inverts the direction of the whole record.

A rate that matches no closed slot refuses loudly and is never rounded to the nearest member. The transient 2022-2024 5% rate is exactly this case: no slot exists for it, so a pre-2025 document must refuse rather than silently resolve to the nearest slot and mint an invoice whose cuota disagrees with its face.

class FacturaeInvoiceClass(*values)[source]

Bases: StrEnum

Class code stated by Facturae’s InvoiceHeader/InvoiceClass.

ORIGINAL
ORIGINAL_CORRECTIVE
ORIGINAL_SUMMARY
COPY
COPY_CORRECTIVE
COPY_SUMMARY
class ParsedEInvoiceLine(*, description=None, quantity=None, unit_price=None, taxable_base=None, iva_rate=None, iva_amount=None)[source]

Bases: object

One parsed line item, syntax-independent.

Parameters:
  • description (str | None)

  • quantity (Decimal | None)

  • unit_price (Decimal | None)

  • taxable_base (Decimal | None)

  • iva_rate (Decimal | None)

  • iva_amount (Decimal | None)

description
quantity
unit_price
taxable_base
iva_rate
iva_amount
class ParsedEInvoice(*, shape)[source]

Bases: object

A structured invoice read exactly from its own machine-readable record.

Parameters:

shape (DocumentShape)

shape
supplier_tax_id: str | None
customer_tax_id: str | None
supplier_name: str | None
customer_name: str | None
supplier_postal_code: str | None
customer_postal_code: str | None
supplier_country_code: str | None
customer_country_code: str | None
invoice_number: str | None
invoice_series: str | None
facturae_invoice_class: FacturaeInvoiceClass | None
rectifies_invoice_number: str | None
invoice_date: str | None
currency: str | None
taxable_base: Decimal | None
iva_amount: Decimal | None
grand_total: Decimal | None
recargo_amount: Decimal | None
retencion_amount: Decimal | None
suplidos_amount: Decimal | None
iva_category: str | None
regime_legend: str | None
record_text: str
lines: list[ParsedEInvoiceLine]
iva_breakdown: list[tuple[Decimal | None, Decimal | None, Decimal | None]]
parse_einvoice_document(data)[source]

Read a structured e-invoice exactly, in whichever syntax it arrives.

Handles a standalone XML document and a ZUGFeRD / Factur-X PDF carrying its invoice as an embedded file, resolving the embedded payload transparently.

Parameters:

data (bytes) – The document’s full in-memory bytes.

Return type:

ParsedEInvoice

Returns:

A ParsedEInvoice with whatever the document states.

Raises:

EInvoiceXmlParseError – If the document carries no structured record, or its record is malformed. Refuses outright rather than returning a partial record: a structured reader that returned half a record on malformed input would look exact while being wrong.