diff --git a/front_end/src/Apps/Payroll.jsx b/front_end/src/Apps/Payroll.jsx index 7da61e604..3c33784d7 100644 --- a/front_end/src/Apps/Payroll.jsx +++ b/front_end/src/Apps/Payroll.jsx @@ -1,4 +1,4 @@ -import { useEffect, useReducer, useState } from "react"; +import { useEffect, useReducer, useState, useMemo } from "react"; import EditPayroll from "../Components/EditPayroll"; import * as api from "../Components/EditPayroll/api"; @@ -6,7 +6,10 @@ import * as api from "../Components/EditPayroll/api"; const initialPayrollState = []; export default function Payroll() { - const [payroll, dispatch] = useReducer(payrollReducer, initialPayrollState); + const [allPayroll, dispatch] = useReducer( + payrollReducer, + initialPayrollState + ); const [saveSuccess, setSaveSuccess] = useState(false); useEffect(() => { @@ -19,10 +22,20 @@ 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(payroll); + await api.postPayrollData(allPayroll); setSaveSuccess(true); localStorage.setItem("saveSuccess", "true"); @@ -38,12 +51,33 @@ export default function Payroll() { } return ( - + <> + {saveSuccess && ( +
+
+

+ Success +

+
+
+ )} +

Payroll

+ +

Non-payroll

+ + + ); } diff --git a/front_end/src/Components/EditPayroll/EmployeeRow/index.jsx b/front_end/src/Components/EditPayroll/EmployeeRow/index.jsx index 0463ea4d0..086008e6f 100644 --- a/front_end/src/Components/EditPayroll/EmployeeRow/index.jsx +++ b/front_end/src/Components/EditPayroll/EmployeeRow/index.jsx @@ -4,7 +4,12 @@ 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/index.jsx b/front_end/src/Components/EditPayroll/index.jsx index f930ff0df..956770968 100644 --- a/front_end/src/Components/EditPayroll/index.jsx +++ b/front_end/src/Components/EditPayroll/index.jsx @@ -7,15 +7,15 @@ import PayrollTable from "./PayrollTable/index"; * @param {types.PayrollData[]} props.payroll * @returns */ -export default function EditPayroll({ - payroll, - onSavePayroll, - onTogglePayPeriods, - saveSuccess, -}) { +export default function EditPayroll({ payroll, onTogglePayPeriods }) { const headers = [ "Name", + "Grade", "Employee No", + "FTE", + "Programme Code", + "Budget Type", + "Assignment Status", "Apr", "May", "Jun", @@ -30,27 +30,10 @@ export default function EditPayroll({ "Mar", ]; return ( - <> - {saveSuccess && ( -
-
-

- Success -

-
-
- )} - - - + ); } diff --git a/front_end/src/Components/EditPayroll/types.js b/front_end/src/Components/EditPayroll/types.js index 13c2fadad..387c48368 100644 --- a/front_end/src/Components/EditPayroll/types.js +++ b/front_end/src/Components/EditPayroll/types.js @@ -1,7 +1,13 @@ /** * @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/payroll/migrations/0012_employee_assignment_status_employee_fte_and_more.py b/payroll/migrations/0012_employee_assignment_status_employee_fte_and_more.py new file mode 100644 index 000000000..4a2240344 --- /dev/null +++ b/payroll/migrations/0012_employee_assignment_status_employee_fte_and_more.py @@ -0,0 +1,39 @@ +# 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/models.py b/payroll/models.py index 807c36d60..c5c89322d 100644 --- a/payroll/models.py +++ b/payroll/models.py @@ -1,5 +1,21 @@ 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 Employee(models.Model): @@ -19,6 +35,19 @@ class Employee(models.Model): employee_no = models.CharField(max_length=8, unique=True) first_name = models.CharField(max_length=32) last_name = models.CharField(max_length=32) + grade = models.ForeignKey( + to="gifthospitality.Grade", + on_delete=models.PROTECT, + null=True, + blank=True, + ) + fte = models.FloatField(default=1.0) + 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}" @@ -67,6 +96,11 @@ class Meta: period_11 = models.BooleanField(default=True) period_12 = models.BooleanField(default=True) + # 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) + @property def periods(self) -> list[bool]: return [getattr(self, f"period_{i + 1}") for i in range(12)] diff --git a/payroll/services/payroll.py b/payroll/services/payroll.py index c7b8c6ea0..b34994232 100644 --- a/payroll/services/payroll.py +++ b/payroll/services/payroll.py @@ -42,6 +42,7 @@ 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", @@ -61,7 +62,13 @@ 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] @@ -69,16 +76,31 @@ def get_payroll_data( cost_centre: CostCentre, financial_year: FinancialYear, ) -> Iterator[EmployeePayroll]: - qs = EmployeePayPeriods.objects.select_related("employee") - qs = qs.filter( - employee__cost_centre=cost_centre, - year=financial_year, + 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() ) for obj in qs: yield EmployeePayroll( - name=obj.employee.get_full_name(), - employee_no=obj.employee.employee_no, - pay_periods=obj.periods, + 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, ) diff --git a/payroll/templates/payroll/page/edit_payroll.html b/payroll/templates/payroll/page/edit_payroll.html index bcdfe55ae..30508ae3f 100644 --- a/payroll/templates/payroll/page/edit_payroll.html +++ b/payroll/templates/payroll/page/edit_payroll.html @@ -11,13 +11,10 @@ {% block content %}

Edit payroll

+
-

Forecast

-

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

+

Payroll forecast

@@ -52,8 +49,7 @@

Forecast

-

Vacancies

- +

Vacancies