aeat.domain.deadlines._festivos module

Spanish business-day calendar and AEAT deadline shift.

This module is the holiday-adjustment service. It loads BOE- published national plus autonomous-community (“CCAA”) holiday calendars from the project’s registry/aeat/calendars/ directory and exposes pure functions that answer two questions:

  • Is a given date a día hábil (business day) for AEAT filings in the taxpayer’s CCAA of tax residence?

  • Given an AEAT-registered close date, what is the legally-due filing deadline once weekends, national holidays, and CCAA holidays are considered, and what was the reason for the shift?

AEAT’s published deadline-shift rule (Calendario del Contribuyente, “Vencimientos en días inhábiles, sábados o festivos”) is:

“Si el último día del plazo coincide con un sábado, domingo o festivo, el plazo se entiende ampliado hasta el primer día hábil siguiente.”

The rule covers national holidays plus the autonomous-community holiday of the taxpayer’s domicilio fiscal. Local (municipal) holidays do NOT affect AEAT filing deadlines and are not part of this calendar.

One well-known exception: Modelo 369 (OSS / IOSS one-stop-shop) deadlines do NOT shift, even when the close date falls on a non- business day, because the OSS / IOSS regime is governed by the EU Council Directive’s harmonised cutoffs and the AEAT cannot lengthen the EU-wide window unilaterally. The exception list is encoded in MODELOS_WITHOUT_SHIFT so future modelo additions land as data, not as a fork in shift_deadline().

The substrate is pure domain logic: it never touches the CLI, never mutates input, and never reaches outside the project for live calendar data (calendars are git-tracked, BOE-cited TOML).

class CalendarCCAA(*values)[source]

Bases: StrEnum

Spanish autonomous communities and the two autonomous cities, keyed by ISO 3166-2:ES code.

AEAT filing deadlines may shift when the close date coincides with a holiday in the taxpayer’s CCAA of tax residence (domicilio fiscal). The two autonomous cities of Ceuta and Melilla each publish their own holiday calendar and behave like a CCAA for this purpose.

The codes match the ISO 3166-2:ES standard and the BOE-published holiday resolutions.

ANDALUCIA
ARAGON
ASTURIAS
ILLES_BALEARS
CANARIAS
CANTABRIA
CASTILLA_LA_MANCHA
CASTILLA_Y_LEON
CATALUNA
EXTREMADURA
GALICIA
LA_RIOJA
MADRID
MURCIA
NAVARRA
PAIS_VASCO
VALENCIA
CEUTA
MELILLA
class HolidayJurisdiction(*values)[source]

Bases: StrEnum

Layer of government that declared the holiday.

  • NATIONAL — declared by the State; observed everywhere.

  • CCAA — declared by an autonomous community; observed only in that CCAA’s territory.

AEAT does not consider municipal-level holidays for filing-deadline shifts, so a corresponding LOCAL value would be out of scope.

NATIONAL
CCAA
class Holiday(**data)[source]

Bases: BaseModel

A single declared holiday.

Parameters:
  • holiday_date (date)

  • jurisdiction (HolidayJurisdiction)

  • ccaa_code (CalendarCCAA | None)

  • name (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=200, pattern=None, ascii_only=None)])

holiday_date: date
jurisdiction: HolidayJurisdiction
ccaa_code: CalendarCCAA | None
name: _NonEmptyShortString
class HolidayCalendar(**data)[source]

Bases: BaseModel

A year’s BOE-published holiday calendar.

boe_ref is the citation stem of the BOE Resolución that published the annual relación de fiestas laborales. boe_url is an optional convenience anchor for the same resolution.

Parameters:
  • year (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Ge(ge=2000), Le(le=2100)])])

  • boe_ref (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=200, pattern=None, ascii_only=None)])

  • boe_url (str | None)

  • national (tuple[Holiday, ...])

  • ccaa (tuple[Holiday, ...])

year: Annotated[int, Field(ge=2000, le=2100)]
boe_ref: _NonEmptyShortString
boe_url: str | None
national: tuple[Holiday, ...]
ccaa: tuple[Holiday, ...]
class DeadlineShift(**data)[source]

Bases: BaseModel

Outcome of applying the AEAT deadline-shift rule to one close date.

Carries the original AEAT-registered close date, the legally-adjusted close date after weekend / holiday shifts, a structured reason, and references to the holiday source(s) that drove the shift.

shifted is the boolean predicate “did the close date move?”; shift_days is the (always non-negative) day count between original and adjusted; shift_reason is a stable identifier consumers may use for output-formatting and rule-explanation; the optional holiday_refs and jurisdictions tuples carry the specific holidays whose presence on the calendar caused the shift.

Parameters:
  • original_close_date (date)

  • adjusted_close_date (date)

  • shifted (bool)

  • shift_days (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Ge(ge=0)])])

  • shift_reason (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=200, pattern=None, ascii_only=None)])

  • jurisdictions (tuple[HolidayJurisdiction, ...])

  • holiday_refs (tuple[str, ...])

original_close_date: date
adjusted_close_date: date
shifted: bool
shift_days: Annotated[int, Field(ge=0)]
shift_reason: _NonEmptyShortString
jurisdictions: tuple[HolidayJurisdiction, ...]
holiday_refs: tuple[str, ...]
MODELOS_WITHOUT_SHIFT: tuple[str, ...]

Modelos whose deadlines are NOT shifted by the AEAT día-inhábil rule.

Per the AEAT Calendario del Contribuyente, the OSS / IOSS one-stop-shop regime (Modelo 369) is governed by the EU-harmonised cutoff date and AEAT cannot extend the window unilaterally even when the close date falls on a Spanish holiday or weekend. Adding new modelos to this tuple keeps the exception list data-driven; shift_deadline() never grows a switch statement.

load_holiday_calendar(year)[source]

Load the BOE-published holiday calendar for year.

Reads registry/aeat/calendars/festivos-{year}.toml, parses it into immutable Holiday records under the HolidayCalendar aggregate, and caches the result. The cache is unbounded by use but bounded by call site (lru_cache with maxsize=64).

Raises DeadlineValidationError when the TOML file is missing or malformed (caller-recoverable; the surrounding deadline engine can degrade to weekend-only shifts).

Return type:

HolidayCalendar

Parameters:

year (int)

is_business_day(candidate, *, calendar, ccaa_code)[source]

Return True when candidate is a business day for AEAT filings.

A date is a business day when it is not a Saturday, not a Sunday, not a national holiday for that year, and (when ccaa_code is supplied) not a CCAA holiday for that ccaa-year pair. When ccaa_code is None the predicate degrades to national-only: callers with no tax-residence information get weekend + national detection but miss CCAA shifts.

Return type:

bool

Parameters:
next_business_day(start, *, calendar, ccaa_code)[source]

Return the first date on or after start that is a business day.

The walk is bounded by 14 days (a fortnight) to keep the function safe against pathological inputs; in practice the AEAT calendar never strings more than four non-business days together (e.g. Semana Santa weekend + Jueves Santo + Viernes Santo + Lunes de Pascua = 4 days).

Return type:

date

Parameters:
shift_deadline(original_close_date, *, modelo, ccaa_code, calendar=None)[source]

Apply the AEAT deadline-shift rule and return a DeadlineShift result.

The rule moves the deadline to the next business day when the original close date is a Saturday, Sunday, national holiday, or CCAA holiday of the taxpayer’s tax residence.

Modelo-specific exceptions (e.g., Modelo 369 OSS / IOSS) bypass the shift and return an unshifted DeadlineShift with reason modelo_exception.

When calendar is omitted the function loads it via load_holiday_calendar() keyed by the original close date’s year. Callers that already hold the calendar may pass it directly to avoid a second lookup.

Return type:

DeadlineShift

Parameters: