Compare commits

...

28 Commits

Author SHA1 Message Date
Herculino Trotta
627b5d250b Merge pull request #164
feat: insights page
2025-02-16 00:14:56 -03:00
Herculino Trotta
195a8a68d6 feat: insight page 2025-02-16 00:14:23 -03:00
Herculino Trotta
daf1f68b82 Merge remote-tracking branch 'origin/insights' into insights 2025-02-15 00:49:25 -03:00
Herculino Trotta
dd24fd56d3 insights (wip) 2025-02-15 00:49:00 -03:00
Herculino Trotta
7a2acb6497 fix(insights): sankey diagram inconsistent sizing 2025-02-15 00:48:59 -03:00
Herculino Trotta
9c339faa72 chore(frontend): install chartjs-chart-sankey 2025-02-15 00:48:59 -03:00
Herculino Trotta
02376ad02b feat(insights): sankey diagram (WIP) 2025-02-15 00:48:59 -03:00
Herculino Trotta
b53a4a0286 feat(insights): create app 2025-02-15 00:48:59 -03:00
Herculino Trotta
a1f618434b Merge pull request #163 from eitchtee/dca_improvements
feat(dca): link transactions to DCA
2025-02-15 00:43:07 -03:00
Herculino Trotta
7b5be29f0d locale: update locales 2025-02-15 00:42:38 -03:00
Herculino Trotta
56a73b181a Merge remote-tracking branch 'origin/main' into dca_improvements
# Conflicts:
#	app/locale/nl/LC_MESSAGES/django.po
2025-02-15 00:41:49 -03:00
Herculino Trotta
865618e054 feat(dca): link transactions to DCA 2025-02-15 00:41:06 -03:00
Herculino Trotta
9e912b2736 locale: update locales 2025-02-15 00:40:44 -03:00
Herculino Trotta
da7680e70f Merge pull request #159 from DragonHeart69/main
update NL to version 0.9.4
2025-02-14 10:20:40 -03:00
Herculino Trotta
ab594eb511 Merge pull request #162
fix(style): selecting transaction no longer highlights it
2025-02-14 00:50:30 -03:00
Herculino Trotta
cffaaa369a fix(style): selecting transaction no longer highlights it 2025-02-14 00:50:01 -03:00
Herculino Trotta
5f414e82ee Merge pull request #161
feat(internal): trigger rules on bulk actions
2025-02-14 00:35:10 -03:00
Herculino Trotta
f3bcef534e feat(internal): trigger rules on bulk actions 2025-02-14 00:34:51 -03:00
Herculino Trotta
d140ff5b70 Merge pull request #160
fix(frontend): loading indicator on empty div too close to the top
2025-02-14 00:04:03 -03:00
Herculino Trotta
7eceacfe68 fix(frontend): loading indicator on empty div too close to the top 2025-02-14 00:03:43 -03:00
Herculino Trotta
038438fba7 insights (wip) 2025-02-12 09:48:31 -03:00
Dimitri Decrock
ee98a5ef12 update NL to version 0.9.4 2025-02-12 06:59:28 +01:00
Herculino Trotta
28b12faaf0 fix(insights): sankey diagram inconsistent sizing 2025-02-11 00:40:37 -03:00
Herculino Trotta
d0f2742637 chore(frontend): install chartjs-chart-sankey 2025-02-11 00:37:48 -03:00
Herculino Trotta
9c55dac866 feat(insights): sankey diagram (WIP) 2025-02-11 00:37:30 -03:00
Herculino Trotta
e6d8b548b7 Merge pull request #157
fix(docker): procrastinate can't recover if it crashes in a running instance
2025-02-10 23:13:33 -03:00
Herculino Trotta
4f8c2215c1 fix(docker): procrastinate can't recover if it crashes in a running instance 2025-02-10 23:13:16 -03:00
Herculino Trotta
a3a8791e96 feat(insights): create app 2025-02-09 23:00:33 -03:00
40 changed files with 2275 additions and 708 deletions

View File

@@ -49,4 +49,5 @@ urlpatterns = [
path("", include("apps.dca.urls")),
path("", include("apps.mini_tools.urls")),
path("", include("apps.import_app.urls")),
path("", include("apps.insights.urls")),
]

View File

@@ -12,15 +12,14 @@ class DynamicModelChoiceField(forms.ModelChoiceField):
self.to_field_name = kwargs.pop("to_field_name", "pk")
self.create_field = kwargs.pop("create_field", None)
if not self.create_field:
raise ValueError("The 'create_field' parameter is required.")
self.queryset = kwargs.pop("queryset", model.objects.all())
super().__init__(queryset=self.queryset, *args, **kwargs)
self._created_instance = None
self.widget = TomSelect(clear_button=True, create=True)
super().__init__(queryset=self.queryset, *args, **kwargs)
self._created_instance = None
def to_python(self, value):
if value in self.empty_values:
return None
@@ -53,14 +52,19 @@ class DynamicModelChoiceField(forms.ModelChoiceField):
else:
raise self.model.DoesNotExist
except self.model.DoesNotExist:
try:
with transaction.atomic():
instance, _ = self.model.objects.update_or_create(
**{self.create_field: value}
if self.create_field:
try:
with transaction.atomic():
instance, _ = self.model.objects.update_or_create(
**{self.create_field: value}
)
self._created_instance = instance
return instance
except Exception as e:
raise ValidationError(
self.error_messages["invalid_choice"], code="invalid_choice"
)
self._created_instance = instance
return instance
except Exception as e:
else:
raise ValidationError(
self.error_messages["invalid_choice"], code="invalid_choice"
)

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

View File

@@ -1,4 +1,5 @@
from django.forms import widgets, SelectMultiple
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -129,3 +130,28 @@ class TomSelect(widgets.Select):
class TomSelectMultiple(SelectMultiple, TomSelect):
pass
class TransactionSelect(TomSelect):
def __init__(self, income: bool = True, expense: bool = True, *args, **kwargs):
super().__init__(*args, **kwargs)
self.load_income = income
self.load_expense = expense
self.create = False
def build_attrs(self, base_attrs, extra_attrs=None):
attrs = super().build_attrs(base_attrs, extra_attrs)
if self.load_income and self.load_expense:
attrs["data-load"] = reverse("transactions_search")
elif self.load_income and not self.load_expense:
attrs["data-load"] = reverse(
"transactions_search", kwargs={"filter_type": "income"}
)
elif self.load_expense and not self.load_income:
attrs["data-load"] = reverse(
"transactions_search", kwargs={"filter_type": "expenses"}
)
return attrs

View File

@@ -1,14 +1,22 @@
from crispy_forms.bootstrap import FormActions
from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion
from crispy_forms.bootstrap import FormActions, AccordionGroup
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Row, Column
from crispy_forms.layout import Layout, Row, Column, HTML
from django import forms
from django.utils.translation import gettext_lazy as _
from apps.accounts.models import Account
from apps.common.widgets.crispy.submit import NoClassSubmit
from apps.common.widgets.datepicker import AirDatePickerInput
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
from apps.common.widgets.tom_select import TomSelect
from apps.dca.models import DCAStrategy, DCAEntry
from apps.common.widgets.tom_select import TransactionSelect
from apps.transactions.models import Transaction, TransactionTag, TransactionCategory
from apps.common.fields.forms.dynamic_select import (
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
)
class DCAStrategyForm(forms.ModelForm):
@@ -53,6 +61,75 @@ class DCAStrategyForm(forms.ModelForm):
class DCAEntryForm(forms.ModelForm):
create_transaction = forms.BooleanField(
label=_("Create transaction"), initial=False, required=False
)
from_account = forms.ModelChoiceField(
queryset=Account.objects.filter(is_archived=False),
label=_("From Account"),
widget=TomSelect(clear_button=False, group_by="group"),
required=False,
)
to_account = forms.ModelChoiceField(
queryset=Account.objects.filter(is_archived=False),
label=_("To Account"),
widget=TomSelect(clear_button=False, group_by="group"),
required=False,
)
from_category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory,
required=False,
label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
)
to_category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory,
required=False,
label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
)
from_tags = DynamicModelMultipleChoiceField(
model=TransactionTag,
to_field_name="name",
create_field="name",
required=False,
label=_("Tags"),
queryset=TransactionTag.objects.filter(active=True),
)
to_tags = DynamicModelMultipleChoiceField(
model=TransactionTag,
to_field_name="name",
create_field="name",
required=False,
label=_("Tags"),
queryset=TransactionTag.objects.filter(active=True),
)
expense_transaction = DynamicModelChoiceField(
model=Transaction,
to_field_name="id",
label=_("Expense Transaction"),
required=False,
queryset=Transaction.objects.none(),
widget=TransactionSelect(clear_button=True, income=False, expense=True),
help_text=_("Type to search for a transaction to link to this entry"),
)
income_transaction = DynamicModelChoiceField(
model=Transaction,
to_field_name="id",
label=_("Income Transaction"),
required=False,
queryset=Transaction.objects.none(),
widget=TransactionSelect(clear_button=True, income=True, expense=False),
help_text=_("Type to search for a transaction to link to this entry"),
)
class Meta:
model = DCAEntry
fields = [
@@ -60,13 +137,19 @@ class DCAEntryForm(forms.ModelForm):
"amount_paid",
"amount_received",
"notes",
"expense_transaction",
"income_transaction",
]
widgets = {
"notes": forms.Textarea(attrs={"rows": 3}),
}
def __init__(self, *args, **kwargs):
strategy = kwargs.pop("strategy", None)
super().__init__(*args, **kwargs)
self.strategy = strategy if strategy else self.instance.strategy
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
@@ -75,18 +158,66 @@ class DCAEntryForm(forms.ModelForm):
Column("amount_paid", css_class="form-group col-md-6"),
Column("amount_received", css_class="form-group col-md-6"),
),
Row(
Column("expense_transaction", css_class="form-group col-md-6"),
Column("income_transaction", css_class="form-group col-md-6"),
),
"notes",
BS5Accordion(
AccordionGroup(
_("Create transaction"),
Switch("create_transaction"),
Row(
Column(
Row(
Column(
"from_account",
css_class="form-group col-md-6 mb-0",
),
css_class="form-row",
),
Row(
Column(
"from_category",
css_class="form-group col-md-6 mb-0",
),
Column(
"from_tags", css_class="form-group col-md-6 mb-0"
),
css_class="form-row",
),
),
css_class="p-1 mx-1 my-3 border rounded-3",
),
Row(
Column(
Row(
Column(
"to_account",
css_class="form-group col-md-6 mb-0",
),
css_class="form-row",
),
Row(
Column(
"to_category", css_class="form-group col-md-6 mb-0"
),
Column("to_tags", css_class="form-group col-md-6 mb-0"),
css_class="form-row",
),
),
css_class="p-1 mx-1 my-3 border rounded-3",
),
active=False,
),
AccordionGroup(
_("Link transaction"),
"income_transaction",
"expense_transaction",
),
flush=False,
always_open=False,
css_class="mb-3",
),
)
if self.instance and self.instance.pk:
# decimal_places = self.instance.account.currency.decimal_places
# self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput(
# decimal_places=decimal_places
# )
self.helper.layout.append(
FormActions(
NoClassSubmit(
@@ -95,7 +226,6 @@ class DCAEntryForm(forms.ModelForm):
),
)
else:
# self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.helper.layout.append(
FormActions(
NoClassSubmit(
@@ -107,3 +237,118 @@ class DCAEntryForm(forms.ModelForm):
self.fields["amount_paid"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["amount_received"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["date"].widget = AirDatePickerInput(clear_button=False)
expense_transaction = None
income_transaction = None
if self.instance and self.instance.pk:
# Edit mode - get from instance
expense_transaction = self.instance.expense_transaction
income_transaction = self.instance.income_transaction
elif self.data.get("expense_transaction"):
# Form validation - get from submitted data
try:
expense_transaction = Transaction.objects.get(
id=self.data["expense_transaction"]
)
income_transaction = Transaction.objects.get(
id=self.data["income_transaction"]
)
except Transaction.DoesNotExist:
pass
# If we have a current transaction, ensure it's in the queryset
if income_transaction:
self.fields["income_transaction"].queryset = Transaction.objects.filter(
id=income_transaction.id
)
if expense_transaction:
self.fields["expense_transaction"].queryset = Transaction.objects.filter(
id=expense_transaction.id
)
def clean(self):
cleaned_data = super().clean()
if cleaned_data.get("create_transaction"):
from_account = cleaned_data.get("from_account")
to_account = cleaned_data.get("to_account")
if not from_account and not to_account:
raise forms.ValidationError(
{
"from_account": _("You must provide an account."),
"to_account": _("You must provide an account."),
}
)
elif not from_account and to_account:
raise forms.ValidationError(
{"from_account": _("You must provide an account.")}
)
elif not to_account and from_account:
raise forms.ValidationError(
{"to_account": _("You must provide an account.")}
)
if from_account == to_account:
raise forms.ValidationError(
_("From and To accounts must be different.")
)
return cleaned_data
def save(self, **kwargs):
instance = super().save(commit=False)
if self.cleaned_data.get("create_transaction"):
from_account = self.cleaned_data["from_account"]
to_account = self.cleaned_data["to_account"]
from_amount = instance.amount_paid
to_amount = instance.amount_received
date = instance.date
description = _("DCA for %(strategy_name)s") % {
"strategy_name": self.strategy.name
}
from_category = self.cleaned_data.get("from_category")
to_category = self.cleaned_data.get("to_category")
notes = self.cleaned_data.get("notes")
# Create "From" transaction
from_transaction = Transaction.objects.create(
account=from_account,
type=Transaction.Type.EXPENSE,
is_paid=True,
date=date,
amount=from_amount,
description=description,
category=from_category,
notes=notes,
)
from_transaction.tags.set(self.cleaned_data.get("from_tags", []))
# Create "To" transaction
to_transaction = Transaction.objects.create(
account=to_account,
type=Transaction.Type.INCOME,
is_paid=True,
date=date,
amount=to_amount,
description=description,
category=to_category,
notes=notes,
)
to_transaction.tags.set(self.cleaned_data.get("to_tags", []))
instance.expense_transaction = from_transaction
instance.income_transaction = to_transaction
else:
if instance.expense_transaction:
instance.expense_transaction.amount = instance.amount_paid
instance.expense_transaction.save()
if instance.income_transaction:
instance.income_transaction.amount = instance.amount_received
instance.income_transaction.save()
instance.strategy = self.strategy
instance.save()
return instance

View File

@@ -155,11 +155,9 @@ def strategy_detail(request, strategy_id):
def strategy_entry_add(request, strategy_id):
strategy = get_object_or_404(DCAStrategy, id=strategy_id)
if request.method == "POST":
form = DCAEntryForm(request.POST)
form = DCAEntryForm(request.POST, strategy=strategy)
if form.is_valid():
entry = form.save(commit=False)
entry.strategy = strategy
entry.save()
entry = form.save()
messages.success(request, _("Entry added successfully"))
return HttpResponse(
@@ -169,7 +167,7 @@ def strategy_entry_add(request, strategy_id):
},
)
else:
form = DCAEntryForm()
form = DCAEntryForm(strategy=strategy)
return render(
request,

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class InsightsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.insights"

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

View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

17
app/apps/insights/urls.py Normal file
View File

@@ -0,0 +1,17 @@
from django.urls import path
from . import views
urlpatterns = [
path("insights/", views.index, name="insights_index"),
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

View File

@@ -0,0 +1,248 @@
from django.utils.translation import gettext_lazy as _
from decimal import Decimal
from typing import Dict, List, TypedDict
class SankeyNode(TypedDict):
name: str
class SankeyFlow(TypedDict):
from_node: str
to_node: str
flow: float
currency: Dict
original_amount: float
percentage: float
def generate_sankey_data_by_account(transactions_queryset):
"""
Generates Sankey diagram data from transaction queryset using account as intermediary.
"""
nodes: Dict[str, Dict] = {}
flows: List[SankeyFlow] = []
# Aggregate transactions
income_data = {} # {(category, currency, account) -> amount}
expense_data = {} # {(category, currency, account) -> 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
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")) + 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, 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
flows.append(
{
"from_node": from_node_id,
"to_node": to_node_id,
"flow": float(scaled_flow),
"currency": {
"code": currency.code,
"prefix": currency.prefix,
"suffix": currency.suffix,
"decimal_places": currency.decimal_places,
},
"original_amount": float(amount),
"percentage": float(percentage),
}
)
# Process income
for (category, currency, account), amount in income_data.items():
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_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_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": 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

@@ -0,0 +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_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):
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/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

@@ -92,6 +92,8 @@ def transactions_list(request, month: int, year: int):
"account__currency",
"installment_plan",
"entities",
"dca_expense_entries",
"dca_income_entries",
)
)

View File

@@ -1,15 +1,23 @@
from django.dispatch import Signal, receiver
from django.dispatch import receiver
from apps.transactions.models import Transaction
from apps.transactions.models import (
Transaction,
transaction_created,
transaction_updated,
)
from apps.rules.tasks import check_for_transaction_rules
transaction_created = Signal()
transaction_updated = Signal()
@receiver(transaction_created)
@receiver(transaction_updated)
def transaction_changed_receiver(sender: Transaction, signal, **kwargs):
for dca_entry in sender.dca_expense_entries.all():
dca_entry.amount_paid = sender.amount
dca_entry.save()
for dca_entry in sender.dca_income_entries.all():
dca_entry.amount_received = sender.amount
dca_entry.save()
check_for_transaction_rules.defer(
instance_id=sender.id,
signal=(

View File

@@ -1,22 +1,66 @@
import logging
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import Q
from django.dispatch import Signal
from django.template.defaultfilters import date
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from apps.common.fields.month_year import MonthYearModelField
from apps.common.functions.decimals import truncate_decimal
from apps.common.templatetags.decimal import localize_number, drop_trailing_zeros
from apps.currencies.utils.convert import convert
from apps.transactions.validators import validate_decimal_places, validate_non_negative
logger = logging.getLogger()
transaction_created = Signal()
transaction_updated = Signal()
class SoftDeleteQuerySet(models.QuerySet):
@staticmethod
def _emit_signals(instances, created=False):
"""Helper to emit signals for multiple instances"""
for instance in instances:
if created:
transaction_created.send(sender=instance)
else:
transaction_updated.send(sender=instance)
def bulk_create(self, objs, emit_signal=True, **kwargs):
instances = super().bulk_create(objs, **kwargs)
if emit_signal:
self._emit_signals(instances, created=True)
return instances
def bulk_update(self, objs, fields, emit_signal=True, **kwargs):
result = super().bulk_update(objs, fields, **kwargs)
if emit_signal:
self._emit_signals(objs, created=False)
return result
def update(self, emit_signal=True, **kwargs):
# Get instances before update
instances = list(self)
result = super().update(**kwargs)
if emit_signal:
# Refresh instances to get new values
refreshed = self.model.objects.filter(pk__in=[obj.pk for obj in instances])
self._emit_signals(refreshed, created=False)
return result
def delete(self):
if not settings.ENABLE_SOFT_DELETE:
# If soft deletion is disabled, perform a normal delete
@@ -274,7 +318,13 @@ class Transaction(models.Model):
def __str__(self):
type_display = self.get_type_display()
return f"{self.description} - {type_display} - {self.account} - {self.date}"
frmt_date = date(self.date, "SHORT_DATE_FORMAT")
account = self.account
tags = ", ".join([x.name for x in self.tags.all()]) or _("No tags")
category = self.category or _("No category")
amount = localize_number(drop_trailing_zeros(self.amount))
description = self.description or _("No description")
return f"[{frmt_date}][{type_display}][{account}] {description}{category}{tags}{amount}"
class InstallmentPlan(models.Model):

View File

@@ -86,6 +86,16 @@ urlpatterns = [
views.transactions_bulk_edit,
name="transactions_bulk_edit",
),
path(
"transactions/json/search/",
views.get_recent_transactions,
name="transactions_search",
),
path(
"transactions/json/search/<str:filter_type>/",
views.get_recent_transactions,
name="transactions_search",
),
path(
"transaction/<int:transaction_id>/clone/",
views.transaction_clone,

View File

@@ -18,9 +18,6 @@ def bulk_pay_transactions(request):
count = transactions.count()
transactions.update(is_paid=True)
for transaction in transactions:
transaction_updated.send(sender=transaction)
messages.success(
request,
ngettext_lazy(
@@ -45,9 +42,6 @@ def bulk_unpay_transactions(request):
count = transactions.count()
transactions.update(is_paid=False)
for transaction in transactions:
transaction_updated.send(sender=transaction)
messages.success(
request,
ngettext_lazy(
@@ -94,7 +88,7 @@ def bulk_undelete_transactions(request):
selected_transactions = request.GET.getlist("transactions", [])
transactions = Transaction.deleted_objects.filter(id__in=selected_transactions)
count = transactions.count()
transactions.update(deleted=False, deleted_at=None)
transactions.update(deleted=False, deleted_at=None, emit_signal=False)
messages.success(
request,

View File

@@ -4,14 +4,14 @@ from copy import deepcopy
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.http import HttpResponse
from django.db.models import Q
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render, get_object_or_404
from django.utils import timezone
from django.utils.translation import gettext_lazy as _, ngettext_lazy
from django.views.decorators.http import require_http_methods
from apps.common.decorators.htmx import only_htmx
from apps.common.utils.dicts import remove_falsey_entries
from apps.rules.signals import transaction_created, transaction_updated
from apps.transactions.filters import TransactionsFilter
from apps.transactions.forms import (
@@ -363,6 +363,8 @@ def transaction_all_list(request):
"account__currency",
"installment_plan",
"entities",
"dca_expense_entries",
"dca_income_entries",
).all()
transactions = default_order(transactions, order=order)
@@ -395,6 +397,9 @@ def transaction_all_summary(request):
"account__exchange_currency",
"account__currency",
"installment_plan",
"entities",
"dca_expense_entries",
"dca_income_entries",
).all()
f = TransactionsFilter(request.GET, queryset=transactions)
@@ -426,6 +431,9 @@ def transaction_all_account_summary(request):
"account__exchange_currency",
"account__currency",
"installment_plan",
"entities",
"dca_expense_entries",
"dca_income_entries",
).all()
f = TransactionsFilter(request.GET, queryset=transactions)
@@ -453,6 +461,9 @@ def transaction_all_currency_summary(request):
"account__exchange_currency",
"account__currency",
"installment_plan",
"entities",
"dca_expense_entries",
"dca_income_entries",
).all()
f = TransactionsFilter(request.GET, queryset=transactions)
@@ -484,6 +495,9 @@ def transactions_trash_can_index(request):
return render(request, "transactions/pages/trash.html")
@only_htmx
@login_required
@require_http_methods(["GET"])
def transactions_trash_can_list(request):
transactions = Transaction.deleted_objects.prefetch_related(
"account",
@@ -493,6 +507,10 @@ def transactions_trash_can_list(request):
"account__exchange_currency",
"account__currency",
"installment_plan",
"entities",
"entities",
"dca_expense_entries",
"dca_income_entries",
).all()
return render(
@@ -500,3 +518,41 @@ def transactions_trash_can_list(request):
"transactions/fragments/trash_list.html",
{"transactions": transactions},
)
@login_required
@require_http_methods(["GET"])
def get_recent_transactions(request, filter_type=None):
"""Return the 100 most recent non-deleted transactions with optional search."""
# Get search term from query params
search_term = request.GET.get("q", "").strip()
# Base queryset with selected fields
queryset = (
Transaction.objects.filter(deleted=False)
.select_related("account", "category")
.order_by("-created_at")
)
if filter_type:
if filter_type == "expenses":
queryset = queryset.filter(type=Transaction.Type.EXPENSE)
elif filter_type == "income":
queryset = queryset.filter(type=Transaction.Type.INCOME)
# Apply search if provided
if search_term:
queryset = queryset.filter(
Q(description__icontains=search_term)
| Q(notes__icontains=search_term)
| Q(internal_note__icontains=search_term)
| Q(tags__name__icontains=search_term)
| Q(category__name__icontains=search_term)
)
# Prepare data for JSON response
data = []
for t in queryset:
data.append({"text": str(t), "value": str(t.id)})
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-09 17:27-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"
@@ -24,7 +24,7 @@ msgstr ""
#: apps/accounts/forms.py:40 apps/accounts/forms.py:96
#: apps/currencies/forms.py:53 apps/currencies/forms.py:91
#: apps/currencies/forms.py:142 apps/dca/forms.py:41 apps/dca/forms.py:93
#: apps/currencies/forms.py:142 apps/dca/forms.py:49 apps/dca/forms.py:224
#: apps/import_app/forms.py:34 apps/rules/forms.py:45 apps/rules/forms.py:87
#: apps/rules/forms.py:359 apps/transactions/forms.py:190
#: apps/transactions/forms.py:257 apps/transactions/forms.py:581
@@ -34,9 +34,9 @@ msgid "Update"
msgstr ""
#: apps/accounts/forms.py:48 apps/accounts/forms.py:104
#: apps/common/widgets/tom_select.py:12 apps/currencies/forms.py:61
#: apps/common/widgets/tom_select.py:13 apps/currencies/forms.py:61
#: apps/currencies/forms.py:99 apps/currencies/forms.py:150
#: apps/dca/forms.py:49 apps/dca/forms.py:102 apps/import_app/forms.py:42
#: apps/dca/forms.py:57 apps/dca/forms.py:232 apps/import_app/forms.py:42
#: apps/rules/forms.py:53 apps/rules/forms.py:95 apps/rules/forms.py:367
#: apps/transactions/forms.py:174 apps/transactions/forms.py:199
#: apps/transactions/forms.py:589 apps/transactions/forms.py:632
@@ -68,30 +68,32 @@ msgstr ""
msgid "New balance"
msgstr ""
#: apps/accounts/forms.py:119 apps/rules/forms.py:168 apps/rules/forms.py:183
#: apps/rules/models.py:32 apps/rules/models.py:280
#: apps/transactions/forms.py:39 apps/transactions/forms.py:291
#: apps/transactions/forms.py:298 apps/transactions/forms.py:478
#: apps/transactions/forms.py:723 apps/transactions/models.py:159
#: apps/transactions/models.py:328 apps/transactions/models.py:508
#: apps/accounts/forms.py:119 apps/dca/forms.py:85 apps/dca/forms.py:92
#: apps/rules/forms.py:168 apps/rules/forms.py:183 apps/rules/models.py:32
#: apps/rules/models.py:280 apps/transactions/forms.py:39
#: apps/transactions/forms.py:291 apps/transactions/forms.py:298
#: apps/transactions/forms.py:478 apps/transactions/forms.py:723
#: apps/transactions/models.py:203 apps/transactions/models.py:378
#: apps/transactions/models.py:558
msgid "Category"
msgstr ""
#: apps/accounts/forms.py:126 apps/rules/forms.py:171 apps/rules/forms.py:180
#: apps/rules/models.py:33 apps/rules/models.py:284
#: apps/transactions/filters.py:74 apps/transactions/forms.py:47
#: apps/transactions/forms.py:307 apps/transactions/forms.py:315
#: apps/transactions/forms.py:471 apps/transactions/forms.py:716
#: apps/transactions/models.py:165 apps/transactions/models.py:330
#: apps/transactions/models.py:512 templates/includes/navbar.html:105
#: templates/tags/fragments/list.html:5 templates/tags/pages/index.html:4
#: apps/accounts/forms.py:126 apps/dca/forms.py:101 apps/dca/forms.py:109
#: apps/rules/forms.py:171 apps/rules/forms.py:180 apps/rules/models.py:33
#: apps/rules/models.py:284 apps/transactions/filters.py:74
#: apps/transactions/forms.py:47 apps/transactions/forms.py:307
#: 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:108 templates/tags/fragments/list.html:5
#: templates/tags/pages/index.html:4
msgid "Tags"
msgstr ""
#: apps/accounts/models.py:9 apps/accounts/models.py:21 apps/dca/models.py:14
#: apps/import_app/models.py:14 apps/rules/models.py:10
#: apps/transactions/models.py:67 apps/transactions/models.py:87
#: apps/transactions/models.py:106
#: apps/transactions/models.py:111 apps/transactions/models.py:131
#: apps/transactions/models.py:150
#: templates/account_groups/fragments/list.html:25
#: templates/accounts/fragments/list.html:25
#: templates/categories/fragments/table.html:16
@@ -112,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 ""
@@ -153,15 +155,15 @@ msgstr ""
#: apps/accounts/models.py:59 apps/rules/forms.py:160 apps/rules/forms.py:173
#: apps/rules/models.py:24 apps/rules/models.py:236
#: apps/transactions/forms.py:59 apps/transactions/forms.py:463
#: apps/transactions/forms.py:708 apps/transactions/models.py:132
#: apps/transactions/models.py:288 apps/transactions/models.py:490
#: apps/transactions/forms.py:708 apps/transactions/models.py:176
#: apps/transactions/models.py:338 apps/transactions/models.py:540
msgid "Account"
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
@@ -232,13 +234,13 @@ msgstr ""
msgid "Either 'date' or 'reference_date' must be provided."
msgstr ""
#: apps/common/fields/forms/dynamic_select.py:127
#: apps/common/fields/forms/dynamic_select.py:163
#: apps/common/fields/forms/dynamic_select.py:131
#: apps/common/fields/forms/dynamic_select.py:167
msgid "Error creating new instance"
msgstr ""
#: apps/common/fields/forms/grouped_select.py:24
#: apps/common/widgets/tom_select.py:91 apps/common/widgets/tom_select.py:94
#: apps/common/widgets/tom_select.py:92 apps/common/widgets/tom_select.py:95
msgid "Ungrouped"
msgstr ""
@@ -330,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 ""
@@ -337,18 +340,18 @@ msgstr ""
msgid "Now"
msgstr ""
#: apps/common/widgets/tom_select.py:10
#: apps/common/widgets/tom_select.py:11
msgid "Remove"
msgstr ""
#: apps/common/widgets/tom_select.py:14
#: apps/common/widgets/tom_select.py:15
#: templates/mini_tools/unit_price_calculator.html:174
#: templates/monthly_overview/pages/overview.html:172
#: templates/transactions/pages/transactions.html:17
msgid "Clear"
msgstr ""
#: apps/common/widgets/tom_select.py:15
#: apps/common/widgets/tom_select.py:16
msgid "No results..."
msgstr ""
@@ -363,7 +366,7 @@ msgstr ""
#: apps/currencies/forms.py:69 apps/dca/models.py:156 apps/rules/forms.py:163
#: apps/rules/forms.py:176 apps/rules/models.py:27 apps/rules/models.py:248
#: apps/transactions/forms.py:63 apps/transactions/forms.py:319
#: apps/transactions/models.py:142
#: apps/transactions/models.py:186
#: templates/dca/fragments/strategy/details.html:52
#: templates/exchange_rates/fragments/table.html:10
#: templates/exchange_rates_services/fragments/table.html:10
@@ -384,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
@@ -414,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 ""
@@ -442,8 +445,8 @@ msgstr ""
msgid "Service Type"
msgstr ""
#: apps/currencies/models.py:107 apps/transactions/models.py:71
#: apps/transactions/models.py:90 apps/transactions/models.py:109
#: apps/currencies/models.py:107 apps/transactions/models.py:115
#: apps/transactions/models.py:134 apps/transactions/models.py:153
#: templates/categories/fragments/list.html:21
#: templates/entities/fragments/list.html:21
#: templates/recurring_transactions/fragments/list.html:21
@@ -559,6 +562,48 @@ msgstr ""
msgid "Services queued successfully"
msgstr ""
#: apps/dca/forms.py:65 apps/dca/forms.py:164
msgid "Create transaction"
msgstr ""
#: apps/dca/forms.py:70 apps/transactions/forms.py:266
msgid "From Account"
msgstr ""
#: apps/dca/forms.py:76 apps/transactions/forms.py:271
msgid "To Account"
msgstr ""
#: apps/dca/forms.py:116 apps/dca/models.py:169
msgid "Expense Transaction"
msgstr ""
#: apps/dca/forms.py:120 apps/dca/forms.py:130
msgid "Type to search for a transaction to link to this entry"
msgstr ""
#: apps/dca/forms.py:126 apps/dca/models.py:177
msgid "Income Transaction"
msgstr ""
#: apps/dca/forms.py:210
msgid "Link transaction"
msgstr ""
#: apps/dca/forms.py:279 apps/dca/forms.py:280 apps/dca/forms.py:285
#: apps/dca/forms.py:289
msgid "You must provide an account."
msgstr ""
#: apps/dca/forms.py:294 apps/transactions/forms.py:413
msgid "From and To accounts must be different."
msgstr ""
#: apps/dca/forms.py:308
#, python-format
msgid "DCA for %(strategy_name)s"
msgstr ""
#: apps/dca/models.py:17
msgid "Target Currency"
msgstr ""
@@ -569,8 +614,8 @@ msgstr ""
#: apps/dca/models.py:27 apps/dca/models.py:179 apps/rules/forms.py:167
#: apps/rules/forms.py:182 apps/rules/models.py:31 apps/rules/models.py:264
#: apps/transactions/forms.py:333 apps/transactions/models.py:155
#: apps/transactions/models.py:337 apps/transactions/models.py:518
#: apps/transactions/forms.py:333 apps/transactions/models.py:199
#: apps/transactions/models.py:387 apps/transactions/models.py:568
msgid "Notes"
msgstr ""
@@ -594,14 +639,6 @@ msgstr ""
msgid "Amount Received"
msgstr ""
#: apps/dca/models.py:169
msgid "Expense Transaction"
msgstr ""
#: apps/dca/models.py:177
msgid "Income Transaction"
msgstr ""
#: apps/dca/models.py:184
msgid "DCA Entry"
msgstr ""
@@ -622,15 +659,15 @@ msgstr ""
msgid "DCA strategy deleted successfully"
msgstr ""
#: apps/dca/views.py:163
#: apps/dca/views.py:161
msgid "Entry added successfully"
msgstr ""
#: apps/dca/views.py:190
#: apps/dca/views.py:188
msgid "Entry updated successfully"
msgstr ""
#: apps/dca/views.py:216
#: apps/dca/views.py:214
msgid "Entry deleted successfully"
msgstr ""
@@ -640,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 ""
@@ -708,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 ""
@@ -724,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 ""
@@ -741,14 +787,14 @@ msgid "Operator"
msgstr ""
#: apps/rules/forms.py:161 apps/rules/forms.py:174 apps/rules/models.py:25
#: apps/rules/models.py:240 apps/transactions/models.py:139
#: apps/transactions/models.py:293 apps/transactions/models.py:496
#: apps/rules/models.py:240 apps/transactions/models.py:183
#: apps/transactions/models.py:343 apps/transactions/models.py:546
msgid "Type"
msgstr ""
#: apps/rules/forms.py:162 apps/rules/forms.py:175 apps/rules/models.py:26
#: apps/rules/models.py:244 apps/transactions/filters.py:23
#: apps/transactions/models.py:141 templates/cotton/transaction/item.html:21
#: apps/transactions/models.py:185 templates/cotton/transaction/item.html:21
#: templates/cotton/transaction/item.html:31
#: templates/transactions/widgets/paid_toggle_button.html:12
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:16
@@ -758,41 +804,41 @@ msgstr ""
#: apps/rules/forms.py:164 apps/rules/forms.py:177 apps/rules/models.py:28
#: apps/rules/models.py:252 apps/transactions/forms.py:66
#: apps/transactions/forms.py:322 apps/transactions/forms.py:492
#: apps/transactions/models.py:143 apps/transactions/models.py:311
#: apps/transactions/models.py:520
#: apps/transactions/models.py:187 apps/transactions/models.py:361
#: apps/transactions/models.py:570
msgid "Reference Date"
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:148
#: apps/transactions/models.py:501
#: apps/rules/models.py:256 apps/transactions/models.py:192
#: apps/transactions/models.py:551 templates/insights/fragments/sankey.html:85
msgid "Amount"
msgstr ""
#: apps/rules/forms.py:166 apps/rules/forms.py:179 apps/rules/models.py:11
#: apps/rules/models.py:30 apps/rules/models.py:260
#: apps/transactions/forms.py:325 apps/transactions/models.py:153
#: apps/transactions/models.py:295 apps/transactions/models.py:504
#: apps/transactions/forms.py:325 apps/transactions/models.py:197
#: apps/transactions/models.py:345 apps/transactions/models.py:554
msgid "Description"
msgstr ""
#: apps/rules/forms.py:169 apps/rules/forms.py:184 apps/rules/models.py:268
#: apps/transactions/models.py:192
#: apps/transactions/models.py:236
msgid "Internal Note"
msgstr ""
#: apps/rules/forms.py:170 apps/rules/forms.py:185 apps/rules/models.py:272
#: apps/transactions/models.py:194
#: apps/transactions/models.py:238
msgid "Internal ID"
msgstr ""
#: apps/rules/forms.py:172 apps/rules/forms.py:181 apps/rules/models.py:34
#: apps/rules/models.py:276 apps/transactions/filters.py:81
#: apps/transactions/forms.py:55 apps/transactions/forms.py:486
#: apps/transactions/forms.py:731 apps/transactions/models.py:117
#: apps/transactions/models.py:170 apps/transactions/models.py:333
#: apps/transactions/models.py:515 templates/entities/fragments/list.html:5
#: templates/entities/pages/index.html:4 templates/includes/navbar.html:107
#: 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:110
msgid "Entities"
msgstr ""
@@ -946,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 ""
@@ -974,14 +1020,6 @@ msgstr ""
msgid "More"
msgstr ""
#: apps/transactions/forms.py:266
msgid "From Account"
msgstr ""
#: apps/transactions/forms.py:271
msgid "To Account"
msgstr ""
#: apps/transactions/forms.py:278
msgid "From Amount"
msgstr ""
@@ -995,10 +1033,6 @@ msgstr ""
msgid "Transfer"
msgstr ""
#: apps/transactions/forms.py:413
msgid "From and To accounts must be different."
msgstr ""
#: apps/transactions/forms.py:610
msgid "Tag name"
msgstr ""
@@ -1019,44 +1053,44 @@ msgstr ""
msgid "End date should be after the start date"
msgstr ""
#: apps/transactions/models.py:68
#: apps/transactions/models.py:112
msgid "Mute"
msgstr ""
#: apps/transactions/models.py:73
#: apps/transactions/models.py:117
msgid ""
"Deactivated categories won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:78
#: apps/transactions/models.py:122
msgid "Transaction Category"
msgstr ""
#: apps/transactions/models.py:79
#: apps/transactions/models.py:123
msgid "Transaction Categories"
msgstr ""
#: apps/transactions/models.py:92
#: apps/transactions/models.py:136
msgid ""
"Deactivated tags won't be able to be selected when creating new transactions"
msgstr ""
#: apps/transactions/models.py:97 apps/transactions/models.py:98
#: apps/transactions/models.py:141 apps/transactions/models.py:142
msgid "Transaction Tags"
msgstr ""
#: apps/transactions/models.py:111
#: apps/transactions/models.py:155
msgid ""
"Deactivated entities won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:116
#: apps/transactions/models.py:160
msgid "Entity"
msgstr ""
#: apps/transactions/models.py:126
#: apps/transactions/models.py:170
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: templates/calendar_view/fragments/list.html:52
@@ -1066,7 +1100,7 @@ msgstr ""
msgid "Income"
msgstr ""
#: apps/transactions/models.py:127
#: apps/transactions/models.py:171
#: templates/calendar_view/fragments/list.html:46
#: templates/calendar_view/fragments/list.html:48
#: templates/calendar_view/fragments/list.html:56
@@ -1075,123 +1109,135 @@ msgstr ""
msgid "Expense"
msgstr ""
#: apps/transactions/models.py:181 apps/transactions/models.py:340
#: apps/transactions/models.py:225 apps/transactions/models.py:390
msgid "Installment Plan"
msgstr ""
#: apps/transactions/models.py:190 apps/transactions/models.py:541
#: apps/transactions/models.py:234 apps/transactions/models.py:591
msgid "Recurring Transaction"
msgstr ""
#: apps/transactions/models.py:198
#: apps/transactions/models.py:242
msgid "Deleted"
msgstr ""
#: apps/transactions/models.py:203
#: apps/transactions/models.py:247
msgid "Deleted At"
msgstr ""
#: apps/transactions/models.py:211
#: apps/transactions/models.py:255
msgid "Transaction"
msgstr ""
#: apps/transactions/models.py:212 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
msgid "Transactions"
msgstr ""
#: apps/transactions/models.py:282
#: apps/transactions/models.py:323 templates/tags/fragments/table.html:53
msgid "No tags"
msgstr ""
#: apps/transactions/models.py:324
msgid "No category"
msgstr ""
#: apps/transactions/models.py:326
msgid "No description"
msgstr ""
#: apps/transactions/models.py:332
msgid "Yearly"
msgstr ""
#: apps/transactions/models.py:283 apps/users/models.py:26
#: apps/transactions/models.py:333 apps/users/models.py:26
#: templates/includes/navbar.html:26
msgid "Monthly"
msgstr ""
#: apps/transactions/models.py:284
#: apps/transactions/models.py:334
msgid "Weekly"
msgstr ""
#: apps/transactions/models.py:285
#: apps/transactions/models.py:335
msgid "Daily"
msgstr ""
#: apps/transactions/models.py:298
#: apps/transactions/models.py:348
msgid "Number of Installments"
msgstr ""
#: apps/transactions/models.py:303
#: apps/transactions/models.py:353
msgid "Installment Start"
msgstr ""
#: apps/transactions/models.py:304
#: apps/transactions/models.py:354
msgid "The installment number to start counting from"
msgstr ""
#: apps/transactions/models.py:309 apps/transactions/models.py:524
#: apps/transactions/models.py:359 apps/transactions/models.py:574
msgid "Start Date"
msgstr ""
#: apps/transactions/models.py:313 apps/transactions/models.py:525
#: apps/transactions/models.py:363 apps/transactions/models.py:575
msgid "End Date"
msgstr ""
#: apps/transactions/models.py:318
#: apps/transactions/models.py:368
msgid "Recurrence"
msgstr ""
#: apps/transactions/models.py:321
#: apps/transactions/models.py:371
msgid "Installment Amount"
msgstr ""
#: apps/transactions/models.py:341 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"
msgstr ""
#: apps/transactions/models.py:483
#: apps/transactions/models.py:533
msgid "day(s)"
msgstr ""
#: apps/transactions/models.py:484
#: apps/transactions/models.py:534
msgid "week(s)"
msgstr ""
#: apps/transactions/models.py:485
#: apps/transactions/models.py:535
msgid "month(s)"
msgstr ""
#: apps/transactions/models.py:486
#: apps/transactions/models.py:536
msgid "year(s)"
msgstr ""
#: apps/transactions/models.py:488
#: apps/transactions/models.py:538
#: templates/recurring_transactions/fragments/list.html:24
msgid "Paused"
msgstr ""
#: apps/transactions/models.py:527
#: apps/transactions/models.py:577
msgid "Recurrence Type"
msgstr ""
#: apps/transactions/models.py:530
#: apps/transactions/models.py:580
msgid "Recurrence Interval"
msgstr ""
#: apps/transactions/models.py:534
#: apps/transactions/models.py:584
msgid "Last Generated Date"
msgstr ""
#: apps/transactions/models.py:537
#: apps/transactions/models.py:587
msgid "Last Generated Reference Date"
msgstr ""
#: apps/transactions/models.py:542 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"
@@ -1207,35 +1253,35 @@ msgstr ""
msgid "%(value)s is not a non-negative number"
msgstr ""
#: apps/transactions/views/actions.py:23
#: apps/transactions/views/actions.py:24
#, python-format
msgid "%(count)s transaction marked as paid"
msgid_plural "%(count)s transactions marked as paid"
msgstr[0] ""
msgstr[1] ""
#: apps/transactions/views/actions.py:47
#: apps/transactions/views/actions.py:48
#, python-format
msgid "%(count)s transaction marked as not paid"
msgid_plural "%(count)s transactions marked as not paid"
msgstr[0] ""
msgstr[1] ""
#: apps/transactions/views/actions.py:71
#: apps/transactions/views/actions.py:72
#, python-format
msgid "%(count)s transaction deleted successfully"
msgid_plural "%(count)s transactions deleted successfully"
msgstr[0] ""
msgstr[1] ""
#: apps/transactions/views/actions.py:95
#: apps/transactions/views/actions.py:96
#, python-format
msgid "%(count)s transaction restored successfully"
msgid_plural "%(count)s transactions restored successfully"
msgstr[0] ""
msgstr[1] ""
#: apps/transactions/views/actions.py:130
#: apps/transactions/views/actions.py:131
#, python-format
msgid "%(count)s transaction duplicated successfully"
msgid_plural "%(count)s transactions duplicated successfully"
@@ -1496,7 +1542,7 @@ msgstr ""
#: templates/account_groups/fragments/list.html:36
#: templates/accounts/fragments/list.html:41
#: templates/categories/fragments/table.html:29
#: templates/cotton/transaction/item.html:127
#: templates/cotton/transaction/item.html:130
#: templates/cotton/ui/transactions_action_bar.html:49
#: templates/currencies/fragments/list.html:37
#: templates/dca/fragments/strategy/details.html:67
@@ -1518,8 +1564,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:43
#: templates/accounts/fragments/list.html:48
#: templates/categories/fragments/table.html:36
#: templates/cotton/transaction/item.html:142
#: templates/cotton/transaction/item.html:161
#: templates/cotton/transaction/item.html:145
#: templates/cotton/transaction/item.html:164
#: templates/cotton/ui/deleted_transactions_action_bar.html:55
#: templates/cotton/ui/transactions_action_bar.html:86
#: templates/currencies/fragments/list.html:44
@@ -1544,8 +1590,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:47
#: templates/accounts/fragments/list.html:52
#: templates/categories/fragments/table.html:41
#: templates/cotton/transaction/item.html:146
#: templates/cotton/transaction/item.html:165
#: templates/cotton/transaction/item.html:149
#: templates/cotton/transaction/item.html:168
#: templates/cotton/ui/deleted_transactions_action_bar.html:57
#: templates/cotton/ui/transactions_action_bar.html:88
#: templates/currencies/fragments/list.html:48
@@ -1573,8 +1619,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:48
#: templates/accounts/fragments/list.html:53
#: templates/categories/fragments/table.html:42
#: templates/cotton/transaction/item.html:147
#: templates/cotton/transaction/item.html:166
#: templates/cotton/transaction/item.html:150
#: templates/cotton/transaction/item.html:169
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
#: templates/cotton/ui/transactions_action_bar.html:89
#: templates/currencies/fragments/list.html:49
@@ -1595,8 +1641,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:49
#: templates/accounts/fragments/list.html:54
#: templates/categories/fragments/table.html:43
#: templates/cotton/transaction/item.html:148
#: templates/cotton/transaction/item.html:167
#: templates/cotton/transaction/item.html:151
#: templates/cotton/transaction/item.html:170
#: templates/currencies/fragments/list.html:50
#: templates/dca/fragments/strategy/details.html:82
#: templates/dca/fragments/strategy/list.html:48
@@ -1647,7 +1693,7 @@ msgstr ""
msgid "Is Asset"
msgstr ""
#: templates/accounts/fragments/list.html:70
#: templates/accounts/fragments/list.html:69
msgid "No accounts"
msgstr ""
@@ -1725,12 +1771,16 @@ msgstr ""
msgid "Select"
msgstr ""
#: templates/cotton/transaction/item.html:134
#: templates/cotton/transaction/item.html:56
msgid "DCA"
msgstr ""
#: templates/cotton/transaction/item.html:137
#: templates/cotton/ui/transactions_action_bar.html:78
msgid "Duplicate"
msgstr ""
#: templates/cotton/transaction/item.html:155
#: templates/cotton/transaction/item.html:158
#: templates/cotton/ui/deleted_transactions_action_bar.html:47
msgid "Restore"
msgstr ""
@@ -2030,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
@@ -2060,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 ""
@@ -2197,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 ""
@@ -2274,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 ""
@@ -2575,10 +2667,6 @@ msgstr ""
msgid "Edit tag"
msgstr ""
#: templates/tags/fragments/table.html:53
msgid "No tags"
msgstr ""
#: templates/transactions/fragments/add.html:5
#: templates/transactions/pages/add.html:5
msgid "New transaction"
@@ -2642,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 ""

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-09 17:27-0300\n"
"PO-Revision-Date: 2025-02-09 17:30-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"
@@ -25,7 +25,7 @@ msgstr "Nome do grupo"
#: apps/accounts/forms.py:40 apps/accounts/forms.py:96
#: apps/currencies/forms.py:53 apps/currencies/forms.py:91
#: apps/currencies/forms.py:142 apps/dca/forms.py:41 apps/dca/forms.py:93
#: apps/currencies/forms.py:142 apps/dca/forms.py:49 apps/dca/forms.py:224
#: apps/import_app/forms.py:34 apps/rules/forms.py:45 apps/rules/forms.py:87
#: apps/rules/forms.py:359 apps/transactions/forms.py:190
#: apps/transactions/forms.py:257 apps/transactions/forms.py:581
@@ -35,9 +35,9 @@ msgid "Update"
msgstr "Atualizar"
#: apps/accounts/forms.py:48 apps/accounts/forms.py:104
#: apps/common/widgets/tom_select.py:12 apps/currencies/forms.py:61
#: apps/common/widgets/tom_select.py:13 apps/currencies/forms.py:61
#: apps/currencies/forms.py:99 apps/currencies/forms.py:150
#: apps/dca/forms.py:49 apps/dca/forms.py:102 apps/import_app/forms.py:42
#: apps/dca/forms.py:57 apps/dca/forms.py:232 apps/import_app/forms.py:42
#: apps/rules/forms.py:53 apps/rules/forms.py:95 apps/rules/forms.py:367
#: apps/transactions/forms.py:174 apps/transactions/forms.py:199
#: apps/transactions/forms.py:589 apps/transactions/forms.py:632
@@ -69,30 +69,32 @@ msgstr "Grupo da Conta"
msgid "New balance"
msgstr "Novo saldo"
#: apps/accounts/forms.py:119 apps/rules/forms.py:168 apps/rules/forms.py:183
#: apps/rules/models.py:32 apps/rules/models.py:280
#: apps/transactions/forms.py:39 apps/transactions/forms.py:291
#: apps/transactions/forms.py:298 apps/transactions/forms.py:478
#: apps/transactions/forms.py:723 apps/transactions/models.py:159
#: apps/transactions/models.py:328 apps/transactions/models.py:508
#: apps/accounts/forms.py:119 apps/dca/forms.py:85 apps/dca/forms.py:92
#: apps/rules/forms.py:168 apps/rules/forms.py:183 apps/rules/models.py:32
#: apps/rules/models.py:280 apps/transactions/forms.py:39
#: apps/transactions/forms.py:291 apps/transactions/forms.py:298
#: apps/transactions/forms.py:478 apps/transactions/forms.py:723
#: apps/transactions/models.py:203 apps/transactions/models.py:378
#: apps/transactions/models.py:558
msgid "Category"
msgstr "Categoria"
#: apps/accounts/forms.py:126 apps/rules/forms.py:171 apps/rules/forms.py:180
#: apps/rules/models.py:33 apps/rules/models.py:284
#: apps/transactions/filters.py:74 apps/transactions/forms.py:47
#: apps/transactions/forms.py:307 apps/transactions/forms.py:315
#: apps/transactions/forms.py:471 apps/transactions/forms.py:716
#: apps/transactions/models.py:165 apps/transactions/models.py:330
#: apps/transactions/models.py:512 templates/includes/navbar.html:105
#: templates/tags/fragments/list.html:5 templates/tags/pages/index.html:4
#: apps/accounts/forms.py:126 apps/dca/forms.py:101 apps/dca/forms.py:109
#: apps/rules/forms.py:171 apps/rules/forms.py:180 apps/rules/models.py:33
#: apps/rules/models.py:284 apps/transactions/filters.py:74
#: apps/transactions/forms.py:47 apps/transactions/forms.py:307
#: 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:108 templates/tags/fragments/list.html:5
#: templates/tags/pages/index.html:4
msgid "Tags"
msgstr "Tags"
#: apps/accounts/models.py:9 apps/accounts/models.py:21 apps/dca/models.py:14
#: apps/import_app/models.py:14 apps/rules/models.py:10
#: apps/transactions/models.py:67 apps/transactions/models.py:87
#: apps/transactions/models.py:106
#: apps/transactions/models.py:111 apps/transactions/models.py:131
#: apps/transactions/models.py:150
#: templates/account_groups/fragments/list.html:25
#: templates/accounts/fragments/list.html:25
#: templates/categories/fragments/table.html:16
@@ -113,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"
@@ -157,15 +159,15 @@ msgstr ""
#: apps/accounts/models.py:59 apps/rules/forms.py:160 apps/rules/forms.py:173
#: apps/rules/models.py:24 apps/rules/models.py:236
#: apps/transactions/forms.py:59 apps/transactions/forms.py:463
#: apps/transactions/forms.py:708 apps/transactions/models.py:132
#: apps/transactions/models.py:288 apps/transactions/models.py:490
#: apps/transactions/forms.py:708 apps/transactions/models.py:176
#: apps/transactions/models.py:338 apps/transactions/models.py:540
msgid "Account"
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
@@ -236,13 +238,13 @@ msgstr "Dados da entidade inválidos. Forneça um ID ou nome."
msgid "Either 'date' or 'reference_date' must be provided."
msgstr "É necessário fornecer “date” ou “reference_date”."
#: apps/common/fields/forms/dynamic_select.py:127
#: apps/common/fields/forms/dynamic_select.py:163
#: apps/common/fields/forms/dynamic_select.py:131
#: apps/common/fields/forms/dynamic_select.py:167
msgid "Error creating new instance"
msgstr "Erro criando nova instância"
#: apps/common/fields/forms/grouped_select.py:24
#: apps/common/widgets/tom_select.py:91 apps/common/widgets/tom_select.py:94
#: apps/common/widgets/tom_select.py:92 apps/common/widgets/tom_select.py:95
msgid "Ungrouped"
msgstr "Não agrupado"
@@ -334,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"
@@ -341,18 +344,18 @@ msgstr "Hoje"
msgid "Now"
msgstr "Agora"
#: apps/common/widgets/tom_select.py:10
#: apps/common/widgets/tom_select.py:11
msgid "Remove"
msgstr "Remover"
#: apps/common/widgets/tom_select.py:14
#: apps/common/widgets/tom_select.py:15
#: templates/mini_tools/unit_price_calculator.html:174
#: templates/monthly_overview/pages/overview.html:172
#: templates/transactions/pages/transactions.html:17
msgid "Clear"
msgstr "Limpar"
#: apps/common/widgets/tom_select.py:15
#: apps/common/widgets/tom_select.py:16
msgid "No results..."
msgstr "Sem resultados..."
@@ -367,7 +370,7 @@ msgstr "Sufixo"
#: apps/currencies/forms.py:69 apps/dca/models.py:156 apps/rules/forms.py:163
#: apps/rules/forms.py:176 apps/rules/models.py:27 apps/rules/models.py:248
#: apps/transactions/forms.py:63 apps/transactions/forms.py:319
#: apps/transactions/models.py:142
#: apps/transactions/models.py:186
#: templates/dca/fragments/strategy/details.html:52
#: templates/exchange_rates/fragments/table.html:10
#: templates/exchange_rates_services/fragments/table.html:10
@@ -388,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
@@ -418,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"
@@ -446,8 +449,8 @@ msgstr "Nome do Serviço"
msgid "Service Type"
msgstr "Tipo de Serviço"
#: apps/currencies/models.py:107 apps/transactions/models.py:71
#: apps/transactions/models.py:90 apps/transactions/models.py:109
#: apps/currencies/models.py:107 apps/transactions/models.py:115
#: apps/transactions/models.py:134 apps/transactions/models.py:153
#: templates/categories/fragments/list.html:21
#: templates/entities/fragments/list.html:21
#: templates/recurring_transactions/fragments/list.html:21
@@ -573,6 +576,48 @@ msgstr "Serviço apagado com sucesso"
msgid "Services queued successfully"
msgstr "Serviços marcados para execução com sucesso"
#: apps/dca/forms.py:65 apps/dca/forms.py:164
msgid "Create transaction"
msgstr "Criar transação"
#: apps/dca/forms.py:70 apps/transactions/forms.py:266
msgid "From Account"
msgstr "Conta de origem"
#: apps/dca/forms.py:76 apps/transactions/forms.py:271
msgid "To Account"
msgstr "Conta de destino"
#: apps/dca/forms.py:116 apps/dca/models.py:169
msgid "Expense Transaction"
msgstr "Transação de saída"
#: apps/dca/forms.py:120 apps/dca/forms.py:130
msgid "Type to search for a transaction to link to this entry"
msgstr "Digite para buscar uma transação para conectar à esta entrada"
#: apps/dca/forms.py:126 apps/dca/models.py:177
msgid "Income Transaction"
msgstr "Transação de entrada"
#: apps/dca/forms.py:210
msgid "Link transaction"
msgstr "Conectar transação"
#: apps/dca/forms.py:279 apps/dca/forms.py:280 apps/dca/forms.py:285
#: apps/dca/forms.py:289
msgid "You must provide an account."
msgstr "Você deve informar uma conta."
#: apps/dca/forms.py:294 apps/transactions/forms.py:413
msgid "From and To accounts must be different."
msgstr "As contas De e Para devem ser diferentes."
#: apps/dca/forms.py:308
#, python-format
msgid "DCA for %(strategy_name)s"
msgstr "CMP para %(strategy_name)s"
#: apps/dca/models.py:17
msgid "Target Currency"
msgstr "Moeda de destino"
@@ -583,8 +628,8 @@ msgstr "Moeda de pagamento"
#: apps/dca/models.py:27 apps/dca/models.py:179 apps/rules/forms.py:167
#: apps/rules/forms.py:182 apps/rules/models.py:31 apps/rules/models.py:264
#: apps/transactions/forms.py:333 apps/transactions/models.py:155
#: apps/transactions/models.py:337 apps/transactions/models.py:518
#: apps/transactions/forms.py:333 apps/transactions/models.py:199
#: apps/transactions/models.py:387 apps/transactions/models.py:568
msgid "Notes"
msgstr "Notas"
@@ -608,14 +653,6 @@ msgstr "Quantia paga"
msgid "Amount Received"
msgstr "Quantia recebida"
#: apps/dca/models.py:169
msgid "Expense Transaction"
msgstr "Transação de saída"
#: apps/dca/models.py:177
msgid "Income Transaction"
msgstr "Transação de entrada"
#: apps/dca/models.py:184
msgid "DCA Entry"
msgstr "Entrada CMP"
@@ -636,15 +673,15 @@ msgstr "Estratégia CMP atualizada com sucesso"
msgid "DCA strategy deleted successfully"
msgstr "Estratégia CMP apagada com sucesso"
#: apps/dca/views.py:163
#: apps/dca/views.py:161
msgid "Entry added successfully"
msgstr "Entrada adicionada com sucesso"
#: apps/dca/views.py:190
#: apps/dca/views.py:188
msgid "Entry updated successfully"
msgstr "Entrada atualizada com sucesso"
#: apps/dca/views.py:216
#: apps/dca/views.py:214
msgid "Entry deleted successfully"
msgstr "Entrada apagada com sucesso"
@@ -654,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"
@@ -722,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"
@@ -738,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"
@@ -755,14 +801,14 @@ msgid "Operator"
msgstr "Operador"
#: apps/rules/forms.py:161 apps/rules/forms.py:174 apps/rules/models.py:25
#: apps/rules/models.py:240 apps/transactions/models.py:139
#: apps/transactions/models.py:293 apps/transactions/models.py:496
#: apps/rules/models.py:240 apps/transactions/models.py:183
#: apps/transactions/models.py:343 apps/transactions/models.py:546
msgid "Type"
msgstr "Tipo"
#: apps/rules/forms.py:162 apps/rules/forms.py:175 apps/rules/models.py:26
#: apps/rules/models.py:244 apps/transactions/filters.py:23
#: apps/transactions/models.py:141 templates/cotton/transaction/item.html:21
#: apps/transactions/models.py:185 templates/cotton/transaction/item.html:21
#: templates/cotton/transaction/item.html:31
#: templates/transactions/widgets/paid_toggle_button.html:12
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:16
@@ -772,41 +818,41 @@ msgstr "Pago"
#: apps/rules/forms.py:164 apps/rules/forms.py:177 apps/rules/models.py:28
#: apps/rules/models.py:252 apps/transactions/forms.py:66
#: apps/transactions/forms.py:322 apps/transactions/forms.py:492
#: apps/transactions/models.py:143 apps/transactions/models.py:311
#: apps/transactions/models.py:520
#: apps/transactions/models.py:187 apps/transactions/models.py:361
#: apps/transactions/models.py:570
msgid "Reference Date"
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:148
#: apps/transactions/models.py:501
#: apps/rules/models.py:256 apps/transactions/models.py:192
#: apps/transactions/models.py:551 templates/insights/fragments/sankey.html:85
msgid "Amount"
msgstr "Quantia"
#: apps/rules/forms.py:166 apps/rules/forms.py:179 apps/rules/models.py:11
#: apps/rules/models.py:30 apps/rules/models.py:260
#: apps/transactions/forms.py:325 apps/transactions/models.py:153
#: apps/transactions/models.py:295 apps/transactions/models.py:504
#: apps/transactions/forms.py:325 apps/transactions/models.py:197
#: apps/transactions/models.py:345 apps/transactions/models.py:554
msgid "Description"
msgstr "Descrição"
#: apps/rules/forms.py:169 apps/rules/forms.py:184 apps/rules/models.py:268
#: apps/transactions/models.py:192
#: apps/transactions/models.py:236
msgid "Internal Note"
msgstr "Nota Interna"
#: apps/rules/forms.py:170 apps/rules/forms.py:185 apps/rules/models.py:272
#: apps/transactions/models.py:194
#: apps/transactions/models.py:238
msgid "Internal ID"
msgstr "ID Interna"
#: apps/rules/forms.py:172 apps/rules/forms.py:181 apps/rules/models.py:34
#: apps/rules/models.py:276 apps/transactions/filters.py:81
#: apps/transactions/forms.py:55 apps/transactions/forms.py:486
#: apps/transactions/forms.py:731 apps/transactions/models.py:117
#: apps/transactions/models.py:170 apps/transactions/models.py:333
#: apps/transactions/models.py:515 templates/entities/fragments/list.html:5
#: templates/entities/pages/index.html:4 templates/includes/navbar.html:107
#: 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:110
msgid "Entities"
msgstr "Entidades"
@@ -962,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"
@@ -990,14 +1036,6 @@ msgstr "Quantia máxima"
msgid "More"
msgstr "Mais"
#: apps/transactions/forms.py:266
msgid "From Account"
msgstr "Conta de origem"
#: apps/transactions/forms.py:271
msgid "To Account"
msgstr "Conta de destino"
#: apps/transactions/forms.py:278
msgid "From Amount"
msgstr "Quantia de origem"
@@ -1011,10 +1049,6 @@ msgstr "Quantia de destino"
msgid "Transfer"
msgstr "Transferir"
#: apps/transactions/forms.py:413
msgid "From and To accounts must be different."
msgstr "As contas De e Para devem ser diferentes."
#: apps/transactions/forms.py:610
msgid "Tag name"
msgstr "Nome da Tag"
@@ -1035,11 +1069,11 @@ msgstr "As categorias silenciadas não serão contabilizadas em seu total mensal
msgid "End date should be after the start date"
msgstr "Data final deve ser após data inicial"
#: apps/transactions/models.py:68
#: apps/transactions/models.py:112
msgid "Mute"
msgstr "Silenciada"
#: apps/transactions/models.py:73
#: apps/transactions/models.py:117
msgid ""
"Deactivated categories won't be able to be selected when creating new "
"transactions"
@@ -1047,25 +1081,25 @@ msgstr ""
"As categorias desativadas não poderão ser selecionadas ao criar novas "
"transações"
#: apps/transactions/models.py:78
#: apps/transactions/models.py:122
msgid "Transaction Category"
msgstr "Categoria da Transação"
#: apps/transactions/models.py:79
#: apps/transactions/models.py:123
msgid "Transaction Categories"
msgstr "Categorias da Trasanção"
#: apps/transactions/models.py:92
#: apps/transactions/models.py:136
msgid ""
"Deactivated tags won't be able to be selected when creating new transactions"
msgstr ""
"As tags desativadas não poderão ser selecionadas ao criar novas transações"
#: apps/transactions/models.py:97 apps/transactions/models.py:98
#: apps/transactions/models.py:141 apps/transactions/models.py:142
msgid "Transaction Tags"
msgstr "Tags da Transação"
#: apps/transactions/models.py:111
#: apps/transactions/models.py:155
msgid ""
"Deactivated entities won't be able to be selected when creating new "
"transactions"
@@ -1073,11 +1107,11 @@ msgstr ""
"As entidades desativadas não poderão ser selecionadas ao criar novas "
"transações"
#: apps/transactions/models.py:116
#: apps/transactions/models.py:160
msgid "Entity"
msgstr "Entidade"
#: apps/transactions/models.py:126
#: apps/transactions/models.py:170
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: templates/calendar_view/fragments/list.html:52
@@ -1087,7 +1121,7 @@ msgstr "Entidade"
msgid "Income"
msgstr "Renda"
#: apps/transactions/models.py:127
#: apps/transactions/models.py:171
#: templates/calendar_view/fragments/list.html:46
#: templates/calendar_view/fragments/list.html:48
#: templates/calendar_view/fragments/list.html:56
@@ -1096,123 +1130,135 @@ msgstr "Renda"
msgid "Expense"
msgstr "Despesa"
#: apps/transactions/models.py:181 apps/transactions/models.py:340
#: apps/transactions/models.py:225 apps/transactions/models.py:390
msgid "Installment Plan"
msgstr "Parcelamento"
#: apps/transactions/models.py:190 apps/transactions/models.py:541
#: apps/transactions/models.py:234 apps/transactions/models.py:591
msgid "Recurring Transaction"
msgstr "Transação Recorrente"
#: apps/transactions/models.py:198
#: apps/transactions/models.py:242
msgid "Deleted"
msgstr "Apagado"
#: apps/transactions/models.py:203
#: apps/transactions/models.py:247
msgid "Deleted At"
msgstr "Apagado Em"
#: apps/transactions/models.py:211
#: apps/transactions/models.py:255
msgid "Transaction"
msgstr "Transação"
#: apps/transactions/models.py:212 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
msgid "Transactions"
msgstr "Transações"
#: apps/transactions/models.py:282
#: apps/transactions/models.py:323 templates/tags/fragments/table.html:53
msgid "No tags"
msgstr "Nenhuma tag"
#: apps/transactions/models.py:324
msgid "No category"
msgstr "Sem categoria"
#: apps/transactions/models.py:326
msgid "No description"
msgstr "Sem descrição"
#: apps/transactions/models.py:332
msgid "Yearly"
msgstr "Anual"
#: apps/transactions/models.py:283 apps/users/models.py:26
#: apps/transactions/models.py:333 apps/users/models.py:26
#: templates/includes/navbar.html:26
msgid "Monthly"
msgstr "Mensal"
#: apps/transactions/models.py:284
#: apps/transactions/models.py:334
msgid "Weekly"
msgstr "Semanal"
#: apps/transactions/models.py:285
#: apps/transactions/models.py:335
msgid "Daily"
msgstr "Diária"
#: apps/transactions/models.py:298
#: apps/transactions/models.py:348
msgid "Number of Installments"
msgstr "Número de Parcelas"
#: apps/transactions/models.py:303
#: apps/transactions/models.py:353
msgid "Installment Start"
msgstr "Parcela inicial"
#: apps/transactions/models.py:304
#: apps/transactions/models.py:354
msgid "The installment number to start counting from"
msgstr "O número da parcela a partir do qual se inicia a contagem"
#: apps/transactions/models.py:309 apps/transactions/models.py:524
#: apps/transactions/models.py:359 apps/transactions/models.py:574
msgid "Start Date"
msgstr "Data de Início"
#: apps/transactions/models.py:313 apps/transactions/models.py:525
#: apps/transactions/models.py:363 apps/transactions/models.py:575
msgid "End Date"
msgstr "Data Final"
#: apps/transactions/models.py:318
#: apps/transactions/models.py:368
msgid "Recurrence"
msgstr "Recorrência"
#: apps/transactions/models.py:321
#: apps/transactions/models.py:371
msgid "Installment Amount"
msgstr "Valor da Parcela"
#: apps/transactions/models.py:341 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"
msgstr "Parcelamentos"
#: apps/transactions/models.py:483
#: apps/transactions/models.py:533
msgid "day(s)"
msgstr "dia(s)"
#: apps/transactions/models.py:484
#: apps/transactions/models.py:534
msgid "week(s)"
msgstr "semana(s)"
#: apps/transactions/models.py:485
#: apps/transactions/models.py:535
msgid "month(s)"
msgstr "mês(es)"
#: apps/transactions/models.py:486
#: apps/transactions/models.py:536
msgid "year(s)"
msgstr "ano(s)"
#: apps/transactions/models.py:488
#: apps/transactions/models.py:538
#: templates/recurring_transactions/fragments/list.html:24
msgid "Paused"
msgstr "Pausado"
#: apps/transactions/models.py:527
#: apps/transactions/models.py:577
msgid "Recurrence Type"
msgstr "Tipo de recorrência"
#: apps/transactions/models.py:530
#: apps/transactions/models.py:580
msgid "Recurrence Interval"
msgstr "Intervalo de recorrência"
#: apps/transactions/models.py:534
#: apps/transactions/models.py:584
msgid "Last Generated Date"
msgstr "Última data gerada"
#: apps/transactions/models.py:537
#: apps/transactions/models.py:587
msgid "Last Generated Reference Date"
msgstr "Última data de referência gerada"
#: apps/transactions/models.py:542 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"
@@ -1228,35 +1274,35 @@ msgstr "%(value)s tem muitas casas decimais. O máximo é 30."
msgid "%(value)s is not a non-negative number"
msgstr "%(value)s não é um número positivo"
#: apps/transactions/views/actions.py:23
#: apps/transactions/views/actions.py:24
#, python-format
msgid "%(count)s transaction marked as paid"
msgid_plural "%(count)s transactions marked as paid"
msgstr[0] "%(count)s transação marcada como paga"
msgstr[1] "%(count)s transações marcadas como paga"
#: apps/transactions/views/actions.py:47
#: apps/transactions/views/actions.py:48
#, python-format
msgid "%(count)s transaction marked as not paid"
msgid_plural "%(count)s transactions marked as not paid"
msgstr[0] "%(count)s transação marcada como não paga"
msgstr[1] "%(count)s transações marcadas como não paga"
#: apps/transactions/views/actions.py:71
#: apps/transactions/views/actions.py:72
#, python-format
msgid "%(count)s transaction deleted successfully"
msgid_plural "%(count)s transactions deleted successfully"
msgstr[0] "%(count)s transação apagada com sucesso"
msgstr[1] "%(count)s transações apagadas com sucesso"
#: apps/transactions/views/actions.py:95
#: apps/transactions/views/actions.py:96
#, python-format
msgid "%(count)s transaction restored successfully"
msgid_plural "%(count)s transactions restored successfully"
msgstr[0] "%(count)s transação restaurada com sucesso"
msgstr[1] "%(count)s transações restauradas com sucesso"
#: apps/transactions/views/actions.py:130
#: apps/transactions/views/actions.py:131
#, python-format
msgid "%(count)s transaction duplicated successfully"
msgid_plural "%(count)s transactions duplicated successfully"
@@ -1517,7 +1563,7 @@ msgstr "Ações"
#: templates/account_groups/fragments/list.html:36
#: templates/accounts/fragments/list.html:41
#: templates/categories/fragments/table.html:29
#: templates/cotton/transaction/item.html:127
#: templates/cotton/transaction/item.html:130
#: templates/cotton/ui/transactions_action_bar.html:49
#: templates/currencies/fragments/list.html:37
#: templates/dca/fragments/strategy/details.html:67
@@ -1539,8 +1585,8 @@ msgstr "Editar"
#: templates/account_groups/fragments/list.html:43
#: templates/accounts/fragments/list.html:48
#: templates/categories/fragments/table.html:36
#: templates/cotton/transaction/item.html:142
#: templates/cotton/transaction/item.html:161
#: templates/cotton/transaction/item.html:145
#: templates/cotton/transaction/item.html:164
#: templates/cotton/ui/deleted_transactions_action_bar.html:55
#: templates/cotton/ui/transactions_action_bar.html:86
#: templates/currencies/fragments/list.html:44
@@ -1565,8 +1611,8 @@ msgstr "Apagar"
#: templates/account_groups/fragments/list.html:47
#: templates/accounts/fragments/list.html:52
#: templates/categories/fragments/table.html:41
#: templates/cotton/transaction/item.html:146
#: templates/cotton/transaction/item.html:165
#: templates/cotton/transaction/item.html:149
#: templates/cotton/transaction/item.html:168
#: templates/cotton/ui/deleted_transactions_action_bar.html:57
#: templates/cotton/ui/transactions_action_bar.html:88
#: templates/currencies/fragments/list.html:48
@@ -1594,8 +1640,8 @@ msgstr "Tem certeza?"
#: templates/account_groups/fragments/list.html:48
#: templates/accounts/fragments/list.html:53
#: templates/categories/fragments/table.html:42
#: templates/cotton/transaction/item.html:147
#: templates/cotton/transaction/item.html:166
#: templates/cotton/transaction/item.html:150
#: templates/cotton/transaction/item.html:169
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
#: templates/cotton/ui/transactions_action_bar.html:89
#: templates/currencies/fragments/list.html:49
@@ -1616,8 +1662,8 @@ msgstr "Você não será capaz de reverter isso!"
#: templates/account_groups/fragments/list.html:49
#: templates/accounts/fragments/list.html:54
#: templates/categories/fragments/table.html:43
#: templates/cotton/transaction/item.html:148
#: templates/cotton/transaction/item.html:167
#: templates/cotton/transaction/item.html:151
#: templates/cotton/transaction/item.html:170
#: templates/currencies/fragments/list.html:50
#: templates/dca/fragments/strategy/details.html:82
#: templates/dca/fragments/strategy/list.html:48
@@ -1668,7 +1714,7 @@ msgstr "Editar conta"
msgid "Is Asset"
msgstr "É ativo"
#: templates/accounts/fragments/list.html:70
#: templates/accounts/fragments/list.html:69
msgid "No accounts"
msgstr "Nenhuma conta"
@@ -1746,12 +1792,16 @@ msgstr "Buscar"
msgid "Select"
msgstr "Selecionar"
#: templates/cotton/transaction/item.html:134
#: templates/cotton/transaction/item.html:56
msgid "DCA"
msgstr "CMP"
#: templates/cotton/transaction/item.html:137
#: templates/cotton/ui/transactions_action_bar.html:78
msgid "Duplicate"
msgstr "Duplicar"
#: templates/cotton/transaction/item.html:155
#: templates/cotton/transaction/item.html:158
#: templates/cotton/ui/deleted_transactions_action_bar.html:47
msgid "Restore"
msgstr "Restaurar"
@@ -2052,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
@@ -2082,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"
@@ -2221,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"
@@ -2299,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"
@@ -2605,10 +2697,6 @@ msgstr "Adicionar tag"
msgid "Edit tag"
msgstr "Editar tag"
#: templates/tags/fragments/table.html:53
msgid "No tags"
msgstr "Nenhuma tag"
#: templates/transactions/fragments/add.html:5
#: templates/transactions/pages/add.html:5
msgid "New transaction"
@@ -2673,10 +2761,10 @@ 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"
#~ msgstr "Tags"
#, fuzzy
#~| msgid "Start Date"
@@ -2872,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

@@ -44,13 +44,16 @@
{# Description#}
<div class="mb-2 mb-lg-1 text-white tw-text-base">
{% spaceless %}
<span>{{ transaction.description }}</span>
<span class="{% if transaction.description %}me-2{% endif %}">{{ transaction.description }}</span>
{% if transaction.installment_plan and transaction.installment_id %}
<span
class="badge text-bg-secondary ms-2">{{ transaction.installment_id }}/{{ transaction.installment_plan.installment_total_number }}</span>
class="badge text-bg-secondary">{{ transaction.installment_id }}/{{ transaction.installment_plan.installment_total_number }}</span>
{% endif %}
{% if transaction.recurring_transaction %}
<span class="text-primary tw-text-xs ms-2"><i class="fa-solid fa-arrows-rotate fa-fw"></i></span>
<span class="text-primary tw-text-xs"><i class="fa-solid fa-arrows-rotate fa-fw"></i></span>
{% endif %}
{% if transaction.dca_expense_entries.all or transaction.dca_income_entries.all %}
<span class="badge text-bg-secondary">{% trans 'DCA' %}</span>
{% endif %}
{% endspaceless %}
</div>

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

@@ -0,0 +1,120 @@
{% load i18n %}
{% if type == 'account' %}
<div class="show-loading" hx-get="{% url 'insights_sankey_by_account' %}" hx-trigger="updated from:window" hx-swap="outerHTML" hx-include="#picker-form, #picker-type">
{% 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>
var data = {{ sankey_data|safe }};
function setupSankeyChart(chartId = 'sankeyChart') {
function formatCurrency(value, currency) {
return new Intl.NumberFormat(undefined, {
minimumFractionDigits: currency.decimal_places,
maximumFractionDigits: currency.decimal_places
}).format(value);
}
// Create labels object mapping node IDs to display names
const labels = data.nodes.reduce((acc, node) => {
acc[node.id] = node.name;
return acc;
}, {});
// 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,
}
}
}
};
// 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();
});
}
</script>

View File

@@ -0,0 +1,94 @@
{% extends "layouts/base.html" %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block content %}
<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)/>
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="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>
{% endblock %}

View File

@@ -10,6 +10,4 @@ until [ -f /tmp/migrations_complete ]; do
sleep 2
done
rm -f /tmp/migrations_complete
exec python manage.py procrastinate worker

View File

@@ -24,6 +24,7 @@
"babel-loader": "^8.2.3",
"bootstrap": "^5.3.3",
"chart.js": "^4.4.6",
"chartjs-chart-sankey": "^0.14.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
"core-js": "^3.20.3",
@@ -3235,6 +3236,15 @@
"pnpm": ">=8"
}
},
"node_modules/chartjs-chart-sankey": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/chartjs-chart-sankey/-/chartjs-chart-sankey-0.14.0.tgz",
"integrity": "sha512-MrU3lE73TE9kALy4MjWFlfcwf4R1EN/DBvhHxmv9n4AHap//JLKjlJTLIZwHsUjDsYo0B8PuMkrJODwfirEZUA==",
"license": "MIT",
"peerDependencies": {
"chart.js": ">=3.3.0"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",

View File

@@ -37,6 +37,7 @@
"babel-loader": "^8.2.3",
"bootstrap": "^5.3.3",
"chart.js": "^4.4.6",
"chartjs-chart-sankey": "^0.14.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
"core-js": "^3.20.3",

View File

@@ -1,2 +1,5 @@
import Chart from 'chart.js/auto';
import {SankeyController, Flow} from 'chartjs-chart-sankey';
Chart.register(SankeyController, Flow);
window.Chart = Chart;

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);
};

View File

@@ -3,71 +3,84 @@ import * as Popper from "@popperjs/core";
window.TomSelect = function createDynamicTomSelect(element) {
// Basic configuration
const config = {
plugins: {},
// Basic configuration
const config = {
plugins: {},
// Extract 'create' option from data attribute
create: element.dataset.create === 'true',
copyClassesToDropdown: true,
allowEmptyOption: element.dataset.allowEmptyOption === 'true',
render: {
no_results: function () {
return `<div class="no-results">${element.dataset.txtNoResults || 'No results...'}</div>`;
// Extract 'create' option from data attribute
create: element.dataset.create === 'true',
copyClassesToDropdown: true,
allowEmptyOption: element.dataset.allowEmptyOption === 'true',
render: {
no_results: function () {
return `<div class="no-results">${element.dataset.txtNoResults || 'No results...'}</div>`;
},
option_create: function (data, escape) {
return `<div class="create">${element.dataset.txtCreate || 'Add'} <strong>${escape(data.input)}</strong>&hellip;</div>`;
},
},
option_create: function(data, escape) {
return `<div class="create">${element.dataset.txtCreate || 'Add'} <strong>${escape(data.input)}</strong>&hellip;</div>`;
},
},
onInitialize: function () {
this.popper = Popper.createPopper(this.control, this.dropdown, {
placement: "bottom-start",
modifiers: [
{
name: "sameWidth",
enabled: true,
fn: ({state}) => {
state.styles.popper.width = `${state.rects.reference.width}px`;
onInitialize: function () {
this.popper = Popper.createPopper(this.control, this.dropdown, {
placement: "bottom-start",
modifiers: [
{
name: "sameWidth",
enabled: true,
fn: ({state}) => {
state.styles.popper.width = `${state.rects.reference.width}px`;
},
phase: "beforeWrite",
requires: ["computeStyles"],
},
phase: "beforeWrite",
requires: ["computeStyles"],
},
{
name: 'flip',
options: {
fallbackPlacements: ['top-start'],
{
name: 'flip',
options: {
fallbackPlacements: ['top-start'],
},
},
},
]
]
});
});
},
onDropdownOpen: function () {
this.popper.update();
}
};
},
onDropdownOpen: function () {
this.popper.update();
}
};
if (element.dataset.checkboxes === 'true') {
config.plugins.checkbox_options = {
if (element.dataset.checkboxes === 'true') {
config.plugins.checkbox_options = {
'checkedClassNames': ['ts-checked'],
'uncheckedClassNames': ['ts-unchecked'],
};
}
}
if (element.dataset.clearButton === 'true') {
config.plugins.clear_button = {
if (element.dataset.clearButton === 'true') {
config.plugins.clear_button = {
'title': element.dataset.txtClear || 'Clear',
};
}
}
if (element.dataset.removeButton === 'true') {
config.plugins.remove_button = {
if (element.dataset.removeButton === 'true') {
config.plugins.remove_button = {
'title': element.dataset.txtRemove || 'Remove',
};
}
}
// Create and return the TomSelect instance
return new TomSelect(element, config);
if (element.dataset.load) {
config.load = function (query, callback) {
let url = element.dataset.load + '?q=' + encodeURIComponent(query);
fetch(url)
.then(response => response.json())
.then(json => {
callback(json);
}).catch(() => {
callback();
});
};
}
// Create and return the TomSelect instance
return new TomSelect(element, config);
};

View File

@@ -78,6 +78,7 @@
.show-loading.htmx-request {
position: relative;
top: 0;
min-height: 100px;
&::before {
content: "";

View File

@@ -50,7 +50,7 @@ select[multiple] {
padding: 0 8px !important;
}
.transaction:has(input[type="checkbox"]:checked) > .transaction-item {
.transaction:has(input[type="checkbox"]:checked) > div > .transaction-item {
background-color: $primary-bg-subtle-dark;
}