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 && (
+
{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 && (
-
- )}
-
-
- >
+
);
}
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
|