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 List, NoReturn, Sequence
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_income_tax_data import INCOME_TAX_BANDS, INCOME_TAX_BANDS_HISTORY, OVERRIDING_LIMIT, OVERRIDING_LIMIT_HISTORY, IncomeTaxBand ← this capability’s own data, compiled from data/income-tax-bands.json into the same file by fune build
from .payroll_income_tax_types import IncomeTax
from .payroll_tax_code_parse import parse_tax_code ← from payroll.tax-code-parse ^1.0.0 · built alongside by fune
from .payroll_tax_period_types import PayFrequency
ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
# HMRC's routines work "to 4 decimal places of a pound without correcting the
# final place". Holding amounts as integer ten-thousandths of a pound (1 = 0.0001
# pounds, 100 = 1p) makes every one of those truncations an exact integer division.
UNITS_PER_PENNY = 100
UNITS_PER_POUND = 10000
def _in_force(row, on_date: str) -> bool:
return row.valid_from <= on_date and (row.valid_to is None or on_date <= row.valid_to)
def _no_rule(what: str, on_date: str, history: str, froms: Sequence[str]) -> NoReturn:
# A build installed with history=current only carries the rules still in
# force; answering an older date with this year's bands would be a quiet,
# plausible wrong answer, so say why there is nothing instead.
if history != "full" and len(froms) > 0:
earliest = min(froms)
if on_date < earliest:
raise ValueError(
"no %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, earliest)
)
raise ValueError("no %s on %s" % (what, on_date))
def _bands_for(region: str, on_date: str) -> List[IncomeTaxBand]:
rows = [b for b in INCOME_TAX_BANDS if b.region == region and _in_force(b, on_date)]
if len(rows) == 0:
_no_rule(
"income tax bands for %s" % (region,),
on_date,
INCOME_TAX_BANDS_HISTORY,
[b.valid_from for b in INCOME_TAX_BANDS if b.region == region],
)
return sorted(rows, key=lambda b: b.band)
def _overriding_limit(on_date: str) -> int:
for row in OVERRIDING_LIMIT:
if _in_force(row, on_date):
return row.basis_points
_no_rule("PAYE overriding limit", on_date, OVERRIDING_LIMIT_HISTORY, [r.valid_from for r in OVERRIDING_LIMIT])
def _period_allowance(code_number: int, periods_per_year: int) -> int:
"""Free pay (or K-code additional pay) for one week or month, in pence: HMRC paragraph 4.3.1."""
if code_number == 0:
return 0
# Codes above 500 are split into blocks of 500 and a remainder of 1-500, each
# rounded up to the penny separately; that is how the printed tables were
# built, and computing the whole code in one division is a penny out.
blocks = (code_number - 1) // 500
remainder = (code_number - 1) % 500 + 1
block_value = 9616 if periods_per_year == 52 else 41667
return round_div((remainder * 10 + 9) * 100, periods_per_year, "up") + blocks * block_value
def _banded_tax(taxable_pence: int, bands: Sequence[IncomeTaxBand], n: int, periods_per_year: int) -> int:
"""Tax due to date on positive taxable pay by the banded Tax Formulae of paragraph 4.4, in pence."""
taxable_pounds = round_div(taxable_pence, 100, "down")
previous_threshold = 0
previous_threshold_tax = 0
lower_pounds = 0
cumulative_annual_tax = 0
for band in bands:
if band.up_to is not None:
threshold = round_div(band.up_to * UNITS_PER_POUND * n, periods_per_year, "down")
# The income test compares the unrounded pay with the threshold
# rounded UP to a whole pound (the round-pound limits of Tables C),
# while the formula itself uses the exact threshold.
cvalue = round_div(threshold, UNITS_PER_POUND, "up")
if taxable_pence > cvalue * 100:
cumulative_annual_tax += (band.up_to - lower_pounds) * band.basis_points
lower_pounds = band.up_to
previous_threshold = threshold
previous_threshold_tax = round_div(cumulative_annual_tax * n, periods_per_year, "down")
continue
at_this_rate = round_div((taxable_pounds * UNITS_PER_POUND - previous_threshold) * band.basis_points, 10000, "down")
return round_div(previous_threshold_tax + at_this_rate, UNITS_PER_PENNY, "down")
raise ValueError("income tax bands have no top band")
def _check_gbp(name: str, amount: Money) -> None:
if amount.currency != "GBP":
raise ValueError("payroll amounts must be in GBP, received %s for %s" % (amount.currency, name))
def income_tax(
tax_code: str,
frequency: PayFrequency,
period: int,
pay: Money,
previous_pay_to_date: Money,
previous_tax_to_date: Money,
pay_date: str,
) -> IncomeTax:
"""PAYE income tax for one payment, following HMRC's "Specification for PAYE
tax table routines" (the computerised form of the tax tables, used by
payroll software): cumulative or week 1 / month 1, suffix, K, BR, D and NT
codes, Scottish and Welsh bands, and the 50% overriding limit.
"""
_check_gbp("pay", pay)
_check_gbp("previousPayToDate", previous_pay_to_date)
_check_gbp("previousTaxToDate", previous_tax_to_date)
if not isinstance(pay_date, str) or not ISO_DATE.match(pay_date):
raise ValueError('payDate must be an ISO date (YYYY-MM-DD), received "%s"' % (pay_date,))
if frequency == "monthly":
weeks_in_period, valid_period = 1, 1 <= period <= 12
elif frequency == "weekly":
weeks_in_period, valid_period = 1, 1 <= period <= 53
elif frequency == "fortnightly":
weeks_in_period, valid_period = 2, 1 <= period <= 52 or period == 54
elif frequency == "four-weekly":
weeks_in_period, valid_period = 4, 1 <= period <= 52 or period == 56
else:
raise ValueError('unknown pay frequency "%s"' % (frequency,))
if isinstance(period, bool) or not isinstance(period, int) or not valid_period:
raise ValueError("period %s is not a tax period for %s pay" % (period, frequency))
code = parse_tax_code(tax_code)
# Looked up even when no band is needed (NT, pay under the allowance), so a
# date outside the tax years on file is always refused.
bands = _bands_for(code.region, pay_date)
periods_per_year = 12 if frequency == "monthly" else 52
# Weeks 53, 54 and 56 are always taxed on a week 1 basis (paragraph 14),
# using the week 1, 2 or 4 figures.
cumulative = code.cumulative and period <= 52
n = period if cumulative else weeks_in_period
pay_to_date_for_tax = previous_pay_to_date.minor + pay.minor if cumulative else pay.minor
liability = 0
allowance_to_date = 0
taxable_pay = 0
if code.kind in ("allowance", "negative-allowance"):
per_period = _period_allowance(code.number, periods_per_year) * n
allowance_to_date = per_period if code.kind == "allowance" else -per_period
taxable_pence = pay_to_date_for_tax - allowance_to_date
if taxable_pence > 0:
taxable_pay = round_div(taxable_pence, 100, "down") * 100
liability = _banded_tax(taxable_pence, bands, n, periods_per_year)
elif code.kind in ("basic-rate", "d-rate"):
basic = next((i for i, b in enumerate(bands) if b.basic_rate), -1)
index = basic if code.kind == "basic-rate" else basic + 1 + code.number
if basic < 0 or index >= len(bands):
raise ValueError("no %s rate for %s on %s" % (code.code, code.region, pay_date))
pounds = round_div(pay_to_date_for_tax, 100, "down") if pay_to_date_for_tax > 0 else 0
taxable_pay = pounds * 100
liability = round_div(pounds * bands[index].basis_points, 100, "down")
# The overriding limit: no more than 50% of this payment may go in tax. It
# never restricts a refund, and on negative pay it is zero (paragraph 4.5.4).
limit = round_div(pay.minor * _overriding_limit(pay_date), 10000, "down") if pay.minor > 0 else 0
due = liability - previous_tax_to_date.minor if cumulative else liability
tax = limit if due > limit else due
return IncomeTax(
tax=money(tax, "GBP"),
tax_to_date=money(previous_tax_to_date.minor + tax, "GBP"),
pay_to_date=money(previous_pay_to_date.minor + pay.minor, "GBP"),
allowance_to_date=money(allowance_to_date, "GBP"),
taxable_pay=money(taxable_pay, "GBP"),
cumulative=cumulative,
limit_applied=due > limit,
tax_not_deducted=money(due - limit if due > limit else 0, "GBP"),
)