feat: improve yearly overview by currency calculations

This commit is contained in:
Herculino Trotta
2024-11-17 02:29:20 -03:00
parent fdd9fbc780
commit e81c0c1bb1
2 changed files with 101 additions and 237 deletions

View File

@@ -1,20 +1,18 @@
from datetime import date
from decimal import Decimal
from django.contrib.auth.decorators import login_required
from django.db.models import Sum, F, Value, DecimalField, Q
from django.db.models.expressions import Case, When
from django.db.models.functions import TruncMonth, Coalesce, TruncYear
from django.db.models import Q
from django.http import Http404
from django.shortcuts import render, redirect
from django.utils import timezone
from apps.accounts.models import Account
from apps.currencies.models import Currency
from apps.currencies.utils.convert import convert
from apps.transactions.models import Transaction
from apps.common.decorators.htmx import only_htmx
from apps.transactions.utils.calculations import calculate_account_totals
from apps.common.utils.dicts import remove_falsey_entries
from apps.currencies.models import Currency
from apps.transactions.models import Transaction
from apps.transactions.utils.calculations import (
calculate_account_totals,
calculate_currency_totals,
)
@login_required
@@ -74,162 +72,28 @@ def yearly_overview_by_currency(request, year: int):
if currency:
filter_params["account__currency_id"] = int(currency)
transactions = (
Transaction.objects.filter(**filter_params)
.exclude(Q(category__mute=True) & ~Q(category=None))
.select_related("account__currency") # Optimize by pre-fetching currency data
transactions = Transaction.objects.filter(**filter_params).exclude(
Q(category__mute=True) & ~Q(category=None)
)
date_trunc = TruncMonth("reference_date") if month else TruncYear("reference_date")
data = calculate_currency_totals(transactions)
monthly_data = (
transactions.annotate(month=date_trunc)
.values(
"month",
"account__currency__code",
"account__currency__prefix",
"account__currency__suffix",
"account__currency__decimal_places",
"account__currency_id",
"account__currency__exchange_currency_id",
)
.annotate(
income_paid=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.INCOME, is_paid=True, then=F("amount")
),
default=Value(Decimal("0")),
output_field=DecimalField(),
)
),
Value(Decimal("0")),
),
expense_paid=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.EXPENSE,
is_paid=True,
then=F("amount"),
),
default=Value(Decimal("0")),
output_field=DecimalField(),
)
),
Value(Decimal("0")),
),
income_unpaid=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.INCOME,
is_paid=False,
then=F("amount"),
),
default=Value(Decimal("0")),
output_field=DecimalField(),
)
),
Value(Decimal("0")),
),
expense_unpaid=Coalesce(
Sum(
Case(
When(
type=Transaction.Type.EXPENSE,
is_paid=False,
then=F("amount"),
),
default=Value(Decimal("0")),
output_field=DecimalField(),
)
),
Value(Decimal("0")),
),
)
.annotate(
balance_unpaid=F("income_unpaid") - F("expense_unpaid"),
balance_paid=F("income_paid") - F("expense_paid"),
balance_total=F("income_paid")
+ F("income_unpaid")
- F("expense_paid")
- F("expense_unpaid"),
)
.order_by("month", "account__currency__code")
)
# Fetch all currencies and their exchange currencies in a single query
currencies = {
currency.id: currency
for currency in Currency.objects.select_related("exchange_currency").all()
context = {
"income_current": remove_falsey_entries(data, "income_current"),
"income_projected": remove_falsey_entries(data, "income_projected"),
"expense_current": remove_falsey_entries(data, "expense_current"),
"expense_projected": remove_falsey_entries(data, "expense_projected"),
"total_current": remove_falsey_entries(data, "total_current"),
"total_final": remove_falsey_entries(data, "total_final"),
"total_projected": remove_falsey_entries(data, "total_projected"),
}
result = {
"income_paid": [],
"expense_paid": [],
"income_unpaid": [],
"expense_unpaid": [],
"balance_unpaid": [],
"balance_paid": [],
"balance_total": [],
}
for entry in monthly_data:
if all(entry[field] == 0 for field in result.keys()):
continue # Skip entries where all values are 0
currency_code = entry["account__currency__code"]
prefix = entry["account__currency__prefix"]
suffix = entry["account__currency__suffix"]
decimal_places = entry["account__currency__decimal_places"]
# Get the currency objects for conversion
from_currency = currencies.get(entry["account__currency_id"])
to_currency = (
None
if not from_currency
else currencies.get(from_currency.exchange_currency_id)
)
for field in result.keys():
amount = entry[field]
if amount == 0:
continue
item = {
"code": currency_code,
"prefix": prefix,
"suffix": suffix,
"decimal_places": decimal_places,
"amount": amount,
"exchanged": None,
}
# Add exchange calculation if possible
if from_currency and to_currency:
exchanged_amount, ex_prefix, ex_suffix, ex_decimal_places = convert(
amount=amount,
from_currency=from_currency,
to_currency=to_currency,
)
item["exchanged"] = {
"amount": exchanged_amount,
"code": to_currency.code,
"prefix": ex_prefix,
"suffix": ex_suffix,
"decimal_places": ex_decimal_places,
}
result[field].append(item)
return render(
request,
"yearly_overview/fragments/currency_data.html",
context={
"year": year,
"totals": result,
"totals": context,
},
)
@@ -286,10 +150,6 @@ def yearly_overview_by_account(request, year: int):
data = calculate_account_totals(transactions)
from pprint import pprint
pprint(data)
return render(
request,
"yearly_overview/fragments/account_data.html",

View File

@@ -7,21 +7,22 @@
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace">
{% for entry in totals.income_unpaid %}
{% for entry in totals.income_projected.values %}
<div>
<c-amount.display
:amount="entry.amount"
:prefix="entry.prefix"
:suffix="entry.suffix"
:decimal_places="entry.decimal_places"></c-amount.display>
:amount="entry.income_projected"
:prefix="entry.currency.prefix"
:suffix="entry.currency.suffix"
:decimal_places="entry.currency.decimal_places"
color="green"></c-amount.display>
</div>
{% if entry.exchanged and entry.exchanged.amount %}
{% if entry.exchanged and entry.exchanged.income_projected %}
<div>
<c-amount.display
:amount="entry.exchanged.amount"
:prefix="entry.exchanged.prefix"
:suffix="entry.exchanged.suffix"
:decimal_places="entry.exchanged.decimal_places"
:amount="entry.exchanged.income_projected"
:prefix="entry.exchanged.currency.prefix"
:suffix="entry.exchanged.currency.suffix"
:decimal_places="entry.exchanged.currency.decimal_places"
color="gray"></c-amount.display>
</div>
{% endif %}
@@ -36,21 +37,22 @@
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace">
{% for entry in totals.expense_unpaid %}
{% for entry in totals.expense_projected.values %}
<div>
<c-amount.display
:amount="entry.amount"
:prefix="entry.prefix"
:suffix="entry.suffix"
:decimal_places="entry.decimal_places"></c-amount.display>
:amount="entry.expense_projected"
:prefix="entry.currency.prefix"
:suffix="entry.currency.suffix"
:decimal_places="entry.currency.decimal_places"
color="red"></c-amount.display>
</div>
{% if entry.exchanged and entry.exchanged.amount %}
{% if entry.exchanged and entry.exchanged.expense_projected %}
<div>
<c-amount.display
:amount="entry.exchanged.amount"
:prefix="entry.exchanged.prefix"
:suffix="entry.exchanged.suffix"
:decimal_places="entry.exchanged.decimal_places"
:amount="entry.exchanged.expense_projected"
:prefix="entry.exchanged.currency.prefix"
:suffix="entry.exchanged.currency.suffix"
:decimal_places="entry.exchanged.currency.decimal_places"
color="gray"></c-amount.display>
</div>
{% endif %}
@@ -65,22 +67,22 @@
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace">
{% for entry in totals.balance_unpaid %}
{% for entry in totals.total_projected.values %}
<div>
<c-amount.display
:amount="entry.amount"
:prefix="entry.prefix"
:suffix="entry.suffix"
:decimal_places="entry.decimal_places"
color="{% if entry.amount > 0 %}green{% elif entry.amount < 0 %}red{% endif %}"></c-amount.display>
:amount="entry.total_projected"
:prefix="entry.currency.prefix"
:suffix="entry.currency.suffix"
:decimal_places="entry.currency.decimal_places"
color="{% if entry.total_projected > 0 %}green{% elif entry.total_projected < 0 %}red{% endif %}"></c-amount.display>
</div>
{% if entry.exchanged and entry.exchanged.amount %}
{% if entry.exchanged and entry.exchanged.total_projected %}
<div>
<c-amount.display
:amount="entry.exchanged.amount"
:prefix="entry.exchanged.prefix"
:suffix="entry.exchanged.suffix"
:decimal_places="entry.exchanged.decimal_places"
:amount="entry.exchanged.total_projected"
:prefix="entry.exchanged.currency.prefix"
:suffix="entry.exchanged.currency.suffix"
:decimal_places="entry.exchanged.currency.decimal_places"
color="gray"></c-amount.display>
</div>
{% endif %}
@@ -96,21 +98,22 @@
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace">
{% for entry in totals.income_paid %}
<div>
<c-amount.display
:amount="entry.amount"
:prefix="entry.prefix"
:suffix="entry.suffix"
:decimal_places="entry.decimal_places"></c-amount.display>
</div>
{% if entry.exchanged and entry.exchanged.amount %}
{% for entry in totals.income_current.values %}
<div>
<c-amount.display
:amount="entry.exchanged.amount"
:prefix="entry.exchanged.prefix"
:suffix="entry.exchanged.suffix"
:decimal_places="entry.exchanged.decimal_places"
:amount="entry.income_current"
:prefix="entry.currency.prefix"
:suffix="entry.currency.suffix"
:decimal_places="entry.currency.decimal_places"
color="green"></c-amount.display>
</div>
{% if entry.exchanged and entry.exchanged.income_current %}
<div>
<c-amount.display
:amount="entry.exchanged.income_current"
:prefix="entry.exchanged.currency.prefix"
:suffix="entry.exchanged.currency.suffix"
:decimal_places="entry.exchanged.currency.decimal_places"
color="gray"></c-amount.display>
</div>
{% endif %}
@@ -125,21 +128,22 @@
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace">
{% for entry in totals.expense_paid %}
{% for entry in totals.expense_current.values %}
<div>
<c-amount.display
:amount="entry.amount"
:prefix="entry.prefix"
:suffix="entry.suffix"
:decimal_places="entry.decimal_places"></c-amount.display>
:amount="entry.expense_current"
:prefix="entry.currency.prefix"
:suffix="entry.currency.suffix"
:decimal_places="entry.currency.decimal_places"
color="red"></c-amount.display>
</div>
{% if entry.exchanged and entry.exchanged.amount %}
{% if entry.exchanged and entry.exchanged.expense_current %}
<div>
<c-amount.display
:amount="entry.exchanged.amount"
:prefix="entry.exchanged.prefix"
:suffix="entry.exchanged.suffix"
:decimal_places="entry.exchanged.decimal_places"
:amount="entry.exchanged.expense_current"
:prefix="entry.exchanged.currency.prefix"
:suffix="entry.exchanged.currency.suffix"
:decimal_places="entry.exchanged.currency.decimal_places"
color="gray"></c-amount.display>
</div>
{% endif %}
@@ -153,23 +157,23 @@
<div class="tw-text-gray-400">{% translate 'current total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace tw-text-yellow-400">
{% for entry in totals.balance_paid %}
<div class="text-end font-monospace">
{% for entry in totals.total_current.values %}
<div>
<c-amount.display
:amount="entry.amount"
:prefix="entry.prefix"
:suffix="entry.suffix"
:decimal_places="entry.decimal_places"
color="{% if entry.amount > 0 %}green{% elif entry.amount < 0 %}red{% endif %}"></c-amount.display>
:amount="entry.total_current"
:prefix="entry.currency.prefix"
:suffix="entry.currency.suffix"
:decimal_places="entry.currency.decimal_places"
color="{% if entry.total_current > 0 %}green{% elif entry.total_current < 0 %}red{% endif %}"></c-amount.display>
</div>
{% if entry.exchanged and entry.exchanged.amount %}
{% if entry.exchanged and entry.exchanged.total_current %}
<div>
<c-amount.display
:amount="entry.exchanged.amount"
:prefix="entry.exchanged.prefix"
:suffix="entry.exchanged.suffix"
:decimal_places="entry.exchanged.decimal_places"
:amount="entry.exchanged.total_current"
:prefix="entry.exchanged.currency.prefix"
:suffix="entry.exchanged.currency.suffix"
:decimal_places="entry.exchanged.currency.decimal_places"
color="gray"></c-amount.display>
</div>
{% endif %}
@@ -185,22 +189,22 @@
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace">
{% for entry in totals.balance_total %}
{% for entry in totals.total_final.values %}
<div>
<c-amount.display
:amount="entry.amount"
:prefix="entry.prefix"
:suffix="entry.suffix"
:decimal_places="entry.decimal_places"
color="{% if entry.amount > 0 %}green{% elif entry.amount < 0 %}red{% endif %}"></c-amount.display>
:amount="entry.total_final"
:prefix="entry.currency.prefix"
:suffix="entry.currency.suffix"
:decimal_places="entry.currency.decimal_places"
color="{% if entry.total_final > 0 %}green{% elif entry.total_final < 0 %}red{% endif %}"></c-amount.display>
</div>
{% if entry.exchanged and entry.exchanged.amount %}
{% if entry.exchanged and entry.exchanged.total_final %}
<div>
<c-amount.display
:amount="entry.exchanged.amount"
:prefix="entry.exchanged.prefix"
:suffix="entry.exchanged.suffix"
:decimal_places="entry.exchanged.decimal_places"
:amount="entry.exchanged.total_final"
:prefix="entry.exchanged.currency.prefix"
:suffix="entry.exchanged.currency.suffix"
:decimal_places="entry.exchanged.currency.decimal_places"
color="gray"></c-amount.display>
</div>
{% endif %}