diff --git a/core/management/commands/create_stub_forecast_data.py b/core/management/commands/create_stub_forecast_data.py
index 9f163bf4..cd327fa1 100644
--- a/core/management/commands/create_stub_forecast_data.py
+++ b/core/management/commands/create_stub_forecast_data.py
@@ -34,7 +34,7 @@ def create_monthly_figures():
current_financial_year = FinancialYear.objects.get(current=True)
cost_centre_fk = CostCentre.objects.first()
programme_list = ProgrammeCode.objects.all()
- project_list = ProjectCode.objects.all()
+ project_list = list(ProjectCode.objects.all()) + [None]
natural_account_list = NaturalCode.objects.all()
financial_periods = FinancialPeriod.objects.exclude(
period_long_name__icontains="adj"
diff --git a/forecast/templates/forecast/edit/edit.html b/forecast/templates/forecast/edit/edit.html
index 63a0345b..8b43b699 100644
--- a/forecast/templates/forecast/edit/edit.html
+++ b/forecast/templates/forecast/edit/edit.html
@@ -38,14 +38,17 @@
{% csrf_token %}
{{ paste_form }}
-{% endblock %}
-{% block scripts %}
+ {% endblock %}
+ {% block scripts %}
+ {{ view.get_payroll_forecast_report|json_script:'payroll_forecast_data' }}
{% vite_dev_client %}
{% vite_js 'src/index.jsx' %}
diff --git a/forecast/views/edit_forecast.py b/forecast/views/edit_forecast.py
index 96b84e9b..4a20de7b 100644
--- a/forecast/views/edit_forecast.py
+++ b/forecast/views/edit_forecast.py
@@ -6,11 +6,12 @@
from django.db import transaction
from django.db.models import Exists, OuterRef, Prefetch, Q
from django.http import JsonResponse
-from django.shortcuts import redirect
+from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.views.generic.base import TemplateView
from django.views.generic.edit import FormView
+from core.models import FinancialYear
from core.utils.generic_helpers import get_current_financial_year, get_year_display
from costcentre.models import CostCentre
from forecast.forms import (
@@ -51,6 +52,7 @@
NoCostCentreCodeInURLError,
NoFinancialYearInURLError,
)
+from payroll.services import payroll as payroll_service
UNAVAILABLE_FORECAST_EDIT_TITLE = "Forecast editing is locked"
@@ -472,12 +474,10 @@ def get_context_data(self, **kwargs):
"cost_centre_code": self.cost_centre_code,
}
)
-
financial_code_serialiser = get_financial_code_serialiser(
self.cost_centre_code,
self.financial_year,
)
-
serialiser_data = financial_code_serialiser.data
forecast_dump = json.dumps(serialiser_data)
if self.financial_year == get_current_financial_year():
@@ -498,9 +498,20 @@ def get_context_data(self, **kwargs):
context["forecast_dump"] = forecast_dump
context["actuals"] = actual_data
context["period_display"] = period_display
+ context["can_toggle_payroll"] = self.request.user.is_superuser
return context
+ def get_payroll_forecast_report(self):
+ cost_centre_obj = get_object_or_404(CostCentre, pk=self.cost_centre_code)
+ financial_year_obj = get_object_or_404(FinancialYear, pk=self.financial_year)
+ queryset = payroll_service.payroll_forecast_report(
+ cost_centre_obj, financial_year_obj
+ )
+ data = list(queryset)
+
+ return data
+
@property
def future_year_display(self):
if self._future_year_display is None:
diff --git a/front_end/src/Components/Common/ToggleCheckbox/index.jsx b/front_end/src/Components/Common/ToggleCheckbox/index.jsx
new file mode 100644
index 00000000..b2001e38
--- /dev/null
+++ b/front_end/src/Components/Common/ToggleCheckbox/index.jsx
@@ -0,0 +1,26 @@
+export default function ToggleCheckbox({ toggle, handler, id, value, label }) {
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/front_end/src/Components/EditForecast/index.jsx b/front_end/src/Components/EditForecast/index.jsx
index 575b528c..ca82c1c0 100644
--- a/front_end/src/Components/EditForecast/index.jsx
+++ b/front_end/src/Components/EditForecast/index.jsx
@@ -14,6 +14,7 @@ import {
postData,
processForecastData,
} from '../../Util'
+import ToggleCheckbox from '../Common/ToggleCheckbox';
function EditForecast() {
@@ -27,12 +28,19 @@ function EditForecast() {
const editCellId = useSelector(state => state.edit.cellId);
const [sheetUpdating, setSheetUpdating] = useState(false)
+ const [isPayrollEnabled, setIsPayrollEnabled] = useState(false)
+
+ const handleIsPayrollEnabled = () => {
+ setIsPayrollEnabled(!isPayrollEnabled);
+
+ localStorage.setItem('isPayrollEnabled', JSON.stringify(!isPayrollEnabled));
+ }
useEffect(() => {
const timer = () => {
setTimeout(() => {
if (window.table_data) {
- let rows = processForecastData(window.table_data)
+ let rows = processForecastData(window.table_data, window.payroll_forecast_data, isPayrollEnabled)
dispatch({
type: SET_CELLS,
cells: rows
@@ -45,7 +53,7 @@ function EditForecast() {
}
timer()
- }, [dispatch])
+ }, [dispatch, isPayrollEnabled])
useEffect(() => {
const capturePaste = (event) => {
@@ -317,6 +325,7 @@ function EditForecast() {
return (
+ {window.can_toggle_payroll === "True" && }
{errorMessage != null &&
@@ -332,7 +341,7 @@ function EditForecast() {
}
-
+
);
}
diff --git a/front_end/src/Components/Table/index.jsx b/front_end/src/Components/Table/index.jsx
index 2cb6703a..c6360bf8 100644
--- a/front_end/src/Components/Table/index.jsx
+++ b/front_end/src/Components/Table/index.jsx
@@ -23,7 +23,7 @@ import { SET_EDITING_CELL } from '../../Reducers/Edit'
import { SET_SELECTED_ROW, SELECT_ALL, UNSELECT_ALL } from '../../Reducers/Selected'
-function Table({rowData, sheetUpdating}) {
+function Table({rowData, sheetUpdating, payrollData}) {
const dispatch = useDispatch();
const rows = useSelector(state => state.allCells.cells);
@@ -167,7 +167,7 @@ function Table({rowData, sheetUpdating}) {
{window.period_display.map((value, index) => {
- return
+ return
})}
diff --git a/front_end/src/Components/TableCell/index.jsx b/front_end/src/Components/TableCell/index.jsx
index a2ffb0c5..e42e9e4c 100644
--- a/front_end/src/Components/TableCell/index.jsx
+++ b/front_end/src/Components/TableCell/index.jsx
@@ -9,9 +9,10 @@ import {
import { SET_ERROR } from '../../Reducers/Error'
import { SET_CELLS } from '../../Reducers/Cells'
-const TableCell = ({rowIndex, cellId, cellKey, sheetUpdating}) => {
+const TableCell = ({rowIndex, cellId, cellKey, sheetUpdating, payrollData}) => {
let editing = false
+ const isPayrollEnabled = JSON.parse(localStorage.getItem('isPayrollEnabled'))
const checkValue = (val) => {
if (cellId === val) {
@@ -46,6 +47,15 @@ const TableCell = ({rowIndex, cellId, cellKey, sheetUpdating}) => {
const cell = useSelector(state => state.allCells.cells[rowIndex][cellKey]);
const editCellId = useSelector(state => state.edit.cellId, checkValue);
+ const isOverride = () => {
+ // Is override if cell exists, has an override amount and is not an actual
+ return (cell && cell.overrideAmount !== null && cell.isEditable)
+ }
+
+ if (isOverride()) {
+ cell.amount = cell.overrideAmount
+ }
+
const [isUpdating, setIsUpdating] = useState(false)
const selectedRow = useSelector(state => state.selected.selectedRow, checkSelectRow);
@@ -90,23 +100,18 @@ const TableCell = ({rowIndex, cellId, cellKey, sheetUpdating}) => {
}
const getClasses = () => {
- let editable = ''
-
- if (!isEditable) {
- editable = ' not-editable'
- }
-
- if (!cell)
- return "govuk-table__cell forecast-month-cell figure-cell " + (isSelected() ? 'selected' : '') + editable
-
- let negative = ''
-
- if (cell.amount < 0) {
- negative = " negative"
- }
-
- return "govuk-table__cell forecast-month-cell figure-cell " + (wasEdited() ? 'edited ' : '') + (isSelected() ? 'selected' : '') + editable + negative
- }
+ const classes = ["govuk-table__cell", "forecast-month-cell", "figure-cell"];
+
+ if (!isEditable) classes.push("not-editable");
+ if (isSelected()) classes.push("selected");
+ if (!cell) return classes.join(" ");
+
+ if (cell && cell.amount < 0) classes.push("negative");
+ if (isOverride()) classes.push("override");
+ if (wasEdited()) classes.push("edited");
+
+ return classes.join(" ");
+ };
const setContentState = (value) => {
var re = /^-?\d*\.?\d{0,12}$/;
@@ -155,12 +160,13 @@ const TableCell = ({rowIndex, cellId, cellKey, sheetUpdating}) => {
payload.append("amount", intAmount)
postData(
- `/forecast/update-forecast/${window.cost_centre}/${window.financial_year}`,
- payload
+ `/forecast/update-forecast/${window.cost_centre}/${window.financial_year}`,
+ payload
).then((response) => {
- setIsUpdating(false)
- if (response.status === 200) {
- let rows = processForecastData(response.data)
+ setIsUpdating(false)
+ if (response.status === 200) {
+ // TODO (FFT-100): Test paste to excel with locked payroll forecast rows
+ let rows = processForecastData(response.data, payrollData, isPayrollEnabled)
dispatch({
type: SET_CELLS,
cells: rows
@@ -256,7 +262,7 @@ const TableCell = ({rowIndex, cellId, cellKey, sheetUpdating}) => {
className={getClasses()}
id={getId()}
onDoubleClick={ () => {
- if (isEditable) {
+ if (isEditable && !isOverride()) {
dispatch(
SET_EDITING_CELL({
"cellId": cellId
diff --git a/front_end/src/Util.js b/front_end/src/Util.js
index cbaf0870..ee051040 100644
--- a/front_end/src/Util.js
+++ b/front_end/src/Util.js
@@ -117,8 +117,13 @@ export async function postData(url = '', data = {}) {
}
}
-export const processForecastData = (forecastData) => {
+export const processForecastData = (forecastData, payrollData = null, isPayrollEnabled = false) => {
let rows = [];
+ let mappedPayrollData = null
+
+ if (isPayrollEnabled) {
+ mappedPayrollData = processPayrollData(payrollData)
+ }
let financialCodeCols = [
"analysis1_code",
@@ -149,15 +154,33 @@ export const processForecastData = (forecastData) => {
colIndex++
}
+ const forecastKey = makeFinancialCodeKey(
+ rowData.programme,
+ rowData.natural_account_code,
+ rowData.analysis1_code,
+ rowData.analysis2_code,
+ rowData.project_code
+ );
+
// eslint-disable-next-line
for (const [key, monthlyFigure] of Object.entries(rowData["monthly_figures"])) {
+ let overrideAmount = null
+
+ if (isPayrollEnabled && mappedPayrollData[forecastKey]) {
+ const period = `period_${(parseInt(key)+1)}_sum`
+ // TODO (FFT-99): Decide on decimal vs pence
+ // Old code stores monetary values in pence whereas new code has used decimals.
+ overrideAmount = mappedPayrollData[forecastKey][period] * 100
+ }
+
cells[monthlyFigure.month] = {
rowIndex: rowIndex,
colIndex: colIndex,
key: monthlyFigure.month,
amount: monthlyFigure.amount,
startingAmount: monthlyFigure.starting_amount,
- isEditable: !monthlyFigure.actual
+ isEditable: !monthlyFigure.actual,
+ overrideAmount: overrideAmount,
}
colIndex++
@@ -169,6 +192,22 @@ export const processForecastData = (forecastData) => {
return rows;
}
+const processPayrollData = (payrollData) => {
+ const results = {};
+
+ for (const [key, value] of Object.entries(payrollData)) {
+ const generatedKey = makeFinancialCodeKey(value.programme_code, value.pay_element__type__group__natural_code)
+
+ results[generatedKey] = value;
+ }
+
+ return results
+}
+
+const makeFinancialCodeKey = (programme, nac, analysis1=null, analysis2=null, project=null) => {
+ return `${programme}/${nac}/${analysis1}/${analysis2}/${project}`
+}
+
/**
* Retrieves JSON data from an HTML element with the given ID.
diff --git a/front_end/styles/styles.scss b/front_end/styles/styles.scss
index ea962f76..7f909fbb 100644
--- a/front_end/styles/styles.scss
+++ b/front_end/styles/styles.scss
@@ -223,6 +223,10 @@ th {
background-color: rgba(86, 148, 202, 0.25);
}
+.override {
+ background-color: rgba(201, 155, 75, 0.25);
+}
+
.link-button {
border: none;
padding: 0 !important;