"""Art. 81.2 guardería contribution behavior for descendants."""
from __future__ import annotations
from .descendant_maternity import DescendantMaternityMixin
from .family_fact_context import FamilyFactResolutionContext
from .family_types import (
MinimoDescendientesThresholds,
months_of_year_between,
)
[docs]
class DescendantGuarderiaMixin(DescendantMaternityMixin):
"""The guarderia spend facts a descendant carries."""
[docs]
def guarderia_art_81_1_meses(
self,
filing_year: int,
*,
thresholds: MinimoDescendientesThresholds,
context: FamilyFactResolutionContext,
dependencia_assimilation_available: bool = False,
) -> int:
"""Art. 81.1 months the guardería increment prorates by in *filing_year*.
The increment is prorated by "el número de meses en que se cumplan de
forma simultánea los requisitos exigidos en el artículo 81.1 y 2", and
this answers the 81.1 half FOR THE INCREMENT — which is not the same
question as :meth:`maternidad_contributing_meses`, even though every
gate below is shared with it.
Named for the ARTICLE HALF it answers, not for simultaneity, because it
is only one side of the pair: :meth:`guarderia_simultaneous_meses` is
where the two sides actually meet. The plain "simultaneity" name once sat
here, one word away from that method, and the two were mistaken for each
other by a reader holding the source open.
The difference is the child's AGE, and it is the whole reason this
method exists. The deducción itself runs only while the child is under
three, so that method clips to :meth:`maternidad_eligible_meses`. The
increment does not: Capítulo 18 states that where "el descendiente
cumpla los tres años en el mes de enero" or "la madre comience a
trabajar en el año en el que el hijo cumple esa edad, pero después de
haberla cumplido", then "no se podrá aplicar la deducción por
maternidad, si bien ello no impedirá aplicar el incremento". Reusing the
deducción's own month count therefore forces the increment to zero in
exactly the two cases the authority names as still qualifying.
So the age CEILING is dropped and everything else is kept. The relación
gate, the ordinary-mínimo test and the mother's own declared months all
still bind, because the article's remaining requirements are hers and the
child's entitlement to the mínimo — neither of which the birthday ends.
The FLOOR is kept too: no month may precede the child being this
taxpayer's, which is the entitling event rather than the age limb.
Dropping only the ceiling cannot over-grant on its own, because the
Art. 81.2 side bounds the pair and already returns ``0`` for every child
past the period they turn three
(:meth:`guarderia_qualifying_meses`).
"""
if self.relacion not in self.art_81_1_maternity_relations(context=context):
return 0
if not self.is_eligible_ordinary(
filing_year,
thresholds=thresholds,
context=context,
dependencia_assimilation_available=dependencia_assimilation_available,
):
return 0
return len(self._guarderia_art_81_1_months(filing_year, context=context))
def _guarderia_art_81_1_months(self, filing_year: int, *, context: FamilyFactResolutionContext) -> frozenset[int]:
"""The Art. 81.1 months, clipped to the increment's own requirement window."""
return frozenset(self.meses_madre_trabajo) & self._guarderia_requirement_months(filing_year, context=context)
[docs]
def guarderia_simultaneous_meses(
self,
filing_year: int,
*,
thresholds: MinimoDescendientesThresholds,
context: FamilyFactResolutionContext,
dependencia_assimilation_available: bool = False,
) -> int:
"""Months in which the Art. 81.1 AND Art. 81.2 requirements BOTH held.
The proration basis the manual describes — "el número de meses en que se
cumplan de forma simultánea los requisitos exigidos en el artículo 81.1
y 2" — computed as what it says: an intersection of two month sets.
The geometry is the whole point. Two sets of the same SIZES may share
every month, some, or none, and the three have different correct
answers; a ``min`` over their counts returns the same number for all
three. AEAT's caso a is the partial case — mother May to August, nursery
January to June, two shared months, ``1.000 ÷ 12 × 2 = 166,67`` — where
the count reading yields four and 333,33, over-granting the deducción
and so under-declaring tax.
Falls back to the count ONLY where the Art. 81.2 months are genuinely
unknown, which is the annual-total shape
(:meth:`guarderia_qualifying_months` returning ``None``). There the
record holds no month information to intersect, so a bound is the honest
answer rather than a discarded one, and it is disclosed to the operator
by the monthly-detail advisory rather than presented as measured.
"""
mother = self.guarderia_art_81_1_meses(
filing_year,
thresholds=thresholds,
context=context,
dependencia_assimilation_available=dependencia_assimilation_available,
)
if mother <= 0:
return 0
nursery = self.guarderia_qualifying_months(filing_year, context=context)
if nursery is None:
return min(mother, self.guarderia_qualifying_meses(filing_year, context=context))
return len(self._guarderia_art_81_1_months(filing_year, context=context) & nursery)
def _guarderia_requirement_months(
self, filing_year: int, *, context: FamilyFactResolutionContext
) -> frozenset[int]:
"""The Art. 81.1 requirement months for the increment: the deducción window without its age ceiling."""
entitling = months_of_year_between(
(self.birth_date.year, self.birth_date.month),
(filing_year + 1, 1),
filing_year,
) | self._maternidad_entry_window_months(filing_year, context=context)
anchor = self.art_58_2_entry_date()
if anchor is None:
return entitling
return frozenset(month for month in entitling if (filing_year, month) >= (anchor.year, anchor.month))
[docs]
def maternidad_alta_posterior_increment_applies(
self, filing_year: int, *, context: FamilyFactResolutionContext
) -> bool:
"""Whether Art. 81.1's post-birth alta increment applies to this child in *filing_year*.
Two conditions, both the operator's to supply and neither this method's
to infer: a completion month must be declared
(``alta_posterior_nacimiento_mes``), and *filing_year* must be at or
after the ``lirpf-art-81-maternity-post-birth-enrollment-effective-year``
governed fact — the route did not exist before it, so a month recorded against an
earlier filing carries no increment.
Does not itself re-check :meth:`maternidad_contributing_meses`'s
eligibility gate: a caller only consults this for a ``hijo_id`` already
present in that method's contributing pairs, exactly as
:meth:`meses_maternidad_por_descendiente` does.
"""
return self.alta_posterior_nacimiento_mes is not None and filing_year >= context.integer(
"lirpf-art-81-maternity-post-birth-enrollment-effective-year"
)
[docs]
def guarderia_contributing_spend(self, filing_year: int, *, context: FamilyFactResolutionContext) -> int:
"""Art. 81.2 guardería spend this descendant contributes in *filing_year*.
The THIRD BIRTHDAY IS NOT A BOUNDARY HERE, and that is the whole subtlety
of this method. Capítulo 18 says "En el período impositivo en que el hijo
menor cumpla tres años, el incremento podrá resultar de aplicación
respecto de los gastos incurridos con posterioridad al cumplimiento de
dicha edad hasta el mes anterior a aquel en el que pueda comenzar el
segundo ciclo de educación infantil." That sentence GRANTS the months
after the birthday, which the under-three limb could not otherwise
reach; it does not withdraw the months before it. Read as a restriction
instead, it drops every pre-birthday month and hands a taxpayer zero on
facts the authority works to a positive figure.
The manual settles it with its own worked case rather than by wording:
an elder child who "en septiembre cumple 3 años" is granted the
increment over "6 meses completos (de enero a junio)", every one of them
BEFORE the September birthday, for ``1.000 ÷ 12 × 6 = 500``. A
post-birthday-only reading returns zero on exactly those facts, so the
oracle decides the question the prose leaves open.
So the whole period counts, and the declared months are taken as
declared. The UPPER bound is still not derived, and that remains a
deliberate non-derivation rather than an oversight. The statute ends the
extension at the month before the second cycle of infant education may
begin, which each region determines. The informative return reporting
childcare custody is filed EXCLUSIVELY by the centre, never by the
taxpayer, and the centre is required to report exactly those months.
Re-deriving the boundary here would compute, from a calendar this
application does not hold, a determination the law assigns to a party who
does — and risk contradicting the return the authority already holds from
that party. So the months a taxpayer can evidence are taken as the months
the centre determined, and that evidence is the monthly detail.
Returns ``0`` for a non-cohabiting descendant, and for the turning-three
period when only an ANNUAL total is on record. The reason for that zero
is the UPPER edge, not the birthday: a single yearly figure cannot be
apportioned to a window whose closing month this application declines to
derive. It is an unanswerable question about a figure the operator can
replace with the monthly detail their centre already certified, and
:meth:`guarderia_needs_monthly_detail` reports it so they are asked.
"""
if not self.convive_con_contribuyente:
return 0
age_at_year_end = self.age_at_year_end(filing_year)
maximum_age = context.integer("lirpf-art-58-under-three-maximum-age")
if age_at_year_end > maximum_age:
return 0
if self.gastos_guarderia_mensuales:
# Every declared month counts in both periods: under three the child
# qualifies throughout, and in the turning-three period the birthday
# draws no line.
return sum(entry.amount_euros for entry in self.gastos_guarderia_mensuales)
if age_at_year_end < maximum_age:
# An annual total needs no apportioning while the child is under
# three for the whole period.
return self.gastos_guarderia_euros
return 0
[docs]
def guarderia_qualifying_meses(self, filing_year: int, *, context: FamilyFactResolutionContext) -> int:
"""Art. 81.2 qualifying MONTH count for this descendant in *filing_year*.
The proration basis for the increment's per-child cap, which the manual
states "se calculará proporcionalmente al número de meses en que se
cumplan de forma simultánea los requisitos exigidos en el artículo 81.1
y 2" and works as ``1.000 / 12 x meses``. This method answers the 81.2
half; the caller intersects it with the 81.1 half.
The month SELECTION mirrors :meth:`guarderia_contributing_spend` exactly
rather than being re-derived, and that is deliberate: the two answer the
same question about the same months, one in euros and one in count, so a
second derivation is how a spend month and a proration month come to
disagree. Any change to the month rules belongs in both or in neither.
An ANNUAL TOTAL carries no month information, so the count falls back to
the months the child was AGE-ELIGIBLE — alive and under three within the
period. That is an approximation and is the one place this method
returns a number the operator did not evidence. It is chosen over the
two alternatives on the grounds that it invents nothing: assuming twelve
reinstates the flat cap this proration exists to remove, and refusing
outright drops the increment entirely for what is likely the commonest
declaration shape. The approximation is disclosed to the operator rather
than presented as a measured result.
It is the ANNUAL TOTAL that keeps it an approximation, and nothing else.
The Art. 81.1 side now stores which months the mother qualified, so the
simultaneity intersection is real wherever the nursery months are known;
against an annual total there is nothing on THIS side to intersect with,
and the caller falls back to a bound that cannot see the geometry at all.
A mother qualifying January to June and one qualifying July to December
produce the same figure against the same annual total, which is exactly
the blindness the month sets removed everywhere the months are declared.
Only the operator can close it, by declaring the monthly detail their
centre already certified; :meth:`guarderia_needs_monthly_detail` exists
to ask them for it.
Returns ``0`` for a non-cohabiting descendant and for a child past the
period they turn three, matching the spend method's own zeroes — and,
in the turning-three period with only an annual total, the same zero the
spend method returns, for the same upper-edge reason. The age-eligible
fallback is NOT reached there: it counts months under three, which in
that period is a strictly narrower window than the increment's own, so
using it would prorate by a basis the spend it pairs with does not use.
"""
if not self.convive_con_contribuyente:
return 0
age_at_year_end = self.age_at_year_end(filing_year)
maximum_age = context.integer("lirpf-art-58-under-three-maximum-age")
if age_at_year_end > maximum_age:
return 0
if self.gastos_guarderia_mensuales:
return len(self.gastos_guarderia_mensuales)
if age_at_year_end < maximum_age:
return self.age_eligible_guarderia_meses(filing_year, context=context)
return 0
[docs]
def guarderia_qualifying_months(
self, filing_year: int, *, context: FamilyFactResolutionContext
) -> frozenset[int] | None:
"""WHICH months this descendant's declared nursery spend covers, or ``None``.
The Art. 81.2 half of the simultaneity intersection, as months rather
than as their number. ``None`` means the months are genuinely unknown —
an ANNUAL total carries no month information at all — and is distinct
from an empty set, which means no month qualifies.
Callers that receive ``None`` cannot intersect and must fall back to the
count, which is a bound rather than an answer. That bound is honest
there precisely because nothing in the record says which months the
yearly figure covered; it is not the count-based reading this method
exists to replace, which discarded month information the record DID
hold.
"""
if not self.convive_con_contribuyente:
return frozenset[int]()
if self.age_at_year_end(filing_year) > context.integer("lirpf-art-58-under-three-maximum-age"):
return frozenset[int]()
if self.gastos_guarderia_mensuales:
declared = frozenset(entry.month for entry in self.gastos_guarderia_mensuales)
return declared & self._segundo_ciclo_window(filing_year, context=context)
return None
def _segundo_ciclo_window(self, filing_year: int, *, context: FamilyFactResolutionContext) -> frozenset[int]:
"""Months the Art. 81.2 gastos may fall in, bounded by the second-cycle ceiling.
Art. 81.2 admits the gastos "hasta el mes anterior a aquel en el que PUEDA
comenzar el segundo ciclo de educación infantil", and that ceiling applies
ONLY in the período impositivo in which the child turns three. Outside that
period every month is open: the AEAT 2020 caso works a two-year-old's
NON-CONTIGUOUS nursery months — January to June plus OCTOBER and NOVEMBER —
to eight months, so a ceiling applied generally would drop two months the
authority counts.
The month is the OPERATOR's to declare and is never inferred. AEAT writes
"pueda comenzar" in the subjunctive in every year of the manual: the
authority itself declines to fix it, because when the cycle may begin
depends on the child's own región and centre. September is the month its
worked examples happen to use, and defaulting to it would be exactly the
confident month-selection rule this box has already been wrong with once.
Undeclared, this returns EMPTY rather than open: the turning-three window
cannot be computed, so the increment declines to grant it and the operator
is told what to supply
(:meth:`guarderia_needs_segundo_ciclo_month`). Refusing under-grants, which
over-taxes; opening would over-grant, which under-declares. Between two
wrong answers the recoverable one is the one the operator can see and fix.
"""
if self.age_at_year_end(filing_year) != context.integer("lirpf-art-58-under-three-maximum-age"):
return frozenset(range(1, 13))
if self.segundo_ciclo_infantil_inicio_mes is None:
return frozenset[int]()
return frozenset(range(1, self.segundo_ciclo_infantil_inicio_mes))
[docs]
def age_eligible_guarderia_meses(self, filing_year: int, *, context: FamilyFactResolutionContext) -> int:
"""Months of *filing_year* in which this descendant was alive and under three.
Computable from the birth date alone, which is why the Art. 81.2 side of
the proration never needed a stored month set the way the Art. 81.1 side
does. A child born within the period is eligible from their birth month
inclusive; one born earlier and still under three at year-end is eligible
for the whole twelve.
TOTAL over every input rather than correct only where it happens to be
called. The sole production caller —
:meth:`guarderia_qualifying_meses`'s annual-total fallback — reaches it
under an "under three at year-end" guard, and that guard places the
third birthday after the period, so the under-three ceiling below is
inert on that path and only the birth month moves the answer. Deriving
the ceiling regardless keeps the result true of the method's NAME rather
than of one call site's guard, so that guard can move without silently
changing the number; the ceiling is exercised by direct tests rather
than through the fallback.
Public for the same reason: it is the basis
:meth:`guarderia_qualifying_meses` approximates by when only an annual
total is on record, and the operator is told that is what the count
rests on, so it is separately addressable and separately tested rather
than observable only through that one branch.
The month the child turns three is EXCLUDED, because this counts
UNDER-THREE months and that month is not one. That makes this window
strictly NARROWER than the one :meth:`guarderia_contributing_spend`
applies in the turning-three period, where the birthday draws no line at
all and every declared month counts. The two coincide only where this
method is actually consulted — a child under three for the whole period
— which is precisely why the fallback is confined to that branch. Using
it in the turning-three period would prorate by an under-three basis
while the spend it pairs with was measured on a wider window.
"""
if self.birth_date.year > filing_year:
return 0
first_month = self.birth_date.month if self.birth_date.year == filing_year else 1
third_birthday_year = self.birth_date.year + context.integer("lirpf-art-58-under-three-maximum-age")
if third_birthday_year < filing_year:
return 0
# In the year the child turns three they are under three only until the
# birthday month, which is itself excluded. This is an AGE boundary, not
# the increment's: ``guarderia_contributing_spend`` draws no line at the
# birthday in that period.
last_month = self.birth_date.month - 1 if third_birthday_year == filing_year else 12
return max(0, last_month - first_month + 1)
[docs]
def guarderia_needs_monthly_detail(self, filing_year: int, *, context: FamilyFactResolutionContext) -> bool:
"""True when only an annual total is on record for the turning-three period.
The one state where declared spend contributes nothing purely because of
its SHAPE. Reported so the operator is told to supply the monthly
breakdown their centre certified, rather than left to wonder why a
declared figure produced no increase.
"""
if not self.convive_con_contribuyente:
return False
if self.age_at_year_end(filing_year) != context.integer("lirpf-art-58-under-three-maximum-age"):
return False
return self.gastos_guarderia_euros > 0 and not self.gastos_guarderia_mensuales
[docs]
def guarderia_needs_segundo_ciclo_month(self, filing_year: int, *, context: FamilyFactResolutionContext) -> bool:
"""True when declared spend is withheld only for want of the second-cycle month.
Fires on exactly the population the ceiling governs: a cohabiting child
turning three in this período impositivo, with monthly nursery spend on
record and no declared month for when the second cycle may begin. There
the window is unknowable and the increment declines to grant it, so the
operator must be told which fact unlocks it — their centre files the
modelo 233 informative return, so the month is documented for them.
Deliberately silent for a child not turning three (no ceiling applies at
all) and for the annual-total shape, which
:meth:`guarderia_needs_monthly_detail` already reports for a different
reason. Two advisories on one filing for one cause is noise, and noise is
what teaches operators to stop reading the channel.
"""
if not self.convive_con_contribuyente:
return False
if self.age_at_year_end(filing_year) != context.integer("lirpf-art-58-under-three-maximum-age"):
return False
if not self.gastos_guarderia_mensuales:
return False
return self.segundo_ciclo_infantil_inicio_mes is None
[docs]
def is_eligible_guarderia(self, filing_year: int, *, context: FamilyFactResolutionContext) -> bool:
"""True when this descendant may carry an Art. 81.2 guardería increase at all.
Wider than :meth:`is_eligible_menor_tres`, which tests age under three at
year end and is the Art. 81.1 maternidad population. The guardería
increase additionally reaches the period the child TURNS three. Getting
that boundary wrong costs a full birth cohort rather than a minority
case, and the increase reduces cuota directly rather than the base, so
the error lands on tax owed at full value.
The authority is explicit that the increase is not gated on the
maternidad deduction's own eligibility — where the child turns three in
January, or the mother starts work after the birthday, the deduction does
not apply and that does NOT prevent the increase. Hence a separate
predicate rather than a widened shared one.
"""
if not self.convive_con_contribuyente:
return False
return self.age_at_year_end(filing_year) <= context.integer("lirpf-art-58-under-three-maximum-age")
[docs]
def is_eligible_minimo_incremento_menor_tres(
self, filing_year: int, *, context: FamilyFactResolutionContext
) -> bool:
"""True when Art. 58.2 grants the bajo-3-años increase for this descendant.
Two independent limbs, and the second is why this is separate from
:meth:`is_eligible_menor_tres` rather than shared with it:
* the ordinary limb — under three at the devengo instant Art. 61
norma 3ª fixes; and
* the adopción/acogimiento limb, granted "con independencia de la edad
del menor, en el período impositivo en que se inscriba en el Registro
Civil y en los dos siguientes".
The second limb is PERIOD-scoped: whole tax periods counted from the
entry period, so annual granularity suffices and no month-level figure
is required. Art. 81.1's adoption clause instead runs "durante los tres
años siguientes a la FECHA de la inscripción" — date-scoped, a
different shape. One predicate serving both would silently apply one
statute's window to the other's deduction, which is why the two are
resolved apart.
The second limb reads ``relacion`` rather than the presence of a date,
which is what makes the statute's three-way split expressible. Art. 58.1
assimilates "tutela y acogimiento"; Art. 58.2 names "adopción o
acogimiento, tanto preadoptivo como permanente". So a tutela guardian
and a TEMPORAL acogimiento carer both take the ordinary tranches and
neither takes this increase, while a preadoptivo or permanente carer
takes both. Keying the limb on a date alone would grant it to whoever
recorded one, which is the over-grant the relación axis exists to close.
The anchor is :meth:`art_58_2_entry_date` — the FIRST entitling event —
so the three periods cap a fostered-then-adopted child's window rather
than restarting on the adoption.
"""
if not self.convive_con_contribuyente:
return False
if self.age_at_year_end(filing_year) < context.integer("lirpf-art-58-under-three-maximum-age"):
return True
entry_date = self.art_58_2_entry_date()
if entry_date is None:
return False
periods_since_entry = filing_year - entry_date.year
return 0 <= periods_since_entry <= context.integer("madrid-birth-adoption-following-periods")