diff --git a/.env.ci b/.env.ci
index b868fd46e..f5d3c52cb 100644
--- a/.env.ci
+++ b/.env.ci
@@ -10,3 +10,4 @@ AUTHBROKER_URL=
SENTRY_ENVIRONMENT=ci
SENTRY_DSN=
CSP_REPORT_URI=
+PAYROLL={"BASIC_PAY_NAC": "71111001", "PENSION_NAC": "71111002", "ERNIC_NAC": "71111003", "VACANCY_NAC": "71111001"}
diff --git a/.env.example b/.env.example
index d0afcc561..3d8059296 100644
--- a/.env.example
+++ b/.env.example
@@ -32,6 +32,8 @@ CSP_REPORT_URI=
# Vite
VITE_DEV=True
+PAYROLL={"BASIC_PAY_NAC": "71111001", "PENSION_NAC": "71111002", "ERNIC_NAC": "71111003", "VACANCY_NAC": "71111001"}
+
# Not documented (needed?)
# RESTRICT_ADMIN=True
# PUBLIC_PATH="http://localhost:8000"
diff --git a/config/settings/base.py b/config/settings/base.py
index 4c98b9324..d63ffebbd 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -11,6 +11,7 @@
"""
import os
+from dataclasses import dataclass
from pathlib import Path
import dj_database_url
@@ -414,3 +415,16 @@ def FILTERS_VERBOSE_LOOKUPS():
CSP_REPORT_ONLY = True
CSP_REPORT_URI = env.str("CSP_REPORT_URI", default=None)
+
+
+# Payroll
+@dataclass
+class Payroll:
+ BASIC_PAY_NAC: str | None = None
+ PENSION_NAC: str | None = None
+ ERNIC_NAC: str | None = None
+ VACANCY_NAC: str | None = None
+ AVERAGE_SALARY_THRESHOLD: int = 2
+
+
+PAYROLL: Payroll = Payroll(**env.json("PAYROLL", default={}))
diff --git a/config/settings/ci.py b/config/settings/ci.py
index 3a4b59593..dcb1aa5e1 100644
--- a/config/settings/ci.py
+++ b/config/settings/ci.py
@@ -13,3 +13,7 @@
AXES_ENABLED = False
STORAGES["default"]["BACKEND"] = "django.core.files.storage.FileSystemStorage"
+
+# I'm not aware of any case where we need the history whilst running tests. This should
+# hopefully speed up the tests a little bit.
+SIMPLE_HISTORY_ENABLED = False
diff --git a/core/admin.py b/core/admin.py
index 006f84c45..7f36214df 100644
--- a/core/admin.py
+++ b/core/admin.py
@@ -12,7 +12,7 @@
from django.views.decorators.csrf import csrf_exempt
from core.export_data import export_logentry_iterator
-from core.models import CommandLog, FinancialYear
+from core.models import CommandLog, FinancialYear, PayUplift
from core.utils.export_helpers import (
export_csv_from_import,
export_to_csv,
@@ -368,6 +368,25 @@ def has_delete_permission(self, request, obj=None):
return False
+class PayUpliftAdmin(admin.ModelAdmin):
+ list_display = (
+ "financial_year",
+ "apr",
+ "may",
+ "jun",
+ "jul",
+ "aug",
+ "sep",
+ "oct",
+ "nov",
+ "dec",
+ "jan",
+ "feb",
+ "mar",
+ )
+
+
admin.site.register(LogEntry, LogEntryAdmin)
admin.site.register(FinancialYear, FinancialYearAdmin)
admin.site.register(CommandLog, CustomLogModelAdmin)
+admin.site.register(PayUplift, PayUpliftAdmin)
diff --git a/core/constants.py b/core/constants.py
new file mode 100644
index 000000000..b580d3824
--- /dev/null
+++ b/core/constants.py
@@ -0,0 +1,17 @@
+from .types import Months
+
+
+MONTHS: Months = (
+ "apr",
+ "may",
+ "jun",
+ "jul",
+ "aug",
+ "sep",
+ "oct",
+ "nov",
+ "dec",
+ "jan",
+ "feb",
+ "mar",
+)
diff --git a/core/migrations/0014_payuplift.py b/core/migrations/0014_payuplift.py
new file mode 100644
index 000000000..ffb9f470b
--- /dev/null
+++ b/core/migrations/0014_payuplift.py
@@ -0,0 +1,47 @@
+# Generated by Django 5.1.3 on 2024-12-06 15:33
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("core", "0013_alter_historicalgroup_options_and_more"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="PayUplift",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("apr", models.FloatField(default=1.0)),
+ ("may", models.FloatField(default=1.0)),
+ ("jun", models.FloatField(default=1.0)),
+ ("jul", models.FloatField(default=1.0)),
+ ("aug", models.FloatField(default=1.0)),
+ ("sep", models.FloatField(default=1.0)),
+ ("oct", models.FloatField(default=1.0)),
+ ("nov", models.FloatField(default=1.0)),
+ ("dec", models.FloatField(default=1.0)),
+ ("jan", models.FloatField(default=1.0)),
+ ("feb", models.FloatField(default=1.0)),
+ ("mar", models.FloatField(default=1.0)),
+ (
+ "financial_year",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ to="core.financialyear",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/core/models.py b/core/models.py
index 0c69ae69f..fff434a5c 100644
--- a/core/models.py
+++ b/core/models.py
@@ -3,6 +3,8 @@
from django.db import models
from simple_history import register
+from core.constants import MONTHS
+
from .metamodels import BaseModel
@@ -87,6 +89,29 @@ def __str__(self):
return str(self.financial_year_display)
+class PayUplift(models.Model):
+ @property
+ def periods(self) -> list[float]:
+ return [getattr(self, month) for month in MONTHS]
+
+ financial_year = models.ForeignKey(
+ FinancialYear,
+ on_delete=models.PROTECT,
+ )
+ apr = models.FloatField(default=1.0)
+ may = models.FloatField(default=1.0)
+ jun = models.FloatField(default=1.0)
+ jul = models.FloatField(default=1.0)
+ aug = models.FloatField(default=1.0)
+ sep = models.FloatField(default=1.0)
+ oct = models.FloatField(default=1.0)
+ nov = models.FloatField(default=1.0)
+ dec = models.FloatField(default=1.0)
+ jan = models.FloatField(default=1.0)
+ feb = models.FloatField(default=1.0)
+ mar = models.FloatField(default=1.0)
+
+
# Track changes to permissions
register(Permission, app=__package__, inherit=True)
register(get_user_model(), app=__package__, inherit=True)
diff --git a/core/templates/base_generic.html b/core/templates/base_generic.html
index d35f42160..ec802ae80 100644
--- a/core/templates/base_generic.html
+++ b/core/templates/base_generic.html
@@ -1,6 +1,7 @@
{% load util %}
{% load breadcrumbs %}
{% load forecast_permissions %}
+{% load payroll_permissions %}
{% load upload_permissions %}
{% load download_permissions %}
{% load upload_percentage_permissions %}
@@ -128,8 +129,18 @@
+ {% can_edit_payroll user as can_edit_payroll %}
+ {% if can_edit_payroll %}
+
+ {% endif %}
+
{% is_forecast_user user as is_forecast_user %}
- {% if is_forecast_user == True %}
+ {% if is_forecast_user %}
{% can_edit_at_least_one_cost_centre user as can_edit_at_least_one_cost_centre %}
- {% if can_edit_at_least_one_cost_centre == True %}
+ {% if can_edit_at_least_one_cost_centre %}
{% endif %}
{% has_mi_report_download_permission user as has_mi_report_download_permission %}
- {% if has_mi_report_download_permission == True %}
+ {% if has_mi_report_download_permission %}