From 35027ee0ae392fca06cca84e7fbed350e4ee5694 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Thu, 27 Feb 2025 23:33:05 -0300 Subject: [PATCH] feat(insights): add Categories Overview Closes #94 --- app/apps/insights/urls.py | 5 + app/apps/insights/utils/category_overview.py | 165 ++++++++++++++++++ app/apps/insights/views.py | 22 +++ app/locale/de/LC_MESSAGES/django.po | 24 ++- app/locale/en/LC_MESSAGES/django.po | 22 ++- app/locale/nl/LC_MESSAGES/django.po | 24 ++- app/locale/pt_BR/LC_MESSAGES/django.po | 24 ++- .../fragments/category_overview/index.html | 69 ++++++++ app/templates/insights/pages/index.html | 7 + 9 files changed, 338 insertions(+), 24 deletions(-) create mode 100644 app/apps/insights/utils/category_overview.py create mode 100644 app/templates/insights/fragments/category_overview/index.html diff --git a/app/apps/insights/urls.py b/app/apps/insights/urls.py index 2c09413..ccd2492 100644 --- a/app/apps/insights/urls.py +++ b/app/apps/insights/urls.py @@ -29,6 +29,11 @@ urlpatterns = [ views.category_sum_by_currency, name="category_sum_by_currency", ), + path( + "insights/category-overview/", + views.category_overview, + name="category_overview", + ), path( "insights/late-transactions/", views.late_transactions, diff --git a/app/apps/insights/utils/category_overview.py b/app/apps/insights/utils/category_overview.py new file mode 100644 index 0000000..c951d1d --- /dev/null +++ b/app/apps/insights/utils/category_overview.py @@ -0,0 +1,165 @@ +from decimal import Decimal + +from django.db import models +from django.db.models import Sum, Case, When, Value, DecimalField +from django.db.models.functions import Coalesce + +from apps.transactions.models import Transaction +from apps.currencies.models import Currency +from apps.currencies.utils.convert import convert + + +def get_categories_totals(transactions_queryset, ignore_empty=False): + # Get metrics for each category and currency in a single query + category_currency_metrics = ( + transactions_queryset.values( + "category", + "category__name", + "account__currency", + "account__currency__code", + "account__currency__name", + "account__currency__decimal_places", + "account__currency__prefix", + "account__currency__suffix", + "account__currency__exchange_currency", + ) + .annotate( + expense_current=Coalesce( + Sum( + Case( + When( + type=Transaction.Type.EXPENSE, is_paid=True, then="amount" + ), + default=Value(0), + output_field=models.DecimalField(), + ) + ), + Decimal("0"), + ), + expense_projected=Coalesce( + Sum( + Case( + When( + type=Transaction.Type.EXPENSE, is_paid=False, then="amount" + ), + default=Value(0), + output_field=models.DecimalField(), + ) + ), + Decimal("0"), + ), + income_current=Coalesce( + Sum( + Case( + When(type=Transaction.Type.INCOME, is_paid=True, then="amount"), + default=Value(0), + output_field=models.DecimalField(), + ) + ), + Decimal("0"), + ), + income_projected=Coalesce( + Sum( + Case( + When( + type=Transaction.Type.INCOME, is_paid=False, then="amount" + ), + default=Value(0), + output_field=models.DecimalField(), + ) + ), + Decimal("0"), + ), + ) + .order_by("category__name") + ) + + # Process the results to structure by category + result = {} + + for metric in category_currency_metrics: + # Skip empty categories if ignore_empty is True + if ignore_empty and all( + metric[field] == Decimal("0") + for field in [ + "expense_current", + "expense_projected", + "income_current", + "income_projected", + ] + ): + continue + + # Calculate derived totals + total_current = metric["income_current"] - metric["expense_current"] + total_projected = metric["income_projected"] - metric["expense_projected"] + total_income = metric["income_current"] + metric["income_projected"] + total_expense = metric["expense_current"] + metric["expense_projected"] + total_final = total_current + total_projected + + category_id = metric["category"] + currency_id = metric["account__currency"] + + if category_id not in result: + result[category_id] = {"name": metric["category__name"], "currencies": {}} + + # Add currency data + currency_data = { + "currency": { + "code": metric["account__currency__code"], + "name": metric["account__currency__name"], + "decimal_places": metric["account__currency__decimal_places"], + "prefix": metric["account__currency__prefix"], + "suffix": metric["account__currency__suffix"], + }, + "expense_current": metric["expense_current"], + "expense_projected": metric["expense_projected"], + "total_expense": total_expense, + "income_current": metric["income_current"], + "income_projected": metric["income_projected"], + "total_income": total_income, + "total_current": total_current, + "total_projected": total_projected, + "total_final": total_final, + } + + # Add exchanged values if exchange_currency exists + if metric["account__currency__exchange_currency"]: + from_currency = Currency.objects.get(id=currency_id) + exchange_currency = Currency.objects.get( + id=metric["account__currency__exchange_currency"] + ) + + exchanged = {} + for field in [ + "expense_current", + "expense_projected", + "income_current", + "income_projected", + "total_income", + "total_expense", + "total_current", + "total_projected", + "total_final", + ]: + amount, prefix, suffix, decimal_places = convert( + amount=currency_data[field], + from_currency=from_currency, + to_currency=exchange_currency, + ) + if amount is not None: + exchanged[field] = amount + if "currency" not in exchanged: + exchanged["currency"] = { + "prefix": prefix, + "suffix": suffix, + "decimal_places": decimal_places, + "code": exchange_currency.code, + "name": exchange_currency.name, + } + if exchanged: + currency_data["exchanged"] = exchanged + + result[category_id]["currencies"][currency_id] = currency_data + + return result diff --git a/app/apps/insights/views.py b/app/apps/insights/views.py index 94c134b..2551031 100644 --- a/app/apps/insights/views.py +++ b/app/apps/insights/views.py @@ -1,3 +1,6 @@ +import decimal +import json + from dateutil.relativedelta import relativedelta from django.contrib.auth.decorators import login_required from django.shortcuts import render @@ -23,6 +26,7 @@ from apps.insights.utils.sankey import ( ) from apps.insights.utils.transactions import get_transactions from apps.transactions.models import TransactionCategory, Transaction +from apps.insights.utils.category_overview import get_categories_totals @login_required @@ -159,6 +163,24 @@ def category_sum_by_currency(request): ) +@only_htmx +@login_required +@require_http_methods(["GET"]) +def category_overview(request): + # Get filtered transactions + transactions = get_transactions(request, include_silent=True) + + total_table = get_categories_totals( + transactions_queryset=transactions, ignore_empty=False + ) + + return render( + request, + "insights/fragments/category_overview/index.html", + {"total_table": total_table}, + ) + + @only_htmx @login_required @require_http_methods(["GET"]) diff --git a/app/locale/de/LC_MESSAGES/django.po b/app/locale/de/LC_MESSAGES/django.po index cbf7a60..25e628a 100644 --- a/app/locale/de/LC_MESSAGES/django.po +++ b/app/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-02-24 16:30-0300\n" +"POT-Creation-Date: 2025-02-27 23:32-0300\n" "PO-Revision-Date: 2025-02-24 22:33+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -76,6 +76,7 @@ msgstr "Neuer Saldo" #: apps/transactions/forms.py:299 apps/transactions/forms.py:479 #: apps/transactions/forms.py:724 apps/transactions/models.py:203 #: apps/transactions/models.py:378 apps/transactions/models.py:558 +#: templates/insights/fragments/category_overview/index.html:9 msgid "Category" msgstr "Kategorie" @@ -880,6 +881,7 @@ msgstr "Vorgang erfolgreich gelöscht" #: apps/insights/forms.py:119 apps/insights/utils/sankey.py:36 #: apps/insights/utils/sankey.py:167 +#: templates/insights/fragments/category_overview/index.html:18 msgid "Uncategorized" msgstr "Unkategorisiert" @@ -1252,6 +1254,7 @@ msgstr "Entität" #: templates/calendar_view/fragments/list.html:52 #: templates/calendar_view/fragments/list.html:54 #: templates/cotton/ui/quick_transactions_buttons.html:10 +#: templates/insights/fragments/category_overview/index.html:10 #: templates/monthly_overview/fragments/monthly_summary.html:39 msgid "Income" msgstr "Einnahme" @@ -1262,6 +1265,7 @@ msgstr "Einnahme" #: templates/calendar_view/fragments/list.html:56 #: templates/calendar_view/fragments/list.html:58 #: templates/cotton/ui/quick_transactions_buttons.html:18 +#: templates/insights/fragments/category_overview/index.html:11 msgid "Expense" msgstr "Ausgabe" @@ -1888,6 +1892,7 @@ msgid "Muted" msgstr "Ausgeblendet" #: templates/categories/fragments/table.html:57 +#: templates/insights/fragments/category_overview/index.html:67 msgid "No categories" msgstr "Keine Kategorien" @@ -2451,6 +2456,11 @@ msgstr "Einnahmen/Ausgaben nach Konto" msgid "Income/Expense by Currency" msgstr "Einnahmen/Ausgaben nach Währung" +#: templates/insights/fragments/category_overview/index.html:12 +#: templates/monthly_overview/fragments/monthly_summary.html:167 +msgid "Total" +msgstr "Gesamt" + #: templates/insights/fragments/late_transactions.html:15 msgid "All good!" msgstr "Alles gut!" @@ -2506,10 +2516,16 @@ msgid "Category Explorer" msgstr "Kategorien-Explorer" #: templates/insights/pages/index.html:102 +#, fuzzy +#| msgid "Categories" +msgid "Categories Overview" +msgstr "Kategorien" + +#: templates/insights/pages/index.html:109 msgid "Late Transactions" msgstr "Verspätete Transaktionen" -#: templates/insights/pages/index.html:108 +#: templates/insights/pages/index.html:115 msgid "Latest Transactions" msgstr "Letzte Transaktionen" @@ -2607,10 +2623,6 @@ msgstr "erwartet" msgid "Expenses" msgstr "Ausgaben" -#: templates/monthly_overview/fragments/monthly_summary.html:167 -msgid "Total" -msgstr "Gesamt" - #: templates/monthly_overview/fragments/monthly_summary.html:257 msgid "Distribution" msgstr "Verteilung" diff --git a/app/locale/en/LC_MESSAGES/django.po b/app/locale/en/LC_MESSAGES/django.po index d45d73e..e4fe883 100644 --- a/app/locale/en/LC_MESSAGES/django.po +++ b/app/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-02-24 23:04-0300\n" +"POT-Creation-Date: 2025-02-27 23:32-0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -75,6 +75,7 @@ msgstr "" #: apps/transactions/forms.py:299 apps/transactions/forms.py:479 #: apps/transactions/forms.py:724 apps/transactions/models.py:203 #: apps/transactions/models.py:378 apps/transactions/models.py:558 +#: templates/insights/fragments/category_overview/index.html:9 msgid "Category" msgstr "" @@ -862,6 +863,7 @@ msgstr "" #: apps/insights/forms.py:119 apps/insights/utils/sankey.py:36 #: apps/insights/utils/sankey.py:167 +#: templates/insights/fragments/category_overview/index.html:18 msgid "Uncategorized" msgstr "" @@ -1221,6 +1223,7 @@ msgstr "" #: templates/calendar_view/fragments/list.html:52 #: templates/calendar_view/fragments/list.html:54 #: templates/cotton/ui/quick_transactions_buttons.html:10 +#: templates/insights/fragments/category_overview/index.html:10 #: templates/monthly_overview/fragments/monthly_summary.html:39 msgid "Income" msgstr "" @@ -1231,6 +1234,7 @@ msgstr "" #: templates/calendar_view/fragments/list.html:56 #: templates/calendar_view/fragments/list.html:58 #: templates/cotton/ui/quick_transactions_buttons.html:18 +#: templates/insights/fragments/category_overview/index.html:11 msgid "Expense" msgstr "" @@ -1856,6 +1860,7 @@ msgid "Muted" msgstr "" #: templates/categories/fragments/table.html:57 +#: templates/insights/fragments/category_overview/index.html:67 msgid "No categories" msgstr "" @@ -2414,6 +2419,11 @@ msgstr "" msgid "Income/Expense by Currency" msgstr "" +#: templates/insights/fragments/category_overview/index.html:12 +#: templates/monthly_overview/fragments/monthly_summary.html:167 +msgid "Total" +msgstr "" + #: templates/insights/fragments/late_transactions.html:15 msgid "All good!" msgstr "" @@ -2469,10 +2479,14 @@ msgid "Category Explorer" msgstr "" #: templates/insights/pages/index.html:102 +msgid "Categories Overview" +msgstr "" + +#: templates/insights/pages/index.html:109 msgid "Late Transactions" msgstr "" -#: templates/insights/pages/index.html:108 +#: templates/insights/pages/index.html:115 msgid "Latest Transactions" msgstr "" @@ -2567,10 +2581,6 @@ msgstr "" msgid "Expenses" msgstr "" -#: templates/monthly_overview/fragments/monthly_summary.html:167 -msgid "Total" -msgstr "" - #: templates/monthly_overview/fragments/monthly_summary.html:257 msgid "Distribution" msgstr "" diff --git a/app/locale/nl/LC_MESSAGES/django.po b/app/locale/nl/LC_MESSAGES/django.po index 783955b..aeb42b2 100644 --- a/app/locale/nl/LC_MESSAGES/django.po +++ b/app/locale/nl/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-02-24 16:30-0300\n" +"POT-Creation-Date: 2025-02-27 23:32-0300\n" "PO-Revision-Date: 2025-02-22 15:03+0100\n" "Last-Translator: Dimitri Decrock \n" "Language-Team: \n" @@ -76,6 +76,7 @@ msgstr "Nieuw saldo" #: apps/transactions/forms.py:299 apps/transactions/forms.py:479 #: apps/transactions/forms.py:724 apps/transactions/models.py:203 #: apps/transactions/models.py:378 apps/transactions/models.py:558 +#: templates/insights/fragments/category_overview/index.html:9 msgid "Category" msgstr "Categorie" @@ -880,6 +881,7 @@ msgstr "Run met succes verwijderd" #: apps/insights/forms.py:119 apps/insights/utils/sankey.py:36 #: apps/insights/utils/sankey.py:167 +#: templates/insights/fragments/category_overview/index.html:18 msgid "Uncategorized" msgstr "Ongecategoriseerd" @@ -1247,6 +1249,7 @@ msgstr "Bedrijf" #: templates/calendar_view/fragments/list.html:52 #: templates/calendar_view/fragments/list.html:54 #: templates/cotton/ui/quick_transactions_buttons.html:10 +#: templates/insights/fragments/category_overview/index.html:10 #: templates/monthly_overview/fragments/monthly_summary.html:39 msgid "Income" msgstr "Ontvangsten Transactie" @@ -1257,6 +1260,7 @@ msgstr "Ontvangsten Transactie" #: templates/calendar_view/fragments/list.html:56 #: templates/calendar_view/fragments/list.html:58 #: templates/cotton/ui/quick_transactions_buttons.html:18 +#: templates/insights/fragments/category_overview/index.html:11 msgid "Expense" msgstr "Uitgave Transactie" @@ -1882,6 +1886,7 @@ msgid "Muted" msgstr "Gedempt" #: templates/categories/fragments/table.html:57 +#: templates/insights/fragments/category_overview/index.html:67 msgid "No categories" msgstr "Geen categorieën" @@ -2443,6 +2448,11 @@ msgstr "Inkomsten/uitgaven per rekening" msgid "Income/Expense by Currency" msgstr "Inkomsten/uitgaven per Munteenheid" +#: templates/insights/fragments/category_overview/index.html:12 +#: templates/monthly_overview/fragments/monthly_summary.html:167 +msgid "Total" +msgstr "Totaal" + #: templates/insights/fragments/late_transactions.html:15 msgid "All good!" msgstr "Allemaal goed!" @@ -2498,10 +2508,16 @@ msgid "Category Explorer" msgstr "Categorie Verkenner" #: templates/insights/pages/index.html:102 +#, fuzzy +#| msgid "Categories" +msgid "Categories Overview" +msgstr "Categorieën" + +#: templates/insights/pages/index.html:109 msgid "Late Transactions" msgstr "Betalingsachterstanden" -#: templates/insights/pages/index.html:108 +#: templates/insights/pages/index.html:115 msgid "Latest Transactions" msgstr "Laatste Verrichtingen" @@ -2598,10 +2614,6 @@ msgstr "verwacht" msgid "Expenses" msgstr "Uitgaven" -#: templates/monthly_overview/fragments/monthly_summary.html:167 -msgid "Total" -msgstr "Totaal" - #: templates/monthly_overview/fragments/monthly_summary.html:257 msgid "Distribution" msgstr "Verdeling" diff --git a/app/locale/pt_BR/LC_MESSAGES/django.po b/app/locale/pt_BR/LC_MESSAGES/django.po index a600625..0c4cb70 100644 --- a/app/locale/pt_BR/LC_MESSAGES/django.po +++ b/app/locale/pt_BR/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-02-24 16:30-0300\n" +"POT-Creation-Date: 2025-02-27 23:32-0300\n" "PO-Revision-Date: 2025-02-19 23:06-0300\n" "Last-Translator: Herculino Trotta\n" "Language-Team: \n" @@ -76,6 +76,7 @@ msgstr "Novo saldo" #: apps/transactions/forms.py:299 apps/transactions/forms.py:479 #: apps/transactions/forms.py:724 apps/transactions/models.py:203 #: apps/transactions/models.py:378 apps/transactions/models.py:558 +#: templates/insights/fragments/category_overview/index.html:9 msgid "Category" msgstr "Categoria" @@ -878,6 +879,7 @@ msgstr "Importação apagada com sucesso" #: apps/insights/forms.py:119 apps/insights/utils/sankey.py:36 #: apps/insights/utils/sankey.py:167 +#: templates/insights/fragments/category_overview/index.html:18 msgid "Uncategorized" msgstr "Sem categoria" @@ -1244,6 +1246,7 @@ msgstr "Entidade" #: templates/calendar_view/fragments/list.html:52 #: templates/calendar_view/fragments/list.html:54 #: templates/cotton/ui/quick_transactions_buttons.html:10 +#: templates/insights/fragments/category_overview/index.html:10 #: templates/monthly_overview/fragments/monthly_summary.html:39 msgid "Income" msgstr "Renda" @@ -1254,6 +1257,7 @@ msgstr "Renda" #: templates/calendar_view/fragments/list.html:56 #: templates/calendar_view/fragments/list.html:58 #: templates/cotton/ui/quick_transactions_buttons.html:18 +#: templates/insights/fragments/category_overview/index.html:11 msgid "Expense" msgstr "Despesa" @@ -1879,6 +1883,7 @@ msgid "Muted" msgstr "Silenciada" #: templates/categories/fragments/table.html:57 +#: templates/insights/fragments/category_overview/index.html:67 msgid "No categories" msgstr "Nenhum categoria" @@ -2441,6 +2446,11 @@ msgstr "Gasto/Despesa por Conta" msgid "Income/Expense by Currency" msgstr "Gasto/Despesa por Moeda" +#: templates/insights/fragments/category_overview/index.html:12 +#: templates/monthly_overview/fragments/monthly_summary.html:167 +msgid "Total" +msgstr "Total" + #: templates/insights/fragments/late_transactions.html:15 msgid "All good!" msgstr "Tudo certo!" @@ -2496,10 +2506,16 @@ msgid "Category Explorer" msgstr "Explorador de Categoria" #: templates/insights/pages/index.html:102 +#, fuzzy +#| msgid "Categories" +msgid "Categories Overview" +msgstr "Categorias" + +#: templates/insights/pages/index.html:109 msgid "Late Transactions" msgstr "Transações Atrasadas" -#: templates/insights/pages/index.html:108 +#: templates/insights/pages/index.html:115 msgid "Latest Transactions" msgstr "Últimas Transações" @@ -2596,10 +2612,6 @@ msgstr "previsto" msgid "Expenses" msgstr "Despesas" -#: templates/monthly_overview/fragments/monthly_summary.html:167 -msgid "Total" -msgstr "Total" - #: templates/monthly_overview/fragments/monthly_summary.html:257 msgid "Distribution" msgstr "Distribuição" diff --git a/app/templates/insights/fragments/category_overview/index.html b/app/templates/insights/fragments/category_overview/index.html new file mode 100644 index 0000000..d57465f --- /dev/null +++ b/app/templates/insights/fragments/category_overview/index.html @@ -0,0 +1,69 @@ +{% load i18n %} + +
+ {% if total_table %} +
+ + + + + + + + + + + {% for category in total_table.values %} + + + + + + + {% endfor %} + +
{% trans 'Category' %}{% trans 'Income' %}{% trans 'Expense' %}{% trans 'Total' %}
{% if category.name %}{{ category.name }}{% else %}{% trans 'Uncategorized' %}{% endif %} + {% for currency in category.currencies.values %} + {% if currency.total_income != 0 %} + + {% else %} +
-
+ {% endif %} + {% endfor %} +
+ {% for currency in category.currencies.values %} + {% if currency.total_expense != 0 %} + + {% else %} +
-
+ {% endif %} + {% endfor %} +
+ {% for currency in category.currencies.values %} + {% if currency.total_final != 0 %} + + {% else %} +
-
+ {% endif %} + {% endfor %} +
+
+ {% else %} + + {% endif %} +
diff --git a/app/templates/insights/pages/index.html b/app/templates/insights/pages/index.html index 256c9f9..e98487f 100644 --- a/app/templates/insights/pages/index.html +++ b/app/templates/insights/pages/index.html @@ -94,6 +94,13 @@ hx-indicator="#tab-content" hx-target="#tab-content">{% trans 'Category Explorer' %} +