From 835316d0f3a973fff18f8e762949877a6b128bb6 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Tue, 18 Feb 2025 20:46:06 -0300 Subject: [PATCH] feat(insights:category-explorer): separate current and projected totals --- app/apps/insights/utils/category_explorer.py | 96 ++++++++-- .../category_explorer/charts/account.html | 169 ++++++++++-------- .../category_explorer/charts/currency.html | 162 +++++++++-------- .../fragments/category_explorer/index.html | 5 +- 4 files changed, 262 insertions(+), 170 deletions(-) diff --git a/app/apps/insights/utils/category_explorer.py b/app/apps/insights/utils/category_explorer.py index 639750c..f7dc401 100644 --- a/app/apps/insights/utils/category_explorer.py +++ b/app/apps/insights/utils/category_explorer.py @@ -3,7 +3,7 @@ from django.db.models.functions import Coalesce from django.utils.translation import gettext_lazy as _ -def get_category_sums_by_account(queryset, category): +def get_category_sums_by_account(queryset, category=None): """ Returns income/expense sums per account for a specific category. """ @@ -11,10 +11,11 @@ def get_category_sums_by_account(queryset, category): queryset.filter(category=category) .values("account__name") .annotate( - income=Coalesce( + current_income=Coalesce( Sum( Case( When(type="IN", then="amount"), + When(is_paid=True, then="amount"), default=Value(0), output_field=DecimalField(max_digits=42, decimal_places=30), ) @@ -22,10 +23,35 @@ def get_category_sums_by_account(queryset, category): Value(0), output_field=DecimalField(max_digits=42, decimal_places=30), ), - expense=Coalesce( + current_expense=Coalesce( Sum( Case( When(type="EX", then=-F("amount")), + When(is_paid=True, then="amount"), + default=Value(0), + output_field=DecimalField(max_digits=42, decimal_places=30), + ) + ), + Value(0), + output_field=DecimalField(max_digits=42, decimal_places=30), + ), + projected_income=Coalesce( + Sum( + Case( + When(type="IN", then="amount"), + When(is_paid=False, then="amount"), + default=Value(0), + output_field=DecimalField(max_digits=42, decimal_places=30), + ) + ), + Value(0), + output_field=DecimalField(max_digits=42, decimal_places=30), + ), + projected_expense=Coalesce( + Sum( + Case( + When(type="EX", then=-F("amount")), + When(is_paid=False, then="amount"), default=Value(0), output_field=DecimalField(max_digits=42, decimal_places=30), ) @@ -41,18 +67,26 @@ def get_category_sums_by_account(queryset, category): "labels": [item["account__name"] for item in sums], "datasets": [ { - "label": _("Income"), - "data": [float(item["income"]) for item in sums], + "label": _("Current Income"), + "data": [float(item["current_income"]) for item in sums], }, { - "label": _("Expenses"), - "data": [float(item["expense"]) for item in sums], + "label": _("Current Expenses"), + "data": [float(item["current_expense"]) for item in sums], + }, + { + "label": _("Projected Income"), + "data": [float(item["projected_income"]) for item in sums], + }, + { + "label": _("Projected Expenses"), + "data": [float(item["projected_expense"]) for item in sums], }, ], } -def get_category_sums_by_currency(queryset, category): +def get_category_sums_by_currency(queryset, category=None): """ Returns income/expense sums per currency for a specific category. """ @@ -60,10 +94,11 @@ def get_category_sums_by_currency(queryset, category): queryset.filter(category=category) .values("account__currency__name") .annotate( - income=Coalesce( + current_income=Coalesce( Sum( Case( When(type="IN", then="amount"), + When(is_paid=True, then="amount"), default=Value(0), output_field=DecimalField(max_digits=42, decimal_places=30), ) @@ -71,10 +106,35 @@ def get_category_sums_by_currency(queryset, category): Value(0), output_field=DecimalField(max_digits=42, decimal_places=30), ), - expense=Coalesce( + current_expense=Coalesce( Sum( Case( When(type="EX", then=-F("amount")), + When(is_paid=True, then="amount"), + default=Value(0), + output_field=DecimalField(max_digits=42, decimal_places=30), + ) + ), + Value(0), + output_field=DecimalField(max_digits=42, decimal_places=30), + ), + projected_income=Coalesce( + Sum( + Case( + When(type="IN", then="amount"), + When(is_paid=False, then="amount"), + default=Value(0), + output_field=DecimalField(max_digits=42, decimal_places=30), + ) + ), + Value(0), + output_field=DecimalField(max_digits=42, decimal_places=30), + ), + projected_expense=Coalesce( + Sum( + Case( + When(type="EX", then=-F("amount")), + When(is_paid=False, then="amount"), default=Value(0), output_field=DecimalField(max_digits=42, decimal_places=30), ) @@ -90,12 +150,20 @@ def get_category_sums_by_currency(queryset, category): "labels": [item["account__currency__name"] for item in sums], "datasets": [ { - "label": _("Income"), - "data": [float(item["income"]) for item in sums], + "label": _("Current Income"), + "data": [float(item["current_income"]) for item in sums], }, { - "label": _("Expenses"), - "data": [float(item["expense"]) for item in sums], + "label": _("Current Expenses"), + "data": [float(item["current_expense"]) for item in sums], + }, + { + "label": _("Projected Income"), + "data": [float(item["projected_income"]) for item in sums], + }, + { + "label": _("Projected Expenses"), + "data": [float(item["projected_expense"]) for item in sums], }, ], } diff --git a/app/templates/insights/fragments/category_explorer/charts/account.html b/app/templates/insights/fragments/category_explorer/charts/account.html index a3b91f3..5ed053a 100644 --- a/app/templates/insights/fragments/category_explorer/charts/account.html +++ b/app/templates/insights/fragments/category_explorer/charts/account.html @@ -1,83 +1,100 @@ {% load i18n %} {% if account_data.labels %} -
- -
+
+ +
- + new Chart( + document.getElementById('accountChart'), + { + type: 'bar', + data: { + labels: accountData.labels, + datasets: [ + { + label: "{% trans 'Current Income' %}", + data: accountData.datasets[0].data, + backgroundColor: '#4dde80', + stack: 'stack0' + }, + { + label: "{% trans 'Current Expenses' %}", + data: accountData.datasets[1].data, + backgroundColor: '#f87171', + stack: 'stack0' + }, + { + label: "{% trans 'Projected Income' %}", + data: accountData.datasets[2].data, + backgroundColor: '#4dde8080', // Added transparency + stack: 'stack0' + }, + { + label: "{% trans 'Projected Expenses' %}", + data: accountData.datasets[3].data, + backgroundColor: '#f8717180', // Added transparency + stack: 'stack0' + } + ] + }, + options: { + ...chartOptions, + plugins: { + ...chartOptions.plugins, + title: { + display: false, + } + } + } + } + ); + } + {% else %} - + {% endif %} diff --git a/app/templates/insights/fragments/category_explorer/charts/currency.html b/app/templates/insights/fragments/category_explorer/charts/currency.html index 1622bb9..b3ab300 100644 --- a/app/templates/insights/fragments/category_explorer/charts/currency.html +++ b/app/templates/insights/fragments/category_explorer/charts/currency.html @@ -1,84 +1,92 @@ {% load i18n %} {% if currency_data.labels %} -
- -
+
+ +
- + new Chart( + document.getElementById('currencyChart'), + { + type: 'bar', + data: { + labels: currencyData.labels, + datasets: [ + { + label: "{% trans 'Current Income' %}", + data: currencyData.datasets[0].data, + backgroundColor: '#4dde80', + stack: 'stack0' + }, + { + label: "{% trans 'Current Expenses' %}", + data: currencyData.datasets[1].data, + backgroundColor: '#f87171', + stack: 'stack0' + }, + { + label: "{% trans 'Projected Income' %}", + data: currencyData.datasets[2].data, + backgroundColor: '#4dde8080', // Added transparency + stack: 'stack0' + }, + { + label: "{% trans 'Projected Expenses' %}", + data: currencyData.datasets[3].data, + backgroundColor: '#f8717180', // Added transparency + stack: 'stack0' + } + ] + }, + options: chartOptions + } + ); + } + {% else %} - + {% endif %} diff --git a/app/templates/insights/fragments/category_explorer/index.html b/app/templates/insights/fragments/category_explorer/index.html index c131a02..c0dd4df 100644 --- a/app/templates/insights/fragments/category_explorer/index.html +++ b/app/templates/insights/fragments/category_explorer/index.html @@ -2,7 +2,8 @@ {% load crispy_forms_tags %}
+ on change trigger updated + init trigger updated" id="category-form"> {% crispy category_form %}
@@ -15,7 +16,6 @@
-
@@ -28,7 +28,6 @@
-