feat: replace action row with a FAB

This commit is contained in:
Herculino Trotta
2025-06-15 23:12:22 -03:00
parent 02f6bb0c29
commit 1d3dc3f5a2
9 changed files with 1358 additions and 501 deletions
+324 -66
View File
@@ -1,14 +1,15 @@
import datetime
from decimal import Decimal
from collections import OrderedDict
import json # Added for view tests
import json # Added for view tests
from django.db.models import Q
from django.test import TestCase, Client
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.template.defaultfilters import date as date_filter
from django.urls import reverse # Added for view tests
from dateutil.relativedelta import relativedelta # Added for date calculations
from django.urls import reverse # Added for view tests
from dateutil.relativedelta import relativedelta # Added for date calculations
from apps.currencies.models import Currency
from apps.accounts.models import Account, AccountGroup
@@ -17,44 +18,68 @@ from apps.net_worth.utils.calculate_net_worth import (
calculate_historical_currency_net_worth,
calculate_historical_account_balance,
)
# Mocking get_current_user from thread_local
from apps.common.middleware.thread_local import get_current_user, set_current_user
from apps.common.middleware.thread_local import get_current_user, write_current_user
from apps.common.models import SharedObject
User = get_user_model()
class BaseNetWorthTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(email="networthuser@example.com", password="password")
self.other_user = User.objects.create_user(email="othernetworth@example.com", password="password")
self.user = User.objects.create_user(
email="networthuser@example.com", password="password"
)
self.other_user = User.objects.create_user(
email="othernetworth@example.com", password="password"
)
# Set current user for thread_local middleware
set_current_user(self.user)
write_current_user(self.user)
self.client = Client()
self.client.login(email="networthuser@example.com", password="password")
self.currency_usd = Currency.objects.create(code="USD", name="US Dollar", decimal_places=2)
self.currency_eur = Currency.objects.create(code="EUR", name="Euro", decimal_places=2)
self.currency_usd = Currency.objects.create(
code="USD", name="US Dollar", decimal_places=2
)
self.currency_eur = Currency.objects.create(
code="EUR", name="Euro", decimal_places=2
)
self.account_group_main = AccountGroup.objects.create(name="Main Group", owner=self.user)
self.account_group_main = AccountGroup.objects.create(
name="Main Group", owner=self.user
)
self.account_usd_1 = Account.objects.create(
name="USD Account 1", currency=self.currency_usd, owner=self.user, group=self.account_group_main
name="USD Account 1",
currency=self.currency_usd,
owner=self.user,
group=self.account_group_main,
)
self.account_usd_2 = Account.objects.create(
name="USD Account 2", currency=self.currency_usd, owner=self.user, group=self.account_group_main
name="USD Account 2",
currency=self.currency_usd,
owner=self.user,
group=self.account_group_main,
)
self.account_eur_1 = Account.objects.create(
name="EUR Account 1", currency=self.currency_eur, owner=self.user, group=self.account_group_main
name="EUR Account 1",
currency=self.currency_eur,
owner=self.user,
group=self.account_group_main,
)
# Public account for visibility tests
self.account_public_usd = Account.objects.create(
name="Public USD Account", currency=self.currency_usd, visibility=Account.Visibility.PUBLIC
name="Public USD Account",
currency=self.currency_usd,
visibility=SharedObject.Visibility.public,
)
def tearDown(self):
# Clear current user
set_current_user(None)
write_current_user(None)
class CalculateNetWorthUtilsTests(BaseNetWorthTest):
@@ -63,32 +88,66 @@ class CalculateNetWorthUtilsTests(BaseNetWorthTest):
result = calculate_historical_currency_net_worth(qs)
current_month_str = date_filter(timezone.localdate(timezone.now()), "b Y")
next_month_str = date_filter(timezone.localdate(timezone.now()) + relativedelta(months=1), "b Y")
next_month_str = date_filter(
timezone.localdate(timezone.now()) + relativedelta(months=1), "b Y"
)
self.assertIn(current_month_str, result)
self.assertIn(next_month_str, result)
expected_currencies_present = {"US Dollar", "Euro"} # Based on created accounts for self.user
expected_currencies_present = {
"US Dollar",
"Euro",
} # Based on created accounts for self.user
actual_currencies_in_result = set()
if result and result[current_month_str]: # Check if current_month_str key exists and has data
if (
result and result[current_month_str]
): # Check if current_month_str key exists and has data
actual_currencies_in_result = set(result[current_month_str].keys())
self.assertTrue(expected_currencies_present.issubset(actual_currencies_in_result) or not result[current_month_str])
self.assertTrue(
expected_currencies_present.issubset(actual_currencies_in_result)
or not result[current_month_str]
)
def test_calculate_historical_currency_net_worth_single_currency(self):
Transaction.objects.create(account=self.account_usd_1, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("1000"), date=datetime.date(2023, 10, 5), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(account=self.account_usd_1, owner=self.user, type=Transaction.Type.EXPENSE, amount=Decimal("200"), date=datetime.date(2023, 10, 15), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(account=self.account_usd_2, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("300"), date=datetime.date(2023, 11, 5), reference_date=datetime.date(2023,11,1), is_paid=True)
Transaction.objects.create(
account=self.account_usd_1,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("1000"),
date=datetime.date(2023, 10, 5),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(
account=self.account_usd_1,
owner=self.user,
type=Transaction.Type.EXPENSE,
amount=Decimal("200"),
date=datetime.date(2023, 10, 15),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(
account=self.account_usd_2,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("300"),
date=datetime.date(2023, 11, 5),
reference_date=datetime.date(2023, 11, 1),
is_paid=True,
)
qs = Transaction.objects.filter(owner=self.user, account__currency=self.currency_usd)
qs = Transaction.objects.filter(
owner=self.user, account__currency=self.currency_usd
)
result = calculate_historical_currency_net_worth(qs)
oct_str = date_filter(datetime.date(2023, 10, 1), "b Y")
nov_str = date_filter(datetime.date(2023, 11, 1), "b Y")
dec_str = date_filter(datetime.date(2023, 12, 1), "b Y")
self.assertIn(oct_str, result)
self.assertEqual(result[oct_str]["US Dollar"], Decimal("800.00"))
@@ -98,12 +157,43 @@ class CalculateNetWorthUtilsTests(BaseNetWorthTest):
self.assertIn(dec_str, result)
self.assertEqual(result[dec_str]["US Dollar"], Decimal("1100.00"))
def test_calculate_historical_currency_net_worth_multi_currency(self):
Transaction.objects.create(account=self.account_usd_1, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("1000"), date=datetime.date(2023, 10, 5), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(account=self.account_eur_1, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("500"), date=datetime.date(2023, 10, 10), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(account=self.account_usd_1, owner=self.user, type=Transaction.Type.EXPENSE, amount=Decimal("100"), date=datetime.date(2023, 11, 5), reference_date=datetime.date(2023,11,1), is_paid=True)
Transaction.objects.create(account=self.account_eur_1, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("50"), date=datetime.date(2023, 11, 15), reference_date=datetime.date(2023,11,1), is_paid=True)
Transaction.objects.create(
account=self.account_usd_1,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("1000"),
date=datetime.date(2023, 10, 5),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(
account=self.account_eur_1,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("500"),
date=datetime.date(2023, 10, 10),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(
account=self.account_usd_1,
owner=self.user,
type=Transaction.Type.EXPENSE,
amount=Decimal("100"),
date=datetime.date(2023, 11, 5),
reference_date=datetime.date(2023, 11, 1),
is_paid=True,
)
Transaction.objects.create(
account=self.account_eur_1,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("50"),
date=datetime.date(2023, 11, 15),
reference_date=datetime.date(2023, 11, 1),
is_paid=True,
)
qs = Transaction.objects.filter(owner=self.user)
result = calculate_historical_currency_net_worth(qs)
@@ -117,33 +207,81 @@ class CalculateNetWorthUtilsTests(BaseNetWorthTest):
self.assertEqual(result[nov_str]["Euro"], Decimal("550.00"))
def test_calculate_historical_currency_net_worth_public_account_visibility(self):
Transaction.objects.create(account=self.account_usd_1, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("100"), date=datetime.date(2023,10,1), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(account=self.account_public_usd, type=Transaction.Type.INCOME, amount=Decimal("200"), date=datetime.date(2023,10,1), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(
account=self.account_usd_1,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("100"),
date=datetime.date(2023, 10, 1),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(
account=self.account_public_usd,
type=Transaction.Type.INCOME,
amount=Decimal("200"),
date=datetime.date(2023, 10, 1),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
qs = Transaction.objects.filter(Q(owner=self.user) | Q(account__visibility=Account.Visibility.PUBLIC))
qs = Transaction.objects.filter(
Q(owner=self.user) | Q(account__visibility=SharedObject.Visibility.public)
)
result = calculate_historical_currency_net_worth(qs)
oct_str = date_filter(datetime.date(2023, 10, 1), "b Y")
self.assertEqual(result[oct_str]["US Dollar"], Decimal("300.00"))
def test_calculate_historical_account_balance_no_transactions(self):
qs = Transaction.objects.none()
result = calculate_historical_account_balance(qs)
current_month_str = date_filter(timezone.localdate(timezone.now()), "b Y")
next_month_str = date_filter(timezone.localdate(timezone.now()) + relativedelta(months=1), "b Y")
next_month_str = date_filter(
timezone.localdate(timezone.now()) + relativedelta(months=1), "b Y"
)
self.assertIn(current_month_str, result)
self.assertIn(next_month_str, result)
if result and result[current_month_str]:
for account_name in [self.account_usd_1.name, self.account_eur_1.name, self.account_public_usd.name]:
self.assertEqual(result[current_month_str].get(account_name, Decimal(0)), Decimal("0.00"))
for account_name in [
self.account_usd_1.name,
self.account_eur_1.name,
self.account_public_usd.name,
]:
self.assertEqual(
result[current_month_str].get(account_name, Decimal(0)),
Decimal("0.00"),
)
def test_calculate_historical_account_balance_single_account(self):
Transaction.objects.create(account=self.account_usd_1, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("1000"), date=datetime.date(2023, 10, 5), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(account=self.account_usd_1, owner=self.user, type=Transaction.Type.EXPENSE, amount=Decimal("200"), date=datetime.date(2023, 10, 15), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(account=self.account_usd_1, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("50"), date=datetime.date(2023, 11, 5), reference_date=datetime.date(2023,11,1), is_paid=True)
Transaction.objects.create(
account=self.account_usd_1,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("1000"),
date=datetime.date(2023, 10, 5),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(
account=self.account_usd_1,
owner=self.user,
type=Transaction.Type.EXPENSE,
amount=Decimal("200"),
date=datetime.date(2023, 10, 15),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(
account=self.account_usd_1,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("50"),
date=datetime.date(2023, 11, 5),
reference_date=datetime.date(2023, 11, 1),
is_paid=True,
)
qs = Transaction.objects.filter(account=self.account_usd_1)
result = calculate_historical_account_balance(qs)
@@ -155,9 +293,33 @@ class CalculateNetWorthUtilsTests(BaseNetWorthTest):
self.assertEqual(result[nov_str][self.account_usd_1.name], Decimal("850.00"))
def test_calculate_historical_account_balance_multiple_accounts(self):
Transaction.objects.create(account=self.account_usd_1, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("100"), date=datetime.date(2023,10,1), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(account=self.account_eur_1, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("200"), date=datetime.date(2023,10,1), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(account=self.account_usd_1, owner=self.user, type=Transaction.Type.EXPENSE, amount=Decimal("30"), date=datetime.date(2023,11,1), reference_date=datetime.date(2023,11,1), is_paid=True)
Transaction.objects.create(
account=self.account_usd_1,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("100"),
date=datetime.date(2023, 10, 1),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(
account=self.account_eur_1,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("200"),
date=datetime.date(2023, 10, 1),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(
account=self.account_usd_1,
owner=self.user,
type=Transaction.Type.EXPENSE,
amount=Decimal("30"),
date=datetime.date(2023, 11, 1),
reference_date=datetime.date(2023, 11, 1),
is_paid=True,
)
qs = Transaction.objects.filter(owner=self.user)
result = calculate_historical_account_balance(qs)
@@ -169,12 +331,13 @@ class CalculateNetWorthUtilsTests(BaseNetWorthTest):
self.assertEqual(result[nov_str][self.account_usd_1.name], Decimal("70.00"))
self.assertEqual(result[nov_str][self.account_eur_1.name], Decimal("200.00"))
def test_date_range_handling_in_utils(self):
qs_empty = Transaction.objects.none()
today = timezone.localdate(timezone.now())
start_of_this_month_str = date_filter(today.replace(day=1), "b Y")
start_of_next_month_str = date_filter((today.replace(day=1) + relativedelta(months=1)), "b Y")
start_of_next_month_str = date_filter(
(today.replace(day=1) + relativedelta(months=1)), "b Y"
)
currency_result = calculate_historical_currency_net_worth(qs_empty)
self.assertIn(start_of_this_month_str, currency_result)
@@ -186,27 +349,66 @@ class CalculateNetWorthUtilsTests(BaseNetWorthTest):
def test_archived_account_exclusion_in_currency_net_worth(self):
archived_usd_acc = Account.objects.create(
name="Archived USD", currency=self.currency_usd, owner=self.user, is_archived=True
name="Archived USD",
currency=self.currency_usd,
owner=self.user,
is_archived=True,
)
Transaction.objects.create(
account=self.account_usd_1,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("100"),
date=datetime.date(2023, 10, 1),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(
account=archived_usd_acc,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("500"),
date=datetime.date(2023, 10, 1),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(account=self.account_usd_1, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("100"), date=datetime.date(2023,10,1), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(account=archived_usd_acc, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("500"), date=datetime.date(2023,10,1), reference_date=datetime.date(2023,10,1), is_paid=True)
qs = Transaction.objects.filter(owner=self.user, account__is_archived=False)
result = calculate_historical_currency_net_worth(qs)
oct_str = date_filter(datetime.date(2023, 10, 1), "b Y")
if oct_str in result:
self.assertEqual(result[oct_str].get("US Dollar", Decimal(0)), Decimal("100.00"))
self.assertEqual(
result[oct_str].get("US Dollar", Decimal(0)), Decimal("100.00")
)
elif result:
self.fail(f"{oct_str} not found in result, but other data exists.")
def test_archived_account_exclusion_in_account_balance(self):
archived_usd_acc = Account.objects.create(
name="Archived USD Acct Bal", currency=self.currency_usd, owner=self.user, is_archived=True
name="Archived USD Acct Bal",
currency=self.currency_usd,
owner=self.user,
is_archived=True,
)
Transaction.objects.create(
account=self.account_usd_1,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("100"),
date=datetime.date(2023, 10, 1),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(
account=archived_usd_acc,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("500"),
date=datetime.date(2023, 10, 1),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(account=self.account_usd_1, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("100"), date=datetime.date(2023,10,1), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(account=archived_usd_acc, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("500"), date=datetime.date(2023,10,1), reference_date=datetime.date(2023,10,1), is_paid=True)
qs = Transaction.objects.filter(owner=self.user)
result = calculate_historical_account_balance(qs)
@@ -214,18 +416,45 @@ class CalculateNetWorthUtilsTests(BaseNetWorthTest):
if oct_str in result:
self.assertIn(self.account_usd_1.name, result[oct_str])
self.assertEqual(result[oct_str][self.account_usd_1.name], Decimal("100.00"))
self.assertEqual(
result[oct_str][self.account_usd_1.name], Decimal("100.00")
)
self.assertNotIn(archived_usd_acc.name, result[oct_str])
elif result:
self.fail(f"{oct_str} not found in result for account balance, but other data exists.")
self.fail(
f"{oct_str} not found in result for account balance, but other data exists."
)
class NetWorthViewTests(BaseNetWorthTest):
def test_net_worth_current_view(self):
Transaction.objects.create(account=self.account_usd_1, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("1200.50"), date=datetime.date(2023, 10, 5), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(account=self.account_eur_1, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("800.75"), date=datetime.date(2023, 10, 10), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(account=self.account_usd_2, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("300.00"), date=datetime.date(2023, 9, 1), reference_date=datetime.date(2023,9,1), is_paid=False) # This is unpaid
Transaction.objects.create(
account=self.account_usd_1,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("1200.50"),
date=datetime.date(2023, 10, 5),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(
account=self.account_eur_1,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("800.75"),
date=datetime.date(2023, 10, 10),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(
account=self.account_usd_2,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("300.00"),
date=datetime.date(2023, 9, 1),
reference_date=datetime.date(2023, 9, 1),
is_paid=False,
) # This is unpaid
response = self.client.get(reverse("net_worth_current"))
self.assertEqual(response.status_code, 200)
@@ -246,16 +475,38 @@ class NetWorthViewTests(BaseNetWorthTest):
# Historical chart data in net_worth_current view uses a queryset that is NOT filtered by is_paid.
sep_str = date_filter(datetime.date(2023, 9, 1), "b Y")
if sep_str in chart_data_currency["labels"]:
usd_dataset = next((ds for ds in chart_data_currency["datasets"] if ds["label"] == "US Dollar"), None)
usd_dataset = next(
(
ds
for ds in chart_data_currency["datasets"]
if ds["label"] == "US Dollar"
),
None,
)
self.assertIsNotNone(usd_dataset)
sep_idx = chart_data_currency["labels"].index(sep_str)
# The $300 from Sep (account_usd_2) should be part of the historical calculation for the chart
self.assertEqual(usd_dataset["data"][sep_idx], 300.00)
def test_net_worth_projected_view(self):
Transaction.objects.create(account=self.account_usd_1, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("1000"), date=datetime.date(2023, 10, 5), reference_date=datetime.date(2023,10,1), is_paid=True)
Transaction.objects.create(account=self.account_usd_2, owner=self.user, type=Transaction.Type.INCOME, amount=Decimal("500"), date=datetime.date(2023, 11, 1), reference_date=datetime.date(2023,11,1), is_paid=False) # Unpaid
Transaction.objects.create(
account=self.account_usd_1,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("1000"),
date=datetime.date(2023, 10, 5),
reference_date=datetime.date(2023, 10, 1),
is_paid=True,
)
Transaction.objects.create(
account=self.account_usd_2,
owner=self.user,
type=Transaction.Type.INCOME,
amount=Decimal("500"),
date=datetime.date(2023, 11, 1),
reference_date=datetime.date(2023, 11, 1),
is_paid=False,
) # Unpaid
response = self.client.get(reverse("net_worth_projected"))
self.assertEqual(response.status_code, 200)
@@ -263,7 +514,7 @@ class NetWorthViewTests(BaseNetWorthTest):
# `currency_net_worth` in projected view also uses a queryset NOT filtered by is_paid when calling `calculate_currency_totals`.
self.assertContains(response, "US Dollar")
self.assertContains(response, "1,500.00") # 1000 (paid) + 500 (unpaid)
self.assertContains(response, "1,500.00") # 1000 (paid) + 500 (unpaid)
chart_data_currency_json = response.context.get("chart_data_currency_json")
self.assertIsNotNone(chart_data_currency_json)
@@ -275,7 +526,14 @@ class NetWorthViewTests(BaseNetWorthTest):
oct_str = date_filter(datetime.date(2023, 10, 1), "b Y")
if nov_str in chart_data_currency["labels"]:
usd_dataset = next((ds for ds in chart_data_currency["datasets"] if ds["label"] == "US Dollar"), None)
usd_dataset = next(
(
ds
for ds in chart_data_currency["datasets"]
if ds["label"] == "US Dollar"
),
None,
)
if usd_dataset:
nov_idx = chart_data_currency["labels"].index(nov_str)
# Value in Nov should be cumulative: 1000 (from Oct) + 500 (from Nov unpaid)