cadrumo.application.invoices.catalogue_creation module

Application service for creating one rich catalogue Invoice.

The operator-facing aeat app ledger invoice add verb mints a linkable invoice directly: the Invoice in the InvoiceCatalogue is the only invoice record, and it carries linked_transaction_ids and is the reconciliation authority link targets.

create_catalogue_invoice() builds a strict Invoice from operator-friendly fields and persists it through the required catalogue creation port bundle (no parallel write path). A caller that supplies no line set gets a single line synthesised from the taxable base and IVA rate; a caller that supplies one gets those lines, which is how an invoice carrying several IVA rates is expressed.

This is the ONE line-synthesis site. The bulk import path does not carry its own: it routes every row through build_catalogue_invoice(), so the two transports cannot disagree about the shape they produce. Its row model does still admit only one rate per row, which is a limit of that file format rather than a second synthesis to keep in step. The returned Invoice.invoice_id is the content-addressed hash link --invoice-id resolves, closing the documented add->link gap without collapsing the two stores.

class CatalogueInvoiceCreateResult(**data)[source]

Bases: BaseModel

Result of persisting one rich catalogue invoice.

Parameters:

data (Any)

invoice: Invoice
catalogue: InvoiceCatalogue
bucket_event_ids: tuple[str, ...]
emit_catalogue_invoice_event(*, invoice, bucket_id, slot, event_repository, occurred_at, actor)[source]

Append the creation event for a canonically-written invoice.

The canonical write paths emitted NO bucket event of any kind, while the slim store emitted six types and returned their ids to the operator. So repointing the operator’s verbs onto this store would have dropped the invoice audit trail and the event-ids field in the same change – a capability loss with no replacement, which is why it blocked the fold.

The event type is chosen by direction so this store speaks the vocabulary that already exists rather than minting a parallel one; the six members outlive the slim store that used to be their only emitter.

Return type:

tuple[str, ...]

Parameters:
resolve_iva_rate_slot(iva_rate, on_date)[source]

Map a printed percentage to its dated authority-backed persisted slot.

None resolves to the registry-declared exempt slot so a base-only invoice with no cuota is accepted. A percentage outside the governed slot vocabulary is refused with the accepted set named, never a bare “value invalid”.

Public because the percentage does not only come from an operator: the ledger’s evidence-confirm path reads it off the document itself and needs the SAME refusal. Two resolvers previously split that job between them and disagreed about the outcome – one raised a localised error naming the accepted set, the other a raw English one – so which message an operator saw depended on whether their document happened to print a cuota.

Return type:

IvaRate

Parameters:
build_catalogue_invoice(*, bucket_id, kind, counterparty_name, counterparty_tax_id, counterparty_country, invoice_number, issued_at, taxable_base, iva_rate, currency, payment_status=PaymentStatus.PENDING, notes='', iva_category=None, operation_type=None, operation_date=None, retention_rate=None, retention_amount=None, invoice_class=None, series=None, rectifies_invoice_number=None, recargo_amount=None, lines=None, rate_provider, operation=None)[source]

Return a strict rich Invoice from operator-supplied fields.

When lines is omitted a single line item is synthesised from taxable_base and the resolved IVA rate slot, and the invoice totals are derived from that line so the Invoice arithmetic invariants hold. The returned invoice carries no linked transactions yet — link --invoice-id populates them later.

When lines IS supplied they are authoritative and the totals are summed from them, which is what lets one invoice carry several IVA rates. A real invoice mixing 21% and 10% lines is not exotic — collapsing it to one line at a single rate reports the right grand total while attributing the cuota to the wrong rate, and the per-rate breakdown is precisely what the IVA modelos declare.

taxable_base must then AGREE with the summed line subtotals, and a mismatch refuses rather than resolving. Two disagreeing sources of truth for the same base is the shape that silently mis-declares, so the caller is made to state one number, not two.

iva_category carries the intra-community classification the M349 recapitulative resolver reads for historical goods/triangulation records. operation_type carries the explicit Modelo 349 clave for invoice records that need a key not represented by an IVA category.

retention_rate / retention_amount record the RIRPF art. 95 withholding a payer settles against a received invoice (or a customer against an issued one). Neither is derived here: Invoice’s own _validate_retencion_consistency accepts an amount alone, requires an amount whenever a rate is supplied, and refuses either that does not match the invoice’s base_total.

invoice_class, series, rectifies_invoice_number and recargo_amount reach axes the aggregate has always modelled and no write path could set. Until they existed here every canonically-written invoice was ordinary with no series and no recargo by construction, and a rectificativa was unrepresentable — so the aggregate claimed a vocabulary the writer could not speak.

The recargo rides INSIDE grand_total (LIVA art. 161) while a retención is settled outside it, which is why only the recargo enters the totals identity. The model re-checks that identity exactly, so a stated recargo the lines do not support refuses rather than being balanced silently.

Return type:

Invoice

Parameters:
create_catalogue_invoice(*, invoice, ports, occurred_at=None, actor='cli')[source]

Persist one pre-built catalogue invoice and return the updated catalogue.

build_catalogue_invoice() is the sole construction authority for operator-supplied fields and line synthesis. This service owns only the catalogue mutation and its post-save audit event, so the construction contract cannot drift between an in-memory candidate and the written record.

Return type:

CatalogueInvoiceCreateResult

Parameters: