feat(net-worth): add consolidated chart lines

This commit is contained in:
Herculino Trotta
2026-08-15 14:36:13 -03:00
parent 3bd16602a0
commit 8e09f3e8d8
3 changed files with 150 additions and 7 deletions
+81 -2
View File
@@ -1,3 +1,82 @@
from django.test import TestCase import json
import tempfile
from datetime import date
from decimal import Decimal
# Create your tests here. from django.contrib.auth import get_user_model
from django.test import TestCase, override_settings
from django.urls import reverse
from django.utils import timezone
from apps.accounts.models import Account
from apps.currencies.models import Currency, ExchangeRate
from apps.transactions.models import Transaction
@override_settings(
STATIC_ROOT=tempfile.gettempdir(),
STORAGES={
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
},
},
)
class NetWorthCurrencyChartTests(TestCase):
def test_consolidated_currency_is_a_selectable_dashed_matching_color_line(self):
user = get_user_model().objects.create_user(
email="chart@example.com", password="password"
)
usd = Currency.objects.create(code="USD", name="US Dollar", prefix="$ ")
eur = Currency.objects.create(
code="EUR", name="Euro", prefix="", exchange_currency=usd
)
usd_account = Account.all_objects.create(
name="USD account", currency=usd, owner=user
)
eur_account = Account.all_objects.create(
name="EUR account", currency=eur, owner=user
)
ExchangeRate.objects.create(
from_currency=eur,
to_currency=usd,
rate=Decimal("1.234567"),
date=timezone.now(),
)
for account, amount in ((usd_account, "100"), (eur_account, "50")):
Transaction.userless_all_objects.create(
account=account,
owner=user,
type=Transaction.Type.INCOME,
amount=Decimal(amount),
date=date(2026, 1, 15),
reference_date=date(2026, 1, 1),
is_paid=True,
)
self.client.force_login(user)
response = self.client.get(reverse("net_worth"))
self.assertEqual(response.status_code, 200)
chart_data = json.loads(response.context["chart_data_currency_json"])
datasets = {dataset["label"]: dataset for dataset in chart_data["datasets"]}
self.assertIn("US Dollar Consolidated", datasets)
regular = datasets["US Dollar"]
consolidated = datasets["US Dollar Consolidated"]
self.assertEqual(consolidated["data"], [161.73])
self.assertNotIn("borderColor", regular)
self.assertNotIn("borderColor", consolidated)
self.assertEqual(consolidated["colorSource"], "US Dollar")
self.assertEqual(consolidated["borderDash"], [12, 6])
self.assertEqual(consolidated["pointRadius"], 0)
self.assertEqual(consolidated["pointHitRadius"], 8)
self.assertContains(
response,
"showOnlyCurrencyDataset('US Dollar Consolidated', 'US Dollar')",
html=False,
)
self.assertContains(
response,
'<span class="text-start shrink">Consolidated</span>',
html=False,
)
+53
View File
@@ -3,8 +3,11 @@ import json
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.utils.translation import gettext
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from apps.currencies.models import Currency
from apps.currencies.utils.convert import convert
from apps.net_worth.utils.calculate_net_worth import ( from apps.net_worth.utils.calculate_net_worth import (
calculate_historical_currency_net_worth, calculate_historical_currency_net_worth,
calculate_historical_account_balance, calculate_historical_account_balance,
@@ -78,6 +81,17 @@ def net_worth(request):
) )
datasets = [] datasets = []
currency_models = {
currency.name: currency
for currency in Currency.objects.filter(name__in=currencies).select_related(
"exchange_currency"
)
}
consolidated_currencies = {
data["currency"]["name"]
for data in currency_net_worth.values()
if data["consolidated"]["total_final"] != data["total_final"]
}
for i, currency in enumerate(currencies): for i, currency in enumerate(currencies):
data = [ data = [
float(month_data[currency]) float(month_data[currency])
@@ -93,6 +107,45 @@ def net_worth(request):
} }
) )
if currency in consolidated_currencies:
target = currency_models[currency]
sources = [
source
for source in currency_models.values()
if source.exchange_currency_id == target.id
]
rates = {}
for source in sources:
converted, _, _, _ = convert(1, source, target)
if converted is not None:
rates[source.name] = converted
consolidated_data = [
float(
round(
month_data[currency]
+ sum(
month_data[source] * rate for source, rate in rates.items()
),
target.decimal_places,
)
)
for month_data in historical_currency_net_worth.values()
]
datasets.append(
{
"label": f"{currency} {gettext('Consolidated')}",
"data": consolidated_data,
"yAxisID": f"y{i}",
"fill": False,
"tension": 0.1,
"colorSource": currency,
"borderDash": [12, 6],
"pointRadius": 0,
"pointHitRadius": 8,
}
)
chart_data_currency = {"labels": labels, "datasets": datasets} chart_data_currency = {"labels": labels, "datasets": datasets}
chart_data_currency_json = json.dumps(chart_data_currency, cls=DjangoJSONEncoder) chart_data_currency_json = json.dumps(chart_data_currency, cls=DjangoJSONEncoder)
+16 -5
View File
@@ -56,7 +56,7 @@
<li> <li>
{% if currency.consolidated and currency.consolidated.total_final != currency.total_final %} {% if currency.consolidated and currency.consolidated.total_final != currency.total_final %}
<a class="cursor-pointer select-auto flex justify-between items-center w-full" <a class="cursor-pointer select-auto flex justify-between items-center w-full"
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }}')"> _="on click showOnlyCurrencyDataset('{{ currency.currency.name }}', '{{ currency.currency.name }}')">
<span <span
class="currency-name text-start font-mono shrink text-ellipsis">{{ currency.currency.name }}</span> class="currency-name text-start font-mono shrink text-ellipsis">{{ currency.currency.name }}</span>
<span class="text-end shrink-0"> <span class="text-end shrink-0">
@@ -80,7 +80,8 @@
</a> </a>
<ul> <ul>
<li> <li>
<a class="text-base-content/60 select-auto flex justify-between items-center w-full"> <a class="cursor-pointer text-base-content/60 select-auto flex justify-between items-center w-full"
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }} {% trans "Consolidated" %}', '{{ currency.currency.name }}')">
<span class="text-start shrink">{% trans "Consolidated" %}</span> <span class="text-start shrink">{% trans "Consolidated" %}</span>
<span class="text-end shrink-0"> <span class="text-end shrink-0">
<c-amount.display :amount="currency.consolidated.total_final" <c-amount.display :amount="currency.consolidated.total_final"
@@ -95,7 +96,7 @@
</ul> </ul>
{% else %} {% else %}
<a class="cursor-pointer select-auto flex justify-between items-center w-full" <a class="cursor-pointer select-auto flex justify-between items-center w-full"
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }}')"> _="on click showOnlyCurrencyDataset('{{ currency.currency.name }}', '{{ currency.currency.name }}')">
<span class="currency-name text-start font-mono shrink">{{ currency.currency.name }}</span> <span class="currency-name text-start font-mono shrink">{{ currency.currency.name }}</span>
<span class="text-end shrink-0"> <span class="text-end shrink-0">
<div> <div>
@@ -302,6 +303,16 @@
} }
} }
}); });
for (const dataset of currencyChart.data.datasets) {
if (!dataset.colorSource) continue;
const source = currencyChart.data.datasets.find(
candidate => candidate.label === dataset.colorSource
);
dataset.borderColor = source.borderColor;
dataset.backgroundColor = source.backgroundColor;
}
currencyChart.update('none');
} }
</script> </script>
<script id="accountBalanceChartScript"> <script id="accountBalanceChartScript">
@@ -448,7 +459,7 @@
call accountChart.update() call accountChart.update()
end end
def showOnlyCurrencyDataset(datasetName) def showOnlyCurrencyDataset(datasetName, differenceDatasetName)
for dataset in currencyChart.data.datasets for dataset in currencyChart.data.datasets
set isMatch to dataset.label is datasetName set isMatch to dataset.label is datasetName
call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), isMatch) call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), isMatch)
@@ -456,7 +467,7 @@
call currencyChart.update() call currencyChart.update()
for dataset in monthlyDifferenceChart.data.datasets for dataset in monthlyDifferenceChart.data.datasets
set isMatch to dataset.label is datasetName set isMatch to dataset.label is differenceDatasetName
call monthlyDifferenceChart.setDatasetVisibility(monthlyDifferenceChart.data.datasets.indexOf(dataset), isMatch) call monthlyDifferenceChart.setDatasetVisibility(monthlyDifferenceChart.data.datasets.indexOf(dataset), isMatch)
end end
call monthlyDifferenceChart.update() call monthlyDifferenceChart.update()