Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import Optional
from .dates_add_days import add_days, days_between, parse_iso_date ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_add_months import add_months ← from dates.add-months ^1.0.0 · built alongside by fune
from .math_round_div import round_div ← from math.round-div ^1.0.0 · built alongside by fune
from .payroll_holiday_entitlement_data import HOLIDAY_RULES, HOLIDAY_RULES_HISTORY, HolidayRule ← this capability’s own data, compiled from data/holiday-rules.json into the same file by fune build
from .payroll_holiday_entitlement_types import HolidayEntitlement, HolidayWorker
def _rule_for(basis: str, leave_year_start: str) -> HolidayRule:
best: Optional[HolidayRule] = None
for rule in HOLIDAY_RULES:
if rule.basis != basis or leave_year_start < rule.valid_from:
continue
if rule.valid_to is not None and leave_year_start > rule.valid_to:
continue
if best is None or rule.valid_from > best.valid_from:
best = rule
if best is None:
what = "12.07% accrual rule" if basis == "accrual" else "5.6-week rule"
if HOLIDAY_RULES_HISTORY != "full":
raise ValueError(
"no holiday %s for a leave year beginning %s: this build was installed with history=%s. "
"Reinstall with history=full for older leave years." % (what, leave_year_start, HOLIDAY_RULES_HISTORY)
)
raise ValueError("no holiday %s for a leave year beginning %s" % (what, leave_year_start))
return best
def holiday_entitlement(worker: HolidayWorker, leave_year_start: str) -> HolidayEntitlement:
"""Statutory annual leave under the Working Time Regulations 1998.
Regular workers get 5.6 weeks of their working days, capped at 28 days, and
a proportion of it for a leave year they only partly work. Irregular-hours
and part-year workers, for leave years from 1 April 2024, instead accrue
12.07% of the hours they work each pay period, to the nearest whole hour.
"""
parse_iso_date(leave_year_start)
if worker.kind in ("irregular-hours", "part-year"):
rule = _rule_for("accrual", leave_year_start)
minutes = worker.minutes_worked
if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 0:
raise ValueError("minutesWorked must be a whole number of minutes, not negative, received %s" % (minutes,))
# reg 15B(5): under 30 minutes of a fraction is dropped, 30 or more is a whole hour.
scaled = minutes * rule.accrual_basis_points
hour = 60 * 10000
hours = scaled // hour
if scaled - hours * hour >= hour // 2:
hours += 1
return HolidayEntitlement(kind=worker.kind, days_x100=0, capped=False, hours_accrued=hours)
if worker.kind != "regular":
raise ValueError('unknown kind of worker "%s"' % (worker.kind,))
rule = _rule_for("weeks", leave_year_start)
per_week = worker.days_per_week_x100
if isinstance(per_week, bool) or not isinstance(per_week, int) or per_week < 1 or per_week > 700:
raise ValueError("daysPerWeekX100 must be 1 to 700, received %s" % (per_week,))
# In thousandths of a day: 5.6 weeks of 5 days is 28000.
raw = rule.weeks_x10 * per_week
cap = rule.cap_days * 1000
full = min(raw, cap)
next_year = add_months(leave_year_start, 12)
year_end = add_days(next_year, -1)
start = worker.employment_start if worker.employment_start is not None and worker.employment_start > leave_year_start else leave_year_start
end = worker.employment_end if worker.employment_end is not None and worker.employment_end < year_end else year_end
parse_iso_date(start)
parse_iso_date(end)
if end < start:
raise ValueError("employment %s to %s does not overlap the leave year" % (start, end))
worked = days_between(start, end) + 1
year_days = days_between(leave_year_start, next_year)
return HolidayEntitlement(kind="regular", days_x100=round_div(full * worked, year_days * 10, "up"), capped=raw > cap, hours_accrued=0)