feat: improve calculations for net_worth

This commit is contained in:
Herculino Trotta
2024-11-17 11:36:32 -03:00
parent e5e002497f
commit 84bb247918
3 changed files with 56 additions and 169 deletions

View File

@@ -19,133 +19,6 @@ from apps.currencies.utils.convert import convert
from apps.transactions.models import Transaction
def calculate_account_net_worth():
ungrouped_id = None # Special ID for ungrouped accounts
# Subquery to calculate balance for each account
balance_subquery = (
Transaction.objects.filter(account=OuterRef("pk"), is_paid=True)
.values("account")
.annotate(
balance=Sum(
Case(
When(type=Transaction.Type.INCOME, then=F("amount")),
When(type=Transaction.Type.EXPENSE, then=-F("amount")),
default=0,
output_field=DecimalField(),
)
)
)
.values("balance")
)
# Main query to fetch all account data
accounts_data = (
Account.objects.filter(is_archived=False)
.annotate(balance=Coalesce(Subquery(balance_subquery), Decimal("0")))
.select_related("currency", "exchange_currency", "group")
)
account_net_worth = {ungrouped_id: {"name": _("Ungrouped"), "accounts": {}}}
for account in accounts_data:
account_data = {
"name": account.name,
"balance": account.balance,
"currency": {
"code": account.currency.code,
"name": account.currency.name,
"prefix": account.currency.prefix,
"suffix": account.currency.suffix,
"decimal_places": account.currency.decimal_places,
},
}
if account.exchange_currency:
converted_amount, prefix, suffix, decimal_places = convert(
amount=account.balance,
from_currency=account.currency,
to_currency=account.exchange_currency,
)
if converted_amount:
account_data["exchange"] = {
"amount": converted_amount,
"prefix": prefix,
"suffix": suffix,
"decimal_places": decimal_places,
}
group_id = account.group.id if account.group else ungrouped_id
group_name = account.group.name if account.group else _("Ungrouped")
if group_id not in account_net_worth:
account_net_worth[group_id] = {"name": group_name, "accounts": {}}
account_net_worth[group_id]["accounts"][account.id] = account_data
# Remove the "Ungrouped" category if it's empty
if not account_net_worth[ungrouped_id]["accounts"]:
del account_net_worth[ungrouped_id]
return account_net_worth
def calculate_currency_net_worth():
# Subquery to calculate balance for each currency
balance_subquery = (
Transaction.objects.filter(account__currency=OuterRef("pk"), is_paid=True)
.values("account__currency")
.annotate(
balance=Sum(
Case(
When(type=Transaction.Type.INCOME, then=F("amount")),
When(type=Transaction.Type.EXPENSE, then=-F("amount")),
default=0,
output_field=DecimalField(),
)
)
)
.values("balance")
)
# Fetch all currencies with their balances in a single query
currencies_data = Currency.objects.annotate(
balance=Coalesce(Subquery(balance_subquery), Decimal("0"))
).select_related(
"exchange_currency"
) # Optimize by pre-fetching exchange_currency
net_worth = {}
for currency in currencies_data:
# Skip conversion if no exchange currency is set
exchanged_value = None
if currency.exchange_currency:
exchanged_amount, ex_prefix, ex_suffix, ex_decimal_places = convert(
amount=currency.balance,
from_currency=currency,
to_currency=currency.exchange_currency,
)
exchanged_value = {
"amount": exchanged_amount,
"name": currency.exchange_currency.name,
"prefix": ex_prefix,
"suffix": ex_suffix,
"decimal_places": ex_decimal_places,
}
net_worth[currency.name] = {
"amount": currency.balance,
"code": currency.code,
"name": currency.name,
"prefix": currency.prefix,
"suffix": currency.suffix,
"decimal_places": currency.decimal_places,
"exchanged": exchanged_value,
}
return net_worth
def calculate_historical_currency_net_worth():
# Get all currencies and date range in a single query
aggregates = Transaction.objects.aggregate(

View File

@@ -9,11 +9,24 @@ from apps.net_worth.utils.calculate_net_worth import (
calculate_account_net_worth,
calculate_historical_account_balance,
)
from apps.transactions.models import Transaction
from apps.transactions.utils.calculations import (
calculate_currency_totals,
calculate_account_totals,
)
def net_worth_main(request):
currency_net_worth = calculate_currency_net_worth()
account_net_worth = calculate_account_net_worth()
transactions_queryset = Transaction.objects.filter(is_paid=True).order_by(
"account__group",
"account__name",
)
currency_net_worth = calculate_currency_totals(
transactions_queryset=transactions_queryset
)
account_net_worth = calculate_account_totals(
transactions_queryset=transactions_queryset
)
historical_currency_net_worth = calculate_historical_currency_net_worth()
@@ -83,7 +96,7 @@ def net_worth_main(request):
request,
"net_worth/net_worth.html",
{
"currency_net_worth": currency_net_worth.values(),
"currency_net_worth": currency_net_worth,
"account_net_worth": account_net_worth,
"chart_data_currency_json": chart_data_currency_json,
"currencies": currencies,

View File

@@ -21,29 +21,29 @@
</div>
<div class="card-body">
<h5 class="tw-text-yellow-400 fw-bold mb-3">{% translate 'By currency' %}</h5>
{% for currency in currency_net_worth %}
{% for currency in currency_net_worth.values %}
<div class="d-flex justify-content-between mt-2">
<div class="d-flex align-items-baseline w-100">
<div class="currency-name text-start font-monospace tw-text-gray-300">{{ currency.name }}</div>
<div class="currency-name text-start font-monospace tw-text-gray-300">{{ currency.currency.name }}</div>
<div class="dotted-line flex-grow-1"></div>
<div>
<c-amount.display
:amount="currency.amount"
:prefix="currency.prefix"
:suffix="currency.suffix"
:decimal_places="currency.decimal_places"
color="{% if currency.amount > 0 %}green{% elif currency.amount < 0 %}red{% endif %}"
:amount="currency.total_current"
:prefix="currency.currency.prefix"
:suffix="currency.currency.suffix"
:decimal_places="currency.currency.decimal_places"
color="{% if currency.total_current > 0 %}green{% elif currency.total_current < 0 %}red{% endif %}"
text-end></c-amount.display>
</div>
</div>
</div>
{% if currency.exchanged and currency.exchanged.amount %}
{% if currency.exchanged and currency.exchanged.total_current %}
<div>
<c-amount.display
:amount="currency.exchanged.amount"
:prefix="currency.exchanged.prefix"
:suffix="currency.exchanged.suffix"
:decimal_places="currency.exchanged.decimal_places"
:amount="currency.exchanged.total_current"
:prefix="currency.exchanged.currency.prefix"
:suffix="currency.exchanged.currency.suffix"
:decimal_places="currency.exchanged.currency.decimal_places"
text-end
color="grey"></c-amount.display>
</div>
@@ -60,62 +60,63 @@
</div>
<div class="card-body">
<h5 class="tw-text-blue-400 fw-bold mb-3">{% translate 'By account' %}</h5>
{% for group_id, group_data in account_net_worth.items %}
{% if group_id %}
{% regroup account_net_worth.values by account.group as account_data %}
{% for data in account_data %}
{% if data.grouper %}
<div class="d-flex justify-content-between mt-2">
<div class="d-flex align-items-baseline w-100">
<div class="text-start font-monospace tw-text-gray-300"><span class="badge text-bg-primary">
{{ group_data.name }}</span></div>
{{ data.grouper }}</span></div>
</div>
</div>
{% for account_id, account_data in group_data.accounts.items %}
{% for account in data.list %}
<div class="d-flex justify-content-between mt-2">
<div class="d-flex align-items-baseline w-100">
<div class="text-start font-monospace tw-text-gray-300">
<span class="hierarchy-line-icon"></span>{{ account_data.name }}</div>
<span class="hierarchy-line-icon"></span>{{ account.account.name }}</div>
<div class="dotted-line flex-grow-1"></div>
<div class="">
<c-amount.display
:amount="account_data.balance"
:prefix="account_data.currency.prefix"
:suffix="account_data.currency.suffix"
:decimal_places="account_data.currency.decimal_places"
color="{% if account_data.balance > 0 %}green{% elif account_data.balance < 0 %}red{% endif %}"></c-amount.display>
:amount="account.total_final"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"
color="{% if account.total_final > 0 %}green{% elif account.total_final < 0 %}red{% endif %}"></c-amount.display>
</div>
</div>
</div>
{% if account_data.exchange %}
{% if account.exchanged and account.exchanged.total_final %}
<c-amount.display
:amount="account_data.exchange.amount"
:prefix="account_data.exchange.prefix"
:suffix="account_data.exchange.suffix"
:decimal_places="account_data.exchange.decimal_places"
:amount="account.exchanged.total_final"
:prefix="account.exchanged.currency.prefix"
:suffix="account.exchanged.currency.suffix"
:decimal_places="account.exchanged.currency.decimal_places"
color="grey"
text-end></c-amount.display>
{% endif %}
{% endfor %}
{% else %}
{% for account_id, account_data in group_data.accounts.items %}
{% for account in data.list %}
<div class="d-flex justify-content-between mt-2">
<div class="d-flex align-items-baseline w-100">
<div class="currency-name text-start font-monospace tw-text-gray-300">{{ account_data.name }}</div>
<div class="currency-name text-start font-monospace tw-text-gray-300">{{ account.account.name }}</div>
<div class="dotted-line flex-grow-1"></div>
<div>
<c-amount.display
:amount="account_data.balance"
:prefix="account_data.currency.prefix"
:suffix="account_data.currency.suffix"
:decimal_places="account_data.currency.decimal_places"
color="{% if account_data.balance > 0 %}green{% elif account_data.balance < 0 %}red{% endif %}"></c-amount.display>
:amount="account.total_final"
:prefix="account.currency.prefix"
:suffix="account.currency.suffix"
:decimal_places="account.currency.decimal_places"
color="{% if account.total_final > 0 %}green{% elif account.total_final < 0 %}red{% endif %}"></c-amount.display>
</div>
</div>
</div>
{% if account_data.exchange %}
{% if account.exchanged and account.exchanged.total_final %}
<c-amount.display
:amount="account_data.exchange.amount"
:prefix="account_data.exchange.prefix"
:suffix="account_data.exchange.suffix"
:decimal_places="account_data.exchange.decimal_places"
:amount="account.exchanged.total_final"
:prefix="account.exchanged.currency.prefix"
:suffix="account.exchanged.currency.suffix"
:decimal_places="account.exchanged.currency.decimal_places"
color="grey"
text-end></c-amount.display>
{% endif %}