Functional Weave
Code in TypeScript

payroll.holiday-entitlement@1.0.0

impl/python.py

3,842 bytes · the Python implementation · view raw

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)