payroll.pension-auto-enrolment@1.0.2
impl/python.py
4,976 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.
import re
from typing import NoReturn, Optional
from .math_round_div import round_div ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, money ← from money.amount ^1.0.0 · built alongside by fune
from .payroll_pension_auto_enrolment_data import AE_MINIMUMS, AE_MINIMUMS_HISTORY, AE_MINIMUMS_HORIZON, AE_THRESHOLDS, AE_THRESHOLDS_HISTORY, AE_THRESHOLDS_HORIZON ← this capability’s own data, compiled from data/ae-thresholds.json into the same file by fune build
from .payroll_pension_auto_enrolment_types import PensionContributions, PensionScheme
from .payroll_tax_period_types import PayFrequency
ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")
def _missing(what: str, on_date: str, history: str, horizon: Optional[str]) -> NoReturn:
if history != "full" and horizon is not None and on_date < horizon:
raise ValueError(
"%s on %s: this build was installed with history=%s, so it only carries rules from %s. "
"Reinstall with history=full for earlier tax years." % (what, on_date, history, horizon)
)
raise ValueError("%s on %s" % (what, on_date))
def _check_rate(name: str, value: Optional[int]) -> None:
if value is not None and (isinstance(value, bool) or not isinstance(value, int) or value < 0):
raise ValueError("%s must be a whole number of basis points, 0 or more, received %s" % (name, value))
def _in_force(r, on_date: str) -> bool:
return on_date >= r.valid_from and (r.valid_to is None or on_date <= r.valid_to)
def pension_contributions(earnings: Money, frequency: PayFrequency, scheme: PensionScheme, pay_date: str) -> PensionContributions:
"""Workplace pension contributions for one pay reference period.
The band is applied with the published per-period figures (£520 to £4,189
a month, £120 to £967 a week), never the annual band divided on the fly.
Each contribution is rounded once to the nearest penny, half up; under
relief at source the deduction from pay is then 80% of the gross
contribution, again rounded half up, and the difference is the relief the
provider claims.
"""
if earnings.currency != "GBP":
raise ValueError("pension contributions must be in GBP, received %s" % (earnings.currency,))
if earnings.minor < 0:
raise ValueError("earnings must not be negative, received %s" % (earnings.minor,))
_check_rate("employeeBasisPoints", scheme.employee_basis_points)
_check_rate("employerBasisPoints", scheme.employer_basis_points)
if scheme.earnings_basis not in ("qualifying-earnings", "all-earnings"):
raise ValueError('unknown earnings basis "%s"' % (scheme.earnings_basis,))
if scheme.arrangement not in ("relief-at-source", "net-pay"):
raise ValueError('unknown relief arrangement "%s"' % (scheme.arrangement,))
if frequency not in ("weekly", "fortnightly", "four-weekly", "monthly"):
raise ValueError('unknown pay frequency "%s"' % (frequency,))
if not isinstance(pay_date, str) or not ISO_DATE.fullmatch(pay_date):
raise ValueError('payDate must be an ISO date (YYYY-MM-DD), received "%s"' % (pay_date,))
t = next((r for r in AE_THRESHOLDS if _in_force(r, pay_date)), None)
if t is None:
_missing("no auto-enrolment earnings thresholds", pay_date, AE_THRESHOLDS_HISTORY, AE_THRESHOLDS_HORIZON)
m = next((r for r in AE_MINIMUMS if _in_force(r, pay_date)), None)
if m is None:
_missing("no auto-enrolment minimum contributions", pay_date, AE_MINIMUMS_HISTORY, AE_MINIMUMS_HORIZON)
lower, trigger, upper = {
"weekly": (t.lower_weekly, t.trigger_weekly, t.upper_weekly),
"fortnightly": (t.lower_fortnightly, t.trigger_fortnightly, t.upper_fortnightly),
"four-weekly": (t.lower_four_weekly, t.trigger_four_weekly, t.upper_four_weekly),
"monthly": (t.lower_monthly, t.trigger_monthly, t.upper_monthly),
}[frequency]
# Qualifying earnings are the part of pay above the lower level and not
# above the upper one (Pensions Act 2008 s.13).
if scheme.earnings_basis == "all-earnings":
pensionable = earnings.minor
else:
pensionable = max(0, min(earnings.minor, upper) - lower)
employer_rate = scheme.employer_basis_points if scheme.employer_basis_points is not None else m.employer_basis_points
employee_rate = (
scheme.employee_basis_points if scheme.employee_basis_points is not None else max(0, m.total_basis_points - employer_rate)
)
employee_gross = round_div(pensionable * employee_rate, 10000, "half-up")
employer = round_div(pensionable * employer_rate, 10000, "half-up")
net_pay = scheme.arrangement == "net-pay"
deduction = employee_gross if net_pay else round_div(employee_gross * (10000 - m.relief_at_source_basis_points), 10000, "half-up")
return PensionContributions(
pensionable_earnings=money(pensionable, "GBP"),
employee_gross=money(employee_gross, "GBP"),
employee_deduction=money(deduction, "GBP"),
employer=money(employer, "GBP"),
deduct_before_tax=net_pay,
earnings_trigger_met=earnings.minor > trigger,
)