diff --git a/config/settings/base.py b/config/settings/base.py index 2fa2e73a..94014a00 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -40,6 +40,7 @@ VCAP_SERVICES = env.json("VCAP_SERVICES", {}) INSTALLED_APPS = [ + "staff.apps.StaffConfig", "user", "authbroker_client", "future_years.apps.FutureYearsConfig", diff --git a/config/urls.py b/config/urls.py index 9b85eacb..ebe6dd1c 100644 --- a/config/urls.py +++ b/config/urls.py @@ -35,6 +35,7 @@ path("data-lake/", include("data_lake.urls")), path("oscar_return/", include("oscar_return.urls")), path("upload_split_file/", include("upload_split_file.urls")), + path("staff/", include("staff.urls")), path("admin/", admin.site.urls), # TODO - split below out into develop only? path( diff --git a/core/models.py b/core/models.py index ee7ce1a7..0c69ae69 100644 --- a/core/models.py +++ b/core/models.py @@ -61,6 +61,17 @@ def future_year_dictionary(self): ) +class FinancialYearQuerySet(models.QuerySet): + def current(self): + return self.filter(current=True).first() + + def future(self): + current_financial_year = self.current().financial_year + return self.filter(financial_year__gt=current_financial_year).order_by( + "-financial_year" + ) + + class FinancialYear(BaseModel): """Key and representation of the financial year""" @@ -69,7 +80,7 @@ class FinancialYear(BaseModel): current = models.BooleanField(default=False) archived = models.BooleanField(default=False) archived_at = models.DateTimeField(blank=True, null=True) - objects = models.Manager() # The default manager. + objects = FinancialYearQuerySet.as_manager() financial_year_objects = FinancialYearManager() def __str__(self): diff --git a/docker-compose.yml b/docker-compose.yml index 0f0f4878..e93709a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3" - services: web: build: diff --git a/forecast/models.py b/forecast/models.py index 89180b7b..3508ae53 100644 --- a/forecast/models.py +++ b/forecast/models.py @@ -153,6 +153,11 @@ def __str__(self): return self.forecast_expenditure_type_name +class FinancialPeriodQuerySet(models.QuerySet): + def months(self): + return self.filter(financial_period_code__lte=12) + + class FinancialPeriodManager(models.Manager): def month_display_list(self): return list( @@ -296,7 +301,7 @@ class FinancialPeriod(BaseModel): actual_loaded_previous_year = models.BooleanField(default=False) display_figure = models.BooleanField(default=True) - objects = models.Manager() # The default manager. + objects = FinancialPeriodQuerySet.as_manager() financial_period_info = FinancialPeriodManager() class Meta: diff --git a/staff/__init__.py b/staff/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/staff/admin.py b/staff/admin.py new file mode 100644 index 00000000..e2c9629f --- /dev/null +++ b/staff/admin.py @@ -0,0 +1,61 @@ +from django.contrib import admin + +from staff.services.staff import staff_created + +from .models import Staff, StaffForecast, Payroll, PayElement, PayElementGroup + + +@admin.register(Staff) +class StaffAdmin(admin.ModelAdmin): + list_display = [ + "employee_no", + "first_name", + "last_name", + "cost_centre", + ] + + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + + if not change: + staff_created(obj) + + +@admin.register(StaffForecast) +class StaffForecastAdmin(admin.ModelAdmin): + list_display = [ + "staff", + "year", + "period_1", + "period_2", + "period_3", + "period_4", + "period_5", + "period_6", + "period_7", + "period_8", + "period_9", + "period_10", + "period_11", + "period_12", + ] + + +@admin.register(Payroll) +class PayrollAdmin(admin.ModelAdmin): + list_display = [ + "staff", + "pay_element", + "debit_amount", + "credit_amount", + ] + + +@admin.register(PayElement) +class PayElementAdmin(admin.ModelAdmin): + pass + + +@admin.register(PayElementGroup) +class PayElementGroupAdmin(admin.ModelAdmin): + pass diff --git a/staff/apps.py b/staff/apps.py new file mode 100644 index 00000000..7a459066 --- /dev/null +++ b/staff/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class StaffConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "staff" diff --git a/staff/fixtures/test_staff_data.json b/staff/fixtures/test_staff_data.json new file mode 100644 index 00000000..40d9e26b --- /dev/null +++ b/staff/fixtures/test_staff_data.json @@ -0,0 +1,322 @@ +[ + { + "model": "staff.staff", + "pk": 1, + "fields": { + "cost_centre": "888812", + "employee_no": "00000001", + "first_name": "John", + "last_name": "Smith" + } + }, + { + "model": "staff.staff", + "pk": 2, + "fields": { + "cost_centre": "888812", + "employee_no": "00000002", + "first_name": "Jane", + "last_name": "Doe" + } + }, + { + "model": "staff.staffforecast", + "pk": 1, + "fields": { + "staff": 1, + "year": 2024, + "period_1": true, + "period_2": true, + "period_3": true, + "period_4": true, + "period_5": true, + "period_6": true, + "period_7": true, + "period_8": true, + "period_9": true, + "period_10": true, + "period_11": true, + "period_12": true + } + }, + { + "model": "staff.staffforecast", + "pk": 2, + "fields": { + "staff": 1, + "year": 2025, + "period_1": true, + "period_2": true, + "period_3": true, + "period_4": true, + "period_5": true, + "period_6": true, + "period_7": true, + "period_8": true, + "period_9": true, + "period_10": true, + "period_11": true, + "period_12": true + } + }, + { + "model": "staff.staffforecast", + "pk": 3, + "fields": { + "staff": 1, + "year": 2026, + "period_1": true, + "period_2": true, + "period_3": true, + "period_4": true, + "period_5": true, + "period_6": true, + "period_7": true, + "period_8": true, + "period_9": true, + "period_10": true, + "period_11": true, + "period_12": true + } + }, + { + "model": "staff.staffforecast", + "pk": 4, + "fields": { + "staff": 1, + "year": 2027, + "period_1": true, + "period_2": true, + "period_3": true, + "period_4": true, + "period_5": true, + "period_6": true, + "period_7": true, + "period_8": true, + "period_9": true, + "period_10": true, + "period_11": true, + "period_12": true + } + }, + { + "model": "staff.staffforecast", + "pk": 5, + "fields": { + "staff": 2, + "year": 2024, + "period_1": true, + "period_2": true, + "period_3": true, + "period_4": true, + "period_5": true, + "period_6": true, + "period_7": true, + "period_8": true, + "period_9": true, + "period_10": true, + "period_11": true, + "period_12": true + } + }, + { + "model": "staff.staffforecast", + "pk": 6, + "fields": { + "staff": 2, + "year": 2025, + "period_1": true, + "period_2": true, + "period_3": true, + "period_4": true, + "period_5": true, + "period_6": true, + "period_7": true, + "period_8": true, + "period_9": true, + "period_10": true, + "period_11": true, + "period_12": true + } + }, + { + "model": "staff.staffforecast", + "pk": 7, + "fields": { + "staff": 2, + "year": 2026, + "period_1": true, + "period_2": true, + "period_3": true, + "period_4": true, + "period_5": true, + "period_6": true, + "period_7": true, + "period_8": true, + "period_9": true, + "period_10": true, + "period_11": true, + "period_12": true + } + }, + { + "model": "staff.staffforecast", + "pk": 8, + "fields": { + "staff": 2, + "year": 2027, + "period_1": true, + "period_2": true, + "period_3": true, + "period_4": true, + "period_5": true, + "period_6": true, + "period_7": true, + "period_8": true, + "period_9": true, + "period_10": true, + "period_11": true, + "period_12": true + } + }, + { + "model": "staff.payelementgroup", + "pk": 1, + "fields": { + "name": "Basic Pay", + "natural_code": 71111001 + } + }, + { + "model": "staff.payelementgroup", + "pk": 2, + "fields": { + "name": "ERNIC", + "natural_code": 71111002 + } + }, + { + "model": "staff.payelementgroup", + "pk": 3, + "fields": { + "name": "Superannuation", + "natural_code": 71111003 + } + }, + { + "model": "staff.payelement", + "pk": 1, + "fields": { + "name": "Basic Pay 1", + "natural_code": 71111001, + "group": 1 + } + }, + { + "model": "staff.payelement", + "pk": 2, + "fields": { + "name": "Basic Pay 2", + "natural_code": 71111001, + "group": 1 + } + }, + { + "model": "staff.payelement", + "pk": 3, + "fields": { + "name": "ERNIC", + "natural_code": 71111002, + "group": 2 + } + }, + { + "model": "staff.payelement", + "pk": 4, + "fields": { + "name": "Superannuation", + "natural_code": 71111003, + "group": 3 + } + }, + { + "model": "staff.payroll", + "pk": 1, + "fields": { + "staff": 1, + "pay_element": 1, + "debit_amount": "2500.00", + "credit_amount": "500.00" + } + }, + { + "model": "staff.payroll", + "pk": 2, + "fields": { + "staff": 1, + "pay_element": 2, + "debit_amount": "300.00", + "credit_amount": "0.00" + } + }, + { + "model": "staff.payroll", + "pk": 3, + "fields": { + "staff": 1, + "pay_element": 3, + "debit_amount": "100.00", + "credit_amount": "10.00" + } + }, + { + "model": "staff.payroll", + "pk": 4, + "fields": { + "staff": 1, + "pay_element": 4, + "debit_amount": "400.00", + "credit_amount": "0.00" + } + }, + { + "model": "staff.payroll", + "pk": 5, + "fields": { + "staff": 2, + "pay_element": 1, + "debit_amount": "3000.00", + "credit_amount": "300.00" + } + }, + { + "model": "staff.payroll", + "pk": 6, + "fields": { + "staff": 2, + "pay_element": 2, + "debit_amount": "0.00", + "credit_amount": "20.00" + } + }, + { + "model": "staff.payroll", + "pk": 7, + "fields": { + "staff": 2, + "pay_element": 3, + "debit_amount": "500.00", + "credit_amount": "0.00" + } + }, + { + "model": "staff.payroll", + "pk": 8, + "fields": { + "staff": 2, + "pay_element": 4, + "debit_amount": "600.00", + "credit_amount": "0.00" + } + } +] diff --git a/staff/migrations/0001_initial.py b/staff/migrations/0001_initial.py new file mode 100644 index 00000000..8bd72df2 --- /dev/null +++ b/staff/migrations/0001_initial.py @@ -0,0 +1,170 @@ +# Generated by Django 4.2.15 on 2024-10-08 11:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("chartofaccountDIT", "0015_alter_simplehistoryanalysis1_options_and_more"), + ("core", "0013_alter_historicalgroup_options_and_more"), + ("costcentre", "0008_alter_simplehistoryarchivedcostcentre_options_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="PayElement", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=128, unique=True)), + ], + ), + migrations.CreateModel( + name="Staff", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("employee_no", models.CharField(max_length=8, unique=True)), + ("first_name", models.CharField(max_length=32)), + ("last_name", models.CharField(max_length=32)), + ( + "cost_centre", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="costcentre.costcentre", + ), + ), + ], + ), + migrations.CreateModel( + name="StaffForecast", + 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)), + ( + "staff", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="forecast", + to="staff.staff", + ), + ), + ( + "year", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="core.financialyear", + ), + ), + ], + ), + migrations.CreateModel( + name="Payroll", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("debit_amount", models.DecimalField(decimal_places=2, max_digits=9)), + ("credit_amount", models.DecimalField(decimal_places=2, max_digits=9)), + ( + "pay_element", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="staff.payelement", + ), + ), + ( + "staff", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="staff.staff" + ), + ), + ], + ), + migrations.CreateModel( + name="PayElementGroup", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=32, unique=True)), + ( + "natural_code", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="chartofaccountDIT.naturalcode", + ), + ), + ], + ), + migrations.AddField( + model_name="payelement", + name="group", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="staff.payelementgroup" + ), + ), + migrations.AddField( + model_name="payelement", + name="natural_code", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="chartofaccountDIT.naturalcode", + ), + ), + migrations.AddConstraint( + model_name="staffforecast", + constraint=models.UniqueConstraint( + fields=("staff", "year"), name="unique_staff_forecast" + ), + ), + ] diff --git a/staff/migrations/__init__.py b/staff/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/staff/models.py b/staff/models.py new file mode 100644 index 00000000..d8e3b60e --- /dev/null +++ b/staff/models.py @@ -0,0 +1,76 @@ +from django.db import models + + +class Staff(models.Model): + cost_centre = models.ForeignKey("costcentre.CostCentre", models.PROTECT) + 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}" + + +class StaffForecast(models.QuerySet): + pass + + +class StaffForecast(models.Model): + class Meta: + constraints = [ + models.UniqueConstraint( + fields=("staff", "year"), + name="unique_staff_forecast", + ) + ] + + objects = StaffForecast.as_manager() + + staff = models.ForeignKey(Staff, models.PROTECT, related_name="forecast") + year = models.ForeignKey("core.FinancialYear", models.PROTECT) + # period 1 = apr, period 2 = may, etc... + # pariod 1 -> 12 = apr -> mar + 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) + + @property + def periods(self) -> list[bool]: + return [getattr(self, f"period_{i + 1}") for i in range(12)] + + +# aka "ToolTypePayment" +class PayElementGroup(models.Model): + name = models.CharField(max_length=32, unique=True) + natural_code = models.ForeignKey("chartofaccountDIT.NaturalCode", models.PROTECT) + + def __str__(self) -> str: + return self.name + + +class PayElement(models.Model): + name = models.CharField(max_length=128, unique=True) + # aka "account code" + natural_code = models.ForeignKey("chartofaccountDIT.NaturalCode", models.PROTECT) + group = models.ForeignKey(PayElementGroup, models.PROTECT) + + def __str__(self) -> str: + return self.name + + +class Payroll(models.Model): + staff = models.ForeignKey(Staff, models.PROTECT) + pay_element = models.ForeignKey(PayElement, models.PROTECT) + # Support up to 9,999,999.99. + 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) diff --git a/staff/services/staff.py b/staff/services/staff.py new file mode 100644 index 00000000..450c2b46 --- /dev/null +++ b/staff/services/staff.py @@ -0,0 +1,65 @@ +from decimal import Decimal +from django.db.models import F, Q, Sum + +from costcentre.models import CostCentre +from staff.models import PayElementGroup, Staff, StaffForecast +from forecast.models import FinancialCode, ForecastingDataView +from core.models import FinancialYear + + +def staff_created(staff: Staff) -> None: + """Hook to be called after a staff instance is created.""" + + # Create StaffForecast records for current and future financial years. + create_staff_forecast(staff) + + return None + + +def create_staff_forecast(staff: Staff) -> None: + current_financial_year = FinancialYear.objects.current() + future_financial_years = FinancialYear.objects.future() + financial_years = [current_financial_year] + list(future_financial_years) + + for financial_year in financial_years: + StaffForecast.objects.get_or_create(staff=staff, year=financial_year) + + +def payroll_forecast_report(cost_centre: CostCentre) -> None: + current_financial_year = FinancialYear.objects.current() + + period_sum_annotations = { + f"period_{i+1}_sum": Sum( + F("payroll__debit_amount") - F("payroll__credit_amount"), + filter=Q(**{f"forecast__period_{i+1}": True}), + default=Decimal(0), + ) + for i in range(12) + } + + qs = ( + Staff.objects.filter( + cost_centre=cost_centre, + forecast__year=current_financial_year, + ) + .values("payroll__pay_element__group", "payroll__pay_element__group__name") + .annotate(**period_sum_annotations) + ) + + return qs + + +def cur_payroll_forecast_report(cost_centre: CostCentre) -> None: + current_financial_year = FinancialYear.objects.current() + + nacs = PayElementGroup.objects.values("natural_code") + financial_codes = FinancialCode.objects.filter( + programme__budget_type="DEL", # FIXME + cost_centre=cost_centre, + natural_account_code__in=nacs, + ) + + qs = ForecastingDataView.objects.all() + qs = qs.filter(financial_code__in=financial_codes) + qs = qs.filter(financial_year=current_financial_year.financial_year) + return qs diff --git a/staff/templates/staff/page/debug.html b/staff/templates/staff/page/debug.html new file mode 100644 index 00000000..55b72c28 --- /dev/null +++ b/staff/templates/staff/page/debug.html @@ -0,0 +1,138 @@ +{% extends "base_generic.html" %} +{% load breadcrumbs %} + +{% block title %}Staff debug{% endblock title %} + +{% block breadcrumbs %} + {{ block.super }} + {% breadcrumb "Staff debug" "edit_payroll" %} +{% endblock breadcrumbs %} + +{% block content %} + +

Staff debug

+ +

Cost centre: {{ cost_centre }}

+

Financial year: {{ financial_year }}

+ + + + + + + + + + + + + + + + + + + + + + {% for obj in staff_forecast %} + + + + {% for enabled in obj.periods %} + + {% endfor %} + + {% endfor %} + +
Employee no.NameAprMayJunJulAugSepOctNovDecJanFebMar
{{ obj.staff.employee_no }}{{ obj.staff.first_name }} {{ obj.staff.last_name }}{{ enabled }}
+ +

New forecast

+ + + + + + + + + + + + + + + + + + + + + {% for obj in new_payroll_forecast_report %} + + + + + + + + + + + + + + + + {% endfor %} + +
Pay typeAprMayJunJulAugSepOctNovDecJanFebMar
{{ obj.payroll__pay_element__group__name }}{{ obj.period_1_sum }}{{ obj.period_2_sum }}{{ obj.period_3_sum }}{{ obj.period_4_sum }}{{ obj.period_5_sum }}{{ obj.period_6_sum }}{{ obj.period_7_sum }}{{ obj.period_8_sum }}{{ obj.period_9_sum }}{{ obj.period_10_sum }}{{ obj.period_11_sum }}{{ obj.period_12_sum }}
+ +

Current forecast

+ + + + + + + + + + + + + + + + + + + + + + + + {% for obj in cur_payroll_forecast_report %} + + + + + + + + + + + + + + + + + + + {% endfor %} + +
NACProgForecast typeProject codeAprMayJunJulAugSepOctNovDecJanFebMar
{{ obj.financial_code.natural_account_code.pk }}{{ obj.financial_code.programme.pk }}{{ obj.financial_code.forecast_expenditure_type }}{{ obj.financial_code.project_code.pk }}{{ obj.apr }}{{ obj.may }}{{ obj.jun }}{{ obj.jul }}{{ obj.aug }}{{ obj.sep }}{{ obj.oct }}{{ obj.nov }}{{ obj.dec }}{{ obj.jan }}{{ obj.feb }}{{ obj.mar }}
+ +{% endblock content %} diff --git a/staff/templates/staff/page/edit_payroll.html b/staff/templates/staff/page/edit_payroll.html new file mode 100644 index 00000000..db841eaa --- /dev/null +++ b/staff/templates/staff/page/edit_payroll.html @@ -0,0 +1,15 @@ +{% extends "base_generic.html" %} +{% load breadcrumbs %} + +{% block title %}Edit payroll{% endblock title %} + +{% block breadcrumbs %} + {{ block.super }} + {% breadcrumb "Edit payroll" "edit_payroll" %} +{% endblock breadcrumbs %} + +{% block content %} + + + +{% endblock content %} diff --git a/staff/tests.py b/staff/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/staff/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/staff/urls.py b/staff/urls.py new file mode 100644 index 00000000..8746fec9 --- /dev/null +++ b/staff/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from .views import staff_debug_page, edit_payroll_page + +app_name = "staff" + +urlpatterns = [ + path("edit-payroll/", edit_payroll_page, name="edit_payroll"), + path("debug/", staff_debug_page, name="debug"), +] diff --git a/staff/views.py b/staff/views.py new file mode 100644 index 00000000..09e3d03c --- /dev/null +++ b/staff/views.py @@ -0,0 +1,50 @@ +from django.http import HttpResponse, HttpRequest +from django.template.response import TemplateResponse +from django.contrib.auth.decorators import user_passes_test + +from core.models import FinancialYear +from costcentre.models import CostCentre +from staff.services.staff import payroll_forecast_report, cur_payroll_forecast_report + +from .models import StaffForecast + + +# TODO: Remove once no longer needed. +def _user_is_superuser(user): + return user.is_superuser + + +@user_passes_test(_user_is_superuser) +def edit_payroll_page(request: HttpRequest) -> HttpResponse: + context = {} + return TemplateResponse(request, "staff/page/edit_payroll.html", context) + + +@user_passes_test(_user_is_superuser) +def staff_debug_page(request: HttpRequest) -> HttpResponse: + if request.GET.get("cost_centre"): + cost_centre = CostCentre.objects.get(pk=request.GET.get("cost_centre")) + else: + cost_centre = CostCentre.objects.first() + + if request.GET.get("financial_year"): + financial_year = FinancialYear.objects.get( + financial_year=request.GET.get("financial_year") + ) + else: + financial_year = FinancialYear.objects.current() + + staff_forecast = StaffForecast.objects.filter( + staff__cost_centre=cost_centre, + year=financial_year, + ) + + context = { + "cost_centre": cost_centre, + "financial_year": financial_year, + "staff_forecast": staff_forecast, + "new_payroll_forecast_report": payroll_forecast_report(cost_centre), + "cur_payroll_forecast_report": cur_payroll_forecast_report(cost_centre), + } + + return TemplateResponse(request, "staff/page/debug.html", context)