diff --git a/front_end/src/Apps/Payroll.jsx b/front_end/src/Apps/Payroll.jsx index 273fa520..7da61e60 100644 --- a/front_end/src/Apps/Payroll.jsx +++ b/front_end/src/Apps/Payroll.jsx @@ -1,19 +1,12 @@ -import { useEffect, useReducer, useState, useMemo } from "react"; +import { useEffect, useReducer, useState } from "react"; import EditPayroll from "../Components/EditPayroll"; import * as api from "../Components/EditPayroll/api"; -import { - payrollHeaders, - vacancyHeaders, -} from "../Components/EditPayroll/constants"; const initialPayrollState = []; export default function Payroll() { - const [allPayroll, dispatch] = useReducer( - payrollReducer, - initialPayrollState - ); + const [payroll, dispatch] = useReducer(payrollReducer, initialPayrollState); const [saveSuccess, setSaveSuccess] = useState(false); useEffect(() => { @@ -26,20 +19,10 @@ export default function Payroll() { api.getPayrollData().then((data) => dispatch({ type: "fetched", data })); }, []); - // Computed properties - const payroll = useMemo( - () => allPayroll.filter((payroll) => payroll.basic_pay > 0), - [allPayroll] - ); - const nonPayroll = useMemo( - () => allPayroll.filter((payroll) => payroll.basic_pay <= 0), - [allPayroll] - ); - // Handlers async function handleSavePayroll() { try { - await api.postPayrollData(allPayroll); + await api.postPayrollData(payroll); setSaveSuccess(true); localStorage.setItem("saveSuccess", "true"); @@ -55,41 +38,12 @@ export default function Payroll() { } return ( - <> - {saveSuccess && ( -
-
-

- Success -

-
-
- )} -

Payroll

- -

Non-payroll

- -

Vacancies

- - - + ); } diff --git a/front_end/src/Components/EditPayroll/EmployeeRow/index.jsx b/front_end/src/Components/EditPayroll/EmployeeRow/index.jsx index 086008e6..0463ea4d 100644 --- a/front_end/src/Components/EditPayroll/EmployeeRow/index.jsx +++ b/front_end/src/Components/EditPayroll/EmployeeRow/index.jsx @@ -4,12 +4,7 @@ const EmployeeRow = ({ row, onTogglePayPeriods }) => { return ( {row.name} - {row.grade} {row.employee_no} - {row.fte} - {row.programme_code} - {row.budget_type} - {row.assignment_status} {row.pay_periods.map((enabled, index) => { return ( diff --git a/front_end/src/Components/EditPayroll/constants.js b/front_end/src/Components/EditPayroll/constants.js deleted file mode 100644 index a2a362f2..00000000 --- a/front_end/src/Components/EditPayroll/constants.js +++ /dev/null @@ -1,35 +0,0 @@ -const monthHeaders = [ - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - "Jan", - "Feb", - "Mar", -]; - -export const payrollHeaders = [ - "Name", - "Grade", - "Employee No", - "FTE", - "Programme Code", - "Budget Type", - "Assignment Status", -].concat(monthHeaders); - -export const vacancyHeaders = [ - "Recruitment Type", - "Grade", - "Programme", - "Budget Type", - "Appointee Name", - "Hiring Manager", - "HR Ref", - "Recruitment Stage", -].concat(monthHeaders); diff --git a/front_end/src/Components/EditPayroll/index.jsx b/front_end/src/Components/EditPayroll/index.jsx index e87b0f8c..f930ff0d 100644 --- a/front_end/src/Components/EditPayroll/index.jsx +++ b/front_end/src/Components/EditPayroll/index.jsx @@ -7,12 +7,50 @@ import PayrollTable from "./PayrollTable/index"; * @param {types.PayrollData[]} props.payroll * @returns */ -export default function EditPayroll({ payroll, headers, onTogglePayPeriods }) { +export default function EditPayroll({ + payroll, + onSavePayroll, + onTogglePayPeriods, + saveSuccess, +}) { + const headers = [ + "Name", + "Employee No", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + "Jan", + "Feb", + "Mar", + ]; return ( - + <> + {saveSuccess && ( +
+
+

+ Success +

+
+
+ )} + + + ); } diff --git a/front_end/src/Components/EditPayroll/types.js b/front_end/src/Components/EditPayroll/types.js index 387c4836..13c2fada 100644 --- a/front_end/src/Components/EditPayroll/types.js +++ b/front_end/src/Components/EditPayroll/types.js @@ -1,13 +1,7 @@ /** * @typedef {Object} PayrollData * @property {string} name - The employee's name. - * @property {string} grade - The employee's grade. * @property {string} employee_no - The employee's number. - * @property {number} fte - The employee's FTE. - * @property {string} programme_code - The employee's programme code. - * @property {string} budget_type - The employee's programme code budget type. - * @property {string} assignment_status - The employee's assignment status. - * @property {string} basic_pay - The employee's basic pay. * @property {boolean[]} pay_periods - Whether the employee is being paid in periods. */ diff --git a/makefile b/makefile index 7afd9ff8..0ef14953 100644 --- a/makefile +++ b/makefile @@ -41,13 +41,13 @@ create-stub-data: # Create stub data for testing $(web) $(manage) create_stub_forecast_data $(web) $(manage) create_stub_future_forecast_data $(web) $(manage) create_data_lake_stub_data - $(web) $(manage) populate_gift_hospitality_table $(web) $(manage) loaddata test_payroll_data $(web) $(manage) create_test_user --password=password setup: # Set up the project from scratch make down make create-stub-data + make gift-hospitality-table $(web) $(manage) create_test_user --password=password $(web) $(manage) create_test_user --email=finance-admin@test.com --group="Finance Administrator" --password=password # /PS-IGNORE $(web) $(manage) create_test_user --email=finance-bp@test.com --group="Finance Business Partner/BSCE" --password=password # /PS-IGNORE diff --git a/payroll/admin.py b/payroll/admin.py index 057b0e84..c761e0c8 100644 --- a/payroll/admin.py +++ b/payroll/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from payroll.services.payroll import employee_created, vacancy_created +from payroll.services.payroll import employee_created from .models import ( Employee, @@ -8,7 +8,6 @@ EmployeePayPeriods, PayElementType, PayElementTypeGroup, - Vacancy, ) @@ -76,21 +75,3 @@ class PayElementTypeGroupAdmin(admin.ModelAdmin): "name", "natural_code", ] - - -@admin.register(Vacancy) -class VacancyAdmin(admin.ModelAdmin): - list_display = [ - "cost_centre", - "grade", - "programme_code", - "appointee_name", - "hiring_manager", - "hr_ref", - ] - - def save_model(self, request, obj, form, change): - super().save_model(request, obj, form, change) - - if not change: - vacancy_created(obj) diff --git a/payroll/fixtures/test_payroll_data.json b/payroll/fixtures/test_payroll_data.json index 3c75997c..278efd6f 100644 --- a/payroll/fixtures/test_payroll_data.json +++ b/payroll/fixtures/test_payroll_data.json @@ -5,11 +5,8 @@ "fields": { "cost_centre": "888812", "employee_no": "00000001", - "programme_code": "338887", "first_name": "John", - "last_name": "Smith", - "grade": "Grade 7", - "assignment_status": "Active Assignment" + "last_name": "Smith" } }, { @@ -18,37 +15,8 @@ "fields": { "cost_centre": "888812", "employee_no": "00000002", - "programme_code": "338887", "first_name": "Jane", - "last_name": "Doe", - "grade": "Grade 7", - "assignment_status": "Active Contingent Assignment" - } - }, - { - "model": "payroll.employee", - "pk": 3, - "fields": { - "cost_centre": "888812", - "employee_no": "00000003", - "programme_code": "338887", - "first_name": "John", - "last_name": "Doe", - "grade": "Grade 7", - "assignment_status": "Loan Out - Non Payroll" - } - }, - { - "model": "payroll.employee", - "pk": 4, - "fields": { - "cost_centre": "888812", - "employee_no": "00000004", - "programme_code": "338887", - "first_name": "Jane", - "last_name": "Smith", - "grade": "Grade 7", - "assignment_status": "Active Assignment" + "last_name": "Doe" } }, { @@ -211,166 +179,6 @@ "period_12": true } }, - { - "model": "payroll.employeepayperiods", - "pk": 9, - "fields": { - "employee": 3, - "year": 2024, - "period_1": false, - "period_2": false, - "period_3": false, - "period_4": false, - "period_5": false, - "period_6": false, - "period_7": false, - "period_8": false, - "period_9": false, - "period_10": false, - "period_11": false, - "period_12": false - } - }, - { - "model": "payroll.employeepayperiods", - "pk": 10, - "fields": { - "employee": 3, - "year": 2025, - "period_1": false, - "period_2": false, - "period_3": false, - "period_4": false, - "period_5": false, - "period_6": false, - "period_7": false, - "period_8": false, - "period_9": false, - "period_10": false, - "period_11": false, - "period_12": false - } - }, - { - "model": "payroll.employeepayperiods", - "pk": 11, - "fields": { - "employee": 3, - "year": 2026, - "period_1": false, - "period_2": false, - "period_3": false, - "period_4": false, - "period_5": false, - "period_6": false, - "period_7": false, - "period_8": false, - "period_9": false, - "period_10": false, - "period_11": false, - "period_12": false - } - }, - { - "model": "payroll.employeepayperiods", - "pk": 12, - "fields": { - "employee": 3, - "year": 2027, - "period_1": false, - "period_2": false, - "period_3": false, - "period_4": false, - "period_5": false, - "period_6": false, - "period_7": false, - "period_8": false, - "period_9": false, - "period_10": false, - "period_11": false, - "period_12": false - } - }, - { - "model": "payroll.employeepayperiods", - "pk": 13, - "fields": { - "employee": 4, - "year": 2024, - "period_1": false, - "period_2": false, - "period_3": false, - "period_4": false, - "period_5": false, - "period_6": false, - "period_7": false, - "period_8": false, - "period_9": false, - "period_10": false, - "period_11": false, - "period_12": false - } - }, - { - "model": "payroll.employeepayperiods", - "pk": 14, - "fields": { - "employee": 4, - "year": 2025, - "period_1": false, - "period_2": false, - "period_3": false, - "period_4": false, - "period_5": false, - "period_6": false, - "period_7": false, - "period_8": false, - "period_9": false, - "period_10": false, - "period_11": false, - "period_12": false - } - }, - { - "model": "payroll.employeepayperiods", - "pk": 15, - "fields": { - "employee": 4, - "year": 2026, - "period_1": false, - "period_2": false, - "period_3": false, - "period_4": false, - "period_5": false, - "period_6": false, - "period_7": false, - "period_8": false, - "period_9": false, - "period_10": false, - "period_11": false, - "period_12": false - } - }, - { - "model": "payroll.employeepayperiods", - "pk": 16, - "fields": { - "employee": 4, - "year": 2027, - "period_1": false, - "period_2": false, - "period_3": false, - "period_4": false, - "period_5": false, - "period_6": false, - "period_7": false, - "period_8": false, - "period_9": false, - "period_10": false, - "period_11": false, - "period_12": false - } - }, { "model": "payroll.payelementtypegroup", "pk": 1, diff --git a/payroll/forms.py b/payroll/forms.py deleted file mode 100644 index 29760637..00000000 --- a/payroll/forms.py +++ /dev/null @@ -1,25 +0,0 @@ -from django import forms - -from payroll.models import Vacancy - - -class VacancyForm(forms.ModelForm): - class Meta: - model = Vacancy - fields = "__all__" - exclude = ["cost_centre"] - widgets = { - "recruitment_type": forms.Select(attrs={"class": "govuk-select"}), - "grade": forms.Select(attrs={"class": "govuk-select"}), - "recruitment_stage": forms.Select(attrs={"class": "govuk-select"}), - "programme_code": forms.Select(attrs={"class": "govuk-select"}), - "appointee_name": forms.TextInput( - attrs={"class": "govuk-input govuk-input--width-20"} - ), - "hiring_manager": forms.TextInput( - attrs={"class": "govuk-input govuk-input--width-20"} - ), - "hr_ref": forms.TextInput( - attrs={"class": "govuk-input govuk-input--width-20"} - ), - } diff --git a/payroll/migrations/0004_vacancy.py b/payroll/migrations/0004_vacancy.py deleted file mode 100644 index 32c92359..00000000 --- a/payroll/migrations/0004_vacancy.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 4.2.16 on 2024-11-11 15:38 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ( - "gifthospitality", - "0006_alter_simplehistorygiftandhospitality_options_and_more", - ), - ("chartofaccountDIT", "0015_alter_simplehistoryanalysis1_options_and_more"), - ("payroll", "0003_employee_programme_code"), - ] - - operations = [ - migrations.CreateModel( - name="Vacancy", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "programme_switch_vacancy", - models.CharField( - choices=[("PS", "Programme Switch"), ("V", "Vacancy")], - max_length=2, - verbose_name="Programme switch / Vacancy", - ), - ), - ( - "appointee_name", - models.CharField(blank=True, max_length=255, null=True), - ), - ( - "hiring_manager", - models.CharField(blank=True, max_length=255, null=True), - ), - ("hr_ref", models.CharField(blank=True, max_length=255, null=True)), - ( - "grade", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - to="gifthospitality.grade", - ), - ), - ( - "programme_code", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - to="chartofaccountDIT.programmecode", - ), - ), - ], - ), - ] diff --git a/payroll/migrations/0005_alter_vacancy_options.py b/payroll/migrations/0005_alter_vacancy_options.py deleted file mode 100644 index 50cbc1df..00000000 --- a/payroll/migrations/0005_alter_vacancy_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.16 on 2024-11-12 11:56 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("payroll", "0004_vacancy"), - ] - - operations = [ - migrations.AlterModelOptions( - name="vacancy", - options={"verbose_name_plural": "Vacancies"}, - ), - ] diff --git a/payroll/migrations/0006_alter_vacancy_programme_switch_vacancy.py b/payroll/migrations/0006_alter_vacancy_programme_switch_vacancy.py deleted file mode 100644 index 672aa3ea..00000000 --- a/payroll/migrations/0006_alter_vacancy_programme_switch_vacancy.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.2.16 on 2024-11-12 15:12 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("payroll", "0005_alter_vacancy_options"), - ] - - operations = [ - migrations.AlterField( - model_name="vacancy", - name="programme_switch_vacancy", - field=models.CharField( - choices=[ - ("programme_switch", "Programme Switch"), - ("vacancy", "Vacancy"), - ], - max_length=16, - verbose_name="Programme switch / Vacancy", - ), - ), - ] diff --git a/payroll/migrations/0007_vacancy_cost_centre.py b/payroll/migrations/0007_vacancy_cost_centre.py deleted file mode 100644 index f9ab69ab..00000000 --- a/payroll/migrations/0007_vacancy_cost_centre.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.2.16 on 2024-11-13 13:44 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("costcentre", "0008_alter_simplehistoryarchivedcostcentre_options_and_more"), - ("payroll", "0006_alter_vacancy_programme_switch_vacancy"), - ] - - operations = [ - migrations.AddField( - model_name="vacancy", - name="cost_centre", - field=models.ForeignKey( - default="888812", - on_delete=django.db.models.deletion.PROTECT, - to="costcentre.costcentre", - ), - preserve_default=False, - ), - ] diff --git a/payroll/migrations/0008_vacancy_recruitment_stage_vacancy_recruitment_type.py b/payroll/migrations/0008_vacancy_recruitment_stage_vacancy_recruitment_type.py deleted file mode 100644 index e2768cde..00000000 --- a/payroll/migrations/0008_vacancy_recruitment_stage_vacancy_recruitment_type.py +++ /dev/null @@ -1,55 +0,0 @@ -# Generated by Django 4.2.16 on 2024-11-13 16:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("payroll", "0007_vacancy_cost_centre"), - ] - - operations = [ - migrations.AddField( - model_name="vacancy", - name="recruitment_stage", - field=models.IntegerField( - choices=[ - (1, "Preparing"), - (2, "Advert (Vac ref to be provided)"), - (3, "Sift"), - (4, "Interview"), - (5, "Onboarding"), - (6, "Unsuccessful recruitment"), - (7, "Not (yet) advertised"), - (8, "Not required"), - ], - default=1, - ), - ), - migrations.AddField( - model_name="vacancy", - name="recruitment_type", - field=models.CharField( - choices=[ - ("expression_of_interest", "Expression of Interest"), - ( - "external_recruitment_non_bulk", - "External Recruitment (Non Bulk)", - ), - ( - "external_recruitment_bulk", - "External Recruitment (Bulk campaign)", - ), - ("internal_managed_move", "Internal Managed Move"), - ("internal_redeployment", "Internal Redeployment"), - ("other", "Other"), - ("inactive_post", "Inactive Post"), - ("expected_unknown_leavers", "Expected Unknown Leavers"), - ("missing_staff", "Missing Staff"), - ], - default="expression_of_interest", - max_length=29, - ), - ), - ] diff --git a/payroll/migrations/0009_remove_vacancy_programme_switch_vacancy.py b/payroll/migrations/0009_remove_vacancy_programme_switch_vacancy.py deleted file mode 100644 index 69e87876..00000000 --- a/payroll/migrations/0009_remove_vacancy_programme_switch_vacancy.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.16 on 2024-11-13 16:13 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("payroll", "0008_vacancy_recruitment_stage_vacancy_recruitment_type"), - ] - - operations = [ - migrations.RemoveField( - model_name="vacancy", - name="programme_switch_vacancy", - ), - ] diff --git a/payroll/migrations/0010_alter_vacancy_appointee_name_and_more.py b/payroll/migrations/0010_alter_vacancy_appointee_name_and_more.py deleted file mode 100644 index 23dd74ba..00000000 --- a/payroll/migrations/0010_alter_vacancy_appointee_name_and_more.py +++ /dev/null @@ -1,59 +0,0 @@ -# Generated by Django 4.2.16 on 2024-11-18 12:07 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("payroll", "0009_remove_vacancy_programme_switch_vacancy"), - ] - - operations = [ - migrations.AlterField( - model_name="vacancy", - name="appointee_name", - field=models.CharField( - blank=True, - max_length=255, - null=True, - validators=[ - django.core.validators.RegexValidator( - message="Only letters, spaces, - and ' are allowed", - regex="^[a-zA-Z '-]*$", - ) - ], - ), - ), - migrations.AlterField( - model_name="vacancy", - name="hiring_manager", - field=models.CharField( - blank=True, - max_length=255, - null=True, - validators=[ - django.core.validators.RegexValidator( - message="Only letters, spaces, - and ' are allowed", - regex="^[a-zA-Z '-]*$", - ) - ], - ), - ), - migrations.AlterField( - model_name="vacancy", - name="hr_ref", - field=models.CharField( - blank=True, - max_length=255, - null=True, - validators=[ - django.core.validators.RegexValidator( - message="Only letters, spaces, - and ' are allowed", - regex="^[a-zA-Z '-]*$", - ) - ], - ), - ), - ] diff --git a/payroll/migrations/0011_alter_vacancy_appointee_name_and_more.py b/payroll/migrations/0011_alter_vacancy_appointee_name_and_more.py deleted file mode 100644 index 61a1897a..00000000 --- a/payroll/migrations/0011_alter_vacancy_appointee_name_and_more.py +++ /dev/null @@ -1,59 +0,0 @@ -# Generated by Django 4.2.16 on 2024-11-18 13:52 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("payroll", "0010_alter_vacancy_appointee_name_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="vacancy", - name="appointee_name", - field=models.CharField( - blank=True, - max_length=255, - null=True, - validators=[ - django.core.validators.RegexValidator( - message="Only letters, spaces, - and ' are allowed.", - regex="^[a-zA-Z '-]*$", - ) - ], - ), - ), - migrations.AlterField( - model_name="vacancy", - name="hiring_manager", - field=models.CharField( - blank=True, - max_length=255, - null=True, - validators=[ - django.core.validators.RegexValidator( - message="Only letters, spaces, - and ' are allowed.", - regex="^[a-zA-Z '-]*$", - ) - ], - ), - ), - migrations.AlterField( - model_name="vacancy", - name="hr_ref", - field=models.CharField( - blank=True, - max_length=255, - null=True, - validators=[ - django.core.validators.RegexValidator( - message="Only letters, spaces, - and ' are allowed.", - regex="^[a-zA-Z '-]*$", - ) - ], - ), - ), - ] diff --git a/payroll/migrations/0012_employee_assignment_status_employee_fte_and_more.py b/payroll/migrations/0012_employee_assignment_status_employee_fte_and_more.py deleted file mode 100644 index 4a224034..00000000 --- a/payroll/migrations/0012_employee_assignment_status_employee_fte_and_more.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 4.2.16 on 2024-11-12 15:52 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ( - "gifthospitality", - "0006_alter_simplehistorygiftandhospitality_options_and_more", - ), - ("payroll", "0011_alter_vacancy_appointee_name_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="employee", - name="assignment_status", - field=models.CharField(default="Active Assignment", max_length=32), - preserve_default=False, - ), - migrations.AddField( - model_name="employee", - name="fte", - field=models.FloatField(default=1.0), - ), - migrations.AddField( - model_name="employee", - name="grade", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - to="gifthospitality.grade", - ), - ), - ] diff --git a/payroll/migrations/0013_vacancy_fte_alter_vacancy_grade_vacancypayperiods_and_more.py b/payroll/migrations/0013_vacancy_fte_alter_vacancy_grade_vacancypayperiods_and_more.py deleted file mode 100644 index a4c8dd21..00000000 --- a/payroll/migrations/0013_vacancy_fte_alter_vacancy_grade_vacancypayperiods_and_more.py +++ /dev/null @@ -1,84 +0,0 @@ -# Generated by Django 4.2.16 on 2024-11-19 16:21 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ( - "gifthospitality", - "0006_alter_simplehistorygiftandhospitality_options_and_more", - ), - ("core", "0013_alter_historicalgroup_options_and_more"), - ("payroll", "0012_employee_assignment_status_employee_fte_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="vacancy", - name="fte", - field=models.FloatField(default=1.0), - ), - migrations.AlterField( - model_name="vacancy", - name="grade", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - to="gifthospitality.grade", - ), - ), - migrations.CreateModel( - name="VacancyPayPeriods", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("period_1", models.BooleanField(default=True)), - ("period_2", models.BooleanField(default=True)), - ("period_3", models.BooleanField(default=True)), - ("period_4", models.BooleanField(default=True)), - ("period_5", models.BooleanField(default=True)), - ("period_6", models.BooleanField(default=True)), - ("period_7", models.BooleanField(default=True)), - ("period_8", models.BooleanField(default=True)), - ("period_9", models.BooleanField(default=True)), - ("period_10", models.BooleanField(default=True)), - ("period_11", models.BooleanField(default=True)), - ("period_12", models.BooleanField(default=True)), - ( - "vacancy", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - related_name="pay_periods", - to="payroll.vacancy", - ), - ), - ( - "year", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - to="core.financialyear", - ), - ), - ], - options={ - "verbose_name_plural": "vacancy pay periods", - }, - ), - migrations.AddConstraint( - model_name="vacancypayperiods", - constraint=models.UniqueConstraint( - fields=("vacancy", "year"), name="unique_vacancy_pay_periods" - ), - ), - ] diff --git a/payroll/models.py b/payroll/models.py index ff1ab681..d7186f08 100644 --- a/payroll/models.py +++ b/payroll/models.py @@ -1,51 +1,45 @@ -from django.core.validators import RegexValidator from django.db import models -from django.db.models import F, Q, Sum - - -class EmployeeQuerySet(models.QuerySet): - def with_basic_pay(self): - return self.annotate( - basic_pay=Sum( - F("pay_element__debit_amount") - F("pay_element__credit_amount"), - # TODO (FFT-107): Resolve hard-coded references to "Basic Pay" - # This might change when we get round to ingesting the data, so I'm OK - # with it staying like this for now. - filter=Q(pay_element__type__group__name="Basic Pay"), - default=0, - output_field=models.FloatField(), - ) - ) - -class Position(models.Model): - class Meta: - abstract = True +class Employee(models.Model): cost_centre = models.ForeignKey( "costcentre.CostCentre", models.PROTECT, ) + # I've been informed that an employee should only be associated to a single + # programme code. However, programme codes are actually assigned on a per pay + # element basis and in some cases an employee can be associated to multiple. This is + # seen as an edge case and we want to model it such that an employee only has a + # single programme code. We will have to handle this discrepancy somewhere. programme_code = models.ForeignKey( "chartofaccountDIT.ProgrammeCode", models.PROTECT, ) - grade = models.ForeignKey( - to="gifthospitality.Grade", - on_delete=models.PROTECT, - null=True, - blank=True, - ) - fte = models.FloatField(default=1.0) + employee_no = models.CharField(max_length=8, unique=True) + first_name = models.CharField(max_length=32) + last_name = models.CharField(max_length=32) + + def __str__(self) -> str: + return f"{self.employee_no} - {self.first_name} {self.last_name}" + def get_full_name(self) -> str: + return f"{self.first_name} {self.last_name}" -class PositionPayPeriods(models.Model): + +class EmployeePayPeriods(models.Model): class Meta: - abstract = True + verbose_name_plural = "employee pay periods" + constraints = [ + models.UniqueConstraint( + fields=("employee", "year"), + name="unique_employee_pay_periods", + ) + ] + employee = models.ForeignKey(Employee, models.PROTECT, related_name="pay_periods") year = models.ForeignKey("core.FinancialYear", models.PROTECT) # period 1 = apr, period 2 = may, etc... - # period 1 -> 12 = apr -> mar + # pariod 1 -> 12 = apr -> mar # Here is a useful text snippet: # apr period_1 # may period_2 @@ -82,42 +76,6 @@ def periods(self, value: list[bool]) -> None: setattr(self, f"period_{i + 1}", enabled) -class Employee(Position): - employee_no = models.CharField(max_length=8, unique=True) - first_name = models.CharField(max_length=32) - last_name = models.CharField(max_length=32) - assignment_status = models.CharField(max_length=32) - - # TODO: Missing fields from Admin Tool which aren't required yet. - # EU/Non-EU (from programme code model) - - objects = EmployeeQuerySet.as_manager() - - def __str__(self) -> str: - return f"{self.employee_no} - {self.first_name} {self.last_name}" - - def get_full_name(self) -> str: - return f"{self.first_name} {self.last_name}" - - -class EmployeePayPeriods(PositionPayPeriods): - class Meta: - verbose_name_plural = "employee pay periods" - constraints = [ - models.UniqueConstraint( - fields=("employee", "year"), - name="unique_employee_pay_periods", - ) - ] - - employee = models.ForeignKey(Employee, models.PROTECT, related_name="pay_periods") - - # TODO: Missing fields from Admin Tool which aren't required yet. - # capital (Real colour of money) - # recharge = models.CharField(max_length=50, null=True, blank=True) - # recharge_reason = models.CharField(max_length=100, null=True, blank=True) - - # aka "ToolTypePayment" class PayElementTypeGroup(models.Model): name = models.CharField(max_length=32, unique=True) @@ -146,93 +104,3 @@ class EmployeePayElement(models.Model): debit_amount = models.DecimalField(max_digits=9, decimal_places=2) # Support up to 9,999,999.99. credit_amount = models.DecimalField(max_digits=9, decimal_places=2) - - -class RecruitmentType(models.TextChoices): - EXPRESSION_OF_INTEREST = "expression_of_interest", "Expression of Interest" - EXTERNAL_RECRUITMENT_NON_BULK = ( - "external_recruitment_non_bulk", - "External Recruitment (Non Bulk)", - ) - EXTERNAL_RECRUITMENT_BULK = ( - "external_recruitment_bulk", - "External Recruitment (Bulk campaign)", - ) - INTERNAL_MANAGED_MOVE = "internal_managed_move", "Internal Managed Move" - INTERNAL_REDEPLOYMENT = "internal_redeployment", "Internal Redeployment" - OTHER = "other", "Other" - INACTIVE_POST = "inactive_post", "Inactive Post" - EXPECTED_UNKNOWN_LEAVERS = "expected_unknown_leavers", "Expected Unknown Leavers" - MISSING_STAFF = "missing_staff", "Missing Staff" - - -class RecruitmentStage(models.IntegerChoices): - PREPARING = 1, "Preparing" - ADVERT = 2, "Advert (Vac ref to be provided)" - SIFT = 3, "Sift" - INTERVIEW = 4, "Interview" - ONBOARDING = 5, "Onboarding" - UNSUCCESSFUL_RECRUITMENT = 6, "Unsuccessful recruitment" - NOT_YET_ADVERTISED = 7, "Not (yet) advertised" - NOT_REQUIRED = 8, "Not required" - - -class Vacancy(Position): - class Meta: - verbose_name_plural = "Vacancies" - - recruitment_type = models.CharField( - max_length=29, - choices=RecruitmentType.choices, - default=RecruitmentType.EXPRESSION_OF_INTEREST, - ) - recruitment_stage = models.IntegerField( - choices=RecruitmentStage.choices, default=RecruitmentStage.PREPARING - ) - - appointee_name = models.CharField( - max_length=255, - null=True, - blank=True, - validators=[ - RegexValidator( - regex=r"^[a-zA-Z '-]*$", - message="Only letters, spaces, - and ' are allowed.", - ) - ], - ) - hiring_manager = models.CharField( - max_length=255, - null=True, - blank=True, - validators=[ - RegexValidator( - regex=r"^[a-zA-Z '-]*$", - message="Only letters, spaces, - and ' are allowed.", - ) - ], - ) - hr_ref = models.CharField( - max_length=255, - null=True, - blank=True, - validators=[ - RegexValidator( - regex=r"^[a-zA-Z '-]*$", - message="Only letters, spaces, - and ' are allowed.", - ) - ], - ) - - -class VacancyPayPeriods(PositionPayPeriods): - class Meta: - verbose_name_plural = "vacancy pay periods" - constraints = [ - models.UniqueConstraint( - fields=("vacancy", "year"), - name="unique_vacancy_pay_periods", - ) - ] - - vacancy = models.ForeignKey(Vacancy, models.PROTECT, related_name="pay_periods") diff --git a/payroll/services/payroll.py b/payroll/services/payroll.py index 4d49683e..c7b8c6ea 100644 --- a/payroll/services/payroll.py +++ b/payroll/services/payroll.py @@ -7,43 +7,25 @@ from core.models import FinancialYear from costcentre.models import CostCentre -from ..models import Employee, EmployeePayPeriods, Vacancy, VacancyPayPeriods +from ..models import Employee, EmployeePayPeriods def employee_created(employee: Employee) -> None: """Hook to be called after an employee instance is created.""" - # Create EmployeePayPeriods records for current and future financial years. - create_pay_periods(employee) - return None + # Create EmployeePayPeriods records for current and future financial years. + create_employee_pay_periods(employee) -def vacancy_created(vacancy: Vacancy) -> None: - """Hook to be called after a vacancy instance is created.""" - # Create VacancyPayPeriods records for current and future financial years. - create_pay_periods(vacancy) return None -def create_pay_periods(instance) -> None: +def create_employee_pay_periods(employee: Employee) -> None: current_financial_year = FinancialYear.objects.current() future_financial_years = FinancialYear.objects.future() financial_years = [current_financial_year] + list(future_financial_years) - pay_periods_model, field_name = None - - if isinstance(instance, Employee): - pay_periods_model = EmployeePayPeriods - field_name = "employee" - elif isinstance(instance, Vacancy): - pay_periods_model = VacancyPayPeriods - field_name = "vacancy" - else: - raise ValueError("Unsupported instance type for creating pay periods") - for financial_year in financial_years: - pay_periods_model.objects.get_or_create( - **{field_name: instance, "year": financial_year} - ) + EmployeePayPeriods.objects.get_or_create(employee=employee, year=financial_year) def payroll_forecast_report(cost_centre: CostCentre, financial_year: FinancialYear): @@ -60,7 +42,6 @@ def payroll_forecast_report(cost_centre: CostCentre, financial_year: FinancialYe Employee.objects.filter( cost_centre=cost_centre, pay_periods__year=financial_year, - pay_element__isnull=False, ) .order_by( "programme_code", @@ -80,13 +61,7 @@ def payroll_forecast_report(cost_centre: CostCentre, financial_year: FinancialYe class EmployeePayroll(TypedDict): name: str - grade: str employee_no: str - fte: float - programme_code: str - budget_type: str - assignment_status: str - basic_pay: float pay_periods: list[bool] @@ -94,31 +69,16 @@ def get_payroll_data( cost_centre: CostCentre, financial_year: FinancialYear, ) -> Iterator[EmployeePayroll]: - qs = ( - Employee.objects.select_related( - "programme_code__budget_type", - ) - .prefetch_related( - "pay_periods", - ) - .filter( - cost_centre=cost_centre, - pay_periods__year=financial_year, - ) - .with_basic_pay() + qs = EmployeePayPeriods.objects.select_related("employee") + qs = qs.filter( + employee__cost_centre=cost_centre, + year=financial_year, ) for obj in qs: yield EmployeePayroll( - name=obj.get_full_name(), - grade=obj.grade.pk, - employee_no=obj.employee_no, - fte=obj.fte, - programme_code=obj.programme_code.pk, - budget_type=obj.programme_code.budget_type.budget_type_display, - assignment_status=obj.assignment_status, - basic_pay=obj.basic_pay, - # `first` is OK as there should only be one `pay_periods` with the filters. - pay_periods=obj.pay_periods.first().periods, + name=obj.employee.get_full_name(), + employee_no=obj.employee.employee_no, + pay_periods=obj.periods, ) @@ -155,78 +115,3 @@ def update_payroll_data( ) pay_periods.periods = payroll["pay_periods"] pay_periods.save() - - -class Vacancies(TypedDict): - id: str - grade: str - programme_code: str - recruitment_type: str - recruitment_stage: str - appointee_name: str - hiring_manager: str - hr_ref: str - pay_periods: list[bool] # Needs to be added to model - - -def get_vacancies_data( - cost_centre: CostCentre, - financial_year: FinancialYear, -) -> Iterator[Vacancies]: - qs = ( - Vacancy.objects.filter( - cost_centre=cost_centre, - pay_periods__year=financial_year, - ) - # .prefetch_related( - # "pay_periods", - # ) - ) - for obj in qs: - yield Vacancies( - grade=obj.grade.pk, - programme_code=obj.programme_code.pk, - recruitment_type=obj.get_recruitment_type_display, - recruitment_stage=obj.get_recruitment_stage_display, - appointee_name=obj.appointee_name, - hiring_manager=obj.hiring_manager, - hr_ref=obj.hr_ref, - # `first` is OK as there should only be one `pay_periods` with the filters. - # pay_periods=obj.pay_periods.first().periods, - ) - - -@transaction.atomic -def update_vacancies_data( - cost_centre: CostCentre, - financial_year: FinancialYear, - vacancies_data: list[Vacancies], -) -> None: - """Update a cost centre vacancies for a given year using the provided list. - - This function is wrapped with a transaction, so if any of the vacancy updates fail, - the whole batch will be rolled back. - - Raises: - ValueError: If a vacancy id is empty. - ValueError: If there are not 12 items in the pay_periods list. - ValueError: If any of the pay_periods are not of type bool. - """ - - for vacancy in vacancies_data: - if not vacancy["id"]: - raise ValueError("id is empty") - - if len(vacancy["pay_periods"]) != 12: - raise ValueError("pay_periods list should be of length 12") - - if not all(isinstance(x, bool) for x in vacancy["pay_periods"]): - raise ValueError("pay_periods items should be of type bool") - - pay_periods = EmployeePayPeriods.objects.get( - vacancy__id=vacancy["id"], - vacancy__cost_centre=cost_centre, - year=financial_year, - ) - pay_periods.periods = vacancy["pay_periods"] - pay_periods.save() diff --git a/payroll/templates/payroll/page/add_vacancy.html b/payroll/templates/payroll/page/add_vacancy.html deleted file mode 100644 index e824fa8e..00000000 --- a/payroll/templates/payroll/page/add_vacancy.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends "base_generic.html" %} -{% load breadcrumbs %} - -{% block title %}Create Vacancy{% endblock title %} - -{% block breadcrumbs %} - {{ block.super }} - {% breadcrumb "Choose cost centre" "payroll:choose_cost_centre" %} - {% breadcrumb "Edit payroll" "payroll:edit" cost_centre_code financial_year %} - {% breadcrumb "Create Vacancy" "" %} -{% endblock breadcrumbs %} - -{% block content %} -

Create Vacancy

- - {% include "payroll/partials/_error_summary.html" with form=form %} - -
- {% csrf_token %} -
- {% include "payroll/partials/_form_field.html" with field=form.recruitment_type %} - {% include "payroll/partials/_form_field.html" with field=form.grade %} - {% include "payroll/partials/_form_field.html" with field=form.recruitment_stage %} - {% include "payroll/partials/_form_field.html" with field=form.programme_code %} - {% include "payroll/partials/_form_field.html" with field=form.appointee_name %} - {% include "payroll/partials/_form_field.html" with field=form.hiring_manager %} - {% include "payroll/partials/_form_field.html" with field=form.hr_ref %} -
- - Back -
- -{% endblock content %} diff --git a/payroll/templates/payroll/page/edit_payroll.html b/payroll/templates/payroll/page/edit_payroll.html index 30508ae3..83fdd1b7 100644 --- a/payroll/templates/payroll/page/edit_payroll.html +++ b/payroll/templates/payroll/page/edit_payroll.html @@ -6,15 +6,18 @@ {% block breadcrumbs %} {{ block.super }} {% breadcrumb "Choose cost centre" "payroll:choose_cost_centre" %} - {% breadcrumb "Edit payroll" "" %} + {% breadcrumb "Edit payroll" "edit_payroll" %} {% endblock breadcrumbs %} {% block content %}

Edit payroll

-
-

Payroll forecast

+

Forecast

+

+ This is a temporary table to demonstrate the forecast figures. Eventually these + figures would end up in the "Edit forecast" table. +

@@ -48,39 +51,6 @@

Payroll forecast

{% endfor %}
- -

Vacancies

- - - - - - - - - - - - - - - {% for vacancy in vacancies %} - - - - - - - - - - - - {% endfor %} - -
Recruitment TypeGradeProgrammeBudget TypeAppointee NameHiring ManagerHR RefRecruitment Stage
{{ vacancy.get_recruitment_type_display }}{{ vacancy.grade }}{{ vacancy.programme_code.programme_code }}{{ vacancy.programme_code.budget_type }}{{ vacancy.appointee_name|default:"" }}{{ vacancy.hiring_manager|default:"" }}{{ vacancy.hr_ref|default:"" }}{{ vacancy.get_recruitment_stage_display }}
- - Add Vacancy {% endblock content %} {% block scripts %} diff --git a/payroll/templates/payroll/partials/_error_summary.html b/payroll/templates/payroll/partials/_error_summary.html deleted file mode 100644 index e0864dfb..00000000 --- a/payroll/templates/payroll/partials/_error_summary.html +++ /dev/null @@ -1,26 +0,0 @@ -{% if form.errors %} - -{% endif %} \ No newline at end of file diff --git a/payroll/templates/payroll/partials/_form_field.html b/payroll/templates/payroll/partials/_form_field.html deleted file mode 100644 index bbd7e8bb..00000000 --- a/payroll/templates/payroll/partials/_form_field.html +++ /dev/null @@ -1,6 +0,0 @@ -
- - {{ field }} -
diff --git a/payroll/urls.py b/payroll/urls.py index 14bef3a6..1e0d1678 100644 --- a/payroll/urls.py +++ b/payroll/urls.py @@ -23,9 +23,4 @@ ChooseCostCentreView.as_view(next_page="payroll"), name="choose_cost_centre", ), - path( - "edit///vacancies/create", - views.add_vacancy_page, - name="add_vacancy", - ), ] diff --git a/payroll/views.py b/payroll/views.py index ed326385..8e321856 100644 --- a/payroll/views.py +++ b/payroll/views.py @@ -3,14 +3,12 @@ from django.contrib.auth.mixins import UserPassesTestMixin from django.core.exceptions import PermissionDenied from django.http import HttpRequest, HttpResponse, JsonResponse -from django.shortcuts import get_object_or_404, redirect, render +from django.shortcuts import get_object_or_404 from django.template.response import TemplateResponse from django.views import View from core.models import FinancialYear from costcentre.models import CostCentre -from payroll.forms import VacancyForm -from payroll.models import Vacancy from .services import payroll as payroll_service @@ -62,7 +60,6 @@ def edit_payroll_page( payroll_forecast_report_data = payroll_service.payroll_forecast_report( cost_centre_obj, financial_year_obj ) - vacancies = Vacancy.objects.filter(cost_centre=cost_centre_code) context = { "cost_centre_code": cost_centre_obj.cost_centre_code, @@ -82,40 +79,6 @@ def edit_payroll_page( "Feb", "Mar", ], - "vacancies": vacancies, } return TemplateResponse(request, "payroll/page/edit_payroll.html", context) - - -def add_vacancy_page( - request: HttpRequest, cost_centre_code: str, financial_year: int -) -> HttpResponse: - if not request.user.is_superuser: - raise PermissionDenied - - context = { - "cost_centre_code": cost_centre_code, - "financial_year": financial_year, - } - cost_centre_obj = get_object_or_404(CostCentre, pk=cost_centre_code) - - if request.method == "POST": - form = VacancyForm(request.POST) - if form.is_valid(): - vacancy = form.save(commit=False) - vacancy.cost_centre = cost_centre_obj - vacancy.save() - - return redirect( - "payroll:edit", - cost_centre_code=cost_centre_code, - financial_year=financial_year, - ) - else: - context["form"] = form - return render(request, "payroll/page/add_vacancy.html", context) - else: - form = VacancyForm() - context["form"] = form - return render(request, "payroll/page/add_vacancy.html", context)