from typing import Optional from .dates_add_days import add_days, days_between, parse_iso_date from .dates_add_months import add_months from .math_round_div import round_div from .payroll_holiday_entitlement_data import HOLIDAY_RULES, HOLIDAY_RULES_HISTORY, HolidayRule 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)