Skip to content

Commit

Permalink
FFT-111 Add actualisation to import actuals
Browse files Browse the repository at this point in the history
  • Loading branch information
SamDudley committed Feb 13, 2025
1 parent 325db57 commit ef25373
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 16 deletions.
1 change: 1 addition & 0 deletions config/flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ACTUALISATION = "actualisation"
17 changes: 17 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pytest
from django.contrib.auth import get_user_model


@pytest.fixture
def test_user(db):
test_user_email = "test@test.com"
test_password = "test_password"

test_user, _ = get_user_model().objects.get_or_create(
username="test_user",
email=test_user_email,
)
test_user.set_password(test_password)
test_user.save()

return test_user
3 changes: 2 additions & 1 deletion core/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .types import Months
from .types import FinancialPeriods, Months


MONTHS: Months = (
Expand All @@ -15,3 +15,4 @@
"feb",
"mar",
)
PERIODS: FinancialPeriods = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
15 changes: 15 additions & 0 deletions core/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
from typing import Literal, TypedDict


FinancialPeriods = tuple[
Literal[1],
Literal[2],
Literal[3],
Literal[4],
Literal[5],
Literal[6],
Literal[7],
Literal[8],
Literal[9],
Literal[10],
Literal[11],
Literal[12],
]

Month = Literal[
"apr",
"may",
Expand Down
49 changes: 46 additions & 3 deletions forecast/import_actuals.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import datetime
import logging

import waffle
from django.db import connection
from django.db.models import F

from config import flags
from core.constants import PERIODS
from core.import_csv import get_fk, get_fk_from_field
from core.models import FinancialYear
from forecast.models import (
Expand Down Expand Up @@ -292,8 +296,16 @@ def upload_trial_balance_report(file_upload, month_number, financial_year):
if check_financial_code.error_found:
final_status = FileUpload.PROCESSEDWITHERROR
else:
uploaded_actuals = ActualUploadMonthlyFigure.objects.filter(
financial_year=financial_year, financial_period=period_obj
)

# Now copy the newly uploaded actuals to the correct table
if year_obj.current:
if waffle.switch_is_active(flags.ACTUALISATION):
for uploaded_actual in uploaded_actuals:
actualisation(period=period_obj, actual=uploaded_actual)

copy_current_year_actuals_to_monthly_figure(period_obj, financial_year)
FinancialPeriod.objects.filter(
financial_period_code__lte=period_obj.financial_period_code
Expand All @@ -307,11 +319,42 @@ def upload_trial_balance_report(file_upload, month_number, financial_year):
if check_financial_code.warning_found:
final_status = FileUpload.PROCESSEDWITHWARNING

ActualUploadMonthlyFigure.objects.filter(
financial_year=financial_year, financial_period=period_obj
).delete()
uploaded_actuals.delete()

set_file_upload_feedback(
file_upload, f"Processed {rows_to_process} rows.", final_status
)
return True


def actualisation(period: FinancialPeriod, actual: ActualUploadMonthlyFigure) -> None:
# get the current forecast that is being turned into an actual
forecast = ForecastMonthlyFigure.objects.filter(
financial_code_id=actual.financial_code_id,
financial_year_id=actual.financial_year_id,
financial_period_id=actual.financial_period_id,
archived_status__isnull=True,
).first()

# work out how many period we have left in the financial year
periods_left = len(PERIODS) - period.financial_period_code
# handle a missing forecast object and assume a forecast amount of 0
forecast_amount = forecast.amount if forecast else 0
# work out the difference the actual will leave us with
difference = forecast_amount - actual.amount

if periods_left:
# floor divide the difference by how many periods are left in the financial year
# TODO: How should monetary values be treated with regards to rounding?
difference //= periods_left

# adjust the remaining forecast periods by the difference
for i in range(periods_left):
ForecastMonthlyFigure.objects.update_or_create(
financial_code_id=actual.financial_code_id,
financial_year_id=actual.financial_year_id,
financial_period_id=period.pk + i + 1,
archived_status=None,
defaults={"amount": F("amount") + difference},
create_defaults={"amount": difference},
)
12 changes: 8 additions & 4 deletions forecast/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,12 @@ def get_max_period(self):


class FinancialPeriod(BaseModel):
"""Financial periods: correspond
to month, but there are 3 extra
periods at the end"""
"""Financial periods: correspond to month, but there are 3 extra periods at the end.
There are 15 objects in total.
The objects are managed in migrations and therefore always available in tests.
"""

financial_period_code = models.IntegerField(primary_key=True) # April = 1
period_long_name = models.CharField(max_length=20)
Expand Down Expand Up @@ -1027,8 +1030,9 @@ class MonthlyFigureAbstract(BaseModel):
"""It contains the forecast and the actuals.
The current month defines what is Actual and what is Forecast"""

amount = models.BigIntegerField(default=0) # stored in pence
id = models.AutoField(primary_key=True)

amount = models.BigIntegerField(default=0) # stored in pence
financial_year = models.ForeignKey(
FinancialYear,
on_delete=models.PROTECT,
Expand Down
8 changes: 4 additions & 4 deletions forecast/services.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from core.constants import MONTHS
from core.constants import PERIODS
from core.models import FinancialYear
from forecast.models import FinancialCode, FinancialPeriod, ForecastMonthlyFigure

Expand Down Expand Up @@ -40,7 +40,7 @@ def update_period(self, *, period: int | FinancialPeriod, amount: int):
figure.save()

def update(self, forecast: list[int]):
assert len(forecast) == len(MONTHS)
assert len(forecast) == len(PERIODS)

for i, _ in enumerate(MONTHS):
self.update_period(period=i + 1, amount=forecast[i])
for period in PERIODS:
self.update_period(period=period, amount=forecast[period - 1])
Binary file added forecast/test/test_assets/actualisation_test.xlsx
Binary file not shown.
Loading

0 comments on commit ef25373

Please sign in to comment.