"""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 :meth:`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
:class:`~domain.filing.errors.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 (:func:`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``
(:func:`_assert_record_order_fidelity`).
* **Casilla numbering/segmento** -- every representable manifest casilla carries
the ``(number, segmento)`` the registry ``CasillaDefinition`` declares
(:func:`_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. :func:`_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
--------------------------------------------
:func:`boe_representable_casilla_ids`, :func:`rendered_casilla_ids`, and
:func:`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:
:mod:`application.filing.export`
Renders the layout and calls :func:`assert_export_mirrors_manifest`
before writing the bytes.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from decimal import Decimal
from ...core.aggregation import BindingAggregationOp
from ...core.casilla_id import CasillaId, validated_casilla_id
from ...core.export_layout_format import ExportLayoutFormat
from ...core.filing_producer_key import FilingProducerKey
from ...core.modelo import Modelo
from ...core.prior_domiciliation_election import PriorDomiciliationElection
from ...core.result_disposition import ResultDisposition, result_disposition_requires_bank_account
from ...domain.calculations.registry.binding_aggregation import binding_aggregation_op
from ...domain.calculations.registry.binding_selector_utils import binding_row_set_selector
from ...domain.calculations.registry.export import fixed_width_record_casilla_ids
from ...domain.calculations.registry.export_parse import xml_dictionary_entries
from ...domain.calculations.registry.rate_box_partition import (
RateBoxPartition,
rate_box_coverage_shortfalls,
)
from ...domain.calculations.registry.schema_exports import ExportLayoutDefinition, ExportRecordDefinition
from ...domain.calculations.registry.schema_surfaces import CalculationCompletenessManifest
from ...domain.filing.protocols import CasillaCollection
from ...domain.filing.schema import ModeloDraft
from .errors import ModeloApplicationError as FilingExportError
from .runtime import CasillaRecordMetadata, RegistrySchemaAccessor
#: ``record_type`` of the bank-account (DID) page in the DR303 export layout.
#: The page is emitted for any disposition whose fichero must carry an account,
#: which is NOT the same as "a refund". Its IBAN field is the one dual-purpose
#: field on the page -- the Diseño names it ``Domiciliación/Devolución - IBAN``
#: -- while the SWIFT-BIC, bank name, address, city, country and marca SEPA
#: fields are each prefixed ``Devolución -`` and apply to a refund only. So a
#: domiciliación filing needs this page for its IBAN alone, and suppressing it
#: there filed a direct-debit election with no account for AEAT to charge.
_DID_PAGE_RECORD_TYPE = "domiciliacion"
_M303_CASILLA_111: CasillaId = validated_casilla_id("111", surface="M303 Nota 3 DID predicate")
_M303_RECTIFICATIVA_HEADER = FilingProducerKey.AMENDMENT_IS_RECTIFICATIVA
def _m303_nota_three_requires_bank_account(
*,
draft: ModeloDraft,
headers: Mapping[FilingProducerKey, object],
prior_domiciliation_election: PriorDomiciliationElection,
) -> bool:
"""Return whether M303 Nota 3 independently requires the DID page.
Casilla 111 is semantically present when it has a value, including zero;
truthiness would incorrectly collapse a stated zero into an absent field.
The typed cancellation election alone disables this Nota-3 requirement.
"""
return (
draft.modelo == Modelo("303")
and headers.get(_M303_RECTIFICATIVA_HEADER) is True
and prior_domiciliation_election is PriorDomiciliationElection.KEEP
and any(value.casilla_id == _M303_CASILLA_111 and value.value is not None for value in draft.values)
)
def _did_page_required(
*,
draft: ModeloDraft,
headers: Mapping[FilingProducerKey, object],
prior_domiciliation_election: PriorDomiciliationElection,
) -> bool:
"""Return whether this filing must include the DID bank-account page."""
declaration_type = headers.get(FilingProducerKey.FILING_RESULT_DISPOSITION, "")
try:
disposition = ResultDisposition(declaration_type)
except ValueError:
disposition_requires_bank_account = False
else:
disposition_requires_bank_account = result_disposition_requires_bank_account(disposition)
return disposition_requires_bank_account or _m303_nota_three_requires_bank_account(
draft=draft,
headers=headers,
prior_domiciliation_election=prior_domiciliation_election,
)
def _did_page_suppressed(
record: ExportRecordDefinition,
*,
draft: ModeloDraft,
headers: Mapping[FilingProducerKey, object],
prior_domiciliation_election: PriorDomiciliationElection,
) -> bool:
"""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.
"""
if record.record_type != _DID_PAGE_RECORD_TYPE:
return False
return not _did_page_required(
draft=draft,
headers=headers,
prior_domiciliation_election=prior_domiciliation_election,
)
[docs]
def boe_representable_casilla_ids(
layout: ExportLayoutDefinition,
*,
draft: ModeloDraft,
headers: Mapping[FilingProducerKey, object],
prior_domiciliation_election: PriorDomiciliationElection,
schema_provider: RegistrySchemaAccessor,
) -> frozenset[CasillaId]:
"""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
:func:`~domain.calculations.registry.export.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:
:class:`~cadrumo.domain.filing.schema.ModeloDraft`.
"""
if layout.format is ExportLayoutFormat.XML_DICTIONARY:
entries = xml_dictionary_entries(
layout,
sources=schema_provider.sources,
source_payloads=schema_provider.source_payloads,
)
return frozenset(entry.casilla_id for entry in entries if entry.casilla_id is not None)
return fixed_width_record_casilla_ids(
tuple(
record
for record in layout.records
if not _did_page_suppressed(
record,
draft=draft,
headers=headers,
prior_domiciliation_election=prior_domiciliation_election,
)
),
)
[docs]
def rendered_casilla_ids(
layout: ExportLayoutDefinition,
*,
draft: ModeloDraft,
headers: Mapping[FilingProducerKey, object],
prior_domiciliation_election: PriorDomiciliationElection,
schema_provider: RegistrySchemaAccessor,
) -> frozenset[CasillaId]:
"""Return the representable casillas whose value actually reaches disk.
A representable casilla reaches disk only when the :class:`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.
"""
# build_draft emits a ModeloValue for every declared casilla, using
# value=None (kind=EMPTY) as the "nothing here" marker, so casilla-id
# membership in draft.values is NOT value presence: an EMPTY casilla would
# render as a blank slot. Filter to real values (value is None iff EMPTY).
valued_casillas = {value.casilla_id for value in draft.values if value.value is not None}
rendered_row_casillas = _rendered_row_binding_casilla_ids(
layout,
draft=draft,
headers=headers,
prior_domiciliation_election=prior_domiciliation_election,
schema_provider=schema_provider,
)
return frozenset(
boe_representable_casilla_ids(
layout,
draft=draft,
headers=headers,
prior_domiciliation_election=prior_domiciliation_election,
schema_provider=schema_provider,
)
& (valued_casillas | rendered_row_casillas)
)
def _rendered_row_binding_casilla_ids(
layout: ExportLayoutDefinition,
*,
draft: ModeloDraft,
headers: Mapping[FilingProducerKey, object],
prior_domiciliation_election: PriorDomiciliationElection,
schema_provider: RegistrySchemaAccessor,
) -> set[CasillaId]:
"""Return row-field casillas whose active binding value reaches a rendered record."""
bindings = {str(binding.id): binding for binding in schema_provider.get_snapshot(draft.modelo).revision.bindings}
active_binding_ids = {
str(value.binding_id) for value in draft.binding_values if value.value is not None and value.value != ""
}
rendered: set[CasillaId] = set()
for record in layout.records:
if _did_page_suppressed(
record,
draft=draft,
headers=headers,
prior_domiciliation_election=prior_domiciliation_election,
):
continue
for field in record.fields:
if field.binding is None or str(field.binding) not in active_binding_ids:
continue
binding = bindings.get(str(field.binding))
if binding is None:
continue
# Row-field casillas are meaningful only for the registry's typed
# row aggregation. Other bindings can share a record field without
# naming a row-set projection and must retain their normal scalar
# completeness path.
if binding_aggregation_op(binding) is not BindingAggregationOp.ROWS:
continue
selector = binding_row_set_selector(binding)
if (
selector is not None
and (casilla_id := record.row_field_casilla_ids.get(selector.row_field)) is not None
):
rendered.add(casilla_id)
return rendered
[docs]
def required_applicable_casilla_ids(
manifest: CalculationCompletenessManifest,
*,
collection: CasillaCollection,
representable: frozenset[CasillaId],
) -> frozenset[CasillaId]:
"""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
(:func:`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
:class:`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.
Args:
manifest: Revision's
:class:`~domain.calculations.registry.schema_surfaces.CalculationCompletenessManifest`.
collection: :class:`~domain.filing.protocols.CasillaCollection` for the modelo,
supplying ``formula`` and ``required`` for each declared casilla.
representable: Casilla IDs the official export record files a slot for
at this filing's disposition; see :func:`boe_representable_casilla_ids`.
Returns:
Frozen set of casilla IDs that must carry a real value before the
fichero-BOE bytes are written.
See Also:
:func:`boe_representable_casilla_ids`
Derives the ``representable`` argument from the export layout and headers.
:func:`assert_export_mirrors_manifest`
Gate that enforces this required set before writing bytes.
"""
return (
frozenset(
casilla.casilla_id
for casilla in manifest.casillas
if (schema := collection.get(casilla.casilla_id)) is not None
and (schema.formula is not None or schema.required)
)
& representable
)
[docs]
def assert_export_mirrors_manifest(
layout: ExportLayoutDefinition,
*,
draft: ModeloDraft,
headers: Mapping[FilingProducerKey, object],
prior_domiciliation_election: PriorDomiciliationElection,
schema_provider: RegistrySchemaAccessor,
manifest: CalculationCompletenessManifest,
casilla_metadata: tuple[CasillaRecordMetadata, ...],
) -> None:
"""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
:class:`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
:class:`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``
(:func:`_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
:class:`~application.filing.runtime.CasillaRecordMetadata`, not the
manifest's own copy (:func:`_assert_casilla_metadata_fidelity`).
Each dimension is a hard, enumerated :class:`FilingExportError`; a structural
divergence is a failure, never a warning.
"""
if layout.format is not ExportLayoutFormat.FIXED_WIDTH:
return
_assert_record_order_fidelity(
modelo=draft.modelo,
layout=layout,
draft=draft,
headers=headers,
prior_domiciliation_election=prior_domiciliation_election,
)
representable = boe_representable_casilla_ids(
layout,
draft=draft,
headers=headers,
prior_domiciliation_election=prior_domiciliation_election,
schema_provider=schema_provider,
)
_assert_casilla_metadata_fidelity(
modelo=draft.modelo,
manifest=manifest,
representable=representable,
casilla_metadata=casilla_metadata,
)
rendered = rendered_casilla_ids(
layout,
draft=draft,
headers=headers,
prior_domiciliation_election=prior_domiciliation_election,
schema_provider=schema_provider,
)
# Require a value on disk only for calculation RESULTS (casillas declaring a
# formula) and schema-required casillas. Optional operator inputs -- retenciones,
# prior payments, deductions the taxpayer may legitimately not have -- render a
# blank slot that is a valid zero, not a thin file, so they are excluded from the
# required set (grounded in the AEAT casilla semantics, e.g. Modelo 131 casillas
# 02/08/09/12/14).
collection = schema_provider.get_collection(draft.modelo)
required_applicable = required_applicable_casilla_ids(
manifest,
collection=collection,
representable=representable,
)
missing = sorted(required_applicable - rendered)
if not missing:
return
metadata = {casilla.casilla_id: (casilla.number, casilla.segmento) for casilla in manifest.casillas}
raise FilingExportError(
translated_message="application.filing.export_parity.errors.required_casillas_omitted",
context={
"modelo": draft.modelo,
"missing_count": len(missing),
"missing_casillas": tuple(
{
"casilla_id": casilla_id,
"number": metadata[casilla_id][0],
"segmento": metadata[casilla_id][1],
}
for casilla_id in missing
),
},
)
[docs]
def assert_rate_boxes_account_for_total(
partitions: Sequence[RateBoxPartition],
*,
draft: ModeloDraft,
) -> None:
"""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
:func:`~domain.calculations.registry.rate_box_partition.rate_box_coverage_shortfalls` over the
same partitions.
Args:
partitions: The revision's derived rate-box partitions, from
:attr:`~application.filing.runtime.RegistryModeloSubview.rate_box_partitions`.
Empty for every revision declaring no rate-specific binding, which
makes this a no-op there.
draft: The :class:`ModeloDraft` about to be written.
Raises:
FilingExportError: A partition's boxes account for less than its total,
naming each shortfall with its tier, its total casilla and its boxes.
"""
if not partitions:
return
values = {value.casilla_id: value.value for value in draft.values if value.value is not None}
numeric = {casilla_id: value for casilla_id, value in values.items() if isinstance(value, Decimal)}
shortfalls = rate_box_coverage_shortfalls(partitions, numeric)
if not shortfalls:
return
raise FilingExportError(
translated_message="application.filing.export_parity.errors.rate_boxes_understate_total",
context={
"modelo": draft.modelo,
"shortfall_count": len(shortfalls),
"shortfalls": tuple(
{
"total_casilla_id": shortfall.partition.total_casilla_id,
"rate_kinds": tuple(shortfall.partition.rate_kinds),
"fact": shortfall.partition.fact,
"declared_total": str(shortfall.total),
"box_casilla_ids": tuple(shortfall.partition.box_casilla_ids),
"boxes_total": str(shortfall.boxes_total),
"unaccounted": str(shortfall.shortfall),
}
for shortfall in shortfalls
),
},
)
def _declared_records_for_filing(
layout: ExportLayoutDefinition,
*,
draft: ModeloDraft,
headers: Mapping[FilingProducerKey, object],
prior_domiciliation_election: PriorDomiciliationElection,
) -> tuple[ExportRecordDefinition, ...]:
"""Return layout records that reach this filing's disposition."""
return tuple(
record
for record in layout.records
if not _did_page_suppressed(
record,
draft=draft,
headers=headers,
prior_domiciliation_election=prior_domiciliation_election,
)
)
def _duplicate_record_orders(records: Sequence[ExportRecordDefinition]) -> tuple[int, ...]:
"""Return each duplicated record order once, in diagnostic order."""
orders = [record.order for record in records]
return tuple(sorted({order for order in orders if orders.count(order) > 1}))
def _record_order_drifts(
declared: Sequence[ExportRecordDefinition],
emitted: Sequence[ExportRecordDefinition],
) -> tuple[dict[str, object], ...]:
"""Describe each position whose emitted record differs from the declaration."""
return tuple(
{
"position": index,
"registry_record_id": declared[index].id,
"emitted_record_id": emitted[index].id,
}
for index in range(len(declared))
if declared[index].id != emitted[index].id
)
def _assert_record_order_fidelity(
*,
modelo: str,
layout: ExportLayoutDefinition,
draft: ModeloDraft,
headers: Mapping[FilingProducerKey, object],
prior_domiciliation_election: PriorDomiciliationElection,
) -> None:
"""Panic if the rendered record order drifts from the registry declaration order.
A fixed-width ``.boe`` emits its records sorted by each record's ``order``
field; the AEAT *Diseño de registros* fixes that page/segment sequence and the
registry export layout declares the records in that official order
(``layout.records``). This assertion re-grounds the emitted record sequence
against that declaration: the records that reach disk (excluding
disposition-suppressed records such as the DID refund page) must, when emitted
in ``order``, appear in the same sequence the registry declares, and no two
rendered records may share an ``order`` (an ambiguous emit sequence). A
permuted or ambiguous record order is a hard, enumerated
:class:`FilingExportError` -- the ``.boe`` record/section sequence must mirror
the official modelo-revision structure.
"""
declared = _declared_records_for_filing(
layout,
draft=draft,
headers=headers,
prior_domiciliation_election=prior_domiciliation_election,
)
duplicate_orders = _duplicate_record_orders(declared)
if duplicate_orders:
raise FilingExportError(
translated_message="application.filing.export_parity.errors.record_emit_order_duplicated",
context={
"modelo": modelo,
"duplicate_order_count": len(duplicate_orders),
"duplicate_orders": tuple(duplicate_orders),
},
)
emitted = tuple(sorted(declared, key=lambda record: record.order))
if [record.id for record in emitted] != [record.id for record in declared]:
order_drifts = _record_order_drifts(
declared,
emitted,
)
raise FilingExportError(
translated_message="application.filing.export_parity.errors.record_emit_order_drift",
context={
"modelo": modelo,
"drift_count": len(order_drifts),
"order_drifts": order_drifts,
},
)
def _assert_casilla_metadata_fidelity(
*,
modelo: str,
manifest: CalculationCompletenessManifest,
representable: frozenset[CasillaId],
casilla_metadata: tuple[CasillaRecordMetadata, ...],
) -> None:
"""Panic if a representable manifest casilla's number/segmento drifts from the registry.
The completeness manifest carries its own copy of each casilla's official
``(number, segmento)`` -- the metadata this parity gate reports and keys on.
This assertion re-grounds that copy against the authoritative
:class:`~application.filing.runtime.CasillaRecordMetadata` projected from
the registry ``CasillaDefinition`` (the same authority the calculation engine
consumes), for every manifest casilla the official record files a slot for
(``representable``). A divergent ``number`` or ``segmento`` -- or a manifest
casilla the registry no longer declares -- is a hard, enumerated
:class:`FilingExportError` with the expected registry value versus the value the
``.boe`` would file, so the exported casilla numbering and segmento cannot drift
from the official modelo-revision structure behind a valid digest.
"""
registry_by_id = {meta.casilla_id: meta for meta in casilla_metadata}
drifts: list[dict[str, object]] = []
for manifest_casilla in manifest.casillas:
casilla_id = manifest_casilla.casilla_id
if casilla_id not in representable:
continue
registry_meta = registry_by_id.get(casilla_id)
if registry_meta is None:
drifts.append(
{
"casilla_id": casilla_id,
"registry_declares_casilla": False,
"manifest_number": manifest_casilla.number,
"manifest_segmento": manifest_casilla.segmento,
},
)
continue
if (manifest_casilla.number, manifest_casilla.segmento) != (registry_meta.number, registry_meta.segmento):
drifts.append(
{
"casilla_id": casilla_id,
"registry_declares_casilla": True,
"registry_number": registry_meta.number,
"registry_segmento": registry_meta.segmento,
"manifest_number": manifest_casilla.number,
"manifest_segmento": manifest_casilla.segmento,
},
)
if not drifts:
return
raise FilingExportError(
translated_message="application.filing.export_parity.errors.casilla_metadata_drift",
context={
"modelo": modelo,
"drift_count": len(drifts),
"casilla_drifts": tuple(drifts),
},
)
did_page_suppressed = _did_page_suppressed
[docs]
def assert_xml_declaration_aux_declared(layout: ExportLayoutDefinition) -> None:
"""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.
Args:
layout: The export layout about to be written.
Raises:
FilingExportError: The layout is ``xml_dictionary`` and cannot supply the
mandatory block, naming the undeclared field.
"""
if layout.format is not ExportLayoutFormat.XML_DICTIONARY:
return
undeclared = [
name
for name, value in (("aux_idioma", layout.aux_idioma), ("aux_version", layout.aux_version))
if value is None
]
if not undeclared:
return
raise FilingExportError(
translated_message="application.filing.export_parity.errors.aux_block_undeclared",
context={
"layout_id": layout.id,
"layout_format": layout.format.value,
"undeclared_count": len(undeclared),
"undeclared_fields": tuple(undeclared),
},
)
__all__ = [
"assert_export_mirrors_manifest",
"assert_rate_boxes_account_for_total",
"assert_xml_declaration_aux_declared",
"boe_representable_casilla_ids",
"did_page_suppressed",
"rendered_casilla_ids",
"required_applicable_casilla_ids",
]