Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FFT-111 Add actualisation to import actuals #620

Merged
merged 2 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ACTUALISATION = "actualisation"
2 changes: 2 additions & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"axes",
"django_chunk_upload_handlers",
"csp",
"waffle",
]

ROOT_URLCONF = "config.urls"
Expand Down Expand Up @@ -284,6 +285,7 @@ def FILTERS_VERBOSE_LOOKUPS():
"core.no_cache_middleware.NoCacheMiddleware",
"simple_history.middleware.HistoryRequestMiddleware",
"axes.middleware.AxesMiddleware",
"waffle.middleware.WaffleMiddleware",
"core.middleware.CoreRequestDataMiddleware",
]

Expand Down
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 = "[email protected]"
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 not shown.
Loading
Loading