Skip to content

Commit

Permalink
FFT-80 Payroll override edit forecast (#539)
Browse files Browse the repository at this point in the history
Co-authored-by: Sam Dudley <[email protected]>
  • Loading branch information
CaitBarnard and SamDudley authored Nov 7, 2024
1 parent d248d77 commit a48183e
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 37 deletions.
2 changes: 1 addition & 1 deletion core/management/commands/create_stub_forecast_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 5 additions & 2 deletions forecast/templates/forecast/edit/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,17 @@
{% csrf_token %}
{{ paste_form }}
</form>
{% endblock %}
{% block scripts %}
{% endblock %}
{% block scripts %}
{{ view.get_payroll_forecast_report|json_script:'payroll_forecast_data' }}
<script>
window.actuals = {{ actuals|safe }};
window.period_display = {{ period_display|safe }};
window.table_data = {{ forecast_dump|safe }};
window.cost_centre = {{ view.cost_centre_details.cost_centre_code|safe }};
window.financial_year = {{ view.financial_year|safe }};
window.payroll_forecast_data = JSON.parse(document.getElementById("payroll_forecast_data").textContent);
window.can_toggle_payroll = "{{ can_toggle_payroll }}";
</script>
{% vite_dev_client %}
{% vite_js 'src/index.jsx' %}
Expand Down
17 changes: 14 additions & 3 deletions forecast/views/edit_forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -51,6 +52,7 @@
NoCostCentreCodeInURLError,
NoFinancialYearInURLError,
)
from payroll.services import payroll as payroll_service


UNAVAILABLE_FORECAST_EDIT_TITLE = "Forecast editing is locked"
Expand Down Expand Up @@ -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():
Expand All @@ -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:
Expand Down
26 changes: 26 additions & 0 deletions front_end/src/Components/Common/ToggleCheckbox/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export default function ToggleCheckbox({ toggle, handler, id, value, label }) {
return (
<>
<div className="govuk-form-group">
<fieldset className="govuk-fieldset" aria-describedby='toggle-hint'>
<div className="govuk-checkboxes govuk-checkboxes--small" data-module="govuk-checkboxes">
<div className="govuk-checkboxes__item">
<input
className="govuk-checkboxes__input"
id={id}
name={id}
type="checkbox"
checked={toggle}
onChange={handler}
value={value}
/>
<label className="govuk-label govuk-checkboxes__label" htmlFor={id}>
{ label }
</label>
</div>
</div>
</fieldset>
</div>
</>
);
}
15 changes: 12 additions & 3 deletions front_end/src/Components/EditForecast/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
postData,
processForecastData,
} from '../../Util'
import ToggleCheckbox from '../Common/ToggleCheckbox';


function EditForecast() {
Expand All @@ -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
Expand All @@ -45,7 +53,7 @@ function EditForecast() {
}

timer()
}, [dispatch])
}, [dispatch, isPayrollEnabled])

useEffect(() => {
const capturePaste = (event) => {
Expand Down Expand Up @@ -317,6 +325,7 @@ function EditForecast() {

return (
<Fragment>
{window.can_toggle_payroll === "True" && <ToggleCheckbox toggle={isPayrollEnabled} handler={handleIsPayrollEnabled} id="payroll-forecast" value="payroll" label="Toggle payroll forecast rows" />}
{errorMessage != null &&
<div className="govuk-error-summary" aria-labelledby="error-summary-title" role="alert" tabIndex="-1" data-module="govuk-error-summary">
<h2 className="govuk-error-summary__title" id="error-summary-title">
Expand All @@ -332,7 +341,7 @@ function EditForecast() {
</div>
}
<EditActionBar />
<Table sheetUpdating={sheetUpdating} />
<Table sheetUpdating={sheetUpdating} payrollData={window.payroll_forecast_data}/>
</Fragment>
);
}
Expand Down
4 changes: 2 additions & 2 deletions front_end/src/Components/Table/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -167,7 +167,7 @@ function Table({rowData, sheetUpdating}) {
<CellValue rowIndex={rowIndex} cellKey={"budget"} format={true} />
</InfoCell>
{window.period_display.map((value, index) => {
return <TableCell key={nanoid()} sheetUpdating={sheetUpdating} cellId={getCellId(rowIndex, value)} rowIndex={rowIndex} cellKey={value} />
return <TableCell key={nanoid()} sheetUpdating={sheetUpdating} payrollData={payrollData} cellId={getCellId(rowIndex, value)} rowIndex={rowIndex} cellKey={value} />
})}
<InfoCell className="figure-cell" rowIndex={rowIndex}>
<AggregateValue rowIndex={rowIndex} actualsOnly={false} extraClasses="" />
Expand Down
54 changes: 30 additions & 24 deletions front_end/src/Components/TableCell/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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}$/;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
43 changes: 41 additions & 2 deletions front_end/src/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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++
Expand All @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions front_end/styles/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit a48183e

Please sign in to comment.