diff --git a/payroll/services/payroll.py b/payroll/services/payroll.py index 7723eae4..64562324 100644 --- a/payroll/services/payroll.py +++ b/payroll/services/payroll.py @@ -94,16 +94,17 @@ def payroll_forecast_report( pay_uplift = np.array(pay_uplift_obj.periods) if pay_uplift_obj else np.ones(12) attrition = np.array(attrition_obj.periods) if attrition_obj else np.ones(12) + pay_uplift_accumulate = np.array(list(accumulate(pay_uplift, operator.mul))) attrition_accumulate = np.array(list(accumulate(attrition, operator.mul))) for employee in employee_qs.iterator(): periods = employee.pay_periods.first().periods periods = np.array(periods) - periods = periods * pay_uplift * attrition_accumulate - prog_report = report[employee.programme_code_id] - prog_report[settings.PAYROLL.BASIC_PAY_NAC] += periods * employee.basic_pay + prog_report[settings.PAYROLL.BASIC_PAY_NAC] += ( + periods * employee.basic_pay * pay_uplift_accumulate * attrition_accumulate + ) prog_report[settings.PAYROLL.PENSION_NAC] += periods * employee.pension prog_report[settings.PAYROLL.ERNIC_NAC] += periods * employee.ernic diff --git a/payroll/tests/factories.py b/payroll/tests/factories.py index 0cba8b15..220d08e6 100644 --- a/payroll/tests/factories.py +++ b/payroll/tests/factories.py @@ -3,6 +3,8 @@ from faker import Faker from chartofaccountDIT.test.factories import NaturalCodeFactory, ProgrammeCodeFactory +from core.models import Attrition, PayUplift +from core.test.factories import FinancialYearFactory from costcentre.test.factories import CostCentreFactory from gifthospitality.test.factories import GradeFactory from payroll.models import Employee, PayElementType, PayElementTypeGroup, Vacancy @@ -63,3 +65,31 @@ class Meta: appointee_name = factory.Faker("name") hiring_manager = factory.Faker("name") hr_ref = factory.Faker("name") + + +class PayModifierFactory(factory.django.DjangoModelFactory): + financial_year = factory.SubFactory(FinancialYearFactory) + apr = 1.0 + may = 1.0 + jun = 1.0 + jul = 1.0 + aug = 1.0 + sep = 1.0 + oct = 1.0 + nov = 1.0 + dec = 1.0 + jan = 1.0 + feb = 1.0 + mar = 1.0 + + +class PayUpliftFactory(PayModifierFactory): + class Meta: + model = PayUplift + + +class AttritionFactory(PayModifierFactory): + class Meta: + model = Attrition + + cost_centre = factory.SubFactory(CostCentreFactory) diff --git a/payroll/tests/services/test_payroll.py b/payroll/tests/services/test_payroll.py index 589c43af..61981a89 100644 --- a/payroll/tests/services/test_payroll.py +++ b/payroll/tests/services/test_payroll.py @@ -2,6 +2,7 @@ import pytest +from core.constants import MONTHS from core.models import FinancialYear from costcentre.test.factories import CostCentreFactory from payroll.services.payroll import ( @@ -10,31 +11,42 @@ vacancy_created, ) -from ..factories import EmployeeFactory, VacancyFactory +from ..factories import ( + AttritionFactory, + EmployeeFactory, + PayUpliftFactory, + VacancyFactory, +) -def test_payroll_forecast(db): - # NOTE: These must match the PAYROLL.BASIC_PAY_NAC and PAYROLL.PENSION_NAC settings. - SALARY_NAC = "71111001" - PENSION_NAC = "71111002" +# NOTE: These must match the PAYROLL.BASIC_PAY_NAC, PAYROLL.PENSION_NAC and PAYROLL.ERNIC_NAC settings. +SALARY_NAC = "71111001" +PENSION_NAC = "71111002" +ERNIC_NAC = "71111003" +NACS = [SALARY_NAC, PENSION_NAC, ERNIC_NAC] - cost_centre = CostCentreFactory.create(cost_centre_code="123456") - # salary_1 = PayElementTypeFactory.create( - # name="Salary 1", - # group__name="Salary", - # group__natural_code__natural_account_code=SALARY_NAC, - # ) - # salary_2 = PayElementTypeFactory.create( - # name="Salary 2", - # group__name="Salary", - # group__natural_code__natural_account_code=SALARY_NAC, - # ) - # pension_1 = PayElementTypeFactory.create( - # name="Pension 1", - # group__name="Pension", - # group__natural_code__natural_account_code=PENSION_NAC, - # ) +def assert_report_results_with_modifiers( + report, salary, pension, ernic, modifiers=None +): + if modifiers is None: + modifiers = {} + + for nac in NACS: + for month in MONTHS: + modifier = modifiers.get(month, 1) + if nac == SALARY_NAC: + expected_result = salary * modifier + elif nac == PENSION_NAC: + expected_result = pension + else: + expected_result = ernic + + assert float(report[nac][month]) == pytest.approx(expected_result) + + +def test_payroll_forecast(db): + cost_centre = CostCentreFactory.create(cost_centre_code="123456") payroll_employee_1 = EmployeeFactory.create( cost_centre=cost_centre, @@ -64,38 +76,6 @@ def test_payroll_forecast(db): employee_created(payroll_employee_1) employee_created(payroll_employee_2) - # payroll_employees[0].pay_element.create( - # type=salary_1, - # debit_amount=2000, - # credit_amount=100, - # ) - # payroll_employees[0].pay_element.create( - # type=salary_2, - # debit_amount=100, - # credit_amount=50, - # ) - # payroll_employees[0].pay_element.create( - # type=pension_1, - # debit_amount=75.5, - # credit_amount=0, - # ) - - # payroll_employees[1].pay_element.create( - # type=salary_1, - # debit_amount=1500, - # credit_amount=55.6, - # ) - # payroll_employees[1].pay_element.create( - # type=salary_2, - # debit_amount=80, - # credit_amount=0, - # ) - # payroll_employees[1].pay_element.create( - # type=pension_1, - # debit_amount=130.25, - # credit_amount=15, - # ) - vacancy = VacancyFactory.create( cost_centre=cost_centre, programme_code__programme_code="123456", @@ -136,3 +116,121 @@ def test_payroll_forecast(db): assert float(report_by_nac[PENSION_NAC]["may"]) == pytest.approx(e1p) assert float(report_by_nac[SALARY_NAC]["jun"]) == pytest.approx(v1s) assert float(report_by_nac[PENSION_NAC]["jun"]) == pytest.approx(0) + + +def test_one_employee_with_no_modifiers(db): + cost_centre = CostCentreFactory.create(cost_centre_code="123456") + + payroll_employee_1 = EmployeeFactory.create( + cost_centre=cost_centre, + basic_pay=195000, + pension=7550, + ernic=2000, + ) + + employee_created(payroll_employee_1) + + financial_year = FinancialYear.objects.current() + + report = payroll_forecast_report(cost_centre, financial_year) + + report_by_nac = {x["natural_account_code"]: x for x in report} + + e1s = ((2000 - 100) + (100 - 50)) * 100 + e1p = (75.5 - 0) * 100 + e1e = payroll_employee_1.ernic + + assert_report_results_with_modifiers(report_by_nac, e1s, e1p, e1e) + + +def test_one_employee_with_pay_uplift(db): + cost_centre = CostCentreFactory.create(cost_centre_code="123456") + + payroll_employee_1 = EmployeeFactory.create( + cost_centre=cost_centre, + basic_pay=195000, + pension=7550, + ernic=2000, + ) + + employee_created(payroll_employee_1) + + financial_year = FinancialYear.objects.current() + + pay_uplift = PayUpliftFactory.create( + financial_year=financial_year, + aug=1.02, + ) + modifier = pay_uplift.aug + + report = payroll_forecast_report(cost_centre, financial_year) + + report_by_nac = {x["natural_account_code"]: x for x in report} + + e1s = ((2000 - 100) + (100 - 50)) * 100 + e1p = (75.5 - 0) * 100 + e1e = payroll_employee_1.ernic + + assert_report_results_with_modifiers( + report_by_nac, + e1s, + e1p, + e1e, + modifiers={ + "aug": modifier, + "sep": modifier, + "oct": modifier, + "nov": modifier, + "dec": modifier, + "jan": modifier, + "feb": modifier, + "mar": modifier, + }, + ) + + +def test_one_employee_with_attrition(db): + cost_centre = CostCentreFactory.create(cost_centre_code="123456") + + payroll_employee_1 = EmployeeFactory.create( + cost_centre=cost_centre, + basic_pay=195000, + pension=7550, + ernic=2000, + ) + + employee_created(payroll_employee_1) + + financial_year = FinancialYear.objects.current() + + attrition = AttritionFactory.create( + cost_centre=cost_centre, + financial_year=financial_year, + aug=0.95, + ) + modifier = attrition.aug + + report = payroll_forecast_report(cost_centre, financial_year) + + report_by_nac = {x["natural_account_code"]: x for x in report} + + e1s = ((2000 - 100) + (100 - 50)) * 100 + e1p = (75.5 - 0) * 100 + e1e = payroll_employee_1.ernic + + assert_report_results_with_modifiers( + report_by_nac, + e1s, + e1p, + e1e, + modifiers={ + "aug": modifier, + "sep": modifier, + "oct": modifier, + "nov": modifier, + "dec": modifier, + "jan": modifier, + "feb": modifier, + "mar": modifier, + }, + )