Source code for aeat.domain.categories._proportionality
"""Proportionality and explainability primitives for category profiles.Defines the closed enums and strict pydantic models that encode howa spending category is deducted on the autónomo filings, plus thecitation chain back to the relevant authority that makes each ruleexplainable. Every :class:`ProportionalityRule` carries at least one:class:`CategoryCitation`; the consistency rules between``kind``-specific fields (``fixed_pct``, ``default_ratio``,``statutory_cap_*``) are enforced by the model validator."""from__future__importannotationsfromdecimalimportDecimalfromenumimportStrEnumfrompydanticimportAnyHttpUrl,BaseModel,Field,TypeAdapter,model_validatorfrom...coreimportSTRICT_FROZEN_CONFIGfrom...core.i18nimportTranslatableastrfrom._errorsimportCategoryValidationErrorclass_ProportionalityStrictFrozenModel(BaseModel):"""Shared strict immutable boundary model."""model_config=STRICT_FROZEN_CONFIGdef_require_translatable_text(value:tr,field_name:str)->None:"""Assert a translatable authority field contains non-blank text."""ifnotstr(value).strip():raiseCategoryValidationError(f"{field_name} must contain authoritative Spanish text")
[docs]classCategoryCitationSource(StrEnum):"""Allowed citation sources for explainable category profiles. Attributes: MANUAL_RENTA: AEAT *Manual práctico Renta*. MANUAL_IVA: AEAT *Manual práctico IVA*. LEY_IRPF: Ley del Impuesto sobre la Renta de las Personas Físicas. REGLAMENTO_IRPF: Reglamento del IRPF. AEAT_HELP: AEAT online help / portal text. """MANUAL_RENTA="manual_renta"MANUAL_IVA="manual_iva"LEY_IRPF="ley_irpf"REGLAMENTO_IRPF="reglamento_irpf"AEAT_HELP="aeat_help"
[docs]classCategoryCitation(_ProportionalityStrictFrozenModel):"""Traceable citation backing one category or proportionality rule. Attributes: source: Originating :class:`CategoryCitationSource`. reference: Human-readable document reference (title, edition, BOE number). locator: Section, article, or page locator within the referenced document. url: Canonical URL where the citation can be checked. quote: Authoritative Spanish-language quote backing the rule. """source:CategoryCitationSourcereference:str=Field(min_length=1,max_length=256)locator:str=Field(min_length=1,max_length=256)url:AnyHttpUrlquote:tr=Field(description="Authoritative Spanish-language quote.")@model_validator(mode="after")def_validate_quote(self)->CategoryCitation:_require_translatable_text(self.quote,"category citation quote")returnself
_HTTP_URL_ADAPTER=TypeAdapter(AnyHttpUrl)
[docs]defparse_http_url(value:str)->AnyHttpUrl:"""Parse a string into a statically typed :class:`AnyHttpUrl`. Args: value: Raw HTTP / HTTPS URL string. Returns: A validated :class:`pydantic.AnyHttpUrl`. """return_HTTP_URL_ADAPTER.validate_python(value)
[docs]classProportionalityKind(StrEnum):"""Supported proportionality kinds for downstream evaluator engines. Attributes: FULL_DEDUCTIBLE: Fully deductible against the activity. FIXED_PERCENTAGE: Deductible at a fixed percentage; requires ``fixed_pct``. USAGE_RATIO_PERSONAL: Deductible at a personal-usage ratio chosen by the taxpayer; may carry ``default_ratio``. USAGE_RATIO_HOME_AREA: Deductible at the home-office area ratio; may carry ``default_ratio``. STATUTORY_CAP: Capped by a statutory daily or annual limit; requires the matching ``statutory_cap_*`` fields. NON_DEDUCTIBLE: Not deductible against the activity. """FULL_DEDUCTIBLE="full_deductible"FIXED_PERCENTAGE="fixed_percentage"USAGE_RATIO_PERSONAL="usage_ratio_personal"USAGE_RATIO_HOME_AREA="usage_ratio_home_area"STATUTORY_CAP="statutory_cap"NON_DEDUCTIBLE="non_deductible"REQUIRES_EXCLUSIVE_USE="requires_exclusive_use"
[docs]classStatutoryCapPeriod(StrEnum):"""Supported statutory-cap accounting periods. Attributes: DAY: Cap applies per day. YEAR_PER_PERSON: Cap applies per year per covered person. """DAY="day"YEAR_PER_PERSON="year_per_person"
[docs]classStatutoryCapVariant(_ProportionalityStrictFrozenModel):"""One legally distinct daily cap inside a statutory-cap rule."""id:str=Field(min_length=1,max_length=64)label:tr=Field(description="Human-readable label.")statutory_cap_eur_per_day:Decimal=Field(ge=Decimal("0"))@model_validator(mode="after")def_validate_label(self)->StatutoryCapVariant:_require_translatable_text(self.label,"statutory cap variant label")returnself
[docs]classProportionalityRule(_ProportionalityStrictFrozenModel):"""Deductibility and proportionality rule for one spending category. Attributes: kind: One of :class:`ProportionalityKind`. fixed_pct: Required when ``kind`` is :attr:`ProportionalityKind.FIXED_PERCENTAGE`; otherwise must be ``None``. default_ratio: Optional default usage ratio; only valid for usage-ratio kinds. statutory_multiplier: Optional statutory factor applied on top of the operator-chosen usage ratio. Only valid for usage-ratio kinds. The canonical example is the LIRPF Art. 30.2 rule 5 (Ley 6/2017, BOE-A-2017-12544) 0.30 multiplier applied to suministros (utility) costs of the habitual vivienda when the operator deducts under estimacion directa: ``effective_deductible_pct = operator_chosen_ratio * statutory_multiplier``. When ``None`` no statutory factor is applied (equivalent to ``Decimal("1")``); the operator's chosen ratio is the effective deductible percentage. statutory_cap_eur_per_day: Daily statutory cap; only valid for :attr:`ProportionalityKind.STATUTORY_CAP`. statutory_cap_eur: Generic statutory cap amount; only valid for :attr:`ProportionalityKind.STATUTORY_CAP` and must be paired with :attr:`statutory_cap_period`. statutory_cap_period: :class:`StatutoryCapPeriod` that the generic cap applies over; required when :attr:`statutory_cap_eur` is set. statutory_cap_variants: Daily statutory caps selected by a legally relevant condition. citations: At least one :class:`CategoryCitation` proving the rule. notes: Authoritative Spanish-language notes describing the rule. """kind:ProportionalityKindfixed_pct:Decimal|None=Field(default=None,ge=Decimal("0"),le=Decimal("1"))default_ratio:Decimal|None=Field(default=None,ge=Decimal("0"),le=Decimal("1"))statutory_multiplier:Decimal|None=Field(default=None,ge=Decimal("0"),le=Decimal("1"))statutory_cap_eur_per_day:Decimal|None=Field(default=None,ge=Decimal("0"))statutory_cap_eur:Decimal|None=Field(default=None,ge=Decimal("0"))statutory_cap_period:StatutoryCapPeriod|None=Nonestatutory_cap_variants:tuple[StatutoryCapVariant,...]=Field(default_factory=tuple)citations:tuple[CategoryCitation,...]=Field(default_factory=tuple)notes:tr=Field(description="Authoritative Spanish-language notes.")@model_validator(mode="after")def_validate_shape(self)->ProportionalityRule:ifnotself.citations:raiseCategoryValidationError("proportionality rules require at least one citation")_require_translatable_text(self.notes,"proportionality rule notes")self._validate_fixed_percentage_invariants()self._validate_usage_ratio_invariants()ifself.kindisProportionalityKind.STATUTORY_CAP:self._validate_statutory_cap_invariants()else:self._reject_statutory_cap_fields_outside_cap_kind()returnselfdef_validate_fixed_percentage_invariants(self)->None:"""``fixed_pct`` is required for FIXED_PERCENTAGE rules and forbidden elsewhere."""ifself.kindisProportionalityKind.FIXED_PERCENTAGEandself.fixed_pctisNone:raiseCategoryValidationError("fixed_percentage rules require fixed_pct")ifself.kindisnotProportionalityKind.FIXED_PERCENTAGEandself.fixed_pctisnotNone:raiseCategoryValidationError("fixed_pct is only valid for fixed_percentage rules")def_validate_usage_ratio_invariants(self)->None:"""``default_ratio`` and ``statutory_multiplier`` are only valid on usage-ratio rules."""is_usage_ratio=self.kindin{ProportionalityKind.USAGE_RATIO_HOME_AREA,ProportionalityKind.USAGE_RATIO_PERSONAL,}ifnotis_usage_ratioandself.default_ratioisnotNone:raiseCategoryValidationError("default_ratio is only valid for usage_ratio rules")ifnotis_usage_ratioandself.statutory_multiplierisnotNone:raiseCategoryValidationError("statutory_multiplier is only valid for usage_ratio rules",)def_validate_statutory_cap_invariants(self)->None:"""STATUTORY_CAP rules require exactly one cap mode and a coherent (eur, period) pair."""has_daily_cap=self.statutory_cap_eur_per_dayisnotNonehas_generic_cap=self.statutory_cap_eurisnotNoneorself.statutory_cap_periodisnotNonehas_variant_caps=bool(self.statutory_cap_variants)ifnothas_daily_capandnothas_generic_capandnothas_variant_caps:raiseCategoryValidationError("statutory_cap rules require a cap amount")mode_count=sum((has_daily_cap,has_generic_cap,has_variant_caps))ifmode_count>1:raiseCategoryValidationError("statutory cap rules must use one cap mode")ifself.statutory_cap_eurisNoneandself.statutory_cap_periodisnotNone:raiseCategoryValidationError("statutory_cap_period requires statutory_cap_eur")ifself.statutory_cap_eurisnotNoneandself.statutory_cap_periodisNone:raiseCategoryValidationError("statutory_cap_eur requires statutory_cap_period")variant_ids=[variant.idforvariantinself.statutory_cap_variants]iflen(set(variant_ids))!=len(variant_ids):raiseCategoryValidationError("statutory cap variant ids must be unique")def_reject_statutory_cap_fields_outside_cap_kind(self)->None:"""Every statutory-cap field is forbidden on non-STATUTORY_CAP kinds."""ifself.statutory_cap_eur_per_dayisnotNone:raiseCategoryValidationError("statutory_cap_eur_per_day is only valid for statutory_cap rules")ifself.statutory_cap_eurisnotNone:raiseCategoryValidationError("statutory_cap_eur is only valid for statutory_cap rules")ifself.statutory_cap_periodisnotNone:raiseCategoryValidationError("statutory_cap_period is only valid for statutory_cap rules")ifself.statutory_cap_variants:raiseCategoryValidationError("statutory_cap_variants are only valid for statutory_cap rules")
[docs]defeffective_usage_ratio(rule:ProportionalityRule,chosen_ratio:Decimal)->Decimal:"""Return the legally-effective deductible percentage for ``chosen_ratio``. Applies the rule's ``statutory_multiplier`` on top of the operator- chosen usage ratio. Only meaningful for usage-ratio kinds; raises when called on a non-usage-ratio rule (the caller is responsible for routing rules to the right evaluator). Args: rule: A :class:`ProportionalityRule` of a usage-ratio kind. chosen_ratio: The operator's stored usage ratio (typically derived from censo ``office_m2 / total_m2`` for HOME_AREA kinds, or a personal-use proportion for PERSONAL kinds). Returns: ``chosen_ratio * (rule.statutory_multiplier or Decimal("1"))``. Raises: CategoryValidationError: When ``rule.kind`` is not a usage-ratio kind. """ifrule.kindnotin{ProportionalityKind.USAGE_RATIO_HOME_AREA,ProportionalityKind.USAGE_RATIO_PERSONAL,}:raiseCategoryValidationError(f"effective_usage_ratio is only valid for usage_ratio rules; got {rule.kind}",)multiplier=rule.statutory_multiplierifrule.statutory_multiplierisnotNoneelseDecimal("1")returnchosen_ratio*multiplier