Source code for ucamstaffoncosts

import collections
import enum
import fractions
import math

from . import tax
from . import pension

_OnCost = collections.namedtuple(
    'OnCost',
    'salary exchange employer_pension employer_nic apprenticeship_levy total'
)


[docs]class OnCost(_OnCost): """An individual on-costs calculation for a gross salary. .. note:: These values are all rounded to the nearest pound and so total may not be the sum of all the other fields. .. py:attribute:: salary Gross salary for the employee. .. py:attribute:: exchange Amount of gross salary exchanged as part of a salary exchange pension. By convention, this value is negative if non-zero. .. py:attribute:: employer_pension Employer pension contribution including any salary exchange amount. .. py:attribute:: employer_nic Employer National Insurance contribution .. py:attribute:: apprenticeship_levy Share of Apprenticeship Levy from this employee .. py:attribute:: total Total on-cost of employing this employee. See note above about situations where this value may not be the sum of the others. """
[docs]class Scheme(enum.Enum): """ Possible pension schemes an employee can be a member of. """ #: No pension scheme. NONE = 'none' #: CPS hybrid CPS_HYBRID = 'cps_hybrid' #: CPS hybrid with salary exchange CPS_HYBRID_EXCHANGE = 'cps_hybrid_exchange' #: USS USS = 'uss' #: USS with salary exchange USS_EXCHANGE = 'uss_exchange' #: NHS NHS = 'nhs'
#: Special value to pass to :py:func:`~.on_cost` to represent the latest tax year which has an #: implementation. LATEST = 'LATEST'
[docs]def on_cost(gross_salary, scheme, year=LATEST): """ Return a :py:class:`OnCost` instance given a tax year, pension scheme and gross salary. :param int year: tax year :param Scheme scheme: pension scheme :param int gross_salary: gross salary of employee :raises NotImplementedError: if there is not an implementation for the specified tax year and pension scheme. """ year = _LATEST_TAX_YEAR if year is LATEST else year try: calculator = _ON_COST_CALCULATORS[year][scheme] except KeyError: raise NotImplementedError() return calculator(gross_salary)
def _on_cost_calculator(employer_nic_cb, employer_pension_cb=lambda _: 0, exchange_cb=lambda _: 0, apprenticeship_levy_cb=tax.standard_apprenticeship_levy): """ Return a callable which will calculate an OnCost entry from a gross salary. Arguments which are callables each take a single argument which is a :py:class:`fractions.Fraction` instance representing the gross salary of the employee. They should return a :py:class:`fractions.Fraction` instance. :param employer_pension_cb: callable which gives employer pension contribution from gross salary. :param employer_nic_cb: callable which gives employer National Insurance contribution from gross salary. :param exchange_cb: callable which gives amount of salary sacrificed in a salary exchange scheme from gross salary. :param apprenticeship_levy_cb: callable which calculates the Apprenticeship Levy from gross salary. """ def on_cost(gross_salary): # Ensure gross salary is a rational gross_salary = fractions.Fraction(gross_salary) # We use the convention that the salary exchange value is negative to match the exchange # column in HR tables. exchange = -exchange_cb(gross_salary) # The employer pension contribution is the contribution based on gross salary along with # the employee contribution sacrificed from their salary. employer_pension = employer_pension_cb(gross_salary) - exchange # the taxable salary is the gross less the amount sacrificed. HR would appear to round the # sacrifice first taxable_salary = gross_salary + _excel_round(exchange) # The employer's NIC is calculated on the taxable salary. employer_nic = employer_nic_cb(taxable_salary) # The Apprenticeship Levy is calculated on the taxable salary. apprenticeship_levy = apprenticeship_levy_cb(taxable_salary) # The total is calculated using the rounded values. total = ( _excel_round(gross_salary) + _excel_round(exchange) + _excel_round(employer_pension) + _excel_round(employer_nic) + _excel_round(apprenticeship_levy) ) # Round all of the values. Note the odd rounding for exchange. This matters since the # tables HR generate seem to include -_excel_round(-exchange) even though the total column # is calculated using _excel_round(exchange). Since Excel always rounds halves up, this # means that _excel_round(exchange) does not, in general, equal -_excel_round(-exchange) as # you might expect. Caveat programmer! return OnCost( salary=_excel_round(gross_salary), exchange=-_excel_round(-exchange), employer_pension=_excel_round(employer_pension), employer_nic=_excel_round(employer_nic), apprenticeship_levy=_excel_round(apprenticeship_levy), total=_excel_round(total), ) return on_cost def _excel_round(n): """ A version of round() which applies the Excel rule that halves rounds *up* rather than the conventional wisdom that they round to the nearest even. """ # Ensure input is a rational n = fractions.Fraction(n) if n.denominator == 2: # always round up halves return math.ceil(n) return round(n) #: On cost calculators keyed initially by year and then by scheme identifier. _ON_COST_CALCULATORS = { 2018: { # An employee with no scheme in tax year 2018/19 Scheme.NONE: _on_cost_calculator( employer_nic_cb=tax.TABLE_A_EMPLOYER_NIC[2018], ), # An employee with USS in tax year 2018/19 Scheme.USS: _on_cost_calculator( employer_pension_cb=pension.uss_employer_contribution, employer_nic_cb=tax.TABLE_A_EMPLOYER_NIC[2018], ), # An employee with USS and salary exchange in tax year 2018/19 Scheme.USS_EXCHANGE: _on_cost_calculator( employer_pension_cb=pension.uss_employer_contribution, exchange_cb=pension.uss_employee_contribution, employer_nic_cb=tax.TABLE_A_EMPLOYER_NIC[2018], ), # An employee with CPS in tax year 2018/19 Scheme.CPS_HYBRID: _on_cost_calculator( employer_pension_cb=pension.cps_hybrid_employer_contribution, employer_nic_cb=tax.TABLE_A_EMPLOYER_NIC[2018], ), # An employee with CPS and salary exchange in tax year 2018/19 Scheme.CPS_HYBRID_EXCHANGE: _on_cost_calculator( employer_pension_cb=pension.cps_hybrid_employer_contribution, exchange_cb=pension.cps_hybrid_employee_contribution, employer_nic_cb=tax.TABLE_A_EMPLOYER_NIC[2018], ), # An employee on the NHS scheme in tax year 2018/19 Scheme.NHS: _on_cost_calculator( employer_pension_cb=pension.nhs_employer_contribution, employer_nic_cb=tax.TABLE_A_EMPLOYER_NIC[2018], ), }, } _LATEST_TAX_YEAR = max(_ON_COST_CALCULATORS.keys())