From 05fc4bca7fbbd21667acbe852d4432dc18b53159 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Sat, 9 Nov 2024 02:57:05 -0300 Subject: [PATCH] feat: show currency exchange rate where needed --- .../net_worth/utils/calculate_net_worth.py | 41 +++-- app/apps/yearly_overview/views.py | 83 +++++---- app/templates/cotton/amount/display.html | 2 +- .../fragments/monthly_summary.html | 24 ++- app/templates/net_worth/net_worth.html | 24 ++- .../fragments/account_data.html | 16 +- .../fragments/currency_data.html | 157 +++++++++++++----- 7 files changed, 246 insertions(+), 101 deletions(-) diff --git a/app/apps/net_worth/utils/calculate_net_worth.py b/app/apps/net_worth/utils/calculate_net_worth.py index 54c75c7..c63ce50 100644 --- a/app/apps/net_worth/utils/calculate_net_worth.py +++ b/app/apps/net_worth/utils/calculate_net_worth.py @@ -108,22 +108,39 @@ def calculate_currency_net_worth(): .values("balance") ) - # Main query to fetch all currency data + # 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 item in currencies_data: - currency_name = item.name - net_worth[currency_name] = { - "amount": net_worth.get(currency_name, {}).get("amount", Decimal("0")) - + item.balance, - "code": item.code, - "name": currency_name, - "prefix": item.prefix, - "suffix": item.suffix, - "decimal_places": item.decimal_places, + 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 diff --git a/app/apps/yearly_overview/views.py b/app/apps/yearly_overview/views.py index e007c67..7bb8782 100644 --- a/app/apps/yearly_overview/views.py +++ b/app/apps/yearly_overview/views.py @@ -73,14 +73,13 @@ 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) + transactions = ( + Transaction.objects.filter(**filter_params) + .exclude(Q(category__mute=True) & ~Q(category=None)) + .select_related("account__currency") # Optimize by pre-fetching currency data ) - if month: - date_trunc = TruncMonth("reference_date") - else: - date_trunc = TruncYear("reference_date") + date_trunc = TruncMonth("reference_date") if month else TruncYear("reference_date") monthly_data = ( transactions.annotate(month=date_trunc) @@ -90,6 +89,8 @@ def yearly_overview_by_currency(request, year: int): "account__currency__prefix", "account__currency__suffix", "account__currency__decimal_places", + "account__currency_id", + "account__currency__exchange_currency_id", ) .annotate( income_paid=Coalesce( @@ -103,7 +104,6 @@ def yearly_overview_by_currency(request, year: int): ) ), Value(Decimal("0")), - output_field=DecimalField(), ), expense_paid=Coalesce( Sum( @@ -118,7 +118,6 @@ def yearly_overview_by_currency(request, year: int): ) ), Value(Decimal("0")), - output_field=DecimalField(), ), income_unpaid=Coalesce( Sum( @@ -133,7 +132,6 @@ def yearly_overview_by_currency(request, year: int): ) ), Value(Decimal("0")), - output_field=DecimalField(), ), expense_unpaid=Coalesce( Sum( @@ -148,7 +146,6 @@ def yearly_overview_by_currency(request, year: int): ) ), Value(Decimal("0")), - output_field=DecimalField(), ), ) .annotate( @@ -162,7 +159,12 @@ def yearly_overview_by_currency(request, year: int): .order_by("month", "account__currency__code") ) - # Create a dictionary to store the final result + # 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() + } + result = { "income_paid": [], "expense_paid": [], @@ -173,32 +175,53 @@ def yearly_overview_by_currency(request, year: int): "balance_total": [], } - # Fill in the data 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"] - for field in [ - "income_paid", - "expense_paid", - "income_unpaid", - "expense_unpaid", - "balance_unpaid", - "balance_paid", - "balance_total", - ]: - if entry[field] != 0: - result[field].append( - { - "code": currency_code, - "prefix": prefix, - "suffix": suffix, - "decimal_places": decimal_places, - "amount": entry[field], - } + # 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, diff --git a/app/templates/cotton/amount/display.html b/app/templates/cotton/amount/display.html index bb8a720..c8905aa 100644 --- a/app/templates/cotton/amount/display.html +++ b/app/templates/cotton/amount/display.html @@ -1,7 +1,7 @@ {% load currency_display %}
-
diff --git a/app/templates/monthly_overview/fragments/monthly_summary.html b/app/templates/monthly_overview/fragments/monthly_summary.html index 5b57cd6..90c6846 100644 --- a/app/templates/monthly_overview/fragments/monthly_summary.html +++ b/app/templates/monthly_overview/fragments/monthly_summary.html @@ -20,7 +20,8 @@ :amount="entry.amount" :prefix="entry.prefix" :suffix="entry.suffix" - :decimal_places="entry.decimal_places"> + :decimal_places="entry.decimal_places" + color="{% if entry.amount > 0 %}green{% elif entry.amount < 0 %}red{% endif %}"> {% empty %}
-
{% endfor %} @@ -48,7 +49,8 @@ :amount="entry.amount" :prefix="entry.prefix" :suffix="entry.suffix" - :decimal_places="entry.decimal_places"> + :decimal_places="entry.decimal_places" + color="{% if entry.amount > 0 %}green{% elif entry.amount < 0 %}red{% endif %}"> {% empty %}
-
{% endfor %} @@ -65,7 +67,8 @@ :amount="entry.amount" :prefix="entry.prefix" :suffix="entry.suffix" - :decimal_places="entry.decimal_places"> + :decimal_places="entry.decimal_places" + color="{% if entry.amount > 0 %}green{% elif entry.amount < 0 %}red{% endif %}"> {% empty %}
-
{% endfor %} @@ -93,7 +96,8 @@ :amount="entry.amount" :prefix="entry.prefix" :suffix="entry.suffix" - :decimal_places="entry.decimal_places"> + :decimal_places="entry.decimal_places" + color="{% if entry.amount > 0 %}green{% elif entry.amount < 0 %}red{% endif %}"> {% empty %}
-
{% endfor %} @@ -110,7 +114,8 @@ :amount="entry.amount" :prefix="entry.prefix" :suffix="entry.suffix" - :decimal_places="entry.decimal_places"> + :decimal_places="entry.decimal_places" + color="{% if entry.amount > 0 %}green{% elif entry.amount < 0 %}red{% endif %}"> {% empty %}
-
{% endfor %} @@ -138,7 +143,8 @@ :amount="entry.amount" :prefix="entry.prefix" :suffix="entry.suffix" - :decimal_places="entry.decimal_places"> + :decimal_places="entry.decimal_places" + color="{% if entry.amount > 0 %}green{% elif entry.amount < 0 %}red{% endif %}"> {% empty %}
-
{% endfor %} @@ -154,7 +160,8 @@ :amount="entry.amount" :prefix="entry.prefix" :suffix="entry.suffix" - :decimal_places="entry.decimal_places"> + :decimal_places="entry.decimal_places" + color="{% if entry.amount > 0 %}green{% elif entry.amount < 0 %}red{% endif %}"> {% empty %}
-
{% endfor %} @@ -168,7 +175,8 @@ :amount="entry.amount" :prefix="entry.prefix" :suffix="entry.suffix" - :decimal_places="entry.decimal_places"> + :decimal_places="entry.decimal_places" + color="{% if entry.amount > 0 %}green{% elif entry.amount < 0 %}red{% endif %}"> {% empty %}
-
{% endfor %} diff --git a/app/templates/net_worth/net_worth.html b/app/templates/net_worth/net_worth.html index b9a9b0c..1b1c85e 100644 --- a/app/templates/net_worth/net_worth.html +++ b/app/templates/net_worth/net_worth.html @@ -26,16 +26,28 @@
{{ currency.name }}
-
+
+ {% if currency.exchanged %} +
+ +
+ {% endif %} {% endfor %} @@ -62,12 +74,13 @@
{{ account_data.name }}
-
+
+ :decimal_places="account_data.currency.decimal_places" + color="{% if account_data.balance > 0 %}green{% elif account_data.balance < 0 %}red{% endif %}">
@@ -87,12 +100,13 @@
{{ account_data.name }}
-
diff --git a/app/templates/yearly_overview/fragments/account_data.html b/app/templates/yearly_overview/fragments/account_data.html index 0dfe368..2bc693c 100644 --- a/app/templates/yearly_overview/fragments/account_data.html +++ b/app/templates/yearly_overview/fragments/account_data.html @@ -61,12 +61,13 @@
+ class="text-end font-monospace"> + :decimal_places="account.currency.decimal_places" + color="{% if account.balance_unpaid > 0 %}green{% elif account.balance_unpaid < 0 %}red{% endif %}">
{% if account.exchange_currency and account.exchange_balance_unpaid %} @@ -129,12 +130,13 @@
+ class="text-end font-monospace"> + :decimal_places="account.currency.decimal_places" + color="{% if account.balance_paid > 0 %}green{% elif account.balance_paid < 0 %}red{% endif %}">
{% if account.exchange_currency and account.exchange_balance_paid %} @@ -153,13 +155,13 @@
{% translate 'final total' %}
-
+
+ :decimal_places="account.currency.decimal_places" + color="{% if account.balance_paid > 0 %}green{% elif account.balance_paid < 0 %}red{% endif %}">
{% if account.exchange_currency and account.exchange_balance_total %} diff --git a/app/templates/yearly_overview/fragments/currency_data.html b/app/templates/yearly_overview/fragments/currency_data.html index 2e97b5a..fde1da7 100644 --- a/app/templates/yearly_overview/fragments/currency_data.html +++ b/app/templates/yearly_overview/fragments/currency_data.html @@ -8,11 +8,23 @@
{% for entry in totals.income_unpaid %} - +
+ +
+ {% if entry.exchanged %} +
+ +
+ {% endif %} {% empty %}
-
{% endfor %} @@ -25,11 +37,23 @@
{% for entry in totals.expense_unpaid %} - +
+ +
+ {% if entry.exchanged %} +
+ +
+ {% endif %} {% empty %}
-
{% endfor %} @@ -42,13 +66,24 @@
{% for entry in totals.balance_unpaid %} -
- +
+
+ {% if entry.exchanged %} +
+ +
+ {% endif %} {% empty %}
-
{% endfor %} @@ -62,11 +97,23 @@
{% for entry in totals.income_paid %} +
+ :amount="entry.amount" + :prefix="entry.prefix" + :suffix="entry.suffix" + :decimal_places="entry.decimal_places"> +
+ {% if entry.exchanged %} +
+ +
+ {% endif %} {% empty %}
-
{% endfor %} @@ -79,11 +126,23 @@
{% for entry in totals.expense_paid %} - +
+ +
+ {% if entry.exchanged %} +
+ +
+ {% endif %} {% empty %}
-
{% endfor %} @@ -96,13 +155,24 @@
{% for entry in totals.balance_paid %} -
- +
+
+ {% if entry.exchanged %} +
+ +
+ {% endif %} {% empty %}
-
{% endfor %} @@ -116,16 +186,27 @@
{% for entry in totals.balance_total %} -
- +
+
+ {% if entry.exchanged %} +
+ +
+ {% endif %} {% empty %}
-
{% endfor %}
-
+