cadrumo.application.filing._export_parity module

Pre-write structural-parity gate for the fichero-BOE export.

A .boe file is accepted by AEAT on its bytes, and a SHA-256 digest proves only that the bytes on disk are the bytes that were written – never that they carry what the modelo requires. A draft that silently lost a computed casilla still serialises to a well-formed, correctly-digested file whose slots are blank: a structurally-thin filing. This module holds the assertions that run between rendering and pathlib.Path.write_bytes(), so such a file is refused before it can exist rather than discovered by AEAT.

Three dimensions are asserted, each a hard enumerated FilingExportError naming exactly what drifted:

  • Casilla presence – every casilla that is a calculation RESULT (declares a formula) or is schema-required, that the completeness manifest lists AND the official record files a slot for at this filing’s disposition, carries a real value on disk (assert_export_mirrors_manifest()).

  • Record/section order – the records reaching disk, emitted in their declared order, follow the registry export-layout declaration order, with no two rendered records sharing an order (_assert_record_order_fidelity()).

  • Casilla numbering/segmento – every representable manifest casilla carries the (number, segmento) the registry CasillaDefinition declares (_assert_casilla_metadata_fidelity()).

Disposition is the axis running through all three

None of the three is a property of the layout alone: each is scoped to what this filing’s disposition actually files. The bank-account (DID) page is the worked case – it belongs to the layout but reaches disk only where the fichero must carry an account, so a casilla it carries is required on such a filing and out of scope otherwise. _did_page_suppressed() is therefore the shared predicate behind representability and record order alike, and lives here with the disposition-scoped concern rather than beside the renderer that also consults it.

“Must carry an account” is NOT “is a refund”, and this prose said it was until a domiciliación filing was measured going out with no account for AEAT to debit. The three refund codes need the page because AEAT pays into the account; U needs it because AEAT charges the account. The Diseño settles it by labelling position 23 Domiciliación/Devolución - IBAN – one field for both directions – while every other field on the page is prefixed Devolución -.

Set derivation and the gate are one concern

boe_representable_casilla_ids(), rendered_casilla_ids(), and required_applicable_casilla_ids() are the gate’s own set algebra, and are public because the parity regressions and the export-completeness gate pin them directly. They are exported here so a change to the required-set semantics propagates from one derivation authority to every production consumer, rather than being mirrored into a second copy that can drift out of agreement with the gate it is supposed to describe.

See also

application.filing.export

Renders the layout and calls assert_export_mirrors_manifest() before writing the bytes.

boe_representable_casilla_ids(layout, *, draft, headers, prior_domiciliation_election, schema_provider)[source]

Return the casillas the .boe layout files a slot for, for this disposition.

A casilla is representable when the official record design carries a field for it that this draft’s disposition does not suppress. xml_dictionary layouts derive their casillas from the dictionary entries; fixed_width layouts from every CASILLA field plus the binding-row casilla mappings (row_field_casilla_ids), across records not suppressed for the disposition (e.g. the DID refund page on a non-refund filing).

The completeness gate intersects the calculation-completeness manifest with this set: a manifest casilla absent here is a calculation-closure casilla the official filed record does not carry, so it is out of scope for the .boe parity gate rather than a drift.

That out-of-scope verdict is no longer taken on trust. The fixed-width branch delegates to fixed_width_record_casilla_ids(), the same derivation the registry-build export-exemption gate runs over EVERY declared record; that gate refuses at build any manifest casilla this set would exempt from a demand unless the casilla declares WHY it files no slot. Absence here therefore carries a reviewed reason behind it rather than being indistinguishable from a forgotten annotation.

Core types: ModeloDraft.

Return type:

frozenset[TypeAliasType]

Parameters:
rendered_casilla_ids(layout, *, draft, headers, prior_domiciliation_election, schema_provider)[source]

Return the representable casillas whose value actually reaches disk.

A representable casilla reaches disk only when the ModeloDraft carries a value for it; a representable casilla absent from draft.values renders as a blank fixed-width slot (or an omitted xml element), which is the structurally-thin file the completeness gate exists to refuse. This is the rendered set the gate compares against the manifest-required-and-representable set.

Return type:

frozenset[TypeAliasType]

Parameters:
required_applicable_casilla_ids(manifest, *, collection, representable)[source]

Return the casillas that must carry a value in a complete fichero-BOE export.

A casilla is required-applicable when it declares a formula (a calculation RESULT) or is schema-required, AND the official export record files a slot for it at this filing’s disposition (representable). Optional operator-input casillas — retenciones, prior payments, deductions the taxpayer may legitimately not have — declare neither a formula nor a required flag, so they are excluded: a blank slot for them is a valid zero, not a structurally-thin file.

This is the single derivation authority behind the completeness gate (assert_export_mirrors_manifest()), so a change to the required-set semantics propagates to every production consumer in one place.

The regression tests deliberately do NOT consume this function as their expectation: they pin it against an independent partition read straight off the CasillaCollection. A test whose expected set comes from the subject itself can only detect that the subject was called, never that its semantics changed – and a relaxation here silently drops a casilla out of the pre-write gate, letting a structurally-thin fichero-BOE ship behind a valid SHA-256 digest. Do not “de-duplicate” the tests onto this function.

Parameters:
Return type:

frozenset[TypeAliasType]

Returns:

Frozen set of casilla IDs that must carry a real value before the fichero-BOE bytes are written.

See also

boe_representable_casilla_ids()

Derives the representable argument from the export layout and headers.

assert_export_mirrors_manifest()

Gate that enforces this required set before writing bytes.

assert_export_mirrors_manifest(layout, *, draft, headers, prior_domiciliation_election, schema_provider, manifest, casilla_metadata)[source]

Panic if the .boe would not mirror the manifest-required structure.

The completeness gate: every casilla that is a calculation RESULT (declares a formula) or is schema-required, and that the calculation-completeness manifest lists AND the official record files (representable for this ModeloDraft’s disposition), MUST carry a value on disk. Such a casilla rendered blank means the calculation did not populate it – a structurally-thin file behind a valid SHA-256 digest, which this gate refuses with a hard FilingExportError naming every missing casilla with its official record number and segmento, so the panic is loud and explicit.

Optional operator-input casillas – retenciones, prior payments, deductions the taxpayer may legitimately not have – are NOT required to carry a value: a blank slot for them is a valid zero, not a thin file (grounded in the AEAT casilla semantics; e.g. Modelo 131 casillas 02/08/09/12/14 are optional inputs), so they are excluded from the required set. A manifest casilla absent from the representable set is a calculation-closure casilla the official record does not file and is likewise out of scope. Callers pass manifest only when the revision declares one; a revision without a manifest is handled by the coverage-advisory path, not here.

The gate applies only to the fixed-width fichero-BOE. In that format every field occupies its byte slot always, so an omitted required casilla renders a blank slot behind a valid digest – the structurally-thin file. An xml_dictionary export instead omits an absent casilla as an absent optional element, which is legitimate (a filer declares only the casillas its situation requires), so completeness is not asserted for that transport.

Beyond casilla presence, the gate asserts two further structural-fidelity dimensions before any bytes are written, so the .boe mirrors the official modelo-revision structure and not merely its casilla set:

  • Record/section order: the records that reach disk, emitted in their declared order, must follow the registry export-layout declaration order, and no two rendered records may share an order (_assert_record_order_fidelity()).

  • Casilla numbering/segmento: every manifest casilla the official record files a slot for must carry the same (number, segmento) the registry CasillaDefinition declares – re-grounded against the projected CasillaRecordMetadata, not the manifest’s own copy (_assert_casilla_metadata_fidelity()).

Each dimension is a hard, enumerated FilingExportError; a structural divergence is a failure, never a warning.

Return type:

None

Parameters:
assert_rate_boxes_account_for_total(partitions, *, draft)[source]

Panic if the draft’s rate boxes account for less than their declared total.

A rate-specific official box asserts a rate, so a ledger row recording a cuota without recording the rate charged reaches the rate-blind total layer and no box. The return keeps the money and the breakdown keeps its integrity; what it loses is the property that the parts sum to the whole, and AEAT reconciles those boxes against that total.

The refusal sits at the write door for the same reason the completeness gate does: the application never files, so the artefact leaves here for a human to submit with nothing behind it. A blank computed slot and a breakdown that does not reach its own total are the same class of defect – a return whose structure contradicts what the calculation determined – and both cost the taxpayer a correction they cannot make, where the refusal costs them a ledger repair they can.

Unlike the completeness gate this applies to every transport. That gate is fixed-width-only because its subject is a blank BYTE SLOT, which the xml transport does not write; this one’s subject is an arithmetic relation between values, which is equally false in either encoding. A box the xml transport legitimately omits reads as zero here, which is what it declares.

The condition is never the operator’s first notice of it: the calculate path raises the same shortfall as a non-blocking advisory, computed by the same rate_box_coverage_shortfalls() over the same partitions.

Parameters:
Raises:

FilingExportError – A partition’s boxes account for less than its total, naming each shortfall with its tier, its total casilla and its boxes.

Return type:

None

did_page_suppressed(record, *, draft, headers, prior_domiciliation_election)

Return whether a DID (bank-account) page record must be suppressed.

The sole DID predicate is disposition_requires_bank_account OR Nota 3. Nota 3 means an M303 rectificativa with casilla 111 semantically present and a KEEP prior-domiciliation election. It is independent of the current disposition, while a selected CANCEL_OR_MODIFY disables only that added path, never a DID page required by the current disposition.

This function is shared by the renderer and parity derivations so they cannot disagree about which official record reaches disk.

Return type:

bool

Parameters:
assert_xml_declaration_aux_declared(layout)[source]

Panic if an xml_dictionary export cannot write its mandatory Aux.

Every AEAT Modelo 100 XSD opens Declaracion with Aux, minOccurs=1, whose Idioma and VERSION children are each minOccurs=1 too. No bundled dictionary declares a single Aux row in any revision, so the dictionary-driven writer cannot reach the block and the values are declared on the layout instead. When one is undeclared the block cannot be written at all: a partial Aux is invalid, and omitting it makes the document fail at its very first element.

The refusal exists because the alternative is worse than an error. AEAT types VERSION as tipo_String4L — four characters, permissive pattern, no enumeration — so an invented token would VALIDATE, the document would start passing every check made of it, and the gap would stop being reported while an unverified value rode to the tax authority. Refusing keeps the gap visible; the export resumes the moment the layout declares a real value, with no code change.

This runs at the write door rather than inside the renderer because the property it defends is that no unfileable artefact reaches the operator, and because an operator who receives a file believes they hold a filing artefact.

Parameters:

layout (ExportLayoutDefinition) – The export layout about to be written.

Raises:

FilingExportError – The layout is xml_dictionary and cannot supply the mandatory block, naming the undeclared field.

Return type:

None