cadrumo.domain.renta.ledger_expenses module

Renta deductible-expense observations derived from ledger facts.

RentaDeductibleExpenseFact is evaluated through evaluate_renta_deductibility() using CategoryProfile and RentaDeductibilityContext; eligible RentaDeductibilityResult values become RentaDeductibleExpenseObservation records routed through the dated facts-registry mapping to CasillaId bindings for Modelo('100').

class RentaExpenseDirection(*values)[source]

Bases: StrEnum

Closed direction values for first-slice Renta expense facts.

OUTGOING_EXPENSE
REFUND
REVERSAL
class RentaDeductibilityStatus(*values)[source]

Bases: StrEnum

Whether a ledger fact can produce a Renta calculation observation.

ELIGIBLE
INELIGIBLE
class RentaInvoiceEvidenceStatus(*values)[source]

Bases: StrEnum

Invoice-evidence state carried by the Renta observation.

NONE
LINKED
class RentaReconciliationStatus(*values)[source]

Bases: StrEnum

Transaction/invoice reconciliation state for duplicate prevention.

TRANSACTION_ONLY
LINKED_INVOICE
class RentaDeductibilityContext(**data)[source]

Bases: _RentaStrictFrozenModel

Inputs that affect proportionality but are not ledger facts.

Parameters:

data (Any)

profile_year: FilingYear
usage_ratios: dict[SpendingCategory, Decimal]
statutory_cap_days: Decimal | None
statutory_cap_variant_id: str | None
statutory_cap_person_count: int
statutory_cap_variant_person_counts: Mapping[str, int]

How many insured persons fall under each annual cap variant, keyed by variant id.

LIRPF art. 30.2.5.a caps the seguro de enfermedad premium at 500 euros per person or 1.500 for a person with discapacidad, so the lawful cap is a SUM over two populations rather than one amount times one count. A single statutory_cap_person_count cannot express that, and using it alone applies the lower limb to everybody – which understates the allowance for exactly the people the higher limb exists for.

exclusive_use_confirmed: bool
iva_deduction_ratio: UnitProportion | None

Fraction of the fact’s input IVA the activity affords a right to deduct (LIVA arts. 94/104).

None (the default) means the axis has not been evaluated for this fact and preserves the historic base-only behaviour: when an invoice-evidenced taxable_base is known, only that net-of-IVA base becomes the IRPF-deductible cost. That is correct for the common case (an activity with full deduction rights recovers its input IVA through Modelo 303, so the IVA is not also an IRPF expense), but it is silently wrong whenever the activity’s right to deduct is less than total: PGC NRV 12.ª treats non-recoverable input IVA as part of the acquisition cost, so the portion the taxpayer cannot deduct for IVA purposes must join the IRPF gasto. Populating this field with the resolved IVA-deduction percentage (1 for full deduction, 0 for a wholly exempt activity with no right to deduct, a fractional value under prorrata general or especial) makes 1 - iva_deduction_ratio of iva_amount join the deductible base. See evaluate_renta_deductibility().

Populated by aggregate_renta_ledger_expenses_from_repositories() for the M100 annual first slice, via application.aggregation.renta_ledger.resolve_iva_deduction_ratio: a wholly EXENTO iva.regime profile fact resolves to 0 outright; otherwise the bucket’s domain.prorrata_register.ProrrataRegister whole-entity entry for the ejercicio contributes its in-force provisional percentage under GENERAL or ESPECIAL. The M130 quarterly pagos fraccionados gasto path (application.aggregation.renta_gasto_ledger) does not construct a RentaDeductibilityContext at all and is not wired to this axis – a named follow-up, not silently covered here.

residence_ccaa: CCAA | None

Optional residence axis consumed by the governed territorial policy.

class RentaDeductibleExpenseFact(**data)[source]

Bases: _RentaStrictFrozenModel

Canonical ledger fact eligible for Renta deductibility evaluation.

Parameters:

data (Any)

transaction_id: TransactionId
invoice_id: str | None
catalogue_id: str
operation_date: date
invoice_issue_date: date | None
posting_date: date | None
payment_date: date | None
gross_amount: Decimal
taxable_base: Decimal | None
iva_amount: Decimal | None
currency: Literal['EUR']
direction: RentaExpenseDirection
category: SpendingCategory
activity_key: str
property filing_date: date

Return the filing date used for first-slice selection.

Prefers invoice_issue_date when present, falling back to operation_date so observations without invoices still get a deterministic anchor.

property sign: Literal[-1, 1]

Return +1 for expenses and -1 for linked corrections.

class RentaDeductibilityResult(**data)[source]

Bases: _RentaStrictFrozenModel

Deductibility evaluation result before registry binding resolution.

Parameters:

data (Any)

transaction_id: TransactionId
invoice_id: str | None
category: SpendingCategory
category_family: SpendingCategoryFamily
profile_year: FilingYear
proportionality_kind: ProportionalityKind
status: RentaDeductibilityStatus
reason: str
gross_amount: Decimal
deductible_amount: Decimal
non_deductible_amount: Decimal
applied_ratio: UnitProportion | None
statutory_cap_applied: Decimal | None
legal_references: tuple[CategoryCitation, ...]
class RentaDeductibleExpenseObservation(**data)[source]

Bases: _RentaStrictFrozenModel

Binding-ready Renta expense observation for the first Modelo 100 slice.

Parameters:

data (Any)

observation_id: str
source_kind: Literal[BindingSourceKind.LEDGER_RENTA_GASTOS_ESTIMACION_DIRECTA_AGGREGATION]
modelo: Literal['100']
period: Literal['0A']
tax_year: FilingYear
activity_key: str
target_casilla_id: CasillaId
transaction_id: TransactionId
invoice_id: str | None
catalogue_id: str
operation_date: date
invoice_issue_date: date | None
posting_date: date | None
payment_date: date | None
filing_date: date
gross_amount: Decimal
taxable_base: Decimal | None
iva_amount: Decimal | None
deductible_amount: Decimal
non_deductible_amount: Decimal
currency: Literal['EUR']
direction: RentaExpenseDirection
sign: Literal[-1, 1]
category: SpendingCategory
category_family: SpendingCategoryFamily
profile_year: FilingYear
proportionality_kind: ProportionalityKind
applied_ratio: UnitProportion | None
invoice_evidence_status: RentaInvoiceEvidenceStatus
reconciliation_status: RentaReconciliationStatus
legal_references: tuple[CategoryCitation, ...]
normalize_spending_category(value)[source]

Normalize persisted category identifiers to closed enum members.

Return type:

SpendingCategory

Returns:

The SpendingCategory member for the given value.

Parameters:

value (SpendingCategory | str)

resolve_region_category_profiles(profile_year)[source]

Return the territorial-regime category-profile overrides for a filing year.

The override layer is provisioned but deliberately empty: the only genuinely region-varying expense-side regimes reach the base through their own dedicated bindings (the Reserva para Inversiones en Canarias, Ley 19/1994 art. 27, is modelled as its own binding, not a SpendingCategory profile), and the Ceuta/Melilla benefit is an art. 68.4 cuota deduction rather than a base-imponible expense rule. No SpendingCategory therefore warrants a per-comunidad deductibility variant today, so this resolver returns an empty mapping and every fact falls through to the state year profile. A future territorial-regime enrollment populates this mapping (grounded to its regime law) with no further architectural change.

Return type:

Mapping[CCAA, Mapping[SpendingCategory, CategoryProfile]]

Returns:

Mapping from CCAA to a per-SpendingCategory override profile mapping; empty until a territorial regime is enrolled.

Parameters:

profile_year (int)

select_deductibility_profile(*, state_profile, region_override_profiles, context)[source]

Select the applicable category profile, honouring territorial-regime overrides.

region_override_profiles holds the per-CCAA overrides declared for the fact’s category (empty for the general state-law case). Selection:

  • no override for the category: return state_profile unchanged (general expense deductibility is state base-imponible law, invariant across comunidades);

  • the category carries an override but context.residence_ccaa is None: return None (D4 fail-closed) so the caller refuses rather than silently choosing a base for an undeclared region;

  • the residence comunidad has an override: return that override profile;

  • the residence comunidad has no override (a different comunidad owns the regime): return state_profile (state law applies to this taxpayer’s region).

Return type:

CategoryProfile | None

Returns:

The selected CategoryProfile, or None when a region override exists for the category but the residence comunidad is undeclared.

Parameters:
evaluate_renta_deductibility(fact, profile, context)[source]

Evaluate one classified ledger fact and return a RentaDeductibilityResult.

deductible_basis (see _deductible_basis_amount()) folds in the non-deductible-for-IVA-purposes share of the fact’s input IVA when context.iva_deduction_ratio states it, so an exempt or prorrata-rationed activity does not lose that real cost.

Return type:

RentaDeductibilityResult

Parameters:
build_renta_deductible_expense_observation(fact, result, *, tax_year)[source]

Build a first-slice Modelo 100 observation from an eligible result.

Return type:

RentaDeductibleExpenseObservation

Returns:

A RentaDeductibleExpenseObservation ready for filing assembly.

Parameters: