Compare commits

...

6 Commits

Author SHA1 Message Date
Herculino Trotta
84c047c5ab Merge pull request #167 from eitchtee/insights
insights
2025-02-16 13:06:03 -03:00
Herculino Trotta
23f5d09bec locale: update locales 2025-02-16 13:05:35 -03:00
Herculino Trotta
2a19075e23 Merge pull request #166
feat(insights): category explorer
2025-02-16 13:03:20 -03:00
Herculino Trotta
7f231175b2 feat(insights): category explorer 2025-02-16 13:03:02 -03:00
Herculino Trotta
062e84f864 Merge pull request #165
fix(insights): sankey diagrams nodes too far from destination
2025-02-16 02:25:45 -03:00
Herculino Trotta
5521eb20bf fix(insights): sankey diagrams nodes too far from destination 2025-02-16 02:25:29 -03:00
13 changed files with 613 additions and 133 deletions

View File

@@ -8,6 +8,7 @@ from apps.common.widgets.datepicker import (
AirYearPickerInput,
AirDatePickerInput,
)
from apps.transactions.models import TransactionCategory
class SingleMonthForm(forms.Form):
@@ -108,3 +109,20 @@ class DateRangeForm(forms.Form):
css_class="mb-0",
),
)
class CategoryForm(forms.Form):
category = forms.ModelChoiceField(
required=False,
label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.disable_csrf = True
self.helper.layout = Layout("category")

View File

@@ -14,4 +14,19 @@ urlpatterns = [
views.sankey_by_currency,
name="insights_sankey_by_currency",
),
path(
"insights/category-explorer/",
views.category_explorer_index,
name="category_explorer_index",
),
path(
"insights/category-explorer/account/",
views.category_sum_by_account,
name="category_sum_by_account",
),
path(
"insights/category-explorer/currency/",
views.category_sum_by_currency,
name="category_sum_by_currency",
),
]

View File

@@ -0,0 +1,101 @@
from django.db.models import Sum, Case, When, F, DecimalField, Value
from django.db.models.functions import Coalesce
from django.utils.translation import gettext_lazy as _
def get_category_sums_by_account(queryset, category):
"""
Returns income/expense sums per account for a specific category.
"""
sums = (
queryset.filter(category=category)
.values("account__name")
.annotate(
income=Coalesce(
Sum(
Case(
When(type="IN", 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),
),
expense=Coalesce(
Sum(
Case(
When(type="EX", then=-F("amount")),
default=Value(0),
output_field=DecimalField(max_digits=42, decimal_places=30),
)
),
Value(0),
output_field=DecimalField(max_digits=42, decimal_places=30),
),
)
.order_by("account__name")
)
return {
"labels": [item["account__name"] for item in sums],
"datasets": [
{
"label": _("Income"),
"data": [float(item["income"]) for item in sums],
},
{
"label": _("Expenses"),
"data": [float(item["expense"]) for item in sums],
},
],
}
def get_category_sums_by_currency(queryset, category):
"""
Returns income/expense sums per currency for a specific category.
"""
sums = (
queryset.filter(category=category)
.values("account__currency__code")
.annotate(
income=Coalesce(
Sum(
Case(
When(type="IN", 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),
),
expense=Coalesce(
Sum(
Case(
When(type="EX", then=-F("amount")),
default=Value(0),
output_field=DecimalField(max_digits=42, decimal_places=30),
)
),
Value(0),
output_field=DecimalField(max_digits=42, decimal_places=30),
),
)
.order_by("account__currency__code")
)
return {
"labels": [item["account__currency__code"] for item in sums],
"datasets": [
{
"label": _("Income"),
"data": [float(item["income"]) for item in sums],
},
{
"label": _("Expenses"),
"data": [float(item["expense"]) for item in sums],
},
],
}

View File

@@ -52,13 +52,29 @@ def generate_sankey_data_by_account(transactions_queryset):
total_volume_by_currency.get(currency, Decimal("0")) + amount
)
unique_accounts = {
account_id: idx
for idx, account_id in enumerate(
transactions_queryset.values_list("account", flat=True).distinct()
)
}
def get_node_priority(node_id: str) -> int:
"""Get priority based on the account ID embedded in the node ID."""
account_id = int(node_id.split("_")[-1])
return unique_accounts[account_id]
def get_node_id(node_type: str, name: str, account_id: int) -> str:
"""Generate unique node ID."""
return f"{node_type}_{name}_{account_id}".lower().replace(" ", "_")
def add_node(node_id: str, display_name: str) -> None:
"""Add node with both ID and display name."""
nodes[node_id] = {"id": node_id, "name": display_name}
"""Add node with ID, display name and priority."""
nodes[node_id] = {
"id": node_id,
"name": display_name,
"priority": get_node_priority(node_id),
}
def add_flow(
from_node_id: str, to_node_id: str, amount: Decimal, currency, is_income: bool
@@ -167,13 +183,29 @@ def generate_sankey_data_by_currency(transactions_queryset):
total_volume_by_currency.get(currency, Decimal("0")) + amount
)
unique_currencies = {
currency_id: idx
for idx, currency_id in enumerate(
transactions_queryset.values_list("account__currency", flat=True).distinct()
)
}
def get_node_priority(node_id: str) -> int:
"""Get priority based on the currency ID embedded in the node ID."""
currency_id = int(node_id.split("_")[-1])
return unique_currencies[currency_id]
def get_node_id(node_type: str, name: str, currency_id: int) -> str:
"""Generate unique node ID including currency information."""
return f"{node_type}_{name}_{currency_id}".lower().replace(" ", "_")
def add_node(node_id: str, display_name: str) -> None:
"""Add node with both ID and display name."""
nodes[node_id] = {"id": node_id, "name": display_name}
"""Add node with ID, display name and priority."""
nodes[node_id] = {
"id": node_id,
"name": display_name,
"priority": get_node_priority(node_id),
}
def add_flow(
from_node_id: str, to_node_id: str, amount: Decimal, currency, is_income: bool

View File

@@ -1,24 +1,28 @@
from dateutil.relativedelta import relativedelta
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from django.utils import timezone
from django.views.decorators.http import require_http_methods
from dateutil.relativedelta import relativedelta
from apps.transactions.models import Transaction
from apps.insights.utils.sankey import (
generate_sankey_data_by_account,
generate_sankey_data_by_currency,
)
from apps.common.decorators.htmx import only_htmx
from apps.insights.forms import (
SingleMonthForm,
SingleYearForm,
MonthRangeForm,
YearRangeForm,
DateRangeForm,
CategoryForm,
)
from apps.insights.utils.category_explorer import (
get_category_sums_by_account,
get_category_sums_by_currency,
)
from apps.insights.utils.sankey import (
generate_sankey_data_by_account,
generate_sankey_data_by_currency,
)
from apps.common.decorators.htmx import only_htmx
from apps.insights.utils.transactions import get_transactions
from apps.transactions.models import TransactionCategory
@login_required
@@ -61,7 +65,7 @@ def index(request):
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])
@require_http_methods(["GET"])
def sankey_by_account(request):
# Get filtered transactions
@@ -79,7 +83,7 @@ def sankey_by_account(request):
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])
@require_http_methods(["GET"])
def sankey_by_currency(request):
# Get filtered transactions
transactions = get_transactions(request)
@@ -92,3 +96,66 @@ def sankey_by_currency(request):
"insights/fragments/sankey.html",
{"sankey_data": sankey_data, "type": "currency"},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def category_explorer_index(request):
category_form = CategoryForm()
return render(
request,
"insights/fragments/category_explorer/index.html",
{"category_form": category_form},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def category_sum_by_account(request):
# Get filtered transactions
transactions = get_transactions(request)
category = request.GET.get("category")
if category:
category = TransactionCategory.objects.get(id=category)
# Generate data
account_data = get_category_sums_by_account(transactions, category)
else:
account_data = None
return render(
request,
"insights/fragments/category_explorer/charts/account.html",
{"account_data": account_data},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def category_sum_by_currency(request):
# Get filtered transactions
transactions = get_transactions(request)
category = request.GET.get("category")
if category:
category = TransactionCategory.objects.get(id=category)
# Generate data
currency_data = get_category_sums_by_currency(transactions, category)
else:
currency_data = None
print(currency_data)
return render(
request,
"insights/fragments/category_explorer/charts/currency.html",
{"currency_data": currency_data},
)

View File

@@ -2,13 +2,13 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-16 00:03-0300\n"
"POT-Creation-Date: 2025-02-16 13:04-0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -69,12 +69,12 @@ msgid "New balance"
msgstr ""
#: apps/accounts/forms.py:119 apps/dca/forms.py:85 apps/dca/forms.py:92
#: apps/rules/forms.py:168 apps/rules/forms.py:183 apps/rules/models.py:32
#: apps/rules/models.py:280 apps/transactions/forms.py:39
#: apps/transactions/forms.py:291 apps/transactions/forms.py:298
#: apps/transactions/forms.py:478 apps/transactions/forms.py:723
#: apps/transactions/models.py:203 apps/transactions/models.py:378
#: apps/transactions/models.py:558
#: apps/insights/forms.py:117 apps/rules/forms.py:168 apps/rules/forms.py:183
#: apps/rules/models.py:32 apps/rules/models.py:280
#: apps/transactions/forms.py:39 apps/transactions/forms.py:291
#: apps/transactions/forms.py:298 apps/transactions/forms.py:478
#: apps/transactions/forms.py:723 apps/transactions/models.py:203
#: apps/transactions/models.py:378 apps/transactions/models.py:558
msgid "Category"
msgstr ""
@@ -745,12 +745,33 @@ msgstr ""
msgid "Run deleted successfully"
msgstr ""
#: apps/insights/utils/sankey.py:37 apps/insights/utils/sankey.py:154
#: apps/insights/utils/category_explorer.py:44
#: apps/insights/utils/category_explorer.py:93 apps/transactions/models.py:170
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: 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_explorer/charts/account.html:54
#: templates/insights/fragments/category_explorer/charts/currency.html:55
#: templates/monthly_overview/fragments/monthly_summary.html:39
msgid "Income"
msgstr ""
#: apps/insights/utils/category_explorer.py:48
#: apps/insights/utils/category_explorer.py:97
#: templates/insights/fragments/category_explorer/charts/account.html:60
#: templates/insights/fragments/category_explorer/charts/currency.html:61
#: templates/monthly_overview/fragments/monthly_summary.html:103
msgid "Expenses"
msgstr ""
#: apps/insights/utils/sankey.py:36 apps/insights/utils/sankey.py:167
msgid "Uncategorized"
msgstr ""
#: apps/insights/utils/sankey.py:118 apps/insights/utils/sankey.py:119
#: apps/insights/utils/sankey.py:234 apps/insights/utils/sankey.py:235
#: apps/insights/utils/sankey.py:133 apps/insights/utils/sankey.py:134
#: apps/insights/utils/sankey.py:263 apps/insights/utils/sankey.py:264
msgid "Saved"
msgstr ""
@@ -770,7 +791,7 @@ msgstr ""
msgid "Set field"
msgstr ""
#: apps/rules/forms.py:65 templates/insights/fragments/sankey.html:84
#: apps/rules/forms.py:65 templates/insights/fragments/sankey.html:90
msgid "To"
msgstr ""
@@ -811,7 +832,7 @@ msgstr ""
#: apps/rules/forms.py:165 apps/rules/forms.py:178 apps/rules/models.py:29
#: apps/rules/models.py:256 apps/transactions/models.py:192
#: apps/transactions/models.py:551 templates/insights/fragments/sankey.html:85
#: apps/transactions/models.py:551 templates/insights/fragments/sankey.html:91
msgid "Amount"
msgstr ""
@@ -1090,16 +1111,6 @@ msgstr ""
msgid "Entity"
msgstr ""
#: apps/transactions/models.py:170
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: templates/calendar_view/fragments/list.html:52
#: templates/calendar_view/fragments/list.html:54
#: templates/cotton/ui/quick_transactions_buttons.html:10
#: templates/monthly_overview/fragments/monthly_summary.html:39
msgid "Income"
msgstr ""
#: apps/transactions/models.py:171
#: templates/calendar_view/fragments/list.html:46
#: templates/calendar_view/fragments/list.html:48
@@ -2328,11 +2339,19 @@ msgstr ""
msgid "Confirm"
msgstr ""
#: templates/insights/fragments/sankey.html:83
#: templates/insights/fragments/category_explorer/index.html:13
msgid "Income/Expense by Account"
msgstr ""
#: templates/insights/fragments/category_explorer/index.html:25
msgid "Income/Expense by Currency"
msgstr ""
#: templates/insights/fragments/sankey.html:89
msgid "From"
msgstr ""
#: templates/insights/fragments/sankey.html:86
#: templates/insights/fragments/sankey.html:92
msgid "Percentage"
msgstr ""
@@ -2366,6 +2385,10 @@ msgstr ""
msgid "Currency Flow"
msgstr ""
#: templates/insights/pages/index.html:88
msgid "Category Explorer"
msgstr ""
#: templates/installment_plans/fragments/add.html:5
msgid "Add installment plan"
msgstr ""
@@ -2464,10 +2487,6 @@ msgstr ""
msgid "projected"
msgstr ""
#: templates/monthly_overview/fragments/monthly_summary.html:103
msgid "Expenses"
msgstr ""
#: templates/monthly_overview/fragments/monthly_summary.html:167
msgid "Total"
msgstr ""

View File

@@ -2,13 +2,13 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-16 00:03-0300\n"
"POT-Creation-Date: 2025-02-16 13:04-0300\n"
"PO-Revision-Date: 2025-02-12 06:58+0100\n"
"Last-Translator: Dimitri Decrock <dimitri@fam-decrock.eu>\n"
"Language-Team: \n"
@@ -70,12 +70,12 @@ msgid "New balance"
msgstr "Nieuw saldo"
#: apps/accounts/forms.py:119 apps/dca/forms.py:85 apps/dca/forms.py:92
#: apps/rules/forms.py:168 apps/rules/forms.py:183 apps/rules/models.py:32
#: apps/rules/models.py:280 apps/transactions/forms.py:39
#: apps/transactions/forms.py:291 apps/transactions/forms.py:298
#: apps/transactions/forms.py:478 apps/transactions/forms.py:723
#: apps/transactions/models.py:203 apps/transactions/models.py:378
#: apps/transactions/models.py:558
#: apps/insights/forms.py:117 apps/rules/forms.py:168 apps/rules/forms.py:183
#: apps/rules/models.py:32 apps/rules/models.py:280
#: apps/transactions/forms.py:39 apps/transactions/forms.py:291
#: apps/transactions/forms.py:298 apps/transactions/forms.py:478
#: apps/transactions/forms.py:723 apps/transactions/models.py:203
#: apps/transactions/models.py:378 apps/transactions/models.py:558
msgid "Category"
msgstr "Categorie"
@@ -765,14 +765,35 @@ msgstr "Importrun met succes in de wachtrij geplaatst"
msgid "Run deleted successfully"
msgstr "Run met succes verwijderd"
#: apps/insights/utils/sankey.py:37 apps/insights/utils/sankey.py:154
#: apps/insights/utils/category_explorer.py:44
#: apps/insights/utils/category_explorer.py:93 apps/transactions/models.py:170
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: 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_explorer/charts/account.html:54
#: templates/insights/fragments/category_explorer/charts/currency.html:55
#: templates/monthly_overview/fragments/monthly_summary.html:39
msgid "Income"
msgstr "Ontvangsten Transactie"
#: apps/insights/utils/category_explorer.py:48
#: apps/insights/utils/category_explorer.py:97
#: templates/insights/fragments/category_explorer/charts/account.html:60
#: templates/insights/fragments/category_explorer/charts/currency.html:61
#: templates/monthly_overview/fragments/monthly_summary.html:103
msgid "Expenses"
msgstr "Uitgaven"
#: apps/insights/utils/sankey.py:36 apps/insights/utils/sankey.py:167
#, fuzzy
#| msgid "Categories"
msgid "Uncategorized"
msgstr "Categorieën"
#: apps/insights/utils/sankey.py:118 apps/insights/utils/sankey.py:119
#: apps/insights/utils/sankey.py:234 apps/insights/utils/sankey.py:235
#: apps/insights/utils/sankey.py:133 apps/insights/utils/sankey.py:134
#: apps/insights/utils/sankey.py:263 apps/insights/utils/sankey.py:264
#, fuzzy
#| msgid "Save"
msgid "Saved"
@@ -794,7 +815,7 @@ msgstr "Als..."
msgid "Set field"
msgstr "Veld instellen"
#: apps/rules/forms.py:65 templates/insights/fragments/sankey.html:84
#: apps/rules/forms.py:65 templates/insights/fragments/sankey.html:90
msgid "To"
msgstr "Naar"
@@ -835,7 +856,7 @@ msgstr "Referentiedatum"
#: apps/rules/forms.py:165 apps/rules/forms.py:178 apps/rules/models.py:29
#: apps/rules/models.py:256 apps/transactions/models.py:192
#: apps/transactions/models.py:551 templates/insights/fragments/sankey.html:85
#: apps/transactions/models.py:551 templates/insights/fragments/sankey.html:91
msgid "Amount"
msgstr "Bedrag"
@@ -1123,16 +1144,6 @@ msgstr ""
msgid "Entity"
msgstr "Bedrijf"
#: apps/transactions/models.py:170
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: templates/calendar_view/fragments/list.html:52
#: templates/calendar_view/fragments/list.html:54
#: templates/cotton/ui/quick_transactions_buttons.html:10
#: templates/monthly_overview/fragments/monthly_summary.html:39
msgid "Income"
msgstr "Ontvangsten Transactie"
#: apps/transactions/models.py:171
#: templates/calendar_view/fragments/list.html:46
#: templates/calendar_view/fragments/list.html:48
@@ -2368,11 +2379,21 @@ msgstr "Annuleer"
msgid "Confirm"
msgstr "Bevestig"
#: templates/insights/fragments/sankey.html:83
#: templates/insights/fragments/category_explorer/index.html:13
msgid "Income/Expense by Account"
msgstr ""
#: templates/insights/fragments/category_explorer/index.html:25
#, fuzzy
#| msgid "Exchange Currency"
msgid "Income/Expense by Currency"
msgstr "Eenheid Wisselgeld"
#: templates/insights/fragments/sankey.html:89
msgid "From"
msgstr ""
#: templates/insights/fragments/sankey.html:86
#: templates/insights/fragments/sankey.html:92
msgid "Percentage"
msgstr ""
@@ -2416,6 +2437,12 @@ msgstr "Rekening"
msgid "Currency Flow"
msgstr "Munteenheids Code"
#: templates/insights/pages/index.html:88
#, fuzzy
#| msgid "Category name"
msgid "Category Explorer"
msgstr "Naam van categorie"
#: templates/installment_plans/fragments/add.html:5
msgid "Add installment plan"
msgstr "Afbetalingsplan toevoegen"
@@ -2516,10 +2543,6 @@ msgstr "actueel"
msgid "projected"
msgstr "verwacht"
#: templates/monthly_overview/fragments/monthly_summary.html:103
msgid "Expenses"
msgstr "Uitgaven"
#: templates/monthly_overview/fragments/monthly_summary.html:167
msgid "Total"
msgstr "Totaal"
@@ -2879,11 +2902,6 @@ msgstr "Jaaroverzicht"
#~ msgid "Search Category"
#~ msgstr "Categorie"
#, fuzzy
#~| msgid "Category name"
#~ msgid "Category Operator"
#~ msgstr "Naam van categorie"
#, fuzzy
#~| msgid "Search"
#~ msgid "Search Tags"

View File

@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-16 00:03-0300\n"
"PO-Revision-Date: 2025-02-16 00:04-0300\n"
"POT-Creation-Date: 2025-02-16 13:04-0300\n"
"PO-Revision-Date: 2025-02-16 13:05-0300\n"
"Last-Translator: Herculino Trotta\n"
"Language-Team: \n"
"Language: pt_BR\n"
@@ -70,12 +70,12 @@ msgid "New balance"
msgstr "Novo saldo"
#: apps/accounts/forms.py:119 apps/dca/forms.py:85 apps/dca/forms.py:92
#: apps/rules/forms.py:168 apps/rules/forms.py:183 apps/rules/models.py:32
#: apps/rules/models.py:280 apps/transactions/forms.py:39
#: apps/transactions/forms.py:291 apps/transactions/forms.py:298
#: apps/transactions/forms.py:478 apps/transactions/forms.py:723
#: apps/transactions/models.py:203 apps/transactions/models.py:378
#: apps/transactions/models.py:558
#: apps/insights/forms.py:117 apps/rules/forms.py:168 apps/rules/forms.py:183
#: apps/rules/models.py:32 apps/rules/models.py:280
#: apps/transactions/forms.py:39 apps/transactions/forms.py:291
#: apps/transactions/forms.py:298 apps/transactions/forms.py:478
#: apps/transactions/forms.py:723 apps/transactions/models.py:203
#: apps/transactions/models.py:378 apps/transactions/models.py:558
msgid "Category"
msgstr "Categoria"
@@ -759,12 +759,33 @@ msgstr "Importação adicionada à fila com sucesso"
msgid "Run deleted successfully"
msgstr "Importação apagada com sucesso"
#: apps/insights/utils/sankey.py:37 apps/insights/utils/sankey.py:154
#: apps/insights/utils/category_explorer.py:44
#: apps/insights/utils/category_explorer.py:93 apps/transactions/models.py:170
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: 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_explorer/charts/account.html:54
#: templates/insights/fragments/category_explorer/charts/currency.html:55
#: templates/monthly_overview/fragments/monthly_summary.html:39
msgid "Income"
msgstr "Renda"
#: apps/insights/utils/category_explorer.py:48
#: apps/insights/utils/category_explorer.py:97
#: templates/insights/fragments/category_explorer/charts/account.html:60
#: templates/insights/fragments/category_explorer/charts/currency.html:61
#: templates/monthly_overview/fragments/monthly_summary.html:103
msgid "Expenses"
msgstr "Despesas"
#: apps/insights/utils/sankey.py:36 apps/insights/utils/sankey.py:167
msgid "Uncategorized"
msgstr "Sem categoria"
#: apps/insights/utils/sankey.py:118 apps/insights/utils/sankey.py:119
#: apps/insights/utils/sankey.py:234 apps/insights/utils/sankey.py:235
#: apps/insights/utils/sankey.py:133 apps/insights/utils/sankey.py:134
#: apps/insights/utils/sankey.py:263 apps/insights/utils/sankey.py:264
msgid "Saved"
msgstr "Salvo"
@@ -784,7 +805,7 @@ msgstr "Se..."
msgid "Set field"
msgstr "Definir campo"
#: apps/rules/forms.py:65 templates/insights/fragments/sankey.html:84
#: apps/rules/forms.py:65 templates/insights/fragments/sankey.html:90
msgid "To"
msgstr "Para"
@@ -825,7 +846,7 @@ msgstr "Data de Referência"
#: apps/rules/forms.py:165 apps/rules/forms.py:178 apps/rules/models.py:29
#: apps/rules/models.py:256 apps/transactions/models.py:192
#: apps/transactions/models.py:551 templates/insights/fragments/sankey.html:85
#: apps/transactions/models.py:551 templates/insights/fragments/sankey.html:91
msgid "Amount"
msgstr "Quantia"
@@ -1111,16 +1132,6 @@ msgstr ""
msgid "Entity"
msgstr "Entidade"
#: apps/transactions/models.py:170
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: templates/calendar_view/fragments/list.html:52
#: templates/calendar_view/fragments/list.html:54
#: templates/cotton/ui/quick_transactions_buttons.html:10
#: templates/monthly_overview/fragments/monthly_summary.html:39
msgid "Income"
msgstr "Renda"
#: apps/transactions/models.py:171
#: templates/calendar_view/fragments/list.html:46
#: templates/calendar_view/fragments/list.html:48
@@ -2353,11 +2364,19 @@ msgstr "Cancelar"
msgid "Confirm"
msgstr "Confirmar"
#: templates/insights/fragments/sankey.html:83
#: templates/insights/fragments/category_explorer/index.html:13
msgid "Income/Expense by Account"
msgstr "Gasto/Despesa por Conta"
#: templates/insights/fragments/category_explorer/index.html:25
msgid "Income/Expense by Currency"
msgstr "Gasto/Despesa por Moeda"
#: templates/insights/fragments/sankey.html:89
msgid "From"
msgstr "De"
#: templates/insights/fragments/sankey.html:86
#: templates/insights/fragments/sankey.html:92
msgid "Percentage"
msgstr "Porcentagem"
@@ -2391,6 +2410,10 @@ msgstr "Fluxo de Conta"
msgid "Currency Flow"
msgstr "Fluxo de Moeda"
#: templates/insights/pages/index.html:88
msgid "Category Explorer"
msgstr "Explorador de Categoria"
#: templates/installment_plans/fragments/add.html:5
msgid "Add installment plan"
msgstr "Adicionar parcelamento"
@@ -2491,10 +2514,6 @@ msgstr "atual"
msgid "projected"
msgstr "previsto"
#: templates/monthly_overview/fragments/monthly_summary.html:103
msgid "Expenses"
msgstr "Despesas"
#: templates/monthly_overview/fragments/monthly_summary.html:167
msgid "Total"
msgstr "Total"
@@ -2856,11 +2875,6 @@ msgstr "Visão Anual"
#~ msgid "Search Category"
#~ msgstr "Categoria"
#, fuzzy
#~| msgid "Category name"
#~ msgid "Category Operator"
#~ msgstr "Nome da Categoria"
#, fuzzy
#~| msgid "Search"
#~ msgid "Search Tags"

View File

@@ -0,0 +1,79 @@
{% load i18n %}
<div class="chart-container" style="position: relative; height:400px; width:100%" _="init call setupAccountChart() end">
<canvas id="accountChart"></canvas>
</div>
<script>
// Get the data from your Django view (passed as JSON)
var accountData = {{ account_data|safe }};
function setupAccountChart() {
var chartOptions = {
indexAxis: 'y', // This makes the chart horizontal
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true, // Enable stacking on the x-axis
title: {
display: false,
},
},
y: {
stacked: true, // Enable stacking on the y-axis
title: {
display: false,
},
}
},
plugins: {
legend: {
display: false,
},
tooltip: {
callbacks: {
label: function (context) {
if (context.parsed.x !== null) {
return new Intl.NumberFormat(undefined).format(Math.abs(context.parsed.x)); // Using abs for display
}
return "";
},
}
}
}
};
new Chart(
document.getElementById('accountChart'),
{
type: 'bar',
data: {
labels: accountData.labels,
datasets: [
{
label: "{% trans 'Income' %}",
data: accountData.datasets[0].data,
backgroundColor: '#4dde80',
stack: 'stack0'
},
{
label: "{% trans 'Expenses' %}",
data: accountData.datasets[1].data,
backgroundColor: '#f87171',
stack: 'stack0'
}
]
},
options: {
...chartOptions,
plugins: {
...chartOptions.plugins,
title: {
display: false,
}
}
}
}
);
}
</script>

View File

@@ -0,0 +1,80 @@
{% load i18n %}
<div class="chart-container" style="position: relative; height:400px; width:100%"
_="init call setupCurrencyChart() end">
<canvas id="currencyChart"></canvas>
</div>
<script>
// Get the data from your Django view (passed as JSON)
var currencyData = {{ currency_data|safe }};
function setupCurrencyChart() {
var chartOptions = {
indexAxis: 'y', // This makes the chart horizontal
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true, // Enable stacking on the x-axis
title: {
display: false,
},
},
y: {
stacked: true, // Enable stacking on the y-axis
title: {
display: false,
},
}
},
plugins: {
legend: {
display: false,
},
tooltip: {
callbacks: {
label: function (context) {
if (context.parsed.x !== null) {
return new Intl.NumberFormat(undefined).format(Math.abs(context.parsed.x)); // Using abs for display
}
return "";
},
}
}
}
};
new Chart(
document.getElementById('currencyChart'),
{
type: 'bar',
data: {
labels: currencyData.labels,
datasets: [
{
label: "{% trans 'Income' %}",
data: currencyData.datasets[0].data,
backgroundColor: '#4dde80',
stack: 'stack0'
},
{
label: "{% trans 'Expenses' %}",
data: currencyData.datasets[1].data,
backgroundColor: '#f87171',
stack: 'stack0'
}
]
},
options: {
...chartOptions,
plugins: {
...chartOptions.plugins,
title: {
display: false,
}
}
}
}
);
}
</script>

View File

@@ -0,0 +1,37 @@
{% load i18n %}
{% load crispy_forms_tags %}
<form _="install init_tom_select
on change trigger updated" id="category-form">
{% crispy category_form %}
</form>
<div class="row row-cols-1 row-cols-lg-2 gx-3 gy-3">
<div class="col">
<div class="card">
<div class="card-header">
{% trans "Income/Expense by Account" %}
</div>
<div class="card-body">
<div id="account-card" class="show-loading" hx-get="{% url 'category_sum_by_account' %}"
hx-trigger="updated from:window" hx-include="#category-form, #picker-form, #picker-type">
</div>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-header">
{% trans "Income/Expense by Currency" %}
</div>
<div class="card-body">
<div id="currency-card" class="show-loading" hx-get="{% url 'category_sum_by_currency' %}"
hx-trigger="updated from:window" hx-include="#category-form, #picker-form, #picker-type">
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,15 +1,17 @@
{% load i18n %}
{% if type == 'account' %}
<div class="show-loading" hx-get="{% url 'insights_sankey_by_account' %}" hx-trigger="updated from:window" hx-swap="outerHTML" hx-include="#picker-form, #picker-type">
<div class="show-loading" hx-get="{% url 'insights_sankey_by_account' %}" hx-trigger="updated from:window"
hx-swap="outerHTML" hx-include="#picker-form, #picker-type">
{% else %}
<div class="show-loading" hx-get="{% url 'insights_sankey_by_currency' %}" hx-trigger="updated from:window" hx-swap="outerHTML" hx-include="#picker-form, #picker-type">
<div class="show-loading" hx-get="{% url 'insights_sankey_by_currency' %}" hx-trigger="updated from:window"
hx-swap="outerHTML" hx-include="#picker-form, #picker-type">
{% endif %}
<div class="chart-container position-relative tw-min-h-[60vh] tw-max-h-[60vh] tw-h-full tw-w-full"
id="sankeyContainer"
_="init call setupSankeyChart() end">
<canvas id="sankeyChart"></canvas>
</div>
<div class="chart-container position-relative tw-min-h-[60vh] tw-max-h-[60vh] tw-h-full tw-w-full"
id="sankeyContainer"
_="init call setupSankeyChart() end">
<canvas id="sankeyChart"></canvas>
</div>
</div>
<script>
@@ -61,7 +63,11 @@
colorMode: 'gradient',
alpha: 0.5,
size: 'max',
color: "white"
color: "white",
priority: data.nodes.reduce((acc, node) => {
acc[node.id] = node.priority;
return acc;
}, {}),
}]
};
@@ -98,23 +104,10 @@
}
};
// Destroy existing chart if it exists
const existingChart = Chart.getChart(chartId);
if (existingChart) {
existingChart.destroy();
}
// Create new chart
var chart = new Chart(
new Chart(
document.getElementById(chartId),
config
);
window.addEventListener('resize', () => {
chart.resize();
});
document.addEventListener('fullscreenchange', function () {
console.log('oi');
chart.resize();
});
}
</script>

View File

@@ -80,6 +80,13 @@
hx-indicator="#tab-content"
hx-target="#tab-content">{% trans 'Currency Flow' %}
</button>
<button class="nav-link" id="v-pills-tab" data-bs-toggle="pill" data-bs-target="#v-pills-content"
type="button" role="tab" aria-controls="v-pills-content" aria-selected="false"
hx-get="{% url 'category_explorer_index' %}"
hx-include="#picker-form, #picker-type"
hx-indicator="#tab-content"
hx-target="#tab-content">{% trans 'Category Explorer' %}
</button>
</div>
</div>
<div class="col-md-9 col-lg-10">