Source code for ucamstaffoncosts

"""
.. testsetup::

    from ucamstaffoncosts import *
    import datetime

"""
import collections
import datetime
import fractions

from . import costs

from .costs import Scheme, Cost
from .salary.progression import SalaryRecord
from .salary.scales import Grade


__all__ = [
    'employment_expenditure_and_commitments', 'CommitmentExplanation', 'Scheme', 'Grade', 'Cost',
    'SalaryRecord'
]


[docs]def employment_expenditure_and_commitments(until_date, initial_grade, initial_point, scheme, start_date=None, from_date=None, occupancy=1, tax_year_start_month=4, tax_year_start_day=6, **kwargs): """ Calculate the existing expenditure and remaining commitments for an employee's contract. Returns a pair giving the total commitment and a list of :py:class:`CommitmentExplanation` tuples explaining the calculation. :param from_date: date at which to differentiate between expenditure and commitments. If ``None``, today's date is used. :param start_date: date at which employee started. Results will be returned from this date. If ``None``, the "from date" is used. :param occupancy: proportion of full-time this employee works :type occupancy: :py:class:`numbers.Rational` Remaining keyword arguments are passed to :py:func:`costs_by_tax_year`. >>> from ucamstaffoncosts import Grade, Scheme >>> from ucamstaffoncosts.salary.scales import EXAMPLE_SALARY_SCALES >>> initial_grade = Grade.GRADE_2 >>> initial_point = EXAMPLE_SALARY_SCALES.starting_point_for_grade(initial_grade) >>> scheme = Scheme.USS_EXCHANGE >>> next_anniversary_date = datetime.date(2016, 6, 1) >>> from_date = next_anniversary_date - datetime.timedelta(days=400) >>> until_date = from_date + datetime.timedelta(days=1000) >>> expenditure, commitments, _ = employment_expenditure_and_commitments( ... until_date, initial_grade, initial_point, scheme, ... from_date=from_date, next_anniversary_date=next_anniversary_date, ... scale_table=EXAMPLE_SALARY_SCALES) >>> expenditure 0 >>> commitments 50146 The *occupancy* parameter allows one to specify what proportion of full time this person works: >>> import fractions >>> _, half_time_commitments, _ = employment_expenditure_and_commitments( ... until_date, initial_grade, initial_point, scheme, ... occupancy=fractions.Fraction(50, 100), ... from_date=from_date, next_anniversary_date=next_anniversary_date, ... scale_table=EXAMPLE_SALARY_SCALES) >>> half_time_commitments 24221 Notice that this value is not half of the full-time commitments since employer costs do not scale linearly! The first tax year returned will contain the *from_date*: >>> from_date = datetime.date(2016, 1, 1) >>> _, _, explanations = employment_expenditure_and_commitments( ... until_date, initial_grade, initial_point, scheme, ... from_date=from_date, next_anniversary_date=next_anniversary_date, ... scale_table=EXAMPLE_SALARY_SCALES) >>> explanations[0].tax_year 2016 """ occupancy_fraction = fractions.Fraction(occupancy) from_date = from_date if from_date is not None else datetime.datetime.now().date() start_date = start_date if start_date is not None else from_date start_year = start_date.year if datetime.date(start_year, tax_year_start_month, tax_year_start_day) > start_date: start_year -= 1 # Calculate costs all_costs = costs.costs_by_tax_year( start_year, initial_grade, initial_point, scheme, occupancy=occupancy_fraction, start_date=start_date, tax_year_start_month=tax_year_start_month, tax_year_start_day=tax_year_start_day, until_date=until_date, **kwargs) explanations = [] total_expenditure, total_commitment = 0, 0 for year, cost, salaries in all_costs: # What range of dates does this cover? start_date = salaries[0].date end_date = salaries[-1].date tax_year_start_date = datetime.date( year, tax_year_start_month, tax_year_start_day) tax_year_end_date = datetime.date( year+1, tax_year_start_month, tax_year_start_day) tax_year_days = (tax_year_end_date - tax_year_start_date).days # Should never get salary records after until_date assert end_date <= until_date # Compute total earnings earned *after or on* the from_date. earnings_after_from_date = fractions.Fraction(0, 1) for salary_start, salary_end in zip(salaries, salaries[1:]): salary_start_date = max(salary_start.date, from_date) salary_end_date = max(salary_start_date, salary_end.date) # how many days in this range? days = (salary_end_date - salary_start_date).days earnings_after_from_date += fractions.Fraction( days * salary_start.base_salary, tax_year_days) * occupancy_fraction earnings_after_from_date = round(earnings_after_from_date) assert earnings_after_from_date >= 0 assert earnings_after_from_date <= cost.salary # commitment is share of total cost weighted by the ratio of earnings after from date to # total earnings for that year commitment = round(fractions.Fraction(earnings_after_from_date, cost.salary) * cost.total) expenditure = cost.total - commitment explanations.append(CommitmentExplanation( tax_year=start_date.year, salary=cost.salary, salary_to_come=earnings_after_from_date, expenditure=expenditure, commitment=commitment, salaries=salaries, cost=cost )) total_expenditure += expenditure total_commitment += commitment return total_expenditure, total_commitment, explanations
_CommitmentExplanation = collections.namedtuple( 'CommitmentExplanation', 'tax_year salary salary_to_come expenditure commitment salaries cost')
[docs]class CommitmentExplanation(_CommitmentExplanation): """ An explanation of the commitment calculation for a given tax year. .. py:attribute:: tax_year Integer giving the calendar year when the tax year started .. py:attribute:: salary Total base salary for the tax year .. py:attribute:: salary_to_come Salary which is yet to come. I.e. the salary which has not yet been earned as of the "from" date. .. py:attribute:: expenditure The expenditure for this tax year. I.e. the amount of total cost of employment which has already been spent. .. py:attribute:: commitment The commitment for this tax year. This is the total cost of employment minus the expenditure. .. py:attribute:: salaries A list of :py:class:`~.SalaryRecord` tuples which explain why the employee's salary is what it is during the tax year. .. py:attribute:: cost A :py:class:`~.Cost` tuple explaining the total cost of employment for the tax year. """