mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-03-18 07:24:00 +01:00
feat: insight page
This commit is contained in:
@@ -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
110
app/apps/insights/forms.py
Normal 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",
|
||||
),
|
||||
)
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
}
|
||||
|
||||
96
app/apps/insights/utils/transactions.py
Normal file
96
app/apps/insights/utils/transactions.py
Normal 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
|
||||
@@ -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"},
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user