cadrumo.application.aggregation.renta_income_ledger module

Repository-backed Renta activity-income aggregation.

This module owns the generic ledger projection and period/window mechanics. The selected filing’s model, target casilla, activity selectors, and binding applicability are supplied by the registry boundary by callers; no filing declaration is embedded here.

The quarterly entry point aggregate_renta_income_ledger_from_repositories() loads a TransactionCatalogue and its linked invoice catalogue through the application-owned catalogue read capabilities, then delegates to aggregate_renta_income_ledger() for period-scoped aggregation.

The annual counterpart uses the same generic projection over its requested window. A separate caller supplies its target and model identity.

Cumulative window rule (RD 439/2007 art. 110.2):

For period Qn in year Y the window is [Jan 1, Y] through [last day of Qn, Y]. Q1 covers Jan-Mar; Q2 covers Jan-Jun; Q3 covers Jan-Sep; Q4 covers Jan-Dec.

Only ACTIVE, EUR-denominated, INCOMING transactions whose business_classification is BUSINESS or MIXED are eligible. Transactions whose value_date (or booked_date if absent) falls outside the cumulative window are excluded with a traceable issue record.

class RentaIncomeLedgerAggregationIssueReason(*values)[source]

Bases: StrEnum

Machine-readable reasons why a ledger row did not produce an income observation.

Every member here EXCLUDES a row. The income pipeline’s third outcome — a row that is declarable but ungrounded — is deliberately NOT modelled as an issue: such a row still contributes, so recording it as an exclusion would misstate the aggregation. MISSING_TAXABLE_BASE is the shared vocabulary for the ungrounded condition (spelled identically by the IVA and gasto ledgers, where it genuinely does exclude), and the income pipeline reports it through the LedgerIncomeGrounding marker on the observation instead.

UNSUPPORTED_DIRECTION
UNSUPPORTED_CURRENCY
UNCLASSIFIED_BUSINESS_STATE
PERSONAL_TRANSACTION
OUTSIDE_PERIOD
UNSUPPORTED_PERIOD
TRABAJO_INCOME
class RentaIncomeLedgerAggregationIssue(**data)[source]

Bases: BaseModel

Traceable exclusion emitted while aggregating income ledger rows.

Parameters:

data (Any)

model_config: ClassVar[ConfigDict]

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

transaction_id: TransactionId
reason: RentaIncomeLedgerAggregationIssueReason
detail: IssueDetail
class RentaIncomeObservation(**data)[source]

Bases: BaseModel

One eligible INCOMING professional-income ledger row.

Carries the typed gross amount and the target casilla id it feeds. The domain registry resolver matches target_casilla_id against the binding selector and sums gross_amount (or taxable_base_amount) across all observations for that casilla depending on the declared fact.

taxable_base_amount is the IVA-exclusive base imponible from the original invoice (transaction.taxable_base). It feeds the taxable_base_sum fact path used by the rendimiento-neto binding (casilla 03). None when the transaction carries no explicit taxable_base.

grounding states which of those two states the row is in, as a fact rather than a nullness heuristic every consumer re-derives. A CASH_FALLBACK row declares no invoice substrate, so the only measure available is the raw bank-credited amount — net of any retención practicada and possibly IVA-inclusive. It is therefore NOT ingresos íntegros: the ingresos_integros_sum fact folds that cash in anyway (deliberately — dropping the row would under-declare by its whole value, strictly worse) while taxable_base_sum contributes nothing for it. Both are surfaced as a non-blocking advisory rather than being silently absorbed; the marker is what the advisory, the evidence bundle, and the tests key on.

source_jurisdiction propagates the per-transaction ISO 3166-1 alpha-2 source-jurisdiction provenance from the originating ledger row. LIRPF Art. 8 establishes the universal-base presumption for Spanish residents, so the selected income targets aggregate ALL source jurisdictions into the same base — the field is preserved for audit and for downstream IRNR / Beckham engines that read foreign-source rows.

Parameters:

data (Any)

model_config: ClassVar[ConfigDict]

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

transaction_id: TransactionId
target_casilla_id: CasillaId
gross_amount: Decimal
taxable_base_amount: Decimal | None
withheld_amount: Decimal
withheld_derivation: LedgerWithholdingDerivation
filing_date: date
source_jurisdiction: str | None
grounding: LedgerIncomeGrounding
sales_invoice_refusal: _renta_income_evidence.SalesInvoiceEvidenceRefusal | None
class UnadmittedActivityIncome(**data)[source]

Bases: BaseModel

Eligible income an activity narrowing kept out of the target casilla.

The measurable half of a silence. A casilla fed by an activity-narrowed aggregation resolves to zero in two indistinguishable situations: the taxpayer genuinely earned nothing that the casilla covers, and the taxpayer earned something whose activity nobody ever declared. The computed value is the same zero in both, so the difference has to be carried alongside it.

any_activity_declared is what separates them, and it is a PREDICATE over the excluded rows rather than a copy of any activity code: a row asserting some other activity is a positive answer – the box is correctly empty and the operator has spoken – while a row asserting none leaves the question open. Deliberately not a code, both because no code would be the right one to name and because the activity type has exactly one stored home.

income_total is measured with _computable_income_amount(), the same fact the admitted rows fold through, so the figure an advisory quotes is on the same footing as the figure in the box it is quoted against.

Parameters:

data (Any)

model_config: ClassVar[ConfigDict]

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

target_casilla_id: CasillaId

The casilla the narrowing was guarding, named even when nothing was excluded.

Carried here rather than recovered from the fold, because the fold is EMPTY in exactly the situation that matters: a casilla that admitted nothing has no key in casilla_values, so a consumer reading the fold alone cannot name the box it is reasoning about.

row_count: int
income_total: Decimal
any_activity_declared: bool

Whether ANY excluded row named an activity, without saying which.

A predicate rather than the activity type itself, deliberately: the single-home gate refuses a class-body annotation naming that type, because the accepted design puts the value on a per-activity profile row and has the transaction carry a reference. A bool cannot become a second home for the fact.

It is also NARROWER than the enum it stands in for, and this is the site that will be wrong first if that ever matters. It can answer “was anything declared” and never “was something else declared”, so a future advisory needing to distinguish a third activity kind here cannot be built on it and should not widen it – by then the profile row is the right source.

class RentaIncomeLedgerAggregation(**data)[source]

Bases: LedgerAggregationResultBase[RentaIncomeObservation, RentaIncomeLedgerAggregationIssue]

Cumulative income observations for one selected quarter window.

out_of_window_summary is populated by repository-backed date partitions. Full-catalogue aggregation keeps row-level issues because every transaction is already loaded for classification.

unadmitted_activity_income is populated only by a projection that NARROWS rows by activity when requested. None on an un-narrowed path states that no narrowing ran, which is not the same claim as a narrowing that excluded nothing, and the two must stay distinguishable: the second is a fact about this taxpayer’s ledger, the first is a fact about which projection was asked for.

Parameters:
out_of_window_summary: OutOfWindowTransactionSummary | None
unadmitted_activity_income: UnadmittedActivityIncome | None
model_config: ClassVar[ConfigDict]

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

aggregate_renta_income_ledger_from_repositories(*, bucket_id, period, modelo, target_casilla_id, activity_category_matcher, employment_category_matcher, ports)[source]

Load the transaction catalogue and aggregate a cumulative income window.

Returns a RentaIncomeLedgerAggregation.

Return type:

RentaIncomeLedgerAggregation

Parameters:
aggregate_renta_income_ledger(transactions, invoices=None, *, bucket_id, period, modelo, target_casilla_id, activity_category_matcher, employment_category_matcher)[source]

Aggregate incoming professional-income transactions into a selected target.

Parameters:
  • transactions (TransactionCatalogue) – The TransactionCatalogue of ledger transactions to aggregate.

  • invoices (InvoiceCatalogue | None) – The bucket’s InvoiceCatalogue, whose linked sales invoices supply the base, cuota and retención for rows that reference one. Optional so an in-process caller holding no invoices need not construct an empty catalogue; the production entry point above always loads and passes the real one, so the evidence path is wired rather than latent.

  • bucket_id (str) – Bucket identifier carried through to provenance and audit records so the resulting aggregation cannot be silently misattributed.

  • period (Period) – The quarterly Period whose year anchors the cumulative window.

  • modelo (str) – Selected model identifier receiving the aggregation.

  • target_casilla_id (TypeAliasType) – Registry-selected output casilla for professional income.

  • activity_category_matcher (Callable[[Transaction], bool]) – Predicate for activity-category transactions.

  • employment_category_matcher (Callable[[Transaction], bool]) – Predicate for employment-category transactions.

Return type:

RentaIncomeLedgerAggregation

Returns a RentaIncomeLedgerAggregation covering the cumulative fiscal window. period must be quarterly. The cumulative window extends from Jan 1 of the period’s year through the last day of the declared quarter, implementing the year-to-date accumulation rule for IRPF pagos fraccionados (RD 439/2007 art. 110.2).

aggregate_renta_m100_income_ledger_from_repositories(*, bucket_id, period, modelo, target_casilla_id, activity_category_matcher, employment_category_matcher, ports)[source]

Load the catalogue and aggregate an annual activity-income window.

Return type:

RentaIncomeLedgerAggregation

Returns:

The RentaIncomeLedgerAggregation for the requested annual period.

Parameters:
aggregate_renta_m100_income_ledger(transactions, invoices=None, *, bucket_id, period, modelo, target_casilla_id, activity_category_matcher, employment_category_matcher)[source]

Aggregate annual activity-income from a TransactionCatalogue into a selected target.

The annual counterpart of aggregate_renta_income_ledger(): it applies the same activity-income eligibility (excluding nómina/trabajo and personal flows) over the FULL ejercicio (Jan 1 to Dec 31 of period.filing_year) and targets the caller-selected annual casilla. period must be the annual period. The InvoiceCatalogue passed as invoices is consulted when classifying each transaction; an empty catalogue is used when it is omitted.

Return type:

RentaIncomeLedgerAggregation

Returns:

The RentaIncomeLedgerAggregation built from eligible transactions.

Parameters:
aggregate_renta_m131_agrario_income_ledger_from_repositories(*, bucket_id, period, modelo, target_casilla_id, agrarian_activity_codes, activity_category_matcher, employment_category_matcher, ports)[source]

Load the catalogue and aggregate a selected activity-narrowed quarter.

Return type:

RentaIncomeLedgerAggregation

Returns:

The RentaIncomeLedgerAggregation for the requested quarter.

Parameters:
aggregate_renta_m131_agrario_income_ledger(transactions, invoices=None, *, bucket_id, period, modelo, target_casilla_id, agrarian_activity_codes, activity_category_matcher, employment_category_matcher)[source]

Aggregate an activity-narrowed quarterly income volume into a target.

Two filters separate this narrowed projection from an un-narrowed path, and both are supplied by the selected registry scope:

  • Activity. Only rows whose declared TipoActividad are in the supplied activity-code set contribute. A row with no declared activity contributes NOTHING here — the opposite of the concept default below, and deliberately so. Silence about activity cannot mean “agrarian”: routing an undeclared row into an activity-narrowed box would misroute a non-matching filer’s income, and the same row may be claimed by another projection. Under-filling a box the operator can still complete by hand is recoverable; mis-routing income between two boxes of one return is not.

  • Concept. Rows whose ConceptoIngreso art. 110.1.c) excludes — subvenciones de capital and indemnizaciones — are dropped. An undeclared concept IS included, because an unmarked receipt is far more likely to be ordinary income than an exceptional one.

The window is the quarter itself rather than a cumulative year-to-date window.

Parameters:
  • transactions (TransactionCatalogue) – The TransactionCatalogue to project.

  • invoices (InvoiceCatalogue | None) – The InvoiceCatalogue consulted when classifying; an empty catalogue when omitted.

  • bucket_id (str) – The active profile bucket.

  • period (Period) – The quarter being filed.

  • modelo (str) – Selected model identifier supplied by the registry boundary.

  • target_casilla_id (TypeAliasType) – Selected output target supplied by the registry boundary.

  • agrarian_activity_codes (frozenset[TipoActividad]) – Selected activity-code set supplied by the registry.

  • activity_category_matcher (Callable[[Transaction], bool]) – Predicate for activity-category transactions.

  • employment_category_matcher (Callable[[Transaction], bool]) – Predicate for employment-category transactions.

Return type:

RentaIncomeLedgerAggregation

Returns:

The RentaIncomeLedgerAggregation for the selected target.

Raises:

AggregationPeriodError – If period is not a quarter.

unusable_sales_invoice_diagnostics(observations, *, resolver_id)[source]

Surface rows whose linked sales invoice could not be trusted.

These rows are NOT excluded – they contribute their bank cash, because the taxpayer was paid and that income is declarable whatever state its paperwork is in. What they lost is the invoice’s base, cuota and retención, so the figure they contribute is the credited cash rather than the ingresos íntegros the casilla asks for. Without this advisory that downgrade is invisible: the row looks exactly like one that never had an invoice at all.

One advisory per refusal reason, not per row: the actionable unit is “these links are unusable, and this is what is wrong with them”, and a per-row advisory trains operators to ignore the channel. The id sample is fitted to the message budget by fitted_diagnostic_id_list(), so a long list degrades to a count instead of raising out of the diagnostic and taking the calculation down with it.

Return type:

tuple[CalculationSourceDiagnostic, ...]

Parameters:
fitted_diagnostic_id_list(identifiers, *, budget)[source]

Render identifiers into at most budget characters, stating omissions.

Shows as many ids as fit alongside the “(and N more)” suffix that describes the ones it dropped – the suffix is part of the budget, because a truncation notice that itself overflows would defeat the cap it exists to respect.

Degrades rather than raises: when even one id plus its suffix cannot fit, it reports the bare count. The caller is an advisory about a measurement risk, so losing the id sample is acceptable where losing the whole diagnostic – and with it the calculation – is not.

Return type:

str

Parameters: