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:
StrEnumMachine-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_BASEis 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 theLedgerIncomeGroundingmarker on the observation instead.- UNSUPPORTED_DIRECTION¶
- UNSUPPORTED_CURRENCY¶
- UNCLASSIFIED_BUSINESS_STATE¶
- PERSONAL_TRANSACTION¶
- OUTSIDE_PERIOD¶
- UNSUPPORTED_PERIOD¶
- TRABAJO_INCOME¶
- class RentaIncomeLedgerAggregationIssue(**data)[source]¶
Bases:
BaseModelTraceable 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:
BaseModelOne 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_idagainst the binding selector and sumsgross_amount(ortaxable_base_amount) across all observations for that casilla depending on the declared fact.taxable_base_amountis the IVA-exclusive base imponible from the original invoice (transaction.taxable_base). It feeds thetaxable_base_sumfact path used by the rendimiento-neto binding (casilla 03).Nonewhen the transaction carries no explicittaxable_base.groundingstates which of those two states the row is in, as a fact rather than a nullness heuristic every consumer re-derives. ACASH_FALLBACKrow 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: theingresos_integros_sumfact folds that cash in anyway (deliberately — dropping the row would under-declare by its whole value, strictly worse) whiletaxable_base_sumcontributes 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_jurisdictionpropagates 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:
BaseModelEligible 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_declaredis 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_totalis 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_summaryis populated by repository-backed date partitions. Full-catalogue aggregation keeps row-level issues because every transaction is already loaded for classification.unadmitted_activity_incomeis populated only by a projection that NARROWS rows by activity when requested.Noneon 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:
- Parameters:
bucket_id (str)
period (Period)
modelo (str)
target_casilla_id (CasillaId)
activity_category_matcher (Callable[[Transaction], bool])
employment_category_matcher (Callable[[Transaction], bool])
ports (InvoiceCatalogueReadPorts)
- 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) – TheTransactionCatalogueof ledger transactions to aggregate.invoices (
InvoiceCatalogue|None) – The bucket’sInvoiceCatalogue, 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 quarterlyPeriodwhose 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:
Returns a
RentaIncomeLedgerAggregationcovering the cumulative fiscal window.periodmust 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:
- Returns:
The
RentaIncomeLedgerAggregationfor the requested annual period.- Parameters:
bucket_id (str)
period (Period)
modelo (str)
target_casilla_id (CasillaId)
activity_category_matcher (Callable[[Transaction], bool])
employment_category_matcher (Callable[[Transaction], bool])
ports (InvoiceCatalogueReadPorts)
- 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
TransactionCatalogueinto 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 ofperiod.filing_year) and targets the caller-selected annual casilla.periodmust be the annual period. TheInvoiceCataloguepassed asinvoicesis consulted when classifying each transaction; an empty catalogue is used when it is omitted.- Return type:
- Returns:
The
RentaIncomeLedgerAggregationbuilt from eligible transactions.- Parameters:
transactions (TransactionCatalogue)
invoices (InvoiceCatalogue | None)
bucket_id (str)
period (Period)
modelo (str)
target_casilla_id (CasillaId)
activity_category_matcher (Callable[[Transaction], bool])
employment_category_matcher (Callable[[Transaction], bool])
- 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:
- Returns:
The
RentaIncomeLedgerAggregationfor the requested quarter.- Parameters:
bucket_id (str)
period (Period)
modelo (str)
target_casilla_id (CasillaId)
agrarian_activity_codes (frozenset[TipoActividad])
activity_category_matcher (Callable[[Transaction], bool])
employment_category_matcher (Callable[[Transaction], bool])
ports (InvoiceCatalogueReadPorts)
- 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
TipoActividadare 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
ConceptoIngresoart. 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) – TheTransactionCatalogueto project.invoices (
InvoiceCatalogue|None) – TheInvoiceCatalogueconsulted 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:
- Returns:
The
RentaIncomeLedgerAggregationfor the selected target.- Raises:
AggregationPeriodError – If
periodis 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:
- Parameters:
observations (Sequence[RentaIncomeObservation])
resolver_id (str)
- fitted_diagnostic_id_list(identifiers, *, budget)[source]¶
Render
identifiersinto at mostbudgetcharacters, 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.