cadrumo.application.ledger.closure_findings module

Arithmetic identities a real invoice must satisfy, checked at read time.

Three families of identity, all deterministic, all computed from figures the document itself states:

The total closes. total = base + cuota + recargo + suplido. Every term is a component the document prints; when they do not reach the printed total, either a component was misread or the document carries one the draft cannot represent. Both are silent under-declarations when the printed figure is discarded unexamined, which is why the printed total is never normalised toward the computed one – the disagreement IS the finding.

Cash closes. cash = total - retencion. IRPF withheld at source is subtracted from the invoice total to reach the money that actually moves, so a draft that drops the retención reconciles the ledger against the wrong figure – and reconciles successfully, against a bank movement that is short by exactly the withholding.

The per-rate breakdown closes. The per-rate subtotals must sum to the flat base and cuota beside them. These are two independent readings of one document, so a disagreement means at least one is wrong and the draft cannot say which. Modelo 303 declares cuota devengada per tier, so a breakdown that sums correctly in total while misattributing base between tiers still declares into the wrong tier – checked per tier as well as in aggregate.

On tolerance, deliberately. Real invoices round per line, so a few cents of drift across several terms is arithmetic rather than error. The allowance is one cent per contributing term and no more (ROUNDING_ALLOWANCE_PER_TERM). That is wide enough to absorb genuine per-line rounding and far too narrow to absorb a misread component. A tolerance loose enough to let a real discrepancy pass is not a more forgiving version of this check – it is the check switched off, which is why the gate mutation-proves the boundary rather than trusting it.

This module is distinct from printed_total_discrepancy(), and the distinction is the point: that function compares the document against the invoice that was actually WRITTEN, so it can only run at confirm. These identities are internal to the document and run the moment it is read, which is where the operator is still deciding.

See also

DraftDiscrepancyFinding

The finding record produced here.

DraftDiscrepancyKind

The closed kind axis; this module never widens it.

printed_total_discrepancy()

The confirm-time, document-versus-record counterpart.

ROUNDING_ALLOWANCE_PER_TERM

Cents of drift allowed per contributing term, and not one more.

Per-line rounding is real: a document stating four components can legitimately land a cent away from their sum. Scaling the allowance by the number of terms admits exactly that and nothing else. It is deliberately NOT a percentage – a percentage grows with the invoice, so the largest invoices, where a misread component costs the most, would get the widest licence to be wrong.

within_rounding_allowance(difference, *, term_count)[source]

Return whether difference is inside the per-term rounding allowance.

Parameters:
  • difference (Decimal) – Signed difference between the stated and computed figures.

  • term_count (int) – How many document-stated terms contributed to the computation. At least one.

Return type:

bool

Returns:

True when the difference is attributable to per-line rounding.

closure_findings(draft)[source]

Return every arithmetic-closure finding the draft’s own figures raise.

Deterministic and total: the same draft always yields the same findings, and an empty result means every identity that COULD be checked held – never that the document is correct, because an identity with a missing term is not checked at all.

Parameters:

draft (InvoiceDraft) – The draft to check, carrying the figures the document stated.

Return type:

tuple[DraftDiscrepancyFinding, ...]

Returns:

Findings in a stable order – total closure, cash closure, flat rate consistency, per-tier rate consistency, then breakdown sums – so an operator surface and a test both read them the same way.