feat: insight page

This commit is contained in:
Herculino Trotta
2025-02-16 00:14:23 -03:00
parent daf1f68b82
commit 195a8a68d6
15 changed files with 1062 additions and 262 deletions

View File

@@ -227,3 +227,56 @@ class AirMonthYearPickerInput(AirDatePickerInput):
except (ValueError, KeyError):
return None
return None
class AirYearPickerInput(AirDatePickerInput):
def __init__(self, attrs=None, format=None, *args, **kwargs):
super().__init__(attrs=attrs, format=format, *args, **kwargs)
# Store the display format for AirDatepicker
self.display_format = "yyyy"
# Store the Python format for internal use
self.python_format = "%Y"
def build_attrs(self, base_attrs, extra_attrs=None):
attrs = super().build_attrs(base_attrs, extra_attrs)
# Add data attributes for AirDatepicker configuration
attrs["data-now-button-txt"] = _("Today")
attrs["data-date-format"] = "yyyy"
return attrs
def format_value(self, value):
"""Format the value for display in the widget."""
if value:
self.attrs["data-value"] = (
value # We use this to dynamically select the initial date on AirDatePicker
)
if value is None:
return ""
if isinstance(value, str):
try:
value = datetime.datetime.strptime(value, "%Y-%m-%d").date()
except ValueError:
return value
if isinstance(value, (datetime.datetime, datetime.date)):
# Use Django's date translation
return f"{value.year}"
return value
def value_from_datadict(self, data, files, name):
"""Convert the value from the widget format back to a format Django can handle."""
value = super().value_from_datadict(data, files, name)
if value:
try:
# Split the value into month name and year
year_str = value
year = int(year_str)
if year:
# Return the first day of the month in Django's expected format
return datetime.date(year, 1, 1).strftime("%Y-%m-%d")
except (ValueError, KeyError):
return None
return None

110
app/apps/insights/forms.py Normal file
View File

@@ -0,0 +1,110 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, Row, Column
from django import forms
from django.utils.translation import gettext_lazy as _
from apps.common.widgets.datepicker import (
AirMonthYearPickerInput,
AirYearPickerInput,
AirDatePickerInput,
)
class SingleMonthForm(forms.Form):
month = forms.DateField(
widget=AirMonthYearPickerInput(clear_button=False), label="", required=False
)
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(Field("month"))
class SingleYearForm(forms.Form):
year = forms.DateField(
widget=AirYearPickerInput(clear_button=False), label="", required=False
)
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(Field("year"))
class MonthRangeForm(forms.Form):
month_from = forms.DateField(
widget=AirMonthYearPickerInput(clear_button=False), label="", required=False
)
month_to = forms.DateField(
widget=AirMonthYearPickerInput(clear_button=False), label="", required=False
)
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(
Row(
Column("month_from", css_class="form-group col-md-6"),
Column("month_to", css_class="form-group col-md-6"),
),
)
class YearRangeForm(forms.Form):
year_from = forms.DateField(
widget=AirYearPickerInput(clear_button=False), label="", required=False
)
year_to = forms.DateField(
widget=AirYearPickerInput(clear_button=False), label="", required=False
)
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(
Row(
Column("year_from", css_class="form-group col-md-6"),
Column("year_to", css_class="form-group col-md-6"),
),
)
class DateRangeForm(forms.Form):
date_from = forms.DateField(
widget=AirDatePickerInput(clear_button=False), label="", required=False
)
date_to = forms.DateField(
widget=AirDatePickerInput(clear_button=False), label="", required=False
)
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(
Row(
Column("date_from", css_class="form-group col-md-6"),
Column("date_to", css_class="form-group col-md-6"),
css_class="mb-0",
),
)

View File

@@ -4,5 +4,14 @@ from . import views
urlpatterns = [
path("insights/", views.index, name="insights_index"),
path("insights/sankey/", views.sankey, name="sankey"),
path(
"insights/sankey/account/",
views.sankey_by_account,
name="insights_sankey_by_account",
),
path(
"insights/sankey/currency/",
views.sankey_by_currency,
name="insights_sankey_by_currency",
),
]

View File

@@ -1,4 +1,3 @@
from django.db.models import Sum
from django.utils.translation import gettext_lazy as _
from decimal import Decimal
from typing import Dict, List, TypedDict
@@ -17,41 +16,64 @@ class SankeyFlow(TypedDict):
percentage: float
def generate_sankey_data(transactions_queryset):
def generate_sankey_data_by_account(transactions_queryset):
"""
Generates Sankey diagram data from transaction queryset.
Uses a 1-5 scale for flows based on percentages.
Generates Sankey diagram data from transaction queryset using account as intermediary.
"""
nodes: Dict[str, SankeyNode] = {}
nodes: Dict[str, Dict] = {}
flows: List[SankeyFlow] = []
# Aggregate transactions
income_data = {} # {(category, currency, account) -> amount}
expense_data = {} # {(category, currency, account) -> amount}
total_amount = Decimal("0")
total_income_by_currency = {} # {currency -> amount}
total_expense_by_currency = {} # {currency -> amount}
total_volume_by_currency = {} # {currency -> amount}
for transaction in transactions_queryset:
currency = transaction.account.currency
account = transaction.account
category = transaction.category or _("Uncategorized")
key = (category, currency, account)
amount = transaction.amount
if transaction.type == "IN":
income_data[key] = income_data.get(key, Decimal("0")) + transaction.amount
income_data[key] = income_data.get(key, Decimal("0")) + amount
total_income_by_currency[currency] = (
total_income_by_currency.get(currency, Decimal("0")) + amount
)
else:
expense_data[key] = expense_data.get(key, Decimal("0")) + transaction.amount
expense_data[key] = expense_data.get(key, Decimal("0")) + amount
total_expense_by_currency[currency] = (
total_expense_by_currency.get(currency, Decimal("0")) + amount
)
total_amount += transaction.amount
total_volume_by_currency[currency] = (
total_volume_by_currency.get(currency, Decimal("0")) + amount
)
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}
def add_flow(
from_node_id: str, to_node_id: str, amount: Decimal, currency, is_income: bool
) -> None:
"""
Add flow with percentage based on total transaction volume for the specific currency.
"""
total_volume = total_volume_by_currency.get(currency, Decimal("0"))
percentage = (amount / total_volume) * 100 if total_volume else 0
scaled_flow = percentage / 100
# Function to add flow
def add_flow(from_node, to_node, amount, currency):
percentage = (amount / total_amount) * 100 if total_amount else 0
scaled_flow = 1 + min(percentage / 20, 4) # Scale 1-5, capping at 100%
flows.append(
{
"from_node": from_node,
"to_node": to_node,
"from_node": from_node_id,
"to_node": to_node_id,
"flow": float(scaled_flow),
"currency": {
"code": currency.code,
@@ -63,40 +85,164 @@ def generate_sankey_data(transactions_queryset):
"percentage": float(percentage),
}
)
nodes[from_node] = {"name": from_node}
nodes[to_node] = {"name": to_node}
# Process income
for (category, currency, account), amount in income_data.items():
category_name = f"{category} ({currency.code})"
account_name = f"{account.name} ({currency.code})"
add_flow(category_name, account_name, amount, currency)
category_node_id = get_node_id("income", category, account.id)
account_node_id = get_node_id("account", account.name, account.id)
add_node(category_node_id, str(category))
add_node(account_node_id, account.name)
add_flow(category_node_id, account_node_id, amount, currency, is_income=True)
# Process expenses
for (category, currency, account), amount in expense_data.items():
category_name = f"{category} ({currency.code})"
account_name = f"{account.name} ({currency.code})"
add_flow(account_name, category_name, amount, currency)
category_node_id = get_node_id("expense", category, account.id)
account_node_id = get_node_id("account", account.name, account.id)
add_node(category_node_id, str(category))
add_node(account_node_id, account.name)
add_flow(account_node_id, category_node_id, amount, currency, is_income=False)
# Calculate and add savings flows
savings_data = {} # {(account, currency) -> amount}
for (category, currency, account), amount in income_data.items():
key = (account, currency)
savings_data[key] = savings_data.get(key, Decimal("0")) + amount
for (category, currency, account), amount in expense_data.items():
key = (account, currency)
savings_data[key] = savings_data.get(key, Decimal("0")) - amount
for (account, currency), amount in savings_data.items():
if amount > 0:
account_name = f"{account.name} ({currency.code})"
savings_name = f"{_('Savings')} ({currency.code})"
add_flow(account_name, savings_name, amount, currency)
account_node_id = get_node_id("account", account.name, account.id)
savings_node_id = get_node_id("savings", _("Saved"), account.id)
add_node(savings_node_id, str(_("Saved")))
add_flow(account_node_id, savings_node_id, amount, currency, is_income=True)
# Calculate total across all currencies (for reference only)
total_amount = sum(float(amount) for amount in total_income_by_currency.values())
return {
"nodes": list(nodes.values()),
"flows": flows,
"total_amount": float(total_amount),
"total_amount": total_amount,
"total_by_currency": {
curr.code: float(amount)
for curr, amount in total_income_by_currency.items()
},
}
def generate_sankey_data_by_currency(transactions_queryset):
"""
Generates Sankey diagram data from transaction queryset, using currency as intermediary.
"""
nodes: Dict[str, Dict] = {}
flows: List[SankeyFlow] = []
# Aggregate transactions
income_data = {} # {(category, currency) -> amount}
expense_data = {} # {(category, currency) -> amount}
total_income_by_currency = {} # {currency -> amount}
total_expense_by_currency = {} # {currency -> amount}
total_volume_by_currency = {} # {currency -> amount}
for transaction in transactions_queryset:
currency = transaction.account.currency
category = transaction.category or _("Uncategorized")
key = (category, currency)
amount = transaction.amount
if transaction.type == "IN":
income_data[key] = income_data.get(key, Decimal("0")) + amount
total_income_by_currency[currency] = (
total_income_by_currency.get(currency, Decimal("0")) + amount
)
else:
expense_data[key] = expense_data.get(key, Decimal("0")) + amount
total_expense_by_currency[currency] = (
total_expense_by_currency.get(currency, Decimal("0")) + amount
)
total_volume_by_currency[currency] = (
total_volume_by_currency.get(currency, Decimal("0")) + amount
)
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}
def add_flow(
from_node_id: str, to_node_id: str, amount: Decimal, currency, is_income: bool
) -> None:
"""
Add flow with percentage based on total transaction volume for the specific currency.
"""
total_volume = total_volume_by_currency.get(currency, Decimal("0"))
percentage = (amount / total_volume) * 100 if total_volume else 0
scaled_flow = percentage / 100
flows.append(
{
"from_node": from_node_id,
"to_node": to_node_id,
"flow": float(scaled_flow),
"currency": {
"code": currency.code,
"name": currency.name,
"prefix": currency.prefix,
"suffix": currency.suffix,
"decimal_places": currency.decimal_places,
},
"original_amount": float(amount),
"percentage": float(percentage),
}
)
# Process income
for (category, currency), amount in income_data.items():
category_node_id = get_node_id("income", category, currency.id)
currency_node_id = get_node_id("currency", currency.name, currency.id)
add_node(category_node_id, str(category))
add_node(currency_node_id, currency.name)
add_flow(category_node_id, currency_node_id, amount, currency, is_income=True)
# Process expenses
for (category, currency), amount in expense_data.items():
category_node_id = get_node_id("expense", category, currency.id)
currency_node_id = get_node_id("currency", currency.name, currency.id)
add_node(category_node_id, str(category))
add_node(currency_node_id, currency.name)
add_flow(currency_node_id, category_node_id, amount, currency, is_income=False)
# Calculate and add savings flows
savings_data = {} # {currency -> amount}
for (category, currency), amount in income_data.items():
savings_data[currency] = savings_data.get(currency, Decimal("0")) + amount
for (category, currency), amount in expense_data.items():
savings_data[currency] = savings_data.get(currency, Decimal("0")) - amount
for currency, amount in savings_data.items():
if amount > 0:
currency_node_id = get_node_id("currency", currency.name, currency.id)
savings_node_id = get_node_id("savings", _("Saved"), currency.id)
add_node(savings_node_id, str(_("Saved")))
add_flow(
currency_node_id, savings_node_id, amount, currency, is_income=True
)
# Calculate total across all currencies (for reference only)
total_amount = sum(float(amount) for amount in total_income_by_currency.values())
return {
"nodes": list(nodes.values()),
"flows": flows,
"total_amount": total_amount,
"total_by_currency": {
curr.name: float(amount)
for curr, amount in total_income_by_currency.items()
},
}

View File

@@ -0,0 +1,96 @@
from django.db.models import Q
from django.utils import timezone
from dateutil.relativedelta import relativedelta
from apps.transactions.models import Transaction
from apps.insights.forms import (
SingleMonthForm,
SingleYearForm,
MonthRangeForm,
YearRangeForm,
DateRangeForm,
)
def get_transactions(request, include_unpaid=True, include_silent=False):
transactions = Transaction.objects.all()
filter_type = request.GET.get("type", None)
if filter_type is not None:
if filter_type == "month":
form = SingleMonthForm(request.GET)
if form.is_valid():
month = form.cleaned_data["month"].replace(day=1)
else:
month = timezone.localdate(timezone.now()).replace(day=1)
transactions = transactions.filter(
reference_date__month=month.month, reference_date__year=month.year
)
elif filter_type == "year":
form = SingleYearForm(request.GET)
if form.is_valid():
year = form.cleaned_data["year"].replace(day=1, month=1)
else:
year = timezone.localdate(timezone.now()).replace(day=1, month=1)
transactions = transactions.filter(reference_date__year=year.year)
elif filter_type == "month-range":
form = MonthRangeForm(request.GET)
if form.is_valid():
month_from = form.cleaned_data["month_from"].replace(day=1)
month_to = form.cleaned_data["month_to"].replace(day=1)
else:
month_from = timezone.localdate(timezone.now()).replace(day=1)
month_to = (
timezone.localdate(timezone.now()) + relativedelta(months=1)
).replace(day=1)
transactions = transactions.filter(
reference_date__gte=month_from,
reference_date__lte=month_to,
)
elif filter_type == "year-range":
form = YearRangeForm(request.GET)
if form.is_valid():
year_from = form.cleaned_data["year_from"].replace(day=1, month=1)
year_to = form.cleaned_data["year_to"].replace(day=31, month=12)
else:
year_from = timezone.localdate(timezone.now()).replace(day=1, month=1)
year_to = (
timezone.localdate(timezone.now()) + relativedelta(years=1)
).replace(day=31, month=12)
transactions = transactions.filter(
reference_date__gte=year_from,
reference_date__lte=year_to,
)
elif filter_type == "date-range":
form = DateRangeForm(request.GET)
if form.is_valid():
date_from = form.cleaned_data["date_from"]
date_to = form.cleaned_data["date_to"]
else:
date_from = timezone.localdate(timezone.now())
date_to = timezone.localdate(timezone.now()) + relativedelta(months=1)
transactions = transactions.filter(
date__gte=date_from,
date__lte=date_to,
)
else: # Default to current month
month = timezone.localdate(timezone.now())
transactions = transactions.filter(
reference_date__month=month.month, reference_date__year=month.year
)
if not include_unpaid:
transactions = transactions.filter(is_paid=True)
if not include_silent:
transactions = transactions.exclude(Q(category__mute=True) & ~Q(category=None))
return transactions

View File

@@ -1,21 +1,94 @@
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
from apps.insights.utils.sankey import (
generate_sankey_data_by_account,
generate_sankey_data_by_currency,
)
from apps.insights.forms import (
SingleMonthForm,
SingleYearForm,
MonthRangeForm,
YearRangeForm,
DateRangeForm,
)
from apps.common.decorators.htmx import only_htmx
from apps.insights.utils.transactions import get_transactions
@login_required
@require_http_methods(["GET"])
def index(request):
return render(request, "insights/pages/index.html")
def sankey(request):
# Get filtered transactions
transactions = Transaction.objects.filter(date__year=2025)
# Generate Sankey data
sankey_data = generate_sankey_data(transactions)
print(sankey_data)
date = timezone.localdate(timezone.now())
month_form = SingleMonthForm(initial={"month": date.replace(day=1)})
year_form = SingleYearForm(initial={"year": date.replace(day=1)})
month_range_form = MonthRangeForm(
initial={
"month_from": date.replace(day=1),
"month_to": date.replace(day=1) + relativedelta(months=1),
}
)
year_range_form = YearRangeForm(
initial={
"year_from": date.replace(day=1, month=1),
"year_to": date.replace(day=1, month=1) + relativedelta(years=1),
}
)
date_range_form = DateRangeForm(
initial={
"date_from": date,
"date_to": date + relativedelta(months=1),
}
)
return render(
request, "insights/fragments/sankey.html", {"sankey_data": sankey_data}
request,
"insights/pages/index.html",
context={
"month_form": month_form,
"year_form": year_form,
"month_range_form": month_range_form,
"year_range_form": year_range_form,
"date_range_form": date_range_form,
},
)
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])
def sankey_by_account(request):
# Get filtered transactions
transactions = get_transactions(request)
# Generate Sankey data
sankey_data = generate_sankey_data_by_account(transactions)
return render(
request,
"insights/fragments/sankey.html",
{"sankey_data": sankey_data, "type": "account"},
)
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])
def sankey_by_currency(request):
# Get filtered transactions
transactions = get_transactions(request)
# Generate Sankey data
sankey_data = generate_sankey_data_by_currency(transactions)
return render(
request,
"insights/fragments/sankey.html",
{"sankey_data": sankey_data, "type": "currency"},
)

View File

@@ -555,6 +555,4 @@ def get_recent_transactions(request, filter_type=None):
for t in queryset:
data.append({"text": str(t), "value": str(t.id)})
print(data)
return JsonResponse(data, safe=False)

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-15 00:38-0300\n"
"POT-Creation-Date: 2025-02-16 00:03-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"
@@ -85,7 +85,7 @@ msgstr ""
#: apps/transactions/forms.py:315 apps/transactions/forms.py:471
#: apps/transactions/forms.py:716 apps/transactions/models.py:209
#: apps/transactions/models.py:380 apps/transactions/models.py:562
#: templates/includes/navbar.html:105 templates/tags/fragments/list.html:5
#: templates/includes/navbar.html:108 templates/tags/fragments/list.html:5
#: templates/tags/pages/index.html:4
msgid "Tags"
msgstr ""
@@ -114,7 +114,7 @@ msgstr ""
#: apps/accounts/models.py:13 templates/account_groups/fragments/list.html:5
#: templates/account_groups/pages/index.html:4
#: templates/includes/navbar.html:115
#: templates/includes/navbar.html:118
msgid "Account Groups"
msgstr ""
@@ -162,8 +162,8 @@ msgstr ""
#: apps/accounts/models.py:60 apps/transactions/filters.py:53
#: templates/accounts/fragments/list.html:5
#: templates/accounts/pages/index.html:4 templates/includes/navbar.html:111
#: templates/includes/navbar.html:113
#: templates/accounts/pages/index.html:4 templates/includes/navbar.html:114
#: templates/includes/navbar.html:116
#: templates/monthly_overview/pages/overview.html:94
#: templates/transactions/fragments/summary.html:12
#: templates/transactions/pages/transactions.html:72
@@ -332,6 +332,7 @@ msgid "Cache cleared successfully"
msgstr ""
#: apps/common/widgets/datepicker.py:47 apps/common/widgets/datepicker.py:186
#: apps/common/widgets/datepicker.py:244
msgid "Today"
msgstr ""
@@ -386,8 +387,8 @@ msgstr ""
#: apps/currencies/models.py:40 apps/transactions/filters.py:60
#: templates/currencies/fragments/list.html:5
#: templates/currencies/pages/index.html:4 templates/includes/navbar.html:119
#: templates/includes/navbar.html:121
#: templates/currencies/pages/index.html:4 templates/includes/navbar.html:122
#: templates/includes/navbar.html:124
#: templates/monthly_overview/pages/overview.html:81
#: templates/transactions/fragments/summary.html:8
#: templates/transactions/pages/transactions.html:59
@@ -416,7 +417,7 @@ msgstr ""
#: apps/currencies/models.py:74 templates/exchange_rates/fragments/list.html:6
#: templates/exchange_rates/pages/index.html:4
#: templates/includes/navbar.html:123
#: templates/includes/navbar.html:126
msgid "Exchange Rates"
msgstr ""
@@ -676,7 +677,7 @@ msgstr ""
#: apps/import_app/forms.py:61
#: templates/import_app/fragments/profiles/list.html:62
#: templates/includes/navbar.html:131
#: templates/includes/navbar.html:134
msgid "Import"
msgstr ""
@@ -744,6 +745,15 @@ msgstr ""
msgid "Run deleted successfully"
msgstr ""
#: apps/insights/utils/sankey.py:37 apps/insights/utils/sankey.py:154
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
msgid "Saved"
msgstr ""
#: apps/rules/forms.py:20
msgid "Run on creation"
msgstr ""
@@ -760,7 +770,7 @@ msgstr ""
msgid "Set field"
msgstr ""
#: apps/rules/forms.py:65
#: apps/rules/forms.py:65 templates/insights/fragments/sankey.html:84
msgid "To"
msgstr ""
@@ -801,7 +811,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
#: apps/transactions/models.py:551 templates/insights/fragments/sankey.html:85
msgid "Amount"
msgstr ""
@@ -828,7 +838,7 @@ msgstr ""
#: apps/transactions/forms.py:731 apps/transactions/models.py:161
#: apps/transactions/models.py:214 apps/transactions/models.py:383
#: apps/transactions/models.py:565 templates/entities/fragments/list.html:5
#: templates/entities/pages/index.html:4 templates/includes/navbar.html:107
#: templates/entities/pages/index.html:4 templates/includes/navbar.html:110
msgid "Entities"
msgstr ""
@@ -982,7 +992,7 @@ msgid "Transaction Type"
msgstr ""
#: apps/transactions/filters.py:67 templates/categories/fragments/list.html:5
#: templates/categories/pages/index.html:4 templates/includes/navbar.html:103
#: templates/categories/pages/index.html:4 templates/includes/navbar.html:106
msgid "Categories"
msgstr ""
@@ -1119,8 +1129,8 @@ msgstr ""
msgid "Transaction"
msgstr ""
#: apps/transactions/models.py:256 templates/includes/navbar.html:54
#: templates/includes/navbar.html:101
#: apps/transactions/models.py:256 templates/includes/navbar.html:57
#: templates/includes/navbar.html:104
#: templates/recurring_transactions/fragments/list_transactions.html:5
#: templates/recurring_transactions/fragments/table.html:37
#: templates/transactions/pages/transactions.html:5
@@ -1184,7 +1194,7 @@ msgstr ""
msgid "Installment Amount"
msgstr ""
#: apps/transactions/models.py:391 templates/includes/navbar.html:69
#: apps/transactions/models.py:391 templates/includes/navbar.html:72
#: templates/installment_plans/fragments/list.html:5
#: templates/installment_plans/pages/index.html:4
msgid "Installment Plans"
@@ -1227,7 +1237,7 @@ msgstr ""
msgid "Last Generated Reference Date"
msgstr ""
#: apps/transactions/models.py:592 templates/includes/navbar.html:71
#: apps/transactions/models.py:592 templates/includes/navbar.html:74
#: templates/recurring_transactions/fragments/list.html:5
#: templates/recurring_transactions/pages/index.html:4
msgid "Recurring Transactions"
@@ -2070,7 +2080,7 @@ msgid "Edit exchange rate"
msgstr ""
#: templates/exchange_rates/fragments/list.html:25
#: templates/includes/navbar.html:58
#: templates/includes/navbar.html:61
#: templates/installment_plans/fragments/list.html:21
#: templates/yearly_overview/pages/overview_by_account.html:92
#: templates/yearly_overview/pages/overview_by_currency.html:94
@@ -2100,7 +2110,7 @@ msgstr ""
#: templates/exchange_rates_services/fragments/list.html:6
#: templates/exchange_rates_services/pages/index.html:4
#: templates/includes/navbar.html:133
#: templates/includes/navbar.html:136
msgid "Automatic Exchange Rates"
msgstr ""
@@ -2237,52 +2247,56 @@ msgstr ""
msgid "Current"
msgstr ""
#: templates/includes/navbar.html:63
#: templates/includes/navbar.html:50
msgid "Insights"
msgstr ""
#: templates/includes/navbar.html:66
msgid "Trash Can"
msgstr ""
#: templates/includes/navbar.html:79
#: templates/includes/navbar.html:82
msgid "Tools"
msgstr ""
#: templates/includes/navbar.html:83
#: templates/includes/navbar.html:86
msgid "Dollar Cost Average Tracker"
msgstr ""
#: templates/includes/navbar.html:86
#: templates/includes/navbar.html:89
#: templates/mini_tools/unit_price_calculator.html:5
#: templates/mini_tools/unit_price_calculator.html:10
msgid "Unit Price Calculator"
msgstr ""
#: templates/includes/navbar.html:89
#: templates/includes/navbar.html:92
#: templates/mini_tools/currency_converter/currency_converter.html:8
#: templates/mini_tools/currency_converter/currency_converter.html:15
msgid "Currency Converter"
msgstr ""
#: templates/includes/navbar.html:98
#: templates/includes/navbar.html:101
msgid "Management"
msgstr ""
#: templates/includes/navbar.html:127
#: templates/includes/navbar.html:130
msgid "Automation"
msgstr ""
#: templates/includes/navbar.html:129 templates/rules/fragments/list.html:5
#: templates/includes/navbar.html:132 templates/rules/fragments/list.html:5
#: templates/rules/pages/index.html:4
msgid "Rules"
msgstr ""
#: templates/includes/navbar.html:143
#: templates/includes/navbar.html:146
msgid "Only use this if you know what you're doing"
msgstr ""
#: templates/includes/navbar.html:144
#: templates/includes/navbar.html:147
msgid "Django Admin"
msgstr ""
#: templates/includes/navbar.html:153
#: templates/includes/navbar.html:156
msgid "Calculator"
msgstr ""
@@ -2314,6 +2328,44 @@ msgstr ""
msgid "Confirm"
msgstr ""
#: templates/insights/fragments/sankey.html:83
msgid "From"
msgstr ""
#: templates/insights/fragments/sankey.html:86
msgid "Percentage"
msgstr ""
#: templates/insights/pages/index.html:33
msgid "Month"
msgstr ""
#: templates/insights/pages/index.html:36
#: templates/yearly_overview/pages/overview_by_account.html:61
#: templates/yearly_overview/pages/overview_by_currency.html:63
msgid "Year"
msgstr ""
#: templates/insights/pages/index.html:39
msgid "Month Range"
msgstr ""
#: templates/insights/pages/index.html:42
msgid "Year Range"
msgstr ""
#: templates/insights/pages/index.html:45
msgid "Date Range"
msgstr ""
#: templates/insights/pages/index.html:74
msgid "Account Flow"
msgstr ""
#: templates/insights/pages/index.html:81
msgid "Currency Flow"
msgstr ""
#: templates/installment_plans/fragments/add.html:5
msgid "Add installment plan"
msgstr ""
@@ -2678,8 +2730,3 @@ msgstr ""
#: templates/yearly_overview/pages/overview_by_currency.html:9
msgid "Yearly Overview"
msgstr ""
#: templates/yearly_overview/pages/overview_by_account.html:61
#: templates/yearly_overview/pages/overview_by_currency.html:63
msgid "Year"
msgstr ""

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-15 00:42-0300\n"
"POT-Creation-Date: 2025-02-16 00:03-0300\n"
"PO-Revision-Date: 2025-02-12 06:58+0100\n"
"Last-Translator: Dimitri Decrock <dimitri@fam-decrock.eu>\n"
"Language-Team: \n"
@@ -86,7 +86,7 @@ msgstr "Categorie"
#: apps/transactions/forms.py:315 apps/transactions/forms.py:471
#: apps/transactions/forms.py:716 apps/transactions/models.py:209
#: apps/transactions/models.py:380 apps/transactions/models.py:562
#: templates/includes/navbar.html:105 templates/tags/fragments/list.html:5
#: templates/includes/navbar.html:108 templates/tags/fragments/list.html:5
#: templates/tags/pages/index.html:4
msgid "Tags"
msgstr "Labels"
@@ -115,7 +115,7 @@ msgstr "Accountgroep"
#: apps/accounts/models.py:13 templates/account_groups/fragments/list.html:5
#: templates/account_groups/pages/index.html:4
#: templates/includes/navbar.html:115
#: templates/includes/navbar.html:118
msgid "Account Groups"
msgstr "Accountgroepen"
@@ -167,8 +167,8 @@ msgstr "Rekening"
#: apps/accounts/models.py:60 apps/transactions/filters.py:53
#: templates/accounts/fragments/list.html:5
#: templates/accounts/pages/index.html:4 templates/includes/navbar.html:111
#: templates/includes/navbar.html:113
#: templates/accounts/pages/index.html:4 templates/includes/navbar.html:114
#: templates/includes/navbar.html:116
#: templates/monthly_overview/pages/overview.html:94
#: templates/transactions/fragments/summary.html:12
#: templates/transactions/pages/transactions.html:72
@@ -338,6 +338,7 @@ msgid "Cache cleared successfully"
msgstr "Categorie succesvol bijgewerkt"
#: apps/common/widgets/datepicker.py:47 apps/common/widgets/datepicker.py:186
#: apps/common/widgets/datepicker.py:244
msgid "Today"
msgstr "Vandaag"
@@ -392,8 +393,8 @@ msgstr "Cijfers na de komma"
#: apps/currencies/models.py:40 apps/transactions/filters.py:60
#: templates/currencies/fragments/list.html:5
#: templates/currencies/pages/index.html:4 templates/includes/navbar.html:119
#: templates/includes/navbar.html:121
#: templates/currencies/pages/index.html:4 templates/includes/navbar.html:122
#: templates/includes/navbar.html:124
#: templates/monthly_overview/pages/overview.html:81
#: templates/transactions/fragments/summary.html:8
#: templates/transactions/pages/transactions.html:59
@@ -422,7 +423,7 @@ msgstr "Datum en Tijd"
#: apps/currencies/models.py:74 templates/exchange_rates/fragments/list.html:6
#: templates/exchange_rates/pages/index.html:4
#: templates/includes/navbar.html:123
#: templates/includes/navbar.html:126
msgid "Exchange Rates"
msgstr "Wisselkoersen"
@@ -696,7 +697,7 @@ msgstr "Selecteer een bestand"
#: apps/import_app/forms.py:61
#: templates/import_app/fragments/profiles/list.html:62
#: templates/includes/navbar.html:131
#: templates/includes/navbar.html:134
msgid "Import"
msgstr "Importeer"
@@ -764,6 +765,19 @@ 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
#, 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
#, fuzzy
#| msgid "Save"
msgid "Saved"
msgstr "Opslaan"
#: apps/rules/forms.py:20
msgid "Run on creation"
msgstr "Uitvoeren na het aanmaken"
@@ -780,7 +794,7 @@ msgstr "Als..."
msgid "Set field"
msgstr "Veld instellen"
#: apps/rules/forms.py:65
#: apps/rules/forms.py:65 templates/insights/fragments/sankey.html:84
msgid "To"
msgstr "Naar"
@@ -821,7 +835,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
#: apps/transactions/models.py:551 templates/insights/fragments/sankey.html:85
msgid "Amount"
msgstr "Bedrag"
@@ -848,7 +862,7 @@ msgstr "Interne ID"
#: apps/transactions/forms.py:731 apps/transactions/models.py:161
#: apps/transactions/models.py:214 apps/transactions/models.py:383
#: apps/transactions/models.py:565 templates/entities/fragments/list.html:5
#: templates/entities/pages/index.html:4 templates/includes/navbar.html:107
#: templates/entities/pages/index.html:4 templates/includes/navbar.html:110
msgid "Entities"
msgstr "Bedrijven"
@@ -1005,7 +1019,7 @@ msgid "Transaction Type"
msgstr "Soort transactie"
#: apps/transactions/filters.py:67 templates/categories/fragments/list.html:5
#: templates/categories/pages/index.html:4 templates/includes/navbar.html:103
#: templates/categories/pages/index.html:4 templates/includes/navbar.html:106
msgid "Categories"
msgstr "Categorieën"
@@ -1148,8 +1162,8 @@ msgstr "Verwijderd Op"
msgid "Transaction"
msgstr "Verrichting"
#: apps/transactions/models.py:256 templates/includes/navbar.html:54
#: templates/includes/navbar.html:101
#: apps/transactions/models.py:256 templates/includes/navbar.html:57
#: templates/includes/navbar.html:104
#: templates/recurring_transactions/fragments/list_transactions.html:5
#: templates/recurring_transactions/fragments/table.html:37
#: templates/transactions/pages/transactions.html:5
@@ -1217,7 +1231,7 @@ msgstr "Terugkeerpatroon"
msgid "Installment Amount"
msgstr "Termijnbedrag"
#: apps/transactions/models.py:391 templates/includes/navbar.html:69
#: apps/transactions/models.py:391 templates/includes/navbar.html:72
#: templates/installment_plans/fragments/list.html:5
#: templates/installment_plans/pages/index.html:4
msgid "Installment Plans"
@@ -1260,7 +1274,7 @@ msgstr "Laatste Gegenereerde Datum"
msgid "Last Generated Reference Date"
msgstr "Laatste Gegenereerde Referentiedatum"
#: apps/transactions/models.py:592 templates/includes/navbar.html:71
#: apps/transactions/models.py:592 templates/includes/navbar.html:74
#: templates/recurring_transactions/fragments/list.html:5
#: templates/recurring_transactions/pages/index.html:4
msgid "Recurring Transactions"
@@ -2103,7 +2117,7 @@ msgid "Edit exchange rate"
msgstr "Wisselkoers bewerken"
#: templates/exchange_rates/fragments/list.html:25
#: templates/includes/navbar.html:58
#: templates/includes/navbar.html:61
#: templates/installment_plans/fragments/list.html:21
#: templates/yearly_overview/pages/overview_by_account.html:92
#: templates/yearly_overview/pages/overview_by_currency.html:94
@@ -2133,7 +2147,7 @@ msgstr "Paginanavigatie"
#: templates/exchange_rates_services/fragments/list.html:6
#: templates/exchange_rates_services/pages/index.html:4
#: templates/includes/navbar.html:133
#: templates/includes/navbar.html:136
msgid "Automatic Exchange Rates"
msgstr "Automatische Wisselkoersen"
@@ -2271,52 +2285,56 @@ msgstr "Netto Waarde"
msgid "Current"
msgstr "Huidige"
#: templates/includes/navbar.html:63
#: templates/includes/navbar.html:50
msgid "Insights"
msgstr ""
#: templates/includes/navbar.html:66
msgid "Trash Can"
msgstr "Prullenbak"
#: templates/includes/navbar.html:79
#: templates/includes/navbar.html:82
msgid "Tools"
msgstr "Hulpmiddelen"
#: templates/includes/navbar.html:83
#: templates/includes/navbar.html:86
msgid "Dollar Cost Average Tracker"
msgstr "Dollar Kostgemiddelde Tracker"
#: templates/includes/navbar.html:86
#: templates/includes/navbar.html:89
#: templates/mini_tools/unit_price_calculator.html:5
#: templates/mini_tools/unit_price_calculator.html:10
msgid "Unit Price Calculator"
msgstr "Eenheidsprijs berekenen"
#: templates/includes/navbar.html:89
#: templates/includes/navbar.html:92
#: templates/mini_tools/currency_converter/currency_converter.html:8
#: templates/mini_tools/currency_converter/currency_converter.html:15
msgid "Currency Converter"
msgstr "Valuta omrekenen"
#: templates/includes/navbar.html:98
#: templates/includes/navbar.html:101
msgid "Management"
msgstr "Beheer"
#: templates/includes/navbar.html:127
#: templates/includes/navbar.html:130
msgid "Automation"
msgstr "Automatisatie"
#: templates/includes/navbar.html:129 templates/rules/fragments/list.html:5
#: templates/includes/navbar.html:132 templates/rules/fragments/list.html:5
#: templates/rules/pages/index.html:4
msgid "Rules"
msgstr "Regels"
#: templates/includes/navbar.html:143
#: templates/includes/navbar.html:146
msgid "Only use this if you know what you're doing"
msgstr "Gebruik dit alleen als je weet wat je doet"
#: templates/includes/navbar.html:144
#: templates/includes/navbar.html:147
msgid "Django Admin"
msgstr "Django Beheerder"
#: templates/includes/navbar.html:153
#: templates/includes/navbar.html:156
msgid "Calculator"
msgstr "Rekenmachine"
@@ -2350,6 +2368,54 @@ msgstr "Annuleer"
msgid "Confirm"
msgstr "Bevestig"
#: templates/insights/fragments/sankey.html:83
msgid "From"
msgstr ""
#: templates/insights/fragments/sankey.html:86
msgid "Percentage"
msgstr ""
#: templates/insights/pages/index.html:33
#, fuzzy
#| msgid "Monthly"
msgid "Month"
msgstr "Maandelijks"
#: templates/insights/pages/index.html:36
#: templates/yearly_overview/pages/overview_by_account.html:61
#: templates/yearly_overview/pages/overview_by_currency.html:63
msgid "Year"
msgstr "Jaar"
#: templates/insights/pages/index.html:39
#, fuzzy
#| msgid "Unchanged"
msgid "Month Range"
msgstr "Ongewijzigd"
#: templates/insights/pages/index.html:42
msgid "Year Range"
msgstr ""
#: templates/insights/pages/index.html:45
#, fuzzy
#| msgid "Date and Time"
msgid "Date Range"
msgstr "Datum en Tijd"
#: templates/insights/pages/index.html:74
#, fuzzy
#| msgid "Account"
msgid "Account Flow"
msgstr "Rekening"
#: templates/insights/pages/index.html:81
#, fuzzy
#| msgid "Currency Code"
msgid "Currency Flow"
msgstr "Munteenheids Code"
#: templates/installment_plans/fragments/add.html:5
msgid "Add installment plan"
msgstr "Afbetalingsplan toevoegen"
@@ -2723,11 +2789,6 @@ msgstr "Bedragen tonen"
msgid "Yearly Overview"
msgstr "Jaaroverzicht"
#: templates/yearly_overview/pages/overview_by_account.html:61
#: templates/yearly_overview/pages/overview_by_currency.html:63
msgid "Year"
msgstr "Jaar"
#, fuzzy
#~| msgid "Start Date"
#~ msgid "Search Date"

View File

@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-15 00:38-0300\n"
"PO-Revision-Date: 2025-02-15 00:39-0300\n"
"POT-Creation-Date: 2025-02-16 00:03-0300\n"
"PO-Revision-Date: 2025-02-16 00:04-0300\n"
"Last-Translator: Herculino Trotta\n"
"Language-Team: \n"
"Language: pt_BR\n"
@@ -86,7 +86,7 @@ msgstr "Categoria"
#: apps/transactions/forms.py:315 apps/transactions/forms.py:471
#: apps/transactions/forms.py:716 apps/transactions/models.py:209
#: apps/transactions/models.py:380 apps/transactions/models.py:562
#: templates/includes/navbar.html:105 templates/tags/fragments/list.html:5
#: templates/includes/navbar.html:108 templates/tags/fragments/list.html:5
#: templates/tags/pages/index.html:4
msgid "Tags"
msgstr "Tags"
@@ -115,7 +115,7 @@ msgstr "Grupo da Conta"
#: apps/accounts/models.py:13 templates/account_groups/fragments/list.html:5
#: templates/account_groups/pages/index.html:4
#: templates/includes/navbar.html:115
#: templates/includes/navbar.html:118
msgid "Account Groups"
msgstr "Grupos da Conta"
@@ -166,8 +166,8 @@ msgstr "Conta"
#: apps/accounts/models.py:60 apps/transactions/filters.py:53
#: templates/accounts/fragments/list.html:5
#: templates/accounts/pages/index.html:4 templates/includes/navbar.html:111
#: templates/includes/navbar.html:113
#: templates/accounts/pages/index.html:4 templates/includes/navbar.html:114
#: templates/includes/navbar.html:116
#: templates/monthly_overview/pages/overview.html:94
#: templates/transactions/fragments/summary.html:12
#: templates/transactions/pages/transactions.html:72
@@ -336,6 +336,7 @@ msgid "Cache cleared successfully"
msgstr "Cache limpo com sucesso"
#: apps/common/widgets/datepicker.py:47 apps/common/widgets/datepicker.py:186
#: apps/common/widgets/datepicker.py:244
msgid "Today"
msgstr "Hoje"
@@ -390,8 +391,8 @@ msgstr "Casas Decimais"
#: apps/currencies/models.py:40 apps/transactions/filters.py:60
#: templates/currencies/fragments/list.html:5
#: templates/currencies/pages/index.html:4 templates/includes/navbar.html:119
#: templates/includes/navbar.html:121
#: templates/currencies/pages/index.html:4 templates/includes/navbar.html:122
#: templates/includes/navbar.html:124
#: templates/monthly_overview/pages/overview.html:81
#: templates/transactions/fragments/summary.html:8
#: templates/transactions/pages/transactions.html:59
@@ -420,7 +421,7 @@ msgstr "Data e Tempo"
#: apps/currencies/models.py:74 templates/exchange_rates/fragments/list.html:6
#: templates/exchange_rates/pages/index.html:4
#: templates/includes/navbar.html:123
#: templates/includes/navbar.html:126
msgid "Exchange Rates"
msgstr "Taxas de Câmbio"
@@ -690,7 +691,7 @@ msgstr "Selecione um arquivo"
#: apps/import_app/forms.py:61
#: templates/import_app/fragments/profiles/list.html:62
#: templates/includes/navbar.html:131
#: templates/includes/navbar.html:134
msgid "Import"
msgstr "Importar"
@@ -758,6 +759,15 @@ 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
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
msgid "Saved"
msgstr "Salvo"
#: apps/rules/forms.py:20
msgid "Run on creation"
msgstr "Rodar na criação"
@@ -774,7 +784,7 @@ msgstr "Se..."
msgid "Set field"
msgstr "Definir campo"
#: apps/rules/forms.py:65
#: apps/rules/forms.py:65 templates/insights/fragments/sankey.html:84
msgid "To"
msgstr "Para"
@@ -815,7 +825,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
#: apps/transactions/models.py:551 templates/insights/fragments/sankey.html:85
msgid "Amount"
msgstr "Quantia"
@@ -842,7 +852,7 @@ msgstr "ID Interna"
#: apps/transactions/forms.py:731 apps/transactions/models.py:161
#: apps/transactions/models.py:214 apps/transactions/models.py:383
#: apps/transactions/models.py:565 templates/entities/fragments/list.html:5
#: templates/entities/pages/index.html:4 templates/includes/navbar.html:107
#: templates/entities/pages/index.html:4 templates/includes/navbar.html:110
msgid "Entities"
msgstr "Entidades"
@@ -998,7 +1008,7 @@ msgid "Transaction Type"
msgstr "Tipo de Transação"
#: apps/transactions/filters.py:67 templates/categories/fragments/list.html:5
#: templates/categories/pages/index.html:4 templates/includes/navbar.html:103
#: templates/categories/pages/index.html:4 templates/includes/navbar.html:106
msgid "Categories"
msgstr "Categorias"
@@ -1140,8 +1150,8 @@ msgstr "Apagado Em"
msgid "Transaction"
msgstr "Transação"
#: apps/transactions/models.py:256 templates/includes/navbar.html:54
#: templates/includes/navbar.html:101
#: apps/transactions/models.py:256 templates/includes/navbar.html:57
#: templates/includes/navbar.html:104
#: templates/recurring_transactions/fragments/list_transactions.html:5
#: templates/recurring_transactions/fragments/table.html:37
#: templates/transactions/pages/transactions.html:5
@@ -1205,7 +1215,7 @@ msgstr "Recorrência"
msgid "Installment Amount"
msgstr "Valor da Parcela"
#: apps/transactions/models.py:391 templates/includes/navbar.html:69
#: apps/transactions/models.py:391 templates/includes/navbar.html:72
#: templates/installment_plans/fragments/list.html:5
#: templates/installment_plans/pages/index.html:4
msgid "Installment Plans"
@@ -1248,7 +1258,7 @@ msgstr "Última data gerada"
msgid "Last Generated Reference Date"
msgstr "Última data de referência gerada"
#: apps/transactions/models.py:592 templates/includes/navbar.html:71
#: apps/transactions/models.py:592 templates/includes/navbar.html:74
#: templates/recurring_transactions/fragments/list.html:5
#: templates/recurring_transactions/pages/index.html:4
msgid "Recurring Transactions"
@@ -2092,7 +2102,7 @@ msgid "Edit exchange rate"
msgstr "Editar taxa de câmbio"
#: templates/exchange_rates/fragments/list.html:25
#: templates/includes/navbar.html:58
#: templates/includes/navbar.html:61
#: templates/installment_plans/fragments/list.html:21
#: templates/yearly_overview/pages/overview_by_account.html:92
#: templates/yearly_overview/pages/overview_by_currency.html:94
@@ -2122,7 +2132,7 @@ msgstr "Navegação por página"
#: templates/exchange_rates_services/fragments/list.html:6
#: templates/exchange_rates_services/pages/index.html:4
#: templates/includes/navbar.html:133
#: templates/includes/navbar.html:136
msgid "Automatic Exchange Rates"
msgstr "Taxas de Câmbio Automáticas"
@@ -2261,52 +2271,56 @@ msgstr "Patrimônio"
msgid "Current"
msgstr "Atual"
#: templates/includes/navbar.html:63
#: templates/includes/navbar.html:50
msgid "Insights"
msgstr "Insights"
#: templates/includes/navbar.html:66
msgid "Trash Can"
msgstr "Lixeira"
#: templates/includes/navbar.html:79
#: templates/includes/navbar.html:82
msgid "Tools"
msgstr "Ferramentas"
#: templates/includes/navbar.html:83
#: templates/includes/navbar.html:86
msgid "Dollar Cost Average Tracker"
msgstr "Rastreador de Custo Médio Ponderado"
#: templates/includes/navbar.html:86
#: templates/includes/navbar.html:89
#: templates/mini_tools/unit_price_calculator.html:5
#: templates/mini_tools/unit_price_calculator.html:10
msgid "Unit Price Calculator"
msgstr "Calculadora de preço unitário"
#: templates/includes/navbar.html:89
#: templates/includes/navbar.html:92
#: templates/mini_tools/currency_converter/currency_converter.html:8
#: templates/mini_tools/currency_converter/currency_converter.html:15
msgid "Currency Converter"
msgstr "Conversor de Moeda"
#: templates/includes/navbar.html:98
#: templates/includes/navbar.html:101
msgid "Management"
msgstr "Gerenciar"
#: templates/includes/navbar.html:127
#: templates/includes/navbar.html:130
msgid "Automation"
msgstr "Automação"
#: templates/includes/navbar.html:129 templates/rules/fragments/list.html:5
#: templates/includes/navbar.html:132 templates/rules/fragments/list.html:5
#: templates/rules/pages/index.html:4
msgid "Rules"
msgstr "Regras"
#: templates/includes/navbar.html:143
#: templates/includes/navbar.html:146
msgid "Only use this if you know what you're doing"
msgstr "Só use isso se você souber o que está fazendo"
#: templates/includes/navbar.html:144
#: templates/includes/navbar.html:147
msgid "Django Admin"
msgstr "Django Admin"
#: templates/includes/navbar.html:153
#: templates/includes/navbar.html:156
msgid "Calculator"
msgstr "Calculadora"
@@ -2339,6 +2353,44 @@ msgstr "Cancelar"
msgid "Confirm"
msgstr "Confirmar"
#: templates/insights/fragments/sankey.html:83
msgid "From"
msgstr "De"
#: templates/insights/fragments/sankey.html:86
msgid "Percentage"
msgstr "Porcentagem"
#: templates/insights/pages/index.html:33
msgid "Month"
msgstr "Mês"
#: templates/insights/pages/index.html:36
#: templates/yearly_overview/pages/overview_by_account.html:61
#: templates/yearly_overview/pages/overview_by_currency.html:63
msgid "Year"
msgstr "Ano"
#: templates/insights/pages/index.html:39
msgid "Month Range"
msgstr "Intervalo de Mês"
#: templates/insights/pages/index.html:42
msgid "Year Range"
msgstr "Intervalo de Ano"
#: templates/insights/pages/index.html:45
msgid "Date Range"
msgstr "Intervalo de Data"
#: templates/insights/pages/index.html:74
msgid "Account Flow"
msgstr "Fluxo de Conta"
#: templates/insights/pages/index.html:81
msgid "Currency Flow"
msgstr "Fluxo de Moeda"
#: templates/installment_plans/fragments/add.html:5
msgid "Add installment plan"
msgstr "Adicionar parcelamento"
@@ -2709,11 +2761,6 @@ msgstr "Mostrar valores"
msgid "Yearly Overview"
msgstr "Visão Anual"
#: templates/yearly_overview/pages/overview_by_account.html:61
#: templates/yearly_overview/pages/overview_by_currency.html:63
msgid "Year"
msgstr "Ano"
#, fuzzy
#~| msgid "Tags"
#~ msgid "No Tags"
@@ -2913,9 +2960,6 @@ msgstr "Ano"
#~ msgid "Is an asset account?"
#~ msgstr "É uma conta de ativos?"
#~ msgid "Month"
#~ msgstr "Mês"
#~ msgid ""
#~ "This transaction is part of a Installment Plan, you can't delete it "
#~ "directly."

View File

@@ -46,8 +46,11 @@
href="{% url 'net_worth_projected' %}">{% translate 'Projected' %}</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link {% active_link views='insights_index' %}" href="{% url 'insights_index' %}">{% trans 'Insights' %}</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% active_link views='installment_plans_index||recurring_trasanctions_index||transactions_all_index' %}"
<a class="nav-link dropdown-toggle {% active_link views='installment_plans_index||recurring_trasanctions_index||transactions_all_index||transactions_trash_index' %}"
href="#" role="button"
data-bs-toggle="dropdown"
aria-expanded="false">
@@ -91,7 +94,7 @@
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% active_link views='tags_index||entities_index||categories_index||accounts_index||account_groups_index||currencies_index||exchange_rates_index||rules_index' %}"
<a class="nav-link dropdown-toggle {% active_link views='tags_index||entities_index||categories_index||accounts_index||account_groups_index||currencies_index||exchange_rates_index||rules_index||import_profiles_index||automatic_exchange_rates_index' %}"
href="#" role="button"
data-bs-toggle="dropdown"
aria-expanded="false">

View File

@@ -17,6 +17,10 @@
for x in datepickers
MonthYearPicker(it)
end
set datepickers to <.airyearpickerinput/> in me
for x in datepickers
YearPicker(it)
end
end
end
</script>

View File

@@ -1,107 +1,120 @@
{% extends "layouts/base.html" %}
{% load i18n %}
{% block content %}
<canvas id="sankeyChart" height="379"></canvas>
{% 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">
{% 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">
{% 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>
<script>
function setupSankeyChart(data, chartId = 'sankeyChart') {
// Format currency value
function formatCurrency(value, currency) {
return new Intl.NumberFormat(undefined, {
minimumFractionDigits: currency.decimal_places,
maximumFractionDigits: currency.decimal_places
}).format(value);
}
var data = {{ sankey_data|safe }};
const nodeIndices = {};
data.nodes.forEach((node, index) => {
nodeIndices[node.name] = index;
});
function setupSankeyChart(chartId = 'sankeyChart') {
function formatCurrency(value, currency) {
return new Intl.NumberFormat(undefined, {
minimumFractionDigits: currency.decimal_places,
maximumFractionDigits: currency.decimal_places
}).format(value);
}
// Format data for Chart.js
const chartData = {
datasets: [{
data: data.flows.map(flow => ({
from: flow.from_node,
to: flow.to_node,
flow: flow.flow
})),
colorMode: 'to',
labels: data.nodes.map(node => node.name),
size: 'max',
}]
};
// Create labels object mapping node IDs to display names
const labels = data.nodes.reduce((acc, node) => {
acc[node.id] = node.name;
return acc;
}, {});
// Chart configuration
const config = {
type: 'sankey',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
tooltip: {
callbacks: {
label: (context) => {
const flow = data.flows[context.dataIndex];
const formattedValue = formatCurrency(flow.original_amount, flow.currency);
return [
`De: ${flow.from_node}`,
`Para: ${flow.to_node}`,
`Valor: ${flow.currency.prefix}${formattedValue}${flow.currency.suffix}`,
`Porcentagem: ${flow.percentage.toFixed(2)}%`
];
// Define colors for each node based on its type
const colors = {};
data.nodes.forEach(node => {
if (node.id.startsWith('income_')) {
colors[node.id] = '#4dde80'; // Green for income
} else if (node.id.startsWith('expense_')) {
colors[node.id] = '#f87171'; // Red for expenses
} else {
colors[node.id] = '#fbb700'; // Primary for others
}
});
// Color getter functions
const getColor = (nodeId) => colors[nodeId];
const getHover = (nodeId) => colors[nodeId];
// Format data for Chart.js
const chartData = {
datasets: [{
data: data.flows.map(flow => ({
from: flow.from_node,
to: flow.to_node,
flow: flow.flow
})),
labels: labels,
colorFrom: (c) => getColor(c.dataset.data[c.dataIndex].from),
colorTo: (c) => getColor(c.dataset.data[c.dataIndex].to),
hoverColorFrom: (c) => getHover(c.dataset.data[c.dataIndex].from),
hoverColorTo: (c) => getHover(c.dataset.data[c.dataIndex].to),
colorMode: 'gradient',
alpha: 0.5,
size: 'max',
color: "white"
}]
};
const config = {
type: 'sankey',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
tooltip: {
callbacks: {
label: (context) => {
const flow = data.flows[context.dataIndex];
const fromNode = data.nodes.find(n => n.id === flow.from_node);
const toNode = data.nodes.find(n => n.id === flow.to_node);
const formattedValue = formatCurrency(flow.original_amount, flow.currency);
return [
`{% trans 'From' %}: ${fromNode.name}`,
`{% trans 'To' %}: ${toNode.name}`,
`{% trans 'Amount' %}: ${flow.currency.prefix}${formattedValue}${flow.currency.suffix}`,
`{% trans 'Percentage' %}: ${flow.percentage.toFixed(2)}%`
];
}
}
},
legend: {
display: false
},
title: {
display: false,
}
},
legend: {
display: false
},
title: {
display: true,
text: 'Fluxo de Transações',
font: {
size: 16
}
}
},
layout: {
padding: {
top: 20,
right: 20,
bottom: 20,
left: 20
}
}
};
// Destroy existing chart if it exists
const existingChart = Chart.getChart(chartId);
if (existingChart) {
existingChart.destroy();
}
};
// Destroy existing chart if it exists
const existingChart = Chart.getChart(chartId);
if (existingChart) {
existingChart.destroy();
// Create new chart
var chart = new Chart(
document.getElementById(chartId),
config
);
window.addEventListener('resize', () => {
chart.resize();
});
document.addEventListener('fullscreenchange', function () {
console.log('oi');
chart.resize();
});
}
// Create new chart
return new Chart(
document.getElementById(chartId),
config
);
}
// Usage in Django template or JavaScript file
document.addEventListener('DOMContentLoaded', function() {
// Assuming you have the Sankey data in a variable called sankeyData
// For Django template:
const sankeyData = {{ sankey_data|safe }};
console.log(sankeyData);
const chart = setupSankeyChart(sankeyData);
// Optional: Handle resize
window.addEventListener('resize', () => {
chart.resize();
});
});
</script>
{% endblock %}

View File

@@ -1,23 +1,94 @@
{% extends "layouts/base.html" %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block content %}
<div class="container-fluid">
<div class="row mx-3 mt-3">
<div class="card shadow w-auto">
<div class="card-body">
<div class="btn-group" role="group" aria-label="Basic radio toggle button group" _="on change log 'oi'">
<input type="radio" class="btn-check" name="btnradio" id="btnradio1" autocomplete="off" checked>
<label class="btn btn-sm btn-outline-primary" for="btnradio1">{% translate 'Month' %}</label>
<div class="container-fluid">
<div class="row my-3">
<div class="col-lg-2 col-md-3 mb-3 mb-md-0">
<div class="">
<div class="mb-2 w-100 d-lg-inline-flex d-grid gap-2 flex-wrap justify-content-lg-center" role="group"
_="on change
set type to event.target.value
add .tw-hidden to <#picker-form > div:not(.tw-hidden)/>
<input type="radio" class="btn-check" name="btnradio" id="btnradio2" autocomplete="off">
<label class="btn btn-sm btn-outline-primary" for="btnradio2">{% translate 'Year' %}</label>
if type == 'month'
remove .tw-hidden from #month-form
end
if type == 'year'
remove .tw-hidden from #year-form
end
if type == 'month-range'
remove .tw-hidden from #month-range-form
end
if type == 'year-range'
remove .tw-hidden from #year-range-form
end
if type == 'date-range'
remove .tw-hidden from #date-range-form
end
then trigger updated"
id="picker-type">
<input type="radio" class="btn-check" name="type" value="month" id="monthradio" autocomplete="off" checked>
<label class="btn btn-sm btn-outline-primary flex-grow-1" for="monthradio">{% translate 'Month' %}</label>
<input type="radio" class="btn-check" name="btnradio" id="btnradio3" autocomplete="off">
<label class="btn btn-sm btn-outline-primary" for="btnradio3">{% translate 'Range' %}</label>
<input type="radio" class="btn-check" name="type" value="year" id="yearradio" autocomplete="off">
<label class="btn btn-sm btn-outline-primary flex-grow-1" for="yearradio">{% translate 'Year' %}</label>
<input type="radio" class="btn-check" name="type" value="month-range" id="monthrangeradio" autocomplete="off">
<label class="btn btn-sm btn-outline-primary flex-grow-1" for="monthrangeradio">{% translate 'Month Range' %}</label>
<input type="radio" class="btn-check" name="type" value="year-range" id="yearrangeradio" autocomplete="off">
<label class="btn btn-sm btn-outline-primary flex-grow-1" for="yearrangeradio">{% translate 'Year Range' %}</label>
<input type="radio" class="btn-check" name="type" value="date-range" id="daterangeradio" autocomplete="off">
<label class="btn btn-sm btn-outline-primary flex-grow-1" for="daterangeradio">{% translate 'Date Range' %}</label>
</div>
<form id="picker-form"
_="install init_datepicker
on change trigger updated">
<div id="month-form" class="">
{% crispy month_form %}
</div>
<div id="year-form" class="tw-hidden">
{% crispy year_form %}
</div>
<div id="month-range-form" class="tw-hidden">
{% crispy month_range_form %}
</div>
<div id="year-range-form" class="tw-hidden">
{% crispy year_range_form %}
</div>
<div id="date-range-form" class="tw-hidden">
{% crispy date_range_form %}
</div>
</form>
</div>
<hr class="mt-0">
<div class="nav flex-column nav-pills" id="v-pills-tab" role="tablist"
aria-orientation="vertical">
<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 'insights_sankey_by_account' %}" hx-include="#picker-form, #picker-type"
hx-indicator="#tab-content"
hx-target="#tab-content">{% trans 'Account 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 'insights_sankey_by_currency' %}"
hx-include="#picker-form, #picker-type"
hx-indicator="#tab-content"
hx-target="#tab-content">{% trans 'Currency Flow' %}
</button>
</div>
</div>
<div class="col-md-9 col-lg-10">
<div class="tab-content w-100" id="v-pills-tabContent">
<div class="tab-pane fade" id="v-pills-content" role="tabpanel" tabindex="0">
<div id="tab-content" class="show-loading"></div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -168,3 +168,75 @@ window.MonthYearPicker = function createDynamicDatePicker(element) {
}
return new AirDatepicker(element, opts);
};
window.YearPicker = function createDynamicDatePicker(element) {
let todayButton = {
content: element.dataset.nowButtonTxt,
onClick: (dp) => {
let date = new Date();
dp.selectDate(date, {updateTime: true});
dp.setViewDate(date);
}
}
let isOnMobile = isMobile();
let baseOpts = {
isMobile: isOnMobile,
view: 'years',
minView: 'years',
dateFormat: 'yyyy',
autoClose: element.dataset.autoClose === 'true',
buttons: element.dataset.clearButton === 'true' ? ['clear', todayButton] : [todayButton],
locale: locales[element.dataset.language],
onSelect: ({date, formattedDate, datepicker}) => {
const _event = new CustomEvent("change", {
bubbles: true,
});
datepicker.$el.dispatchEvent(_event);
}
};
const positionConfig = !isOnMobile ? {
position({$datepicker, $target, $pointer, done}) {
let popper = createPopper($target, $datepicker, {
placement: 'bottom',
modifiers: [
{
name: 'flip',
options: {
padding: {
top: 64
}
}
},
{
name: 'offset',
options: {
offset: [0, 20]
}
},
{
name: 'arrow',
options: {
element: $pointer
}
}
]
});
return function completeHide() {
popper.destroy();
done();
};
}
} : {};
let opts = {...baseOpts, ...positionConfig};
if (element.dataset.value) {
opts["selectedDates"] = [new Date(element.dataset.value + "T00:00:00")];
opts["startDate"] = [new Date(element.dataset.value + "T00:00:00")];
}
return new AirDatepicker(element, opts);
};