From 09e380a480780b3289d635f30a6df932de6fbc27 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Tue, 18 Feb 2025 20:45:07 -0300 Subject: [PATCH 1/5] feat(insights:category-explorer): allow for uncategorized totals --- app/apps/insights/forms.py | 3 +++ app/apps/insights/views.py | 6 ++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/apps/insights/forms.py b/app/apps/insights/forms.py index 34c6c8f..50d8428 100644 --- a/app/apps/insights/forms.py +++ b/app/apps/insights/forms.py @@ -9,6 +9,7 @@ from apps.common.widgets.datepicker import ( AirDatePickerInput, ) from apps.transactions.models import TransactionCategory +from apps.common.widgets.tom_select import TomSelect class SingleMonthForm(forms.Form): @@ -115,7 +116,9 @@ class CategoryForm(forms.Form): category = forms.ModelChoiceField( required=False, label=_("Category"), + empty_label=_("Uncategorized"), queryset=TransactionCategory.objects.filter(active=True), + widget=TomSelect(clear_button=True), ) def __init__(self, *args, **kwargs): diff --git a/app/apps/insights/views.py b/app/apps/insights/views.py index 177f57f..c6738cc 100644 --- a/app/apps/insights/views.py +++ b/app/apps/insights/views.py @@ -126,7 +126,7 @@ def category_sum_by_account(request): # Generate data account_data = get_category_sums_by_account(transactions, category) else: - account_data = None + account_data = get_category_sums_by_account(transactions, category=None) return render( request, @@ -150,9 +150,7 @@ def category_sum_by_currency(request): # Generate data currency_data = get_category_sums_by_currency(transactions, category) else: - currency_data = None - - print(currency_data) + currency_data = get_category_sums_by_currency(transactions, category=None) return render( request, From 835316d0f3a973fff18f8e762949877a6b128bb6 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Tue, 18 Feb 2025 20:46:06 -0300 Subject: [PATCH 2/5] 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 @@
-
From beeb0579ce111ee2c99bbcb5bc3fbd1174c69018 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Tue, 18 Feb 2025 21:04:09 -0300 Subject: [PATCH 3/5] feat(insights): make sidebar sticky --- app/templates/insights/pages/index.html | 125 +++++++++++++----------- 1 file changed, 67 insertions(+), 58 deletions(-) diff --git a/app/templates/insights/pages/index.html b/app/templates/insights/pages/index.html index 3afbe45..3ec8aa3 100644 --- a/app/templates/insights/pages/index.html +++ b/app/templates/insights/pages/index.html @@ -4,11 +4,12 @@ {% block content %}
-
+
-
-
+
+
- - + id="picker-type"> + + - - + + - - + + - - + + - - -
-
+ +
+ -
- {% crispy month_form %} -
-
- {% crispy year_form %} -
-
- {% crispy month_range_form %} -
-
- {% crispy year_range_form %} -
-
- {% crispy date_range_form %} -
- -
-
- +
+
From 9ac69fd92a238c344f705c411ff6b146cafde10b Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Wed, 19 Feb 2025 08:59:30 -0300 Subject: [PATCH 4/5] fix(transactions:actions): sum considers everything an expense --- app/templates/cotton/transaction/item.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/cotton/transaction/item.html b/app/templates/cotton/transaction/item.html index 21f1fda..1fa4f35 100644 --- a/app/templates/cotton/transaction/item.html +++ b/app/templates/cotton/transaction/item.html @@ -1,7 +1,7 @@ {% load markdown %} {% load i18n %} -
-
+
+
{% if not disable_selection %}