diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 2a704fa5..038881a1 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -60,12 +60,6 @@ jobs:
- name: Install dependencies
run: uv sync --frozen
- # Import code reads/writes the Docker image's paths directly
- - name: Create container paths used by the app
- run: |
- sudo mkdir -p /usr/src/app/temp /usr/src/app/import_presets
- sudo chown -R "$(id -u):$(id -g)" /usr/src/app
-
- name: Check for missing migrations
working-directory: app
run: uv run python manage.py makemigrations --check --dry-run
diff --git a/.gitignore b/.gitignore
index 93ba54a4..7c4160ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -168,3 +168,4 @@ postgres_data/
# Private local uploads
app/attachments/
+app/temp/
diff --git a/README.md b/README.md
index 9154a734..ba019170 100644
--- a/README.md
+++ b/README.md
@@ -192,6 +192,8 @@ To create the first user, open the container's console using Unraid's UI, by cli
| ENABLE_SOFT_DELETE | true\|false | false | Whether to enable transactions soft delete, if enabled, deleted transactions will remain in the database. Useful for imports and avoiding duplicate entries. |
| KEEP_DELETED_TRANSACTIONS_FOR | int | 365 | Time in days to keep soft deleted transactions for. If 0, will keep all transactions indefinitely. Only works if ENABLE_SOFT_DELETE is true. |
| TASK_WORKERS | int | 1 | How many workers to have for async tasks. One should be enough for most use cases |
+| IMPORT_TEMP_ROOT | string | /usr/src/app/temp | Where uploaded import files wait to be processed. Defaults to the `temp` folder inside the app folder. Must be writable and shared between the web and task processes. |
+| CACHE_ROOT | string | /var/tmp | The writable folder the file-based caches are stored in. |
| DEMO | true\|false | false | If demo mode is enabled. |
| ADMIN_EMAIL | string | None | Automatically creates an admin account with this email. Must have `ADMIN_PASSWORD` also set. |
| ADMIN_PASSWORD | string | None | Automatically creates an admin account with this password. Must have `ADMIN_EMAIL` also set. |
diff --git a/app/WYGIWYH/settings.py b/app/WYGIWYH/settings.py
index c87e0f93..9adb0d72 100644
--- a/app/WYGIWYH/settings.py
+++ b/app/WYGIWYH/settings.py
@@ -78,6 +78,7 @@ INSTALLED_APPS = [
"apps.rules.apps.RulesConfig",
"apps.calendar_view.apps.CalendarViewConfig",
"apps.dca.apps.DcaConfig",
+ "apps.investments.apps.InvestmentsConfig",
"pwa",
"allauth",
"allauth.account",
@@ -314,6 +315,9 @@ LOCALE_PATHS = [BASE_DIR / "locale"]
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "static_files"
ATTACHMENT_MEDIA_ROOT = BASE_DIR / "attachments"
+IMPORT_PRESETS_ROOT = BASE_DIR / "import_presets"
+# Web and task workers must see the same folder: uploads wait here to be processed
+IMPORT_TEMP_ROOT = Path(os.getenv("IMPORT_TEMP_ROOT") or BASE_DIR / "temp").resolve()
STATICFILES_DIRS = [
ROOT_DIR / "frontend" / "build",
@@ -325,15 +329,17 @@ STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
+CACHE_ROOT = Path(os.getenv("CACHE_ROOT") or "/var/tmp").resolve()
+
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
- "LOCATION": "/var/tmp/django_cache",
+ "LOCATION": CACHE_ROOT / "django_cache",
},
# Kept apart from "default": cachalot fills it and FileBasedCache culls random entries past MAX_ENTRIES
"update_check": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
- "LOCATION": "/var/tmp/django_cache_update_check",
+ "LOCATION": CACHE_ROOT / "django_cache_update_check",
},
}
diff --git a/app/WYGIWYH/urls.py b/app/WYGIWYH/urls.py
index 10cd8fc6..fb8c9cf0 100644
--- a/app/WYGIWYH/urls.py
+++ b/app/WYGIWYH/urls.py
@@ -94,6 +94,7 @@ urlpatterns = [
path("", include("apps.rules.urls")),
path("", include("apps.calendar_view.urls")),
path("", include("apps.dca.urls")),
+ path("", include("apps.investments.urls")),
path("", include("apps.mini_tools.urls")),
path("", include("apps.import_app.urls")),
path("", include("apps.export_app.urls")),
diff --git a/app/apps/accounts/forms.py b/app/apps/accounts/forms.py
index 59030301..abb3cf8d 100644
--- a/app/apps/accounts/forms.py
+++ b/app/apps/accounts/forms.py
@@ -1,6 +1,7 @@
import calendar
-from apps.accounts.models import Account, AccountGroup
+from apps.accounts.models import Account, AccountGroup, InvestmentClass
+from apps.common.middleware.thread_local import get_current_user
from apps.common.fields.forms.dynamic_select import (
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
@@ -9,12 +10,16 @@ from apps.common.widgets.crispy.daisyui import Switch
from apps.common.widgets.crispy.submit import NoClassSubmit
from apps.common.widgets.datepicker import AirDatePickerInput, AirMonthYearPickerInput
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
-from apps.common.widgets.tom_select import TomSelect
+from apps.common.widgets.tom_select import TomSelect, TomSelectMultiple
from apps.currencies.models import Currency
-from apps.transactions.models import TransactionCategory, TransactionTag
+from apps.transactions.models import (
+ TransactionCategory,
+ TransactionEntity,
+ TransactionTag,
+)
from crispy_forms.bootstrap import FormActions
from crispy_forms.helper import FormHelper
-from crispy_forms.layout import Column, Field, Layout, Row
+from crispy_forms.layout import HTML, Column, Div, Field, Layout, Row
from django import forms
from django.db.models import Q
from django.utils import timezone
@@ -51,6 +56,30 @@ class AccountGroupForm(forms.ModelForm):
)
+class InvestmentClassForm(forms.ModelForm):
+ class Meta:
+ model = InvestmentClass
+ fields = ["name", "target"]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.helper = FormHelper()
+ self.helper.form_tag = False
+ self.helper.form_method = "post"
+ self.helper.layout = Layout(
+ "name",
+ "target",
+ FormActions(
+ NoClassSubmit(
+ "submit",
+ _("Update") if self.instance.pk else _("Add"),
+ css_class="btn btn-primary",
+ ),
+ ),
+ )
+
+
class AccountForm(forms.ModelForm):
group = DynamicModelChoiceField(
create_field="name",
@@ -59,6 +88,14 @@ class AccountForm(forms.ModelForm):
required=False,
)
+ investment_class = DynamicModelChoiceField(
+ create_field="name",
+ label=_("Asset class"),
+ model=InvestmentClass,
+ required=False,
+ help_text=Account._meta.get_field("investment_class").help_text,
+ )
+
class Meta:
model = Account
fields = [
@@ -68,16 +105,39 @@ class AccountForm(forms.ModelForm):
"exchange_currency",
"is_asset",
"is_archived",
+ "is_investment",
+ "investment_class",
+ "income_entities",
+ "income_categories",
+ "income_tags",
]
widgets = {
"currency": TomSelect(),
"exchange_currency": TomSelect(),
+ "income_entities": TomSelectMultiple(),
+ "income_categories": TomSelectMultiple(),
+ "income_tags": TomSelectMultiple(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["group"].queryset = AccountGroup.objects.all()
+ self.fields["investment_class"].queryset = InvestmentClass.all_objects.filter(
+ Q(owner=get_current_user())
+ | Q(owner=None)
+ # Keep a class from the account's owner when it's shared with us
+ | Q(accounts=self.instance.id or 0)
+ ).distinct()
+ selectable = Q(active=True)
+ if self.instance.id:
+ selectable |= Q(investment_accounts=self.instance.id)
+ for name, model in (
+ ("income_entities", TransactionEntity),
+ ("income_categories", TransactionCategory),
+ ("income_tags", TransactionTag),
+ ):
+ self.fields[name].queryset = model.objects.filter(selectable).distinct()
if self.instance.id:
qs = Currency.objects.filter(
@@ -101,6 +161,21 @@ class AccountForm(forms.ModelForm):
Switch("is_archived"),
"currency",
"exchange_currency",
+ Switch("is_investment"),
+ # Only relevant for investments: shown while the switch above is on
+ Div(
+ "investment_class",
+ "income_entities",
+ "income_categories",
+ "income_tags",
+ css_id="investment_income",
+ css_class="" if self.instance.is_investment else "hidden",
+ ),
+ HTML(
+ '
'
+ ),
)
if self.instance and self.instance.pk:
diff --git a/app/apps/accounts/migrations/0017_account_income_entity_account_is_investment.py b/app/apps/accounts/migrations/0017_account_income_entity_account_is_investment.py
new file mode 100644
index 00000000..56649971
--- /dev/null
+++ b/app/apps/accounts/migrations/0017_account_income_entity_account_is_investment.py
@@ -0,0 +1,25 @@
+# Generated by Django 5.2.17 on 2026-11-05 01:12
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0016_account_untracked_by'),
+ ('transactions', '0052_transfer_templates'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='account',
+ name='income_entity',
+ field=models.ForeignKey(blank=True, help_text="Income transactions with this entity (dividends, interest, rent) count towards this investment's return.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='investment_accounts', to='transactions.transactionentity', verbose_name='Income entity'),
+ ),
+ migrations.AddField(
+ model_name='account',
+ name='is_investment',
+ field=models.BooleanField(default=False, help_text='Show this account in the Investments tracker. Set an exchange currency to price it by units (stocks, ETFs, crypto); leave it empty to track it by value (fixed income, property, pensions).', verbose_name='Investment'),
+ ),
+ ]
diff --git a/app/apps/accounts/migrations/0018_income_entities.py b/app/apps/accounts/migrations/0018_income_entities.py
new file mode 100644
index 00000000..fa4c91e7
--- /dev/null
+++ b/app/apps/accounts/migrations/0018_income_entities.py
@@ -0,0 +1,43 @@
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+def copy_income_entity(apps, schema_editor):
+ Account = apps.get_model("accounts", "Account")
+ for account in Account.objects.exclude(income_entity=None):
+ account.income_entities.add(account.income_entity_id)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("accounts", "0017_account_income_entity_account_is_investment"),
+ ("transactions", "0052_transfer_templates"),
+ ]
+
+ operations = [
+ # Free the "investment_accounts" reverse name for the new field
+ migrations.AlterField(
+ model_name="account",
+ name="income_entity",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="+",
+ to="transactions.transactionentity",
+ ),
+ ),
+ migrations.AddField(
+ model_name="account",
+ name="income_entities",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="Income transactions with any of these entities (dividends, interest, rent) count towards this investment's return.",
+ related_name="investment_accounts",
+ to="transactions.transactionentity",
+ verbose_name="Income entities",
+ ),
+ ),
+ migrations.RunPython(copy_income_entity, migrations.RunPython.noop),
+ migrations.RemoveField(model_name="account", name="income_entity"),
+ ]
diff --git a/app/apps/accounts/migrations/0019_income_categories_and_tags.py b/app/apps/accounts/migrations/0019_income_categories_and_tags.py
new file mode 100644
index 00000000..9a30e0b2
--- /dev/null
+++ b/app/apps/accounts/migrations/0019_income_categories_and_tags.py
@@ -0,0 +1,29 @@
+# Generated by Django 5.2.17 on 2026-11-07 04:07
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0018_income_entities'),
+ ('transactions', '0052_transfer_templates'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='account',
+ name='income_categories',
+ field=models.ManyToManyField(blank=True, help_text="Transactions in this account with any of these categories are its result (interest, taxes, fees). On accounts tracked by value, everything else that isn't a transfer counts as money coming in or going out.", related_name='investment_accounts', to='transactions.transactioncategory', verbose_name='Income categories'),
+ ),
+ migrations.AddField(
+ model_name='account',
+ name='income_tags',
+ field=models.ManyToManyField(blank=True, help_text='Same as income categories, for transactions with any of these tags.', related_name='investment_accounts', to='transactions.transactiontag', verbose_name='Income tags'),
+ ),
+ migrations.AlterField(
+ model_name='account',
+ name='income_entities',
+ field=models.ManyToManyField(blank=True, help_text="Transactions with any of these entities count towards this investment's return: in this account, and income paid into other accounts (dividends, rent).", related_name='investment_accounts', to='transactions.transactionentity', verbose_name='Income entities'),
+ ),
+ ]
diff --git a/app/apps/accounts/migrations/0020_investment_classes.py b/app/apps/accounts/migrations/0020_investment_classes.py
new file mode 100644
index 00000000..274abe34
--- /dev/null
+++ b/app/apps/accounts/migrations/0020_investment_classes.py
@@ -0,0 +1,38 @@
+# Generated by Django 5.2.17 on 2026-11-08 03:26
+
+import django.core.validators
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0019_income_categories_and_tags'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='InvestmentClass',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255, verbose_name='Name')),
+ ('target', models.DecimalField(blank=True, decimal_places=2, help_text='Share of your investments you want in this class. Leave it empty to leave the class out of the targets.', max_digits=5, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='Target allocation (%)')),
+ ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'verbose_name': 'Asset class',
+ 'verbose_name_plural': 'Asset classes',
+ 'db_table': 'investment_classes',
+ 'ordering': ['name', 'id'],
+ 'unique_together': {('owner', 'name')},
+ },
+ ),
+ migrations.AddField(
+ model_name='account',
+ name='investment_class',
+ field=models.ForeignKey(blank=True, help_text='Groups investments in the allocation charts, e.g. Fixed income, Stocks, Real estate.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='accounts', to='accounts.investmentclass', verbose_name='Asset class'),
+ ),
+ ]
diff --git a/app/apps/accounts/models.py b/app/apps/accounts/models.py
index 55c064c0..79d5e383 100644
--- a/app/apps/accounts/models.py
+++ b/app/apps/accounts/models.py
@@ -1,10 +1,17 @@
from django.conf import settings
from django.core.exceptions import ValidationError
+from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
+from django.template.defaultfilters import floatformat
from django.utils.translation import gettext_lazy as _
from apps.common.middleware.thread_local import get_current_user
-from apps.common.models import SharedObject, SharedObjectManager
+from apps.common.models import (
+ OwnedObject,
+ OwnedObjectManager,
+ SharedObject,
+ SharedObjectManager,
+)
from apps.transactions.models import Transaction
@@ -25,6 +32,54 @@ class AccountGroup(SharedObject):
return self.name
+class InvestmentClass(OwnedObject):
+ name = models.CharField(max_length=255, verbose_name=_("Name"))
+ target = models.DecimalField(
+ max_digits=5,
+ decimal_places=2,
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(0), MaxValueValidator(100)],
+ verbose_name=_("Target allocation (%)"),
+ help_text=_(
+ "Share of your investments you want in this class. Leave it empty to "
+ "leave the class out of the targets."
+ ),
+ )
+
+ objects = OwnedObjectManager()
+ all_objects = models.Manager() # Unfiltered manager
+
+ def clean(self):
+ # Shared by the form and the API; owner is set on save, so neither can
+ # rely on Django's (owner, name) unique check.
+ others = InvestmentClass.all_objects.filter(
+ owner=self.owner or get_current_user()
+ ).exclude(pk=self.pk)
+ errors = {}
+ if self.name and others.filter(name=self.name).exists():
+ errors["name"] = _("You already have an asset class with this name.")
+ if self.target:
+ taken = others.aggregate(total=models.Sum("target"))["total"] or 0
+ if taken + self.target > 100:
+ errors["target"] = _(
+ "Targets can't add up to more than 100%%. Other classes already "
+ "take %(taken)s%%."
+ ) % {"taken": floatformat(taken, -2)}
+ if errors:
+ raise ValidationError(errors)
+
+ class Meta:
+ verbose_name = _("Asset class")
+ verbose_name_plural = _("Asset classes")
+ db_table = "investment_classes"
+ unique_together = (("owner", "name"),)
+ ordering = ["name", "id"]
+
+ def __str__(self):
+ return self.name
+
+
class Account(SharedObject):
name = models.CharField(max_length=255, verbose_name=_("Name"))
group = models.ForeignKey(
@@ -57,6 +112,58 @@ class Account(SharedObject):
"Asset accounts count towards your Net Worth, but not towards your month."
),
)
+ is_investment = models.BooleanField(
+ default=False,
+ verbose_name=_("Investment"),
+ help_text=_(
+ "Show this account in the Investments tracker. Set an exchange currency to "
+ "price it by units (stocks, ETFs, crypto); leave it empty to track it by "
+ "value (fixed income, property, pensions)."
+ ),
+ )
+ investment_class = models.ForeignKey(
+ InvestmentClass,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ related_name="accounts",
+ verbose_name=_("Asset class"),
+ help_text=_(
+ "Groups investments in the allocation charts, e.g. Fixed income, Stocks, "
+ "Real estate."
+ ),
+ )
+ income_entities = models.ManyToManyField(
+ "transactions.TransactionEntity",
+ blank=True,
+ related_name="investment_accounts",
+ verbose_name=_("Income entities"),
+ help_text=_(
+ "Transactions with any of these entities count towards this investment's "
+ "return: in this account, and income paid into other accounts (dividends, "
+ "rent)."
+ ),
+ )
+ income_categories = models.ManyToManyField(
+ "transactions.TransactionCategory",
+ blank=True,
+ related_name="investment_accounts",
+ verbose_name=_("Income categories"),
+ help_text=_(
+ "Transactions in this account with any of these categories are its result "
+ "(interest, taxes, fees). On accounts tracked by value, everything else "
+ "that isn't a transfer counts as money coming in or going out."
+ ),
+ )
+ income_tags = models.ManyToManyField(
+ "transactions.TransactionTag",
+ blank=True,
+ related_name="investment_accounts",
+ verbose_name=_("Income tags"),
+ help_text=_(
+ "Same as income categories, for transactions with any of these tags."
+ ),
+ )
is_archived = models.BooleanField(
default=False,
verbose_name=_("Archived"),
diff --git a/app/apps/api/serializers/accounts.py b/app/apps/api/serializers/accounts.py
index db95f256..02082ba1 100644
--- a/app/apps/api/serializers/accounts.py
+++ b/app/apps/api/serializers/accounts.py
@@ -1,9 +1,13 @@
+from copy import copy
+from decimal import Decimal
+
+from django.core.exceptions import ValidationError as DjangoValidationError
from django.db.models import Q
from rest_framework import serializers
from rest_framework.permissions import IsAuthenticated
+from apps.accounts.models import AccountGroup, Account, InvestmentClass
from apps.api.serializers.currencies import CurrencySerializer
-from apps.accounts.models import AccountGroup, Account
from apps.currencies.models import Currency
@@ -15,6 +19,24 @@ class AccountGroupSerializer(serializers.ModelSerializer):
fields = "__all__"
+class InvestmentClassSerializer(serializers.ModelSerializer):
+ permission_classes = [IsAuthenticated]
+
+ class Meta:
+ model = InvestmentClass
+ fields = ["id", "name", "target"]
+
+ def validate(self, attrs):
+ instance = copy(self.instance) if self.instance else InvestmentClass()
+ for name, value in attrs.items():
+ setattr(instance, name, value)
+ try:
+ instance.clean() # unique name and targets up to 100%
+ except DjangoValidationError as e:
+ raise serializers.ValidationError(e.message_dict)
+ return attrs
+
+
class AccountSerializer(serializers.ModelSerializer):
group = AccountGroupSerializer(read_only=True)
group_id = serializers.PrimaryKeyRelatedField(
@@ -36,6 +58,15 @@ class AccountSerializer(serializers.ModelSerializer):
allow_null=True,
)
+ investment_class = InvestmentClassSerializer(read_only=True)
+ investment_class_id = serializers.PrimaryKeyRelatedField(
+ queryset=InvestmentClass.objects.all(),
+ source="investment_class",
+ write_only=True,
+ allow_null=True,
+ required=False,
+ )
+
permission_classes = [IsAuthenticated]
class Meta:
@@ -50,6 +81,12 @@ class AccountSerializer(serializers.ModelSerializer):
"exchange_currency",
"exchange_currency_id",
"is_asset",
+ "is_investment",
+ "investment_class",
+ "investment_class_id",
+ "income_entities",
+ "income_categories",
+ "income_tags",
]
def __init__(self, *args, **kwargs):
@@ -58,6 +95,7 @@ class AccountSerializer(serializers.ModelSerializer):
if request and request.user.is_authenticated:
# Reload the queryset to get an updated version with the requesting user
self.fields["group_id"].queryset = AccountGroup.objects.all()
+ self.fields["investment_class_id"].queryset = InvestmentClass.objects.all()
def create(self, validated_data):
return Account.objects.create(**validated_data)
@@ -76,3 +114,44 @@ class AccountBalanceSerializer(serializers.Serializer):
projected_balance = serializers.DecimalField(max_digits=20, decimal_places=10)
currency = CurrencySerializer()
+
+
+class InvestmentPerformanceSerializer(serializers.Serializer):
+ """Serializer for an investment account's performance."""
+
+ currency = CurrencySerializer()
+ priced = serializers.BooleanField()
+ quantity = serializers.DecimalField(max_digits=42, decimal_places=30)
+ price = serializers.DecimalField(max_digits=42, decimal_places=30, allow_null=True)
+ price_date = serializers.DateTimeField(allow_null=True)
+ average_cost = serializers.DecimalField(
+ max_digits=42, decimal_places=30, allow_null=True
+ )
+ cost_basis = serializers.DecimalField(max_digits=42, decimal_places=30)
+ market_value = serializers.DecimalField(
+ max_digits=42, decimal_places=30, allow_null=True
+ )
+ unrealized = serializers.DecimalField(
+ max_digits=42, decimal_places=30, allow_null=True
+ )
+ realized = serializers.DecimalField(max_digits=42, decimal_places=30)
+ income = serializers.DecimalField(max_digits=42, decimal_places=30)
+ contributed = serializers.DecimalField(max_digits=42, decimal_places=30)
+ withdrawn = serializers.DecimalField(max_digits=42, decimal_places=30)
+ total_return = serializers.DecimalField(max_digits=42, decimal_places=30)
+ return_percentage = serializers.DecimalField(
+ max_digits=42, decimal_places=30, allow_null=True
+ )
+ estimated = serializers.BooleanField()
+ money_weighted_return = serializers.SerializerMethodField(
+ help_text="Return weighted by when money went in and out, in percent: "
+ "annual (XIRR) when the first contribution is a year old or more, "
+ "otherwise over the period held."
+ )
+ money_weighted_annualized = serializers.SerializerMethodField()
+
+ def get_money_weighted_return(self, perf) -> Decimal | None:
+ return perf.money_weighted and perf.money_weighted["percentage"]
+
+ def get_money_weighted_annualized(self, perf) -> bool | None:
+ return perf.money_weighted and perf.money_weighted["annualized"]
diff --git a/app/apps/api/tests/test_investment_classes.py b/app/apps/api/tests/test_investment_classes.py
new file mode 100644
index 00000000..00409afe
--- /dev/null
+++ b/app/apps/api/tests/test_investment_classes.py
@@ -0,0 +1,81 @@
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Permission
+from django.test import TestCase, override_settings
+from rest_framework.test import APIClient
+
+from apps.accounts.models import Account, InvestmentClass
+from apps.currencies.models import Currency
+
+
+@override_settings(
+ STORAGES={
+ "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
+ "staticfiles": {
+ "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
+ },
+ },
+ WHITENOISE_AUTOREFRESH=True,
+ DEMO=False,
+)
+class InvestmentClassAPITests(TestCase):
+ def setUp(self):
+ User = get_user_model()
+ perms = Permission.objects.filter(
+ codename__in=[
+ "add_investmentclass",
+ "change_investmentclass",
+ "delete_investmentclass",
+ "change_account",
+ ]
+ )
+ self.user = User.objects.create_user(email="u@test.com", password="pw")
+ self.other = User.objects.create_user(email="o@test.com", password="pw")
+ for user in (self.user, self.other):
+ user.user_permissions.set(perms)
+ self.api = APIClient()
+ self.api.force_authenticate(User.objects.get(pk=self.user.pk))
+
+ def test_crud_and_validation(self):
+ response = self.api.post(
+ "/api/investment-classes/", {"name": "Stocks", "target": "60"}, format="json"
+ )
+ self.assertEqual(response.status_code, 201, response.data)
+ stocks_id = response.data["id"]
+
+ # Same name again, and targets past 100%: rejected, not a server error
+ response = self.api.post("/api/investment-classes/", {"name": "Stocks"}, format="json")
+ self.assertEqual(response.status_code, 400)
+ self.assertIn("name", response.data)
+ response = self.api.post(
+ "/api/investment-classes/", {"name": "Bonds", "target": "50"}, format="json"
+ )
+ self.assertEqual(response.status_code, 400)
+ self.assertIn("target", response.data)
+
+ # Editing keeps its own target out of the sum
+ response = self.api.patch(
+ f"/api/investment-classes/{stocks_id}/", {"target": "100"}, format="json"
+ )
+ self.assertEqual(response.status_code, 200, response.data)
+
+ # Another user's names and targets don't count, and they can't see ours
+ other = APIClient()
+ other.force_authenticate(get_user_model().objects.get(pk=self.other.pk))
+ response = other.post(
+ "/api/investment-classes/", {"name": "Stocks", "target": "100"}, format="json"
+ )
+ self.assertEqual(response.status_code, 201, response.data)
+ self.assertEqual(other.get(f"/api/investment-classes/{stocks_id}/").status_code, 404)
+
+ def test_account_takes_a_class(self):
+ brl = Currency.objects.create(code="BRL", name="Real", decimal_places=2)
+ stocks = InvestmentClass.all_objects.create(name="Stocks", owner=self.user)
+ account = Account.all_objects.create(name="PETR4", currency=brl, owner=self.user)
+
+ response = self.api.patch(
+ f"/api/accounts/{account.id}/", {"investment_class_id": stocks.id}, format="json"
+ )
+ self.assertEqual(response.status_code, 200, response.data)
+ self.assertEqual(response.data["investment_class"]["name"], "Stocks")
+ response = self.api.get("/api/accounts/", {"investment_class": stocks.id})
+ self.assertEqual(response.data["count"], 1)
diff --git a/app/apps/api/urls.py b/app/apps/api/urls.py
index ee2d1fee..40d2b95e 100644
--- a/app/apps/api/urls.py
+++ b/app/apps/api/urls.py
@@ -12,6 +12,7 @@ router.register(r"installment-plans", views.InstallmentPlanViewSet)
router.register(r"recurring-transactions", views.RecurringTransactionViewSet)
router.register(r"account-groups", views.AccountGroupViewSet)
router.register(r"accounts", views.AccountViewSet)
+router.register(r"investment-classes", views.InvestmentClassViewSet)
router.register(r"currencies", views.CurrencyViewSet)
router.register(r"exchange-rates", views.ExchangeRateViewSet)
router.register(r"dca/strategies", views.DCAStrategyViewSet)
diff --git a/app/apps/api/views/accounts.py b/app/apps/api/views/accounts.py
index f7e44e1b..c30157b9 100644
--- a/app/apps/api/views/accounts.py
+++ b/app/apps/api/views/accounts.py
@@ -4,14 +4,17 @@ from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
-from apps.accounts.models import AccountGroup, Account
+from apps.accounts.models import AccountGroup, Account, InvestmentClass
from apps.accounts.services import get_account_balance
from apps.api.permissions import SHARED_OBJECT_PERMISSIONS
from apps.api.serializers import (
AccountGroupSerializer,
+ InvestmentClassSerializer,
AccountSerializer,
AccountBalanceSerializer,
+ InvestmentPerformanceSerializer,
)
+from apps.investments.services import get_holding_performance
class AccountGroupViewSet(viewsets.ModelViewSet):
@@ -32,12 +35,31 @@ class AccountGroupViewSet(viewsets.ModelViewSet):
return AccountGroup.objects.all()
+class InvestmentClassViewSet(viewsets.ModelViewSet):
+ """ViewSet for managing asset classes and their target allocation."""
+
+ queryset = InvestmentClass.objects.all()
+ serializer_class = InvestmentClassSerializer
+ filterset_fields = {"name": ["exact", "icontains"]}
+ search_fields = ["name"]
+ ordering_fields = "__all__"
+ ordering = ["id"]
+
+ def get_queryset(self):
+ return InvestmentClass.objects.all()
+
+
@extend_schema_view(
balance=extend_schema(
summary="Get account balance",
description="Returns the current and projected balance for the account, along with currency data.",
responses={200: AccountBalanceSerializer},
),
+ performance=extend_schema(
+ summary="Get investment performance",
+ description="Returns quantity, cost basis, market value, realized/unrealized gains, income and total return, in the account's exchange currency (or its own currency when it has none).",
+ responses={200: InvestmentPerformanceSerializer},
+ ),
)
class AccountViewSet(viewsets.ModelViewSet):
"""ViewSet for managing accounts."""
@@ -48,9 +70,11 @@ class AccountViewSet(viewsets.ModelViewSet):
filterset_fields = {
"name": ["exact", "icontains"],
"group": ["exact", "isnull"],
+ "investment_class": ["exact", "isnull"],
"currency": ["exact"],
"exchange_currency": ["exact", "isnull"],
"is_asset": ["exact"],
+ "is_investment": ["exact"],
"is_archived": ["exact"],
"owner": ["exact"],
}
@@ -60,7 +84,7 @@ class AccountViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Account.objects.all().select_related(
- "group", "currency", "exchange_currency"
+ "group", "currency", "exchange_currency", "investment_class"
)
@action(detail=True, methods=["get"], permission_classes=[IsAuthenticated])
@@ -80,3 +104,11 @@ class AccountViewSet(viewsets.ModelViewSet):
)
return Response(serializer.data)
+
+ @action(detail=True, methods=["get"], permission_classes=[IsAuthenticated])
+ def performance(self, request, pk=None):
+ """Get investment performance (cost basis, value, returns) for an account."""
+ account = self.get_object()
+ return Response(
+ InvestmentPerformanceSerializer(get_holding_performance(account)).data
+ )
diff --git a/app/apps/api/views/imports.py b/app/apps/api/views/imports.py
index f8e6edb7..9abb945d 100644
--- a/app/apps/api/views/imports.py
+++ b/app/apps/api/views/imports.py
@@ -1,3 +1,4 @@
+from django.conf import settings
from django.core.files.storage import FileSystemStorage
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view, inline_serializer
@@ -130,7 +131,7 @@ class ImportViewSet(viewsets.ViewSet):
uploaded_file = serializer.validated_data["file"]
# Save file to temp location
- fs = FileSystemStorage(location="/usr/src/app/temp")
+ fs = FileSystemStorage(location=settings.IMPORT_TEMP_ROOT)
filename = fs.save(uploaded_file.name, uploaded_file)
file_path = fs.path(filename)
diff --git a/app/apps/common/fields/forms/dynamic_select.py b/app/apps/common/fields/forms/dynamic_select.py
index 3a49517a..4259c542 100644
--- a/app/apps/common/fields/forms/dynamic_select.py
+++ b/app/apps/common/fields/forms/dynamic_select.py
@@ -21,11 +21,19 @@ class DynamicModelChoiceField(forms.ModelChoiceField):
super().__init__(queryset=self.queryset, *args, **kwargs)
self._created_instance = None
+ def _get(self, **lookup):
+ # The form's queryset may hold choices the default manager hides, like
+ # another user's object already set on a shared record
+ try:
+ return self.queryset.get(**lookup)
+ except self.model.DoesNotExist:
+ return self.model.objects.get(**lookup)
+
def to_python(self, value):
if value in self.empty_values:
return None
try:
- return self.model.objects.get(**{self.to_field_name: value})
+ return self._get(**{self.to_field_name: value})
except (ValueError, TypeError, self.model.DoesNotExist):
return value # Return the raw value; we'll handle creation in clean()
@@ -49,7 +57,7 @@ class DynamicModelChoiceField(forms.ModelChoiceField):
try:
if value.isdigit():
- return self.model.objects.get(id=value)
+ return self._get(id=value)
else:
raise self.model.DoesNotExist
except self.model.DoesNotExist:
diff --git a/app/apps/common/oauth_views.py b/app/apps/common/oauth_views.py
index 54733580..a73b3b84 100644
--- a/app/apps/common/oauth_views.py
+++ b/app/apps/common/oauth_views.py
@@ -88,7 +88,7 @@ def _dcr_initial_access_token_ok(request):
scheme, _, value = header.partition(" ")
if scheme.lower() != "bearer" or not value:
return False
- return hmac.compare_digest(value, expected)
+ return hmac.compare_digest(value.encode(), expected.encode())
@require_http_methods(["GET"])
diff --git a/app/apps/common/tests/test_oauth.py b/app/apps/common/tests/test_oauth.py
index b096ada9..7453c923 100644
--- a/app/apps/common/tests/test_oauth.py
+++ b/app/apps/common/tests/test_oauth.py
@@ -224,6 +224,18 @@ class DynamicClientRegistrationInitialAccessTokenTests(TestCase):
self.assertEqual(response.json()["error"], "invalid_token")
self.assertEqual(Application.objects.count(), 0)
+ def test_rejects_registration_with_non_ascii_initial_access_token(self):
+ response = self.client.post(
+ reverse("oauth-dynamic-client-registration"),
+ data=json.dumps({"redirect_uris": ["http://127.0.0.1:8765/callback"]}),
+ content_type="application/json",
+ HTTP_AUTHORIZATION="Bearer s3cret-iat\u00e9",
+ )
+
+ self.assertEqual(response.status_code, 401)
+ self.assertEqual(response.json()["error"], "invalid_token")
+ self.assertEqual(Application.objects.count(), 0)
+
def test_allows_registration_with_initial_access_token(self):
response = self.client.post(
reverse("oauth-dynamic-client-registration"),
diff --git a/app/apps/common/widgets/tom_select.py b/app/apps/common/widgets/tom_select.py
index 5c9b0548..0f22d106 100644
--- a/app/apps/common/widgets/tom_select.py
+++ b/app/apps/common/widgets/tom_select.py
@@ -1,3 +1,5 @@
+import json
+
from django.forms import SelectMultiple, widgets
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -142,6 +144,7 @@ class TransactionSelect(TomSelect):
def build_attrs(self, base_attrs, extra_attrs=None):
attrs = super().build_attrs(base_attrs, extra_attrs)
+ attrs["data-render"] = "transaction"
if self.load_income and self.load_expense:
attrs["data-load"] = reverse("transactions_search")
@@ -155,3 +158,13 @@ class TransactionSelect(TomSelect):
)
return attrs
+
+ def create_option(self, name, value, *args, **kwargs):
+ option = super().create_option(name, value, *args, **kwargs)
+ instance = getattr(value, "instance", None)
+ if instance is not None:
+ from apps.transactions.utils.select_search import transaction_option
+
+ # tom-select reads this to render the selected transaction like the search results
+ option["attrs"]["data-data"] = json.dumps(transaction_option(instance))
+ return option
diff --git a/app/apps/currencies/exchange_rates/fetcher.py b/app/apps/currencies/exchange_rates/fetcher.py
index 6b486304..c8450f70 100644
--- a/app/apps/currencies/exchange_rates/fetcher.py
+++ b/app/apps/currencies/exchange_rates/fetcher.py
@@ -166,6 +166,31 @@ class ExchangeRateFetcher:
return target_currencies_qs, exchange_currencies
+ @staticmethod
+ def _store_rate(service, from_currency, to_currency, rate):
+ now = timezone.now()
+ rates = ExchangeRate.objects.filter(
+ automatic=True, from_currency=from_currency, to_currency=to_currency
+ )
+ today = rates.filter(date__date=timezone.localdate(now)).order_by("-date")
+ existing = today.first() if service.one_per_day else None
+ if existing:
+ existing.rate, existing.date = rate, now
+ existing.save()
+ else:
+ ExchangeRate.objects.create(
+ automatic=True,
+ from_currency=from_currency,
+ to_currency=to_currency,
+ rate=rate,
+ date=now,
+ )
+ if service.max_rates:
+ stale = rates.order_by("-date").values_list("id", flat=True)[
+ service.max_rates :
+ ]
+ ExchangeRate.objects.filter(id__in=list(stale)).delete()
+
@staticmethod
def _fetch_service_rates(service: ExchangeRateService) -> None:
"""Fetch rates for a specific service"""
@@ -193,71 +218,15 @@ class ExchangeRateFetcher:
processed_pairs = set()
for from_currency, to_currency, rate in rates:
- # Create a unique identifier for this currency pair
+ if provider.rates_inverted:
+ from_currency, to_currency = to_currency, from_currency
pair_key = (from_currency.id, to_currency.id)
if pair_key in processed_pairs:
continue
-
- if provider.rates_inverted:
- # If rates are inverted, we need to swap currencies
- if service.singleton:
- # Try to get the last automatically created exchange rate
- exchange_rate = (
- ExchangeRate.objects.filter(
- automatic=True,
- from_currency=to_currency,
- to_currency=from_currency,
- )
- .order_by("-date")
- .first()
- )
- else:
- exchange_rate = None
-
- if not exchange_rate:
- ExchangeRate.objects.create(
- automatic=True,
- from_currency=to_currency,
- to_currency=from_currency,
- rate=rate,
- date=timezone.now(),
- )
- else:
- exchange_rate.rate = rate
- exchange_rate.date = timezone.now()
- exchange_rate.save()
-
- processed_pairs.add((to_currency.id, from_currency.id))
- else:
- # If rates are not inverted, we can use them as is
- if service.singleton:
- # Try to get the last automatically created exchange rate
- exchange_rate = (
- ExchangeRate.objects.filter(
- automatic=True,
- from_currency=from_currency,
- to_currency=to_currency,
- )
- .order_by("-date")
- .first()
- )
- else:
- exchange_rate = None
-
- if not exchange_rate:
- ExchangeRate.objects.create(
- automatic=True,
- from_currency=from_currency,
- to_currency=to_currency,
- rate=rate,
- date=timezone.now(),
- )
- else:
- exchange_rate.rate = rate
- exchange_rate.date = timezone.now()
- exchange_rate.save()
-
- processed_pairs.add((from_currency.id, to_currency.id))
+ ExchangeRateFetcher._store_rate(
+ service, from_currency, to_currency, rate
+ )
+ processed_pairs.add(pair_key)
service.last_fetch = timezone.now()
service.failure_count = 0
diff --git a/app/apps/currencies/forms.py b/app/apps/currencies/forms.py
index 3090e8d5..526368a2 100644
--- a/app/apps/currencies/forms.py
+++ b/app/apps/currencies/forms.py
@@ -112,7 +112,8 @@ class ExchangeRateServiceForm(forms.ModelForm):
"fetch_interval",
"target_currencies",
"target_accounts",
- "singleton",
+ "one_per_day",
+ "max_rates",
]
widgets = {"options": forms.Textarea(attrs={"rows": 2})}
@@ -149,7 +150,6 @@ class ExchangeRateServiceForm(forms.ModelForm):
"name",
"service_type",
Switch("is_active"),
- Switch("singleton"),
"api_key",
"options",
Row(
@@ -158,6 +158,8 @@ class ExchangeRateServiceForm(forms.ModelForm):
),
"target_currencies",
"target_accounts",
+ Switch("one_per_day"),
+ "max_rates",
)
if self.instance and self.instance.pk:
diff --git a/app/apps/currencies/migrations/0027_exchangerateservice_retention.py b/app/apps/currencies/migrations/0027_exchangerateservice_retention.py
new file mode 100644
index 00000000..abddedcf
--- /dev/null
+++ b/app/apps/currencies/migrations/0027_exchangerateservice_retention.py
@@ -0,0 +1,43 @@
+import django.core.validators
+from django.db import migrations, models
+
+
+def singleton_to_max_rates(apps, schema_editor):
+ Service = apps.get_model("currencies", "ExchangeRateService")
+ Service.objects.filter(singleton=True).update(max_rates=1)
+
+
+def max_rates_to_singleton(apps, schema_editor):
+ Service = apps.get_model("currencies", "ExchangeRateService")
+ Service.objects.filter(max_rates=1).update(singleton=True)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("currencies", "0026_exchangerateservice_options"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="exchangerateservice",
+ name="one_per_day",
+ field=models.BooleanField(
+ default=False,
+ help_text="Update the day's exchange rate on each fetch instead of adding a new one.",
+ verbose_name="One exchange rate per day",
+ ),
+ ),
+ migrations.AddField(
+ model_name="exchangerateservice",
+ name="max_rates",
+ field=models.PositiveIntegerField(
+ blank=True,
+ null=True,
+ validators=[django.core.validators.MinValueValidator(1)],
+ help_text="Keep at most this many automatic exchange rates per currency pair, deleting the oldest first. Leave empty to keep all.",
+ verbose_name="Exchange rates to keep",
+ ),
+ ),
+ migrations.RunPython(singleton_to_max_rates, max_rates_to_singleton),
+ migrations.RemoveField(model_name="exchangerateservice", name="singleton"),
+ ]
diff --git a/app/apps/currencies/models.py b/app/apps/currencies/models.py
index cd01d36b..6cf9c173 100644
--- a/app/apps/currencies/models.py
+++ b/app/apps/currencies/models.py
@@ -167,11 +167,20 @@ class ExchangeRateService(models.Model):
blank=True,
)
- singleton = models.BooleanField(
- verbose_name=_("Single exchange rate"),
+ one_per_day = models.BooleanField(
+ verbose_name=_("One exchange rate per day"),
default=False,
help_text=_(
- "Create one exchange rate and keep updating it. Avoids database clutter."
+ "Update the day's exchange rate on each fetch instead of adding a new one."
+ ),
+ )
+ max_rates = models.PositiveIntegerField(
+ verbose_name=_("Exchange rates to keep"),
+ null=True,
+ blank=True,
+ validators=[MinValueValidator(1)],
+ help_text=_(
+ "Keep at most this many automatic exchange rates per currency pair, deleting the oldest first. Leave empty to keep all."
),
)
diff --git a/app/apps/currencies/tests/test_automatic_exchange_rates.py b/app/apps/currencies/tests/test_automatic_exchange_rates.py
index c3e04d9b..8c10b874 100644
--- a/app/apps/currencies/tests/test_automatic_exchange_rates.py
+++ b/app/apps/currencies/tests/test_automatic_exchange_rates.py
@@ -1,10 +1,11 @@
+import datetime
from decimal import Decimal
from unittest.mock import patch, MagicMock
from django.test import TestCase
from django.utils import timezone
-from apps.currencies.models import Currency, ExchangeRateService
+from apps.currencies.models import Currency, ExchangeRate, ExchangeRateService
from apps.currencies.exchange_rates.fetcher import ExchangeRateFetcher
@@ -107,3 +108,42 @@ class ExchangeRateServiceFailureTrackingTests(TestCase):
self.service.refresh_from_db()
self.assertIsNotNone(self.service.last_fetch)
self.assertEqual(self.service.failure_count, 0)
+
+
+class ExchangeRateRetentionTests(TestCase):
+ def setUp(self):
+ self.usd = Currency.objects.create(code="USD", name="Dollar", decimal_places=2)
+ self.eur = Currency.objects.create(
+ code="EUR", name="Euro", decimal_places=2, exchange_currency=self.usd
+ )
+ self.service = ExchangeRateService.objects.create(
+ name="S", service_type=ExchangeRateService.ServiceType.FRANKFURTER
+ )
+
+ def fetch(self, rate, when):
+ with patch("django.utils.timezone.now", return_value=when):
+ ExchangeRateFetcher._store_rate(self.service, self.eur, self.usd, Decimal(rate))
+
+ def rates(self):
+ return list(
+ ExchangeRate.objects.order_by("date").values_list("rate", flat=True)
+ )
+
+ def test_keeps_every_fetch_by_default(self):
+ for hour in (1, 2, 3):
+ self.fetch(hour, timezone.now().replace(hour=hour))
+ self.assertEqual(len(self.rates()), 3)
+
+ def test_max_rates_deletes_the_oldest(self):
+ self.service.max_rates = 2
+ for hour in (1, 2, 3):
+ self.fetch(hour, timezone.now().replace(hour=hour))
+ self.assertEqual(self.rates(), [Decimal(2), Decimal(3)])
+
+ def test_one_per_day_updates_the_days_rate(self):
+ self.service.one_per_day = True
+ day = timezone.localtime().replace(hour=10)
+ self.fetch(1, day)
+ self.fetch(2, day.replace(hour=11))
+ self.fetch(3, day + datetime.timedelta(days=1))
+ self.assertEqual(self.rates(), [Decimal(2), Decimal(3)])
diff --git a/app/apps/dca/forms.py b/app/apps/dca/forms.py
index 39b73aca..03a9f539 100644
--- a/app/apps/dca/forms.py
+++ b/app/apps/dca/forms.py
@@ -12,7 +12,7 @@ from apps.dca.models import DCAEntry, DCAStrategy
from apps.transactions.models import Transaction, TransactionCategory, TransactionTag
from crispy_forms.bootstrap import AccordionGroup, FormActions, Accordion
from crispy_forms.helper import FormHelper
-from crispy_forms.layout import HTML, Column, Layout, Row
+from crispy_forms.layout import HTML, Column, Div, Layout, Row
from django import forms
from django.utils.translation import gettext_lazy as _
@@ -157,34 +157,21 @@ class DCAEntryForm(forms.ModelForm):
AccordionGroup(
_("Create transaction"),
Switch("create_transaction"),
- Row(
- Column(
- Row(
- Column(
- "from_account",
- ),
- ),
- Row(
- Column("from_category"),
- Column("from_tags"),
- ),
+ Div(
+ "from_account",
+ Row(
+ Column("from_category"),
+ Column("from_tags"),
),
- css_class="p-1 mx-1 my-3 border rounded-3",
+ css_class="bg-base-100 rounded-box p-4 border-base-content/60 border my-3",
),
- Row(
- Column(
- Row(
- Column(
- "to_account",
- css_class="form-group",
- ),
- ),
- Row(
- Column("to_category"),
- Column("to_tags"),
- ),
+ Div(
+ "to_account",
+ Row(
+ Column("to_category"),
+ Column("to_tags"),
),
- css_class="p-1 mx-1 my-3 border rounded-3",
+ css_class="bg-base-100 rounded-box p-4 border-base-content/60 border",
),
active=False,
),
@@ -289,6 +276,14 @@ class DCAEntryForm(forms.ModelForm):
raise forms.ValidationError(
_("From and To accounts must be different.")
)
+ else:
+ # Both sides of a purchase are linked as a transfer on save
+ expense = cleaned_data.get("expense_transaction")
+ income = cleaned_data.get("income_transaction")
+ if expense and income:
+ error = expense.transfer_link_error(income)
+ if error:
+ self.add_error("income_transaction", error)
return cleaned_data
@@ -334,6 +329,8 @@ class DCAEntryForm(forms.ModelForm):
)
to_transaction.tags.set(self.cleaned_data.get("to_tags", []))
+ from_transaction.link_transfer(to_transaction)
+
instance.expense_transaction = from_transaction
instance.income_transaction = to_transaction
else:
@@ -343,6 +340,8 @@ class DCAEntryForm(forms.ModelForm):
if instance.income_transaction:
instance.income_transaction.amount = instance.amount_received
instance.income_transaction.save()
+ if instance.expense_transaction and instance.income_transaction:
+ instance.expense_transaction.link_transfer(instance.income_transaction)
instance.strategy = self.strategy
instance.save()
diff --git a/app/apps/dca/migrations/0005_link_entry_transactions.py b/app/apps/dca/migrations/0005_link_entry_transactions.py
new file mode 100644
index 00000000..e440deeb
--- /dev/null
+++ b/app/apps/dca/migrations/0005_link_entry_transactions.py
@@ -0,0 +1,39 @@
+from django.db import migrations
+
+
+def link_entry_transactions(apps, schema_editor):
+ """Joins each DCA entry's expense and income into a transfer, so investment
+ tracking sees the purchase. Mirrors Transaction.transfer_link_error()."""
+ DCAEntry = apps.get_model("dca", "DCAEntry")
+ Transaction = apps.get_model("transactions", "Transaction")
+
+ entries = DCAEntry.objects.filter(
+ expense_transaction__isnull=False, income_transaction__isnull=False
+ ).select_related("expense_transaction", "income_transaction")
+ for entry in entries:
+ expense, income = entry.expense_transaction, entry.income_transaction
+ if (
+ expense.type != "EX"
+ or income.type != "IN"
+ or expense.account_id == income.account_id
+ or expense.linked_transaction_id
+ or income.linked_transaction_id
+ # a legacy one-way link already points at one of them
+ or Transaction.objects.filter(
+ linked_transaction_id__in=(expense.pk, income.pk)
+ ).exists()
+ ):
+ continue
+ Transaction.objects.filter(pk=expense.pk).update(linked_transaction=income)
+ Transaction.objects.filter(pk=income.pk).update(linked_transaction=expense)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("dca", "0004_alter_dcastrategy_owner_and_more"),
+ ("transactions", "0052_transfer_templates"),
+ ]
+
+ operations = [
+ migrations.RunPython(link_entry_transactions, migrations.RunPython.noop),
+ ]
diff --git a/app/apps/dca/tests/test_transfer_linking.py b/app/apps/dca/tests/test_transfer_linking.py
new file mode 100644
index 00000000..c98c9e2a
--- /dev/null
+++ b/app/apps/dca/tests/test_transfer_linking.py
@@ -0,0 +1,99 @@
+from datetime import date
+from decimal import Decimal
+
+from django.contrib.auth import get_user_model
+from django.core.exceptions import ValidationError
+from django.test import TestCase
+
+from apps.accounts.models import Account
+from apps.common.middleware.thread_local import delete_current_user, write_current_user
+from apps.currencies.models import Currency
+from apps.dca.forms import DCAEntryForm
+from apps.dca.models import DCAStrategy
+from apps.transactions.models import Transaction
+
+IN, EX = Transaction.Type.INCOME, Transaction.Type.EXPENSE
+
+
+class DCATransferLinkingTests(TestCase):
+ def setUp(self):
+ # Form querysets are scoped to the current user
+ write_current_user(get_user_model().objects.create_user(email="d@test.com"))
+ self.addCleanup(delete_current_user)
+ brl = Currency.objects.create(code="BRL", name="Real", decimal_places=2)
+ btc = Currency.objects.create(code="BTC", name="Bitcoin", decimal_places=8)
+ self.cash = Account.objects.create(name="Cash", currency=brl)
+ self.wallet = Account.objects.create(name="Wallet", currency=btc)
+ self.strategy = DCAStrategy.objects.create(
+ name="BTC", target_currency=btc, payment_currency=brl
+ )
+
+ def tx(self, account, type_, amount):
+ return Transaction.objects.create(
+ account=account, type=type_, amount=Decimal(amount), date=date(2026, 1, 5)
+ )
+
+ def entry_data(self, **extra):
+ return {
+ "date": "2026-01-05",
+ "amount_paid": "500",
+ "amount_received": "0.001",
+ **extra,
+ }
+
+ def test_created_transactions_are_linked(self):
+ form = DCAEntryForm(
+ data=self.entry_data(
+ create_transaction="on",
+ from_account=self.cash.id,
+ to_account=self.wallet.id,
+ ),
+ strategy=self.strategy,
+ )
+ self.assertTrue(form.is_valid(), form.errors)
+ entry = form.save()
+ self.assertEqual(
+ entry.expense_transaction.linked_transaction, entry.income_transaction
+ )
+ self.assertEqual(
+ entry.income_transaction.linked_transaction, entry.expense_transaction
+ )
+
+ def test_existing_transactions_are_linked(self):
+ expense, income = self.tx(self.cash, EX, "500"), self.tx(self.wallet, IN, "0.001")
+ form = DCAEntryForm(
+ data=self.entry_data(
+ expense_transaction=str(expense.id), income_transaction=str(income.id)
+ ),
+ strategy=self.strategy,
+ )
+ self.assertTrue(form.is_valid(), form.errors)
+ form.save()
+ expense.refresh_from_db()
+ income.refresh_from_db()
+ self.assertEqual(expense.linked_transaction, income)
+ self.assertEqual(income.linked_transaction, expense)
+
+ def test_transaction_in_another_transfer_is_rejected(self):
+ expense, income = self.tx(self.cash, EX, "500"), self.tx(self.wallet, IN, "0.001")
+ other = self.tx(self.wallet, IN, "0.002")
+ expense.link_transfer(other)
+ form = DCAEntryForm(
+ data=self.entry_data(
+ expense_transaction=str(expense.id), income_transaction=str(income.id)
+ ),
+ strategy=self.strategy,
+ )
+ self.assertFalse(form.is_valid())
+ self.assertIn("income_transaction", form.errors)
+
+ def test_link_transfer_rules(self):
+ expense = self.tx(self.cash, EX, "500")
+ with self.assertRaises(ValidationError):
+ expense.link_transfer(self.tx(self.cash, IN, "500")) # same account
+ with self.assertRaises(ValidationError):
+ expense.link_transfer(self.tx(self.wallet, EX, "1")) # two expenses
+ income = self.tx(self.wallet, IN, "0.001")
+ expense.link_transfer(income)
+ expense.link_transfer(income) # already linked to each other: no-op
+ self.assertEqual(Transaction.objects.get(pk=income.pk).linked_transaction, expense)
diff --git a/app/apps/export_app/forms.py b/app/apps/export_app/forms.py
index f9b2d229..caa436c8 100644
--- a/app/apps/export_app/forms.py
+++ b/app/apps/export_app/forms.py
@@ -108,6 +108,7 @@ class ExportForm(forms.Form):
self.helper.layout = Layout(
"users",
"accounts",
+ "investment_classes",
"currencies",
"transactions",
"categories",
@@ -135,6 +136,7 @@ class RestoreForm(forms.Form):
)
users = forms.FileField(required=False, label=_("Users"))
accounts = forms.FileField(required=False, label=_("Accounts"))
+ investment_classes = forms.FileField(required=False, label=_("Asset classes"))
currencies = forms.FileField(required=False, label=_("Currencies"))
transactions_categories = forms.FileField(required=False, label=_("Categories"))
transactions_tags = forms.FileField(required=False, label=_("Tags"))
diff --git a/app/apps/export_app/resources/accounts.py b/app/apps/export_app/resources/accounts.py
index 7c993bae..45cac926 100644
--- a/app/apps/export_app/resources/accounts.py
+++ b/app/apps/export_app/resources/accounts.py
@@ -1,10 +1,24 @@
from import_export import fields, resources, widgets
-from apps.accounts.models import Account, AccountGroup
+from apps.accounts.models import Account, AccountGroup, InvestmentClass
from apps.export_app.widgets.foreign_key import AutoCreateForeignKeyWidget
from apps.currencies.models import Currency
+class InvestmentClassResource(resources.ModelResource):
+ class Meta:
+ model = InvestmentClass
+
+ def get_queryset(self):
+ return InvestmentClass.all_objects.all()
+
+
+class AllClassesWidget(widgets.ForeignKeyWidget):
+ # By id, across owners: a shared account may carry another user's class
+ def get_queryset(self, value, row, *args, **kwargs):
+ return InvestmentClass.all_objects.all()
+
+
class AccountResource(resources.ModelResource):
group = fields.Field(
attribute="group",
@@ -22,6 +36,12 @@ class AccountResource(resources.ModelResource):
widget=widgets.ForeignKeyWidget(Currency, "name"),
)
+ investment_class = fields.Field(
+ attribute="investment_class",
+ column_name="investment_class",
+ widget=AllClassesWidget(InvestmentClass),
+ )
+
class Meta:
model = Account
diff --git a/app/apps/export_app/tests.py b/app/apps/export_app/tests.py
index 7ce503c2..e70c0357 100644
--- a/app/apps/export_app/tests.py
+++ b/app/apps/export_app/tests.py
@@ -1,3 +1,53 @@
+import io
+import zipfile
+from decimal import Decimal
+
+from django.contrib.auth import get_user_model
from django.test import TestCase
-# Create your tests here.
+from apps.accounts.models import Account, InvestmentClass
+from apps.common.middleware.thread_local import delete_current_user, write_current_user
+from apps.currencies.models import Currency
+from apps.export_app.resources.accounts import AccountResource, InvestmentClassResource
+from apps.export_app.resources.transactions import TransactionEntityResource
+from apps.export_app.views import process_imports
+from apps.transactions.models import TransactionEntity
+
+
+class InvestmentRestoreTests(TestCase):
+ def setUp(self):
+ self.user = get_user_model().objects.create_user(email="u@test.com", password="pw")
+ write_current_user(self.user)
+
+ def tearDown(self):
+ delete_current_user()
+
+ def test_round_trip_keeps_asset_class_and_income_links(self):
+ brl = Currency.objects.create(code="BRL", name="Real", decimal_places=2)
+ stocks = InvestmentClass.objects.create(name="Stocks", target=Decimal("60"))
+ broker = TransactionEntity.objects.create(name="Broker")
+ account = Account.objects.create(
+ name="PETR4", currency=brl, is_investment=True, investment_class=stocks
+ )
+ account.income_entities.add(broker)
+
+ backup = io.BytesIO()
+ with zipfile.ZipFile(backup, "w") as z:
+ for resource, name in (
+ (AccountResource, "accounts"),
+ (InvestmentClassResource, "investment_classes"),
+ (TransactionEntityResource, "transactions_entities"),
+ ):
+ z.writestr(f"{name}.csv", resource().export().csv)
+
+ Account.all_objects.all().delete()
+ InvestmentClass.all_objects.all().delete()
+ TransactionEntity.all_objects.all().delete()
+
+ backup.seek(0)
+ process_imports(None, {"zip_file": backup})
+
+ account = Account.all_objects.get(name="PETR4")
+ self.assertEqual(account.investment_class.name, "Stocks")
+ self.assertEqual(account.investment_class.target, Decimal("60"))
+ self.assertEqual([e.name for e in account.income_entities.all()], ["Broker"])
diff --git a/app/apps/export_app/views.py b/app/apps/export_app/views.py
index a74e4ae0..c413d3e4 100644
--- a/app/apps/export_app/views.py
+++ b/app/apps/export_app/views.py
@@ -14,7 +14,10 @@ from tablib import Dataset
from apps.common.decorators.htmx import only_htmx
from apps.export_app.forms import ExportForm, RestoreForm
-from apps.export_app.resources.accounts import AccountResource
+from apps.export_app.resources.accounts import (
+ AccountResource,
+ InvestmentClassResource,
+)
from apps.export_app.resources.bank_sync import (
BankSyncAccountResource,
BankSyncServiceResource,
@@ -95,6 +98,9 @@ def export_form(request):
exports.append((UserResource().export(), "users"))
if export_accounts:
exports.append((AccountResource().export(), "accounts"))
+ exports.append(
+ (InvestmentClassResource().export(), "investment_classes")
+ )
if export_currencies:
exports.append((CurrencyResource().export(), "currencies"))
if export_transactions:
@@ -233,10 +239,13 @@ def process_imports(request, cleaned_data):
"currencies",
CurrencyResource,
), # We do a double pass because exchange_currency may not exist when currency is initially created
+ ("investment_classes", InvestmentClassResource),
("accounts", AccountResource),
("transactions_categories", TransactionCategoyResource),
("transactions_tags", TransactionTagResource),
("transactions_entities", TransactionEntityResource),
+ # Again: income categories, tags and entities don't exist on the first pass
+ ("accounts", AccountResource),
("automatic_exchange_rates", ExchangeRateServiceResource),
("exchange_rates", ExchangeRateResource),
("installment_plans", InstallmentPlanResource),
diff --git a/app/apps/import_app/services/presets.py b/app/apps/import_app/services/presets.py
index 824a2468..e4cef311 100644
--- a/app/apps/import_app/services/presets.py
+++ b/app/apps/import_app/services/presets.py
@@ -1,17 +1,17 @@
import json
from pathlib import Path
+from django.conf import settings
+
from apps.import_app.models import ImportProfile
class PresetService:
- PRESET_PATH = "/usr/src/app/import_presets"
-
@classmethod
def get_all_presets(cls):
presets = []
- for folder in Path(cls.PRESET_PATH).iterdir():
+ for folder in settings.IMPORT_PRESETS_ROOT.iterdir():
if folder.is_dir():
manifest_path = folder / "manifest.json"
config_path = folder / "config.yml"
diff --git a/app/apps/import_app/services/v1.py b/app/apps/import_app/services/v1.py
index bed860ac..2bc16981 100644
--- a/app/apps/import_app/services/v1.py
+++ b/app/apps/import_app/services/v1.py
@@ -7,12 +7,14 @@ import zipfile
from django.db import transaction
from datetime import datetime, date
from decimal import Decimal, InvalidOperation
+from pathlib import Path
from typing import Dict, Any, Literal, Union
import openpyxl
import xlrd
import yaml
from cachalot.api import cachalot_disabled
+from django.conf import settings
from django.core.exceptions import FieldDoesNotExist
from django.utils import timezone
from openpyxl.utils.exceptions import InvalidFileException
@@ -39,8 +41,6 @@ logger = logging.getLogger(__name__)
class ImportService:
- TEMP_DIR = "/usr/src/app/temp"
-
def __init__(self, import_run: ImportRun):
self.import_run: ImportRun = import_run
self.profile: ImportProfile = import_run.profile
@@ -54,7 +54,7 @@ class ImportService:
self.mapping: Dict[str, version_1.ColumnMapping] = self.config.mapping
# Ensure temp directory exists
- os.makedirs(self.TEMP_DIR, exist_ok=True)
+ os.makedirs(settings.IMPORT_TEMP_ROOT, exist_ok=True)
def _load_config(self) -> version_1.ImportProfileSchema:
yaml_data = yaml.safe_load(self.profile.yaml_config)
@@ -1131,9 +1131,12 @@ class ImportService:
Validates that the file path is within the allowed temporary directory.
Returns the absolute path.
"""
- abs_path = os.path.abspath(file_path)
- if not abs_path.startswith(self.TEMP_DIR):
- raise ValueError(f"Invalid file path. File must be in {self.TEMP_DIR}")
+ # resolve() like IMPORT_TEMP_ROOT, so symlinks can't point outside it
+ abs_path = str(Path(file_path).resolve())
+ if not Path(abs_path).is_relative_to(settings.IMPORT_TEMP_ROOT):
+ raise ValueError(
+ f"Invalid file path. File must be in {settings.IMPORT_TEMP_ROOT}"
+ )
return abs_path
def process_file(self, file_path: str):
diff --git a/app/apps/import_app/tests/test_paths.py b/app/apps/import_app/tests/test_paths.py
new file mode 100644
index 00000000..a9eb7807
--- /dev/null
+++ b/app/apps/import_app/tests/test_paths.py
@@ -0,0 +1,161 @@
+"""Tests for the folders import code reads from and writes to."""
+
+import json
+import os
+import runpy
+import shutil
+import tempfile
+from pathlib import Path
+from unittest.mock import patch
+
+from django.conf import settings
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.test import SimpleTestCase, override_settings
+from django.urls import reverse
+from rest_framework import status
+from rest_framework.test import APIClient
+
+from apps.import_app.models import ImportRun
+from apps.import_app.services import PresetService
+from apps.import_app.services.v1 import ImportService
+from apps.import_app.tests.test_view_permissions import (
+ HTMX,
+ ImportObjectPermissionTestCase,
+)
+
+
+def load_settings(**env):
+ """Evaluates the settings module from scratch under the given env vars."""
+ with patch.dict(os.environ, env):
+ return runpy.run_path(str(Path(settings.BASE_DIR) / "WYGIWYH" / "settings.py"))
+
+
+class PathSettingsTests(SimpleTestCase):
+ def test_defaults_match_the_docker_image_layout(self):
+ loaded = load_settings(IMPORT_TEMP_ROOT="", CACHE_ROOT="")
+
+ base_dir = loaded["BASE_DIR"]
+ self.assertEqual(loaded["IMPORT_TEMP_ROOT"], base_dir / "temp")
+ self.assertEqual(loaded["IMPORT_PRESETS_ROOT"], base_dir / "import_presets")
+ self.assertEqual(
+ loaded["CACHES"]["default"]["LOCATION"],
+ Path("/var/tmp/django_cache").resolve(),
+ )
+ self.assertEqual(
+ loaded["CACHES"]["update_check"]["LOCATION"],
+ Path("/var/tmp/django_cache_update_check").resolve(),
+ )
+
+ def test_env_vars_move_temp_and_cache_folders(self):
+ with tempfile.TemporaryDirectory() as root:
+ root = Path(root).resolve()
+ loaded = load_settings(
+ IMPORT_TEMP_ROOT=str(root / "uploads"), CACHE_ROOT=str(root / "cache")
+ )
+
+ self.assertEqual(loaded["IMPORT_TEMP_ROOT"], root / "uploads")
+ self.assertEqual(
+ loaded["CACHES"]["default"]["LOCATION"], root / "cache" / "django_cache"
+ )
+ self.assertEqual(
+ loaded["CACHES"]["update_check"]["LOCATION"],
+ root / "cache" / "django_cache_update_check",
+ )
+
+
+class PresetServiceTests(SimpleTestCase):
+ def test_lists_the_presets_shipped_with_the_app(self):
+ folder_names = {
+ preset["folder_name"] for preset in PresetService.get_all_presets()
+ }
+
+ self.assertIn("qif_standard", folder_names)
+
+ def test_reads_presets_from_configured_folder(self):
+ with tempfile.TemporaryDirectory() as root:
+ preset = Path(root) / "my_bank"
+ preset.mkdir()
+ (preset / "manifest.json").write_text(json.dumps({"name": "My Bank"}))
+ (preset / "config.yml").write_text("settings: {}")
+
+ with override_settings(IMPORT_PRESETS_ROOT=Path(root)):
+ presets = PresetService.get_all_presets()
+
+ self.assertEqual([p["name"] for p in presets], ["My Bank"])
+
+
+class ImportTempRootTests(ImportObjectPermissionTestCase):
+ def setUp(self):
+ super().setUp()
+ self.temp_root = Path(tempfile.mkdtemp()).resolve()
+ self.addCleanup(shutil.rmtree, self.temp_root, ignore_errors=True)
+ temp_root_override = override_settings(IMPORT_TEMP_ROOT=self.temp_root)
+ temp_root_override.enable()
+ self.addCleanup(temp_root_override.disable)
+
+ @patch("apps.import_app.views.process_import.defer")
+ def test_web_upload_is_stored_in_temp_root(self, mock_defer):
+ self.login(self.owner)
+ response = self.client.post(
+ reverse("import_run_add", kwargs={"profile_id": self.private_profile.id}),
+ {"file": SimpleUploadedFile("web.csv", b"date,amount\n")},
+ **HTMX,
+ )
+
+ self.assertEqual(response.status_code, 204)
+ self.assertEqual(
+ mock_defer.call_args.kwargs["file_path"], str(self.temp_root / "web.csv")
+ )
+ self.assertTrue((self.temp_root / "web.csv").is_file())
+
+ @patch("apps.api.views.imports.process_import.defer")
+ def test_api_upload_is_stored_in_temp_root(self, mock_defer):
+ client = APIClient()
+ client.force_authenticate(user=self.owner)
+ response = client.post(
+ "/api/import/import/",
+ {
+ "profile_id": self.private_profile.id,
+ "file": SimpleUploadedFile("api.csv", b"date,amount\n"),
+ },
+ format="multipart",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
+ self.assertEqual(
+ mock_defer.call_args.kwargs["file_path"], str(self.temp_root / "api.csv")
+ )
+ self.assertTrue((self.temp_root / "api.csv").is_file())
+
+ def _service(self):
+ run = ImportRun.objects.create(
+ profile=self.private_profile, file_name="file.csv", owner=self.owner
+ )
+ return ImportService(run)
+
+ def test_service_accepts_files_inside_temp_root(self):
+ path = str(self.temp_root / "file.csv")
+
+ self.assertEqual(self._service()._validate_file_path(path), path)
+
+ def test_service_rejects_files_outside_temp_root(self):
+ service = self._service()
+
+ for path in (
+ "/etc/passwd",
+ self.temp_root / ".." / "file.csv",
+ # Shares the temp root as a string prefix, but is a different folder
+ f"{self.temp_root}-other/file.csv",
+ ):
+ with self.subTest(path=path), self.assertRaises(ValueError):
+ service._validate_file_path(str(path))
+
+ def test_service_rejects_symlinks_pointing_outside_temp_root(self):
+ link = self.temp_root / "link.csv"
+ try:
+ link.symlink_to("/etc/passwd")
+ except OSError:
+ self.skipTest("symlinks not available")
+
+ with self.assertRaises(ValueError):
+ self._service()._validate_file_path(str(link))
diff --git a/app/apps/import_app/tests/test_qif_import.py b/app/apps/import_app/tests/test_qif_import.py
index a79662b3..d2b7e9db 100644
--- a/app/apps/import_app/tests/test_qif_import.py
+++ b/app/apps/import_app/tests/test_qif_import.py
@@ -1,7 +1,7 @@
from decimal import Decimal
import os
import shutil
-from django.test import TestCase
+from django.test import TestCase, override_settings
from django.contrib.auth import get_user_model
from apps.accounts.models import Account, AccountGroup
from apps.currencies.models import Currency
@@ -15,10 +15,11 @@ from apps.transactions.models import (
class QIFImportTests(TestCase):
def setUp(self):
- # Patch TEMP_DIR for testing
- self.original_temp_dir = ImportService.TEMP_DIR
+ # Point the import temp folder somewhere disposable for testing
self.test_dir = os.path.abspath("temp_test_import")
- ImportService.TEMP_DIR = self.test_dir
+ temp_root_override = override_settings(IMPORT_TEMP_ROOT=self.test_dir)
+ temp_root_override.enable()
+ self.addCleanup(temp_root_override.disable)
os.makedirs(self.test_dir, exist_ok=True)
# Create user and set context
@@ -41,7 +42,6 @@ class QIFImportTests(TestCase):
def tearDown(self):
delete_current_user()
- ImportService.TEMP_DIR = self.original_temp_dir
if os.path.exists(self.test_dir):
shutil.rmtree(self.test_dir)
diff --git a/app/apps/import_app/views.py b/app/apps/import_app/views.py
index adef7679..45dc61ab 100644
--- a/app/apps/import_app/views.py
+++ b/app/apps/import_app/views.py
@@ -1,5 +1,6 @@
import shutil
+from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
@@ -204,7 +205,7 @@ def import_run_add(request, profile_id):
if form.is_valid():
uploaded_file = request.FILES["file"]
- fs = FileSystemStorage(location="/usr/src/app/temp")
+ fs = FileSystemStorage(location=settings.IMPORT_TEMP_ROOT)
filename = fs.save(uploaded_file.name, uploaded_file)
file_path = fs.path(filename)
diff --git a/app/apps/investments/__init__.py b/app/apps/investments/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/app/apps/investments/apps.py b/app/apps/investments/apps.py
new file mode 100644
index 00000000..4b50cd38
--- /dev/null
+++ b/app/apps/investments/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class InvestmentsConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "apps.investments"
diff --git a/app/apps/investments/services.py b/app/apps/investments/services.py
new file mode 100644
index 00000000..a6371ada
--- /dev/null
+++ b/app/apps/investments/services.py
@@ -0,0 +1,515 @@
+"""Investment performance, derived from an account's transactions.
+
+Rules (see the account's "Investment" help text):
+- A linked transaction (a transfer) moves money in or out. Its other side says
+ what was paid or received, which is the cost or the proceeds.
+- An unlinked transaction on an account tracked by value is money coming in or
+ going out (a salary, a bill), unless it has one of the account's income
+ categories, tags or entities: then it is a gain or a loss (a reconciled balance,
+ interest, a tax withheld).
+- On a unit-priced account an unlinked outflow is a loss, and an unlinked inflow
+ (a gift, an opening balance) has no known cost, so it is costed at the market
+ price of that day and the result is flagged as estimated.
+- Cost is tracked with the average cost method.
+"""
+
+import datetime
+from dataclasses import dataclass, field
+from decimal import Decimal
+from functools import cached_property
+
+from django.db.models import Q
+from django.utils import timezone
+
+from apps.currencies.models import ExchangeRate
+from apps.currencies.utils.convert import convert, get_exchange_rate
+from apps.transactions.models import Transaction
+
+ZERO = Decimal("0")
+
+
+@dataclass
+class HoldingPerformance:
+ account: object
+ currency: object # valuation currency
+ priced: bool # True: units x price. False: tracked by value.
+ quantity: Decimal = ZERO
+ cost_basis: Decimal = ZERO
+ price: Decimal | None = None
+ price_date: datetime.datetime | None = None
+ realized: Decimal = ZERO
+ income: Decimal = ZERO
+ contributed: Decimal = ZERO
+ withdrawn: Decimal = ZERO
+ estimated: bool = False
+ # Unlinked transactions counted as gains or losses, on accounts tracked by value
+ result_ids: set = field(default_factory=set)
+ history: list = field(default_factory=list)
+ # (date, "buy" or "sell", unit price) for linked trades on unit-priced accounts
+ trades: list = field(default_factory=list)
+ # (date, signed amount) for the gains and losses of an account tracked by value
+ results: list = field(default_factory=list)
+ # (date, amount, kind) in the valuation currency: negative = money in ("in"),
+ # positive = money out ("out" sales/withdrawals, "income"). Used for
+ # money-weighted returns and currency conversion.
+ flows: list = field(default_factory=list)
+ as_of: datetime.date | None = None
+
+ @cached_property
+ def money_weighted(self):
+ return money_weighted_return(self.flows, self.market_value, self.as_of)
+
+ @property
+ def average_cost(self):
+ return self.cost_basis / self.quantity if self.quantity else None
+
+ @property
+ def market_value(self):
+ if self.price is None:
+ return None
+ return self.quantity * self.price
+
+ @property
+ def unrealized(self):
+ value = self.market_value
+ return None if value is None else value - self.cost_basis
+
+ @property
+ def total_return(self):
+ return (self.unrealized or ZERO) + self.realized + self.income
+
+ @property
+ def return_percentage(self):
+ if not self.contributed:
+ return None
+ return self.total_return / self.contributed * 100
+
+ @property
+ def unrealized_percentage(self):
+ if not self.cost_basis or self.unrealized is None:
+ return None
+ return self.unrealized / self.cost_basis * 100
+
+
+def _to(amount, from_currency, to_currency, date):
+ if from_currency == to_currency or not amount:
+ return amount
+ return convert(amount, from_currency, to_currency, date)[0]
+
+
+def _price(account, valuation_currency, date):
+ if account.currency == valuation_currency:
+ return Decimal("1"), None
+ rate = get_exchange_rate(account.currency, valuation_currency, date)
+ if rate is None:
+ return None, None
+ return rate.effective_rate, rate.date
+
+
+def _linked_ids(account, field_name):
+ # The join table, not the related manager: that one is per-user and comes back
+ # empty without a current user
+ relation = account._meta.get_field(field_name)
+ return list(
+ relation.remote_field.through.objects.filter(account=account).values_list(
+ relation.m2m_reverse_name(), flat=True
+ )
+ )
+
+
+def income_entity_ids(account):
+ return _linked_ids(account, "income_entities")
+
+
+def _result_ids(account):
+ return set(
+ Transaction.objects.filter(account=account)
+ .filter(
+ Q(category__in=_linked_ids(account, "income_categories"))
+ | Q(tags__in=_linked_ids(account, "income_tags"))
+ | Q(entities__in=income_entity_ids(account))
+ )
+ .values_list("id", flat=True)
+ )
+
+
+def get_holding_performance(account, as_of=None, with_history=False):
+ """Performance of an investment account up to ``as_of`` (today by default),
+ in the account's exchange currency, or its own currency if it has none."""
+ today = timezone.localdate()
+ as_of = as_of or today
+ valuation_currency = account.exchange_currency or account.currency
+ perf = HoldingPerformance(
+ account=account,
+ currency=valuation_currency,
+ priced=account.exchange_currency_id is not None,
+ )
+
+ transactions = (
+ Transaction.objects.filter(account=account, is_paid=True, date__lte=as_of)
+ .select_related("linked_transaction__account__currency")
+ .order_by("date", "id")
+ )
+
+ month_ends = _month_ends(transactions, as_of) if with_history else []
+ if not perf.priced:
+ perf.result_ids = _result_ids(account)
+
+ for tx in transactions:
+ while month_ends and month_ends[0] < tx.date:
+ perf.history.append(_snapshot(perf, account, month_ends.pop(0)))
+
+ # shortcut: only the forward link is read; WYGIWYH transfers link both ways.
+ partner = tx.linked_transaction
+ if partner and (partner.account_id == account.id or partner.deleted):
+ partner = None
+ partner_value = (
+ _to(partner.amount, partner.account.currency, valuation_currency, tx.date)
+ if partner
+ else None
+ )
+ external = not (perf.priced or partner or tx.id in perf.result_ids)
+ if perf.priced and partner_value is not None and tx.amount:
+ kind = "buy" if tx.type == Transaction.Type.INCOME else "sell"
+ perf.trades.append((tx.date, kind, partner_value / tx.amount))
+ elif not (perf.priced or partner or external):
+ sign = 1 if tx.type == Transaction.Type.INCOME else -1
+ perf.results.append((tx.date, sign * tx.amount))
+
+ if tx.type == Transaction.Type.INCOME:
+ if external:
+ cost = tx.amount
+ perf.contributed += cost
+ elif partner_value is not None:
+ cost = partner_value
+ perf.contributed += cost
+ elif perf.priced or partner:
+ # No known cost: price it at that day's market value
+ cost = _to(tx.amount, account.currency, valuation_currency, tx.date)
+ perf.estimated = True
+ if partner:
+ perf.contributed += cost or ZERO
+ else:
+ cost = ZERO # value-tracked: a gain
+ if cost:
+ perf.flows.append((tx.date, -cost, "in"))
+ perf.quantity += tx.amount
+ perf.cost_basis += cost or ZERO
+ else:
+ if perf.quantity > 0:
+ removed = perf.cost_basis * min(tx.amount, perf.quantity) / perf.quantity
+ else:
+ removed = ZERO
+ if partner or external:
+ proceeds = tx.amount if external else partner_value
+ if proceeds is None:
+ proceeds = _to(tx.amount, account.currency, valuation_currency, tx.date) or ZERO
+ perf.estimated = True
+ perf.realized += proceeds - removed
+ perf.withdrawn += proceeds
+ perf.flows.append((tx.date, proceeds, "out"))
+ perf.cost_basis -= removed
+ # otherwise a loss, tax or fee: cost stays, so value drops
+ perf.quantity -= tx.amount
+ if perf.quantity <= 0 and perf.cost_basis:
+ perf.realized -= perf.cost_basis
+ perf.cost_basis = ZERO
+
+ for month_end in month_ends:
+ perf.history.append(_snapshot(perf, account, month_end))
+
+ perf.price, perf.price_date = _price(
+ account,
+ valuation_currency,
+ timezone.now() if as_of == today else as_of,
+ )
+
+ entity_ids = income_entity_ids(account)
+ if entity_ids:
+ income = Transaction.objects.filter(
+ entities__in=entity_ids,
+ type=Transaction.Type.INCOME,
+ is_paid=True,
+ date__lte=as_of,
+ ).exclude(account=account).distinct().select_related("account__currency")
+ for tx in income:
+ amount = _to(tx.amount, tx.account.currency, valuation_currency, tx.date)
+ if amount:
+ perf.income += amount
+ perf.flows.append((tx.date, amount, "income"))
+
+ perf.as_of = as_of
+ return perf
+
+
+def xirr(flows):
+ """Annual rate r where the dated flows' present value is zero, as a float
+ (0.138 = 13.8% a year). None when it can't be solved."""
+ if not flows:
+ return None
+ start = min(flow[0] for flow in flows)
+ points = [((flow[0] - start).days / 365, float(flow[1])) for flow in flows]
+ if not (any(a < 0 for _, a in points) and any(a > 0 for _, a in points)):
+ return None
+
+ def npv(rate):
+ return sum(a / (1 + rate) ** t for t, a in points)
+
+ # Bisection: slower than Newton but can't diverge on odd cash flows
+ low, high = -0.9999, 100.0
+ npv_low = npv(low)
+ if npv_low * npv(high) > 0:
+ return None
+ for _ in range(200):
+ mid = (low + high) / 2
+ npv_mid = npv(mid)
+ if npv_low * npv_mid <= 0:
+ high = mid
+ else:
+ low, npv_low = mid, npv_mid
+ return (low + high) / 2
+
+
+def money_weighted_return(flows, market_value, as_of):
+ """Return that accounts for when money went in and out, as
+ ``{"percentage": Decimal, "annualized": bool}`` or None.
+
+ Over a year or more it is annualized (XIRR). Below a year an annual rate
+ exaggerates, so it is turned back into the return over the period held."""
+ if not flows or market_value is None:
+ return None
+ rate = xirr(flows + [(as_of, market_value)])
+ if rate is None:
+ return None
+ days = (as_of - min(flow[0] for flow in flows)).days
+ if days >= 365:
+ return {"percentage": Decimal(str(round(rate * 100, 4))), "annualized": True}
+ period = (1 + rate) ** (days / 365) - 1
+ return {"percentage": Decimal(str(round(period * 100, 4))), "annualized": False}
+
+
+def _month_ends(transactions, as_of):
+ first = transactions.values_list("date", flat=True).first()
+ if not first:
+ return []
+ ends = []
+ year, month = first.year, first.month
+ while True:
+ next_first = datetime.date(year + month // 12, month % 12 + 1, 1)
+ end = next_first - datetime.timedelta(days=1)
+ if end >= as_of:
+ ends.append(as_of)
+ return ends
+ ends.append(end)
+ year, month = next_first.year, next_first.month
+
+
+def _snapshot(perf, account, date):
+ price, _ = _price(account, perf.currency, date)
+ return {
+ "date": date,
+ "invested": perf.cost_basis,
+ "value": perf.quantity * price if price is not None else None,
+ "price": price,
+ "average_cost": perf.average_cost,
+ }
+
+
+def conversion_path(currency):
+ """``currency`` followed by its exchange currencies (USD -> BRL -> ...), for as
+ long as an exchange rate between each step exists."""
+ path = [currency]
+ while True:
+ target = path[-1].exchange_currency
+ if (
+ target is None
+ or target in path
+ or get_exchange_rate(path[-1], target, timezone.now()) is None
+ ):
+ return path
+ path.append(target)
+
+
+def _along(amount, path, date):
+ for from_currency, to_currency in zip(path, path[1:]):
+ amount = _to(amount, from_currency, to_currency, date)
+ return amount
+
+
+def _native(perf):
+ return {
+ "currency": perf.currency,
+ "market_value": perf.market_value,
+ "cost_basis": perf.cost_basis,
+ "income": perf.income,
+ "total_return": perf.total_return,
+ "currency_effect": ZERO,
+ "flows": perf.flows,
+ }
+
+
+def in_home_currency(perf):
+ """The holding converted along its currency's exchange chain, each cash flow
+ at its own date's rate, so currency moves count towards the return.
+ None when its currency converts to nothing."""
+ path = conversion_path(perf.currency)
+ if len(path) == 1:
+ return None
+ flows = [(d, _along(a, path, d), kind) for d, a, kind in perf.flows]
+ value = None
+ if perf.market_value is not None:
+ value = _along(perf.market_value, path, perf.as_of)
+ total_return = (value or ZERO) + sum((a for _, a, _ in flows), ZERO)
+
+ put_in = -sum((a for _, a, k in flows if k == "in"), ZERO)
+ put_in_native = -sum((a for _, a, k in perf.flows if k == "in"), ZERO)
+ # shortcut: what you still hold is valued at the average rate you paid in,
+ # not lot by lot; exact for buy-and-hold, approximate after partial sales.
+ cost_basis = (
+ perf.cost_basis * put_in / put_in_native
+ if put_in_native
+ else _along(perf.cost_basis, path, perf.as_of)
+ )
+ return {
+ "currency": path[-1],
+ "path": path,
+ "market_value": value,
+ "cost_basis": cost_basis,
+ "income": sum((a for _, a, k in flows if k == "income"), ZERO),
+ "total_return": total_return,
+ # what the return gained or lost from the exchange rate moving
+ "currency_effect": total_return
+ - _along(perf.total_return, path, perf.as_of),
+ # With a single rate every date converts the same, so the effect reads 0
+ "has_rate_history": all(
+ ExchangeRate.objects.filter(
+ Q(from_currency=a, to_currency=b) | Q(from_currency=b, to_currency=a)
+ )[:2].count()
+ > 1
+ for a, b in zip(path, path[1:])
+ ),
+ "flows": flows,
+ "money_weighted": money_weighted_return(flows, value, perf.as_of),
+ }
+
+
+def summarize(performances, convert=False):
+ """Totals per currency. With ``convert``, holdings are first converted along
+ their exchange chain (see in_home_currency); the ones that can't be stay in
+ their own currency."""
+ totals = {}
+ for perf in performances:
+ figures = (convert and in_home_currency(perf)) or _native(perf)
+ total = totals.setdefault(
+ figures["currency"].id,
+ {
+ "currency": figures["currency"],
+ "market_value": ZERO,
+ "cost_basis": ZERO,
+ "total_return": ZERO,
+ "income": ZERO,
+ "currency_effect": ZERO,
+ "converted_from": [],
+ "has_rate_history": True,
+ "flows": [],
+ "as_of": perf.as_of,
+ "holdings": [],
+ },
+ )
+ total["holdings"].append(perf)
+ for key in ("cost_basis", "total_return", "income", "currency_effect"):
+ total[key] += figures[key]
+ total["has_rate_history"] &= figures.get("has_rate_history", True)
+ if figures["currency"] != perf.currency and perf.currency not in total["converted_from"]:
+ total["converted_from"].append(perf.currency)
+ # A holding without a price has no end value, so its flows would skew the rate
+ if figures["market_value"] is not None:
+ total["market_value"] += figures["market_value"]
+ total["flows"] += figures["flows"]
+ for total in totals.values():
+ total["money_weighted"] = money_weighted_return(
+ total["flows"], total["market_value"], total["as_of"]
+ )
+ # The same holdings in their own currencies, for a converted total
+ total["breakdown"] = (
+ sorted(
+ summarize(total["holdings"]),
+ key=lambda part: part["currency"] != total["currency"],
+ )
+ if total["converted_from"]
+ else []
+ )
+ return list(totals.values())
+
+
+def portfolio_charts(performances):
+ """Chart data per total currency (as in summarize with ``convert``): value by
+ asset class and by currency, return by investment, and value against invested
+ at each month end. Classes are InvestmentClass objects, None for holdings
+ without one. Unpriced holdings are left out of the value figures."""
+ charts = {}
+ for perf in performances:
+ figures = in_home_currency(perf) or _native(perf)
+ path = figures.get("path", [perf.currency])
+ chart = charts.setdefault(
+ figures["currency"].id,
+ {
+ "currency": figures["currency"],
+ "classes": {},
+ "currencies": {},
+ "returns": [],
+ "history": {},
+ },
+ )
+ value = figures["market_value"]
+ if value:
+ key = perf.account.investment_class
+ chart["classes"][key] = chart["classes"].get(key, ZERO) + value
+ name = perf.currency.name
+ chart["currencies"][name] = chart["currencies"].get(name, ZERO) + value
+ chart["returns"].append((perf.account.name, figures["total_return"]))
+ # shortcut: one rate lookup per holding, month and hop; cache rates per
+ # date if portfolios with long histories make this tab slow.
+ for point in perf.history:
+ totals = chart["history"].setdefault(point["date"], [ZERO, ZERO])
+ for i, amount in enumerate((point["value"], point["invested"])):
+ if amount is not None:
+ totals[i] += _along(amount, path, point["date"]) or ZERO
+ for chart in charts.values():
+ for key in ("classes", "currencies"):
+ chart[key] = sorted(chart[key].items(), key=lambda item: -item[1])
+ chart["returns"].sort(key=lambda item: -item[1])
+ chart["history"] = sorted(chart["history"].items())
+ return list(charts.values())
+
+
+def drift_limit(target):
+ """How far a class may drift from its target, in percentage points: the
+ common 5/25 rule, 5 points or a quarter of the target, whichever is less."""
+ return min(Decimal(5), target / 4)
+
+
+def rebalance(classes, targeted):
+ """What to buy or sell so the classes in ``targeted`` (InvestmentClass with a
+ target) match their targets, given ``classes`` as (class, value) from
+ portfolio_charts. Holdings without a class or target are left out and their
+ value is returned as ``left_out``. ``move`` is positive to buy."""
+ values = dict(classes)
+ total = sum((values.get(cls, ZERO) for cls in targeted), ZERO)
+ rows = []
+ for cls in targeted:
+ value = values.get(cls, ZERO)
+ share = value / total * 100 if total else ZERO
+ move = total * cls.target / 100 - value
+ rows.append(
+ {
+ "class": cls,
+ "value": value,
+ "share": share,
+ "target": cls.target,
+ "move": move,
+ "off": abs(share - cls.target) > drift_limit(cls.target),
+ }
+ )
+ left_out = sum(values.values(), ZERO) - total
+ return {"total": total, "rows": rows, "left_out": left_out}
diff --git a/app/apps/investments/tests/__init__.py b/app/apps/investments/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/app/apps/investments/tests/test_services.py b/app/apps/investments/tests/test_services.py
new file mode 100644
index 00000000..5ac80688
--- /dev/null
+++ b/app/apps/investments/tests/test_services.py
@@ -0,0 +1,415 @@
+import datetime
+from decimal import Decimal
+
+from django.contrib.auth import get_user_model
+from django.test import TestCase, override_settings
+from django.urls import reverse
+from django.utils import timezone
+
+from apps.accounts.models import Account, InvestmentClass
+from apps.common.middleware.thread_local import delete_current_user, write_current_user
+from apps.currencies.models import Currency, ExchangeRate
+from apps.investments.services import get_holding_performance, summarize
+from apps.transactions.models import (
+ Transaction,
+ TransactionCategory,
+ TransactionEntity,
+ TransactionTag,
+)
+
+D = Decimal
+IN, EX = Transaction.Type.INCOME, Transaction.Type.EXPENSE
+
+
+def tx(account, type_, amount, date, **kwargs):
+ return Transaction.objects.create(
+ account=account, type=type_, amount=D(amount), date=date, **kwargs
+ )
+
+
+def transfer(from_account, from_amount, to_account, to_amount, date):
+ out = tx(from_account, EX, from_amount, date)
+ inc = tx(to_account, IN, to_amount, date, linked_transaction=out)
+ out.linked_transaction = inc
+ out.save()
+ return out, inc
+
+
+class HoldingPerformanceTests(TestCase):
+ def setUp(self):
+ self.eur = Currency.objects.create(code="EUR", name="Euro", decimal_places=2)
+ self.brl = Currency.objects.create(code="BRL", name="Real", decimal_places=2)
+ self.vwce = Currency.objects.create(
+ code="VWCE.DE", name="VWCE", decimal_places=4, exchange_currency=self.eur
+ )
+ self.cash = Account.objects.create(name="Cash", currency=self.eur)
+ self.etf = Account.objects.create(
+ name="VWCE",
+ currency=self.vwce,
+ exchange_currency=self.eur,
+ is_investment=True,
+ is_asset=True,
+ )
+ self.d1 = datetime.date(2026, 1, 10)
+ self.d2 = datetime.date(2026, 2, 10)
+ self.d3 = datetime.date(2026, 3, 10)
+
+ def rate(self, value, date):
+ ExchangeRate.objects.create(
+ from_currency=self.vwce,
+ to_currency=self.eur,
+ rate=D(value),
+ date=timezone.make_aware(datetime.datetime.combine(date, datetime.time())),
+ )
+
+ def test_priced_average_cost_realized_and_income(self):
+ transfer(self.cash, "1000", self.etf, "10", self.d1)
+ transfer(self.cash, "1200", self.etf, "10", self.d2)
+ transfer(self.etf, "5", self.cash, "700", self.d3)
+ self.rate("130", timezone.localdate())
+
+ entity = TransactionEntity.objects.create(name="Vanguard")
+ other = TransactionEntity.objects.create(name="Vanguard JCP")
+ self.etf.income_entities.set([entity, other])
+ dividend = tx(self.cash, IN, "30", self.d3)
+ dividend.entities.add(entity, other) # matches twice, counted once
+
+ perf = get_holding_performance(self.etf)
+
+ self.assertTrue(perf.priced)
+ self.assertEqual(perf.quantity, D("15"))
+ self.assertEqual(perf.cost_basis, D("1650"))
+ self.assertEqual(perf.average_cost, D("110"))
+ self.assertEqual(perf.realized, D("150")) # 700 - 5 * 110
+ self.assertEqual(perf.market_value, D("1950"))
+ self.assertEqual(perf.unrealized, D("300"))
+ self.assertEqual(perf.income, D("30"))
+ self.assertEqual(perf.contributed, D("2200"))
+ self.assertEqual(perf.withdrawn, D("700"))
+ self.assertEqual(perf.total_return, D("480"))
+ self.assertFalse(perf.estimated)
+ self.assertEqual(
+ perf.trades,
+ [(self.d1, "buy", D("100")), (self.d2, "buy", D("120")), (self.d3, "sell", D("140"))],
+ )
+
+ def test_unlinked_inflow_on_priced_account_is_costed_at_market(self):
+ self.rate("90", self.d1)
+ tx(self.etf, IN, "2", self.d1)
+
+ perf = get_holding_performance(self.etf)
+
+ self.assertEqual(perf.cost_basis, D("180"))
+ self.assertTrue(perf.estimated)
+ self.assertEqual(perf.contributed, D("0"))
+
+ def test_value_tracked_cdb_with_tax_on_redemption(self):
+ cdb = Account.objects.create(
+ name="CDB", currency=self.brl, is_investment=True, is_asset=True
+ )
+ checking = Account.objects.create(name="Checking", currency=self.brl)
+ result = TransactionCategory.objects.create(name="Yield")
+ cdb.income_categories.set([result])
+ transfer(checking, "1000", cdb, "1000", self.d1)
+ tx(cdb, IN, "100", self.d2, category=result) # balance reconciliation
+ tx(cdb, EX, "22.50", self.d3, category=result) # income tax withheld
+
+ perf = get_holding_performance(cdb)
+ self.assertFalse(perf.priced)
+ self.assertEqual(perf.results, [(self.d2, D("100")), (self.d3, D("-22.50"))])
+ self.assertEqual(perf.market_value, D("1077.50"))
+ self.assertEqual(perf.cost_basis, D("1000"))
+ self.assertEqual(perf.unrealized, D("77.50"))
+
+ transfer(cdb, "1077.50", checking, "1077.50", self.d3)
+ perf = get_holding_performance(cdb)
+ self.assertEqual(perf.quantity, D("0"))
+ self.assertEqual(perf.cost_basis, D("0"))
+ self.assertEqual(perf.realized, D("77.50"))
+ self.assertEqual(perf.total_return, D("77.50"))
+
+ def test_value_tracked_salary_account_separates_money_from_yield(self):
+ account = Account.objects.create(
+ name="Salary", currency=self.brl, is_investment=True
+ )
+ tag = TransactionTag.objects.create(name="Yield")
+ account.income_tags.set([tag])
+ tx(account, IN, "5000", self.d1) # salary, from outside
+ tx(account, EX, "1000", self.d2) # rent, not a loss
+ tx(account, IN, "40", self.d3).tags.add(tag) # reconciled balance
+
+ perf = get_holding_performance(account)
+
+ self.assertEqual(perf.contributed, D("5000"))
+ self.assertEqual(perf.withdrawn, D("1000"))
+ self.assertEqual(perf.market_value, D("4040"))
+ self.assertEqual(perf.cost_basis, D("4000"))
+ self.assertEqual(perf.total_return, D("40"))
+ self.assertFalse(perf.estimated)
+
+ def test_own_entity_is_a_result_but_only_income_counts_elsewhere(self):
+ account = Account.objects.create(
+ name="Savings", currency=self.brl, is_investment=True
+ )
+ other = Account.objects.create(name="Other", currency=self.brl)
+ bank = TransactionEntity.objects.create(name="Bank")
+ account.income_entities.set([bank])
+ tx(account, IN, "1000", self.d1)
+ tx(account, IN, "10", self.d2).entities.add(bank)
+ tx(other, EX, "99", self.d2).entities.add(bank)
+
+ perf = get_holding_performance(account)
+
+ self.assertEqual(perf.total_return, D("10"))
+ self.assertEqual(perf.income, D("0"))
+
+ def test_history_has_one_point_per_month_end(self):
+ transfer(self.cash, "1000", self.etf, "10", self.d1)
+ self.rate("100", self.d1)
+ self.rate("120", self.d3)
+
+ perf = get_holding_performance(
+ self.etf, as_of=datetime.date(2026, 3, 15), with_history=True
+ )
+
+ self.assertEqual(
+ [p["date"] for p in perf.history],
+ [
+ datetime.date(2026, 1, 31),
+ datetime.date(2026, 2, 28),
+ datetime.date(2026, 3, 15),
+ ],
+ )
+ self.assertEqual(perf.history[-1]["value"], D("1200"))
+ self.assertEqual(perf.history[0]["invested"], D("1000"))
+ self.assertEqual(perf.history[-1]["price"], D("120"))
+ self.assertEqual(perf.history[-1]["average_cost"], D("100"))
+
+ def test_summarize_groups_by_currency(self):
+ transfer(self.cash, "1000", self.etf, "10", self.d1)
+ self.rate("110", timezone.localdate())
+ totals = summarize([get_holding_performance(self.etf)])
+ self.assertEqual(len(totals), 1)
+ self.assertEqual(totals[0]["market_value"], D("1100"))
+ self.assertEqual(totals[0]["total_return"], D("100"))
+
+ def test_portfolio_charts_group_by_asset_class(self):
+ from apps.investments.services import portfolio_charts
+
+ stocks = InvestmentClass.objects.create(name="Stocks")
+ self.etf.investment_class = stocks
+ self.etf.save()
+ cdb = Account.objects.create(name="CDB", currency=self.eur, is_investment=True)
+ transfer(self.cash, "1000", self.etf, "10", self.d1)
+ transfer(self.cash, "500", cdb, "500", self.d1)
+ self.rate("110", self.d1)
+
+ perfs = [get_holding_performance(a, with_history=True) for a in (self.etf, cdb)]
+ [chart] = portfolio_charts(perfs)
+
+ self.assertEqual(chart["classes"], [(stocks, D("1100")), (None, D("500"))])
+ # Priced in euros, so that is its currency exposure
+ self.assertEqual(chart["currencies"], [("Euro", D("1600"))])
+ self.assertEqual(chart["returns"][0], ("VWCE", D("100")))
+ self.assertEqual(chart["history"][-1][1], [D("1600"), D("1500")])
+
+ def test_rebalance_moves_to_targets(self):
+ from apps.investments.services import rebalance
+
+ stocks = InvestmentClass.objects.create(name="Stocks", target=D("60"))
+ bonds = InvestmentClass.objects.create(name="Bonds", target=D("30"))
+ gold = InvestmentClass.objects.create(name="Gold", target=D("10"))
+ loose = InvestmentClass.objects.create(name="Loose") # no target
+
+ plan = rebalance(
+ [(stocks, D("700")), (bonds, D("300")), (loose, D("50")), (None, D("25"))],
+ [stocks, bonds, gold],
+ )
+
+ self.assertEqual(plan["total"], D("1000"))
+ self.assertEqual(plan["left_out"], D("75"))
+ moves = {row["class"].name: row["move"] for row in plan["rows"]}
+ self.assertEqual(moves, {"Stocks": D("-100"), "Bonds": D("0"), "Gold": D("100")})
+ self.assertEqual(sum(moves.values()), 0)
+ # 70% vs 60%: past the 5 points; gold at 0% vs 10%: past 2.5 (a quarter)
+ off = {row["class"].name: row["off"] for row in plan["rows"]}
+ self.assertEqual(off, {"Stocks": True, "Bonds": False, "Gold": True})
+
+ def test_class_names_are_unique(self):
+ from apps.accounts.forms import InvestmentClassForm
+
+ stocks = InvestmentClass.objects.create(name="Stocks")
+ self.assertFalse(InvestmentClassForm({"name": "Stocks"}).is_valid())
+ self.assertTrue(InvestmentClassForm({"name": "Stocks"}, instance=stocks).is_valid())
+
+ def test_targets_cannot_pass_100(self):
+ from apps.accounts.forms import InvestmentClassForm
+
+ InvestmentClass.objects.create(name="Stocks", target=D("70"))
+ bonds = InvestmentClass.objects.create(name="Bonds", target=D("30"))
+
+ self.assertFalse(InvestmentClassForm({"name": "Gold", "target": "1"}).is_valid())
+ # Editing a class doesn't count its own current target
+ self.assertTrue(
+ InvestmentClassForm({"name": "Bonds", "target": "30"}, instance=bonds).is_valid()
+ )
+
+ def test_xirr_and_money_weighted_return(self):
+ from apps.investments.services import money_weighted_return, xirr
+
+ start, year = datetime.date(2025, 1, 1), datetime.date(2026, 1, 1)
+ self.assertAlmostEqual(xirr([(start, D(-1000)), (year, D(1100))]), 0.10, places=6)
+ self.assertIsNone(xirr([(start, D(-1000))]))
+
+ # A year or more: annualized
+ mw = money_weighted_return([(start, D(-1000), "in")], D(1100), year)
+ self.assertTrue(mw["annualized"])
+ self.assertAlmostEqual(float(mw["percentage"]), 10, places=3)
+
+ # Less than a year: the return over the period, not an inflated annual rate
+ half = datetime.date(2025, 7, 2)
+ mw = money_weighted_return([(start, D(-1000), "in")], D(1050), half)
+ self.assertFalse(mw["annualized"])
+ self.assertAlmostEqual(float(mw["percentage"]), 5, places=3)
+
+ # Money added late weighs less: +100 on 1000 held all year beats the
+ # simple +100 / 2000 = 5% when the second 1000 only came in at the end
+ flows = [(start, D(-1000), "in"), (datetime.date(2025, 12, 31), D(-1000), "in")]
+ mw = money_weighted_return(flows, D(2100), year)
+ self.assertGreater(float(mw["percentage"]), 9.9)
+
+ def test_home_currency_counts_the_exchange_rate_move(self):
+ from apps.investments.services import in_home_currency
+
+ usd = Currency.objects.create(code="USD", name="Dollar", decimal_places=2)
+ usd.exchange_currency = self.brl
+ usd.save()
+ etf = Currency.objects.create(
+ code="VT", name="VT", decimal_places=4, exchange_currency=usd
+ )
+ cash = Account.objects.create(name="USD cash", currency=usd)
+ holding = Account.objects.create(
+ name="VT", currency=etf, exchange_currency=usd, is_investment=True
+ )
+ aware = lambda d: timezone.make_aware(datetime.datetime.combine(d, datetime.time()))
+ ExchangeRate.objects.create(from_currency=usd, to_currency=self.brl, rate=D("5"), date=aware(self.d1))
+ ExchangeRate.objects.create(from_currency=usd, to_currency=self.brl, rate=D("6"), date=aware(timezone.localdate()))
+ ExchangeRate.objects.create(from_currency=etf, to_currency=usd, rate=D("100"), date=aware(timezone.localdate()))
+ transfer(cash, "1000", holding, "10", self.d1)
+
+ perf = get_holding_performance(holding)
+ self.assertEqual(perf.total_return, D("0")) # flat in dollars
+
+ home = in_home_currency(perf)
+ self.assertEqual(home["currency"], self.brl)
+ self.assertEqual(home["market_value"], D("6000"))
+ self.assertEqual(home["cost_basis"], D("5000"))
+ self.assertEqual(home["total_return"], D("1000"))
+ self.assertEqual(home["currency_effect"], D("1000")) # all of it from the dollar
+
+ totals = summarize([perf], convert=True)
+ self.assertEqual([t["currency"] for t in totals], [self.brl])
+ self.assertEqual(totals[0]["converted_from"], [usd])
+ self.assertEqual(totals[0]["breakdown"][0]["currency"], usd)
+ self.assertEqual(totals[0]["breakdown"][0]["market_value"], D("1000"))
+ # A single-currency total has nothing to break down
+ self.assertEqual(summarize([perf])[0]["breakdown"], [])
+ # No rate to anything: stays in its own currency
+ self.assertEqual(summarize([get_holding_performance(self.etf)], convert=True)[0]["currency"], self.eur)
+
+
+@override_settings(
+ STORAGES={
+ "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
+ "staticfiles": {
+ "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
+ },
+ },
+ WHITENOISE_AUTOREFRESH=True,
+ DEMO=False,
+)
+class InvestmentViewTests(TestCase):
+ HTMX = {"HTTP_HX_REQUEST": "true"}
+
+ def setUp(self):
+ User = get_user_model()
+ self.owner = User.objects.create_user(email="o@test.com", password="pw")
+ self.stranger = User.objects.create_user(email="s@test.com", password="pw")
+ brl = Currency.objects.create(code="BRL", name="Real", decimal_places=2)
+ self.account = Account.all_objects.create(
+ name="CDB", currency=brl, is_investment=True, owner=self.owner
+ )
+
+ def test_owner_sees_list_and_detail(self):
+ self.client.force_login(self.owner)
+ response = self.client.get(reverse("investments_list"), **self.HTMX)
+ self.assertContains(response, "CDB")
+ response = self.client.get(
+ reverse("investment_detail", args=[self.account.id]), **self.HTMX
+ )
+ self.assertEqual(response.status_code, 200)
+
+ def test_shared_account_keeps_its_owners_class(self):
+ from apps.accounts.forms import AccountForm
+
+ cls = InvestmentClass.all_objects.create(name="Stocks", owner=self.owner)
+ self.account.investment_class = cls
+ self.account.save()
+ self.account.shared_with.add(self.stranger)
+
+ write_current_user(self.stranger)
+ try:
+ form = AccountForm(
+ {"name": "CDB", "currency": self.account.currency_id,
+ "is_investment": "on", "investment_class": str(cls.id)},
+ instance=self.account,
+ )
+ self.assertTrue(form.is_valid(), form.errors)
+ self.assertEqual(form.cleaned_data["investment_class"], cls)
+ finally:
+ delete_current_user()
+ # Not a new class named after the id
+ self.assertEqual(InvestmentClass.all_objects.count(), 1)
+
+ def test_rebalance_and_class_pages_render(self):
+ self.client.force_login(self.owner)
+ stocks = InvestmentClass.all_objects.create(name="Stocks", target=D("60"), owner=self.owner)
+ self.account.investment_class = stocks
+ self.account.save()
+
+ # Targets short of 100%: a warning instead of amounts
+ response = self.client.get(reverse("investments_rebalance"), **self.HTMX)
+ self.assertContains(response, "alert-warning")
+
+ response = self.client.post(
+ reverse("investment_class_add"), {"name": "Bonds", "target": "40"}, **self.HTMX
+ )
+ self.assertEqual(response.status_code, 204)
+ response = self.client.get(reverse("investments_rebalance"), **self.HTMX)
+ self.assertContains(response, "Bonds")
+ self.assertNotContains(response, "alert-warning")
+
+ response = self.client.get(reverse("investment_classes_list"), **self.HTMX)
+ self.assertContains(response, "Stocks")
+ response = self.client.get(
+ reverse("investment_class_edit", args=[stocks.id]), **self.HTMX
+ )
+ self.assertContains(response, "Stocks")
+ # Someone else's class is out of reach
+ self.client.force_login(self.stranger)
+ response = self.client.get(
+ reverse("investment_class_edit", args=[stocks.id]), **self.HTMX
+ )
+ self.assertEqual(response.status_code, 404)
+
+ def test_charts_render(self):
+ self.client.force_login(self.owner)
+ response = self.client.get(reverse("investments_charts"), **self.HTMX)
+ self.assertContains(response, "investment-charts-0")
+
+ def test_stranger_cannot_see_detail(self):
+ self.client.force_login(self.stranger)
+ response = self.client.get(
+ reverse("investment_detail_index", args=[self.account.id])
+ )
+ self.assertEqual(response.status_code, 404)
diff --git a/app/apps/investments/urls.py b/app/apps/investments/urls.py
new file mode 100644
index 00000000..25c52f19
--- /dev/null
+++ b/app/apps/investments/urls.py
@@ -0,0 +1,49 @@
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+ path("investments/", views.investments_index, name="investments_index"),
+ path("investments/list/", views.investments_list, name="investments_list"),
+ path("investments/charts/", views.investments_charts, name="investments_charts"),
+ path(
+ "investments/rebalance/",
+ views.investments_rebalance,
+ name="investments_rebalance",
+ ),
+ path(
+ "investments/classes/",
+ views.investment_classes_index,
+ name="investment_classes_index",
+ ),
+ path(
+ "investments/classes/list/",
+ views.investment_classes_list,
+ name="investment_classes_list",
+ ),
+ path(
+ "investments/classes/add/",
+ views.investment_class_add,
+ name="investment_class_add",
+ ),
+ path(
+ "investments/classes//edit/",
+ views.investment_class_edit,
+ name="investment_class_edit",
+ ),
+ path(
+ "investments/classes//delete/",
+ views.investment_class_delete,
+ name="investment_class_delete",
+ ),
+ path(
+ "investments//",
+ views.investment_detail_index,
+ name="investment_detail_index",
+ ),
+ path(
+ "investments//details/",
+ views.investment_detail,
+ name="investment_detail",
+ ),
+]
diff --git a/app/apps/investments/views.py b/app/apps/investments/views.py
new file mode 100644
index 00000000..db0dec6c
--- /dev/null
+++ b/app/apps/investments/views.py
@@ -0,0 +1,297 @@
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponse
+from django.shortcuts import get_object_or_404, render
+from django.utils.formats import date_format
+from django.utils.translation import gettext as _
+from django.views.decorators.http import require_http_methods
+
+from apps.accounts.forms import InvestmentClassForm
+from apps.accounts.models import Account, InvestmentClass
+from apps.common.decorators.htmx import only_htmx
+from apps.common.functions.format import get_format
+from apps.common.functions.permissions import READ, get_shared_object_or_error
+from apps.investments.services import (
+ ZERO,
+ get_holding_performance,
+ in_home_currency,
+ income_entity_ids,
+ portfolio_charts,
+ rebalance,
+ summarize,
+)
+from apps.transactions.models import Transaction
+
+
+def _holdings():
+ return Account.objects.filter(is_investment=True, is_archived=False).select_related(
+ "group", "currency", "exchange_currency"
+ )
+
+
+@login_required
+def investments_index(request):
+ return render(request, "investments/pages/index.html")
+
+
+@only_htmx
+@login_required
+def investments_list(request):
+ performances = [get_holding_performance(account) for account in _holdings()]
+
+ groups = {}
+ for perf in performances:
+ group = perf.account.group
+ groups.setdefault(group.id if group else None, (group, []))[1].append(perf)
+
+ return render(
+ request,
+ "investments/fragments/list.html",
+ {
+ "groups": [
+ {"group": group, "holdings": items, "totals": summarize(items)}
+ for group, items in sorted(
+ groups.values(), key=lambda g: (g[0] is None, g[0] and g[0].name)
+ )
+ ],
+ "totals": summarize(performances, convert=True),
+ },
+ )
+
+
+def _money(currency):
+ """What chartMoney (charts.js) needs to format amounts like the rest of the app."""
+ return {
+ "prefix": currency.prefix,
+ "suffix": currency.suffix,
+ "decimals": currency.decimal_places,
+ "decimal": get_format("DECIMAL_SEPARATOR"),
+ "thousand": get_format("THOUSAND_SEPARATOR"),
+ }
+
+
+def _number(amount):
+ return None if amount is None else float(amount)
+
+
+def _per_month(history, entries):
+ """Sums (date, amount) entries into the months of ``history``, zero when empty."""
+ sums = {(p["date"].year, p["date"].month): ZERO for p in history}
+ for date, amount in entries:
+ if (date.year, date.month) in sums:
+ sums[(date.year, date.month)] += amount
+ return [float(amount) for amount in sums.values()]
+
+
+def _trades_per_month(history, trades, kind):
+ """The last trade price of each month, None in months without one."""
+ prices = {(p["date"].year, p["date"].month): None for p in history}
+ for date, trade_kind, price in trades:
+ if trade_kind == kind and (date.year, date.month) in prices:
+ prices[(date.year, date.month)] = float(price)
+ return list(prices.values())
+
+
+# Categorical slots in fixed order; past the last one, the rest fold into "Other"
+PALETTE = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6"]
+OTHER_COLOR = "#898781"
+
+
+def _shares(items):
+ colors = PALETTE[: len(items)]
+ if len(items) > len(PALETTE):
+ rest = sum(amount for _name, amount in items[len(PALETTE) - 1 :])
+ items = items[: len(PALETTE) - 1] + [(_("Other"), rest)]
+ colors = PALETTE[:-1] + [OTHER_COLOR]
+ total = sum(amount for _name, amount in items)
+ return [
+ {
+ "key": name,
+ "name": str(name),
+ "amount": amount,
+ "share": amount / total * 100 if total else 0,
+ "color": color,
+ }
+ for (name, amount), color in zip(items, colors)
+ ]
+
+
+@only_htmx
+@login_required
+def investments_charts(request):
+ performances = [
+ get_holding_performance(account, with_history=True) for account in _holdings()
+ ]
+ charts = portfolio_charts(performances)
+ targeted = _targeted()
+ # Shares only mean something against targets that add up to 100%
+ if sum(cls.target for cls in targeted) != 100:
+ targeted = []
+ for i, chart in enumerate(charts):
+ chart["data_id"] = f"investment-charts-{i}"
+ chart["returns_height"] = max(128, 32 * len(chart["returns"]))
+ plan = {row["class"]: row for row in rebalance(chart["classes"], targeted)["rows"]}
+ chart["classes"] = _shares(
+ [(cls or _("Unclassified"), amount) for cls, amount in chart["classes"]]
+ )
+ for share in chart["classes"]:
+ share["plan"] = plan.get(share["key"])
+ chart["currencies"] = _shares(chart["currencies"])
+ chart["data"] = {
+ "currency": _money(chart["currency"]),
+ "classes": [[s["name"], float(s["amount"]), s["color"]] for s in chart["classes"]],
+ "currencies": [[s["name"], float(s["amount"]), s["color"]] for s in chart["currencies"]],
+ "returns": [[name, float(amount)] for name, amount in chart["returns"]],
+ "history": {
+ "labels": [date_format(d, "M Y") for d, _t in chart["history"]],
+ "value": [float(t[0]) for _d, t in chart["history"]],
+ "invested": [float(t[1]) for _d, t in chart["history"]],
+ },
+ }
+ return render(request, "investments/fragments/charts.html", {"charts": charts})
+
+
+def _targeted():
+ return list(InvestmentClass.objects.filter(target__isnull=False))
+
+
+@only_htmx
+@login_required
+def investments_rebalance(request):
+ targeted = _targeted()
+ total_target = sum(cls.target for cls in targeted)
+ plans = []
+ for chart in portfolio_charts([get_holding_performance(a) for a in _holdings()]):
+ plan = rebalance(chart["classes"], targeted)
+ plan["currency"] = chart["currency"]
+ for row in plan["rows"]:
+ # Fractions of a cent shouldn't show as "Buy 0.00"
+ row["move"] = round(row["move"], chart["currency"].decimal_places)
+ row["size"] = abs(row["move"])
+ plans.append(plan)
+ return render(
+ request,
+ "investments/fragments/rebalance.html",
+ {"plans": plans, "total_target": total_target, "targeted": targeted},
+ )
+
+
+@login_required
+def investment_detail_index(request, account_id):
+ account = get_shared_object_or_error(
+ Account, request, id=account_id, level=READ, is_investment=True
+ )
+ return render(request, "investments/pages/detail.html", {"account": account})
+
+
+@only_htmx
+@login_required
+def investment_detail(request, account_id):
+ account = get_shared_object_or_error(
+ Account, request, id=account_id, level=READ, is_investment=True
+ )
+ perf = get_holding_performance(account, with_history=True)
+ transactions = (
+ Transaction.objects.filter(account=account)
+ .select_related("linked_transaction__account__currency", "category")
+ .order_by("-date", "-id")[:50]
+ )
+ income = []
+ entity_ids = income_entity_ids(account)
+ if entity_ids:
+ income = (
+ Transaction.objects.filter(
+ entities__in=entity_ids, type=Transaction.Type.INCOME
+ )
+ .exclude(account=account)
+ .distinct()
+ .select_related("account__currency")
+ .order_by("-date", "-id")[:20]
+ )
+
+ history = perf.history
+ income_flows = [(d, amount) for d, amount, kind in perf.flows if kind == "income"]
+ chart = {
+ "currency": _money(perf.currency),
+ "labels": [date_format(p["date"], "M Y") for p in history],
+ "value": [_number(p["value"]) for p in history],
+ "invested": [float(p["invested"]) for p in history],
+ "price": [_number(p["price"]) for p in history] if perf.priced else None,
+ "average_cost": [_number(p["average_cost"]) for p in history],
+ "buys": _trades_per_month(history, perf.trades, "buy"),
+ "sells": _trades_per_month(history, perf.trades, "sell"),
+ "earnings": _per_month(history, perf.results) if perf.results else None,
+ "income": _per_month(history, income_flows) if income_flows else None,
+ }
+
+ return render(
+ request,
+ "investments/fragments/detail.html",
+ {
+ "account": account,
+ "perf": perf,
+ "home": in_home_currency(perf),
+ "transactions": transactions,
+ "income": income,
+ "income_entities": list(account.income_entities.all()),
+ "chart": chart,
+ },
+ )
+
+
+@login_required
+@require_http_methods(["GET"])
+def investment_classes_index(request):
+ return render(request, "investment_classes/pages/index.html")
+
+
+@only_htmx
+@login_required
+@require_http_methods(["GET"])
+def investment_classes_list(request):
+ classes = InvestmentClass.objects.all()
+ return render(
+ request,
+ "investment_classes/fragments/list.html",
+ {
+ "classes": classes,
+ "total_target": sum(cls.target or 0 for cls in classes),
+ },
+ )
+
+
+@only_htmx
+@login_required
+@require_http_methods(["GET", "POST"])
+def investment_class_add(request):
+ return _investment_class_form(request, InvestmentClass(), _("Asset class added successfully"))
+
+
+@only_htmx
+@login_required
+@require_http_methods(["GET", "POST"])
+def investment_class_edit(request, pk):
+ cls = get_object_or_404(InvestmentClass, pk=pk)
+ return _investment_class_form(request, cls, _("Asset class updated successfully"))
+
+
+def _investment_class_form(request, instance, success):
+ form = InvestmentClassForm(request.POST or None, instance=instance)
+ if request.method == "POST" and form.is_valid():
+ form.save()
+ messages.success(request, success)
+ return HttpResponse(status=204, headers={"HX-Trigger": "updated, hide_offcanvas"})
+ return render(
+ request,
+ "investment_classes/fragments/form.html",
+ {"form": form, "object": instance},
+ )
+
+
+@only_htmx
+@login_required
+@require_http_methods(["DELETE"])
+def investment_class_delete(request, pk):
+ get_object_or_404(InvestmentClass, pk=pk).delete()
+ messages.success(request, _("Asset class deleted successfully"))
+ return HttpResponse(status=204, headers={"HX-Trigger": "updated, hide_offcanvas"})
diff --git a/app/apps/transactions/models.py b/app/apps/transactions/models.py
index 03255867..8093b2e1 100644
--- a/app/apps/transactions/models.py
+++ b/app/apps/transactions/models.py
@@ -509,6 +509,30 @@ class Transaction(OwnedObject):
self.save()
return to_transaction
+ def transfer_link_error(self, other):
+ """Why ``self`` and ``other`` can't be joined into a transfer, or None."""
+ if self.linked_transaction_id == other.pk and other.linked_transaction_id == self.pk:
+ return None
+ if {self.type, other.type} != {self.Type.INCOME, self.Type.EXPENSE}:
+ return _("A transfer needs one expense and one income.")
+ if self.account_id == other.account_id:
+ return _("A transfer needs two different accounts.")
+ if self.linked_transaction_id not in (None, other.pk) or (
+ other.linked_transaction_id not in (None, self.pk)
+ ):
+ return _("This transaction is already part of another transfer.")
+ return None
+
+ def link_transfer(self, other):
+ """Joins an existing expense and income into a transfer."""
+ error = self.transfer_link_error(other)
+ if error:
+ raise ValidationError(error)
+ # update(): only the link changes; dates, amounts and rules are left alone
+ Transaction.all_objects.filter(pk=self.pk).update(linked_transaction=other)
+ Transaction.all_objects.filter(pk=other.pk).update(linked_transaction=self)
+ self.linked_transaction, other.linked_transaction = other, self
+
@property
def transfer_from(self):
if self.linked_transaction_id and self.type == self.Type.INCOME:
diff --git a/app/apps/transactions/tests/test_transaction_search.py b/app/apps/transactions/tests/test_transaction_search.py
new file mode 100644
index 00000000..6b02e95b
--- /dev/null
+++ b/app/apps/transactions/tests/test_transaction_search.py
@@ -0,0 +1,74 @@
+from datetime import timedelta
+from decimal import Decimal
+
+from django.contrib.auth import get_user_model
+from django.test import TestCase
+from django.urls import reverse
+from django.utils import timezone
+
+from apps.accounts.models import Account
+from apps.currencies.models import Currency
+from apps.transactions.models import (
+ Transaction,
+ TransactionCategory,
+ TransactionEntity,
+ TransactionTag,
+)
+
+
+class TransactionSearchTests(TestCase):
+ def setUp(self):
+ self.user = get_user_model().objects.create_user(
+ email="search@test.com", password="pass"
+ )
+ self.client.login(username="search@test.com", password="pass")
+ self.user.settings.date_format = "d/m/Y"
+ self.user.settings.number_format = "DC"
+ self.user.settings.save()
+
+ currency = Currency.objects.create(code="BRL", name="Real", decimal_places=2)
+ self.account = Account.objects.create(name="Wallet", currency=currency)
+ self.today = timezone.localdate(timezone.now())
+
+ def make(self, days, description="", amount="10", **kwargs):
+ return Transaction.objects.create(
+ account=self.account,
+ type=Transaction.Type.EXPENSE,
+ date=self.today + timedelta(days=days),
+ reference_date=(self.today + timedelta(days=days)).replace(day=1),
+ amount=Decimal(amount),
+ description=description,
+ **kwargs,
+ )
+
+ def search(self, q=""):
+ response = self.client.get(reverse("transactions_search"), {"q": q})
+ self.assertEqual(response.status_code, 200)
+ return [row["description"] for row in response.json()]
+
+ def test_order_matches_monthly_view(self):
+ for days, name in [(-30, "old"), (-2, "recent"), (-1, "yesterday"),
+ (10, "far"), (2, "soon"), (1, "tomorrow"), (0, "today")]:
+ self.make(days, name)
+ self.assertEqual(
+ self.search(),
+ ["today", "tomorrow", "yesterday", "soon", "far", "recent", "old"],
+ )
+
+ def test_search_fields(self):
+ t = self.make(-5, "Groceries", amount="1234.56")
+ t.category = TransactionCategory.objects.create(name="Food")
+ t.save()
+ t.tags.add(TransactionTag.objects.create(name="home"),
+ TransactionTag.objects.create(name="weekly"))
+ t.entities.add(TransactionEntity.objects.create(name="Market"))
+ self.make(-6, "Other", amount="5")
+
+ day = (self.today - timedelta(days=5))
+ for q in ["food", "market", "1.234,56", "1234",
+ day.strftime("%d/%m/%Y"), day.strftime("%d/%m"),
+ "home weekly", "groceries food"]:
+ self.assertEqual(self.search(q), ["Groceries"], q)
+
+ self.assertEqual(self.search("wallet"), ["Groceries", "Other"])
+ self.assertEqual(self.search("groceries nomatch"), [])
diff --git a/app/apps/transactions/utils/select_search.py b/app/apps/transactions/utils/select_search.py
new file mode 100644
index 00000000..161383a5
--- /dev/null
+++ b/app/apps/transactions/utils/select_search.py
@@ -0,0 +1,95 @@
+import datetime
+import re
+from decimal import Decimal, InvalidOperation
+
+from apps.common.functions.format import get_format
+from apps.common.utils.django import django_to_python_datetime
+from apps.transactions.templatetags.currency_display import currency_display
+from django.db.models import Q
+from django.template.defaultfilters import date as date_filter
+
+
+def transaction_option(t):
+ """Data used by the transaction picker (tom-select) to render a mini transaction."""
+ currency = t.account.currency
+ return {
+ "value": str(t.id),
+ "text": str(t),
+ "description": t.description,
+ "date": date_filter(t.date, "SHORT_DATE_FORMAT"),
+ "type": t.type,
+ "is_paid": t.is_paid,
+ "amount": currency_display(
+ t.amount,
+ currency.prefix,
+ currency.suffix,
+ currency.decimal_places,
+ string=True,
+ ),
+ "account": (
+ f"{t.account.group.name} • {t.account.name}"
+ if t.account.group
+ else t.account.name
+ ),
+ "category": t.category.name if t.category else "",
+ "tags": [tag.name for tag in t.tags.all()],
+ "entities": [entity.name for entity in t.entities.all()],
+ }
+
+
+def _parse_date(term):
+ """Match a term against the user's date format; without a year it matches any year."""
+ fmt = django_to_python_datetime(get_format("SHORT_DATE_FORMAT"))
+ try:
+ return Q(date=datetime.datetime.strptime(term, fmt).date())
+ except ValueError:
+ pass
+ # Drop the year and its adjacent separator, e.g. "%d/%m/%Y" -> "%d/%m"
+ no_year = re.sub(r"[^%\w]?%[Yy][^%\w]?", "", fmt, count=1).strip()
+ if no_year == fmt or not no_year:
+ return None
+ try:
+ # 2000 is a leap year, so 29/02 parses
+ parsed = datetime.datetime.strptime(f"{term} 2000", f"{no_year} %Y")
+ except ValueError:
+ return None
+ return Q(date__day=parsed.day, date__month=parsed.month)
+
+
+def _parse_amount(term):
+ if not re.fullmatch(r"[\d.,\s]*\d[\d.,\s]*", term):
+ return None
+ thousand = get_format("THOUSAND_SEPARATOR")
+ decimal = get_format("DECIMAL_SEPARATOR")
+ normalized = term.replace(thousand, "").replace(decimal, ".")
+ try:
+ value = Decimal(normalized)
+ except InvalidOperation:
+ return None
+ if decimal not in term:
+ # Whole number: "12" finds 12.00 to 12.99
+ return Q(amount__gte=value, amount__lt=value + 1)
+ return Q(amount=value)
+
+
+def search_transactions(queryset, search_term):
+ """Every word must match some field (description, category, entity, date, amount...)."""
+ matches = queryset.model.objects.all()
+ for term in search_term.split():
+ term_q = (
+ Q(description__icontains=term)
+ | Q(notes__icontains=term)
+ | Q(internal_note__icontains=term)
+ | Q(tags__name__icontains=term)
+ | Q(entities__name__icontains=term)
+ | Q(category__name__icontains=term)
+ | Q(account__name__icontains=term)
+ | Q(account__group__name__icontains=term)
+ )
+ for extra in (_parse_date(term), _parse_amount(term)):
+ if extra is not None:
+ term_q |= extra
+ # One filter() per word so different words can match different tags/entities
+ matches = matches.filter(term_q)
+ # id__in avoids duplicate rows from the tag/entity joins
+ return queryset.filter(id__in=matches.values("id"))
diff --git a/app/apps/transactions/views/transactions.py b/app/apps/transactions/views/transactions.py
index a18c17bf..07b7f731 100644
--- a/app/apps/transactions/views/transactions.py
+++ b/app/apps/transactions/views/transactions.py
@@ -20,11 +20,15 @@ from apps.transactions.utils.calculations import (
calculate_percentage_distribution,
)
from apps.transactions.utils.default_ordering import default_order
+from apps.transactions.utils.select_search import (
+ search_transactions,
+ transaction_option,
+)
from dateutil.relativedelta import relativedelta
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
-from django.db.models import Case, IntegerField, Q, Value, When
+from django.db.models import Case, F, IntegerField, Q, Value, When
from django.http import FileResponse, Http404, HttpResponse, JsonResponse, QueryDict
from django.shortcuts import get_object_or_404, render
from django.utils import timezone
@@ -119,6 +123,15 @@ def transaction_attachment_delete(request, attachment_id):
return response
+def _prefill(request, *fields, entities=False):
+ """Form initial values passed as query parameters; only the named ones, and
+ only when present, so they never override other defaults."""
+ initial = {name: request.GET[name] for name in fields if request.GET.get(name)}
+ if entities and request.GET.getlist("entities"):
+ initial["entities"] = request.GET.getlist("entities") # entity names
+ return initial
+
+
@only_htmx
@login_required
@require_http_methods(["GET", "POST"])
@@ -204,6 +217,8 @@ def transaction_add(request):
initial={
"date": expected_date,
"type": transaction_type,
+ # Optional prefill, e.g. a dividend from the investments page
+ **_prefill(request, "account", entities=True),
},
)
@@ -541,6 +556,8 @@ def transactions_transfer(request):
initial={
"reference_date": expected_date,
"date": expected_date,
+ # Optional prefill, e.g. buy/sell from the investments page
+ **_prefill(request, "from_account", "to_account"),
},
)
@@ -971,51 +988,41 @@ def transactions_trash_can_list(request):
@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
+ """Return up to 100 non-deleted transactions, ordered like the monthly view, with optional search."""
search_term = request.GET.get("q", "").strip()
today = timezone.localdate(timezone.now())
yesterday = today - timezone.timedelta(days=1)
tomorrow = today + timezone.timedelta(days=1)
- # Base queryset with selected fields
+ queryset = Transaction.objects.filter(deleted=False)
+
+ if filter_type == "expenses":
+ queryset = queryset.filter(type=Transaction.Type.EXPENSE)
+ elif filter_type == "income":
+ queryset = queryset.filter(type=Transaction.Type.INCOME)
+
+ if search_term:
+ queryset = search_transactions(queryset, search_term)
+
+ # Today, tomorrow, yesterday, then upcoming (soonest first), then past (newest first)
queryset = (
- Transaction.objects.filter(deleted=False)
- .annotate(
+ queryset.annotate(
date_order=Case(
When(date=today, then=Value(0)),
When(date=tomorrow, then=Value(1)),
When(date=yesterday, then=Value(2)),
When(date__gt=tomorrow, then=Value(3)),
- When(date__lt=yesterday, then=Value(4)),
- default=Value(5),
+ default=Value(4),
output_field=IntegerField(),
- )
+ ),
+ upcoming_date=Case(When(date__gt=tomorrow, then=F("date"))),
)
- .select_related("account", "category")
- .order_by("date_order", "date", "id")
+ .select_related("account__currency", "account__group", "category")
+ .prefetch_related("tags", "entities")
+ .order_by(
+ "date_order", F("upcoming_date").asc(nulls_last=True), "-date", "-id"
+ )[:100]
)
- 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)
+ return JsonResponse([transaction_option(t) for t in queryset], safe=False)
diff --git a/app/locale/de/LC_MESSAGES/django.po b/app/locale/de/LC_MESSAGES/django.po
index 7371dcab..4ed1ee37 100644
--- a/app/locale/de/LC_MESSAGES/django.po
+++ b/app/locale/de/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-11-08 06:01+0000\n"
+"POT-Creation-Date: 2026-11-08 18:27+0000\n"
"PO-Revision-Date: 2026-08-10 12:57+0000\n"
"Last-Translator: GitEbu \n"
"Language-Team: German \n"
"Language-Team: LANGUAGE \n"
@@ -18,73 +18,80 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: apps/accounts/forms.py:28
+#: apps/accounts/forms.py:33
msgid "Group name"
msgstr ""
-#: apps/accounts/forms.py:43 apps/accounts/forms.py:109
-#: apps/currencies/forms.py:57 apps/currencies/forms.py:91
-#: apps/currencies/forms.py:166 apps/dca/forms.py:46 apps/dca/forms.py:205
-#: apps/import_app/forms.py:32 apps/rules/forms.py:60 apps/rules/forms.py:100
-#: apps/rules/forms.py:385 apps/transactions/forms.py:219
-#: apps/transactions/forms.py:452 apps/transactions/forms.py:571
-#: apps/transactions/forms.py:718 apps/transactions/forms.py:940
-#: apps/transactions/forms.py:987 apps/transactions/forms.py:1015
-#: apps/transactions/forms.py:1046 apps/transactions/forms.py:1208
-#: apps/users/forms.py:264 apps/users/forms.py:422
+#: apps/accounts/forms.py:48 apps/accounts/forms.py:76
+#: apps/accounts/forms.py:184 apps/currencies/forms.py:57
+#: apps/currencies/forms.py:91 apps/currencies/forms.py:168
+#: apps/dca/forms.py:46 apps/dca/forms.py:192 apps/import_app/forms.py:32
+#: apps/rules/forms.py:60 apps/rules/forms.py:100 apps/rules/forms.py:385
+#: apps/transactions/forms.py:219 apps/transactions/forms.py:452
+#: apps/transactions/forms.py:571 apps/transactions/forms.py:718
+#: apps/transactions/forms.py:940 apps/transactions/forms.py:987
+#: apps/transactions/forms.py:1015 apps/transactions/forms.py:1046
+#: apps/transactions/forms.py:1208 apps/users/forms.py:264
+#: apps/users/forms.py:422
#: templates/rules/fragments/transaction_rule/dry_run/updated.html:5
#: templates/rules/fragments/transaction_rule/view.html:128
msgid "Update"
msgstr ""
-#: apps/accounts/forms.py:49 apps/accounts/forms.py:115
-#: apps/common/widgets/tom_select.py:13 apps/currencies/forms.py:63
-#: apps/currencies/forms.py:97 apps/currencies/forms.py:172
-#: apps/dca/forms.py:52 apps/dca/forms.py:211 apps/import_app/forms.py:38
-#: apps/rules/forms.py:66 apps/rules/forms.py:106 apps/rules/forms.py:391
-#: apps/transactions/forms.py:206 apps/transactions/forms.py:226
-#: apps/transactions/forms.py:459 apps/transactions/forms.py:946
-#: apps/transactions/forms.py:993 apps/transactions/forms.py:1021
-#: apps/transactions/forms.py:1052 apps/transactions/forms.py:1214
-#: apps/users/forms.py:270 apps/users/forms.py:428
-#: templates/mini_tools/simulator.html:177
+#: apps/accounts/forms.py:54 apps/accounts/forms.py:76
+#: apps/accounts/forms.py:190 apps/common/widgets/tom_select.py:15
+#: apps/currencies/forms.py:63 apps/currencies/forms.py:97
+#: apps/currencies/forms.py:174 apps/dca/forms.py:52 apps/dca/forms.py:198
+#: apps/import_app/forms.py:38 apps/rules/forms.py:66 apps/rules/forms.py:106
+#: apps/rules/forms.py:391 apps/transactions/forms.py:206
+#: apps/transactions/forms.py:226 apps/transactions/forms.py:459
+#: apps/transactions/forms.py:946 apps/transactions/forms.py:993
+#: apps/transactions/forms.py:1021 apps/transactions/forms.py:1052
+#: apps/transactions/forms.py:1214 apps/users/forms.py:270
+#: apps/users/forms.py:428 templates/mini_tools/simulator.html:177
#: templates/mini_tools/unit_price_calculator.html:168
msgid "Add"
msgstr ""
-#: apps/accounts/forms.py:57 templates/accounts/fragments/list.html:23
+#: apps/accounts/forms.py:86 templates/accounts/fragments/list.html:23
msgid "Group"
msgstr ""
-#: apps/accounts/forms.py:123
+#: apps/accounts/forms.py:93 apps/accounts/models.py:73
+#: apps/accounts/models.py:130
+#: templates/investments/fragments/rebalance.html:22
+msgid "Asset class"
+msgstr ""
+
+#: apps/accounts/forms.py:198
msgid "New balance"
msgstr ""
-#: apps/accounts/forms.py:129 apps/dca/forms.py:79 apps/dca/forms.py:86
+#: apps/accounts/forms.py:204 apps/dca/forms.py:79 apps/dca/forms.py:86
#: apps/insights/forms.py:117 apps/insights/views.py:44 apps/rules/forms.py:181
#: apps/rules/forms.py:197 apps/rules/models.py:44 apps/rules/models.py:311
#: apps/transactions/forms.py:62 apps/transactions/forms.py:330
#: apps/transactions/forms.py:510 apps/transactions/forms.py:607
#: apps/transactions/forms.py:614 apps/transactions/forms.py:823
#: apps/transactions/forms.py:1075 apps/transactions/models.py:357
-#: apps/transactions/models.py:743 apps/transactions/models.py:999
-#: apps/transactions/models.py:1306
+#: apps/transactions/models.py:767 apps/transactions/models.py:1023
+#: apps/transactions/models.py:1330
#: templates/insights/fragments/month_by_month.html:84
#: templates/insights/fragments/year_by_year.html:52
msgid "Category"
msgstr ""
-#: apps/accounts/forms.py:136 apps/dca/forms.py:95 apps/dca/forms.py:103
-#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
+#: apps/accounts/forms.py:211 apps/dca/forms.py:95 apps/dca/forms.py:103
+#: apps/export_app/forms.py:43 apps/export_app/forms.py:134
#: apps/insights/views.py:50 apps/rules/forms.py:184 apps/rules/forms.py:194
#: apps/rules/models.py:45 apps/rules/models.py:315
#: apps/transactions/filters.py:79 apps/transactions/forms.py:70
#: apps/transactions/forms.py:338 apps/transactions/forms.py:518
#: apps/transactions/forms.py:623 apps/transactions/forms.py:631
#: apps/transactions/forms.py:816 apps/transactions/forms.py:1068
-#: apps/transactions/models.py:363 apps/transactions/models.py:745
-#: apps/transactions/models.py:1003 apps/transactions/models.py:1312
-#: templates/includes/sidebar.html:156
+#: apps/transactions/models.py:363 apps/transactions/models.py:769
+#: apps/transactions/models.py:1027 apps/transactions/models.py:1336
+#: templates/includes/sidebar.html:162
#: templates/insights/fragments/month_by_month.html:29
#: templates/insights/fragments/month_by_month.html:32
#: templates/insights/fragments/year_by_year.html:24
@@ -93,23 +100,23 @@ msgstr ""
msgid "Tags"
msgstr ""
-#: apps/accounts/forms.py:179
+#: apps/accounts/forms.py:254
msgid "All paid transactions"
msgstr ""
-#: apps/accounts/forms.py:180
+#: apps/accounts/forms.py:255
msgid "Up to date"
msgstr ""
-#: apps/accounts/forms.py:181
+#: apps/accounts/forms.py:256
msgid "Up to reference month"
msgstr ""
-#: apps/accounts/forms.py:185
+#: apps/accounts/forms.py:260
msgid "Balance against"
msgstr ""
-#: apps/accounts/forms.py:189 apps/currencies/forms.py:70
+#: apps/accounts/forms.py:264 apps/currencies/forms.py:70
#: apps/dca/models.py:158 apps/rules/forms.py:176 apps/rules/forms.py:190
#: apps/rules/models.py:38 apps/rules/models.py:279
#: apps/transactions/forms.py:86 apps/transactions/forms.py:482
@@ -120,14 +127,15 @@ msgstr ""
msgid "Date"
msgstr ""
-#: apps/accounts/forms.py:194
+#: apps/accounts/forms.py:269
msgid "Reference month"
msgstr ""
-#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
+#: apps/accounts/models.py:19 apps/accounts/models.py:36
+#: apps/accounts/models.py:84 apps/dca/models.py:13
#: apps/import_app/models.py:20 apps/rules/models.py:13
#: apps/transactions/models.py:249 apps/transactions/models.py:274
-#: apps/transactions/models.py:298 apps/transactions/models.py:1274
+#: apps/transactions/models.py:298 apps/transactions/models.py:1298
#: apps/users/models.py:577 templates/account_groups/fragments/list.html:22
#: templates/accounts/fragments/list.html:22
#: templates/categories/fragments/table.html:17
@@ -136,6 +144,7 @@ msgstr ""
#: templates/exchange_rates_services/fragments/list.html:28
#: templates/import_app/fragments/profiles/list.html:34
#: templates/installment_plans/fragments/table.html:16
+#: templates/investment_classes/fragments/list.html:27
#: templates/quick_transactions/fragments/list.html:13
#: templates/recurring_transactions/fragments/table.html:18
#: templates/rules/fragments/list.html:24
@@ -144,42 +153,119 @@ msgstr ""
msgid "Name"
msgstr ""
-#: apps/accounts/models.py:18 apps/accounts/models.py:33
+#: apps/accounts/models.py:25 apps/accounts/models.py:88
msgid "Account Group"
msgstr ""
-#: apps/accounts/models.py:19 templates/account_groups/fragments/list.html:9
+#: apps/accounts/models.py:26 templates/account_groups/fragments/list.html:9
#: templates/account_groups/pages/index.html:4
-#: templates/includes/sidebar.html:176
+#: templates/includes/sidebar.html:182
msgid "Account Groups"
msgstr ""
-#: apps/accounts/models.py:39 apps/currencies/models.py:44
+#: apps/accounts/models.py:43
+msgid "Target allocation (%)"
+msgstr ""
+
+#: apps/accounts/models.py:45
+msgid ""
+"Share of your investments you want in this class. Leave it empty to leave "
+"the class out of the targets."
+msgstr ""
+
+#: apps/accounts/models.py:61
+msgid "You already have an asset class with this name."
+msgstr ""
+
+#: apps/accounts/models.py:66
+#, python-format
+msgid ""
+"Targets can't add up to more than 100%%. Other classes already take "
+"%(taken)s%%."
+msgstr ""
+
+#: apps/accounts/models.py:74 apps/export_app/forms.py:131
+#: templates/includes/sidebar.html:190
+#: templates/investment_classes/fragments/list.html:9
+#: templates/investment_classes/pages/index.html:4
+#: templates/investments/fragments/rebalance.html:6
+#: templates/investments/fragments/rebalance.html:11
+msgid "Asset classes"
+msgstr ""
+
+#: apps/accounts/models.py:94 apps/currencies/models.py:44
#: templates/accounts/fragments/list.html:24
#: templates/mini_tools/simulator.html:37
#: templates/yearly_overview/pages/yearly_overview_base.html:19
msgid "Currency"
msgstr ""
-#: apps/accounts/models.py:45 apps/currencies/models.py:27
+#: apps/accounts/models.py:100 apps/currencies/models.py:27
#: templates/accounts/fragments/list.html:25
msgid "Exchange Currency"
msgstr ""
-#: apps/accounts/models.py:50 apps/currencies/models.py:32
+#: apps/accounts/models.py:105 apps/currencies/models.py:32
msgid "Default currency for exchange calculations"
msgstr ""
-#: apps/accounts/models.py:55
+#: apps/accounts/models.py:110
msgid "Asset account"
msgstr ""
-#: apps/accounts/models.py:57
+#: apps/accounts/models.py:112
msgid ""
"Asset accounts count towards your Net Worth, but not towards your month."
msgstr ""
-#: apps/accounts/models.py:62 apps/currencies/models.py:37
+#: apps/accounts/models.py:117 templates/investments/fragments/list.html:46
+msgid "Investment"
+msgstr ""
+
+#: apps/accounts/models.py:119
+msgid ""
+"Show this account in the Investments tracker. Set an exchange currency to "
+"price it by units (stocks, ETFs, crypto); leave it empty to track it by "
+"value (fixed income, property, pensions)."
+msgstr ""
+
+#: apps/accounts/models.py:132
+msgid ""
+"Groups investments in the allocation charts, e.g. Fixed income, Stocks, Real "
+"estate."
+msgstr ""
+
+#: apps/accounts/models.py:140
+msgid "Income entities"
+msgstr ""
+
+#: apps/accounts/models.py:142
+msgid ""
+"Transactions with any of these entities count towards this investment's "
+"return: in this account, and income paid into other accounts (dividends, "
+"rent)."
+msgstr ""
+
+#: apps/accounts/models.py:151
+msgid "Income categories"
+msgstr ""
+
+#: apps/accounts/models.py:153
+msgid ""
+"Transactions in this account with any of these categories are its result "
+"(interest, taxes, fees). On accounts tracked by value, everything else that "
+"isn't a transfer counts as money coming in or going out."
+msgstr ""
+
+#: apps/accounts/models.py:162
+msgid "Income tags"
+msgstr ""
+
+#: apps/accounts/models.py:164
+msgid "Same as income categories, for transactions with any of these tags."
+msgstr ""
+
+#: apps/accounts/models.py:169 apps/currencies/models.py:37
#: templates/accounts/fragments/list.html:27
#: templates/categories/fragments/list.html:20
#: templates/currencies/fragments/list.html:24
@@ -188,17 +274,17 @@ msgstr ""
msgid "Archived"
msgstr ""
-#: apps/accounts/models.py:63
+#: apps/accounts/models.py:170
msgid "Archived accounts don't show up nor count towards your net worth"
msgstr ""
-#: apps/accounts/models.py:75 apps/rules/forms.py:173 apps/rules/forms.py:187
+#: apps/accounts/models.py:182 apps/rules/forms.py:173 apps/rules/forms.py:187
#: apps/rules/models.py:35 apps/rules/models.py:267
#: apps/transactions/forms.py:82 apps/transactions/forms.py:350
#: apps/transactions/forms.py:477 apps/transactions/forms.py:808
#: apps/transactions/forms.py:1060 apps/transactions/models.py:329
-#: apps/transactions/models.py:703 apps/transactions/models.py:981
-#: apps/transactions/models.py:1280
+#: apps/transactions/models.py:727 apps/transactions/models.py:1005
+#: apps/transactions/models.py:1304
#: templates/installment_plans/fragments/table.html:17
#: templates/quick_transactions/fragments/list.html:14
#: templates/recurring_transactions/fragments/table.html:19
@@ -206,17 +292,17 @@ msgstr ""
msgid "Account"
msgstr ""
-#: apps/accounts/models.py:76 apps/export_app/forms.py:19
-#: apps/export_app/forms.py:129 apps/transactions/filters.py:63
+#: apps/accounts/models.py:183 apps/export_app/forms.py:19
+#: apps/export_app/forms.py:130 apps/transactions/filters.py:63
#: templates/accounts/fragments/list.html:9
-#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:168
-#: templates/includes/sidebar.html:170
+#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:174
+#: templates/includes/sidebar.html:176
#: templates/monthly_overview/pages/overview.html:77
#: templates/transactions/pages/transactions.html:26
msgid "Accounts"
msgstr ""
-#: apps/accounts/models.py:93
+#: apps/accounts/models.py:200
msgid "Exchange currency cannot be the same as the account's main currency."
msgstr ""
@@ -234,7 +320,7 @@ msgstr ""
#: apps/accounts/views/account_groups.py:109
#: apps/accounts/views/accounts.py:137 apps/dca/views.py:106
-#: apps/import_app/views.py:147 apps/rules/views.py:191
+#: apps/import_app/views.py:148 apps/rules/views.py:191
#: apps/transactions/views/categories.py:163
#: apps/transactions/views/entities.py:135 apps/transactions/views/tags.py:131
msgid "Item no longer shared with you"
@@ -242,7 +328,7 @@ msgstr ""
#: apps/accounts/views/account_groups.py:132
#: apps/accounts/views/accounts.py:182 apps/dca/views.py:131
-#: apps/import_app/views.py:273 apps/rules/views.py:217
+#: apps/import_app/views.py:274 apps/rules/views.py:217
#: apps/transactions/views/categories.py:188
#: apps/transactions/views/entities.py:160 apps/transactions/views/tags.py:154
msgid "Ownership taken successfully"
@@ -250,7 +336,7 @@ msgstr ""
#: apps/accounts/views/account_groups.py:152
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:151
-#: apps/import_app/views.py:294 apps/rules/views.py:238
+#: apps/import_app/views.py:295 apps/rules/views.py:238
#: apps/transactions/views/categories.py:130
#: apps/transactions/views/entities.py:180 apps/transactions/views/tags.py:174
msgid "Configuration saved successfully"
@@ -308,7 +394,7 @@ msgstr ""
msgid "Invalid entity data. Provide an ID or name."
msgstr ""
-#: apps/api/serializers/transactions.py:141 apps/transactions/models.py:953
+#: apps/api/serializers/transactions.py:141 apps/transactions/models.py:977
msgid "This field is required."
msgstr ""
@@ -328,13 +414,14 @@ msgstr ""
msgid "Shared with users"
msgstr ""
-#: apps/common/fields/forms/dynamic_select.py:71
-#: apps/common/fields/forms/dynamic_select.py:142
+#: apps/common/fields/forms/dynamic_select.py:79
+#: apps/common/fields/forms/dynamic_select.py:150
msgid "Error creating new instance"
msgstr ""
#: apps/common/fields/forms/grouped_select.py:24
-#: apps/common/widgets/tom_select.py:92 apps/common/widgets/tom_select.py:95
+#: apps/common/widgets/tom_select.py:94 apps/common/widgets/tom_select.py:97
+#: templates/investments/fragments/list.html:26
msgid "Ungrouped"
msgstr ""
@@ -477,14 +564,15 @@ msgid "Today"
msgstr ""
#: apps/common/widgets/datepicker.py:142
+#: templates/investments/fragments/rebalance.html:24
msgid "Now"
msgstr ""
-#: apps/common/widgets/tom_select.py:11
+#: apps/common/widgets/tom_select.py:13
msgid "Remove"
msgstr ""
-#: apps/common/widgets/tom_select.py:15 templates/mini_tools/simulator.html:187
+#: apps/common/widgets/tom_select.py:17 templates/mini_tools/simulator.html:187
#: templates/mini_tools/unit_price_calculator.html:180
#: templates/monthly_overview/pages/overview.html:286
#: templates/transactions/fragments/filter_actions.html:48
@@ -492,7 +580,7 @@ msgstr ""
msgid "Clear"
msgstr ""
-#: apps/common/widgets/tom_select.py:16
+#: apps/common/widgets/tom_select.py:18
msgid "No results..."
msgstr ""
@@ -526,10 +614,10 @@ msgid "Decimal Places"
msgstr ""
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
-#: apps/export_app/forms.py:130 apps/transactions/filters.py:70
+#: apps/export_app/forms.py:132 apps/transactions/filters.py:70
#: templates/currencies/fragments/list.html:9
-#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:182
-#: templates/includes/sidebar.html:184
+#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:196
+#: templates/includes/sidebar.html:198
#: templates/monthly_overview/pages/overview.html:63
#: templates/transactions/pages/transactions.html:12
msgid "Currencies"
@@ -561,9 +649,9 @@ msgid "Auto"
msgstr ""
#: apps/currencies/models.py:82 apps/export_app/forms.py:67
-#: apps/export_app/forms.py:142 templates/exchange_rates/fragments/list.html:10
+#: apps/export_app/forms.py:144 templates/exchange_rates/fragments/list.html:10
#: templates/exchange_rates/pages/index.html:4
-#: templates/includes/sidebar.html:190
+#: templates/includes/sidebar.html:204
msgid "Exchange Rates"
msgstr ""
@@ -652,36 +740,47 @@ msgid ""
msgstr ""
#: apps/currencies/models.py:171
-msgid "Single exchange rate"
+msgid "One exchange rate per day"
msgstr ""
#: apps/currencies/models.py:174
-msgid "Create one exchange rate and keep updating it. Avoids database clutter."
+msgid ""
+"Update the day's exchange rate on each fetch instead of adding a new one."
msgstr ""
-#: apps/currencies/models.py:179
+#: apps/currencies/models.py:178
+msgid "Exchange rates to keep"
+msgstr ""
+
+#: apps/currencies/models.py:183
+msgid ""
+"Keep at most this many automatic exchange rates per currency pair, deleting "
+"the oldest first. Leave empty to keep all."
+msgstr ""
+
+#: apps/currencies/models.py:188
msgid "Exchange Rate Service"
msgstr ""
-#: apps/currencies/models.py:180
+#: apps/currencies/models.py:189
msgid "Exchange Rate Services"
msgstr ""
-#: apps/currencies/models.py:234
+#: apps/currencies/models.py:243
msgid "'Every X hours' interval type requires a positive integer."
msgstr ""
-#: apps/currencies/models.py:243
+#: apps/currencies/models.py:252
msgid "'Every X hours' interval must be between 1 and 24."
msgstr ""
-#: apps/currencies/models.py:257
+#: apps/currencies/models.py:266
msgid ""
"Invalid hour format. Use comma-separated hours (0-23) and/or ranges (e.g., "
"'1-5,8,10-12')."
msgstr ""
-#: apps/currencies/models.py:268
+#: apps/currencies/models.py:277
msgid ""
"Invalid format. Please check the requirements for your selected interval "
"type."
@@ -751,21 +850,21 @@ msgstr ""
msgid "Income Transaction"
msgstr ""
-#: apps/dca/forms.py:192
+#: apps/dca/forms.py:179
msgid "Link transaction"
msgstr ""
-#: apps/dca/forms.py:275 apps/dca/forms.py:276 apps/dca/forms.py:281
-#: apps/dca/forms.py:285
+#: apps/dca/forms.py:262 apps/dca/forms.py:263 apps/dca/forms.py:268
+#: apps/dca/forms.py:272
msgid "You must provide an account."
msgstr ""
-#: apps/dca/forms.py:290 apps/transactions/forms.py:752
-#: apps/transactions/models.py:956
+#: apps/dca/forms.py:277 apps/transactions/forms.py:752
+#: apps/transactions/models.py:980
msgid "From and To accounts must be different."
msgstr ""
-#: apps/dca/forms.py:304
+#: apps/dca/forms.py:299
#, python-format
msgid "DCA for %(strategy_name)s"
msgstr ""
@@ -781,8 +880,8 @@ msgstr ""
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
#: apps/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
#: apps/transactions/forms.py:504 apps/transactions/forms.py:651
-#: apps/transactions/models.py:353 apps/transactions/models.py:752
-#: apps/transactions/models.py:1009 apps/transactions/models.py:1302
+#: apps/transactions/models.py:353 apps/transactions/models.py:776
+#: apps/transactions/models.py:1033 apps/transactions/models.py:1326
msgid "Notes"
msgstr ""
@@ -790,7 +889,7 @@ msgstr ""
msgid "DCA Strategy"
msgstr ""
-#: apps/dca/models.py:35 apps/export_app/forms.py:150
+#: apps/dca/models.py:35 apps/export_app/forms.py:152
msgid "DCA Strategies"
msgstr ""
@@ -810,7 +909,7 @@ msgstr ""
msgid "DCA Entry"
msgstr ""
-#: apps/dca/models.py:187 apps/export_app/forms.py:151
+#: apps/dca/models.py:187 apps/export_app/forms.py:153
msgid "DCA Entries"
msgstr ""
@@ -838,25 +937,25 @@ msgstr ""
msgid "Entry deleted successfully"
msgstr ""
-#: apps/export_app/forms.py:13 apps/export_app/forms.py:128
-#: templates/includes/sidebar.html:227 templates/users/fragments/list.html:10
+#: apps/export_app/forms.py:13 apps/export_app/forms.py:129
+#: templates/includes/sidebar.html:241 templates/users/fragments/list.html:10
#: templates/users/pages/index.html:4
msgid "Users"
msgstr ""
-#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
+#: apps/export_app/forms.py:31 apps/export_app/forms.py:136
#: apps/transactions/models.py:422 templates/includes/sidebar.html:81
-#: templates/includes/sidebar.html:148
+#: templates/includes/sidebar.html:154
#: 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/export_app/forms.py:37 apps/export_app/forms.py:131
+#: apps/export_app/forms.py:37 apps/export_app/forms.py:133
#: apps/insights/views.py:43 apps/transactions/filters.py:74
#: templates/categories/fragments/list.html:9
-#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:150
+#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:156
#: templates/insights/fragments/month_by_month.html:18
#: templates/insights/fragments/month_by_month.html:21
#: templates/insights/fragments/year_by_year.html:13
@@ -864,16 +963,16 @@ msgstr ""
msgid "Categories"
msgstr ""
-#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
+#: apps/export_app/forms.py:49 apps/export_app/forms.py:135
#: apps/insights/views.py:57 apps/rules/forms.py:185 apps/rules/forms.py:195
#: apps/rules/models.py:46 apps/rules/models.py:307
#: apps/transactions/filters.py:84 apps/transactions/forms.py:78
#: apps/transactions/forms.py:346 apps/transactions/forms.py:526
#: apps/transactions/forms.py:831 apps/transactions/forms.py:1083
#: apps/transactions/models.py:312 apps/transactions/models.py:368
-#: apps/transactions/models.py:748 apps/transactions/models.py:1006
-#: apps/transactions/models.py:1317 templates/entities/fragments/list.html:9
-#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:162
+#: apps/transactions/models.py:772 apps/transactions/models.py:1030
+#: apps/transactions/models.py:1341 templates/entities/fragments/list.html:9
+#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:168
#: templates/insights/fragments/month_by_month.html:40
#: templates/insights/fragments/month_by_month.html:43
#: templates/insights/fragments/year_by_year.html:35
@@ -881,28 +980,28 @@ msgstr ""
msgid "Entities"
msgstr ""
-#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
-#: apps/transactions/models.py:1046 templates/includes/sidebar.html:110
+#: apps/export_app/forms.py:55 apps/export_app/forms.py:139
+#: apps/transactions/models.py:1070 templates/includes/sidebar.html:110
#: templates/recurring_transactions/fragments/list.html:9
#: templates/recurring_transactions/pages/index.html:4
msgid "Recurring Transactions"
msgstr ""
-#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
-#: apps/transactions/models.py:766 templates/includes/sidebar.html:104
+#: apps/export_app/forms.py:61 apps/export_app/forms.py:137
+#: apps/transactions/models.py:790 templates/includes/sidebar.html:104
#: templates/installment_plans/fragments/list.html:9
#: templates/installment_plans/pages/index.html:4
msgid "Installment Plans"
msgstr ""
-#: apps/export_app/forms.py:73 apps/export_app/forms.py:140
+#: apps/export_app/forms.py:73 apps/export_app/forms.py:142
#: templates/exchange_rates_services/fragments/list.html:10
#: templates/exchange_rates_services/pages/index.html:4
-#: templates/includes/sidebar.html:218
+#: templates/includes/sidebar.html:232
msgid "Automatic Exchange Rates"
msgstr ""
-#: apps/export_app/forms.py:79 templates/includes/sidebar.html:198
+#: apps/export_app/forms.py:79 templates/includes/sidebar.html:212
#: templates/rules/fragments/list.html:9 templates/rules/pages/index.html:4
msgid "Rules"
msgstr ""
@@ -911,57 +1010,57 @@ msgstr ""
msgid "DCA"
msgstr ""
-#: apps/export_app/forms.py:91 apps/export_app/forms.py:152
+#: apps/export_app/forms.py:91 apps/export_app/forms.py:154
#: templates/import_app/fragments/profiles/list.html:21
#: templates/import_app/pages/profiles_index.html:4
msgid "Import Profiles"
msgstr ""
-#: apps/export_app/forms.py:117 templates/export_app/fragments/export.html:5
+#: apps/export_app/forms.py:118 templates/export_app/fragments/export.html:5
#: templates/export_app/pages/index.html:13
msgid "Export"
msgstr ""
-#: apps/export_app/forms.py:125
+#: apps/export_app/forms.py:126
msgid "Import a ZIP file exported from WYGIWYH"
msgstr ""
-#: apps/export_app/forms.py:126
+#: apps/export_app/forms.py:127
msgid "ZIP File"
msgstr ""
-#: apps/export_app/forms.py:143 apps/rules/models.py:27
+#: apps/export_app/forms.py:145 apps/rules/models.py:27
msgid "Transaction rules"
msgstr ""
-#: apps/export_app/forms.py:145 apps/rules/models.py:68
+#: apps/export_app/forms.py:147 apps/rules/models.py:68
msgid "Edit transaction action"
msgstr ""
-#: apps/export_app/forms.py:148 apps/rules/models.py:327
+#: apps/export_app/forms.py:150 apps/rules/models.py:327
msgid "Update or create transaction actions"
msgstr ""
-#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:341
+#: apps/export_app/forms.py:183 templates/cotton/transaction/item.html:341
#: templates/cotton/ui/deleted_transactions_action_bar.html:53
#: templates/export_app/fragments/restore.html:5
#: templates/export_app/pages/index.html:19
msgid "Restore"
msgstr ""
-#: apps/export_app/forms.py:191
+#: apps/export_app/forms.py:193
msgid "Please upload either a ZIP file or at least one CSV file"
msgstr ""
-#: apps/export_app/views.py:177
+#: apps/export_app/views.py:183
msgid "You have to select at least one export"
msgstr ""
-#: apps/export_app/views.py:197
+#: apps/export_app/views.py:203
msgid "Data restored successfully"
msgstr ""
-#: apps/export_app/views.py:209
+#: apps/export_app/views.py:215
msgid ""
"There was an error restoring your data. Check the logs for more details."
msgstr ""
@@ -972,7 +1071,7 @@ msgstr ""
#: apps/import_app/forms.py:55
#: templates/import_app/fragments/profiles/list.html:75
-#: templates/includes/sidebar.html:204
+#: templates/includes/sidebar.html:218
msgid "Import"
msgstr ""
@@ -1020,23 +1119,23 @@ msgstr ""
msgid "File name"
msgstr ""
-#: apps/import_app/views.py:75
+#: apps/import_app/views.py:76
msgid "Import Profile added successfully"
msgstr ""
-#: apps/import_app/views.py:113
+#: apps/import_app/views.py:114
msgid "Import Profile update successfully"
msgstr ""
-#: apps/import_app/views.py:142
+#: apps/import_app/views.py:143
msgid "Import Profile deleted successfully"
msgstr ""
-#: apps/import_app/views.py:222
+#: apps/import_app/views.py:223
msgid "Import Run queued successfully"
msgstr ""
-#: apps/import_app/views.py:249
+#: apps/import_app/views.py:250
msgid "Run deleted successfully"
msgstr ""
@@ -1105,7 +1204,7 @@ msgstr ""
msgid "Untagged"
msgstr ""
-#: apps/insights/views.py:53 apps/transactions/models.py:589
+#: apps/insights/views.py:53 apps/transactions/models.py:613
#: templates/tags/fragments/table.html:69
msgid "No tags"
msgstr ""
@@ -1126,6 +1225,26 @@ msgstr ""
msgid "No entities"
msgstr ""
+#: apps/investments/views.py:104
+msgid "Other"
+msgstr ""
+
+#: apps/investments/views.py:135
+msgid "Unclassified"
+msgstr ""
+
+#: apps/investments/views.py:267
+msgid "Asset class added successfully"
+msgstr ""
+
+#: apps/investments/views.py:275
+msgid "Asset class updated successfully"
+msgstr ""
+
+#: apps/investments/views.py:296
+msgid "Asset class deleted successfully"
+msgstr ""
+
#: apps/net_worth/views.py:137 templates/net_worth/net_worth.html:84
#: templates/net_worth/net_worth.html:85
msgid "Consolidated"
@@ -1179,8 +1298,8 @@ msgstr ""
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
#: apps/rules/models.py:271 apps/transactions/forms.py:468
-#: apps/transactions/models.py:336 apps/transactions/models.py:708
-#: apps/transactions/models.py:987 apps/transactions/models.py:1287
+#: apps/transactions/models.py:336 apps/transactions/models.py:732
+#: apps/transactions/models.py:1011 apps/transactions/models.py:1311
#: templates/mini_tools/simulator.html:63
msgid "Type"
msgstr ""
@@ -1188,7 +1307,7 @@ msgstr ""
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
#: apps/rules/models.py:275 apps/transactions/filters.py:22
#: apps/transactions/forms.py:472 apps/transactions/models.py:338
-#: apps/transactions/models.py:1289 templates/cotton/transaction/item.html:23
+#: apps/transactions/models.py:1313 templates/cotton/transaction/item.html:23
#: templates/cotton/transaction/item.html:34
#: templates/transactions/widgets/paid_toggle_button.html:10
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
@@ -1199,14 +1318,14 @@ msgstr ""
#: apps/rules/models.py:283 apps/transactions/forms.py:90
#: apps/transactions/forms.py:488 apps/transactions/forms.py:638
#: apps/transactions/forms.py:837 apps/transactions/models.py:340
-#: apps/transactions/models.py:726 apps/transactions/models.py:1011
+#: apps/transactions/models.py:750 apps/transactions/models.py:1035
msgid "Reference Date"
msgstr ""
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
#: apps/rules/models.py:287 apps/transactions/forms.py:495
-#: apps/transactions/models.py:346 apps/transactions/models.py:992
-#: apps/transactions/models.py:1295
+#: apps/transactions/models.py:346 apps/transactions/models.py:1016
+#: apps/transactions/models.py:1319
#: templates/insights/fragments/sankey.html:102
#: templates/installment_plans/fragments/table.html:18
#: templates/mini_tools/simulator.html:70
@@ -1218,27 +1337,27 @@ msgstr ""
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
#: apps/rules/models.py:42 apps/rules/models.py:291
#: apps/transactions/forms.py:499 apps/transactions/forms.py:642
-#: apps/transactions/models.py:351 apps/transactions/models.py:710
-#: apps/transactions/models.py:995 apps/transactions/models.py:1300
+#: apps/transactions/models.py:351 apps/transactions/models.py:734
+#: apps/transactions/models.py:1019 apps/transactions/models.py:1324
msgid "Description"
msgstr ""
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
#: apps/rules/models.py:299 apps/transactions/models.py:398
-#: apps/transactions/models.py:1322
+#: apps/transactions/models.py:1346
msgid "Internal Note"
msgstr ""
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
#: apps/rules/models.py:303 apps/transactions/models.py:400
-#: apps/transactions/models.py:1324
+#: apps/transactions/models.py:1348
msgid "Internal ID"
msgstr ""
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
#: apps/rules/models.py:319 apps/transactions/forms.py:655
#: apps/transactions/models.py:250 apps/transactions/models.py:341
-#: apps/transactions/models.py:1290
+#: apps/transactions/models.py:1314
msgid "Mute"
msgstr ""
@@ -1251,7 +1370,7 @@ msgid "Set Values"
msgstr ""
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
-#: apps/transactions/models.py:421 apps/transactions/models.py:645
+#: apps/transactions/models.py:421 apps/transactions/models.py:669
msgid "Transaction"
msgstr ""
@@ -1287,7 +1406,12 @@ msgstr ""
msgid "Field"
msgstr ""
-#: apps/rules/models.py:61
+#: apps/rules/models.py:61 templates/investments/fragments/charts.html:86
+#: templates/investments/fragments/detail.html:103
+#: templates/investments/fragments/detail.html:162
+#: templates/investments/fragments/list.html:48
+#: templates/investments/fragments/list.html:49
+#: templates/investments/fragments/rebalance.html:23
msgid "Value"
msgstr ""
@@ -1469,7 +1593,7 @@ msgid "Upload"
msgstr ""
#: apps/transactions/forms.py:355 apps/transactions/forms.py:1089
-#: apps/transactions/models.py:925
+#: apps/transactions/models.py:949
msgid "To account"
msgstr ""
@@ -1485,8 +1609,8 @@ msgstr ""
msgid "To Amount"
msgstr ""
-#: apps/transactions/forms.py:718 apps/transactions/models.py:970
-#: apps/transactions/models.py:1267 templates/cotton/transaction/item.html:52
+#: apps/transactions/forms.py:718 apps/transactions/models.py:994
+#: apps/transactions/models.py:1291 templates/cotton/transaction/item.html:52
#: templates/cotton/transaction/item.html:60
#: templates/cotton/ui/quick_transactions_buttons.html:40
#: templates/cotton/ui/transactions_fab.html:44
@@ -1546,8 +1670,8 @@ msgid ""
"transactions"
msgstr ""
-#: apps/transactions/models.py:323 apps/transactions/models.py:969
-#: apps/transactions/models.py:1266
+#: apps/transactions/models.py:323 apps/transactions/models.py:993
+#: apps/transactions/models.py:1290
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: templates/calendar_view/fragments/list.html:52
@@ -1555,13 +1679,19 @@ msgstr ""
#: templates/cotton/ui/quick_transactions_buttons.html:10
#: templates/cotton/ui/transactions_fab.html:10
#: templates/insights/fragments/overview/_results.html:12
+#: templates/investments/fragments/_totals.html:21
+#: templates/investments/fragments/_totals.html:42
+#: templates/investments/fragments/detail.html:106
+#: templates/investments/fragments/detail.html:210
+#: templates/investments/fragments/detail.html:252
+#: templates/investments/fragments/detail.html:320
#: templates/mini_tools/simulator.html:66
#: templates/monthly_overview/fragments/monthly_summary.html:42
msgid "Income"
msgstr ""
-#: apps/transactions/models.py:324 apps/transactions/models.py:971
-#: apps/transactions/models.py:1268
+#: apps/transactions/models.py:324 apps/transactions/models.py:995
+#: apps/transactions/models.py:1292
#: templates/calendar_view/fragments/list.html:46
#: templates/calendar_view/fragments/list.html:48
#: templates/calendar_view/fragments/list.html:56
@@ -1573,14 +1703,14 @@ msgstr ""
msgid "Expense"
msgstr ""
-#: apps/transactions/models.py:379 apps/transactions/models.py:765
+#: apps/transactions/models.py:379 apps/transactions/models.py:789
#: templates/cotton/transaction/item.html:291
#: templates/cotton/transaction/item.html:308
#: templates/cotton/transaction/item.html:318
msgid "Installment Plan"
msgstr ""
-#: apps/transactions/models.py:388 apps/transactions/models.py:1045
+#: apps/transactions/models.py:388 apps/transactions/models.py:1069
#: templates/cotton/transaction/item.html:281
#: templates/cotton/transaction/item.html:301
msgid "Recurring Transaction"
@@ -1598,150 +1728,162 @@ msgstr ""
msgid "Deleted At"
msgstr ""
-#: apps/transactions/models.py:591
+#: apps/transactions/models.py:517
+msgid "A transfer needs one expense and one income."
+msgstr ""
+
+#: apps/transactions/models.py:519
+msgid "A transfer needs two different accounts."
+msgstr ""
+
+#: apps/transactions/models.py:523
+msgid "This transaction is already part of another transfer."
+msgstr ""
+
+#: apps/transactions/models.py:615
msgid "No category"
msgstr ""
-#: apps/transactions/models.py:593
+#: apps/transactions/models.py:617
msgid "No description"
msgstr ""
-#: apps/transactions/models.py:650
+#: apps/transactions/models.py:674
msgid "File"
msgstr ""
-#: apps/transactions/models.py:652
+#: apps/transactions/models.py:676
msgid "Original Name"
msgstr ""
-#: apps/transactions/models.py:654
+#: apps/transactions/models.py:678
msgid "Content Type"
msgstr ""
-#: apps/transactions/models.py:656
+#: apps/transactions/models.py:680
msgid "Size"
msgstr ""
-#: apps/transactions/models.py:661
+#: apps/transactions/models.py:685
msgid "Uploaded By"
msgstr ""
-#: apps/transactions/models.py:666
+#: apps/transactions/models.py:690
msgid "Transaction Attachment"
msgstr ""
-#: apps/transactions/models.py:667
+#: apps/transactions/models.py:691
msgid "Transaction Attachments"
msgstr ""
-#: apps/transactions/models.py:697 templates/includes/sidebar.html:57
+#: apps/transactions/models.py:721 templates/includes/sidebar.html:57
msgid "Yearly"
msgstr ""
-#: apps/transactions/models.py:698 apps/users/models.py:469
+#: apps/transactions/models.py:722 apps/users/models.py:469
#: templates/includes/sidebar.html:51
msgid "Monthly"
msgstr ""
-#: apps/transactions/models.py:699
+#: apps/transactions/models.py:723
msgid "Weekly"
msgstr ""
-#: apps/transactions/models.py:700
+#: apps/transactions/models.py:724
msgid "Daily"
msgstr ""
-#: apps/transactions/models.py:713
+#: apps/transactions/models.py:737
msgid "Number of Installments"
msgstr ""
-#: apps/transactions/models.py:718
+#: apps/transactions/models.py:742
msgid "Installment Start"
msgstr ""
-#: apps/transactions/models.py:719
+#: apps/transactions/models.py:743
msgid "The installment number to start counting from"
msgstr ""
-#: apps/transactions/models.py:724 apps/transactions/models.py:1015
+#: apps/transactions/models.py:748 apps/transactions/models.py:1039
msgid "Start Date"
msgstr ""
-#: apps/transactions/models.py:728 apps/transactions/models.py:1016
+#: apps/transactions/models.py:752 apps/transactions/models.py:1040
msgid "End Date"
msgstr ""
-#: apps/transactions/models.py:733
+#: apps/transactions/models.py:757
msgid "Recurrence"
msgstr ""
-#: apps/transactions/models.py:736
+#: apps/transactions/models.py:760
msgid "Installment Amount"
msgstr ""
-#: apps/transactions/models.py:755 apps/transactions/models.py:1035
+#: apps/transactions/models.py:779 apps/transactions/models.py:1059
msgid "Add description to transactions"
msgstr ""
-#: apps/transactions/models.py:758 apps/transactions/models.py:1038
+#: apps/transactions/models.py:782 apps/transactions/models.py:1062
msgid "Add notes to transactions"
msgstr ""
-#: apps/transactions/models.py:932
+#: apps/transactions/models.py:956
msgid "To amount"
msgstr ""
-#: apps/transactions/models.py:933
+#: apps/transactions/models.py:957
msgid "Leave empty to use the same amount"
msgstr ""
-#: apps/transactions/models.py:974 templates/mini_tools/simulator.html:129
+#: apps/transactions/models.py:998 templates/mini_tools/simulator.html:129
msgid "day(s)"
msgstr ""
-#: apps/transactions/models.py:975 templates/mini_tools/simulator.html:130
+#: apps/transactions/models.py:999 templates/mini_tools/simulator.html:130
msgid "week(s)"
msgstr ""
-#: apps/transactions/models.py:976 templates/mini_tools/simulator.html:131
+#: apps/transactions/models.py:1000 templates/mini_tools/simulator.html:131
msgid "month(s)"
msgstr ""
-#: apps/transactions/models.py:977 templates/mini_tools/simulator.html:132
+#: apps/transactions/models.py:1001 templates/mini_tools/simulator.html:132
msgid "year(s)"
msgstr ""
-#: apps/transactions/models.py:979
+#: apps/transactions/models.py:1003
#: templates/recurring_transactions/fragments/list.html:18
msgid "Paused"
msgstr ""
-#: apps/transactions/models.py:1018
+#: apps/transactions/models.py:1042
msgid "Recurrence Type"
msgstr ""
-#: apps/transactions/models.py:1021
+#: apps/transactions/models.py:1045
msgid "Recurrence Interval"
msgstr ""
-#: apps/transactions/models.py:1024
+#: apps/transactions/models.py:1048
msgid "Keep at most"
msgstr ""
-#: apps/transactions/models.py:1028
+#: apps/transactions/models.py:1052
msgid "Last Generated Date"
msgstr ""
-#: apps/transactions/models.py:1031
+#: apps/transactions/models.py:1055
msgid "Last Generated Reference Date"
msgstr ""
-#: apps/transactions/models.py:1334 templates/cotton/transaction/item.html:274
+#: apps/transactions/models.py:1358 templates/cotton/transaction/item.html:274
#: templates/cotton/ui/transactions_fab.html:59
msgid "Quick Transaction"
msgstr ""
-#: apps/transactions/models.py:1335 templates/includes/sidebar.html:98
+#: apps/transactions/models.py:1359 templates/includes/sidebar.html:98
#: templates/quick_transactions/pages/index.html:5
#: templates/quick_transactions/pages/index.html:15
msgid "Quick Transactions"
@@ -1864,8 +2006,8 @@ msgid "Item deleted successfully"
msgstr ""
#: apps/transactions/views/quick_transactions.py:164
-#: apps/transactions/views/transactions.py:143
-#: apps/transactions/views/transactions.py:328
+#: apps/transactions/views/transactions.py:156
+#: apps/transactions/views/transactions.py:343
msgid "Transaction added successfully"
msgstr ""
@@ -1901,42 +2043,42 @@ msgstr ""
msgid "Tag deleted successfully"
msgstr ""
-#: apps/transactions/views/transactions.py:61
+#: apps/transactions/views/transactions.py:65
msgid "Attachment uploaded successfully"
msgstr ""
-#: apps/transactions/views/transactions.py:112
+#: apps/transactions/views/transactions.py:116
msgid "Attachment deleted successfully"
msgstr ""
-#: apps/transactions/views/transactions.py:355
+#: apps/transactions/views/transactions.py:370
msgid "Transaction updated successfully"
msgstr ""
-#: apps/transactions/views/transactions.py:406
+#: apps/transactions/views/transactions.py:421
#, python-format
msgid "%(count)s transaction updated successfully"
msgid_plural "%(count)s transactions updated successfully"
msgstr[0] ""
msgstr[1] ""
-#: apps/transactions/views/transactions.py:433
+#: apps/transactions/views/transactions.py:448
msgid "Transaction duplicated successfully"
msgstr ""
-#: apps/transactions/views/transactions.py:478
+#: apps/transactions/views/transactions.py:493
msgid "Transaction deleted successfully"
msgstr ""
-#: apps/transactions/views/transactions.py:508
+#: apps/transactions/views/transactions.py:523
msgid "Transaction restored successfully"
msgstr ""
-#: apps/transactions/views/transactions.py:534
+#: apps/transactions/views/transactions.py:549
msgid "Transfer added successfully"
msgstr ""
-#: apps/transactions/views/transactions.py:562
+#: apps/transactions/views/transactions.py:579
msgid "Transfer updated successfully"
msgstr ""
@@ -2240,6 +2382,7 @@ msgstr ""
#: templates/exchange_rates_services/fragments/table.html:20
#: templates/import_app/fragments/profiles/list.html:42
#: templates/installment_plans/fragments/table.html:25
+#: templates/investment_classes/fragments/list.html:36
#: templates/quick_transactions/fragments/list.html:22
#: templates/recurring_transactions/fragments/table.html:27
#: templates/rules/fragments/list.html:31
@@ -2262,6 +2405,9 @@ msgstr ""
#: templates/exchange_rates_services/fragments/table.html:23
#: templates/import_app/fragments/profiles/list.html:46
#: templates/installment_plans/fragments/table.html:28
+#: templates/investment_classes/fragments/list.html:39
+#: templates/investments/fragments/detail.html:305
+#: templates/investments/fragments/detail.html:331
#: templates/quick_transactions/fragments/list.html:25
#: templates/recurring_transactions/fragments/table.html:30
#: templates/rules/fragments/transaction_rule/view.html:22
@@ -2311,6 +2457,7 @@ msgstr ""
#: templates/import_app/fragments/profiles/list.html:81
#: templates/import_app/fragments/runs/list.html:100
#: templates/installment_plans/fragments/table.html:54
+#: templates/investment_classes/fragments/list.html:45
#: templates/mini_tools/simulator.html:109
#: templates/mini_tools/unit_price_calculator.html:16
#: templates/quick_transactions/fragments/list.html:32
@@ -2344,6 +2491,7 @@ msgstr ""
#: templates/import_app/fragments/runs/list.html:104
#: templates/installment_plans/fragments/table.html:47
#: templates/installment_plans/fragments/table.html:58
+#: templates/investment_classes/fragments/list.html:49
#: templates/quick_transactions/fragments/list.html:37
#: templates/recurring_transactions/fragments/table.html:52
#: templates/recurring_transactions/fragments/table.html:65
@@ -2393,6 +2541,7 @@ msgstr ""
#: templates/import_app/fragments/profiles/list.html:87
#: templates/import_app/fragments/runs/list.html:106
#: templates/installment_plans/fragments/table.html:60
+#: templates/investment_classes/fragments/list.html:51
#: templates/quick_transactions/fragments/list.html:39
#: templates/recurring_transactions/fragments/table.html:94
#: templates/rules/fragments/list.html:62
@@ -2441,6 +2590,7 @@ msgid "Add account"
msgstr ""
#: templates/accounts/fragments/edit.html:5
+#: templates/investments/fragments/detail.html:40
msgid "Edit account"
msgstr ""
@@ -2513,7 +2663,7 @@ msgstr ""
msgid "Pick a month"
msgstr ""
-#: templates/common/fragments/toasts.html:20
+#: templates/common/fragments/toasts.html:18
#: templates/cotton/components/fab.html:9
#: templates/cotton/components/sidebar_collapsible_panel.html:28
#: templates/extends/offcanvas.html:4 templates/includes/sidebar.html:44
@@ -2742,6 +2892,8 @@ msgid "Add DCA strategy"
msgstr ""
#: templates/dca/fragments/strategy/details.html:28
+#: templates/investments/fragments/detail.html:92
+#: templates/investments/fragments/list.html:65
msgid "No exchange rate available"
msgstr ""
@@ -2923,7 +3075,7 @@ msgstr ""
msgid "No services configured"
msgstr ""
-#: templates/export_app/pages/index.html:4 templates/includes/sidebar.html:211
+#: templates/export_app/pages/index.html:4 templates/includes/sidebar.html:225
msgid "Export and Restore"
msgstr ""
@@ -3094,52 +3246,61 @@ msgstr ""
msgid "Tools"
msgstr ""
-#: templates/includes/sidebar.html:118
-msgid "Dollar Cost Average Tracker"
+#: templates/includes/sidebar.html:118 templates/includes/sidebar.html:188
+#: templates/investment_classes/fragments/list.html:29
+#: templates/investments/fragments/detail.html:37
+#: templates/investments/fragments/list.html:9
+#: templates/investments/pages/detail.html:4
+#: templates/investments/pages/index.html:4
+msgid "Investments"
msgstr ""
#: templates/includes/sidebar.html:124
+msgid "Dollar Cost Average Tracker"
+msgstr ""
+
+#: templates/includes/sidebar.html:130
#: templates/mini_tools/unit_price_calculator.html:4
#: templates/mini_tools/unit_price_calculator.html:9
msgid "Unit Price Calculator"
msgstr ""
-#: templates/includes/sidebar.html:130
+#: templates/includes/sidebar.html:136
#: templates/mini_tools/currency_converter/currency_converter.html:7
#: templates/mini_tools/currency_converter/currency_converter.html:14
msgid "Currency Converter"
msgstr ""
-#: templates/includes/sidebar.html:136 templates/mini_tools/simulator.html:5
+#: templates/includes/sidebar.html:142 templates/mini_tools/simulator.html:5
#: templates/mini_tools/simulator.html:21
msgid "Simulator"
msgstr ""
-#: templates/includes/sidebar.html:145
+#: templates/includes/sidebar.html:151
msgid "Management"
msgstr ""
-#: templates/includes/sidebar.html:196
+#: templates/includes/sidebar.html:210
msgid "Automation"
msgstr ""
-#: templates/includes/sidebar.html:225
+#: templates/includes/sidebar.html:239
msgid "Admin"
msgstr ""
-#: templates/includes/sidebar.html:233
+#: templates/includes/sidebar.html:247
msgid "Django Admin"
msgstr ""
-#: templates/includes/sidebar.html:234
+#: templates/includes/sidebar.html:248
msgid "Only use this if you know what you're doing"
msgstr ""
-#: templates/includes/sidebar.html:249
+#: templates/includes/sidebar.html:263
msgid "is available"
msgstr ""
-#: templates/includes/sidebar.html:257
+#: templates/includes/sidebar.html:271
msgid "Calculator"
msgstr ""
@@ -3206,6 +3367,7 @@ msgstr ""
#: templates/insights/fragments/overview/_results.html:173
#: templates/insights/fragments/year_by_year.html:59
#: templates/insights/fragments/year_by_year.html:140
+#: templates/investment_classes/fragments/list.html:65
#: templates/monthly_overview/fragments/monthly_summary.html:168
msgid "Total"
msgstr ""
@@ -3401,6 +3563,273 @@ msgstr ""
msgid "No installment plans"
msgstr ""
+#: templates/investment_classes/fragments/form.html:5
+msgid "Edit asset class"
+msgstr ""
+
+#: templates/investment_classes/fragments/form.html:5
+msgid "Add asset class"
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:19
+#: templates/investments/fragments/rebalance.html:10
+#, python-format
+msgid "Targets add up to %(total)s%%. Set them to 100%% to see the rebalance."
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:28
+#: templates/investments/fragments/rebalance.html:25
+msgid "Target"
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:50
+msgid "Its investments will be left without an asset class."
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:74
+msgid "No asset classes"
+msgstr ""
+
+#: templates/investments/fragments/_allocation.html:3
+msgid "Allocation chart"
+msgstr ""
+
+#: templates/investments/fragments/_allocation.html:11
+#, python-format
+msgid ""
+"%(share)s%% of your investments with a target, against a target of "
+"%(target)s%%"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid "Annual return, weighted by when money went in and out"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid ""
+"Return since the first contribution, weighted by when money went in and out"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid "/yr"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:15
+#: templates/investments/fragments/detail.html:63
+msgid "total return"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:20
+#: templates/investments/fragments/_totals.html:41
+#: templates/investments/fragments/charts.html:96
+#: templates/investments/fragments/detail.html:163
+#: templates/investments/fragments/detail.html:223
+#: templates/investments/fragments/list.html:47
+msgid "Invested"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:24
+#: templates/investments/fragments/detail.html:74
+msgid ""
+"How much of the return came from exchange rates moving since you invested"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:24
+#: templates/investments/fragments/_totals.html:26
+msgid "Currency effect"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:26
+#: templates/investments/fragments/detail.html:78
+msgid ""
+"Every date is converted with the same rate. Add historical exchange rates "
+"(or an automatic rate service) to see how much exchange rates moved your "
+"return."
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:40
+#: templates/investments/fragments/list.html:49
+msgid "Return"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:10
+msgid "By asset class"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:17
+#: templates/net_worth/net_worth.html:52
+#: templates/yearly_overview/pages/overview_by_currency.html:6
+msgid "By currency"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:24
+msgid "Value and invested"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:31
+msgid "Return by investment"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:38
+msgid "Nothing to chart yet"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:10
+msgid "Buy / Contribute"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:16
+msgid "Sell / Withdraw"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:23
+msgid "Dividend / Income"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:30
+msgid "Update value"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:47
+#: templates/investments/fragments/list.html:67
+msgid "Tracked by value"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:48
+msgid ""
+"Some incoming units have no linked payment, so their cost was estimated from "
+"the market price of that day"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:68
+msgid "In"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:74
+#: templates/investments/fragments/detail.html:78
+msgid "currency effect"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:104
+#: templates/investments/fragments/detail.html:179
+msgid "Price"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:105
+#: templates/investments/fragments/detail.html:200
+#: templates/investments/fragments/detail.html:239
+#: templates/investments/fragments/detail.html:290
+msgid "Earnings"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:180
+#: templates/investments/fragments/detail.html:227
+msgid "Average cost"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:181
+#: templates/investments/fragments/rebalance.html:41
+msgid "Buy"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:182
+#: templates/investments/fragments/rebalance.html:43
+msgid "Sell"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:227
+#: templates/investments/fragments/detail.html:257
+msgid "Put in / taken out"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:239
+msgid "Unrealized"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:248
+msgid "Realized"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:271
+msgid "Movements"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:284
+msgid "Bought / contributed"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:284
+msgid "Sold / withdrew"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:288
+msgid "Money in"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:288
+msgid "Money out"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:290
+msgid "Gain"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:290
+msgid "Loss, tax or fee"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:315
+msgid "No movements yet"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:315
+msgid "Use the + button to buy or contribute"
+msgstr ""
+
+#: templates/investments/fragments/list.html:21
+msgid "Holdings"
+msgstr ""
+
+#: templates/investments/fragments/list.html:60
+msgid "Some costs were estimated from market prices"
+msgstr ""
+
+#: templates/investments/fragments/list.html:92
+msgid "Charts"
+msgstr ""
+
+#: templates/investments/fragments/list.html:97
+msgid "Rebalance"
+msgstr ""
+
+#: templates/investments/fragments/list.html:105
+msgid "No investments yet"
+msgstr ""
+
+#: templates/investments/fragments/list.html:106
+msgid "Mark an account as an investment in its settings to see it here"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:4
+msgid "No allocation targets"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:5
+msgid "Set a target for your asset classes to see what to buy and sell"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:26
+msgid "To reach the target"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:34
+msgid "Off target"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:55
+msgid "Left out: investments without an asset class or a target"
+msgstr ""
+
#: templates/layouts/base.html:38
msgid "This is a demo!"
msgstr ""
@@ -3604,11 +4033,6 @@ msgstr ""
msgid "Order by"
msgstr ""
-#: templates/net_worth/net_worth.html:52
-#: templates/yearly_overview/pages/overview_by_currency.html:6
-msgid "By currency"
-msgstr ""
-
#: templates/net_worth/net_worth.html:135
msgid "Evolution"
msgstr ""
diff --git a/app/locale/es/LC_MESSAGES/django.po b/app/locale/es/LC_MESSAGES/django.po
index cc3089e0..af38a69d 100644
--- a/app/locale/es/LC_MESSAGES/django.po
+++ b/app/locale/es/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-11-08 06:01+0000\n"
+"POT-Creation-Date: 2026-11-08 18:27+0000\n"
"PO-Revision-Date: 2026-07-10 04:57+0000\n"
"Last-Translator: Juan David Afanador \n"
"Language-Team: Spanish \n"
"Language-Team: French 1;\n"
"X-Generator: Weblate 2026.10.1\n"
-#: apps/accounts/forms.py:28
+#: apps/accounts/forms.py:33
msgid "Group name"
msgstr "Nom du groupe"
-#: apps/accounts/forms.py:43 apps/accounts/forms.py:109
-#: apps/currencies/forms.py:57 apps/currencies/forms.py:91
-#: apps/currencies/forms.py:166 apps/dca/forms.py:46 apps/dca/forms.py:205
-#: apps/import_app/forms.py:32 apps/rules/forms.py:60 apps/rules/forms.py:100
-#: apps/rules/forms.py:385 apps/transactions/forms.py:219
-#: apps/transactions/forms.py:452 apps/transactions/forms.py:571
-#: apps/transactions/forms.py:718 apps/transactions/forms.py:940
-#: apps/transactions/forms.py:987 apps/transactions/forms.py:1015
-#: apps/transactions/forms.py:1046 apps/transactions/forms.py:1208
-#: apps/users/forms.py:264 apps/users/forms.py:422
+#: apps/accounts/forms.py:48 apps/accounts/forms.py:76
+#: apps/accounts/forms.py:184 apps/currencies/forms.py:57
+#: apps/currencies/forms.py:91 apps/currencies/forms.py:168
+#: apps/dca/forms.py:46 apps/dca/forms.py:192 apps/import_app/forms.py:32
+#: apps/rules/forms.py:60 apps/rules/forms.py:100 apps/rules/forms.py:385
+#: apps/transactions/forms.py:219 apps/transactions/forms.py:452
+#: apps/transactions/forms.py:571 apps/transactions/forms.py:718
+#: apps/transactions/forms.py:940 apps/transactions/forms.py:987
+#: apps/transactions/forms.py:1015 apps/transactions/forms.py:1046
+#: apps/transactions/forms.py:1208 apps/users/forms.py:264
+#: apps/users/forms.py:422
#: templates/rules/fragments/transaction_rule/dry_run/updated.html:5
#: templates/rules/fragments/transaction_rule/view.html:128
msgid "Update"
msgstr "Mise à jour"
-#: apps/accounts/forms.py:49 apps/accounts/forms.py:115
-#: apps/common/widgets/tom_select.py:13 apps/currencies/forms.py:63
-#: apps/currencies/forms.py:97 apps/currencies/forms.py:172
-#: apps/dca/forms.py:52 apps/dca/forms.py:211 apps/import_app/forms.py:38
-#: apps/rules/forms.py:66 apps/rules/forms.py:106 apps/rules/forms.py:391
-#: apps/transactions/forms.py:206 apps/transactions/forms.py:226
-#: apps/transactions/forms.py:459 apps/transactions/forms.py:946
-#: apps/transactions/forms.py:993 apps/transactions/forms.py:1021
-#: apps/transactions/forms.py:1052 apps/transactions/forms.py:1214
-#: apps/users/forms.py:270 apps/users/forms.py:428
-#: templates/mini_tools/simulator.html:177
+#: apps/accounts/forms.py:54 apps/accounts/forms.py:76
+#: apps/accounts/forms.py:190 apps/common/widgets/tom_select.py:15
+#: apps/currencies/forms.py:63 apps/currencies/forms.py:97
+#: apps/currencies/forms.py:174 apps/dca/forms.py:52 apps/dca/forms.py:198
+#: apps/import_app/forms.py:38 apps/rules/forms.py:66 apps/rules/forms.py:106
+#: apps/rules/forms.py:391 apps/transactions/forms.py:206
+#: apps/transactions/forms.py:226 apps/transactions/forms.py:459
+#: apps/transactions/forms.py:946 apps/transactions/forms.py:993
+#: apps/transactions/forms.py:1021 apps/transactions/forms.py:1052
+#: apps/transactions/forms.py:1214 apps/users/forms.py:270
+#: apps/users/forms.py:428 templates/mini_tools/simulator.html:177
#: templates/mini_tools/unit_price_calculator.html:168
msgid "Add"
msgstr "Ajouter"
-#: apps/accounts/forms.py:57 templates/accounts/fragments/list.html:23
+#: apps/accounts/forms.py:86 templates/accounts/fragments/list.html:23
msgid "Group"
msgstr "Groupe"
-#: apps/accounts/forms.py:123
+#: apps/accounts/forms.py:93 apps/accounts/models.py:73
+#: apps/accounts/models.py:130
+#: templates/investments/fragments/rebalance.html:22
+msgid "Asset class"
+msgstr ""
+
+#: apps/accounts/forms.py:198
msgid "New balance"
msgstr "Nouveau solde"
-#: apps/accounts/forms.py:129 apps/dca/forms.py:79 apps/dca/forms.py:86
+#: apps/accounts/forms.py:204 apps/dca/forms.py:79 apps/dca/forms.py:86
#: apps/insights/forms.py:117 apps/insights/views.py:44 apps/rules/forms.py:181
#: apps/rules/forms.py:197 apps/rules/models.py:44 apps/rules/models.py:311
#: apps/transactions/forms.py:62 apps/transactions/forms.py:330
#: apps/transactions/forms.py:510 apps/transactions/forms.py:607
#: apps/transactions/forms.py:614 apps/transactions/forms.py:823
#: apps/transactions/forms.py:1075 apps/transactions/models.py:357
-#: apps/transactions/models.py:743 apps/transactions/models.py:999
-#: apps/transactions/models.py:1306
+#: apps/transactions/models.py:767 apps/transactions/models.py:1023
+#: apps/transactions/models.py:1330
#: templates/insights/fragments/month_by_month.html:84
#: templates/insights/fragments/year_by_year.html:52
msgid "Category"
msgstr "Catégorie"
-#: apps/accounts/forms.py:136 apps/dca/forms.py:95 apps/dca/forms.py:103
-#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
+#: apps/accounts/forms.py:211 apps/dca/forms.py:95 apps/dca/forms.py:103
+#: apps/export_app/forms.py:43 apps/export_app/forms.py:134
#: apps/insights/views.py:50 apps/rules/forms.py:184 apps/rules/forms.py:194
#: apps/rules/models.py:45 apps/rules/models.py:315
#: apps/transactions/filters.py:79 apps/transactions/forms.py:70
#: apps/transactions/forms.py:338 apps/transactions/forms.py:518
#: apps/transactions/forms.py:623 apps/transactions/forms.py:631
#: apps/transactions/forms.py:816 apps/transactions/forms.py:1068
-#: apps/transactions/models.py:363 apps/transactions/models.py:745
-#: apps/transactions/models.py:1003 apps/transactions/models.py:1312
-#: templates/includes/sidebar.html:156
+#: apps/transactions/models.py:363 apps/transactions/models.py:769
+#: apps/transactions/models.py:1027 apps/transactions/models.py:1336
+#: templates/includes/sidebar.html:162
#: templates/insights/fragments/month_by_month.html:29
#: templates/insights/fragments/month_by_month.html:32
#: templates/insights/fragments/year_by_year.html:24
@@ -94,23 +101,23 @@ msgstr "Catégorie"
msgid "Tags"
msgstr "Etiquettes"
-#: apps/accounts/forms.py:179
+#: apps/accounts/forms.py:254
msgid "All paid transactions"
msgstr "Toutes les transactions payées"
-#: apps/accounts/forms.py:180
+#: apps/accounts/forms.py:255
msgid "Up to date"
msgstr "à jour"
-#: apps/accounts/forms.py:181
+#: apps/accounts/forms.py:256
msgid "Up to reference month"
msgstr "Aller au mois de référence"
-#: apps/accounts/forms.py:185
+#: apps/accounts/forms.py:260
msgid "Balance against"
msgstr "Comparer"
-#: apps/accounts/forms.py:189 apps/currencies/forms.py:70
+#: apps/accounts/forms.py:264 apps/currencies/forms.py:70
#: apps/dca/models.py:158 apps/rules/forms.py:176 apps/rules/forms.py:190
#: apps/rules/models.py:38 apps/rules/models.py:279
#: apps/transactions/forms.py:86 apps/transactions/forms.py:482
@@ -121,14 +128,15 @@ msgstr "Comparer"
msgid "Date"
msgstr "Date"
-#: apps/accounts/forms.py:194
+#: apps/accounts/forms.py:269
msgid "Reference month"
msgstr "Mois de référence"
-#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
+#: apps/accounts/models.py:19 apps/accounts/models.py:36
+#: apps/accounts/models.py:84 apps/dca/models.py:13
#: apps/import_app/models.py:20 apps/rules/models.py:13
#: apps/transactions/models.py:249 apps/transactions/models.py:274
-#: apps/transactions/models.py:298 apps/transactions/models.py:1274
+#: apps/transactions/models.py:298 apps/transactions/models.py:1298
#: apps/users/models.py:577 templates/account_groups/fragments/list.html:22
#: templates/accounts/fragments/list.html:22
#: templates/categories/fragments/table.html:17
@@ -137,6 +145,7 @@ msgstr "Mois de référence"
#: templates/exchange_rates_services/fragments/list.html:28
#: templates/import_app/fragments/profiles/list.html:34
#: templates/installment_plans/fragments/table.html:16
+#: templates/investment_classes/fragments/list.html:27
#: templates/quick_transactions/fragments/list.html:13
#: templates/recurring_transactions/fragments/table.html:18
#: templates/rules/fragments/list.html:24
@@ -145,43 +154,128 @@ msgstr "Mois de référence"
msgid "Name"
msgstr "Nom"
-#: apps/accounts/models.py:18 apps/accounts/models.py:33
+#: apps/accounts/models.py:25 apps/accounts/models.py:88
msgid "Account Group"
msgstr "Groupe de comptes"
-#: apps/accounts/models.py:19 templates/account_groups/fragments/list.html:9
+#: apps/accounts/models.py:26 templates/account_groups/fragments/list.html:9
#: templates/account_groups/pages/index.html:4
-#: templates/includes/sidebar.html:176
+#: templates/includes/sidebar.html:182
msgid "Account Groups"
msgstr "Groupes de comptes"
-#: apps/accounts/models.py:39 apps/currencies/models.py:44
+#: apps/accounts/models.py:43
+msgid "Target allocation (%)"
+msgstr ""
+
+#: apps/accounts/models.py:45
+msgid ""
+"Share of your investments you want in this class. Leave it empty to leave "
+"the class out of the targets."
+msgstr ""
+
+#: apps/accounts/models.py:61
+msgid "You already have an asset class with this name."
+msgstr ""
+
+#: apps/accounts/models.py:66
+#, python-format
+msgid ""
+"Targets can't add up to more than 100%%. Other classes already take "
+"%(taken)s%%."
+msgstr ""
+
+#: apps/accounts/models.py:74 apps/export_app/forms.py:131
+#: templates/includes/sidebar.html:190
+#: templates/investment_classes/fragments/list.html:9
+#: templates/investment_classes/pages/index.html:4
+#: templates/investments/fragments/rebalance.html:6
+#: templates/investments/fragments/rebalance.html:11
+msgid "Asset classes"
+msgstr ""
+
+#: apps/accounts/models.py:94 apps/currencies/models.py:44
#: templates/accounts/fragments/list.html:24
#: templates/mini_tools/simulator.html:37
#: templates/yearly_overview/pages/yearly_overview_base.html:19
msgid "Currency"
msgstr "Devise"
-#: apps/accounts/models.py:45 apps/currencies/models.py:27
+#: apps/accounts/models.py:100 apps/currencies/models.py:27
#: templates/accounts/fragments/list.html:25
msgid "Exchange Currency"
msgstr "Devise d'échange"
-#: apps/accounts/models.py:50 apps/currencies/models.py:32
+#: apps/accounts/models.py:105 apps/currencies/models.py:32
msgid "Default currency for exchange calculations"
msgstr "Devise par défaut pour le calcul de change"
-#: apps/accounts/models.py:55
+#: apps/accounts/models.py:110
msgid "Asset account"
msgstr "Compte d'actif"
-#: apps/accounts/models.py:57
+#: apps/accounts/models.py:112
msgid ""
"Asset accounts count towards your Net Worth, but not towards your month."
msgstr ""
"Les comptes d'actifs contribue à votre Valeur Nette mais pas à votre mois."
-#: apps/accounts/models.py:62 apps/currencies/models.py:37
+#: apps/accounts/models.py:117 templates/investments/fragments/list.html:46
+#, fuzzy
+#| msgid "Installment"
+msgid "Investment"
+msgstr "Paiement en plusieurs fois"
+
+#: apps/accounts/models.py:119
+msgid ""
+"Show this account in the Investments tracker. Set an exchange currency to "
+"price it by units (stocks, ETFs, crypto); leave it empty to track it by "
+"value (fixed income, property, pensions)."
+msgstr ""
+
+#: apps/accounts/models.py:132
+msgid ""
+"Groups investments in the allocation charts, e.g. Fixed income, Stocks, Real "
+"estate."
+msgstr ""
+
+#: apps/accounts/models.py:140
+#, fuzzy
+#| msgid "No entities"
+msgid "Income entities"
+msgstr "Pas d'entités"
+
+#: apps/accounts/models.py:142
+msgid ""
+"Transactions with any of these entities count towards this investment's "
+"return: in this account, and income paid into other accounts (dividends, "
+"rent)."
+msgstr ""
+
+#: apps/accounts/models.py:151
+#, fuzzy
+#| msgid "No categories"
+msgid "Income categories"
+msgstr "Pas de catégories"
+
+#: apps/accounts/models.py:153
+msgid ""
+"Transactions in this account with any of these categories are its result "
+"(interest, taxes, fees). On accounts tracked by value, everything else that "
+"isn't a transfer counts as money coming in or going out."
+msgstr ""
+
+#: apps/accounts/models.py:162
+#, fuzzy
+#| msgid "Income"
+msgid "Income tags"
+msgstr "Revenus"
+
+#: apps/accounts/models.py:164
+msgid "Same as income categories, for transactions with any of these tags."
+msgstr ""
+
+#: apps/accounts/models.py:169 apps/currencies/models.py:37
#: templates/accounts/fragments/list.html:27
#: templates/categories/fragments/list.html:20
#: templates/currencies/fragments/list.html:24
@@ -190,19 +284,19 @@ msgstr ""
msgid "Archived"
msgstr "Archivé"
-#: apps/accounts/models.py:63
+#: apps/accounts/models.py:170
msgid "Archived accounts don't show up nor count towards your net worth"
msgstr ""
"Les comptes archivés ne sont ni affichés ni comptabilisés dans votre valeur "
"nette"
-#: apps/accounts/models.py:75 apps/rules/forms.py:173 apps/rules/forms.py:187
+#: apps/accounts/models.py:182 apps/rules/forms.py:173 apps/rules/forms.py:187
#: apps/rules/models.py:35 apps/rules/models.py:267
#: apps/transactions/forms.py:82 apps/transactions/forms.py:350
#: apps/transactions/forms.py:477 apps/transactions/forms.py:808
#: apps/transactions/forms.py:1060 apps/transactions/models.py:329
-#: apps/transactions/models.py:703 apps/transactions/models.py:981
-#: apps/transactions/models.py:1280
+#: apps/transactions/models.py:727 apps/transactions/models.py:1005
+#: apps/transactions/models.py:1304
#: templates/installment_plans/fragments/table.html:17
#: templates/quick_transactions/fragments/list.html:14
#: templates/recurring_transactions/fragments/table.html:19
@@ -210,17 +304,17 @@ msgstr ""
msgid "Account"
msgstr "Compte"
-#: apps/accounts/models.py:76 apps/export_app/forms.py:19
-#: apps/export_app/forms.py:129 apps/transactions/filters.py:63
+#: apps/accounts/models.py:183 apps/export_app/forms.py:19
+#: apps/export_app/forms.py:130 apps/transactions/filters.py:63
#: templates/accounts/fragments/list.html:9
-#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:168
-#: templates/includes/sidebar.html:170
+#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:174
+#: templates/includes/sidebar.html:176
#: templates/monthly_overview/pages/overview.html:77
#: templates/transactions/pages/transactions.html:26
msgid "Accounts"
msgstr "Comptes"
-#: apps/accounts/models.py:93
+#: apps/accounts/models.py:200
msgid "Exchange currency cannot be the same as the account's main currency."
msgstr ""
"La devise de change ne peut pas être identique à la devise principal du "
@@ -240,7 +334,7 @@ msgstr "Groupe de compte supprimé avec succès"
#: apps/accounts/views/account_groups.py:109
#: apps/accounts/views/accounts.py:137 apps/dca/views.py:106
-#: apps/import_app/views.py:147 apps/rules/views.py:191
+#: apps/import_app/views.py:148 apps/rules/views.py:191
#: apps/transactions/views/categories.py:163
#: apps/transactions/views/entities.py:135 apps/transactions/views/tags.py:131
msgid "Item no longer shared with you"
@@ -248,7 +342,7 @@ msgstr "Cette objet n'est plus partagé avec vous"
#: apps/accounts/views/account_groups.py:132
#: apps/accounts/views/accounts.py:182 apps/dca/views.py:131
-#: apps/import_app/views.py:273 apps/rules/views.py:217
+#: apps/import_app/views.py:274 apps/rules/views.py:217
#: apps/transactions/views/categories.py:188
#: apps/transactions/views/entities.py:160 apps/transactions/views/tags.py:154
msgid "Ownership taken successfully"
@@ -256,7 +350,7 @@ msgstr "Propriété acquise avec succès"
#: apps/accounts/views/account_groups.py:152
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:151
-#: apps/import_app/views.py:294 apps/rules/views.py:238
+#: apps/import_app/views.py:295 apps/rules/views.py:238
#: apps/transactions/views/categories.py:130
#: apps/transactions/views/entities.py:180 apps/transactions/views/tags.py:174
msgid "Configuration saved successfully"
@@ -314,7 +408,7 @@ msgstr "Aucune entité avec cet ID n'existe."
msgid "Invalid entity data. Provide an ID or name."
msgstr "Donnée d'entité invalide. Veuillez fournir un ID ou nom."
-#: apps/api/serializers/transactions.py:141 apps/transactions/models.py:953
+#: apps/api/serializers/transactions.py:141 apps/transactions/models.py:977
msgid "This field is required."
msgstr "Ce champ est obligatoire."
@@ -334,13 +428,14 @@ msgstr "Rendre privé"
msgid "Shared with users"
msgstr "Partagé avec les utilisateurs"
-#: apps/common/fields/forms/dynamic_select.py:71
-#: apps/common/fields/forms/dynamic_select.py:142
+#: apps/common/fields/forms/dynamic_select.py:79
+#: apps/common/fields/forms/dynamic_select.py:150
msgid "Error creating new instance"
msgstr "Erreur lors de la création d'une nouvelle instance"
#: apps/common/fields/forms/grouped_select.py:24
-#: apps/common/widgets/tom_select.py:92 apps/common/widgets/tom_select.py:95
+#: apps/common/widgets/tom_select.py:94 apps/common/widgets/tom_select.py:97
+#: templates/investments/fragments/list.html:26
msgid "Ungrouped"
msgstr "Non-groupé"
@@ -488,14 +583,15 @@ msgid "Today"
msgstr "Aujourd'hui"
#: apps/common/widgets/datepicker.py:142
+#: templates/investments/fragments/rebalance.html:24
msgid "Now"
msgstr "Maintenant"
-#: apps/common/widgets/tom_select.py:11
+#: apps/common/widgets/tom_select.py:13
msgid "Remove"
msgstr "Enlever"
-#: apps/common/widgets/tom_select.py:15 templates/mini_tools/simulator.html:187
+#: apps/common/widgets/tom_select.py:17 templates/mini_tools/simulator.html:187
#: templates/mini_tools/unit_price_calculator.html:180
#: templates/monthly_overview/pages/overview.html:286
#: templates/transactions/fragments/filter_actions.html:48
@@ -503,7 +599,7 @@ msgstr "Enlever"
msgid "Clear"
msgstr "Vider"
-#: apps/common/widgets/tom_select.py:16
+#: apps/common/widgets/tom_select.py:18
msgid "No results..."
msgstr "Pas de résultats..."
@@ -537,10 +633,10 @@ msgid "Decimal Places"
msgstr "Décimales"
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
-#: apps/export_app/forms.py:130 apps/transactions/filters.py:70
+#: apps/export_app/forms.py:132 apps/transactions/filters.py:70
#: templates/currencies/fragments/list.html:9
-#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:182
-#: templates/includes/sidebar.html:184
+#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:196
+#: templates/includes/sidebar.html:198
#: templates/monthly_overview/pages/overview.html:63
#: templates/transactions/pages/transactions.html:12
msgid "Currencies"
@@ -572,9 +668,9 @@ msgid "Auto"
msgstr "Auto"
#: apps/currencies/models.py:82 apps/export_app/forms.py:67
-#: apps/export_app/forms.py:142 templates/exchange_rates/fragments/list.html:10
+#: apps/export_app/forms.py:144 templates/exchange_rates/fragments/list.html:10
#: templates/exchange_rates/pages/index.html:4
-#: templates/includes/sidebar.html:190
+#: templates/includes/sidebar.html:204
msgid "Exchange Rates"
msgstr "Taux de changes"
@@ -669,32 +765,45 @@ msgstr ""
"récupérés pour chaque compte par rapport à leur devise d'échange."
#: apps/currencies/models.py:171
-msgid "Single exchange rate"
+#, fuzzy
+#| msgid "Single exchange rate"
+msgid "One exchange rate per day"
msgstr "Taux de change unique"
#: apps/currencies/models.py:174
-msgid "Create one exchange rate and keep updating it. Avoids database clutter."
+msgid ""
+"Update the day's exchange rate on each fetch instead of adding a new one."
msgstr ""
-"Ne créer qu'un seul taux de change et le mettre à jour. Evite d'engorger la "
-"base de donnée."
-#: apps/currencies/models.py:179
+#: apps/currencies/models.py:178
+#, fuzzy
+#| msgid "Exchange Rates"
+msgid "Exchange rates to keep"
+msgstr "Taux de changes"
+
+#: apps/currencies/models.py:183
+msgid ""
+"Keep at most this many automatic exchange rates per currency pair, deleting "
+"the oldest first. Leave empty to keep all."
+msgstr ""
+
+#: apps/currencies/models.py:188
msgid "Exchange Rate Service"
msgstr "Service de taux de change"
-#: apps/currencies/models.py:180
+#: apps/currencies/models.py:189
msgid "Exchange Rate Services"
msgstr "Services de taux de change"
-#: apps/currencies/models.py:234
+#: apps/currencies/models.py:243
msgid "'Every X hours' interval type requires a positive integer."
msgstr "'Toutes les X heures' l'intervalle requiert un entier positif."
-#: apps/currencies/models.py:243
+#: apps/currencies/models.py:252
msgid "'Every X hours' interval must be between 1 and 24."
msgstr "'Toutes les X heures' l'intervalle doit être compris entre 1 et 24."
-#: apps/currencies/models.py:257
+#: apps/currencies/models.py:266
msgid ""
"Invalid hour format. Use comma-separated hours (0-23) and/or ranges (e.g., "
"'1-5,8,10-12')."
@@ -702,7 +811,7 @@ msgstr ""
"Format d'heure invalide. Utilisez les heures séparé par virgule (0-23) et/ou "
"une plage (ex : '1-5,8,10-12')."
-#: apps/currencies/models.py:268
+#: apps/currencies/models.py:277
msgid ""
"Invalid format. Please check the requirements for your selected interval "
"type."
@@ -774,22 +883,22 @@ msgstr "Ecrivez pour rechercher une transaction à lier avec cette entrée"
msgid "Income Transaction"
msgstr "Transaction entrante"
-#: apps/dca/forms.py:192
+#: apps/dca/forms.py:179
msgid "Link transaction"
msgstr "Lié transaction"
-#: apps/dca/forms.py:275 apps/dca/forms.py:276 apps/dca/forms.py:281
-#: apps/dca/forms.py:285
+#: apps/dca/forms.py:262 apps/dca/forms.py:263 apps/dca/forms.py:268
+#: apps/dca/forms.py:272
msgid "You must provide an account."
msgstr "Vous devez fournir un compte."
-#: apps/dca/forms.py:290 apps/transactions/forms.py:752
-#: apps/transactions/models.py:956
+#: apps/dca/forms.py:277 apps/transactions/forms.py:752
+#: apps/transactions/models.py:980
msgid "From and To accounts must be different."
msgstr ""
"Le compte originateur et le compte bénéficiaire doivent être différent."
-#: apps/dca/forms.py:304
+#: apps/dca/forms.py:299
#, python-format
msgid "DCA for %(strategy_name)s"
msgstr "DCA pour %(strategy_name)s"
@@ -805,8 +914,8 @@ msgstr "Devise de paiement"
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
#: apps/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
#: apps/transactions/forms.py:504 apps/transactions/forms.py:651
-#: apps/transactions/models.py:353 apps/transactions/models.py:752
-#: apps/transactions/models.py:1009 apps/transactions/models.py:1302
+#: apps/transactions/models.py:353 apps/transactions/models.py:776
+#: apps/transactions/models.py:1033 apps/transactions/models.py:1326
msgid "Notes"
msgstr "Notes"
@@ -814,7 +923,7 @@ msgstr "Notes"
msgid "DCA Strategy"
msgstr "Stratégie DCA"
-#: apps/dca/models.py:35 apps/export_app/forms.py:150
+#: apps/dca/models.py:35 apps/export_app/forms.py:152
msgid "DCA Strategies"
msgstr "Stratégies DCA"
@@ -834,7 +943,7 @@ msgstr "Somme reçue"
msgid "DCA Entry"
msgstr "Entrée DCA"
-#: apps/dca/models.py:187 apps/export_app/forms.py:151
+#: apps/dca/models.py:187 apps/export_app/forms.py:153
msgid "DCA Entries"
msgstr "Entrées DCA"
@@ -862,25 +971,25 @@ msgstr "Entrée mise à jour avec succès"
msgid "Entry deleted successfully"
msgstr "Entrée supprimée avec succès"
-#: apps/export_app/forms.py:13 apps/export_app/forms.py:128
-#: templates/includes/sidebar.html:227 templates/users/fragments/list.html:10
+#: apps/export_app/forms.py:13 apps/export_app/forms.py:129
+#: templates/includes/sidebar.html:241 templates/users/fragments/list.html:10
#: templates/users/pages/index.html:4
msgid "Users"
msgstr "Utilisateurs"
-#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
+#: apps/export_app/forms.py:31 apps/export_app/forms.py:136
#: apps/transactions/models.py:422 templates/includes/sidebar.html:81
-#: templates/includes/sidebar.html:148
+#: templates/includes/sidebar.html:154
#: templates/recurring_transactions/fragments/list_transactions.html:5
#: templates/recurring_transactions/fragments/table.html:37
#: templates/transactions/pages/transactions.html:5
msgid "Transactions"
msgstr "Transactions"
-#: apps/export_app/forms.py:37 apps/export_app/forms.py:131
+#: apps/export_app/forms.py:37 apps/export_app/forms.py:133
#: apps/insights/views.py:43 apps/transactions/filters.py:74
#: templates/categories/fragments/list.html:9
-#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:150
+#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:156
#: templates/insights/fragments/month_by_month.html:18
#: templates/insights/fragments/month_by_month.html:21
#: templates/insights/fragments/year_by_year.html:13
@@ -888,16 +997,16 @@ msgstr "Transactions"
msgid "Categories"
msgstr "Catégories"
-#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
+#: apps/export_app/forms.py:49 apps/export_app/forms.py:135
#: apps/insights/views.py:57 apps/rules/forms.py:185 apps/rules/forms.py:195
#: apps/rules/models.py:46 apps/rules/models.py:307
#: apps/transactions/filters.py:84 apps/transactions/forms.py:78
#: apps/transactions/forms.py:346 apps/transactions/forms.py:526
#: apps/transactions/forms.py:831 apps/transactions/forms.py:1083
#: apps/transactions/models.py:312 apps/transactions/models.py:368
-#: apps/transactions/models.py:748 apps/transactions/models.py:1006
-#: apps/transactions/models.py:1317 templates/entities/fragments/list.html:9
-#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:162
+#: apps/transactions/models.py:772 apps/transactions/models.py:1030
+#: apps/transactions/models.py:1341 templates/entities/fragments/list.html:9
+#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:168
#: templates/insights/fragments/month_by_month.html:40
#: templates/insights/fragments/month_by_month.html:43
#: templates/insights/fragments/year_by_year.html:35
@@ -905,28 +1014,28 @@ msgstr "Catégories"
msgid "Entities"
msgstr "Entités"
-#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
-#: apps/transactions/models.py:1046 templates/includes/sidebar.html:110
+#: apps/export_app/forms.py:55 apps/export_app/forms.py:139
+#: apps/transactions/models.py:1070 templates/includes/sidebar.html:110
#: templates/recurring_transactions/fragments/list.html:9
#: templates/recurring_transactions/pages/index.html:4
msgid "Recurring Transactions"
msgstr "Transactions récurrentes"
-#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
-#: apps/transactions/models.py:766 templates/includes/sidebar.html:104
+#: apps/export_app/forms.py:61 apps/export_app/forms.py:137
+#: apps/transactions/models.py:790 templates/includes/sidebar.html:104
#: templates/installment_plans/fragments/list.html:9
#: templates/installment_plans/pages/index.html:4
msgid "Installment Plans"
msgstr "Paiements en plusieurs fois"
-#: apps/export_app/forms.py:73 apps/export_app/forms.py:140
+#: apps/export_app/forms.py:73 apps/export_app/forms.py:142
#: templates/exchange_rates_services/fragments/list.html:10
#: templates/exchange_rates_services/pages/index.html:4
-#: templates/includes/sidebar.html:218
+#: templates/includes/sidebar.html:232
msgid "Automatic Exchange Rates"
msgstr "Taux de change automatique"
-#: apps/export_app/forms.py:79 templates/includes/sidebar.html:198
+#: apps/export_app/forms.py:79 templates/includes/sidebar.html:212
#: templates/rules/fragments/list.html:9 templates/rules/pages/index.html:4
msgid "Rules"
msgstr "Règles"
@@ -935,57 +1044,57 @@ msgstr "Règles"
msgid "DCA"
msgstr "DCA"
-#: apps/export_app/forms.py:91 apps/export_app/forms.py:152
+#: apps/export_app/forms.py:91 apps/export_app/forms.py:154
#: templates/import_app/fragments/profiles/list.html:21
#: templates/import_app/pages/profiles_index.html:4
msgid "Import Profiles"
msgstr "Importer des profils"
-#: apps/export_app/forms.py:117 templates/export_app/fragments/export.html:5
+#: apps/export_app/forms.py:118 templates/export_app/fragments/export.html:5
#: templates/export_app/pages/index.html:13
msgid "Export"
msgstr "Exporter"
-#: apps/export_app/forms.py:125
+#: apps/export_app/forms.py:126
msgid "Import a ZIP file exported from WYGIWYH"
msgstr "Importer un fichier ZIP exporté de WYGIWYH"
-#: apps/export_app/forms.py:126
+#: apps/export_app/forms.py:127
msgid "ZIP File"
msgstr "Fichier ZIP"
-#: apps/export_app/forms.py:143 apps/rules/models.py:27
+#: apps/export_app/forms.py:145 apps/rules/models.py:27
msgid "Transaction rules"
msgstr "Règles de transaction"
-#: apps/export_app/forms.py:145 apps/rules/models.py:68
+#: apps/export_app/forms.py:147 apps/rules/models.py:68
msgid "Edit transaction action"
msgstr "Modifier l'action de transaction"
-#: apps/export_app/forms.py:148 apps/rules/models.py:327
+#: apps/export_app/forms.py:150 apps/rules/models.py:327
msgid "Update or create transaction actions"
msgstr "Mettre à jour ou créer des actions de transaction"
-#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:341
+#: apps/export_app/forms.py:183 templates/cotton/transaction/item.html:341
#: templates/cotton/ui/deleted_transactions_action_bar.html:53
#: templates/export_app/fragments/restore.html:5
#: templates/export_app/pages/index.html:19
msgid "Restore"
msgstr "Restaurer"
-#: apps/export_app/forms.py:191
+#: apps/export_app/forms.py:193
msgid "Please upload either a ZIP file or at least one CSV file"
msgstr "Veuillez charger un fichier ZIP ou au moins un fichier CSV"
-#: apps/export_app/views.py:177
+#: apps/export_app/views.py:183
msgid "You have to select at least one export"
msgstr "Vous devez sélectionner au moins un export"
-#: apps/export_app/views.py:197
+#: apps/export_app/views.py:203
msgid "Data restored successfully"
msgstr "Donnée restaurée avec succès"
-#: apps/export_app/views.py:209
+#: apps/export_app/views.py:215
msgid ""
"There was an error restoring your data. Check the logs for more details."
msgstr ""
@@ -998,7 +1107,7 @@ msgstr "Sélectionnez un fichier"
#: apps/import_app/forms.py:55
#: templates/import_app/fragments/profiles/list.html:75
-#: templates/includes/sidebar.html:204
+#: templates/includes/sidebar.html:218
msgid "Import"
msgstr "Importer"
@@ -1046,23 +1155,23 @@ msgstr "Statut"
msgid "File name"
msgstr "Nom du fichier"
-#: apps/import_app/views.py:75
+#: apps/import_app/views.py:76
msgid "Import Profile added successfully"
msgstr "Profil d'import ajouté avec succès"
-#: apps/import_app/views.py:113
+#: apps/import_app/views.py:114
msgid "Import Profile update successfully"
msgstr "Profil d'import mis à jour avec succès"
-#: apps/import_app/views.py:142
+#: apps/import_app/views.py:143
msgid "Import Profile deleted successfully"
msgstr "Profil d'import supprimé avec succès"
-#: apps/import_app/views.py:222
+#: apps/import_app/views.py:223
msgid "Import Run queued successfully"
msgstr "Import d’exécution mis en file d'attente avec succès"
-#: apps/import_app/views.py:249
+#: apps/import_app/views.py:250
msgid "Run deleted successfully"
msgstr "Exécution supprimé avec succès"
@@ -1131,7 +1240,7 @@ msgstr "Etiquette"
msgid "Untagged"
msgstr "Sans étiquettes"
-#: apps/insights/views.py:53 apps/transactions/models.py:589
+#: apps/insights/views.py:53 apps/transactions/models.py:613
#: templates/tags/fragments/table.html:69
msgid "No tags"
msgstr "Aucune étiquette"
@@ -1152,6 +1261,32 @@ msgstr "Pas d'entité"
msgid "No entities"
msgstr "Pas d'entités"
+#: apps/investments/views.py:104
+msgid "Other"
+msgstr ""
+
+#: apps/investments/views.py:135
+msgid "Unclassified"
+msgstr ""
+
+#: apps/investments/views.py:267
+#, fuzzy
+#| msgid "Installment Plan added successfully"
+msgid "Asset class added successfully"
+msgstr "Paiement en plusieurs fois ajouté avec succès"
+
+#: apps/investments/views.py:275
+#, fuzzy
+#| msgid "Tag updated successfully"
+msgid "Asset class updated successfully"
+msgstr "Etiquette mise à jour avec succès"
+
+#: apps/investments/views.py:296
+#, fuzzy
+#| msgid "Tag deleted successfully"
+msgid "Asset class deleted successfully"
+msgstr "Etiquette supprimée avec succès"
+
#: apps/net_worth/views.py:137 templates/net_worth/net_worth.html:84
#: templates/net_worth/net_worth.html:85
msgid "Consolidated"
@@ -1205,8 +1340,8 @@ msgstr "Opérateur"
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
#: apps/rules/models.py:271 apps/transactions/forms.py:468
-#: apps/transactions/models.py:336 apps/transactions/models.py:708
-#: apps/transactions/models.py:987 apps/transactions/models.py:1287
+#: apps/transactions/models.py:336 apps/transactions/models.py:732
+#: apps/transactions/models.py:1011 apps/transactions/models.py:1311
#: templates/mini_tools/simulator.html:63
msgid "Type"
msgstr "Type"
@@ -1214,7 +1349,7 @@ msgstr "Type"
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
#: apps/rules/models.py:275 apps/transactions/filters.py:22
#: apps/transactions/forms.py:472 apps/transactions/models.py:338
-#: apps/transactions/models.py:1289 templates/cotton/transaction/item.html:23
+#: apps/transactions/models.py:1313 templates/cotton/transaction/item.html:23
#: templates/cotton/transaction/item.html:34
#: templates/transactions/widgets/paid_toggle_button.html:10
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
@@ -1225,14 +1360,14 @@ msgstr "Payé"
#: apps/rules/models.py:283 apps/transactions/forms.py:90
#: apps/transactions/forms.py:488 apps/transactions/forms.py:638
#: apps/transactions/forms.py:837 apps/transactions/models.py:340
-#: apps/transactions/models.py:726 apps/transactions/models.py:1011
+#: apps/transactions/models.py:750 apps/transactions/models.py:1035
msgid "Reference Date"
msgstr "Date de référence"
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
#: apps/rules/models.py:287 apps/transactions/forms.py:495
-#: apps/transactions/models.py:346 apps/transactions/models.py:992
-#: apps/transactions/models.py:1295
+#: apps/transactions/models.py:346 apps/transactions/models.py:1016
+#: apps/transactions/models.py:1319
#: templates/insights/fragments/sankey.html:102
#: templates/installment_plans/fragments/table.html:18
#: templates/mini_tools/simulator.html:70
@@ -1244,27 +1379,27 @@ msgstr "Montant"
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
#: apps/rules/models.py:42 apps/rules/models.py:291
#: apps/transactions/forms.py:499 apps/transactions/forms.py:642
-#: apps/transactions/models.py:351 apps/transactions/models.py:710
-#: apps/transactions/models.py:995 apps/transactions/models.py:1300
+#: apps/transactions/models.py:351 apps/transactions/models.py:734
+#: apps/transactions/models.py:1019 apps/transactions/models.py:1324
msgid "Description"
msgstr "Description"
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
#: apps/rules/models.py:299 apps/transactions/models.py:398
-#: apps/transactions/models.py:1322
+#: apps/transactions/models.py:1346
msgid "Internal Note"
msgstr "Note interne"
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
#: apps/rules/models.py:303 apps/transactions/models.py:400
-#: apps/transactions/models.py:1324
+#: apps/transactions/models.py:1348
msgid "Internal ID"
msgstr "ID interne"
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
#: apps/rules/models.py:319 apps/transactions/forms.py:655
#: apps/transactions/models.py:250 apps/transactions/models.py:341
-#: apps/transactions/models.py:1290
+#: apps/transactions/models.py:1314
msgid "Mute"
msgstr "Silencieux"
@@ -1277,7 +1412,7 @@ msgid "Set Values"
msgstr "Configurer les valeurs"
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
-#: apps/transactions/models.py:421 apps/transactions/models.py:645
+#: apps/transactions/models.py:421 apps/transactions/models.py:669
msgid "Transaction"
msgstr "Transaction"
@@ -1313,7 +1448,12 @@ msgstr "Règle"
msgid "Field"
msgstr "Champ"
-#: apps/rules/models.py:61
+#: apps/rules/models.py:61 templates/investments/fragments/charts.html:86
+#: templates/investments/fragments/detail.html:103
+#: templates/investments/fragments/detail.html:162
+#: templates/investments/fragments/list.html:48
+#: templates/investments/fragments/list.html:49
+#: templates/investments/fragments/rebalance.html:23
msgid "Value"
msgstr "Valeur"
@@ -1501,7 +1641,7 @@ msgid "Upload"
msgstr "Envoi"
#: apps/transactions/forms.py:355 apps/transactions/forms.py:1089
-#: apps/transactions/models.py:925
+#: apps/transactions/models.py:949
#, fuzzy
#| msgid "To Account"
msgid "To account"
@@ -1520,8 +1660,8 @@ msgstr "Montant de départ"
msgid "To Amount"
msgstr "Montant d'arrivée"
-#: apps/transactions/forms.py:718 apps/transactions/models.py:970
-#: apps/transactions/models.py:1267 templates/cotton/transaction/item.html:52
+#: apps/transactions/forms.py:718 apps/transactions/models.py:994
+#: apps/transactions/models.py:1291 templates/cotton/transaction/item.html:52
#: templates/cotton/transaction/item.html:60
#: templates/cotton/ui/quick_transactions_buttons.html:40
#: templates/cotton/ui/transactions_fab.html:44
@@ -1588,8 +1728,8 @@ msgstr ""
"Les entités désactivées ne pourront pas être sélectionnées lors de la "
"création de nouvelles transactions"
-#: apps/transactions/models.py:323 apps/transactions/models.py:969
-#: apps/transactions/models.py:1266
+#: apps/transactions/models.py:323 apps/transactions/models.py:993
+#: apps/transactions/models.py:1290
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: templates/calendar_view/fragments/list.html:52
@@ -1597,13 +1737,19 @@ msgstr ""
#: templates/cotton/ui/quick_transactions_buttons.html:10
#: templates/cotton/ui/transactions_fab.html:10
#: templates/insights/fragments/overview/_results.html:12
+#: templates/investments/fragments/_totals.html:21
+#: templates/investments/fragments/_totals.html:42
+#: templates/investments/fragments/detail.html:106
+#: templates/investments/fragments/detail.html:210
+#: templates/investments/fragments/detail.html:252
+#: templates/investments/fragments/detail.html:320
#: templates/mini_tools/simulator.html:66
#: templates/monthly_overview/fragments/monthly_summary.html:42
msgid "Income"
msgstr "Revenus"
-#: apps/transactions/models.py:324 apps/transactions/models.py:971
-#: apps/transactions/models.py:1268
+#: apps/transactions/models.py:324 apps/transactions/models.py:995
+#: apps/transactions/models.py:1292
#: templates/calendar_view/fragments/list.html:46
#: templates/calendar_view/fragments/list.html:48
#: templates/calendar_view/fragments/list.html:56
@@ -1615,14 +1761,14 @@ msgstr "Revenus"
msgid "Expense"
msgstr "Dépense"
-#: apps/transactions/models.py:379 apps/transactions/models.py:765
+#: apps/transactions/models.py:379 apps/transactions/models.py:789
#: templates/cotton/transaction/item.html:291
#: templates/cotton/transaction/item.html:308
#: templates/cotton/transaction/item.html:318
msgid "Installment Plan"
msgstr "Plan d'aménagement"
-#: apps/transactions/models.py:388 apps/transactions/models.py:1045
+#: apps/transactions/models.py:388 apps/transactions/models.py:1069
#: templates/cotton/transaction/item.html:281
#: templates/cotton/transaction/item.html:301
msgid "Recurring Transaction"
@@ -1640,152 +1786,164 @@ msgstr "Supprimé"
msgid "Deleted At"
msgstr "Supprimé à"
-#: apps/transactions/models.py:591
+#: apps/transactions/models.py:517
+msgid "A transfer needs one expense and one income."
+msgstr ""
+
+#: apps/transactions/models.py:519
+msgid "A transfer needs two different accounts."
+msgstr ""
+
+#: apps/transactions/models.py:523
+msgid "This transaction is already part of another transfer."
+msgstr ""
+
+#: apps/transactions/models.py:615
msgid "No category"
msgstr "Pas de catégorie"
-#: apps/transactions/models.py:593
+#: apps/transactions/models.py:617
msgid "No description"
msgstr "Pas de description"
-#: apps/transactions/models.py:650
+#: apps/transactions/models.py:674
msgid "File"
msgstr "Fichier"
-#: apps/transactions/models.py:652
+#: apps/transactions/models.py:676
msgid "Original Name"
msgstr "Nom initial"
-#: apps/transactions/models.py:654
+#: apps/transactions/models.py:678
msgid "Content Type"
msgstr "Type de contenu"
-#: apps/transactions/models.py:656
+#: apps/transactions/models.py:680
msgid "Size"
msgstr "Taille"
-#: apps/transactions/models.py:661
+#: apps/transactions/models.py:685
msgid "Uploaded By"
msgstr "Envoyer par"
-#: apps/transactions/models.py:666
+#: apps/transactions/models.py:690
msgid "Transaction Attachment"
msgstr "Fichiers de la transaction"
-#: apps/transactions/models.py:667
+#: apps/transactions/models.py:691
msgid "Transaction Attachments"
msgstr "Fichiers de transactions"
-#: apps/transactions/models.py:697 templates/includes/sidebar.html:57
+#: apps/transactions/models.py:721 templates/includes/sidebar.html:57
msgid "Yearly"
msgstr "Annuel"
-#: apps/transactions/models.py:698 apps/users/models.py:469
+#: apps/transactions/models.py:722 apps/users/models.py:469
#: templates/includes/sidebar.html:51
msgid "Monthly"
msgstr "Mensuel"
-#: apps/transactions/models.py:699
+#: apps/transactions/models.py:723
msgid "Weekly"
msgstr "Hebdomadaire"
-#: apps/transactions/models.py:700
+#: apps/transactions/models.py:724
msgid "Daily"
msgstr "Quotidien"
-#: apps/transactions/models.py:713
+#: apps/transactions/models.py:737
msgid "Number of Installments"
msgstr "Nombre d'écheances"
-#: apps/transactions/models.py:718
+#: apps/transactions/models.py:742
msgid "Installment Start"
msgstr "Commencer à l'échéance"
-#: apps/transactions/models.py:719
+#: apps/transactions/models.py:743
msgid "The installment number to start counting from"
msgstr "L'échéance à partir de laquelle compter"
-#: apps/transactions/models.py:724 apps/transactions/models.py:1015
+#: apps/transactions/models.py:748 apps/transactions/models.py:1039
msgid "Start Date"
msgstr "Date de début"
-#: apps/transactions/models.py:728 apps/transactions/models.py:1016
+#: apps/transactions/models.py:752 apps/transactions/models.py:1040
msgid "End Date"
msgstr "Date de fin"
-#: apps/transactions/models.py:733
+#: apps/transactions/models.py:757
msgid "Recurrence"
msgstr "Récurrence"
-#: apps/transactions/models.py:736
+#: apps/transactions/models.py:760
msgid "Installment Amount"
msgstr "Montant des échéances"
-#: apps/transactions/models.py:755 apps/transactions/models.py:1035
+#: apps/transactions/models.py:779 apps/transactions/models.py:1059
msgid "Add description to transactions"
msgstr "Rajouter une description à la transaction"
-#: apps/transactions/models.py:758 apps/transactions/models.py:1038
+#: apps/transactions/models.py:782 apps/transactions/models.py:1062
msgid "Add notes to transactions"
msgstr "Ajouter des notes aux transactions"
-#: apps/transactions/models.py:932
+#: apps/transactions/models.py:956
#, fuzzy
#| msgid "To Amount"
msgid "To amount"
msgstr "Montant d'arrivée"
-#: apps/transactions/models.py:933
+#: apps/transactions/models.py:957
msgid "Leave empty to use the same amount"
msgstr "Laisser vide pour garder le même montant"
-#: apps/transactions/models.py:974 templates/mini_tools/simulator.html:129
+#: apps/transactions/models.py:998 templates/mini_tools/simulator.html:129
msgid "day(s)"
msgstr "jour(s)"
-#: apps/transactions/models.py:975 templates/mini_tools/simulator.html:130
+#: apps/transactions/models.py:999 templates/mini_tools/simulator.html:130
msgid "week(s)"
msgstr "semaine(s)"
-#: apps/transactions/models.py:976 templates/mini_tools/simulator.html:131
+#: apps/transactions/models.py:1000 templates/mini_tools/simulator.html:131
msgid "month(s)"
msgstr "mois"
-#: apps/transactions/models.py:977 templates/mini_tools/simulator.html:132
+#: apps/transactions/models.py:1001 templates/mini_tools/simulator.html:132
msgid "year(s)"
msgstr "année(s)"
-#: apps/transactions/models.py:979
+#: apps/transactions/models.py:1003
#: templates/recurring_transactions/fragments/list.html:18
msgid "Paused"
msgstr "Interrompu"
-#: apps/transactions/models.py:1018
+#: apps/transactions/models.py:1042
msgid "Recurrence Type"
msgstr "Type de récurrence"
-#: apps/transactions/models.py:1021
+#: apps/transactions/models.py:1045
msgid "Recurrence Interval"
msgstr "Intervalle de récurrence"
-#: apps/transactions/models.py:1024
+#: apps/transactions/models.py:1048
msgid "Keep at most"
msgstr "Répéter un maximum de"
-#: apps/transactions/models.py:1028
+#: apps/transactions/models.py:1052
msgid "Last Generated Date"
msgstr "Dernière date générée"
-#: apps/transactions/models.py:1031
+#: apps/transactions/models.py:1055
msgid "Last Generated Reference Date"
msgstr "Dernière date de référence générée"
-#: apps/transactions/models.py:1334 templates/cotton/transaction/item.html:274
+#: apps/transactions/models.py:1358 templates/cotton/transaction/item.html:274
#: templates/cotton/ui/transactions_fab.html:59
msgid "Quick Transaction"
msgstr "Transaction rapide"
-#: apps/transactions/models.py:1335 templates/includes/sidebar.html:98
+#: apps/transactions/models.py:1359 templates/includes/sidebar.html:98
#: templates/quick_transactions/pages/index.html:5
#: templates/quick_transactions/pages/index.html:15
msgid "Quick Transactions"
@@ -1908,8 +2066,8 @@ msgid "Item deleted successfully"
msgstr "Item supprimée avec succès"
#: apps/transactions/views/quick_transactions.py:164
-#: apps/transactions/views/transactions.py:143
-#: apps/transactions/views/transactions.py:328
+#: apps/transactions/views/transactions.py:156
+#: apps/transactions/views/transactions.py:343
msgid "Transaction added successfully"
msgstr "Transaction ajoutée avec succès"
@@ -1945,42 +2103,42 @@ msgstr "Etiquette mise à jour avec succès"
msgid "Tag deleted successfully"
msgstr "Etiquette supprimée avec succès"
-#: apps/transactions/views/transactions.py:61
+#: apps/transactions/views/transactions.py:65
msgid "Attachment uploaded successfully"
msgstr "Fichier téléchargé avec succès"
-#: apps/transactions/views/transactions.py:112
+#: apps/transactions/views/transactions.py:116
msgid "Attachment deleted successfully"
msgstr "Fichier supprimé avec succès"
-#: apps/transactions/views/transactions.py:355
+#: apps/transactions/views/transactions.py:370
msgid "Transaction updated successfully"
msgstr "Transaction mise à jour avec succès"
-#: apps/transactions/views/transactions.py:406
+#: apps/transactions/views/transactions.py:421
#, python-format
msgid "%(count)s transaction updated successfully"
msgid_plural "%(count)s transactions updated successfully"
msgstr[0] "%(count)s transaction mise à jour avec succès"
msgstr[1] "%(count)s transactions mises à jour avec succès"
-#: apps/transactions/views/transactions.py:433
+#: apps/transactions/views/transactions.py:448
msgid "Transaction duplicated successfully"
msgstr "Transaction dupliquée avec succès"
-#: apps/transactions/views/transactions.py:478
+#: apps/transactions/views/transactions.py:493
msgid "Transaction deleted successfully"
msgstr "Transaction supprimée avec succès"
-#: apps/transactions/views/transactions.py:508
+#: apps/transactions/views/transactions.py:523
msgid "Transaction restored successfully"
msgstr "Transaction restaurée avec succès"
-#: apps/transactions/views/transactions.py:534
+#: apps/transactions/views/transactions.py:549
msgid "Transfer added successfully"
msgstr "Virement ajouté avec succès"
-#: apps/transactions/views/transactions.py:562
+#: apps/transactions/views/transactions.py:579
msgid "Transfer updated successfully"
msgstr "Virement mis à jour avec succès"
@@ -2301,6 +2459,7 @@ msgstr "Éditer un groupe de comptes"
#: templates/exchange_rates_services/fragments/table.html:20
#: templates/import_app/fragments/profiles/list.html:42
#: templates/installment_plans/fragments/table.html:25
+#: templates/investment_classes/fragments/list.html:36
#: templates/quick_transactions/fragments/list.html:22
#: templates/recurring_transactions/fragments/table.html:27
#: templates/rules/fragments/list.html:31
@@ -2323,6 +2482,9 @@ msgstr "Actions"
#: templates/exchange_rates_services/fragments/table.html:23
#: templates/import_app/fragments/profiles/list.html:46
#: templates/installment_plans/fragments/table.html:28
+#: templates/investment_classes/fragments/list.html:39
+#: templates/investments/fragments/detail.html:305
+#: templates/investments/fragments/detail.html:331
#: templates/quick_transactions/fragments/list.html:25
#: templates/recurring_transactions/fragments/table.html:30
#: templates/rules/fragments/transaction_rule/view.html:22
@@ -2372,6 +2534,7 @@ msgstr "Partager"
#: templates/import_app/fragments/profiles/list.html:81
#: templates/import_app/fragments/runs/list.html:100
#: templates/installment_plans/fragments/table.html:54
+#: templates/investment_classes/fragments/list.html:45
#: templates/mini_tools/simulator.html:109
#: templates/mini_tools/unit_price_calculator.html:16
#: templates/quick_transactions/fragments/list.html:32
@@ -2405,6 +2568,7 @@ msgstr "Supprimer"
#: templates/import_app/fragments/runs/list.html:104
#: templates/installment_plans/fragments/table.html:47
#: templates/installment_plans/fragments/table.html:58
+#: templates/investment_classes/fragments/list.html:49
#: templates/quick_transactions/fragments/list.html:37
#: templates/recurring_transactions/fragments/table.html:52
#: templates/recurring_transactions/fragments/table.html:65
@@ -2454,6 +2618,7 @@ msgstr "Cette opération est irréversible, vous ne pourrez pas l'annuler !"
#: templates/import_app/fragments/profiles/list.html:87
#: templates/import_app/fragments/runs/list.html:106
#: templates/installment_plans/fragments/table.html:60
+#: templates/investment_classes/fragments/list.html:51
#: templates/quick_transactions/fragments/list.html:39
#: templates/recurring_transactions/fragments/table.html:94
#: templates/rules/fragments/list.html:62
@@ -2502,6 +2667,7 @@ msgid "Add account"
msgstr "Ajouter un compte"
#: templates/accounts/fragments/edit.html:5
+#: templates/investments/fragments/detail.html:40
msgid "Edit account"
msgstr "Editer le compte"
@@ -2574,7 +2740,7 @@ msgstr "Modifier la catégorie"
msgid "Pick a month"
msgstr "Sélectionner un mois"
-#: templates/common/fragments/toasts.html:20
+#: templates/common/fragments/toasts.html:18
#: templates/cotton/components/fab.html:9
#: templates/cotton/components/sidebar_collapsible_panel.html:28
#: templates/extends/offcanvas.html:4 templates/includes/sidebar.html:44
@@ -2805,6 +2971,8 @@ msgid "Add DCA strategy"
msgstr "Ajouter une stratégie DCA"
#: templates/dca/fragments/strategy/details.html:28
+#: templates/investments/fragments/detail.html:92
+#: templates/investments/fragments/list.html:65
msgid "No exchange rate available"
msgstr "Pas de taux de changes disponibles"
@@ -2987,7 +3155,7 @@ msgstr "Comptes"
msgid "No services configured"
msgstr "Pas de services configurés"
-#: templates/export_app/pages/index.html:4 templates/includes/sidebar.html:211
+#: templates/export_app/pages/index.html:4 templates/includes/sidebar.html:225
msgid "Export and Restore"
msgstr "Export et Restauration"
@@ -3165,52 +3333,63 @@ msgstr "Corbeille"
msgid "Tools"
msgstr "Outils"
-#: templates/includes/sidebar.html:118
+#: templates/includes/sidebar.html:118 templates/includes/sidebar.html:188
+#: templates/investment_classes/fragments/list.html:29
+#: templates/investments/fragments/detail.html:37
+#: templates/investments/fragments/list.html:9
+#: templates/investments/pages/detail.html:4
+#: templates/investments/pages/index.html:4
+#, fuzzy
+#| msgid "Installments"
+msgid "Investments"
+msgstr "Paiements en plusieurs fois"
+
+#: templates/includes/sidebar.html:124
msgid "Dollar Cost Average Tracker"
msgstr "Suivi Dollar Cost Average"
-#: templates/includes/sidebar.html:124
+#: templates/includes/sidebar.html:130
#: templates/mini_tools/unit_price_calculator.html:4
#: templates/mini_tools/unit_price_calculator.html:9
msgid "Unit Price Calculator"
msgstr "Calculateur de prix unitaire"
-#: templates/includes/sidebar.html:130
+#: templates/includes/sidebar.html:136
#: templates/mini_tools/currency_converter/currency_converter.html:7
#: templates/mini_tools/currency_converter/currency_converter.html:14
msgid "Currency Converter"
msgstr "Convertisseur de devises"
-#: templates/includes/sidebar.html:136 templates/mini_tools/simulator.html:5
+#: templates/includes/sidebar.html:142 templates/mini_tools/simulator.html:5
#: templates/mini_tools/simulator.html:21
msgid "Simulator"
msgstr "Simulateur"
-#: templates/includes/sidebar.html:145
+#: templates/includes/sidebar.html:151
msgid "Management"
msgstr "Gestion"
-#: templates/includes/sidebar.html:196
+#: templates/includes/sidebar.html:210
msgid "Automation"
msgstr "Automatisation"
-#: templates/includes/sidebar.html:225
+#: templates/includes/sidebar.html:239
msgid "Admin"
msgstr "Admin"
-#: templates/includes/sidebar.html:233
+#: templates/includes/sidebar.html:247
msgid "Django Admin"
msgstr "Administration Django"
-#: templates/includes/sidebar.html:234
+#: templates/includes/sidebar.html:248
msgid "Only use this if you know what you're doing"
msgstr "A n'utiliser que si vous savez ce que vous faites"
-#: templates/includes/sidebar.html:249
+#: templates/includes/sidebar.html:263
msgid "is available"
msgstr "est disponible"
-#: templates/includes/sidebar.html:257
+#: templates/includes/sidebar.html:271
msgid "Calculator"
msgstr "Calculatrice"
@@ -3277,6 +3456,7 @@ msgstr "Aucune transaction récente"
#: templates/insights/fragments/overview/_results.html:173
#: templates/insights/fragments/year_by_year.html:59
#: templates/insights/fragments/year_by_year.html:140
+#: templates/investment_classes/fragments/list.html:65
#: templates/monthly_overview/fragments/monthly_summary.html:168
msgid "Total"
msgstr "Total"
@@ -3480,6 +3660,305 @@ msgstr ""
msgid "No installment plans"
msgstr "Aucuns paiements en plusieurs fois"
+#: templates/investment_classes/fragments/form.html:5
+msgid "Edit asset class"
+msgstr ""
+
+#: templates/investment_classes/fragments/form.html:5
+msgid "Add asset class"
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:19
+#: templates/investments/fragments/rebalance.html:10
+#, python-format
+msgid "Targets add up to %(total)s%%. Set them to 100%% to see the rebalance."
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:28
+#: templates/investments/fragments/rebalance.html:25
+#, fuzzy
+#| msgid "Targeting"
+msgid "Target"
+msgstr "Ciblage"
+
+#: templates/investment_classes/fragments/list.html:50
+msgid "Its investments will be left without an asset class."
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:74
+msgid "No asset classes"
+msgstr ""
+
+#: templates/investments/fragments/_allocation.html:3
+msgid "Allocation chart"
+msgstr ""
+
+#: templates/investments/fragments/_allocation.html:11
+#, python-format
+msgid ""
+"%(share)s%% of your investments with a target, against a target of "
+"%(target)s%%"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid "Annual return, weighted by when money went in and out"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid ""
+"Return since the first contribution, weighted by when money went in and out"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid "/yr"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:15
+#: templates/investments/fragments/detail.html:63
+msgid "total return"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:20
+#: templates/investments/fragments/_totals.html:41
+#: templates/investments/fragments/charts.html:96
+#: templates/investments/fragments/detail.html:163
+#: templates/investments/fragments/detail.html:223
+#: templates/investments/fragments/list.html:47
+#, fuzzy
+#| msgid "Total Invested"
+msgid "Invested"
+msgstr "Total Investissement"
+
+#: templates/investments/fragments/_totals.html:24
+#: templates/investments/fragments/detail.html:74
+msgid ""
+"How much of the return came from exchange rates moving since you invested"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:24
+#: templates/investments/fragments/_totals.html:26
+#, fuzzy
+#| msgid "Currency Code"
+msgid "Currency effect"
+msgstr "Code de devise"
+
+#: templates/investments/fragments/_totals.html:26
+#: templates/investments/fragments/detail.html:78
+msgid ""
+"Every date is converted with the same rate. Add historical exchange rates "
+"(or an automatic rate service) to see how much exchange rates moved your "
+"return."
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:40
+#: templates/investments/fragments/list.html:49
+msgid "Return"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:10
+msgid "By asset class"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:17
+#: templates/net_worth/net_worth.html:52
+#: templates/yearly_overview/pages/overview_by_currency.html:6
+msgid "By currency"
+msgstr "Par devises"
+
+#: templates/investments/fragments/charts.html:24
+#, fuzzy
+#| msgid "Total Invested"
+msgid "Value and invested"
+msgstr "Total Investissement"
+
+#: templates/investments/fragments/charts.html:31
+msgid "Return by investment"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:38
+#, fuzzy
+#| msgid "Nothing to see here..."
+msgid "Nothing to chart yet"
+msgstr "Rien à voir ici..."
+
+#: templates/investments/fragments/detail.html:10
+msgid "Buy / Contribute"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:16
+msgid "Sell / Withdraw"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:23
+msgid "Dividend / Income"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:30
+#, fuzzy
+#| msgid "Updated at"
+msgid "Update value"
+msgstr "Mis à jour le"
+
+#: templates/investments/fragments/detail.html:47
+#: templates/investments/fragments/list.html:67
+msgid "Tracked by value"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:48
+msgid ""
+"Some incoming units have no linked payment, so their cost was estimated from "
+"the market price of that day"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:68
+#, fuzzy
+#| msgid "Info"
+msgid "In"
+msgstr "Info"
+
+#: templates/investments/fragments/detail.html:74
+#: templates/investments/fragments/detail.html:78
+#, fuzzy
+#| msgid "Currency Code"
+msgid "currency effect"
+msgstr "Code de devise"
+
+#: templates/investments/fragments/detail.html:104
+#: templates/investments/fragments/detail.html:179
+#, fuzzy
+#| msgid "Entry Price"
+msgid "Price"
+msgstr "Prix d'entrée"
+
+#: templates/investments/fragments/detail.html:105
+#: templates/investments/fragments/detail.html:200
+#: templates/investments/fragments/detail.html:239
+#: templates/investments/fragments/detail.html:290
+#, fuzzy
+#| msgid "Warning"
+msgid "Earnings"
+msgstr "Avertissement"
+
+#: templates/investments/fragments/detail.html:180
+#: templates/investments/fragments/detail.html:227
+msgid "Average cost"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:181
+#: templates/investments/fragments/rebalance.html:41
+#, fuzzy
+#| msgid "By"
+msgid "Buy"
+msgstr "Par"
+
+#: templates/investments/fragments/detail.html:182
+#: templates/investments/fragments/rebalance.html:43
+msgid "Sell"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:227
+#: templates/investments/fragments/detail.html:257
+msgid "Put in / taken out"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:239
+#, fuzzy
+#| msgid "Uncategorized"
+msgid "Unrealized"
+msgstr "Sans catégorie"
+
+#: templates/investments/fragments/detail.html:248
+msgid "Realized"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:271
+#, fuzzy
+#| msgid "Management"
+msgid "Movements"
+msgstr "Gestion"
+
+#: templates/investments/fragments/detail.html:284
+msgid "Bought / contributed"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:284
+msgid "Sold / withdrew"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:288
+msgid "Money in"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:288
+msgid "Money out"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:290
+msgid "Gain"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:290
+msgid "Loss, tax or fee"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:315
+#, fuzzy
+#| msgid "No presets yet"
+msgid "No movements yet"
+msgstr "Pas encore de préréglages"
+
+#: templates/investments/fragments/detail.html:315
+msgid "Use the + button to buy or contribute"
+msgstr ""
+
+#: templates/investments/fragments/list.html:21
+msgid "Holdings"
+msgstr ""
+
+#: templates/investments/fragments/list.html:60
+msgid "Some costs were estimated from market prices"
+msgstr ""
+
+#: templates/investments/fragments/list.html:92
+msgid "Charts"
+msgstr ""
+
+#: templates/investments/fragments/list.html:97
+#, fuzzy
+#| msgid "New balance"
+msgid "Rebalance"
+msgstr "Nouveau solde"
+
+#: templates/investments/fragments/list.html:105
+#, fuzzy
+#| msgid "No presets yet"
+msgid "No investments yet"
+msgstr "Pas encore de préréglages"
+
+#: templates/investments/fragments/list.html:106
+msgid "Mark an account as an investment in its settings to see it here"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:4
+msgid "No allocation targets"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:5
+msgid "Set a target for your asset classes to see what to buy and sell"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:26
+msgid "To reach the target"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:34
+msgid "Off target"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:55
+msgid "Left out: investments without an asset class or a target"
+msgstr ""
+
#: templates/layouts/base.html:38
msgid "This is a demo!"
msgstr "C'est une démo !"
@@ -3691,11 +4170,6 @@ msgstr "Filtrer les transactions"
msgid "Order by"
msgstr "Trier par"
-#: templates/net_worth/net_worth.html:52
-#: templates/yearly_overview/pages/overview_by_currency.html:6
-msgid "By currency"
-msgstr "Par devises"
-
#: templates/net_worth/net_worth.html:135
msgid "Evolution"
msgstr "Évolution"
@@ -4153,6 +4627,12 @@ msgstr "Se connecter avec"
msgid "Yearly Overview"
msgstr "Aperçu annuel"
+#~ msgid ""
+#~ "Create one exchange rate and keep updating it. Avoids database clutter."
+#~ msgstr ""
+#~ "Ne créer qu'un seul taux de change et le mettre à jour. Evite d'engorger "
+#~ "la base de donnée."
+
#~ msgid "Add as quick transaction"
#~ msgstr "Ajouter aux transactions rapides"
diff --git a/app/locale/hu/LC_MESSAGES/django.po b/app/locale/hu/LC_MESSAGES/django.po
index 3666adba..15fd64cc 100644
--- a/app/locale/hu/LC_MESSAGES/django.po
+++ b/app/locale/hu/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-11-08 06:01+0000\n"
+"POT-Creation-Date: 2026-11-08 18:27+0000\n"
"PO-Revision-Date: 2026-01-10 03:09+0000\n"
"Last-Translator: Herculino Trotta \n"
"Language-Team: Hungarian \n"
"Language-Team: Italian \n"
"Language-Team: Dutch \n"
"Language-Team: Polish =20) ? 1 : 2;\n"
"X-Generator: Weblate 2026.6\n"
-#: apps/accounts/forms.py:28
+#: apps/accounts/forms.py:33
msgid "Group name"
msgstr "Nazwa grupy"
-#: apps/accounts/forms.py:43 apps/accounts/forms.py:109
-#: apps/currencies/forms.py:57 apps/currencies/forms.py:91
-#: apps/currencies/forms.py:166 apps/dca/forms.py:46 apps/dca/forms.py:205
-#: apps/import_app/forms.py:32 apps/rules/forms.py:60 apps/rules/forms.py:100
-#: apps/rules/forms.py:385 apps/transactions/forms.py:219
-#: apps/transactions/forms.py:452 apps/transactions/forms.py:571
-#: apps/transactions/forms.py:718 apps/transactions/forms.py:940
-#: apps/transactions/forms.py:987 apps/transactions/forms.py:1015
-#: apps/transactions/forms.py:1046 apps/transactions/forms.py:1208
-#: apps/users/forms.py:264 apps/users/forms.py:422
+#: apps/accounts/forms.py:48 apps/accounts/forms.py:76
+#: apps/accounts/forms.py:184 apps/currencies/forms.py:57
+#: apps/currencies/forms.py:91 apps/currencies/forms.py:168
+#: apps/dca/forms.py:46 apps/dca/forms.py:192 apps/import_app/forms.py:32
+#: apps/rules/forms.py:60 apps/rules/forms.py:100 apps/rules/forms.py:385
+#: apps/transactions/forms.py:219 apps/transactions/forms.py:452
+#: apps/transactions/forms.py:571 apps/transactions/forms.py:718
+#: apps/transactions/forms.py:940 apps/transactions/forms.py:987
+#: apps/transactions/forms.py:1015 apps/transactions/forms.py:1046
+#: apps/transactions/forms.py:1208 apps/users/forms.py:264
+#: apps/users/forms.py:422
#: templates/rules/fragments/transaction_rule/dry_run/updated.html:5
#: templates/rules/fragments/transaction_rule/view.html:128
msgid "Update"
msgstr "Aktualizuj"
-#: apps/accounts/forms.py:49 apps/accounts/forms.py:115
-#: apps/common/widgets/tom_select.py:13 apps/currencies/forms.py:63
-#: apps/currencies/forms.py:97 apps/currencies/forms.py:172
-#: apps/dca/forms.py:52 apps/dca/forms.py:211 apps/import_app/forms.py:38
-#: apps/rules/forms.py:66 apps/rules/forms.py:106 apps/rules/forms.py:391
-#: apps/transactions/forms.py:206 apps/transactions/forms.py:226
-#: apps/transactions/forms.py:459 apps/transactions/forms.py:946
-#: apps/transactions/forms.py:993 apps/transactions/forms.py:1021
-#: apps/transactions/forms.py:1052 apps/transactions/forms.py:1214
-#: apps/users/forms.py:270 apps/users/forms.py:428
-#: templates/mini_tools/simulator.html:177
+#: apps/accounts/forms.py:54 apps/accounts/forms.py:76
+#: apps/accounts/forms.py:190 apps/common/widgets/tom_select.py:15
+#: apps/currencies/forms.py:63 apps/currencies/forms.py:97
+#: apps/currencies/forms.py:174 apps/dca/forms.py:52 apps/dca/forms.py:198
+#: apps/import_app/forms.py:38 apps/rules/forms.py:66 apps/rules/forms.py:106
+#: apps/rules/forms.py:391 apps/transactions/forms.py:206
+#: apps/transactions/forms.py:226 apps/transactions/forms.py:459
+#: apps/transactions/forms.py:946 apps/transactions/forms.py:993
+#: apps/transactions/forms.py:1021 apps/transactions/forms.py:1052
+#: apps/transactions/forms.py:1214 apps/users/forms.py:270
+#: apps/users/forms.py:428 templates/mini_tools/simulator.html:177
#: templates/mini_tools/unit_price_calculator.html:168
msgid "Add"
msgstr "Dodaj"
-#: apps/accounts/forms.py:57 templates/accounts/fragments/list.html:23
+#: apps/accounts/forms.py:86 templates/accounts/fragments/list.html:23
msgid "Group"
msgstr "Grupa"
-#: apps/accounts/forms.py:123
+#: apps/accounts/forms.py:93 apps/accounts/models.py:73
+#: apps/accounts/models.py:130
+#: templates/investments/fragments/rebalance.html:22
+msgid "Asset class"
+msgstr ""
+
+#: apps/accounts/forms.py:198
msgid "New balance"
msgstr "Nowe saldo"
-#: apps/accounts/forms.py:129 apps/dca/forms.py:79 apps/dca/forms.py:86
+#: apps/accounts/forms.py:204 apps/dca/forms.py:79 apps/dca/forms.py:86
#: apps/insights/forms.py:117 apps/insights/views.py:44 apps/rules/forms.py:181
#: apps/rules/forms.py:197 apps/rules/models.py:44 apps/rules/models.py:311
#: apps/transactions/forms.py:62 apps/transactions/forms.py:330
#: apps/transactions/forms.py:510 apps/transactions/forms.py:607
#: apps/transactions/forms.py:614 apps/transactions/forms.py:823
#: apps/transactions/forms.py:1075 apps/transactions/models.py:357
-#: apps/transactions/models.py:743 apps/transactions/models.py:999
-#: apps/transactions/models.py:1306
+#: apps/transactions/models.py:767 apps/transactions/models.py:1023
+#: apps/transactions/models.py:1330
#: templates/insights/fragments/month_by_month.html:84
#: templates/insights/fragments/year_by_year.html:52
msgid "Category"
msgstr "Kategoria"
-#: apps/accounts/forms.py:136 apps/dca/forms.py:95 apps/dca/forms.py:103
-#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
+#: apps/accounts/forms.py:211 apps/dca/forms.py:95 apps/dca/forms.py:103
+#: apps/export_app/forms.py:43 apps/export_app/forms.py:134
#: apps/insights/views.py:50 apps/rules/forms.py:184 apps/rules/forms.py:194
#: apps/rules/models.py:45 apps/rules/models.py:315
#: apps/transactions/filters.py:79 apps/transactions/forms.py:70
#: apps/transactions/forms.py:338 apps/transactions/forms.py:518
#: apps/transactions/forms.py:623 apps/transactions/forms.py:631
#: apps/transactions/forms.py:816 apps/transactions/forms.py:1068
-#: apps/transactions/models.py:363 apps/transactions/models.py:745
-#: apps/transactions/models.py:1003 apps/transactions/models.py:1312
-#: templates/includes/sidebar.html:156
+#: apps/transactions/models.py:363 apps/transactions/models.py:769
+#: apps/transactions/models.py:1027 apps/transactions/models.py:1336
+#: templates/includes/sidebar.html:162
#: templates/insights/fragments/month_by_month.html:29
#: templates/insights/fragments/month_by_month.html:32
#: templates/insights/fragments/year_by_year.html:24
@@ -95,27 +102,27 @@ msgstr "Kategoria"
msgid "Tags"
msgstr "Tagi"
-#: apps/accounts/forms.py:179
+#: apps/accounts/forms.py:254
#, fuzzy
#| msgid "All Transactions"
msgid "All paid transactions"
msgstr "Wszystkie transakcje"
-#: apps/accounts/forms.py:180
+#: apps/accounts/forms.py:255
#, fuzzy
#| msgid "Update"
msgid "Up to date"
msgstr "Aktualizuj"
-#: apps/accounts/forms.py:181
+#: apps/accounts/forms.py:256
msgid "Up to reference month"
msgstr ""
-#: apps/accounts/forms.py:185
+#: apps/accounts/forms.py:260
msgid "Balance against"
msgstr ""
-#: apps/accounts/forms.py:189 apps/currencies/forms.py:70
+#: apps/accounts/forms.py:264 apps/currencies/forms.py:70
#: apps/dca/models.py:158 apps/rules/forms.py:176 apps/rules/forms.py:190
#: apps/rules/models.py:38 apps/rules/models.py:279
#: apps/transactions/forms.py:86 apps/transactions/forms.py:482
@@ -126,16 +133,17 @@ msgstr ""
msgid "Date"
msgstr "Data"
-#: apps/accounts/forms.py:194
+#: apps/accounts/forms.py:269
#, fuzzy
#| msgid "Reference Date"
msgid "Reference month"
msgstr "Data referencyjna"
-#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
+#: apps/accounts/models.py:19 apps/accounts/models.py:36
+#: apps/accounts/models.py:84 apps/dca/models.py:13
#: apps/import_app/models.py:20 apps/rules/models.py:13
#: apps/transactions/models.py:249 apps/transactions/models.py:274
-#: apps/transactions/models.py:298 apps/transactions/models.py:1274
+#: apps/transactions/models.py:298 apps/transactions/models.py:1298
#: apps/users/models.py:577 templates/account_groups/fragments/list.html:22
#: templates/accounts/fragments/list.html:22
#: templates/categories/fragments/table.html:17
@@ -144,6 +152,7 @@ msgstr "Data referencyjna"
#: templates/exchange_rates_services/fragments/list.html:28
#: templates/import_app/fragments/profiles/list.html:34
#: templates/installment_plans/fragments/table.html:16
+#: templates/investment_classes/fragments/list.html:27
#: templates/quick_transactions/fragments/list.html:13
#: templates/recurring_transactions/fragments/table.html:18
#: templates/rules/fragments/list.html:24
@@ -152,44 +161,129 @@ msgstr "Data referencyjna"
msgid "Name"
msgstr "Nazwa"
-#: apps/accounts/models.py:18 apps/accounts/models.py:33
+#: apps/accounts/models.py:25 apps/accounts/models.py:88
msgid "Account Group"
msgstr "Grupa konta"
-#: apps/accounts/models.py:19 templates/account_groups/fragments/list.html:9
+#: apps/accounts/models.py:26 templates/account_groups/fragments/list.html:9
#: templates/account_groups/pages/index.html:4
-#: templates/includes/sidebar.html:176
+#: templates/includes/sidebar.html:182
msgid "Account Groups"
msgstr "Grupy kont"
-#: apps/accounts/models.py:39 apps/currencies/models.py:44
+#: apps/accounts/models.py:43
+msgid "Target allocation (%)"
+msgstr ""
+
+#: apps/accounts/models.py:45
+msgid ""
+"Share of your investments you want in this class. Leave it empty to leave "
+"the class out of the targets."
+msgstr ""
+
+#: apps/accounts/models.py:61
+msgid "You already have an asset class with this name."
+msgstr ""
+
+#: apps/accounts/models.py:66
+#, python-format
+msgid ""
+"Targets can't add up to more than 100%%. Other classes already take "
+"%(taken)s%%."
+msgstr ""
+
+#: apps/accounts/models.py:74 apps/export_app/forms.py:131
+#: templates/includes/sidebar.html:190
+#: templates/investment_classes/fragments/list.html:9
+#: templates/investment_classes/pages/index.html:4
+#: templates/investments/fragments/rebalance.html:6
+#: templates/investments/fragments/rebalance.html:11
+msgid "Asset classes"
+msgstr ""
+
+#: apps/accounts/models.py:94 apps/currencies/models.py:44
#: templates/accounts/fragments/list.html:24
#: templates/mini_tools/simulator.html:37
#: templates/yearly_overview/pages/yearly_overview_base.html:19
msgid "Currency"
msgstr "Waluta"
-#: apps/accounts/models.py:45 apps/currencies/models.py:27
+#: apps/accounts/models.py:100 apps/currencies/models.py:27
#: templates/accounts/fragments/list.html:25
msgid "Exchange Currency"
msgstr "Wymiana waluty"
-#: apps/accounts/models.py:50 apps/currencies/models.py:32
+#: apps/accounts/models.py:105 apps/currencies/models.py:32
msgid "Default currency for exchange calculations"
msgstr "Domyślna waluta na potrzeby przeliczenia kursu wymiany"
-#: apps/accounts/models.py:55
+#: apps/accounts/models.py:110
msgid "Asset account"
msgstr "Rachunek aktywów"
-#: apps/accounts/models.py:57
+#: apps/accounts/models.py:112
msgid ""
"Asset accounts count towards your Net Worth, but not towards your month."
msgstr ""
"Rachunek aktywów wlicza się do dostępnej wartości netto, ale nie jest "
"uwzględniany w miesięcznych podsumowaniach."
-#: apps/accounts/models.py:62 apps/currencies/models.py:37
+#: apps/accounts/models.py:117 templates/investments/fragments/list.html:46
+#, fuzzy
+#| msgid "Invert"
+msgid "Investment"
+msgstr "Zamień"
+
+#: apps/accounts/models.py:119
+msgid ""
+"Show this account in the Investments tracker. Set an exchange currency to "
+"price it by units (stocks, ETFs, crypto); leave it empty to track it by "
+"value (fixed income, property, pensions)."
+msgstr ""
+
+#: apps/accounts/models.py:132
+msgid ""
+"Groups investments in the allocation charts, e.g. Fixed income, Stocks, Real "
+"estate."
+msgstr ""
+
+#: apps/accounts/models.py:140
+#, fuzzy
+#| msgid "Entities"
+msgid "Income entities"
+msgstr "Encje"
+
+#: apps/accounts/models.py:142
+msgid ""
+"Transactions with any of these entities count towards this investment's "
+"return: in this account, and income paid into other accounts (dividends, "
+"rent)."
+msgstr ""
+
+#: apps/accounts/models.py:151
+#, fuzzy
+#| msgid "No categories"
+msgid "Income categories"
+msgstr "Brak kategorii"
+
+#: apps/accounts/models.py:153
+msgid ""
+"Transactions in this account with any of these categories are its result "
+"(interest, taxes, fees). On accounts tracked by value, everything else that "
+"isn't a transfer counts as money coming in or going out."
+msgstr ""
+
+#: apps/accounts/models.py:162
+#, fuzzy
+#| msgid "Income"
+msgid "Income tags"
+msgstr "Przychody"
+
+#: apps/accounts/models.py:164
+msgid "Same as income categories, for transactions with any of these tags."
+msgstr ""
+
+#: apps/accounts/models.py:169 apps/currencies/models.py:37
#: templates/accounts/fragments/list.html:27
#: templates/categories/fragments/list.html:20
#: templates/currencies/fragments/list.html:24
@@ -198,19 +292,19 @@ msgstr ""
msgid "Archived"
msgstr "Zarchiwizowane"
-#: apps/accounts/models.py:63
+#: apps/accounts/models.py:170
msgid "Archived accounts don't show up nor count towards your net worth"
msgstr ""
"Zarchiwizowane konta nie są wyświetlane oraz nie są uwzględniane w "
"podsumowaniach"
-#: apps/accounts/models.py:75 apps/rules/forms.py:173 apps/rules/forms.py:187
+#: apps/accounts/models.py:182 apps/rules/forms.py:173 apps/rules/forms.py:187
#: apps/rules/models.py:35 apps/rules/models.py:267
#: apps/transactions/forms.py:82 apps/transactions/forms.py:350
#: apps/transactions/forms.py:477 apps/transactions/forms.py:808
#: apps/transactions/forms.py:1060 apps/transactions/models.py:329
-#: apps/transactions/models.py:703 apps/transactions/models.py:981
-#: apps/transactions/models.py:1280
+#: apps/transactions/models.py:727 apps/transactions/models.py:1005
+#: apps/transactions/models.py:1304
#: templates/installment_plans/fragments/table.html:17
#: templates/quick_transactions/fragments/list.html:14
#: templates/recurring_transactions/fragments/table.html:19
@@ -218,17 +312,17 @@ msgstr ""
msgid "Account"
msgstr "Konto"
-#: apps/accounts/models.py:76 apps/export_app/forms.py:19
-#: apps/export_app/forms.py:129 apps/transactions/filters.py:63
+#: apps/accounts/models.py:183 apps/export_app/forms.py:19
+#: apps/export_app/forms.py:130 apps/transactions/filters.py:63
#: templates/accounts/fragments/list.html:9
-#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:168
-#: templates/includes/sidebar.html:170
+#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:174
+#: templates/includes/sidebar.html:176
#: templates/monthly_overview/pages/overview.html:77
#: templates/transactions/pages/transactions.html:26
msgid "Accounts"
msgstr "Konta"
-#: apps/accounts/models.py:93
+#: apps/accounts/models.py:200
msgid "Exchange currency cannot be the same as the account's main currency."
msgstr "Waluta do wymiany nie może być taka sama jak waluta konta."
@@ -246,7 +340,7 @@ msgstr "Usunięto grupę kont"
#: apps/accounts/views/account_groups.py:109
#: apps/accounts/views/accounts.py:137 apps/dca/views.py:106
-#: apps/import_app/views.py:147 apps/rules/views.py:191
+#: apps/import_app/views.py:148 apps/rules/views.py:191
#: apps/transactions/views/categories.py:163
#: apps/transactions/views/entities.py:135 apps/transactions/views/tags.py:131
msgid "Item no longer shared with you"
@@ -254,7 +348,7 @@ msgstr "Ten wpis nie jest już dostępny dla Ciebie"
#: apps/accounts/views/account_groups.py:132
#: apps/accounts/views/accounts.py:182 apps/dca/views.py:131
-#: apps/import_app/views.py:273 apps/rules/views.py:217
+#: apps/import_app/views.py:274 apps/rules/views.py:217
#: apps/transactions/views/categories.py:188
#: apps/transactions/views/entities.py:160 apps/transactions/views/tags.py:154
msgid "Ownership taken successfully"
@@ -262,7 +356,7 @@ msgstr "Własność przejęta pomyślnie"
#: apps/accounts/views/account_groups.py:152
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:151
-#: apps/import_app/views.py:294 apps/rules/views.py:238
+#: apps/import_app/views.py:295 apps/rules/views.py:238
#: apps/transactions/views/categories.py:130
#: apps/transactions/views/entities.py:180 apps/transactions/views/tags.py:174
msgid "Configuration saved successfully"
@@ -320,7 +414,7 @@ msgstr "Encja o zadanym ID nie istnieje."
msgid "Invalid entity data. Provide an ID or name."
msgstr "Niepoprawne dane encji. Wprowadź ID lub nazwę encji."
-#: apps/api/serializers/transactions.py:141 apps/transactions/models.py:953
+#: apps/api/serializers/transactions.py:141 apps/transactions/models.py:977
msgid "This field is required."
msgstr ""
@@ -342,13 +436,14 @@ msgstr "Uczyń prywatne"
msgid "Shared with users"
msgstr "Udostępnione użytkownikom"
-#: apps/common/fields/forms/dynamic_select.py:71
-#: apps/common/fields/forms/dynamic_select.py:142
+#: apps/common/fields/forms/dynamic_select.py:79
+#: apps/common/fields/forms/dynamic_select.py:150
msgid "Error creating new instance"
msgstr "Błąd podczas tworzenia wpisu"
#: apps/common/fields/forms/grouped_select.py:24
-#: apps/common/widgets/tom_select.py:92 apps/common/widgets/tom_select.py:95
+#: apps/common/widgets/tom_select.py:94 apps/common/widgets/tom_select.py:97
+#: templates/investments/fragments/list.html:26
msgid "Ungrouped"
msgstr "Niezgrupowane"
@@ -504,14 +599,15 @@ msgid "Today"
msgstr "Dzisiaj"
#: apps/common/widgets/datepicker.py:142
+#: templates/investments/fragments/rebalance.html:24
msgid "Now"
msgstr "Teraz"
-#: apps/common/widgets/tom_select.py:11
+#: apps/common/widgets/tom_select.py:13
msgid "Remove"
msgstr "Usuń"
-#: apps/common/widgets/tom_select.py:15 templates/mini_tools/simulator.html:187
+#: apps/common/widgets/tom_select.py:17 templates/mini_tools/simulator.html:187
#: templates/mini_tools/unit_price_calculator.html:180
#: templates/monthly_overview/pages/overview.html:286
#: templates/transactions/fragments/filter_actions.html:48
@@ -519,7 +615,7 @@ msgstr "Usuń"
msgid "Clear"
msgstr "Wyczyść"
-#: apps/common/widgets/tom_select.py:16
+#: apps/common/widgets/tom_select.py:18
msgid "No results..."
msgstr "Brak wyników..."
@@ -553,10 +649,10 @@ msgid "Decimal Places"
msgstr "Liczba miejsc po przecinku"
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
-#: apps/export_app/forms.py:130 apps/transactions/filters.py:70
+#: apps/export_app/forms.py:132 apps/transactions/filters.py:70
#: templates/currencies/fragments/list.html:9
-#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:182
-#: templates/includes/sidebar.html:184
+#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:196
+#: templates/includes/sidebar.html:198
#: templates/monthly_overview/pages/overview.html:63
#: templates/transactions/pages/transactions.html:12
msgid "Currencies"
@@ -588,9 +684,9 @@ msgid "Auto"
msgstr "Automatycznie"
#: apps/currencies/models.py:82 apps/export_app/forms.py:67
-#: apps/export_app/forms.py:142 templates/exchange_rates/fragments/list.html:10
+#: apps/export_app/forms.py:144 templates/exchange_rates/fragments/list.html:10
#: templates/exchange_rates/pages/index.html:4
-#: templates/includes/sidebar.html:190
+#: templates/includes/sidebar.html:204
msgid "Exchange Rates"
msgstr "Kursy wymiany"
@@ -685,30 +781,45 @@ msgstr ""
"z walut na podstawie jej docelowej waluty wymiany."
#: apps/currencies/models.py:171
-msgid "Single exchange rate"
-msgstr ""
+#, fuzzy
+#| msgid "No exchange rates"
+msgid "One exchange rate per day"
+msgstr "Brak kursów wymiany walut"
#: apps/currencies/models.py:174
-msgid "Create one exchange rate and keep updating it. Avoids database clutter."
+msgid ""
+"Update the day's exchange rate on each fetch instead of adding a new one."
msgstr ""
-#: apps/currencies/models.py:179
+#: apps/currencies/models.py:178
+#, fuzzy
+#| msgid "Exchange Rates"
+msgid "Exchange rates to keep"
+msgstr "Kursy wymiany"
+
+#: apps/currencies/models.py:183
+msgid ""
+"Keep at most this many automatic exchange rates per currency pair, deleting "
+"the oldest first. Leave empty to keep all."
+msgstr ""
+
+#: apps/currencies/models.py:188
msgid "Exchange Rate Service"
msgstr ""
-#: apps/currencies/models.py:180
+#: apps/currencies/models.py:189
msgid "Exchange Rate Services"
msgstr ""
-#: apps/currencies/models.py:234
+#: apps/currencies/models.py:243
msgid "'Every X hours' interval type requires a positive integer."
msgstr "Cykliczność \"co X godzin\" wymaga dodatniej liczby."
-#: apps/currencies/models.py:243
+#: apps/currencies/models.py:252
msgid "'Every X hours' interval must be between 1 and 24."
msgstr "Cykliczność \"co X godzin\" musi być z przedziału 1-24."
-#: apps/currencies/models.py:257
+#: apps/currencies/models.py:266
msgid ""
"Invalid hour format. Use comma-separated hours (0-23) and/or ranges (e.g., "
"'1-5,8,10-12')."
@@ -716,7 +827,7 @@ msgstr ""
"Niepoprawny format godziny. Użyj zakresów (0-23) i/lub oddziel godziny "
"przecinkami (np. \"1-5,8,10-12\")."
-#: apps/currencies/models.py:268
+#: apps/currencies/models.py:277
msgid ""
"Invalid format. Please check the requirements for your selected interval "
"type."
@@ -786,21 +897,21 @@ msgstr ""
msgid "Income Transaction"
msgstr ""
-#: apps/dca/forms.py:192
+#: apps/dca/forms.py:179
msgid "Link transaction"
msgstr ""
-#: apps/dca/forms.py:275 apps/dca/forms.py:276 apps/dca/forms.py:281
-#: apps/dca/forms.py:285
+#: apps/dca/forms.py:262 apps/dca/forms.py:263 apps/dca/forms.py:268
+#: apps/dca/forms.py:272
msgid "You must provide an account."
msgstr "Należy wybrać konto."
-#: apps/dca/forms.py:290 apps/transactions/forms.py:752
-#: apps/transactions/models.py:956
+#: apps/dca/forms.py:277 apps/transactions/forms.py:752
+#: apps/transactions/models.py:980
msgid "From and To accounts must be different."
msgstr "Konta \"Z\" oraz \"Do\" muszą się różnić."
-#: apps/dca/forms.py:304
+#: apps/dca/forms.py:299
#, python-format
msgid "DCA for %(strategy_name)s"
msgstr ""
@@ -816,8 +927,8 @@ msgstr "Waluta dla płatności"
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
#: apps/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
#: apps/transactions/forms.py:504 apps/transactions/forms.py:651
-#: apps/transactions/models.py:353 apps/transactions/models.py:752
-#: apps/transactions/models.py:1009 apps/transactions/models.py:1302
+#: apps/transactions/models.py:353 apps/transactions/models.py:776
+#: apps/transactions/models.py:1033 apps/transactions/models.py:1326
msgid "Notes"
msgstr "Notatki"
@@ -825,7 +936,7 @@ msgstr "Notatki"
msgid "DCA Strategy"
msgstr ""
-#: apps/dca/models.py:35 apps/export_app/forms.py:150
+#: apps/dca/models.py:35 apps/export_app/forms.py:152
msgid "DCA Strategies"
msgstr ""
@@ -845,7 +956,7 @@ msgstr ""
msgid "DCA Entry"
msgstr ""
-#: apps/dca/models.py:187 apps/export_app/forms.py:151
+#: apps/dca/models.py:187 apps/export_app/forms.py:153
msgid "DCA Entries"
msgstr ""
@@ -873,25 +984,25 @@ msgstr "Pomyślnie zaktualizowano wpis"
msgid "Entry deleted successfully"
msgstr "Pomyślnie usunięto wpis"
-#: apps/export_app/forms.py:13 apps/export_app/forms.py:128
-#: templates/includes/sidebar.html:227 templates/users/fragments/list.html:10
+#: apps/export_app/forms.py:13 apps/export_app/forms.py:129
+#: templates/includes/sidebar.html:241 templates/users/fragments/list.html:10
#: templates/users/pages/index.html:4
msgid "Users"
msgstr "Użytkownicy"
-#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
+#: apps/export_app/forms.py:31 apps/export_app/forms.py:136
#: apps/transactions/models.py:422 templates/includes/sidebar.html:81
-#: templates/includes/sidebar.html:148
+#: templates/includes/sidebar.html:154
#: templates/recurring_transactions/fragments/list_transactions.html:5
#: templates/recurring_transactions/fragments/table.html:37
#: templates/transactions/pages/transactions.html:5
msgid "Transactions"
msgstr "Transakcje"
-#: apps/export_app/forms.py:37 apps/export_app/forms.py:131
+#: apps/export_app/forms.py:37 apps/export_app/forms.py:133
#: apps/insights/views.py:43 apps/transactions/filters.py:74
#: templates/categories/fragments/list.html:9
-#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:150
+#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:156
#: templates/insights/fragments/month_by_month.html:18
#: templates/insights/fragments/month_by_month.html:21
#: templates/insights/fragments/year_by_year.html:13
@@ -899,16 +1010,16 @@ msgstr "Transakcje"
msgid "Categories"
msgstr "Kategorie"
-#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
+#: apps/export_app/forms.py:49 apps/export_app/forms.py:135
#: apps/insights/views.py:57 apps/rules/forms.py:185 apps/rules/forms.py:195
#: apps/rules/models.py:46 apps/rules/models.py:307
#: apps/transactions/filters.py:84 apps/transactions/forms.py:78
#: apps/transactions/forms.py:346 apps/transactions/forms.py:526
#: apps/transactions/forms.py:831 apps/transactions/forms.py:1083
#: apps/transactions/models.py:312 apps/transactions/models.py:368
-#: apps/transactions/models.py:748 apps/transactions/models.py:1006
-#: apps/transactions/models.py:1317 templates/entities/fragments/list.html:9
-#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:162
+#: apps/transactions/models.py:772 apps/transactions/models.py:1030
+#: apps/transactions/models.py:1341 templates/entities/fragments/list.html:9
+#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:168
#: templates/insights/fragments/month_by_month.html:40
#: templates/insights/fragments/month_by_month.html:43
#: templates/insights/fragments/year_by_year.html:35
@@ -916,28 +1027,28 @@ msgstr "Kategorie"
msgid "Entities"
msgstr "Encje"
-#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
-#: apps/transactions/models.py:1046 templates/includes/sidebar.html:110
+#: apps/export_app/forms.py:55 apps/export_app/forms.py:139
+#: apps/transactions/models.py:1070 templates/includes/sidebar.html:110
#: templates/recurring_transactions/fragments/list.html:9
#: templates/recurring_transactions/pages/index.html:4
msgid "Recurring Transactions"
msgstr "Zlecenia stałe"
-#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
-#: apps/transactions/models.py:766 templates/includes/sidebar.html:104
+#: apps/export_app/forms.py:61 apps/export_app/forms.py:137
+#: apps/transactions/models.py:790 templates/includes/sidebar.html:104
#: templates/installment_plans/fragments/list.html:9
#: templates/installment_plans/pages/index.html:4
msgid "Installment Plans"
msgstr "Plany ratalne"
-#: apps/export_app/forms.py:73 apps/export_app/forms.py:140
+#: apps/export_app/forms.py:73 apps/export_app/forms.py:142
#: templates/exchange_rates_services/fragments/list.html:10
#: templates/exchange_rates_services/pages/index.html:4
-#: templates/includes/sidebar.html:218
+#: templates/includes/sidebar.html:232
msgid "Automatic Exchange Rates"
msgstr "Automatyczne kursy walut"
-#: apps/export_app/forms.py:79 templates/includes/sidebar.html:198
+#: apps/export_app/forms.py:79 templates/includes/sidebar.html:212
#: templates/rules/fragments/list.html:9 templates/rules/pages/index.html:4
msgid "Rules"
msgstr "Reguły"
@@ -946,57 +1057,57 @@ msgstr "Reguły"
msgid "DCA"
msgstr "DCA"
-#: apps/export_app/forms.py:91 apps/export_app/forms.py:152
+#: apps/export_app/forms.py:91 apps/export_app/forms.py:154
#: templates/import_app/fragments/profiles/list.html:21
#: templates/import_app/pages/profiles_index.html:4
msgid "Import Profiles"
msgstr "Profile importu"
-#: apps/export_app/forms.py:117 templates/export_app/fragments/export.html:5
+#: apps/export_app/forms.py:118 templates/export_app/fragments/export.html:5
#: templates/export_app/pages/index.html:13
msgid "Export"
msgstr "Eksport"
-#: apps/export_app/forms.py:125
+#: apps/export_app/forms.py:126
msgid "Import a ZIP file exported from WYGIWYH"
msgstr "Zaimportuj plik ZIP wyeksportowany z instancji WYGIWYH"
-#: apps/export_app/forms.py:126
+#: apps/export_app/forms.py:127
msgid "ZIP File"
msgstr "Plik ZIP"
-#: apps/export_app/forms.py:143 apps/rules/models.py:27
+#: apps/export_app/forms.py:145 apps/rules/models.py:27
msgid "Transaction rules"
msgstr "Reguły transakcji"
-#: apps/export_app/forms.py:145 apps/rules/models.py:68
+#: apps/export_app/forms.py:147 apps/rules/models.py:68
msgid "Edit transaction action"
msgstr "Edycja akcji transakcyjnych"
-#: apps/export_app/forms.py:148 apps/rules/models.py:327
+#: apps/export_app/forms.py:150 apps/rules/models.py:327
msgid "Update or create transaction actions"
msgstr "Stwórz lub uaktualnij akcje transakcyjne"
-#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:341
+#: apps/export_app/forms.py:183 templates/cotton/transaction/item.html:341
#: templates/cotton/ui/deleted_transactions_action_bar.html:53
#: templates/export_app/fragments/restore.html:5
#: templates/export_app/pages/index.html:19
msgid "Restore"
msgstr "Importuj"
-#: apps/export_app/forms.py:191
+#: apps/export_app/forms.py:193
msgid "Please upload either a ZIP file or at least one CSV file"
msgstr "Wrzuć plik ZIP lub przynajmniej jeden plik CSV"
-#: apps/export_app/views.py:177
+#: apps/export_app/views.py:183
msgid "You have to select at least one export"
msgstr "Musisz wybrać co najmniej jedną opcję do eksportu"
-#: apps/export_app/views.py:197
+#: apps/export_app/views.py:203
msgid "Data restored successfully"
msgstr "Pomyślnie odtworzono dane"
-#: apps/export_app/views.py:209
+#: apps/export_app/views.py:215
msgid ""
"There was an error restoring your data. Check the logs for more details."
msgstr ""
@@ -1009,7 +1120,7 @@ msgstr "Wybierz plik"
#: apps/import_app/forms.py:55
#: templates/import_app/fragments/profiles/list.html:75
-#: templates/includes/sidebar.html:204
+#: templates/includes/sidebar.html:218
msgid "Import"
msgstr "Import"
@@ -1057,23 +1168,23 @@ msgstr "Status"
msgid "File name"
msgstr "Nazwa pliku"
-#: apps/import_app/views.py:75
+#: apps/import_app/views.py:76
msgid "Import Profile added successfully"
msgstr "Profil importu dodany pomyślnie"
-#: apps/import_app/views.py:113
+#: apps/import_app/views.py:114
msgid "Import Profile update successfully"
msgstr "Profil importu zaktualizowany pomyślnie"
-#: apps/import_app/views.py:142
+#: apps/import_app/views.py:143
msgid "Import Profile deleted successfully"
msgstr "Usunięto profil importu"
-#: apps/import_app/views.py:222
+#: apps/import_app/views.py:223
msgid "Import Run queued successfully"
msgstr "Import dodany do kolejki"
-#: apps/import_app/views.py:249
+#: apps/import_app/views.py:250
msgid "Run deleted successfully"
msgstr "Usunięto import"
@@ -1142,7 +1253,7 @@ msgstr "Tag"
msgid "Untagged"
msgstr "Nieotagowane"
-#: apps/insights/views.py:53 apps/transactions/models.py:589
+#: apps/insights/views.py:53 apps/transactions/models.py:613
#: templates/tags/fragments/table.html:69
msgid "No tags"
msgstr "Brak tagów"
@@ -1163,6 +1274,32 @@ msgstr "Brak encji"
msgid "No entities"
msgstr ""
+#: apps/investments/views.py:104
+msgid "Other"
+msgstr ""
+
+#: apps/investments/views.py:135
+msgid "Unclassified"
+msgstr ""
+
+#: apps/investments/views.py:267
+#, fuzzy
+#| msgid "Installment Plan added successfully"
+msgid "Asset class added successfully"
+msgstr "Dodano plan ratalny"
+
+#: apps/investments/views.py:275
+#, fuzzy
+#| msgid "Tag updated successfully"
+msgid "Asset class updated successfully"
+msgstr "Zaktualizowano tag"
+
+#: apps/investments/views.py:296
+#, fuzzy
+#| msgid "Tag deleted successfully"
+msgid "Asset class deleted successfully"
+msgstr "Usunięto tag"
+
#: apps/net_worth/views.py:137 templates/net_worth/net_worth.html:84
#: templates/net_worth/net_worth.html:85
msgid "Consolidated"
@@ -1216,8 +1353,8 @@ msgstr "Operator"
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
#: apps/rules/models.py:271 apps/transactions/forms.py:468
-#: apps/transactions/models.py:336 apps/transactions/models.py:708
-#: apps/transactions/models.py:987 apps/transactions/models.py:1287
+#: apps/transactions/models.py:336 apps/transactions/models.py:732
+#: apps/transactions/models.py:1011 apps/transactions/models.py:1311
#: templates/mini_tools/simulator.html:63
msgid "Type"
msgstr "Typ"
@@ -1225,7 +1362,7 @@ msgstr "Typ"
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
#: apps/rules/models.py:275 apps/transactions/filters.py:22
#: apps/transactions/forms.py:472 apps/transactions/models.py:338
-#: apps/transactions/models.py:1289 templates/cotton/transaction/item.html:23
+#: apps/transactions/models.py:1313 templates/cotton/transaction/item.html:23
#: templates/cotton/transaction/item.html:34
#: templates/transactions/widgets/paid_toggle_button.html:10
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
@@ -1236,14 +1373,14 @@ msgstr "Zapłacono"
#: apps/rules/models.py:283 apps/transactions/forms.py:90
#: apps/transactions/forms.py:488 apps/transactions/forms.py:638
#: apps/transactions/forms.py:837 apps/transactions/models.py:340
-#: apps/transactions/models.py:726 apps/transactions/models.py:1011
+#: apps/transactions/models.py:750 apps/transactions/models.py:1035
msgid "Reference Date"
msgstr "Data referencyjna"
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
#: apps/rules/models.py:287 apps/transactions/forms.py:495
-#: apps/transactions/models.py:346 apps/transactions/models.py:992
-#: apps/transactions/models.py:1295
+#: apps/transactions/models.py:346 apps/transactions/models.py:1016
+#: apps/transactions/models.py:1319
#: templates/insights/fragments/sankey.html:102
#: templates/installment_plans/fragments/table.html:18
#: templates/mini_tools/simulator.html:70
@@ -1255,27 +1392,27 @@ msgstr "Kwota"
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
#: apps/rules/models.py:42 apps/rules/models.py:291
#: apps/transactions/forms.py:499 apps/transactions/forms.py:642
-#: apps/transactions/models.py:351 apps/transactions/models.py:710
-#: apps/transactions/models.py:995 apps/transactions/models.py:1300
+#: apps/transactions/models.py:351 apps/transactions/models.py:734
+#: apps/transactions/models.py:1019 apps/transactions/models.py:1324
msgid "Description"
msgstr "Opis"
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
#: apps/rules/models.py:299 apps/transactions/models.py:398
-#: apps/transactions/models.py:1322
+#: apps/transactions/models.py:1346
msgid "Internal Note"
msgstr "Notatka"
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
#: apps/rules/models.py:303 apps/transactions/models.py:400
-#: apps/transactions/models.py:1324
+#: apps/transactions/models.py:1348
msgid "Internal ID"
msgstr "Wewnętrzne ID"
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
#: apps/rules/models.py:319 apps/transactions/forms.py:655
#: apps/transactions/models.py:250 apps/transactions/models.py:341
-#: apps/transactions/models.py:1290
+#: apps/transactions/models.py:1314
msgid "Mute"
msgstr "Wycisz"
@@ -1288,7 +1425,7 @@ msgid "Set Values"
msgstr "Ustaw wartości"
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
-#: apps/transactions/models.py:421 apps/transactions/models.py:645
+#: apps/transactions/models.py:421 apps/transactions/models.py:669
msgid "Transaction"
msgstr "Transakcja"
@@ -1324,7 +1461,12 @@ msgstr "Reguła"
msgid "Field"
msgstr "Pole"
-#: apps/rules/models.py:61
+#: apps/rules/models.py:61 templates/investments/fragments/charts.html:86
+#: templates/investments/fragments/detail.html:103
+#: templates/investments/fragments/detail.html:162
+#: templates/investments/fragments/list.html:48
+#: templates/investments/fragments/list.html:49
+#: templates/investments/fragments/rebalance.html:23
msgid "Value"
msgstr "Wartość"
@@ -1510,7 +1652,7 @@ msgid "Upload"
msgstr "Odśwież"
#: apps/transactions/forms.py:355 apps/transactions/forms.py:1089
-#: apps/transactions/models.py:925
+#: apps/transactions/models.py:949
#, fuzzy
#| msgid "To Account"
msgid "To account"
@@ -1528,8 +1670,8 @@ msgstr "Od kwoty"
msgid "To Amount"
msgstr "Do kwoty"
-#: apps/transactions/forms.py:718 apps/transactions/models.py:970
-#: apps/transactions/models.py:1267 templates/cotton/transaction/item.html:52
+#: apps/transactions/forms.py:718 apps/transactions/models.py:994
+#: apps/transactions/models.py:1291 templates/cotton/transaction/item.html:52
#: templates/cotton/transaction/item.html:60
#: templates/cotton/ui/quick_transactions_buttons.html:40
#: templates/cotton/ui/transactions_fab.html:44
@@ -1595,8 +1737,8 @@ msgstr ""
"Zdezaktywowanych encji nie da się wykorzystać przy tworzeniu nowych "
"transakcji"
-#: apps/transactions/models.py:323 apps/transactions/models.py:969
-#: apps/transactions/models.py:1266
+#: apps/transactions/models.py:323 apps/transactions/models.py:993
+#: apps/transactions/models.py:1290
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: templates/calendar_view/fragments/list.html:52
@@ -1604,13 +1746,19 @@ msgstr ""
#: templates/cotton/ui/quick_transactions_buttons.html:10
#: templates/cotton/ui/transactions_fab.html:10
#: templates/insights/fragments/overview/_results.html:12
+#: templates/investments/fragments/_totals.html:21
+#: templates/investments/fragments/_totals.html:42
+#: templates/investments/fragments/detail.html:106
+#: templates/investments/fragments/detail.html:210
+#: templates/investments/fragments/detail.html:252
+#: templates/investments/fragments/detail.html:320
#: templates/mini_tools/simulator.html:66
#: templates/monthly_overview/fragments/monthly_summary.html:42
msgid "Income"
msgstr "Przychody"
-#: apps/transactions/models.py:324 apps/transactions/models.py:971
-#: apps/transactions/models.py:1268
+#: apps/transactions/models.py:324 apps/transactions/models.py:995
+#: apps/transactions/models.py:1292
#: templates/calendar_view/fragments/list.html:46
#: templates/calendar_view/fragments/list.html:48
#: templates/calendar_view/fragments/list.html:56
@@ -1622,14 +1770,14 @@ msgstr "Przychody"
msgid "Expense"
msgstr "Wydatek"
-#: apps/transactions/models.py:379 apps/transactions/models.py:765
+#: apps/transactions/models.py:379 apps/transactions/models.py:789
#: templates/cotton/transaction/item.html:291
#: templates/cotton/transaction/item.html:308
#: templates/cotton/transaction/item.html:318
msgid "Installment Plan"
msgstr ""
-#: apps/transactions/models.py:388 apps/transactions/models.py:1045
+#: apps/transactions/models.py:388 apps/transactions/models.py:1069
#: templates/cotton/transaction/item.html:281
#: templates/cotton/transaction/item.html:301
msgid "Recurring Transaction"
@@ -1649,156 +1797,168 @@ msgstr "Usunięte"
msgid "Deleted At"
msgstr ""
-#: apps/transactions/models.py:591
+#: apps/transactions/models.py:517
+msgid "A transfer needs one expense and one income."
+msgstr ""
+
+#: apps/transactions/models.py:519
+msgid "A transfer needs two different accounts."
+msgstr ""
+
+#: apps/transactions/models.py:523
+msgid "This transaction is already part of another transfer."
+msgstr ""
+
+#: apps/transactions/models.py:615
msgid "No category"
msgstr "Brak kategorii"
-#: apps/transactions/models.py:593
+#: apps/transactions/models.py:617
msgid "No description"
msgstr "Brak opisu"
-#: apps/transactions/models.py:650
+#: apps/transactions/models.py:674
#, fuzzy
#| msgid "ZIP File"
msgid "File"
msgstr "Plik ZIP"
-#: apps/transactions/models.py:652
+#: apps/transactions/models.py:676
msgid "Original Name"
msgstr ""
-#: apps/transactions/models.py:654
+#: apps/transactions/models.py:678
#, fuzzy
#| msgid "Content"
msgid "Content Type"
msgstr "Zawartość"
-#: apps/transactions/models.py:656
+#: apps/transactions/models.py:680
msgid "Size"
msgstr ""
-#: apps/transactions/models.py:661
+#: apps/transactions/models.py:685
msgid "Uploaded By"
msgstr ""
-#: apps/transactions/models.py:666
+#: apps/transactions/models.py:690
msgid "Transaction Attachment"
msgstr "Załącznik transakcji"
-#: apps/transactions/models.py:667
+#: apps/transactions/models.py:691
msgid "Transaction Attachments"
msgstr "Załączniki transakcji"
-#: apps/transactions/models.py:697 templates/includes/sidebar.html:57
+#: apps/transactions/models.py:721 templates/includes/sidebar.html:57
msgid "Yearly"
msgstr "Rocznie"
-#: apps/transactions/models.py:698 apps/users/models.py:469
+#: apps/transactions/models.py:722 apps/users/models.py:469
#: templates/includes/sidebar.html:51
msgid "Monthly"
msgstr "Miesięcznie"
-#: apps/transactions/models.py:699
+#: apps/transactions/models.py:723
msgid "Weekly"
msgstr "Tygodniowo"
-#: apps/transactions/models.py:700
+#: apps/transactions/models.py:724
msgid "Daily"
msgstr "Dziennie"
-#: apps/transactions/models.py:713
+#: apps/transactions/models.py:737
msgid "Number of Installments"
msgstr ""
-#: apps/transactions/models.py:718
+#: apps/transactions/models.py:742
msgid "Installment Start"
msgstr ""
-#: apps/transactions/models.py:719
+#: apps/transactions/models.py:743
msgid "The installment number to start counting from"
msgstr ""
-#: apps/transactions/models.py:724 apps/transactions/models.py:1015
+#: apps/transactions/models.py:748 apps/transactions/models.py:1039
msgid "Start Date"
msgstr "Data startowa"
-#: apps/transactions/models.py:728 apps/transactions/models.py:1016
+#: apps/transactions/models.py:752 apps/transactions/models.py:1040
msgid "End Date"
msgstr "Data końcowa"
-#: apps/transactions/models.py:733
+#: apps/transactions/models.py:757
msgid "Recurrence"
msgstr "Zlecenie stałe"
-#: apps/transactions/models.py:736
+#: apps/transactions/models.py:760
msgid "Installment Amount"
msgstr ""
-#: apps/transactions/models.py:755 apps/transactions/models.py:1035
+#: apps/transactions/models.py:779 apps/transactions/models.py:1059
msgid "Add description to transactions"
msgstr "Dodaj opis dla transakcji"
-#: apps/transactions/models.py:758 apps/transactions/models.py:1038
+#: apps/transactions/models.py:782 apps/transactions/models.py:1062
msgid "Add notes to transactions"
msgstr "Dodaj notatki do transakcji"
-#: apps/transactions/models.py:932
+#: apps/transactions/models.py:956
#, fuzzy
#| msgid "To Amount"
msgid "To amount"
msgstr "Do kwoty"
-#: apps/transactions/models.py:933
+#: apps/transactions/models.py:957
msgid "Leave empty to use the same amount"
msgstr ""
-#: apps/transactions/models.py:974 templates/mini_tools/simulator.html:129
+#: apps/transactions/models.py:998 templates/mini_tools/simulator.html:129
msgid "day(s)"
msgstr "dni"
-#: apps/transactions/models.py:975 templates/mini_tools/simulator.html:130
+#: apps/transactions/models.py:999 templates/mini_tools/simulator.html:130
msgid "week(s)"
msgstr "tygodni"
-#: apps/transactions/models.py:976 templates/mini_tools/simulator.html:131
+#: apps/transactions/models.py:1000 templates/mini_tools/simulator.html:131
msgid "month(s)"
msgstr "miesięcy"
-#: apps/transactions/models.py:977 templates/mini_tools/simulator.html:132
+#: apps/transactions/models.py:1001 templates/mini_tools/simulator.html:132
msgid "year(s)"
msgstr "lat"
-#: apps/transactions/models.py:979
+#: apps/transactions/models.py:1003
#: templates/recurring_transactions/fragments/list.html:18
msgid "Paused"
msgstr "Zapauzowane"
-#: apps/transactions/models.py:1018
+#: apps/transactions/models.py:1042
msgid "Recurrence Type"
msgstr "Typ powtarzania"
-#: apps/transactions/models.py:1021
+#: apps/transactions/models.py:1045
msgid "Recurrence Interval"
msgstr "Co ile powtarzać"
-#: apps/transactions/models.py:1024
+#: apps/transactions/models.py:1048
msgid "Keep at most"
msgstr "Zachowuj co najwyżej"
-#: apps/transactions/models.py:1028
+#: apps/transactions/models.py:1052
msgid "Last Generated Date"
msgstr ""
-#: apps/transactions/models.py:1031
+#: apps/transactions/models.py:1055
msgid "Last Generated Reference Date"
msgstr ""
-#: apps/transactions/models.py:1334 templates/cotton/transaction/item.html:274
+#: apps/transactions/models.py:1358 templates/cotton/transaction/item.html:274
#: templates/cotton/ui/transactions_fab.html:59
msgid "Quick Transaction"
msgstr "Szablon transakcji"
-#: apps/transactions/models.py:1335 templates/includes/sidebar.html:98
+#: apps/transactions/models.py:1359 templates/includes/sidebar.html:98
#: templates/quick_transactions/pages/index.html:5
#: templates/quick_transactions/pages/index.html:15
msgid "Quick Transactions"
@@ -1932,8 +2092,8 @@ msgid "Item deleted successfully"
msgstr "Pomyślnie usunięto"
#: apps/transactions/views/quick_transactions.py:164
-#: apps/transactions/views/transactions.py:143
-#: apps/transactions/views/transactions.py:328
+#: apps/transactions/views/transactions.py:156
+#: apps/transactions/views/transactions.py:343
msgid "Transaction added successfully"
msgstr "Dodano transakcję"
@@ -1969,19 +2129,19 @@ msgstr "Zaktualizowano tag"
msgid "Tag deleted successfully"
msgstr "Usunięto tag"
-#: apps/transactions/views/transactions.py:61
+#: apps/transactions/views/transactions.py:65
msgid "Attachment uploaded successfully"
msgstr "Pomyślnie dodano załącznik"
-#: apps/transactions/views/transactions.py:112
+#: apps/transactions/views/transactions.py:116
msgid "Attachment deleted successfully"
msgstr "Usunięto załącznik"
-#: apps/transactions/views/transactions.py:355
+#: apps/transactions/views/transactions.py:370
msgid "Transaction updated successfully"
msgstr "Zaktualizowano transakcję"
-#: apps/transactions/views/transactions.py:406
+#: apps/transactions/views/transactions.py:421
#, python-format
msgid "%(count)s transaction updated successfully"
msgid_plural "%(count)s transactions updated successfully"
@@ -1989,23 +2149,23 @@ msgstr[0] "Zaktualizowano %(count)s transakcję"
msgstr[1] "Zaktualizowano %(count)s transakcje"
msgstr[2] "Zaktualizowano %(count)s transakcji"
-#: apps/transactions/views/transactions.py:433
+#: apps/transactions/views/transactions.py:448
msgid "Transaction duplicated successfully"
msgstr "Zduplikowano transakcję"
-#: apps/transactions/views/transactions.py:478
+#: apps/transactions/views/transactions.py:493
msgid "Transaction deleted successfully"
msgstr "Usunięto transakcję"
-#: apps/transactions/views/transactions.py:508
+#: apps/transactions/views/transactions.py:523
msgid "Transaction restored successfully"
msgstr "Odtworzono transakcję"
-#: apps/transactions/views/transactions.py:534
+#: apps/transactions/views/transactions.py:549
msgid "Transfer added successfully"
msgstr "Dodano przelew"
-#: apps/transactions/views/transactions.py:562
+#: apps/transactions/views/transactions.py:579
#, fuzzy
#| msgid "Transfer added successfully"
msgid "Transfer updated successfully"
@@ -2337,6 +2497,7 @@ msgstr "Edytuj grupę kont"
#: templates/exchange_rates_services/fragments/table.html:20
#: templates/import_app/fragments/profiles/list.html:42
#: templates/installment_plans/fragments/table.html:25
+#: templates/investment_classes/fragments/list.html:36
#: templates/quick_transactions/fragments/list.html:22
#: templates/recurring_transactions/fragments/table.html:27
#: templates/rules/fragments/list.html:31
@@ -2359,6 +2520,9 @@ msgstr "Akcje"
#: templates/exchange_rates_services/fragments/table.html:23
#: templates/import_app/fragments/profiles/list.html:46
#: templates/installment_plans/fragments/table.html:28
+#: templates/investment_classes/fragments/list.html:39
+#: templates/investments/fragments/detail.html:305
+#: templates/investments/fragments/detail.html:331
#: templates/quick_transactions/fragments/list.html:25
#: templates/recurring_transactions/fragments/table.html:30
#: templates/rules/fragments/transaction_rule/view.html:22
@@ -2408,6 +2572,7 @@ msgstr "Udostępnij"
#: templates/import_app/fragments/profiles/list.html:81
#: templates/import_app/fragments/runs/list.html:100
#: templates/installment_plans/fragments/table.html:54
+#: templates/investment_classes/fragments/list.html:45
#: templates/mini_tools/simulator.html:109
#: templates/mini_tools/unit_price_calculator.html:16
#: templates/quick_transactions/fragments/list.html:32
@@ -2441,6 +2606,7 @@ msgstr "Usuń"
#: templates/import_app/fragments/runs/list.html:104
#: templates/installment_plans/fragments/table.html:47
#: templates/installment_plans/fragments/table.html:58
+#: templates/investment_classes/fragments/list.html:49
#: templates/quick_transactions/fragments/list.html:37
#: templates/recurring_transactions/fragments/table.html:52
#: templates/recurring_transactions/fragments/table.html:65
@@ -2490,6 +2656,7 @@ msgstr "Tej akcji nie da się odwrócić!"
#: templates/import_app/fragments/profiles/list.html:87
#: templates/import_app/fragments/runs/list.html:106
#: templates/installment_plans/fragments/table.html:60
+#: templates/investment_classes/fragments/list.html:51
#: templates/quick_transactions/fragments/list.html:39
#: templates/recurring_transactions/fragments/table.html:94
#: templates/rules/fragments/list.html:62
@@ -2538,6 +2705,7 @@ msgid "Add account"
msgstr "Dodaj konto"
#: templates/accounts/fragments/edit.html:5
+#: templates/investments/fragments/detail.html:40
msgid "Edit account"
msgstr "Edytuj konto"
@@ -2610,7 +2778,7 @@ msgstr "Edytuj kategorię"
msgid "Pick a month"
msgstr "Wybierz miesiąc"
-#: templates/common/fragments/toasts.html:20
+#: templates/common/fragments/toasts.html:18
#: templates/cotton/components/fab.html:9
#: templates/cotton/components/sidebar_collapsible_panel.html:28
#: templates/extends/offcanvas.html:4 templates/includes/sidebar.html:44
@@ -2841,6 +3009,8 @@ msgid "Add DCA strategy"
msgstr ""
#: templates/dca/fragments/strategy/details.html:28
+#: templates/investments/fragments/detail.html:92
+#: templates/investments/fragments/list.html:65
msgid "No exchange rate available"
msgstr ""
@@ -3023,7 +3193,7 @@ msgstr "konta"
msgid "No services configured"
msgstr "Nie zdefiniowano serwisów"
-#: templates/export_app/pages/index.html:4 templates/includes/sidebar.html:211
+#: templates/export_app/pages/index.html:4 templates/includes/sidebar.html:225
msgid "Export and Restore"
msgstr "Eksport i przywracanie"
@@ -3197,54 +3367,65 @@ msgstr ""
msgid "Tools"
msgstr "Narzędzia"
-#: templates/includes/sidebar.html:118
+#: templates/includes/sidebar.html:118 templates/includes/sidebar.html:188
+#: templates/investment_classes/fragments/list.html:29
+#: templates/investments/fragments/detail.html:37
+#: templates/investments/fragments/list.html:9
+#: templates/investments/pages/detail.html:4
+#: templates/investments/pages/index.html:4
+#, fuzzy
+#| msgid "Installment Plans"
+msgid "Investments"
+msgstr "Plany ratalne"
+
+#: templates/includes/sidebar.html:124
msgid "Dollar Cost Average Tracker"
msgstr ""
-#: templates/includes/sidebar.html:124
+#: templates/includes/sidebar.html:130
#: templates/mini_tools/unit_price_calculator.html:4
#: templates/mini_tools/unit_price_calculator.html:9
msgid "Unit Price Calculator"
msgstr "Kalkulator ceny jednostkowej"
-#: templates/includes/sidebar.html:130
+#: templates/includes/sidebar.html:136
#: templates/mini_tools/currency_converter/currency_converter.html:7
#: templates/mini_tools/currency_converter/currency_converter.html:14
msgid "Currency Converter"
msgstr "Kalkulator walut"
-#: templates/includes/sidebar.html:136 templates/mini_tools/simulator.html:5
+#: templates/includes/sidebar.html:142 templates/mini_tools/simulator.html:5
#: templates/mini_tools/simulator.html:21
#, fuzzy
#| msgid "Calculator"
msgid "Simulator"
msgstr "Kalkulator"
-#: templates/includes/sidebar.html:145
+#: templates/includes/sidebar.html:151
msgid "Management"
msgstr "Zarządzanie"
-#: templates/includes/sidebar.html:196
+#: templates/includes/sidebar.html:210
msgid "Automation"
msgstr ""
-#: templates/includes/sidebar.html:225
+#: templates/includes/sidebar.html:239
msgid "Admin"
msgstr ""
-#: templates/includes/sidebar.html:233
+#: templates/includes/sidebar.html:247
msgid "Django Admin"
msgstr ""
-#: templates/includes/sidebar.html:234
+#: templates/includes/sidebar.html:248
msgid "Only use this if you know what you're doing"
msgstr ""
-#: templates/includes/sidebar.html:249
+#: templates/includes/sidebar.html:263
msgid "is available"
msgstr ""
-#: templates/includes/sidebar.html:257
+#: templates/includes/sidebar.html:271
msgid "Calculator"
msgstr "Kalkulator"
@@ -3311,6 +3492,7 @@ msgstr "Brak ostatnich transakcji"
#: templates/insights/fragments/overview/_results.html:173
#: templates/insights/fragments/year_by_year.html:59
#: templates/insights/fragments/year_by_year.html:140
+#: templates/investment_classes/fragments/list.html:65
#: templates/monthly_overview/fragments/monthly_summary.html:168
msgid "Total"
msgstr "Łącznie"
@@ -3508,6 +3690,295 @@ msgstr ""
msgid "No installment plans"
msgstr "Brak planów ratalnych"
+#: templates/investment_classes/fragments/form.html:5
+msgid "Edit asset class"
+msgstr ""
+
+#: templates/investment_classes/fragments/form.html:5
+msgid "Add asset class"
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:19
+#: templates/investments/fragments/rebalance.html:10
+#, python-format
+msgid "Targets add up to %(total)s%%. Set them to 100%% to see the rebalance."
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:28
+#: templates/investments/fragments/rebalance.html:25
+msgid "Target"
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:50
+msgid "Its investments will be left without an asset class."
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:74
+msgid "No asset classes"
+msgstr ""
+
+#: templates/investments/fragments/_allocation.html:3
+msgid "Allocation chart"
+msgstr ""
+
+#: templates/investments/fragments/_allocation.html:11
+#, python-format
+msgid ""
+"%(share)s%% of your investments with a target, against a target of "
+"%(target)s%%"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid "Annual return, weighted by when money went in and out"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid ""
+"Return since the first contribution, weighted by when money went in and out"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid "/yr"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:15
+#: templates/investments/fragments/detail.html:63
+msgid "total return"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:20
+#: templates/investments/fragments/_totals.html:41
+#: templates/investments/fragments/charts.html:96
+#: templates/investments/fragments/detail.html:163
+#: templates/investments/fragments/detail.html:223
+#: templates/investments/fragments/list.html:47
+#, fuzzy
+#| msgid "Invert"
+msgid "Invested"
+msgstr "Zamień"
+
+#: templates/investments/fragments/_totals.html:24
+#: templates/investments/fragments/detail.html:74
+msgid ""
+"How much of the return came from exchange rates moving since you invested"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:24
+#: templates/investments/fragments/_totals.html:26
+#, fuzzy
+#| msgid "Currency Code"
+msgid "Currency effect"
+msgstr "Symbol waluty"
+
+#: templates/investments/fragments/_totals.html:26
+#: templates/investments/fragments/detail.html:78
+msgid ""
+"Every date is converted with the same rate. Add historical exchange rates "
+"(or an automatic rate service) to see how much exchange rates moved your "
+"return."
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:40
+#: templates/investments/fragments/list.html:49
+msgid "Return"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:10
+msgid "By asset class"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:17
+#: templates/net_worth/net_worth.html:52
+#: templates/yearly_overview/pages/overview_by_currency.html:6
+msgid "By currency"
+msgstr "Według walut"
+
+#: templates/investments/fragments/charts.html:24
+msgid "Value and invested"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:31
+msgid "Return by investment"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:38
+msgid "Nothing to chart yet"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:10
+msgid "Buy / Contribute"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:16
+msgid "Sell / Withdraw"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:23
+msgid "Dividend / Income"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:30
+#, fuzzy
+#| msgid "Update"
+msgid "Update value"
+msgstr "Aktualizuj"
+
+#: templates/investments/fragments/detail.html:47
+#: templates/investments/fragments/list.html:67
+msgid "Tracked by value"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:48
+msgid ""
+"Some incoming units have no linked payment, so their cost was estimated from "
+"the market price of that day"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:68
+#, fuzzy
+#| msgid "Info"
+msgid "In"
+msgstr "Informacja"
+
+#: templates/investments/fragments/detail.html:74
+#: templates/investments/fragments/detail.html:78
+#, fuzzy
+#| msgid "Currency Code"
+msgid "currency effect"
+msgstr "Symbol waluty"
+
+#: templates/investments/fragments/detail.html:104
+#: templates/investments/fragments/detail.html:179
+msgid "Price"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:105
+#: templates/investments/fragments/detail.html:200
+#: templates/investments/fragments/detail.html:239
+#: templates/investments/fragments/detail.html:290
+#, fuzzy
+#| msgid "Warning"
+msgid "Earnings"
+msgstr "Ostrzeżenie"
+
+#: templates/investments/fragments/detail.html:180
+#: templates/investments/fragments/detail.html:227
+msgid "Average cost"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:181
+#: templates/investments/fragments/rebalance.html:41
+msgid "Buy"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:182
+#: templates/investments/fragments/rebalance.html:43
+msgid "Sell"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:227
+#: templates/investments/fragments/detail.html:257
+msgid "Put in / taken out"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:239
+#, fuzzy
+#| msgid "Uncategorized"
+msgid "Unrealized"
+msgstr "Nieskategoryzowane"
+
+#: templates/investments/fragments/detail.html:248
+msgid "Realized"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:271
+#, fuzzy
+#| msgid "Management"
+msgid "Movements"
+msgstr "Zarządzanie"
+
+#: templates/investments/fragments/detail.html:284
+msgid "Bought / contributed"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:284
+msgid "Sold / withdrew"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:288
+msgid "Money in"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:288
+msgid "Money out"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:290
+msgid "Gain"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:290
+msgid "Loss, tax or fee"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:315
+#, fuzzy
+#| msgid "No attachments yet"
+msgid "No movements yet"
+msgstr "Brak załączników"
+
+#: templates/investments/fragments/detail.html:315
+msgid "Use the + button to buy or contribute"
+msgstr ""
+
+#: templates/investments/fragments/list.html:21
+msgid "Holdings"
+msgstr ""
+
+#: templates/investments/fragments/list.html:60
+msgid "Some costs were estimated from market prices"
+msgstr ""
+
+#: templates/investments/fragments/list.html:92
+msgid "Charts"
+msgstr ""
+
+#: templates/investments/fragments/list.html:97
+#, fuzzy
+#| msgid "New balance"
+msgid "Rebalance"
+msgstr "Nowe saldo"
+
+#: templates/investments/fragments/list.html:105
+#, fuzzy
+#| msgid "No attachments yet"
+msgid "No investments yet"
+msgstr "Brak załączników"
+
+#: templates/investments/fragments/list.html:106
+msgid "Mark an account as an investment in its settings to see it here"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:4
+msgid "No allocation targets"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:5
+msgid "Set a target for your asset classes to see what to buy and sell"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:26
+msgid "To reach the target"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:34
+msgid "Off target"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:55
+msgid "Left out: investments without an asset class or a target"
+msgstr ""
+
#: templates/layouts/base.html:38
msgid "This is a demo!"
msgstr ""
@@ -3727,11 +4198,6 @@ msgstr ""
msgid "Order by"
msgstr ""
-#: templates/net_worth/net_worth.html:52
-#: templates/yearly_overview/pages/overview_by_currency.html:6
-msgid "By currency"
-msgstr "Według walut"
-
#: templates/net_worth/net_worth.html:135
msgid "Evolution"
msgstr "Ewolucja"
diff --git a/app/locale/pt_BR/LC_MESSAGES/django.po b/app/locale/pt_BR/LC_MESSAGES/django.po
index 9a21a7dc..e7856217 100644
--- a/app/locale/pt_BR/LC_MESSAGES/django.po
+++ b/app/locale/pt_BR/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-11-08 06:01+0000\n"
+"POT-Creation-Date: 2026-11-08 18:27+0000\n"
"PO-Revision-Date: 2026-11-02 19:49+0000\n"
"Last-Translator: Herculino Trotta \n"
"Language-Team: Portuguese (Brazil) 1;\n"
"X-Generator: Weblate 2026.10.1\n"
-#: apps/accounts/forms.py:28
+#: apps/accounts/forms.py:33
msgid "Group name"
msgstr "Nome do grupo"
-#: apps/accounts/forms.py:43 apps/accounts/forms.py:109
-#: apps/currencies/forms.py:57 apps/currencies/forms.py:91
-#: apps/currencies/forms.py:166 apps/dca/forms.py:46 apps/dca/forms.py:205
-#: apps/import_app/forms.py:32 apps/rules/forms.py:60 apps/rules/forms.py:100
-#: apps/rules/forms.py:385 apps/transactions/forms.py:219
-#: apps/transactions/forms.py:452 apps/transactions/forms.py:571
-#: apps/transactions/forms.py:718 apps/transactions/forms.py:940
-#: apps/transactions/forms.py:987 apps/transactions/forms.py:1015
-#: apps/transactions/forms.py:1046 apps/transactions/forms.py:1208
-#: apps/users/forms.py:264 apps/users/forms.py:422
+#: apps/accounts/forms.py:48 apps/accounts/forms.py:76
+#: apps/accounts/forms.py:184 apps/currencies/forms.py:57
+#: apps/currencies/forms.py:91 apps/currencies/forms.py:168
+#: apps/dca/forms.py:46 apps/dca/forms.py:192 apps/import_app/forms.py:32
+#: apps/rules/forms.py:60 apps/rules/forms.py:100 apps/rules/forms.py:385
+#: apps/transactions/forms.py:219 apps/transactions/forms.py:452
+#: apps/transactions/forms.py:571 apps/transactions/forms.py:718
+#: apps/transactions/forms.py:940 apps/transactions/forms.py:987
+#: apps/transactions/forms.py:1015 apps/transactions/forms.py:1046
+#: apps/transactions/forms.py:1208 apps/users/forms.py:264
+#: apps/users/forms.py:422
#: templates/rules/fragments/transaction_rule/dry_run/updated.html:5
#: templates/rules/fragments/transaction_rule/view.html:128
msgid "Update"
msgstr "Atualizar"
-#: apps/accounts/forms.py:49 apps/accounts/forms.py:115
-#: apps/common/widgets/tom_select.py:13 apps/currencies/forms.py:63
-#: apps/currencies/forms.py:97 apps/currencies/forms.py:172
-#: apps/dca/forms.py:52 apps/dca/forms.py:211 apps/import_app/forms.py:38
-#: apps/rules/forms.py:66 apps/rules/forms.py:106 apps/rules/forms.py:391
-#: apps/transactions/forms.py:206 apps/transactions/forms.py:226
-#: apps/transactions/forms.py:459 apps/transactions/forms.py:946
-#: apps/transactions/forms.py:993 apps/transactions/forms.py:1021
-#: apps/transactions/forms.py:1052 apps/transactions/forms.py:1214
-#: apps/users/forms.py:270 apps/users/forms.py:428
-#: templates/mini_tools/simulator.html:177
+#: apps/accounts/forms.py:54 apps/accounts/forms.py:76
+#: apps/accounts/forms.py:190 apps/common/widgets/tom_select.py:15
+#: apps/currencies/forms.py:63 apps/currencies/forms.py:97
+#: apps/currencies/forms.py:174 apps/dca/forms.py:52 apps/dca/forms.py:198
+#: apps/import_app/forms.py:38 apps/rules/forms.py:66 apps/rules/forms.py:106
+#: apps/rules/forms.py:391 apps/transactions/forms.py:206
+#: apps/transactions/forms.py:226 apps/transactions/forms.py:459
+#: apps/transactions/forms.py:946 apps/transactions/forms.py:993
+#: apps/transactions/forms.py:1021 apps/transactions/forms.py:1052
+#: apps/transactions/forms.py:1214 apps/users/forms.py:270
+#: apps/users/forms.py:428 templates/mini_tools/simulator.html:177
#: templates/mini_tools/unit_price_calculator.html:168
msgid "Add"
msgstr "Adicionar"
-#: apps/accounts/forms.py:57 templates/accounts/fragments/list.html:23
+#: apps/accounts/forms.py:86 templates/accounts/fragments/list.html:23
msgid "Group"
msgstr "Grupo"
-#: apps/accounts/forms.py:123
+#: apps/accounts/forms.py:93 apps/accounts/models.py:73
+#: apps/accounts/models.py:130
+#: templates/investments/fragments/rebalance.html:22
+msgid "Asset class"
+msgstr ""
+
+#: apps/accounts/forms.py:198
msgid "New balance"
msgstr "Novo saldo"
-#: apps/accounts/forms.py:129 apps/dca/forms.py:79 apps/dca/forms.py:86
+#: apps/accounts/forms.py:204 apps/dca/forms.py:79 apps/dca/forms.py:86
#: apps/insights/forms.py:117 apps/insights/views.py:44 apps/rules/forms.py:181
#: apps/rules/forms.py:197 apps/rules/models.py:44 apps/rules/models.py:311
#: apps/transactions/forms.py:62 apps/transactions/forms.py:330
#: apps/transactions/forms.py:510 apps/transactions/forms.py:607
#: apps/transactions/forms.py:614 apps/transactions/forms.py:823
#: apps/transactions/forms.py:1075 apps/transactions/models.py:357
-#: apps/transactions/models.py:743 apps/transactions/models.py:999
-#: apps/transactions/models.py:1306
+#: apps/transactions/models.py:767 apps/transactions/models.py:1023
+#: apps/transactions/models.py:1330
#: templates/insights/fragments/month_by_month.html:84
#: templates/insights/fragments/year_by_year.html:52
msgid "Category"
msgstr "Categoria"
-#: apps/accounts/forms.py:136 apps/dca/forms.py:95 apps/dca/forms.py:103
-#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
+#: apps/accounts/forms.py:211 apps/dca/forms.py:95 apps/dca/forms.py:103
+#: apps/export_app/forms.py:43 apps/export_app/forms.py:134
#: apps/insights/views.py:50 apps/rules/forms.py:184 apps/rules/forms.py:194
#: apps/rules/models.py:45 apps/rules/models.py:315
#: apps/transactions/filters.py:79 apps/transactions/forms.py:70
#: apps/transactions/forms.py:338 apps/transactions/forms.py:518
#: apps/transactions/forms.py:623 apps/transactions/forms.py:631
#: apps/transactions/forms.py:816 apps/transactions/forms.py:1068
-#: apps/transactions/models.py:363 apps/transactions/models.py:745
-#: apps/transactions/models.py:1003 apps/transactions/models.py:1312
-#: templates/includes/sidebar.html:156
+#: apps/transactions/models.py:363 apps/transactions/models.py:769
+#: apps/transactions/models.py:1027 apps/transactions/models.py:1336
+#: templates/includes/sidebar.html:162
#: templates/insights/fragments/month_by_month.html:29
#: templates/insights/fragments/month_by_month.html:32
#: templates/insights/fragments/year_by_year.html:24
@@ -94,23 +101,23 @@ msgstr "Categoria"
msgid "Tags"
msgstr "Tags"
-#: apps/accounts/forms.py:179
+#: apps/accounts/forms.py:254
msgid "All paid transactions"
msgstr "Todas as transações pagas"
-#: apps/accounts/forms.py:180
+#: apps/accounts/forms.py:255
msgid "Up to date"
msgstr "Até a data"
-#: apps/accounts/forms.py:181
+#: apps/accounts/forms.py:256
msgid "Up to reference month"
msgstr "Até o mês de referência"
-#: apps/accounts/forms.py:185
+#: apps/accounts/forms.py:260
msgid "Balance against"
msgstr "Balancear contra"
-#: apps/accounts/forms.py:189 apps/currencies/forms.py:70
+#: apps/accounts/forms.py:264 apps/currencies/forms.py:70
#: apps/dca/models.py:158 apps/rules/forms.py:176 apps/rules/forms.py:190
#: apps/rules/models.py:38 apps/rules/models.py:279
#: apps/transactions/forms.py:86 apps/transactions/forms.py:482
@@ -121,14 +128,15 @@ msgstr "Balancear contra"
msgid "Date"
msgstr "Data"
-#: apps/accounts/forms.py:194
+#: apps/accounts/forms.py:269
msgid "Reference month"
msgstr "Mês de referência"
-#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
+#: apps/accounts/models.py:19 apps/accounts/models.py:36
+#: apps/accounts/models.py:84 apps/dca/models.py:13
#: apps/import_app/models.py:20 apps/rules/models.py:13
#: apps/transactions/models.py:249 apps/transactions/models.py:274
-#: apps/transactions/models.py:298 apps/transactions/models.py:1274
+#: apps/transactions/models.py:298 apps/transactions/models.py:1298
#: apps/users/models.py:577 templates/account_groups/fragments/list.html:22
#: templates/accounts/fragments/list.html:22
#: templates/categories/fragments/table.html:17
@@ -137,6 +145,7 @@ msgstr "Mês de referência"
#: templates/exchange_rates_services/fragments/list.html:28
#: templates/import_app/fragments/profiles/list.html:34
#: templates/installment_plans/fragments/table.html:16
+#: templates/investment_classes/fragments/list.html:27
#: templates/quick_transactions/fragments/list.html:13
#: templates/recurring_transactions/fragments/table.html:18
#: templates/rules/fragments/list.html:24
@@ -145,44 +154,129 @@ msgstr "Mês de referência"
msgid "Name"
msgstr "Nome"
-#: apps/accounts/models.py:18 apps/accounts/models.py:33
+#: apps/accounts/models.py:25 apps/accounts/models.py:88
msgid "Account Group"
msgstr "Grupo da Conta"
-#: apps/accounts/models.py:19 templates/account_groups/fragments/list.html:9
+#: apps/accounts/models.py:26 templates/account_groups/fragments/list.html:9
#: templates/account_groups/pages/index.html:4
-#: templates/includes/sidebar.html:176
+#: templates/includes/sidebar.html:182
msgid "Account Groups"
msgstr "Grupos da Conta"
-#: apps/accounts/models.py:39 apps/currencies/models.py:44
+#: apps/accounts/models.py:43
+msgid "Target allocation (%)"
+msgstr ""
+
+#: apps/accounts/models.py:45
+msgid ""
+"Share of your investments you want in this class. Leave it empty to leave "
+"the class out of the targets."
+msgstr ""
+
+#: apps/accounts/models.py:61
+msgid "You already have an asset class with this name."
+msgstr ""
+
+#: apps/accounts/models.py:66
+#, python-format
+msgid ""
+"Targets can't add up to more than 100%%. Other classes already take "
+"%(taken)s%%."
+msgstr ""
+
+#: apps/accounts/models.py:74 apps/export_app/forms.py:131
+#: templates/includes/sidebar.html:190
+#: templates/investment_classes/fragments/list.html:9
+#: templates/investment_classes/pages/index.html:4
+#: templates/investments/fragments/rebalance.html:6
+#: templates/investments/fragments/rebalance.html:11
+msgid "Asset classes"
+msgstr ""
+
+#: apps/accounts/models.py:94 apps/currencies/models.py:44
#: templates/accounts/fragments/list.html:24
#: templates/mini_tools/simulator.html:37
#: templates/yearly_overview/pages/yearly_overview_base.html:19
msgid "Currency"
msgstr "Moeda"
-#: apps/accounts/models.py:45 apps/currencies/models.py:27
+#: apps/accounts/models.py:100 apps/currencies/models.py:27
#: templates/accounts/fragments/list.html:25
msgid "Exchange Currency"
msgstr "Moeda de Câmbio"
-#: apps/accounts/models.py:50 apps/currencies/models.py:32
+#: apps/accounts/models.py:105 apps/currencies/models.py:32
msgid "Default currency for exchange calculations"
msgstr "Moeda padrão para os cálculos de câmbio"
-#: apps/accounts/models.py:55
+#: apps/accounts/models.py:110
msgid "Asset account"
msgstr "Conta de ativos"
-#: apps/accounts/models.py:57
+#: apps/accounts/models.py:112
msgid ""
"Asset accounts count towards your Net Worth, but not towards your month."
msgstr ""
"As contas de ativos contam para o seu patrimônio líquido, mas não para o seu "
"mês."
-#: apps/accounts/models.py:62 apps/currencies/models.py:37
+#: apps/accounts/models.py:117 templates/investments/fragments/list.html:46
+#, fuzzy
+#| msgid "Installment"
+msgid "Investment"
+msgstr "Parcelamento"
+
+#: apps/accounts/models.py:119
+msgid ""
+"Show this account in the Investments tracker. Set an exchange currency to "
+"price it by units (stocks, ETFs, crypto); leave it empty to track it by "
+"value (fixed income, property, pensions)."
+msgstr ""
+
+#: apps/accounts/models.py:132
+msgid ""
+"Groups investments in the allocation charts, e.g. Fixed income, Stocks, Real "
+"estate."
+msgstr ""
+
+#: apps/accounts/models.py:140
+#, fuzzy
+#| msgid "No entities"
+msgid "Income entities"
+msgstr "Sem entidades"
+
+#: apps/accounts/models.py:142
+msgid ""
+"Transactions with any of these entities count towards this investment's "
+"return: in this account, and income paid into other accounts (dividends, "
+"rent)."
+msgstr ""
+
+#: apps/accounts/models.py:151
+#, fuzzy
+#| msgid "No categories"
+msgid "Income categories"
+msgstr "Nenhum categoria"
+
+#: apps/accounts/models.py:153
+msgid ""
+"Transactions in this account with any of these categories are its result "
+"(interest, taxes, fees). On accounts tracked by value, everything else that "
+"isn't a transfer counts as money coming in or going out."
+msgstr ""
+
+#: apps/accounts/models.py:162
+#, fuzzy
+#| msgid "Income"
+msgid "Income tags"
+msgstr "Renda"
+
+#: apps/accounts/models.py:164
+msgid "Same as income categories, for transactions with any of these tags."
+msgstr ""
+
+#: apps/accounts/models.py:169 apps/currencies/models.py:37
#: templates/accounts/fragments/list.html:27
#: templates/categories/fragments/list.html:20
#: templates/currencies/fragments/list.html:24
@@ -191,18 +285,18 @@ msgstr ""
msgid "Archived"
msgstr "Arquivada"
-#: apps/accounts/models.py:63
+#: apps/accounts/models.py:170
msgid "Archived accounts don't show up nor count towards your net worth"
msgstr ""
"Contas arquivadas não aparecem nem contam para o seu patrimônio líquido"
-#: apps/accounts/models.py:75 apps/rules/forms.py:173 apps/rules/forms.py:187
+#: apps/accounts/models.py:182 apps/rules/forms.py:173 apps/rules/forms.py:187
#: apps/rules/models.py:35 apps/rules/models.py:267
#: apps/transactions/forms.py:82 apps/transactions/forms.py:350
#: apps/transactions/forms.py:477 apps/transactions/forms.py:808
#: apps/transactions/forms.py:1060 apps/transactions/models.py:329
-#: apps/transactions/models.py:703 apps/transactions/models.py:981
-#: apps/transactions/models.py:1280
+#: apps/transactions/models.py:727 apps/transactions/models.py:1005
+#: apps/transactions/models.py:1304
#: templates/installment_plans/fragments/table.html:17
#: templates/quick_transactions/fragments/list.html:14
#: templates/recurring_transactions/fragments/table.html:19
@@ -210,17 +304,17 @@ msgstr ""
msgid "Account"
msgstr "Conta"
-#: apps/accounts/models.py:76 apps/export_app/forms.py:19
-#: apps/export_app/forms.py:129 apps/transactions/filters.py:63
+#: apps/accounts/models.py:183 apps/export_app/forms.py:19
+#: apps/export_app/forms.py:130 apps/transactions/filters.py:63
#: templates/accounts/fragments/list.html:9
-#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:168
-#: templates/includes/sidebar.html:170
+#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:174
+#: templates/includes/sidebar.html:176
#: templates/monthly_overview/pages/overview.html:77
#: templates/transactions/pages/transactions.html:26
msgid "Accounts"
msgstr "Contas"
-#: apps/accounts/models.py:93
+#: apps/accounts/models.py:200
msgid "Exchange currency cannot be the same as the account's main currency."
msgstr "A moeda de câmbio não pode ser a mesma que a moeda principal da conta."
@@ -238,7 +332,7 @@ msgstr "Grupo de Conta apagado com sucesso"
#: apps/accounts/views/account_groups.py:109
#: apps/accounts/views/accounts.py:137 apps/dca/views.py:106
-#: apps/import_app/views.py:147 apps/rules/views.py:191
+#: apps/import_app/views.py:148 apps/rules/views.py:191
#: apps/transactions/views/categories.py:163
#: apps/transactions/views/entities.py:135 apps/transactions/views/tags.py:131
msgid "Item no longer shared with you"
@@ -246,7 +340,7 @@ msgstr "Item não está mais compartilhado com você"
#: apps/accounts/views/account_groups.py:132
#: apps/accounts/views/accounts.py:182 apps/dca/views.py:131
-#: apps/import_app/views.py:273 apps/rules/views.py:217
+#: apps/import_app/views.py:274 apps/rules/views.py:217
#: apps/transactions/views/categories.py:188
#: apps/transactions/views/entities.py:160 apps/transactions/views/tags.py:154
msgid "Ownership taken successfully"
@@ -254,7 +348,7 @@ msgstr "Propriedade assumida com sucesso"
#: apps/accounts/views/account_groups.py:152
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:151
-#: apps/import_app/views.py:294 apps/rules/views.py:238
+#: apps/import_app/views.py:295 apps/rules/views.py:238
#: apps/transactions/views/categories.py:130
#: apps/transactions/views/entities.py:180 apps/transactions/views/tags.py:174
msgid "Configuration saved successfully"
@@ -312,7 +406,7 @@ msgstr "Entidade com esse ID não existe."
msgid "Invalid entity data. Provide an ID or name."
msgstr "Dados da entidade inválidos. Forneça um ID ou nome."
-#: apps/api/serializers/transactions.py:141 apps/transactions/models.py:953
+#: apps/api/serializers/transactions.py:141 apps/transactions/models.py:977
msgid "This field is required."
msgstr "Este campo é obrigatório."
@@ -332,13 +426,14 @@ msgstr "Tornar privado"
msgid "Shared with users"
msgstr "Compartilhado com os usuários"
-#: apps/common/fields/forms/dynamic_select.py:71
-#: apps/common/fields/forms/dynamic_select.py:142
+#: apps/common/fields/forms/dynamic_select.py:79
+#: apps/common/fields/forms/dynamic_select.py:150
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:92 apps/common/widgets/tom_select.py:95
+#: apps/common/widgets/tom_select.py:94 apps/common/widgets/tom_select.py:97
+#: templates/investments/fragments/list.html:26
msgid "Ungrouped"
msgstr "Não agrupado"
@@ -486,14 +581,15 @@ msgid "Today"
msgstr "Hoje"
#: apps/common/widgets/datepicker.py:142
+#: templates/investments/fragments/rebalance.html:24
msgid "Now"
msgstr "Agora"
-#: apps/common/widgets/tom_select.py:11
+#: apps/common/widgets/tom_select.py:13
msgid "Remove"
msgstr "Remover"
-#: apps/common/widgets/tom_select.py:15 templates/mini_tools/simulator.html:187
+#: apps/common/widgets/tom_select.py:17 templates/mini_tools/simulator.html:187
#: templates/mini_tools/unit_price_calculator.html:180
#: templates/monthly_overview/pages/overview.html:286
#: templates/transactions/fragments/filter_actions.html:48
@@ -501,7 +597,7 @@ msgstr "Remover"
msgid "Clear"
msgstr "Limpar"
-#: apps/common/widgets/tom_select.py:16
+#: apps/common/widgets/tom_select.py:18
msgid "No results..."
msgstr "Sem resultados..."
@@ -535,10 +631,10 @@ msgid "Decimal Places"
msgstr "Casas Decimais"
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
-#: apps/export_app/forms.py:130 apps/transactions/filters.py:70
+#: apps/export_app/forms.py:132 apps/transactions/filters.py:70
#: templates/currencies/fragments/list.html:9
-#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:182
-#: templates/includes/sidebar.html:184
+#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:196
+#: templates/includes/sidebar.html:198
#: templates/monthly_overview/pages/overview.html:63
#: templates/transactions/pages/transactions.html:12
msgid "Currencies"
@@ -570,9 +666,9 @@ msgid "Auto"
msgstr "Automático"
#: apps/currencies/models.py:82 apps/export_app/forms.py:67
-#: apps/export_app/forms.py:142 templates/exchange_rates/fragments/list.html:10
+#: apps/export_app/forms.py:144 templates/exchange_rates/fragments/list.html:10
#: templates/exchange_rates/pages/index.html:4
-#: templates/includes/sidebar.html:190
+#: templates/includes/sidebar.html:204
msgid "Exchange Rates"
msgstr "Taxas de Câmbio"
@@ -668,33 +764,46 @@ msgstr ""
"definida."
#: apps/currencies/models.py:171
-msgid "Single exchange rate"
+#, fuzzy
+#| msgid "Single exchange rate"
+msgid "One exchange rate per day"
msgstr "Taxa de câmbio única"
#: apps/currencies/models.py:174
-msgid "Create one exchange rate and keep updating it. Avoids database clutter."
+msgid ""
+"Update the day's exchange rate on each fetch instead of adding a new one."
msgstr ""
-"Cria uma taxa de câmbio e mantenha-a atualizada. Evita a poluição do banco "
-"de dados."
-#: apps/currencies/models.py:179
+#: apps/currencies/models.py:178
+#, fuzzy
+#| msgid "Exchange Rates"
+msgid "Exchange rates to keep"
+msgstr "Taxas de Câmbio"
+
+#: apps/currencies/models.py:183
+msgid ""
+"Keep at most this many automatic exchange rates per currency pair, deleting "
+"the oldest first. Leave empty to keep all."
+msgstr ""
+
+#: apps/currencies/models.py:188
msgid "Exchange Rate Service"
msgstr "Serviço de Taxa de Câmbio"
-#: apps/currencies/models.py:180
+#: apps/currencies/models.py:189
msgid "Exchange Rate Services"
msgstr "Serviços de Taxa de Câmbio"
-#: apps/currencies/models.py:234
+#: apps/currencies/models.py:243
msgid "'Every X hours' interval type requires a positive integer."
msgstr ""
"Intervalo do tipo 'A cada X horas' requerer um número inteiro positivo."
-#: apps/currencies/models.py:243
+#: apps/currencies/models.py:252
msgid "'Every X hours' interval must be between 1 and 24."
msgstr "Intervalo do tipo 'A cada X horas' requerer um número entre 1 e 24."
-#: apps/currencies/models.py:257
+#: apps/currencies/models.py:266
msgid ""
"Invalid hour format. Use comma-separated hours (0-23) and/or ranges (e.g., "
"'1-5,8,10-12')."
@@ -702,7 +811,7 @@ msgstr ""
"Formato inválido de hora. Use uma lista de horas separada por vírgulas "
"(0-23) e/ou uma faixa (ex.: '1-5,8,10-12')."
-#: apps/currencies/models.py:268
+#: apps/currencies/models.py:277
msgid ""
"Invalid format. Please check the requirements for your selected interval "
"type."
@@ -774,21 +883,21 @@ msgstr "Digite para buscar uma transação para conectar à esta entrada"
msgid "Income Transaction"
msgstr "Transação de entrada"
-#: apps/dca/forms.py:192
+#: apps/dca/forms.py:179
msgid "Link transaction"
msgstr "Conectar transação"
-#: apps/dca/forms.py:275 apps/dca/forms.py:276 apps/dca/forms.py:281
-#: apps/dca/forms.py:285
+#: apps/dca/forms.py:262 apps/dca/forms.py:263 apps/dca/forms.py:268
+#: apps/dca/forms.py:272
msgid "You must provide an account."
msgstr "Você deve informar uma conta."
-#: apps/dca/forms.py:290 apps/transactions/forms.py:752
-#: apps/transactions/models.py:956
+#: apps/dca/forms.py:277 apps/transactions/forms.py:752
+#: apps/transactions/models.py:980
msgid "From and To accounts must be different."
msgstr "As contas De e Para devem ser diferentes."
-#: apps/dca/forms.py:304
+#: apps/dca/forms.py:299
#, python-format
msgid "DCA for %(strategy_name)s"
msgstr "CMP para %(strategy_name)s"
@@ -804,8 +913,8 @@ msgstr "Moeda de pagamento"
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
#: apps/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
#: apps/transactions/forms.py:504 apps/transactions/forms.py:651
-#: apps/transactions/models.py:353 apps/transactions/models.py:752
-#: apps/transactions/models.py:1009 apps/transactions/models.py:1302
+#: apps/transactions/models.py:353 apps/transactions/models.py:776
+#: apps/transactions/models.py:1033 apps/transactions/models.py:1326
msgid "Notes"
msgstr "Notas"
@@ -813,7 +922,7 @@ msgstr "Notas"
msgid "DCA Strategy"
msgstr "Estratégia CMP"
-#: apps/dca/models.py:35 apps/export_app/forms.py:150
+#: apps/dca/models.py:35 apps/export_app/forms.py:152
msgid "DCA Strategies"
msgstr "Estratégias CMP"
@@ -833,7 +942,7 @@ msgstr "Quantia recebida"
msgid "DCA Entry"
msgstr "Entrada CMP"
-#: apps/dca/models.py:187 apps/export_app/forms.py:151
+#: apps/dca/models.py:187 apps/export_app/forms.py:153
msgid "DCA Entries"
msgstr "Entradas CMP"
@@ -861,25 +970,25 @@ msgstr "Entrada atualizada com sucesso"
msgid "Entry deleted successfully"
msgstr "Entrada apagada com sucesso"
-#: apps/export_app/forms.py:13 apps/export_app/forms.py:128
-#: templates/includes/sidebar.html:227 templates/users/fragments/list.html:10
+#: apps/export_app/forms.py:13 apps/export_app/forms.py:129
+#: templates/includes/sidebar.html:241 templates/users/fragments/list.html:10
#: templates/users/pages/index.html:4
msgid "Users"
msgstr "Usuários"
-#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
+#: apps/export_app/forms.py:31 apps/export_app/forms.py:136
#: apps/transactions/models.py:422 templates/includes/sidebar.html:81
-#: templates/includes/sidebar.html:148
+#: templates/includes/sidebar.html:154
#: 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/export_app/forms.py:37 apps/export_app/forms.py:131
+#: apps/export_app/forms.py:37 apps/export_app/forms.py:133
#: apps/insights/views.py:43 apps/transactions/filters.py:74
#: templates/categories/fragments/list.html:9
-#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:150
+#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:156
#: templates/insights/fragments/month_by_month.html:18
#: templates/insights/fragments/month_by_month.html:21
#: templates/insights/fragments/year_by_year.html:13
@@ -887,16 +996,16 @@ msgstr "Transações"
msgid "Categories"
msgstr "Categorias"
-#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
+#: apps/export_app/forms.py:49 apps/export_app/forms.py:135
#: apps/insights/views.py:57 apps/rules/forms.py:185 apps/rules/forms.py:195
#: apps/rules/models.py:46 apps/rules/models.py:307
#: apps/transactions/filters.py:84 apps/transactions/forms.py:78
#: apps/transactions/forms.py:346 apps/transactions/forms.py:526
#: apps/transactions/forms.py:831 apps/transactions/forms.py:1083
#: apps/transactions/models.py:312 apps/transactions/models.py:368
-#: apps/transactions/models.py:748 apps/transactions/models.py:1006
-#: apps/transactions/models.py:1317 templates/entities/fragments/list.html:9
-#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:162
+#: apps/transactions/models.py:772 apps/transactions/models.py:1030
+#: apps/transactions/models.py:1341 templates/entities/fragments/list.html:9
+#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:168
#: templates/insights/fragments/month_by_month.html:40
#: templates/insights/fragments/month_by_month.html:43
#: templates/insights/fragments/year_by_year.html:35
@@ -904,28 +1013,28 @@ msgstr "Categorias"
msgid "Entities"
msgstr "Entidades"
-#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
-#: apps/transactions/models.py:1046 templates/includes/sidebar.html:110
+#: apps/export_app/forms.py:55 apps/export_app/forms.py:139
+#: apps/transactions/models.py:1070 templates/includes/sidebar.html:110
#: templates/recurring_transactions/fragments/list.html:9
#: templates/recurring_transactions/pages/index.html:4
msgid "Recurring Transactions"
msgstr "Transações Recorrentes"
-#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
-#: apps/transactions/models.py:766 templates/includes/sidebar.html:104
+#: apps/export_app/forms.py:61 apps/export_app/forms.py:137
+#: apps/transactions/models.py:790 templates/includes/sidebar.html:104
#: templates/installment_plans/fragments/list.html:9
#: templates/installment_plans/pages/index.html:4
msgid "Installment Plans"
msgstr "Parcelamentos"
-#: apps/export_app/forms.py:73 apps/export_app/forms.py:140
+#: apps/export_app/forms.py:73 apps/export_app/forms.py:142
#: templates/exchange_rates_services/fragments/list.html:10
#: templates/exchange_rates_services/pages/index.html:4
-#: templates/includes/sidebar.html:218
+#: templates/includes/sidebar.html:232
msgid "Automatic Exchange Rates"
msgstr "Taxas de Câmbio Automáticas"
-#: apps/export_app/forms.py:79 templates/includes/sidebar.html:198
+#: apps/export_app/forms.py:79 templates/includes/sidebar.html:212
#: templates/rules/fragments/list.html:9 templates/rules/pages/index.html:4
msgid "Rules"
msgstr "Regras"
@@ -934,57 +1043,57 @@ msgstr "Regras"
msgid "DCA"
msgstr "CMP"
-#: apps/export_app/forms.py:91 apps/export_app/forms.py:152
+#: apps/export_app/forms.py:91 apps/export_app/forms.py:154
#: templates/import_app/fragments/profiles/list.html:21
#: templates/import_app/pages/profiles_index.html:4
msgid "Import Profiles"
msgstr "Perfis de Importação"
-#: apps/export_app/forms.py:117 templates/export_app/fragments/export.html:5
+#: apps/export_app/forms.py:118 templates/export_app/fragments/export.html:5
#: templates/export_app/pages/index.html:13
msgid "Export"
msgstr "Exportar"
-#: apps/export_app/forms.py:125
+#: apps/export_app/forms.py:126
msgid "Import a ZIP file exported from WYGIWYH"
msgstr "Importe um arquivo ZIP exportado do WYGIWYH"
-#: apps/export_app/forms.py:126
+#: apps/export_app/forms.py:127
msgid "ZIP File"
msgstr "Arquivo ZIP"
-#: apps/export_app/forms.py:143 apps/rules/models.py:27
+#: apps/export_app/forms.py:145 apps/rules/models.py:27
msgid "Transaction rules"
msgstr "Regra da transação"
-#: apps/export_app/forms.py:145 apps/rules/models.py:68
+#: apps/export_app/forms.py:147 apps/rules/models.py:68
msgid "Edit transaction action"
msgstr "Ação de editar de transação"
-#: apps/export_app/forms.py:148 apps/rules/models.py:327
+#: apps/export_app/forms.py:150 apps/rules/models.py:327
msgid "Update or create transaction actions"
msgstr "Ações de atualizar ou criar transação"
-#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:341
+#: apps/export_app/forms.py:183 templates/cotton/transaction/item.html:341
#: templates/cotton/ui/deleted_transactions_action_bar.html:53
#: templates/export_app/fragments/restore.html:5
#: templates/export_app/pages/index.html:19
msgid "Restore"
msgstr "Restaurar"
-#: apps/export_app/forms.py:191
+#: apps/export_app/forms.py:193
msgid "Please upload either a ZIP file or at least one CSV file"
msgstr "Carregue um arquivo ZIP ou pelo menos um arquivo CSV"
-#: apps/export_app/views.py:177
+#: apps/export_app/views.py:183
msgid "You have to select at least one export"
msgstr "É necessário selecionar pelo menos uma exportação"
-#: apps/export_app/views.py:197
+#: apps/export_app/views.py:203
msgid "Data restored successfully"
msgstr "Dados restaurados com sucesso"
-#: apps/export_app/views.py:209
+#: apps/export_app/views.py:215
msgid ""
"There was an error restoring your data. Check the logs for more details."
msgstr ""
@@ -997,7 +1106,7 @@ msgstr "Selecione um arquivo"
#: apps/import_app/forms.py:55
#: templates/import_app/fragments/profiles/list.html:75
-#: templates/includes/sidebar.html:204
+#: templates/includes/sidebar.html:218
msgid "Import"
msgstr "Importar"
@@ -1045,23 +1154,23 @@ msgstr "Status"
msgid "File name"
msgstr "Nome do Arquivo"
-#: apps/import_app/views.py:75
+#: apps/import_app/views.py:76
msgid "Import Profile added successfully"
msgstr "Perfil de Importação adicionado com sucesso"
-#: apps/import_app/views.py:113
+#: apps/import_app/views.py:114
msgid "Import Profile update successfully"
msgstr "Importação atualizada com sucesso"
-#: apps/import_app/views.py:142
+#: apps/import_app/views.py:143
msgid "Import Profile deleted successfully"
msgstr "Perfil de Importação apagado com sucesso"
-#: apps/import_app/views.py:222
+#: apps/import_app/views.py:223
msgid "Import Run queued successfully"
msgstr "Importação adicionada à fila com sucesso"
-#: apps/import_app/views.py:249
+#: apps/import_app/views.py:250
msgid "Run deleted successfully"
msgstr "Importação apagada com sucesso"
@@ -1130,7 +1239,7 @@ msgstr "Tag"
msgid "Untagged"
msgstr "Sem tag"
-#: apps/insights/views.py:53 apps/transactions/models.py:589
+#: apps/insights/views.py:53 apps/transactions/models.py:613
#: templates/tags/fragments/table.html:69
msgid "No tags"
msgstr "Nenhuma tag"
@@ -1151,6 +1260,32 @@ msgstr "Sem entidade"
msgid "No entities"
msgstr "Sem entidades"
+#: apps/investments/views.py:104
+msgid "Other"
+msgstr ""
+
+#: apps/investments/views.py:135
+msgid "Unclassified"
+msgstr ""
+
+#: apps/investments/views.py:267
+#, fuzzy
+#| msgid "Installment Plan added successfully"
+msgid "Asset class added successfully"
+msgstr "Parcelamento adicionado com sucesso"
+
+#: apps/investments/views.py:275
+#, fuzzy
+#| msgid "Tag updated successfully"
+msgid "Asset class updated successfully"
+msgstr "Tag atualizada com sucesso"
+
+#: apps/investments/views.py:296
+#, fuzzy
+#| msgid "Tag deleted successfully"
+msgid "Asset class deleted successfully"
+msgstr "Tag apagada com sucesso"
+
#: apps/net_worth/views.py:137 templates/net_worth/net_worth.html:84
#: templates/net_worth/net_worth.html:85
msgid "Consolidated"
@@ -1204,8 +1339,8 @@ msgstr "Operador"
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
#: apps/rules/models.py:271 apps/transactions/forms.py:468
-#: apps/transactions/models.py:336 apps/transactions/models.py:708
-#: apps/transactions/models.py:987 apps/transactions/models.py:1287
+#: apps/transactions/models.py:336 apps/transactions/models.py:732
+#: apps/transactions/models.py:1011 apps/transactions/models.py:1311
#: templates/mini_tools/simulator.html:63
msgid "Type"
msgstr "Tipo"
@@ -1213,7 +1348,7 @@ msgstr "Tipo"
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
#: apps/rules/models.py:275 apps/transactions/filters.py:22
#: apps/transactions/forms.py:472 apps/transactions/models.py:338
-#: apps/transactions/models.py:1289 templates/cotton/transaction/item.html:23
+#: apps/transactions/models.py:1313 templates/cotton/transaction/item.html:23
#: templates/cotton/transaction/item.html:34
#: templates/transactions/widgets/paid_toggle_button.html:10
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
@@ -1224,14 +1359,14 @@ msgstr "Pago"
#: apps/rules/models.py:283 apps/transactions/forms.py:90
#: apps/transactions/forms.py:488 apps/transactions/forms.py:638
#: apps/transactions/forms.py:837 apps/transactions/models.py:340
-#: apps/transactions/models.py:726 apps/transactions/models.py:1011
+#: apps/transactions/models.py:750 apps/transactions/models.py:1035
msgid "Reference Date"
msgstr "Data de Referência"
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
#: apps/rules/models.py:287 apps/transactions/forms.py:495
-#: apps/transactions/models.py:346 apps/transactions/models.py:992
-#: apps/transactions/models.py:1295
+#: apps/transactions/models.py:346 apps/transactions/models.py:1016
+#: apps/transactions/models.py:1319
#: templates/insights/fragments/sankey.html:102
#: templates/installment_plans/fragments/table.html:18
#: templates/mini_tools/simulator.html:70
@@ -1243,27 +1378,27 @@ msgstr "Quantia"
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
#: apps/rules/models.py:42 apps/rules/models.py:291
#: apps/transactions/forms.py:499 apps/transactions/forms.py:642
-#: apps/transactions/models.py:351 apps/transactions/models.py:710
-#: apps/transactions/models.py:995 apps/transactions/models.py:1300
+#: apps/transactions/models.py:351 apps/transactions/models.py:734
+#: apps/transactions/models.py:1019 apps/transactions/models.py:1324
msgid "Description"
msgstr "Descrição"
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
#: apps/rules/models.py:299 apps/transactions/models.py:398
-#: apps/transactions/models.py:1322
+#: apps/transactions/models.py:1346
msgid "Internal Note"
msgstr "Nota Interna"
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
#: apps/rules/models.py:303 apps/transactions/models.py:400
-#: apps/transactions/models.py:1324
+#: apps/transactions/models.py:1348
msgid "Internal ID"
msgstr "ID Interna"
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
#: apps/rules/models.py:319 apps/transactions/forms.py:655
#: apps/transactions/models.py:250 apps/transactions/models.py:341
-#: apps/transactions/models.py:1290
+#: apps/transactions/models.py:1314
msgid "Mute"
msgstr "Silenciada"
@@ -1276,7 +1411,7 @@ msgid "Set Values"
msgstr "Definir valores"
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
-#: apps/transactions/models.py:421 apps/transactions/models.py:645
+#: apps/transactions/models.py:421 apps/transactions/models.py:669
msgid "Transaction"
msgstr "Transação"
@@ -1312,7 +1447,12 @@ msgstr "Regra"
msgid "Field"
msgstr "Campo"
-#: apps/rules/models.py:61
+#: apps/rules/models.py:61 templates/investments/fragments/charts.html:86
+#: templates/investments/fragments/detail.html:103
+#: templates/investments/fragments/detail.html:162
+#: templates/investments/fragments/list.html:48
+#: templates/investments/fragments/list.html:49
+#: templates/investments/fragments/rebalance.html:23
msgid "Value"
msgstr "Valor"
@@ -1498,7 +1638,7 @@ msgid "Upload"
msgstr "Upload"
#: apps/transactions/forms.py:355 apps/transactions/forms.py:1089
-#: apps/transactions/models.py:925
+#: apps/transactions/models.py:949
msgid "To account"
msgstr "Conta de destino"
@@ -1514,8 +1654,8 @@ msgstr "Quantia de origem"
msgid "To Amount"
msgstr "Quantia de destino"
-#: apps/transactions/forms.py:718 apps/transactions/models.py:970
-#: apps/transactions/models.py:1267 templates/cotton/transaction/item.html:52
+#: apps/transactions/forms.py:718 apps/transactions/models.py:994
+#: apps/transactions/models.py:1291 templates/cotton/transaction/item.html:52
#: templates/cotton/transaction/item.html:60
#: templates/cotton/ui/quick_transactions_buttons.html:40
#: templates/cotton/ui/transactions_fab.html:44
@@ -1580,8 +1720,8 @@ msgstr ""
"As entidades desativadas não poderão ser selecionadas ao criar novas "
"transações"
-#: apps/transactions/models.py:323 apps/transactions/models.py:969
-#: apps/transactions/models.py:1266
+#: apps/transactions/models.py:323 apps/transactions/models.py:993
+#: apps/transactions/models.py:1290
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: templates/calendar_view/fragments/list.html:52
@@ -1589,13 +1729,19 @@ msgstr ""
#: templates/cotton/ui/quick_transactions_buttons.html:10
#: templates/cotton/ui/transactions_fab.html:10
#: templates/insights/fragments/overview/_results.html:12
+#: templates/investments/fragments/_totals.html:21
+#: templates/investments/fragments/_totals.html:42
+#: templates/investments/fragments/detail.html:106
+#: templates/investments/fragments/detail.html:210
+#: templates/investments/fragments/detail.html:252
+#: templates/investments/fragments/detail.html:320
#: templates/mini_tools/simulator.html:66
#: templates/monthly_overview/fragments/monthly_summary.html:42
msgid "Income"
msgstr "Renda"
-#: apps/transactions/models.py:324 apps/transactions/models.py:971
-#: apps/transactions/models.py:1268
+#: apps/transactions/models.py:324 apps/transactions/models.py:995
+#: apps/transactions/models.py:1292
#: templates/calendar_view/fragments/list.html:46
#: templates/calendar_view/fragments/list.html:48
#: templates/calendar_view/fragments/list.html:56
@@ -1607,14 +1753,14 @@ msgstr "Renda"
msgid "Expense"
msgstr "Despesa"
-#: apps/transactions/models.py:379 apps/transactions/models.py:765
+#: apps/transactions/models.py:379 apps/transactions/models.py:789
#: templates/cotton/transaction/item.html:291
#: templates/cotton/transaction/item.html:308
#: templates/cotton/transaction/item.html:318
msgid "Installment Plan"
msgstr "Parcelamento"
-#: apps/transactions/models.py:388 apps/transactions/models.py:1045
+#: apps/transactions/models.py:388 apps/transactions/models.py:1069
#: templates/cotton/transaction/item.html:281
#: templates/cotton/transaction/item.html:301
msgid "Recurring Transaction"
@@ -1632,150 +1778,162 @@ msgstr "Apagado"
msgid "Deleted At"
msgstr "Apagado Em"
-#: apps/transactions/models.py:591
+#: apps/transactions/models.py:517
+msgid "A transfer needs one expense and one income."
+msgstr ""
+
+#: apps/transactions/models.py:519
+msgid "A transfer needs two different accounts."
+msgstr ""
+
+#: apps/transactions/models.py:523
+msgid "This transaction is already part of another transfer."
+msgstr ""
+
+#: apps/transactions/models.py:615
msgid "No category"
msgstr "Sem categoria"
-#: apps/transactions/models.py:593
+#: apps/transactions/models.py:617
msgid "No description"
msgstr "Sem descrição"
-#: apps/transactions/models.py:650
+#: apps/transactions/models.py:674
msgid "File"
msgstr "Arquivo"
-#: apps/transactions/models.py:652
+#: apps/transactions/models.py:676
msgid "Original Name"
msgstr "Nome Original"
-#: apps/transactions/models.py:654
+#: apps/transactions/models.py:678
msgid "Content Type"
msgstr "Tipo de Conteúdo"
-#: apps/transactions/models.py:656
+#: apps/transactions/models.py:680
msgid "Size"
msgstr "Tamanho"
-#: apps/transactions/models.py:661
+#: apps/transactions/models.py:685
msgid "Uploaded By"
msgstr "Enviado por"
-#: apps/transactions/models.py:666
+#: apps/transactions/models.py:690
msgid "Transaction Attachment"
msgstr "Anexo de transação"
-#: apps/transactions/models.py:667
+#: apps/transactions/models.py:691
msgid "Transaction Attachments"
msgstr "Anexo de transações"
-#: apps/transactions/models.py:697 templates/includes/sidebar.html:57
+#: apps/transactions/models.py:721 templates/includes/sidebar.html:57
msgid "Yearly"
msgstr "Anual"
-#: apps/transactions/models.py:698 apps/users/models.py:469
+#: apps/transactions/models.py:722 apps/users/models.py:469
#: templates/includes/sidebar.html:51
msgid "Monthly"
msgstr "Mensal"
-#: apps/transactions/models.py:699
+#: apps/transactions/models.py:723
msgid "Weekly"
msgstr "Semanal"
-#: apps/transactions/models.py:700
+#: apps/transactions/models.py:724
msgid "Daily"
msgstr "Diária"
-#: apps/transactions/models.py:713
+#: apps/transactions/models.py:737
msgid "Number of Installments"
msgstr "Número de Parcelas"
-#: apps/transactions/models.py:718
+#: apps/transactions/models.py:742
msgid "Installment Start"
msgstr "Parcela inicial"
-#: apps/transactions/models.py:719
+#: apps/transactions/models.py:743
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:724 apps/transactions/models.py:1015
+#: apps/transactions/models.py:748 apps/transactions/models.py:1039
msgid "Start Date"
msgstr "Data de Início"
-#: apps/transactions/models.py:728 apps/transactions/models.py:1016
+#: apps/transactions/models.py:752 apps/transactions/models.py:1040
msgid "End Date"
msgstr "Data Final"
-#: apps/transactions/models.py:733
+#: apps/transactions/models.py:757
msgid "Recurrence"
msgstr "Recorrência"
-#: apps/transactions/models.py:736
+#: apps/transactions/models.py:760
msgid "Installment Amount"
msgstr "Valor da Parcela"
-#: apps/transactions/models.py:755 apps/transactions/models.py:1035
+#: apps/transactions/models.py:779 apps/transactions/models.py:1059
msgid "Add description to transactions"
msgstr "Adicionar descrição às transações"
-#: apps/transactions/models.py:758 apps/transactions/models.py:1038
+#: apps/transactions/models.py:782 apps/transactions/models.py:1062
msgid "Add notes to transactions"
msgstr "Adicionar notas às transações"
-#: apps/transactions/models.py:932
+#: apps/transactions/models.py:956
msgid "To amount"
msgstr "Quantia de destino"
-#: apps/transactions/models.py:933
+#: apps/transactions/models.py:957
msgid "Leave empty to use the same amount"
msgstr "Deixe em branco para usar a mesma quantia"
-#: apps/transactions/models.py:974 templates/mini_tools/simulator.html:129
+#: apps/transactions/models.py:998 templates/mini_tools/simulator.html:129
msgid "day(s)"
msgstr "dia(s)"
-#: apps/transactions/models.py:975 templates/mini_tools/simulator.html:130
+#: apps/transactions/models.py:999 templates/mini_tools/simulator.html:130
msgid "week(s)"
msgstr "semana(s)"
-#: apps/transactions/models.py:976 templates/mini_tools/simulator.html:131
+#: apps/transactions/models.py:1000 templates/mini_tools/simulator.html:131
msgid "month(s)"
msgstr "mês(es)"
-#: apps/transactions/models.py:977 templates/mini_tools/simulator.html:132
+#: apps/transactions/models.py:1001 templates/mini_tools/simulator.html:132
msgid "year(s)"
msgstr "ano(s)"
-#: apps/transactions/models.py:979
+#: apps/transactions/models.py:1003
#: templates/recurring_transactions/fragments/list.html:18
msgid "Paused"
msgstr "Pausado"
-#: apps/transactions/models.py:1018
+#: apps/transactions/models.py:1042
msgid "Recurrence Type"
msgstr "Tipo de recorrência"
-#: apps/transactions/models.py:1021
+#: apps/transactions/models.py:1045
msgid "Recurrence Interval"
msgstr "Intervalo de recorrência"
-#: apps/transactions/models.py:1024
+#: apps/transactions/models.py:1048
msgid "Keep at most"
msgstr "Manter no máximo"
-#: apps/transactions/models.py:1028
+#: apps/transactions/models.py:1052
msgid "Last Generated Date"
msgstr "Última data gerada"
-#: apps/transactions/models.py:1031
+#: apps/transactions/models.py:1055
msgid "Last Generated Reference Date"
msgstr "Última data de referência gerada"
-#: apps/transactions/models.py:1334 templates/cotton/transaction/item.html:274
+#: apps/transactions/models.py:1358 templates/cotton/transaction/item.html:274
#: templates/cotton/ui/transactions_fab.html:59
msgid "Quick Transaction"
msgstr "Transação Rápida"
-#: apps/transactions/models.py:1335 templates/includes/sidebar.html:98
+#: apps/transactions/models.py:1359 templates/includes/sidebar.html:98
#: templates/quick_transactions/pages/index.html:5
#: templates/quick_transactions/pages/index.html:15
msgid "Quick Transactions"
@@ -1898,8 +2056,8 @@ msgid "Item deleted successfully"
msgstr "Item apagado com sucesso"
#: apps/transactions/views/quick_transactions.py:164
-#: apps/transactions/views/transactions.py:143
-#: apps/transactions/views/transactions.py:328
+#: apps/transactions/views/transactions.py:156
+#: apps/transactions/views/transactions.py:343
msgid "Transaction added successfully"
msgstr "Transação adicionada com sucesso"
@@ -1935,42 +2093,42 @@ msgstr "Tag atualizada com sucesso"
msgid "Tag deleted successfully"
msgstr "Tag apagada com sucesso"
-#: apps/transactions/views/transactions.py:61
+#: apps/transactions/views/transactions.py:65
msgid "Attachment uploaded successfully"
msgstr "Anexo enviado com sucesso"
-#: apps/transactions/views/transactions.py:112
+#: apps/transactions/views/transactions.py:116
msgid "Attachment deleted successfully"
msgstr "Anexo apagado com sucesso"
-#: apps/transactions/views/transactions.py:355
+#: apps/transactions/views/transactions.py:370
msgid "Transaction updated successfully"
msgstr "Transação atualizada com sucesso"
-#: apps/transactions/views/transactions.py:406
+#: apps/transactions/views/transactions.py:421
#, python-format
msgid "%(count)s transaction updated successfully"
msgid_plural "%(count)s transactions updated successfully"
msgstr[0] "%(count)s transação atualizada com sucesso"
msgstr[1] "%(count)s transações atualizadas com sucesso"
-#: apps/transactions/views/transactions.py:433
+#: apps/transactions/views/transactions.py:448
msgid "Transaction duplicated successfully"
msgstr "Transação duplicada com sucesso"
-#: apps/transactions/views/transactions.py:478
+#: apps/transactions/views/transactions.py:493
msgid "Transaction deleted successfully"
msgstr "Transação apagada com sucesso"
-#: apps/transactions/views/transactions.py:508
+#: apps/transactions/views/transactions.py:523
msgid "Transaction restored successfully"
msgstr "Transação restaurada com sucesso"
-#: apps/transactions/views/transactions.py:534
+#: apps/transactions/views/transactions.py:549
msgid "Transfer added successfully"
msgstr "Transferência adicionada com sucesso"
-#: apps/transactions/views/transactions.py:562
+#: apps/transactions/views/transactions.py:579
msgid "Transfer updated successfully"
msgstr "Transferência atualizada com sucesso"
@@ -2283,6 +2441,7 @@ msgstr "Editar grupo de conta"
#: templates/exchange_rates_services/fragments/table.html:20
#: templates/import_app/fragments/profiles/list.html:42
#: templates/installment_plans/fragments/table.html:25
+#: templates/investment_classes/fragments/list.html:36
#: templates/quick_transactions/fragments/list.html:22
#: templates/recurring_transactions/fragments/table.html:27
#: templates/rules/fragments/list.html:31
@@ -2305,6 +2464,9 @@ msgstr "Ações"
#: templates/exchange_rates_services/fragments/table.html:23
#: templates/import_app/fragments/profiles/list.html:46
#: templates/installment_plans/fragments/table.html:28
+#: templates/investment_classes/fragments/list.html:39
+#: templates/investments/fragments/detail.html:305
+#: templates/investments/fragments/detail.html:331
#: templates/quick_transactions/fragments/list.html:25
#: templates/recurring_transactions/fragments/table.html:30
#: templates/rules/fragments/transaction_rule/view.html:22
@@ -2354,6 +2516,7 @@ msgstr "Compartilhar"
#: templates/import_app/fragments/profiles/list.html:81
#: templates/import_app/fragments/runs/list.html:100
#: templates/installment_plans/fragments/table.html:54
+#: templates/investment_classes/fragments/list.html:45
#: templates/mini_tools/simulator.html:109
#: templates/mini_tools/unit_price_calculator.html:16
#: templates/quick_transactions/fragments/list.html:32
@@ -2387,6 +2550,7 @@ msgstr "Apagar"
#: templates/import_app/fragments/runs/list.html:104
#: templates/installment_plans/fragments/table.html:47
#: templates/installment_plans/fragments/table.html:58
+#: templates/investment_classes/fragments/list.html:49
#: templates/quick_transactions/fragments/list.html:37
#: templates/recurring_transactions/fragments/table.html:52
#: templates/recurring_transactions/fragments/table.html:65
@@ -2436,6 +2600,7 @@ msgstr "Você não será capaz de reverter isso!"
#: templates/import_app/fragments/profiles/list.html:87
#: templates/import_app/fragments/runs/list.html:106
#: templates/installment_plans/fragments/table.html:60
+#: templates/investment_classes/fragments/list.html:51
#: templates/quick_transactions/fragments/list.html:39
#: templates/recurring_transactions/fragments/table.html:94
#: templates/rules/fragments/list.html:62
@@ -2484,6 +2649,7 @@ msgid "Add account"
msgstr "Adicionar conta"
#: templates/accounts/fragments/edit.html:5
+#: templates/investments/fragments/detail.html:40
msgid "Edit account"
msgstr "Editar conta"
@@ -2556,7 +2722,7 @@ msgstr "Editar categoria"
msgid "Pick a month"
msgstr "Escolha um mês"
-#: templates/common/fragments/toasts.html:20
+#: templates/common/fragments/toasts.html:18
#: templates/cotton/components/fab.html:9
#: templates/cotton/components/sidebar_collapsible_panel.html:28
#: templates/extends/offcanvas.html:4 templates/includes/sidebar.html:44
@@ -2785,6 +2951,8 @@ msgid "Add DCA strategy"
msgstr "Adicionar estratégia CMP"
#: templates/dca/fragments/strategy/details.html:28
+#: templates/investments/fragments/detail.html:92
+#: templates/investments/fragments/list.html:65
msgid "No exchange rate available"
msgstr "Nenhuma taxa de câmbio disponível"
@@ -2967,7 +3135,7 @@ msgstr "contas"
msgid "No services configured"
msgstr "Nenhum serviço configurado"
-#: templates/export_app/pages/index.html:4 templates/includes/sidebar.html:211
+#: templates/export_app/pages/index.html:4 templates/includes/sidebar.html:225
msgid "Export and Restore"
msgstr "Exportar e Restaurar"
@@ -3142,52 +3310,63 @@ msgstr "Lixeira"
msgid "Tools"
msgstr "Ferramentas"
-#: templates/includes/sidebar.html:118
+#: templates/includes/sidebar.html:118 templates/includes/sidebar.html:188
+#: templates/investment_classes/fragments/list.html:29
+#: templates/investments/fragments/detail.html:37
+#: templates/investments/fragments/list.html:9
+#: templates/investments/pages/detail.html:4
+#: templates/investments/pages/index.html:4
+#, fuzzy
+#| msgid "Installments"
+msgid "Investments"
+msgstr "Parcelas"
+
+#: templates/includes/sidebar.html:124
msgid "Dollar Cost Average Tracker"
msgstr "Rastreador de Custo Médio Ponderado"
-#: templates/includes/sidebar.html:124
+#: templates/includes/sidebar.html:130
#: templates/mini_tools/unit_price_calculator.html:4
#: templates/mini_tools/unit_price_calculator.html:9
msgid "Unit Price Calculator"
msgstr "Calculadora de preço unitário"
-#: templates/includes/sidebar.html:130
+#: templates/includes/sidebar.html:136
#: templates/mini_tools/currency_converter/currency_converter.html:7
#: templates/mini_tools/currency_converter/currency_converter.html:14
msgid "Currency Converter"
msgstr "Conversor de Moeda"
-#: templates/includes/sidebar.html:136 templates/mini_tools/simulator.html:5
+#: templates/includes/sidebar.html:142 templates/mini_tools/simulator.html:5
#: templates/mini_tools/simulator.html:21
msgid "Simulator"
msgstr "Simulador"
-#: templates/includes/sidebar.html:145
+#: templates/includes/sidebar.html:151
msgid "Management"
msgstr "Gerenciar"
-#: templates/includes/sidebar.html:196
+#: templates/includes/sidebar.html:210
msgid "Automation"
msgstr "Automação"
-#: templates/includes/sidebar.html:225
+#: templates/includes/sidebar.html:239
msgid "Admin"
msgstr "Admin"
-#: templates/includes/sidebar.html:233
+#: templates/includes/sidebar.html:247
msgid "Django Admin"
msgstr "Django Admin"
-#: templates/includes/sidebar.html:234
+#: templates/includes/sidebar.html:248
msgid "Only use this if you know what you're doing"
msgstr "Só use isso se você souber o que está fazendo"
-#: templates/includes/sidebar.html:249
+#: templates/includes/sidebar.html:263
msgid "is available"
msgstr "está disponível"
-#: templates/includes/sidebar.html:257
+#: templates/includes/sidebar.html:271
msgid "Calculator"
msgstr "Calculadora"
@@ -3254,6 +3433,7 @@ msgstr "Nenhuma transação recente"
#: templates/insights/fragments/overview/_results.html:173
#: templates/insights/fragments/year_by_year.html:59
#: templates/insights/fragments/year_by_year.html:140
+#: templates/investment_classes/fragments/list.html:65
#: templates/monthly_overview/fragments/monthly_summary.html:168
msgid "Total"
msgstr "Total"
@@ -3454,6 +3634,305 @@ msgstr "Isso excluirá o parcelamento e todas as transações associadas a ele"
msgid "No installment plans"
msgstr "Nenhum parcelamento"
+#: templates/investment_classes/fragments/form.html:5
+msgid "Edit asset class"
+msgstr ""
+
+#: templates/investment_classes/fragments/form.html:5
+msgid "Add asset class"
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:19
+#: templates/investments/fragments/rebalance.html:10
+#, python-format
+msgid "Targets add up to %(total)s%%. Set them to 100%% to see the rebalance."
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:28
+#: templates/investments/fragments/rebalance.html:25
+#, fuzzy
+#| msgid "Targeting"
+msgid "Target"
+msgstr "Alvos"
+
+#: templates/investment_classes/fragments/list.html:50
+msgid "Its investments will be left without an asset class."
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:74
+msgid "No asset classes"
+msgstr ""
+
+#: templates/investments/fragments/_allocation.html:3
+msgid "Allocation chart"
+msgstr ""
+
+#: templates/investments/fragments/_allocation.html:11
+#, python-format
+msgid ""
+"%(share)s%% of your investments with a target, against a target of "
+"%(target)s%%"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid "Annual return, weighted by when money went in and out"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid ""
+"Return since the first contribution, weighted by when money went in and out"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid "/yr"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:15
+#: templates/investments/fragments/detail.html:63
+msgid "total return"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:20
+#: templates/investments/fragments/_totals.html:41
+#: templates/investments/fragments/charts.html:96
+#: templates/investments/fragments/detail.html:163
+#: templates/investments/fragments/detail.html:223
+#: templates/investments/fragments/list.html:47
+#, fuzzy
+#| msgid "Total Invested"
+msgid "Invested"
+msgstr "Total investido"
+
+#: templates/investments/fragments/_totals.html:24
+#: templates/investments/fragments/detail.html:74
+msgid ""
+"How much of the return came from exchange rates moving since you invested"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:24
+#: templates/investments/fragments/_totals.html:26
+#, fuzzy
+#| msgid "Currency Code"
+msgid "Currency effect"
+msgstr "Código da Moeda"
+
+#: templates/investments/fragments/_totals.html:26
+#: templates/investments/fragments/detail.html:78
+msgid ""
+"Every date is converted with the same rate. Add historical exchange rates "
+"(or an automatic rate service) to see how much exchange rates moved your "
+"return."
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:40
+#: templates/investments/fragments/list.html:49
+msgid "Return"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:10
+msgid "By asset class"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:17
+#: templates/net_worth/net_worth.html:52
+#: templates/yearly_overview/pages/overview_by_currency.html:6
+msgid "By currency"
+msgstr "Por moeda"
+
+#: templates/investments/fragments/charts.html:24
+#, fuzzy
+#| msgid "Total Invested"
+msgid "Value and invested"
+msgstr "Total investido"
+
+#: templates/investments/fragments/charts.html:31
+msgid "Return by investment"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:38
+#, fuzzy
+#| msgid "Nothing to see here..."
+msgid "Nothing to chart yet"
+msgstr "Nada para ver aqui..."
+
+#: templates/investments/fragments/detail.html:10
+msgid "Buy / Contribute"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:16
+msgid "Sell / Withdraw"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:23
+msgid "Dividend / Income"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:30
+#, fuzzy
+#| msgid "Updated at"
+msgid "Update value"
+msgstr "Atualizado em"
+
+#: templates/investments/fragments/detail.html:47
+#: templates/investments/fragments/list.html:67
+msgid "Tracked by value"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:48
+msgid ""
+"Some incoming units have no linked payment, so their cost was estimated from "
+"the market price of that day"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:68
+#, fuzzy
+#| msgid "Info"
+msgid "In"
+msgstr "Informação"
+
+#: templates/investments/fragments/detail.html:74
+#: templates/investments/fragments/detail.html:78
+#, fuzzy
+#| msgid "Currency Code"
+msgid "currency effect"
+msgstr "Código da Moeda"
+
+#: templates/investments/fragments/detail.html:104
+#: templates/investments/fragments/detail.html:179
+#, fuzzy
+#| msgid "Entry Price"
+msgid "Price"
+msgstr "Preço de Entrada"
+
+#: templates/investments/fragments/detail.html:105
+#: templates/investments/fragments/detail.html:200
+#: templates/investments/fragments/detail.html:239
+#: templates/investments/fragments/detail.html:290
+#, fuzzy
+#| msgid "Warning"
+msgid "Earnings"
+msgstr "Aviso"
+
+#: templates/investments/fragments/detail.html:180
+#: templates/investments/fragments/detail.html:227
+msgid "Average cost"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:181
+#: templates/investments/fragments/rebalance.html:41
+#, fuzzy
+#| msgid "By"
+msgid "Buy"
+msgstr "Por"
+
+#: templates/investments/fragments/detail.html:182
+#: templates/investments/fragments/rebalance.html:43
+msgid "Sell"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:227
+#: templates/investments/fragments/detail.html:257
+msgid "Put in / taken out"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:239
+#, fuzzy
+#| msgid "Uncategorized"
+msgid "Unrealized"
+msgstr "Sem categoria"
+
+#: templates/investments/fragments/detail.html:248
+msgid "Realized"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:271
+#, fuzzy
+#| msgid "Management"
+msgid "Movements"
+msgstr "Gerenciar"
+
+#: templates/investments/fragments/detail.html:284
+msgid "Bought / contributed"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:284
+msgid "Sold / withdrew"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:288
+msgid "Money in"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:288
+msgid "Money out"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:290
+msgid "Gain"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:290
+msgid "Loss, tax or fee"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:315
+#, fuzzy
+#| msgid "No presets yet"
+msgid "No movements yet"
+msgstr "Nenhuma predefinição de importação ainda"
+
+#: templates/investments/fragments/detail.html:315
+msgid "Use the + button to buy or contribute"
+msgstr ""
+
+#: templates/investments/fragments/list.html:21
+msgid "Holdings"
+msgstr ""
+
+#: templates/investments/fragments/list.html:60
+msgid "Some costs were estimated from market prices"
+msgstr ""
+
+#: templates/investments/fragments/list.html:92
+msgid "Charts"
+msgstr ""
+
+#: templates/investments/fragments/list.html:97
+#, fuzzy
+#| msgid "New balance"
+msgid "Rebalance"
+msgstr "Novo saldo"
+
+#: templates/investments/fragments/list.html:105
+#, fuzzy
+#| msgid "No presets yet"
+msgid "No investments yet"
+msgstr "Nenhuma predefinição de importação ainda"
+
+#: templates/investments/fragments/list.html:106
+msgid "Mark an account as an investment in its settings to see it here"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:4
+msgid "No allocation targets"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:5
+msgid "Set a target for your asset classes to see what to buy and sell"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:26
+msgid "To reach the target"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:34
+msgid "Off target"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:55
+msgid "Left out: investments without an asset class or a target"
+msgstr ""
+
#: templates/layouts/base.html:38
msgid "This is a demo!"
msgstr "Isto é uma demonstração!"
@@ -3665,11 +4144,6 @@ msgstr "Filtrar transações"
msgid "Order by"
msgstr "Ordernar por"
-#: templates/net_worth/net_worth.html:52
-#: templates/yearly_overview/pages/overview_by_currency.html:6
-msgid "By currency"
-msgstr "Por moeda"
-
#: templates/net_worth/net_worth.html:135
msgid "Evolution"
msgstr "Evolução"
@@ -4123,6 +4597,12 @@ msgstr "Login com"
msgid "Yearly Overview"
msgstr "Visão Anual"
+#~ msgid ""
+#~ "Create one exchange rate and keep updating it. Avoids database clutter."
+#~ msgstr ""
+#~ "Cria uma taxa de câmbio e mantenha-a atualizada. Evita a poluição do "
+#~ "banco de dados."
+
#~ msgid "Add as quick transaction"
#~ msgstr "Adicionar como transação rápida"
diff --git a/app/locale/ru/LC_MESSAGES/django.po b/app/locale/ru/LC_MESSAGES/django.po
index 31f9fb87..462a4df4 100644
--- a/app/locale/ru/LC_MESSAGES/django.po
+++ b/app/locale/ru/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-11-08 06:01+0000\n"
+"POT-Creation-Date: 2026-11-08 18:27+0000\n"
"PO-Revision-Date: 2026-05-01 07:24+0000\n"
"Last-Translator: masttera \n"
"Language-Team: Russian =20) ? 1 : 2;\n"
"X-Generator: Weblate 5.17\n"
-#: apps/accounts/forms.py:28
+#: apps/accounts/forms.py:33
msgid "Group name"
msgstr "Имя группы"
-#: apps/accounts/forms.py:43 apps/accounts/forms.py:109
-#: apps/currencies/forms.py:57 apps/currencies/forms.py:91
-#: apps/currencies/forms.py:166 apps/dca/forms.py:46 apps/dca/forms.py:205
-#: apps/import_app/forms.py:32 apps/rules/forms.py:60 apps/rules/forms.py:100
-#: apps/rules/forms.py:385 apps/transactions/forms.py:219
-#: apps/transactions/forms.py:452 apps/transactions/forms.py:571
-#: apps/transactions/forms.py:718 apps/transactions/forms.py:940
-#: apps/transactions/forms.py:987 apps/transactions/forms.py:1015
-#: apps/transactions/forms.py:1046 apps/transactions/forms.py:1208
-#: apps/users/forms.py:264 apps/users/forms.py:422
+#: apps/accounts/forms.py:48 apps/accounts/forms.py:76
+#: apps/accounts/forms.py:184 apps/currencies/forms.py:57
+#: apps/currencies/forms.py:91 apps/currencies/forms.py:168
+#: apps/dca/forms.py:46 apps/dca/forms.py:192 apps/import_app/forms.py:32
+#: apps/rules/forms.py:60 apps/rules/forms.py:100 apps/rules/forms.py:385
+#: apps/transactions/forms.py:219 apps/transactions/forms.py:452
+#: apps/transactions/forms.py:571 apps/transactions/forms.py:718
+#: apps/transactions/forms.py:940 apps/transactions/forms.py:987
+#: apps/transactions/forms.py:1015 apps/transactions/forms.py:1046
+#: apps/transactions/forms.py:1208 apps/users/forms.py:264
+#: apps/users/forms.py:422
#: templates/rules/fragments/transaction_rule/dry_run/updated.html:5
#: templates/rules/fragments/transaction_rule/view.html:128
msgid "Update"
msgstr "Обновить"
-#: apps/accounts/forms.py:49 apps/accounts/forms.py:115
-#: apps/common/widgets/tom_select.py:13 apps/currencies/forms.py:63
-#: apps/currencies/forms.py:97 apps/currencies/forms.py:172
-#: apps/dca/forms.py:52 apps/dca/forms.py:211 apps/import_app/forms.py:38
-#: apps/rules/forms.py:66 apps/rules/forms.py:106 apps/rules/forms.py:391
-#: apps/transactions/forms.py:206 apps/transactions/forms.py:226
-#: apps/transactions/forms.py:459 apps/transactions/forms.py:946
-#: apps/transactions/forms.py:993 apps/transactions/forms.py:1021
-#: apps/transactions/forms.py:1052 apps/transactions/forms.py:1214
-#: apps/users/forms.py:270 apps/users/forms.py:428
-#: templates/mini_tools/simulator.html:177
+#: apps/accounts/forms.py:54 apps/accounts/forms.py:76
+#: apps/accounts/forms.py:190 apps/common/widgets/tom_select.py:15
+#: apps/currencies/forms.py:63 apps/currencies/forms.py:97
+#: apps/currencies/forms.py:174 apps/dca/forms.py:52 apps/dca/forms.py:198
+#: apps/import_app/forms.py:38 apps/rules/forms.py:66 apps/rules/forms.py:106
+#: apps/rules/forms.py:391 apps/transactions/forms.py:206
+#: apps/transactions/forms.py:226 apps/transactions/forms.py:459
+#: apps/transactions/forms.py:946 apps/transactions/forms.py:993
+#: apps/transactions/forms.py:1021 apps/transactions/forms.py:1052
+#: apps/transactions/forms.py:1214 apps/users/forms.py:270
+#: apps/users/forms.py:428 templates/mini_tools/simulator.html:177
#: templates/mini_tools/unit_price_calculator.html:168
msgid "Add"
msgstr "Добавить"
-#: apps/accounts/forms.py:57 templates/accounts/fragments/list.html:23
+#: apps/accounts/forms.py:86 templates/accounts/fragments/list.html:23
msgid "Group"
msgstr "Группа"
-#: apps/accounts/forms.py:123
+#: apps/accounts/forms.py:93 apps/accounts/models.py:73
+#: apps/accounts/models.py:130
+#: templates/investments/fragments/rebalance.html:22
+msgid "Asset class"
+msgstr ""
+
+#: apps/accounts/forms.py:198
msgid "New balance"
msgstr "Новый баланс"
-#: apps/accounts/forms.py:129 apps/dca/forms.py:79 apps/dca/forms.py:86
+#: apps/accounts/forms.py:204 apps/dca/forms.py:79 apps/dca/forms.py:86
#: apps/insights/forms.py:117 apps/insights/views.py:44 apps/rules/forms.py:181
#: apps/rules/forms.py:197 apps/rules/models.py:44 apps/rules/models.py:311
#: apps/transactions/forms.py:62 apps/transactions/forms.py:330
#: apps/transactions/forms.py:510 apps/transactions/forms.py:607
#: apps/transactions/forms.py:614 apps/transactions/forms.py:823
#: apps/transactions/forms.py:1075 apps/transactions/models.py:357
-#: apps/transactions/models.py:743 apps/transactions/models.py:999
-#: apps/transactions/models.py:1306
+#: apps/transactions/models.py:767 apps/transactions/models.py:1023
+#: apps/transactions/models.py:1330
#: templates/insights/fragments/month_by_month.html:84
#: templates/insights/fragments/year_by_year.html:52
msgid "Category"
msgstr "Категория"
-#: apps/accounts/forms.py:136 apps/dca/forms.py:95 apps/dca/forms.py:103
-#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
+#: apps/accounts/forms.py:211 apps/dca/forms.py:95 apps/dca/forms.py:103
+#: apps/export_app/forms.py:43 apps/export_app/forms.py:134
#: apps/insights/views.py:50 apps/rules/forms.py:184 apps/rules/forms.py:194
#: apps/rules/models.py:45 apps/rules/models.py:315
#: apps/transactions/filters.py:79 apps/transactions/forms.py:70
#: apps/transactions/forms.py:338 apps/transactions/forms.py:518
#: apps/transactions/forms.py:623 apps/transactions/forms.py:631
#: apps/transactions/forms.py:816 apps/transactions/forms.py:1068
-#: apps/transactions/models.py:363 apps/transactions/models.py:745
-#: apps/transactions/models.py:1003 apps/transactions/models.py:1312
-#: templates/includes/sidebar.html:156
+#: apps/transactions/models.py:363 apps/transactions/models.py:769
+#: apps/transactions/models.py:1027 apps/transactions/models.py:1336
+#: templates/includes/sidebar.html:162
#: templates/insights/fragments/month_by_month.html:29
#: templates/insights/fragments/month_by_month.html:32
#: templates/insights/fragments/year_by_year.html:24
@@ -95,31 +102,31 @@ msgstr "Категория"
msgid "Tags"
msgstr "Тэг"
-#: apps/accounts/forms.py:179
+#: apps/accounts/forms.py:254
#, fuzzy
#| msgid "All Transactions"
msgid "All paid transactions"
msgstr "Все транзакции"
-#: apps/accounts/forms.py:180
+#: apps/accounts/forms.py:255
#, fuzzy
#| msgid "Update"
msgid "Up to date"
msgstr "Обновить"
-#: apps/accounts/forms.py:181
+#: apps/accounts/forms.py:256
#, fuzzy
#| msgid "Move to next month"
msgid "Up to reference month"
msgstr "Перенести на следующий месяц"
-#: apps/accounts/forms.py:185
+#: apps/accounts/forms.py:260
#, fuzzy
#| msgid "Balance"
msgid "Balance against"
msgstr "Баланс"
-#: apps/accounts/forms.py:189 apps/currencies/forms.py:70
+#: apps/accounts/forms.py:264 apps/currencies/forms.py:70
#: apps/dca/models.py:158 apps/rules/forms.py:176 apps/rules/forms.py:190
#: apps/rules/models.py:38 apps/rules/models.py:279
#: apps/transactions/forms.py:86 apps/transactions/forms.py:482
@@ -130,16 +137,17 @@ msgstr "Баланс"
msgid "Date"
msgstr "Дата"
-#: apps/accounts/forms.py:194
+#: apps/accounts/forms.py:269
#, fuzzy
#| msgid "Reference Date"
msgid "Reference month"
msgstr "Дата ссылки"
-#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
+#: apps/accounts/models.py:19 apps/accounts/models.py:36
+#: apps/accounts/models.py:84 apps/dca/models.py:13
#: apps/import_app/models.py:20 apps/rules/models.py:13
#: apps/transactions/models.py:249 apps/transactions/models.py:274
-#: apps/transactions/models.py:298 apps/transactions/models.py:1274
+#: apps/transactions/models.py:298 apps/transactions/models.py:1298
#: apps/users/models.py:577 templates/account_groups/fragments/list.html:22
#: templates/accounts/fragments/list.html:22
#: templates/categories/fragments/table.html:17
@@ -148,6 +156,7 @@ msgstr "Дата ссылки"
#: templates/exchange_rates_services/fragments/list.html:28
#: templates/import_app/fragments/profiles/list.html:34
#: templates/installment_plans/fragments/table.html:16
+#: templates/investment_classes/fragments/list.html:27
#: templates/quick_transactions/fragments/list.html:13
#: templates/recurring_transactions/fragments/table.html:18
#: templates/rules/fragments/list.html:24
@@ -156,44 +165,129 @@ msgstr "Дата ссылки"
msgid "Name"
msgstr "Имя"
-#: apps/accounts/models.py:18 apps/accounts/models.py:33
+#: apps/accounts/models.py:25 apps/accounts/models.py:88
msgid "Account Group"
msgstr "Группа счетов"
-#: apps/accounts/models.py:19 templates/account_groups/fragments/list.html:9
+#: apps/accounts/models.py:26 templates/account_groups/fragments/list.html:9
#: templates/account_groups/pages/index.html:4
-#: templates/includes/sidebar.html:176
+#: templates/includes/sidebar.html:182
msgid "Account Groups"
msgstr "Группы счетов"
-#: apps/accounts/models.py:39 apps/currencies/models.py:44
+#: apps/accounts/models.py:43
+msgid "Target allocation (%)"
+msgstr ""
+
+#: apps/accounts/models.py:45
+msgid ""
+"Share of your investments you want in this class. Leave it empty to leave "
+"the class out of the targets."
+msgstr ""
+
+#: apps/accounts/models.py:61
+msgid "You already have an asset class with this name."
+msgstr ""
+
+#: apps/accounts/models.py:66
+#, python-format
+msgid ""
+"Targets can't add up to more than 100%%. Other classes already take "
+"%(taken)s%%."
+msgstr ""
+
+#: apps/accounts/models.py:74 apps/export_app/forms.py:131
+#: templates/includes/sidebar.html:190
+#: templates/investment_classes/fragments/list.html:9
+#: templates/investment_classes/pages/index.html:4
+#: templates/investments/fragments/rebalance.html:6
+#: templates/investments/fragments/rebalance.html:11
+msgid "Asset classes"
+msgstr ""
+
+#: apps/accounts/models.py:94 apps/currencies/models.py:44
#: templates/accounts/fragments/list.html:24
#: templates/mini_tools/simulator.html:37
#: templates/yearly_overview/pages/yearly_overview_base.html:19
msgid "Currency"
msgstr "Валюта"
-#: apps/accounts/models.py:45 apps/currencies/models.py:27
+#: apps/accounts/models.py:100 apps/currencies/models.py:27
#: templates/accounts/fragments/list.html:25
msgid "Exchange Currency"
msgstr "Обмен валюты"
-#: apps/accounts/models.py:50 apps/currencies/models.py:32
+#: apps/accounts/models.py:105 apps/currencies/models.py:32
msgid "Default currency for exchange calculations"
msgstr "Валюта по умолчанию для расчетов обмена валют"
-#: apps/accounts/models.py:55
+#: apps/accounts/models.py:110
msgid "Asset account"
msgstr "Счет активов"
-#: apps/accounts/models.py:57
+#: apps/accounts/models.py:112
msgid ""
"Asset accounts count towards your Net Worth, but not towards your month."
msgstr ""
"Активы, находящиеся на счетах, учитываются при расчете вашего чистого "
"капитала, но не в ежемесячном исчислении."
-#: apps/accounts/models.py:62 apps/currencies/models.py:37
+#: apps/accounts/models.py:117 templates/investments/fragments/list.html:46
+#, fuzzy
+#| msgid "Installment"
+msgid "Investment"
+msgstr "Рассрочка"
+
+#: apps/accounts/models.py:119
+msgid ""
+"Show this account in the Investments tracker. Set an exchange currency to "
+"price it by units (stocks, ETFs, crypto); leave it empty to track it by "
+"value (fixed income, property, pensions)."
+msgstr ""
+
+#: apps/accounts/models.py:132
+msgid ""
+"Groups investments in the allocation charts, e.g. Fixed income, Stocks, Real "
+"estate."
+msgstr ""
+
+#: apps/accounts/models.py:140
+#, fuzzy
+#| msgid "No entities"
+msgid "Income entities"
+msgstr "Нет объектов"
+
+#: apps/accounts/models.py:142
+msgid ""
+"Transactions with any of these entities count towards this investment's "
+"return: in this account, and income paid into other accounts (dividends, "
+"rent)."
+msgstr ""
+
+#: apps/accounts/models.py:151
+#, fuzzy
+#| msgid "No categories"
+msgid "Income categories"
+msgstr "Категории отсутствуют"
+
+#: apps/accounts/models.py:153
+msgid ""
+"Transactions in this account with any of these categories are its result "
+"(interest, taxes, fees). On accounts tracked by value, everything else that "
+"isn't a transfer counts as money coming in or going out."
+msgstr ""
+
+#: apps/accounts/models.py:162
+#, fuzzy
+#| msgid "Income"
+msgid "Income tags"
+msgstr "Доход"
+
+#: apps/accounts/models.py:164
+msgid "Same as income categories, for transactions with any of these tags."
+msgstr ""
+
+#: apps/accounts/models.py:169 apps/currencies/models.py:37
#: templates/accounts/fragments/list.html:27
#: templates/categories/fragments/list.html:20
#: templates/currencies/fragments/list.html:24
@@ -202,19 +296,19 @@ msgstr ""
msgid "Archived"
msgstr "Архив"
-#: apps/accounts/models.py:63
+#: apps/accounts/models.py:170
msgid "Archived accounts don't show up nor count towards your net worth"
msgstr ""
"Архивированные счета не отображаются и не учитываются при расчете вашего "
"чистого капитала"
-#: apps/accounts/models.py:75 apps/rules/forms.py:173 apps/rules/forms.py:187
+#: apps/accounts/models.py:182 apps/rules/forms.py:173 apps/rules/forms.py:187
#: apps/rules/models.py:35 apps/rules/models.py:267
#: apps/transactions/forms.py:82 apps/transactions/forms.py:350
#: apps/transactions/forms.py:477 apps/transactions/forms.py:808
#: apps/transactions/forms.py:1060 apps/transactions/models.py:329
-#: apps/transactions/models.py:703 apps/transactions/models.py:981
-#: apps/transactions/models.py:1280
+#: apps/transactions/models.py:727 apps/transactions/models.py:1005
+#: apps/transactions/models.py:1304
#: templates/installment_plans/fragments/table.html:17
#: templates/quick_transactions/fragments/list.html:14
#: templates/recurring_transactions/fragments/table.html:19
@@ -222,17 +316,17 @@ msgstr ""
msgid "Account"
msgstr "Счет"
-#: apps/accounts/models.py:76 apps/export_app/forms.py:19
-#: apps/export_app/forms.py:129 apps/transactions/filters.py:63
+#: apps/accounts/models.py:183 apps/export_app/forms.py:19
+#: apps/export_app/forms.py:130 apps/transactions/filters.py:63
#: templates/accounts/fragments/list.html:9
-#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:168
-#: templates/includes/sidebar.html:170
+#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:174
+#: templates/includes/sidebar.html:176
#: templates/monthly_overview/pages/overview.html:77
#: templates/transactions/pages/transactions.html:26
msgid "Accounts"
msgstr "Счета"
-#: apps/accounts/models.py:93
+#: apps/accounts/models.py:200
msgid "Exchange currency cannot be the same as the account's main currency."
msgstr "Обменная валюта не может совпадать с основной валютой счета."
@@ -250,7 +344,7 @@ msgstr "Группа счетов успешно удалена"
#: apps/accounts/views/account_groups.py:109
#: apps/accounts/views/accounts.py:137 apps/dca/views.py:106
-#: apps/import_app/views.py:147 apps/rules/views.py:191
+#: apps/import_app/views.py:148 apps/rules/views.py:191
#: apps/transactions/views/categories.py:163
#: apps/transactions/views/entities.py:135 apps/transactions/views/tags.py:131
msgid "Item no longer shared with you"
@@ -258,7 +352,7 @@ msgstr "Информация больше не предоставляется в
#: apps/accounts/views/account_groups.py:132
#: apps/accounts/views/accounts.py:182 apps/dca/views.py:131
-#: apps/import_app/views.py:273 apps/rules/views.py:217
+#: apps/import_app/views.py:274 apps/rules/views.py:217
#: apps/transactions/views/categories.py:188
#: apps/transactions/views/entities.py:160 apps/transactions/views/tags.py:154
msgid "Ownership taken successfully"
@@ -266,7 +360,7 @@ msgstr "Владелец успешно присвоен"
#: apps/accounts/views/account_groups.py:152
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:151
-#: apps/import_app/views.py:294 apps/rules/views.py:238
+#: apps/import_app/views.py:295 apps/rules/views.py:238
#: apps/transactions/views/categories.py:130
#: apps/transactions/views/entities.py:180 apps/transactions/views/tags.py:174
msgid "Configuration saved successfully"
@@ -324,7 +418,7 @@ msgstr "Объект с таким идентификатором не суще
msgid "Invalid entity data. Provide an ID or name."
msgstr "Неверные данные сущности. Укажите идентификатор или имя."
-#: apps/api/serializers/transactions.py:141 apps/transactions/models.py:953
+#: apps/api/serializers/transactions.py:141 apps/transactions/models.py:977
msgid "This field is required."
msgstr ""
@@ -345,13 +439,14 @@ msgstr "Сделать приватным"
msgid "Shared with users"
msgstr "Доступно пользователям"
-#: apps/common/fields/forms/dynamic_select.py:71
-#: apps/common/fields/forms/dynamic_select.py:142
+#: apps/common/fields/forms/dynamic_select.py:79
+#: apps/common/fields/forms/dynamic_select.py:150
msgid "Error creating new instance"
msgstr "Ошибка при создании нового экземпляра"
#: apps/common/fields/forms/grouped_select.py:24
-#: apps/common/widgets/tom_select.py:92 apps/common/widgets/tom_select.py:95
+#: apps/common/widgets/tom_select.py:94 apps/common/widgets/tom_select.py:97
+#: templates/investments/fragments/list.html:26
msgid "Ungrouped"
msgstr "Негруппированный"
@@ -506,14 +601,15 @@ msgid "Today"
msgstr "Сегодня"
#: apps/common/widgets/datepicker.py:142
+#: templates/investments/fragments/rebalance.html:24
msgid "Now"
msgstr "Сейчас"
-#: apps/common/widgets/tom_select.py:11
+#: apps/common/widgets/tom_select.py:13
msgid "Remove"
msgstr "Снять"
-#: apps/common/widgets/tom_select.py:15 templates/mini_tools/simulator.html:187
+#: apps/common/widgets/tom_select.py:17 templates/mini_tools/simulator.html:187
#: templates/mini_tools/unit_price_calculator.html:180
#: templates/monthly_overview/pages/overview.html:286
#: templates/transactions/fragments/filter_actions.html:48
@@ -521,7 +617,7 @@ msgstr "Снять"
msgid "Clear"
msgstr "Очистить"
-#: apps/common/widgets/tom_select.py:16
+#: apps/common/widgets/tom_select.py:18
msgid "No results..."
msgstr "Нет результатов..."
@@ -555,10 +651,10 @@ msgid "Decimal Places"
msgstr "Десятичные знаки"
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
-#: apps/export_app/forms.py:130 apps/transactions/filters.py:70
+#: apps/export_app/forms.py:132 apps/transactions/filters.py:70
#: templates/currencies/fragments/list.html:9
-#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:182
-#: templates/includes/sidebar.html:184
+#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:196
+#: templates/includes/sidebar.html:198
#: templates/monthly_overview/pages/overview.html:63
#: templates/transactions/pages/transactions.html:12
msgid "Currencies"
@@ -590,9 +686,9 @@ msgid "Auto"
msgstr "Авто"
#: apps/currencies/models.py:82 apps/export_app/forms.py:67
-#: apps/export_app/forms.py:142 templates/exchange_rates/fragments/list.html:10
+#: apps/export_app/forms.py:144 templates/exchange_rates/fragments/list.html:10
#: templates/exchange_rates/pages/index.html:4
-#: templates/includes/sidebar.html:190
+#: templates/includes/sidebar.html:204
msgid "Exchange Rates"
msgstr "Обменные курсы"
@@ -686,33 +782,46 @@ msgstr ""
"валюты."
#: apps/currencies/models.py:171
-msgid "Single exchange rate"
+#, fuzzy
+#| msgid "Single exchange rate"
+msgid "One exchange rate per day"
msgstr "Единый обменный курс"
#: apps/currencies/models.py:174
-msgid "Create one exchange rate and keep updating it. Avoids database clutter."
+msgid ""
+"Update the day's exchange rate on each fetch instead of adding a new one."
msgstr ""
-"Создайте один обменный курс и постоянно обновляйте его. Это позволит "
-"избежать захламления базы данных."
-#: apps/currencies/models.py:179
+#: apps/currencies/models.py:178
+#, fuzzy
+#| msgid "Exchange Rates"
+msgid "Exchange rates to keep"
+msgstr "Обменные курсы"
+
+#: apps/currencies/models.py:183
+msgid ""
+"Keep at most this many automatic exchange rates per currency pair, deleting "
+"the oldest first. Leave empty to keep all."
+msgstr ""
+
+#: apps/currencies/models.py:188
msgid "Exchange Rate Service"
msgstr "Сервис обмена курсов"
-#: apps/currencies/models.py:180
+#: apps/currencies/models.py:189
msgid "Exchange Rate Services"
msgstr "Сервисы обмена курсов"
-#: apps/currencies/models.py:234
+#: apps/currencies/models.py:243
msgid "'Every X hours' interval type requires a positive integer."
msgstr ""
"Для интервалов типа «Каждые X часов» требуется положительное целое число."
-#: apps/currencies/models.py:243
+#: apps/currencies/models.py:252
msgid "'Every X hours' interval must be between 1 and 24."
msgstr "Интервал «каждые X часов» должен составлять от 1 до 24."
-#: apps/currencies/models.py:257
+#: apps/currencies/models.py:266
msgid ""
"Invalid hour format. Use comma-separated hours (0-23) and/or ranges (e.g., "
"'1-5,8,10-12')."
@@ -720,7 +829,7 @@ msgstr ""
"Неверный формат времени. Используйте часы, разделенные запятыми (0-23), и/"
"или диапазоны (например, '1-5,8,10-12')."
-#: apps/currencies/models.py:268
+#: apps/currencies/models.py:277
msgid ""
"Invalid format. Please check the requirements for your selected interval "
"type."
@@ -792,21 +901,21 @@ msgstr "Введите текст для поиска транзакции, чт
msgid "Income Transaction"
msgstr "Доходная транзакция"
-#: apps/dca/forms.py:192
+#: apps/dca/forms.py:179
msgid "Link transaction"
msgstr "Ссылка на транзакцию"
-#: apps/dca/forms.py:275 apps/dca/forms.py:276 apps/dca/forms.py:281
-#: apps/dca/forms.py:285
+#: apps/dca/forms.py:262 apps/dca/forms.py:263 apps/dca/forms.py:268
+#: apps/dca/forms.py:272
msgid "You must provide an account."
msgstr "Вы должны предоставить учетную запись."
-#: apps/dca/forms.py:290 apps/transactions/forms.py:752
-#: apps/transactions/models.py:956
+#: apps/dca/forms.py:277 apps/transactions/forms.py:752
+#: apps/transactions/models.py:980
msgid "From and To accounts must be different."
msgstr "Счета «От» и «Получатель» должны быть разными."
-#: apps/dca/forms.py:304
+#: apps/dca/forms.py:299
#, python-format
msgid "DCA for %(strategy_name)s"
msgstr ""
@@ -822,8 +931,8 @@ msgstr "Валюта платежа"
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
#: apps/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
#: apps/transactions/forms.py:504 apps/transactions/forms.py:651
-#: apps/transactions/models.py:353 apps/transactions/models.py:752
-#: apps/transactions/models.py:1009 apps/transactions/models.py:1302
+#: apps/transactions/models.py:353 apps/transactions/models.py:776
+#: apps/transactions/models.py:1033 apps/transactions/models.py:1326
msgid "Notes"
msgstr "Заметки"
@@ -831,7 +940,7 @@ msgstr "Заметки"
msgid "DCA Strategy"
msgstr "Стратегия DCA"
-#: apps/dca/models.py:35 apps/export_app/forms.py:150
+#: apps/dca/models.py:35 apps/export_app/forms.py:152
msgid "DCA Strategies"
msgstr "Стратегии DCA"
@@ -851,7 +960,7 @@ msgstr "Полученная сумма"
msgid "DCA Entry"
msgstr ""
-#: apps/dca/models.py:187 apps/export_app/forms.py:151
+#: apps/dca/models.py:187 apps/export_app/forms.py:153
msgid "DCA Entries"
msgstr "Записи DCA"
@@ -879,25 +988,25 @@ msgstr "Запись успешно обновлена"
msgid "Entry deleted successfully"
msgstr "Запись успешно удалена"
-#: apps/export_app/forms.py:13 apps/export_app/forms.py:128
-#: templates/includes/sidebar.html:227 templates/users/fragments/list.html:10
+#: apps/export_app/forms.py:13 apps/export_app/forms.py:129
+#: templates/includes/sidebar.html:241 templates/users/fragments/list.html:10
#: templates/users/pages/index.html:4
msgid "Users"
msgstr "Пользователи"
-#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
+#: apps/export_app/forms.py:31 apps/export_app/forms.py:136
#: apps/transactions/models.py:422 templates/includes/sidebar.html:81
-#: templates/includes/sidebar.html:148
+#: templates/includes/sidebar.html:154
#: 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/export_app/forms.py:37 apps/export_app/forms.py:131
+#: apps/export_app/forms.py:37 apps/export_app/forms.py:133
#: apps/insights/views.py:43 apps/transactions/filters.py:74
#: templates/categories/fragments/list.html:9
-#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:150
+#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:156
#: templates/insights/fragments/month_by_month.html:18
#: templates/insights/fragments/month_by_month.html:21
#: templates/insights/fragments/year_by_year.html:13
@@ -905,16 +1014,16 @@ msgstr "Транзакции"
msgid "Categories"
msgstr "Категории"
-#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
+#: apps/export_app/forms.py:49 apps/export_app/forms.py:135
#: apps/insights/views.py:57 apps/rules/forms.py:185 apps/rules/forms.py:195
#: apps/rules/models.py:46 apps/rules/models.py:307
#: apps/transactions/filters.py:84 apps/transactions/forms.py:78
#: apps/transactions/forms.py:346 apps/transactions/forms.py:526
#: apps/transactions/forms.py:831 apps/transactions/forms.py:1083
#: apps/transactions/models.py:312 apps/transactions/models.py:368
-#: apps/transactions/models.py:748 apps/transactions/models.py:1006
-#: apps/transactions/models.py:1317 templates/entities/fragments/list.html:9
-#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:162
+#: apps/transactions/models.py:772 apps/transactions/models.py:1030
+#: apps/transactions/models.py:1341 templates/entities/fragments/list.html:9
+#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:168
#: templates/insights/fragments/month_by_month.html:40
#: templates/insights/fragments/month_by_month.html:43
#: templates/insights/fragments/year_by_year.html:35
@@ -922,28 +1031,28 @@ msgstr "Категории"
msgid "Entities"
msgstr "Объекты"
-#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
-#: apps/transactions/models.py:1046 templates/includes/sidebar.html:110
+#: apps/export_app/forms.py:55 apps/export_app/forms.py:139
+#: apps/transactions/models.py:1070 templates/includes/sidebar.html:110
#: templates/recurring_transactions/fragments/list.html:9
#: templates/recurring_transactions/pages/index.html:4
msgid "Recurring Transactions"
msgstr "Повторяющиеся транзакции"
-#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
-#: apps/transactions/models.py:766 templates/includes/sidebar.html:104
+#: apps/export_app/forms.py:61 apps/export_app/forms.py:137
+#: apps/transactions/models.py:790 templates/includes/sidebar.html:104
#: templates/installment_plans/fragments/list.html:9
#: templates/installment_plans/pages/index.html:4
msgid "Installment Plans"
msgstr "Планы рассрочки"
-#: apps/export_app/forms.py:73 apps/export_app/forms.py:140
+#: apps/export_app/forms.py:73 apps/export_app/forms.py:142
#: templates/exchange_rates_services/fragments/list.html:10
#: templates/exchange_rates_services/pages/index.html:4
-#: templates/includes/sidebar.html:218
+#: templates/includes/sidebar.html:232
msgid "Automatic Exchange Rates"
msgstr "Автоматические обменные курсы"
-#: apps/export_app/forms.py:79 templates/includes/sidebar.html:198
+#: apps/export_app/forms.py:79 templates/includes/sidebar.html:212
#: templates/rules/fragments/list.html:9 templates/rules/pages/index.html:4
msgid "Rules"
msgstr "Правила"
@@ -952,57 +1061,57 @@ msgstr "Правила"
msgid "DCA"
msgstr ""
-#: apps/export_app/forms.py:91 apps/export_app/forms.py:152
+#: apps/export_app/forms.py:91 apps/export_app/forms.py:154
#: templates/import_app/fragments/profiles/list.html:21
#: templates/import_app/pages/profiles_index.html:4
msgid "Import Profiles"
msgstr "Импорт профилей"
-#: apps/export_app/forms.py:117 templates/export_app/fragments/export.html:5
+#: apps/export_app/forms.py:118 templates/export_app/fragments/export.html:5
#: templates/export_app/pages/index.html:13
msgid "Export"
msgstr "Экспорт"
-#: apps/export_app/forms.py:125
+#: apps/export_app/forms.py:126
msgid "Import a ZIP file exported from WYGIWYH"
msgstr "Импорт ZIP-файла, экспортированного из WYGIWYH"
-#: apps/export_app/forms.py:126
+#: apps/export_app/forms.py:127
msgid "ZIP File"
msgstr "ZIP File"
-#: apps/export_app/forms.py:143 apps/rules/models.py:27
+#: apps/export_app/forms.py:145 apps/rules/models.py:27
msgid "Transaction rules"
msgstr "Правила транзакций"
-#: apps/export_app/forms.py:145 apps/rules/models.py:68
+#: apps/export_app/forms.py:147 apps/rules/models.py:68
msgid "Edit transaction action"
msgstr "Редактировать действие транзакции"
-#: apps/export_app/forms.py:148 apps/rules/models.py:327
+#: apps/export_app/forms.py:150 apps/rules/models.py:327
msgid "Update or create transaction actions"
msgstr "Обновление или создание действий транзакции"
-#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:341
+#: apps/export_app/forms.py:183 templates/cotton/transaction/item.html:341
#: templates/cotton/ui/deleted_transactions_action_bar.html:53
#: templates/export_app/fragments/restore.html:5
#: templates/export_app/pages/index.html:19
msgid "Restore"
msgstr "Восстановить"
-#: apps/export_app/forms.py:191
+#: apps/export_app/forms.py:193
msgid "Please upload either a ZIP file or at least one CSV file"
msgstr "Пожалуйста, загрузите либо ZIP-файл, либо хотя бы один CSV-файл"
-#: apps/export_app/views.py:177
+#: apps/export_app/views.py:183
msgid "You have to select at least one export"
msgstr "Необходимо выбрать как минимум один экспорт"
-#: apps/export_app/views.py:197
+#: apps/export_app/views.py:203
msgid "Data restored successfully"
msgstr "Данные успешно восстановлены"
-#: apps/export_app/views.py:209
+#: apps/export_app/views.py:215
msgid ""
"There was an error restoring your data. Check the logs for more details."
msgstr ""
@@ -1015,7 +1124,7 @@ msgstr "Выберите файл"
#: apps/import_app/forms.py:55
#: templates/import_app/fragments/profiles/list.html:75
-#: templates/includes/sidebar.html:204
+#: templates/includes/sidebar.html:218
msgid "Import"
msgstr "Импорт"
@@ -1063,23 +1172,23 @@ msgstr "Статус"
msgid "File name"
msgstr "Имя файла"
-#: apps/import_app/views.py:75
+#: apps/import_app/views.py:76
msgid "Import Profile added successfully"
msgstr "Профиль импорта успешно добавлен"
-#: apps/import_app/views.py:113
+#: apps/import_app/views.py:114
msgid "Import Profile update successfully"
msgstr "Обновление профиля импорта прошло успешно"
-#: apps/import_app/views.py:142
+#: apps/import_app/views.py:143
msgid "Import Profile deleted successfully"
msgstr "Импорт профиля успешно удален"
-#: apps/import_app/views.py:222
+#: apps/import_app/views.py:223
msgid "Import Run queued successfully"
msgstr "Операция импорта успешно поставлена в очередь"
-#: apps/import_app/views.py:249
+#: apps/import_app/views.py:250
msgid "Run deleted successfully"
msgstr "Запуск успешно удален"
@@ -1148,7 +1257,7 @@ msgstr ""
msgid "Untagged"
msgstr ""
-#: apps/insights/views.py:53 apps/transactions/models.py:589
+#: apps/insights/views.py:53 apps/transactions/models.py:613
#: templates/tags/fragments/table.html:69
msgid "No tags"
msgstr "Нет тегов"
@@ -1169,6 +1278,32 @@ msgstr ""
msgid "No entities"
msgstr "Нет объектов"
+#: apps/investments/views.py:104
+msgid "Other"
+msgstr ""
+
+#: apps/investments/views.py:135
+msgid "Unclassified"
+msgstr ""
+
+#: apps/investments/views.py:267
+#, fuzzy
+#| msgid "Installment Plan added successfully"
+msgid "Asset class added successfully"
+msgstr "План рассрочки успешно добавлен"
+
+#: apps/investments/views.py:275
+#, fuzzy
+#| msgid "Tag updated successfully"
+msgid "Asset class updated successfully"
+msgstr "Тег успешно обновлен"
+
+#: apps/investments/views.py:296
+#, fuzzy
+#| msgid "Tag deleted successfully"
+msgid "Asset class deleted successfully"
+msgstr "Тег успешно удален"
+
#: apps/net_worth/views.py:137 templates/net_worth/net_worth.html:84
#: templates/net_worth/net_worth.html:85
msgid "Consolidated"
@@ -1222,8 +1357,8 @@ msgstr "Оператор"
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
#: apps/rules/models.py:271 apps/transactions/forms.py:468
-#: apps/transactions/models.py:336 apps/transactions/models.py:708
-#: apps/transactions/models.py:987 apps/transactions/models.py:1287
+#: apps/transactions/models.py:336 apps/transactions/models.py:732
+#: apps/transactions/models.py:1011 apps/transactions/models.py:1311
#: templates/mini_tools/simulator.html:63
msgid "Type"
msgstr ""
@@ -1231,7 +1366,7 @@ msgstr ""
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
#: apps/rules/models.py:275 apps/transactions/filters.py:22
#: apps/transactions/forms.py:472 apps/transactions/models.py:338
-#: apps/transactions/models.py:1289 templates/cotton/transaction/item.html:23
+#: apps/transactions/models.py:1313 templates/cotton/transaction/item.html:23
#: templates/cotton/transaction/item.html:34
#: templates/transactions/widgets/paid_toggle_button.html:10
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
@@ -1242,14 +1377,14 @@ msgstr "Оплаченный"
#: apps/rules/models.py:283 apps/transactions/forms.py:90
#: apps/transactions/forms.py:488 apps/transactions/forms.py:638
#: apps/transactions/forms.py:837 apps/transactions/models.py:340
-#: apps/transactions/models.py:726 apps/transactions/models.py:1011
+#: apps/transactions/models.py:750 apps/transactions/models.py:1035
msgid "Reference Date"
msgstr "Дата ссылки"
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
#: apps/rules/models.py:287 apps/transactions/forms.py:495
-#: apps/transactions/models.py:346 apps/transactions/models.py:992
-#: apps/transactions/models.py:1295
+#: apps/transactions/models.py:346 apps/transactions/models.py:1016
+#: apps/transactions/models.py:1319
#: templates/insights/fragments/sankey.html:102
#: templates/installment_plans/fragments/table.html:18
#: templates/mini_tools/simulator.html:70
@@ -1261,27 +1396,27 @@ msgstr ""
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
#: apps/rules/models.py:42 apps/rules/models.py:291
#: apps/transactions/forms.py:499 apps/transactions/forms.py:642
-#: apps/transactions/models.py:351 apps/transactions/models.py:710
-#: apps/transactions/models.py:995 apps/transactions/models.py:1300
+#: apps/transactions/models.py:351 apps/transactions/models.py:734
+#: apps/transactions/models.py:1019 apps/transactions/models.py:1324
msgid "Description"
msgstr ""
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
#: apps/rules/models.py:299 apps/transactions/models.py:398
-#: apps/transactions/models.py:1322
+#: apps/transactions/models.py:1346
msgid "Internal Note"
msgstr ""
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
#: apps/rules/models.py:303 apps/transactions/models.py:400
-#: apps/transactions/models.py:1324
+#: apps/transactions/models.py:1348
msgid "Internal ID"
msgstr ""
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
#: apps/rules/models.py:319 apps/transactions/forms.py:655
#: apps/transactions/models.py:250 apps/transactions/models.py:341
-#: apps/transactions/models.py:1290
+#: apps/transactions/models.py:1314
msgid "Mute"
msgstr ""
@@ -1294,7 +1429,7 @@ msgid "Set Values"
msgstr ""
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
-#: apps/transactions/models.py:421 apps/transactions/models.py:645
+#: apps/transactions/models.py:421 apps/transactions/models.py:669
msgid "Transaction"
msgstr "Транзакция"
@@ -1330,7 +1465,12 @@ msgstr ""
msgid "Field"
msgstr ""
-#: apps/rules/models.py:61
+#: apps/rules/models.py:61 templates/investments/fragments/charts.html:86
+#: templates/investments/fragments/detail.html:103
+#: templates/investments/fragments/detail.html:162
+#: templates/investments/fragments/list.html:48
+#: templates/investments/fragments/list.html:49
+#: templates/investments/fragments/rebalance.html:23
msgid "Value"
msgstr ""
@@ -1516,7 +1656,7 @@ msgid "Upload"
msgstr "Перезагрузка"
#: apps/transactions/forms.py:355 apps/transactions/forms.py:1089
-#: apps/transactions/models.py:925
+#: apps/transactions/models.py:949
#, fuzzy
#| msgid "To Account"
msgid "To account"
@@ -1534,8 +1674,8 @@ msgstr ""
msgid "To Amount"
msgstr ""
-#: apps/transactions/forms.py:718 apps/transactions/models.py:970
-#: apps/transactions/models.py:1267 templates/cotton/transaction/item.html:52
+#: apps/transactions/forms.py:718 apps/transactions/models.py:994
+#: apps/transactions/models.py:1291 templates/cotton/transaction/item.html:52
#: templates/cotton/transaction/item.html:60
#: templates/cotton/ui/quick_transactions_buttons.html:40
#: templates/cotton/ui/transactions_fab.html:44
@@ -1598,8 +1738,8 @@ msgid ""
msgstr ""
"Деактивированные объекты нельзя будет выбрать при создании новых транзакций"
-#: apps/transactions/models.py:323 apps/transactions/models.py:969
-#: apps/transactions/models.py:1266
+#: apps/transactions/models.py:323 apps/transactions/models.py:993
+#: apps/transactions/models.py:1290
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: templates/calendar_view/fragments/list.html:52
@@ -1607,13 +1747,19 @@ msgstr ""
#: templates/cotton/ui/quick_transactions_buttons.html:10
#: templates/cotton/ui/transactions_fab.html:10
#: templates/insights/fragments/overview/_results.html:12
+#: templates/investments/fragments/_totals.html:21
+#: templates/investments/fragments/_totals.html:42
+#: templates/investments/fragments/detail.html:106
+#: templates/investments/fragments/detail.html:210
+#: templates/investments/fragments/detail.html:252
+#: templates/investments/fragments/detail.html:320
#: templates/mini_tools/simulator.html:66
#: templates/monthly_overview/fragments/monthly_summary.html:42
msgid "Income"
msgstr "Доход"
-#: apps/transactions/models.py:324 apps/transactions/models.py:971
-#: apps/transactions/models.py:1268
+#: apps/transactions/models.py:324 apps/transactions/models.py:995
+#: apps/transactions/models.py:1292
#: templates/calendar_view/fragments/list.html:46
#: templates/calendar_view/fragments/list.html:48
#: templates/calendar_view/fragments/list.html:56
@@ -1625,14 +1771,14 @@ msgstr "Доход"
msgid "Expense"
msgstr "Расход"
-#: apps/transactions/models.py:379 apps/transactions/models.py:765
+#: apps/transactions/models.py:379 apps/transactions/models.py:789
#: templates/cotton/transaction/item.html:291
#: templates/cotton/transaction/item.html:308
#: templates/cotton/transaction/item.html:318
msgid "Installment Plan"
msgstr "План рассрочки"
-#: apps/transactions/models.py:388 apps/transactions/models.py:1045
+#: apps/transactions/models.py:388 apps/transactions/models.py:1069
#: templates/cotton/transaction/item.html:281
#: templates/cotton/transaction/item.html:301
msgid "Recurring Transaction"
@@ -1652,160 +1798,172 @@ msgstr "Удалено"
msgid "Deleted At"
msgstr ""
-#: apps/transactions/models.py:591
+#: apps/transactions/models.py:517
+msgid "A transfer needs one expense and one income."
+msgstr ""
+
+#: apps/transactions/models.py:519
+msgid "A transfer needs two different accounts."
+msgstr ""
+
+#: apps/transactions/models.py:523
+msgid "This transaction is already part of another transfer."
+msgstr ""
+
+#: apps/transactions/models.py:615
msgid "No category"
msgstr "Нет категории"
-#: apps/transactions/models.py:593
+#: apps/transactions/models.py:617
msgid "No description"
msgstr "Нет описания"
-#: apps/transactions/models.py:650
+#: apps/transactions/models.py:674
#, fuzzy
#| msgid "ZIP File"
msgid "File"
msgstr "ZIP File"
-#: apps/transactions/models.py:652
+#: apps/transactions/models.py:676
msgid "Original Name"
msgstr ""
-#: apps/transactions/models.py:654
+#: apps/transactions/models.py:678
#, fuzzy
#| msgid "Interval Type"
msgid "Content Type"
msgstr "Тип интервала"
-#: apps/transactions/models.py:656
+#: apps/transactions/models.py:680
msgid "Size"
msgstr ""
-#: apps/transactions/models.py:661
+#: apps/transactions/models.py:685
msgid "Uploaded By"
msgstr ""
-#: apps/transactions/models.py:666
+#: apps/transactions/models.py:690
#, fuzzy
#| msgid "Transaction rule"
msgid "Transaction Attachment"
msgstr "Правило транзакции"
-#: apps/transactions/models.py:667
+#: apps/transactions/models.py:691
#, fuzzy
#| msgid "Transaction Tags"
msgid "Transaction Attachments"
msgstr "Теги транзакций"
-#: apps/transactions/models.py:697 templates/includes/sidebar.html:57
+#: apps/transactions/models.py:721 templates/includes/sidebar.html:57
msgid "Yearly"
msgstr "Ежегодно"
-#: apps/transactions/models.py:698 apps/users/models.py:469
+#: apps/transactions/models.py:722 apps/users/models.py:469
#: templates/includes/sidebar.html:51
msgid "Monthly"
msgstr "Ежемесячно"
-#: apps/transactions/models.py:699
+#: apps/transactions/models.py:723
msgid "Weekly"
msgstr "Еженедельно"
-#: apps/transactions/models.py:700
+#: apps/transactions/models.py:724
msgid "Daily"
msgstr "Ежедневно"
-#: apps/transactions/models.py:713
+#: apps/transactions/models.py:737
msgid "Number of Installments"
msgstr "Количество платежей"
-#: apps/transactions/models.py:718
+#: apps/transactions/models.py:742
msgid "Installment Start"
msgstr "Начало выплаты рассрочки"
-#: apps/transactions/models.py:719
+#: apps/transactions/models.py:743
msgid "The installment number to start counting from"
msgstr "Номер платежа, с которого начинается отсчет"
-#: apps/transactions/models.py:724 apps/transactions/models.py:1015
+#: apps/transactions/models.py:748 apps/transactions/models.py:1039
msgid "Start Date"
msgstr "Дата начала"
-#: apps/transactions/models.py:728 apps/transactions/models.py:1016
+#: apps/transactions/models.py:752 apps/transactions/models.py:1040
msgid "End Date"
msgstr "Дата окончания"
-#: apps/transactions/models.py:733
+#: apps/transactions/models.py:757
msgid "Recurrence"
msgstr ""
-#: apps/transactions/models.py:736
+#: apps/transactions/models.py:760
msgid "Installment Amount"
msgstr "Сумма рассрочки"
-#: apps/transactions/models.py:755 apps/transactions/models.py:1035
+#: apps/transactions/models.py:779 apps/transactions/models.py:1059
msgid "Add description to transactions"
msgstr "Добавить описание к транзакциям"
-#: apps/transactions/models.py:758 apps/transactions/models.py:1038
+#: apps/transactions/models.py:782 apps/transactions/models.py:1062
msgid "Add notes to transactions"
msgstr "Добавить примечания к транзакциям"
-#: apps/transactions/models.py:932
+#: apps/transactions/models.py:956
#, fuzzy
#| msgid "Show amounts"
msgid "To amount"
msgstr "Показать суммы"
-#: apps/transactions/models.py:933
+#: apps/transactions/models.py:957
msgid "Leave empty to use the same amount"
msgstr ""
-#: apps/transactions/models.py:974 templates/mini_tools/simulator.html:129
+#: apps/transactions/models.py:998 templates/mini_tools/simulator.html:129
msgid "day(s)"
msgstr "день"
-#: apps/transactions/models.py:975 templates/mini_tools/simulator.html:130
+#: apps/transactions/models.py:999 templates/mini_tools/simulator.html:130
msgid "week(s)"
msgstr "неделя"
-#: apps/transactions/models.py:976 templates/mini_tools/simulator.html:131
+#: apps/transactions/models.py:1000 templates/mini_tools/simulator.html:131
msgid "month(s)"
msgstr "месяц"
-#: apps/transactions/models.py:977 templates/mini_tools/simulator.html:132
+#: apps/transactions/models.py:1001 templates/mini_tools/simulator.html:132
msgid "year(s)"
msgstr "год"
-#: apps/transactions/models.py:979
+#: apps/transactions/models.py:1003
#: templates/recurring_transactions/fragments/list.html:18
msgid "Paused"
msgstr "Приостановлено"
-#: apps/transactions/models.py:1018
+#: apps/transactions/models.py:1042
msgid "Recurrence Type"
msgstr ""
-#: apps/transactions/models.py:1021
+#: apps/transactions/models.py:1045
msgid "Recurrence Interval"
msgstr ""
-#: apps/transactions/models.py:1024
+#: apps/transactions/models.py:1048
msgid "Keep at most"
msgstr "Хранить максимум"
-#: apps/transactions/models.py:1028
+#: apps/transactions/models.py:1052
msgid "Last Generated Date"
msgstr ""
-#: apps/transactions/models.py:1031
+#: apps/transactions/models.py:1055
msgid "Last Generated Reference Date"
msgstr ""
-#: apps/transactions/models.py:1334 templates/cotton/transaction/item.html:274
+#: apps/transactions/models.py:1358 templates/cotton/transaction/item.html:274
#: templates/cotton/ui/transactions_fab.html:59
msgid "Quick Transaction"
msgstr "Быстрая сделка"
-#: apps/transactions/models.py:1335 templates/includes/sidebar.html:98
+#: apps/transactions/models.py:1359 templates/includes/sidebar.html:98
#: templates/quick_transactions/pages/index.html:5
#: templates/quick_transactions/pages/index.html:15
msgid "Quick Transactions"
@@ -1934,8 +2092,8 @@ msgid "Item deleted successfully"
msgstr "Элемент успешно удален"
#: apps/transactions/views/quick_transactions.py:164
-#: apps/transactions/views/transactions.py:143
-#: apps/transactions/views/transactions.py:328
+#: apps/transactions/views/transactions.py:156
+#: apps/transactions/views/transactions.py:343
msgid "Transaction added successfully"
msgstr "Транзакция успешно добавлена"
@@ -1971,46 +2129,46 @@ msgstr "Тег успешно обновлен"
msgid "Tag deleted successfully"
msgstr "Тег успешно удален"
-#: apps/transactions/views/transactions.py:61
+#: apps/transactions/views/transactions.py:65
#, fuzzy
#| msgid "Account updated successfully"
msgid "Attachment uploaded successfully"
msgstr "Счет успешно обновлен"
-#: apps/transactions/views/transactions.py:112
+#: apps/transactions/views/transactions.py:116
#, fuzzy
#| msgid "Account deleted successfully"
msgid "Attachment deleted successfully"
msgstr "Счет успешно удален"
-#: apps/transactions/views/transactions.py:355
+#: apps/transactions/views/transactions.py:370
msgid "Transaction updated successfully"
msgstr "Транзакция успешно обновлена"
-#: apps/transactions/views/transactions.py:406
+#: apps/transactions/views/transactions.py:421
#, python-format
msgid "%(count)s transaction updated successfully"
msgid_plural "%(count)s transactions updated successfully"
msgstr[0] ""
msgstr[1] ""
-#: apps/transactions/views/transactions.py:433
+#: apps/transactions/views/transactions.py:448
msgid "Transaction duplicated successfully"
msgstr "Транзакция успешно продублирована"
-#: apps/transactions/views/transactions.py:478
+#: apps/transactions/views/transactions.py:493
msgid "Transaction deleted successfully"
msgstr "Транзакция успешно удалена"
-#: apps/transactions/views/transactions.py:508
+#: apps/transactions/views/transactions.py:523
msgid "Transaction restored successfully"
msgstr "Транзакция успешно восстановлена"
-#: apps/transactions/views/transactions.py:534
+#: apps/transactions/views/transactions.py:549
msgid "Transfer added successfully"
msgstr "Перевод успешно добавлен"
-#: apps/transactions/views/transactions.py:562
+#: apps/transactions/views/transactions.py:579
#, fuzzy
#| msgid "Transfer added successfully"
msgid "Transfer updated successfully"
@@ -2341,6 +2499,7 @@ msgstr "Редактировать группу учетных записей"
#: templates/exchange_rates_services/fragments/table.html:20
#: templates/import_app/fragments/profiles/list.html:42
#: templates/installment_plans/fragments/table.html:25
+#: templates/investment_classes/fragments/list.html:36
#: templates/quick_transactions/fragments/list.html:22
#: templates/recurring_transactions/fragments/table.html:27
#: templates/rules/fragments/list.html:31
@@ -2363,6 +2522,9 @@ msgstr ""
#: templates/exchange_rates_services/fragments/table.html:23
#: templates/import_app/fragments/profiles/list.html:46
#: templates/installment_plans/fragments/table.html:28
+#: templates/investment_classes/fragments/list.html:39
+#: templates/investments/fragments/detail.html:305
+#: templates/investments/fragments/detail.html:331
#: templates/quick_transactions/fragments/list.html:25
#: templates/recurring_transactions/fragments/table.html:30
#: templates/rules/fragments/transaction_rule/view.html:22
@@ -2412,6 +2574,7 @@ msgstr "Поделиться"
#: templates/import_app/fragments/profiles/list.html:81
#: templates/import_app/fragments/runs/list.html:100
#: templates/installment_plans/fragments/table.html:54
+#: templates/investment_classes/fragments/list.html:45
#: templates/mini_tools/simulator.html:109
#: templates/mini_tools/unit_price_calculator.html:16
#: templates/quick_transactions/fragments/list.html:32
@@ -2445,6 +2608,7 @@ msgstr "Удалить"
#: templates/import_app/fragments/runs/list.html:104
#: templates/installment_plans/fragments/table.html:47
#: templates/installment_plans/fragments/table.html:58
+#: templates/investment_classes/fragments/list.html:49
#: templates/quick_transactions/fragments/list.html:37
#: templates/recurring_transactions/fragments/table.html:52
#: templates/recurring_transactions/fragments/table.html:65
@@ -2494,6 +2658,7 @@ msgstr "Вы не сможете отменить это!"
#: templates/import_app/fragments/profiles/list.html:87
#: templates/import_app/fragments/runs/list.html:106
#: templates/installment_plans/fragments/table.html:60
+#: templates/investment_classes/fragments/list.html:51
#: templates/quick_transactions/fragments/list.html:39
#: templates/recurring_transactions/fragments/table.html:94
#: templates/rules/fragments/list.html:62
@@ -2542,6 +2707,7 @@ msgid "Add account"
msgstr "Добавить счёт"
#: templates/accounts/fragments/edit.html:5
+#: templates/investments/fragments/detail.html:40
msgid "Edit account"
msgstr ""
@@ -2614,7 +2780,7 @@ msgstr "Редактировать категорию"
msgid "Pick a month"
msgstr "Выберите месяц"
-#: templates/common/fragments/toasts.html:20
+#: templates/common/fragments/toasts.html:18
#: templates/cotton/components/fab.html:9
#: templates/cotton/components/sidebar_collapsible_panel.html:28
#: templates/extends/offcanvas.html:4 templates/includes/sidebar.html:44
@@ -2843,6 +3009,8 @@ msgid "Add DCA strategy"
msgstr ""
#: templates/dca/fragments/strategy/details.html:28
+#: templates/investments/fragments/detail.html:92
+#: templates/investments/fragments/list.html:65
msgid "No exchange rate available"
msgstr "Курс обмена недоступен"
@@ -3024,7 +3192,7 @@ msgstr ""
msgid "No services configured"
msgstr "Службы не настроены"
-#: templates/export_app/pages/index.html:4 templates/includes/sidebar.html:211
+#: templates/export_app/pages/index.html:4 templates/includes/sidebar.html:225
msgid "Export and Restore"
msgstr "Экспорт и восстановление"
@@ -3195,54 +3363,65 @@ msgstr ""
msgid "Tools"
msgstr "Инструменты"
-#: templates/includes/sidebar.html:118
+#: templates/includes/sidebar.html:118 templates/includes/sidebar.html:188
+#: templates/investment_classes/fragments/list.html:29
+#: templates/investments/fragments/detail.html:37
+#: templates/investments/fragments/list.html:9
+#: templates/investments/pages/detail.html:4
+#: templates/investments/pages/index.html:4
+#, fuzzy
+#| msgid "Installments"
+msgid "Investments"
+msgstr "Рассрочки"
+
+#: templates/includes/sidebar.html:124
msgid "Dollar Cost Average Tracker"
msgstr "Трекер средней стоимости доллара"
-#: templates/includes/sidebar.html:124
+#: templates/includes/sidebar.html:130
#: templates/mini_tools/unit_price_calculator.html:4
#: templates/mini_tools/unit_price_calculator.html:9
msgid "Unit Price Calculator"
msgstr "Калькулятор цены за единицу"
-#: templates/includes/sidebar.html:130
+#: templates/includes/sidebar.html:136
#: templates/mini_tools/currency_converter/currency_converter.html:7
#: templates/mini_tools/currency_converter/currency_converter.html:14
msgid "Currency Converter"
msgstr "Конвертер валют"
-#: templates/includes/sidebar.html:136 templates/mini_tools/simulator.html:5
+#: templates/includes/sidebar.html:142 templates/mini_tools/simulator.html:5
#: templates/mini_tools/simulator.html:21
#, fuzzy
#| msgid "Calculator"
msgid "Simulator"
msgstr "Калькулятор"
-#: templates/includes/sidebar.html:145
+#: templates/includes/sidebar.html:151
msgid "Management"
msgstr "Управление"
-#: templates/includes/sidebar.html:196
+#: templates/includes/sidebar.html:210
msgid "Automation"
msgstr ""
-#: templates/includes/sidebar.html:225
+#: templates/includes/sidebar.html:239
msgid "Admin"
msgstr "Администратор"
-#: templates/includes/sidebar.html:233
+#: templates/includes/sidebar.html:247
msgid "Django Admin"
msgstr "Администратор Django"
-#: templates/includes/sidebar.html:234
+#: templates/includes/sidebar.html:248
msgid "Only use this if you know what you're doing"
msgstr ""
-#: templates/includes/sidebar.html:249
+#: templates/includes/sidebar.html:263
msgid "is available"
msgstr ""
-#: templates/includes/sidebar.html:257
+#: templates/includes/sidebar.html:271
msgid "Calculator"
msgstr "Калькулятор"
@@ -3309,6 +3488,7 @@ msgstr "Нет недавних транзакций"
#: templates/insights/fragments/overview/_results.html:173
#: templates/insights/fragments/year_by_year.html:59
#: templates/insights/fragments/year_by_year.html:140
+#: templates/investment_classes/fragments/list.html:65
#: templates/monthly_overview/fragments/monthly_summary.html:168
msgid "Total"
msgstr "Итог"
@@ -3506,6 +3686,295 @@ msgstr ""
msgid "No installment plans"
msgstr ""
+#: templates/investment_classes/fragments/form.html:5
+msgid "Edit asset class"
+msgstr ""
+
+#: templates/investment_classes/fragments/form.html:5
+msgid "Add asset class"
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:19
+#: templates/investments/fragments/rebalance.html:10
+#, python-format
+msgid "Targets add up to %(total)s%%. Set them to 100%% to see the rebalance."
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:28
+#: templates/investments/fragments/rebalance.html:25
+msgid "Target"
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:50
+msgid "Its investments will be left without an asset class."
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:74
+msgid "No asset classes"
+msgstr ""
+
+#: templates/investments/fragments/_allocation.html:3
+msgid "Allocation chart"
+msgstr ""
+
+#: templates/investments/fragments/_allocation.html:11
+#, python-format
+msgid ""
+"%(share)s%% of your investments with a target, against a target of "
+"%(target)s%%"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid "Annual return, weighted by when money went in and out"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid ""
+"Return since the first contribution, weighted by when money went in and out"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid "/yr"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:15
+#: templates/investments/fragments/detail.html:63
+msgid "total return"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:20
+#: templates/investments/fragments/_totals.html:41
+#: templates/investments/fragments/charts.html:96
+#: templates/investments/fragments/detail.html:163
+#: templates/investments/fragments/detail.html:223
+#: templates/investments/fragments/list.html:47
+#, fuzzy
+#| msgid "Total Invested"
+msgid "Invested"
+msgstr "Общий объем инвестиций"
+
+#: templates/investments/fragments/_totals.html:24
+#: templates/investments/fragments/detail.html:74
+msgid ""
+"How much of the return came from exchange rates moving since you invested"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:24
+#: templates/investments/fragments/_totals.html:26
+#, fuzzy
+#| msgid "Currency Code"
+msgid "Currency effect"
+msgstr "Код валюты"
+
+#: templates/investments/fragments/_totals.html:26
+#: templates/investments/fragments/detail.html:78
+msgid ""
+"Every date is converted with the same rate. Add historical exchange rates "
+"(or an automatic rate service) to see how much exchange rates moved your "
+"return."
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:40
+#: templates/investments/fragments/list.html:49
+msgid "Return"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:10
+msgid "By asset class"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:17
+#: templates/net_worth/net_worth.html:52
+#: templates/yearly_overview/pages/overview_by_currency.html:6
+msgid "By currency"
+msgstr "По валюте"
+
+#: templates/investments/fragments/charts.html:24
+#, fuzzy
+#| msgid "Total Invested"
+msgid "Value and invested"
+msgstr "Общий объем инвестиций"
+
+#: templates/investments/fragments/charts.html:31
+msgid "Return by investment"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:38
+msgid "Nothing to chart yet"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:10
+msgid "Buy / Contribute"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:16
+msgid "Sell / Withdraw"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:23
+msgid "Dividend / Income"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:30
+#, fuzzy
+#| msgid "Update"
+msgid "Update value"
+msgstr "Обновить"
+
+#: templates/investments/fragments/detail.html:47
+#: templates/investments/fragments/list.html:67
+msgid "Tracked by value"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:48
+msgid ""
+"Some incoming units have no linked payment, so their cost was estimated from "
+"the market price of that day"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:68
+#, fuzzy
+#| msgid "Info"
+msgid "In"
+msgstr "Информация"
+
+#: templates/investments/fragments/detail.html:74
+#: templates/investments/fragments/detail.html:78
+#, fuzzy
+#| msgid "Currency Code"
+msgid "currency effect"
+msgstr "Код валюты"
+
+#: templates/investments/fragments/detail.html:104
+#: templates/investments/fragments/detail.html:179
+msgid "Price"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:105
+#: templates/investments/fragments/detail.html:200
+#: templates/investments/fragments/detail.html:239
+#: templates/investments/fragments/detail.html:290
+#, fuzzy
+#| msgid "Warning"
+msgid "Earnings"
+msgstr "Предупреждение"
+
+#: templates/investments/fragments/detail.html:180
+#: templates/investments/fragments/detail.html:227
+msgid "Average cost"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:181
+#: templates/investments/fragments/rebalance.html:41
+msgid "Buy"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:182
+#: templates/investments/fragments/rebalance.html:43
+msgid "Sell"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:227
+#: templates/investments/fragments/detail.html:257
+msgid "Put in / taken out"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:239
+#, fuzzy
+#| msgid "Uncategorized"
+msgid "Unrealized"
+msgstr "Без категории"
+
+#: templates/investments/fragments/detail.html:248
+msgid "Realized"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:271
+#, fuzzy
+#| msgid "Management"
+msgid "Movements"
+msgstr "Управление"
+
+#: templates/investments/fragments/detail.html:284
+msgid "Bought / contributed"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:284
+msgid "Sold / withdrew"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:288
+msgid "Money in"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:288
+msgid "Money out"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:290
+msgid "Gain"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:290
+msgid "Loss, tax or fee"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:315
+msgid "No movements yet"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:315
+msgid "Use the + button to buy or contribute"
+msgstr ""
+
+#: templates/investments/fragments/list.html:21
+msgid "Holdings"
+msgstr ""
+
+#: templates/investments/fragments/list.html:60
+msgid "Some costs were estimated from market prices"
+msgstr ""
+
+#: templates/investments/fragments/list.html:92
+msgid "Charts"
+msgstr ""
+
+#: templates/investments/fragments/list.html:97
+#, fuzzy
+#| msgid "New balance"
+msgid "Rebalance"
+msgstr "Новый баланс"
+
+#: templates/investments/fragments/list.html:105
+#, fuzzy
+#| msgid "Number of Installments"
+msgid "No investments yet"
+msgstr "Количество платежей"
+
+#: templates/investments/fragments/list.html:106
+msgid "Mark an account as an investment in its settings to see it here"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:4
+msgid "No allocation targets"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:5
+msgid "Set a target for your asset classes to see what to buy and sell"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:26
+msgid "To reach the target"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:34
+msgid "Off target"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:55
+msgid "Left out: investments without an asset class or a target"
+msgstr ""
+
#: templates/layouts/base.html:38
msgid "This is a demo!"
msgstr "Это демо-версия!"
@@ -3723,11 +4192,6 @@ msgstr "Фильтр транзакций"
msgid "Order by"
msgstr "Сортировать по"
-#: templates/net_worth/net_worth.html:52
-#: templates/yearly_overview/pages/overview_by_currency.html:6
-msgid "By currency"
-msgstr "По валюте"
-
#: templates/net_worth/net_worth.html:135
msgid "Evolution"
msgstr ""
@@ -4194,6 +4658,12 @@ msgstr "Войти с помощью"
msgid "Yearly Overview"
msgstr "Годовой обзор"
+#~ msgid ""
+#~ "Create one exchange rate and keep updating it. Avoids database clutter."
+#~ msgstr ""
+#~ "Создайте один обменный курс и постоянно обновляйте его. Это позволит "
+#~ "избежать захламления базы данных."
+
#~ msgid "Add as quick transaction"
#~ msgstr "Добавить как быструю транзакцию"
diff --git a/app/locale/sv/LC_MESSAGES/django.po b/app/locale/sv/LC_MESSAGES/django.po
index a3ea05c6..97346144 100644
--- a/app/locale/sv/LC_MESSAGES/django.po
+++ b/app/locale/sv/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-11-08 06:01+0000\n"
+"POT-Creation-Date: 2026-11-08 18:27+0000\n"
"PO-Revision-Date: 2025-04-14 06:16+0000\n"
"Last-Translator: Emil \n"
"Language-Team: Swedish \n"
"Language-Team: Swahili \n"
"Language-Team: Ukrainian =20) ? 1 : 2;\n"
"X-Generator: Weblate 5.14\n"
-#: apps/accounts/forms.py:28
+#: apps/accounts/forms.py:33
msgid "Group name"
msgstr "Назва групи"
-#: apps/accounts/forms.py:43 apps/accounts/forms.py:109
-#: apps/currencies/forms.py:57 apps/currencies/forms.py:91
-#: apps/currencies/forms.py:166 apps/dca/forms.py:46 apps/dca/forms.py:205
-#: apps/import_app/forms.py:32 apps/rules/forms.py:60 apps/rules/forms.py:100
-#: apps/rules/forms.py:385 apps/transactions/forms.py:219
-#: apps/transactions/forms.py:452 apps/transactions/forms.py:571
-#: apps/transactions/forms.py:718 apps/transactions/forms.py:940
-#: apps/transactions/forms.py:987 apps/transactions/forms.py:1015
-#: apps/transactions/forms.py:1046 apps/transactions/forms.py:1208
-#: apps/users/forms.py:264 apps/users/forms.py:422
+#: apps/accounts/forms.py:48 apps/accounts/forms.py:76
+#: apps/accounts/forms.py:184 apps/currencies/forms.py:57
+#: apps/currencies/forms.py:91 apps/currencies/forms.py:168
+#: apps/dca/forms.py:46 apps/dca/forms.py:192 apps/import_app/forms.py:32
+#: apps/rules/forms.py:60 apps/rules/forms.py:100 apps/rules/forms.py:385
+#: apps/transactions/forms.py:219 apps/transactions/forms.py:452
+#: apps/transactions/forms.py:571 apps/transactions/forms.py:718
+#: apps/transactions/forms.py:940 apps/transactions/forms.py:987
+#: apps/transactions/forms.py:1015 apps/transactions/forms.py:1046
+#: apps/transactions/forms.py:1208 apps/users/forms.py:264
+#: apps/users/forms.py:422
#: templates/rules/fragments/transaction_rule/dry_run/updated.html:5
#: templates/rules/fragments/transaction_rule/view.html:128
msgid "Update"
msgstr "Оновлення"
-#: apps/accounts/forms.py:49 apps/accounts/forms.py:115
-#: apps/common/widgets/tom_select.py:13 apps/currencies/forms.py:63
-#: apps/currencies/forms.py:97 apps/currencies/forms.py:172
-#: apps/dca/forms.py:52 apps/dca/forms.py:211 apps/import_app/forms.py:38
-#: apps/rules/forms.py:66 apps/rules/forms.py:106 apps/rules/forms.py:391
-#: apps/transactions/forms.py:206 apps/transactions/forms.py:226
-#: apps/transactions/forms.py:459 apps/transactions/forms.py:946
-#: apps/transactions/forms.py:993 apps/transactions/forms.py:1021
-#: apps/transactions/forms.py:1052 apps/transactions/forms.py:1214
-#: apps/users/forms.py:270 apps/users/forms.py:428
-#: templates/mini_tools/simulator.html:177
+#: apps/accounts/forms.py:54 apps/accounts/forms.py:76
+#: apps/accounts/forms.py:190 apps/common/widgets/tom_select.py:15
+#: apps/currencies/forms.py:63 apps/currencies/forms.py:97
+#: apps/currencies/forms.py:174 apps/dca/forms.py:52 apps/dca/forms.py:198
+#: apps/import_app/forms.py:38 apps/rules/forms.py:66 apps/rules/forms.py:106
+#: apps/rules/forms.py:391 apps/transactions/forms.py:206
+#: apps/transactions/forms.py:226 apps/transactions/forms.py:459
+#: apps/transactions/forms.py:946 apps/transactions/forms.py:993
+#: apps/transactions/forms.py:1021 apps/transactions/forms.py:1052
+#: apps/transactions/forms.py:1214 apps/users/forms.py:270
+#: apps/users/forms.py:428 templates/mini_tools/simulator.html:177
#: templates/mini_tools/unit_price_calculator.html:168
msgid "Add"
msgstr "Додати"
-#: apps/accounts/forms.py:57 templates/accounts/fragments/list.html:23
+#: apps/accounts/forms.py:86 templates/accounts/fragments/list.html:23
msgid "Group"
msgstr "Група"
-#: apps/accounts/forms.py:123
+#: apps/accounts/forms.py:93 apps/accounts/models.py:73
+#: apps/accounts/models.py:130
+#: templates/investments/fragments/rebalance.html:22
+msgid "Asset class"
+msgstr ""
+
+#: apps/accounts/forms.py:198
msgid "New balance"
msgstr "Новий баланс"
-#: apps/accounts/forms.py:129 apps/dca/forms.py:79 apps/dca/forms.py:86
+#: apps/accounts/forms.py:204 apps/dca/forms.py:79 apps/dca/forms.py:86
#: apps/insights/forms.py:117 apps/insights/views.py:44 apps/rules/forms.py:181
#: apps/rules/forms.py:197 apps/rules/models.py:44 apps/rules/models.py:311
#: apps/transactions/forms.py:62 apps/transactions/forms.py:330
#: apps/transactions/forms.py:510 apps/transactions/forms.py:607
#: apps/transactions/forms.py:614 apps/transactions/forms.py:823
#: apps/transactions/forms.py:1075 apps/transactions/models.py:357
-#: apps/transactions/models.py:743 apps/transactions/models.py:999
-#: apps/transactions/models.py:1306
+#: apps/transactions/models.py:767 apps/transactions/models.py:1023
+#: apps/transactions/models.py:1330
#: templates/insights/fragments/month_by_month.html:84
#: templates/insights/fragments/year_by_year.html:52
msgid "Category"
msgstr "Категорія"
-#: apps/accounts/forms.py:136 apps/dca/forms.py:95 apps/dca/forms.py:103
-#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
+#: apps/accounts/forms.py:211 apps/dca/forms.py:95 apps/dca/forms.py:103
+#: apps/export_app/forms.py:43 apps/export_app/forms.py:134
#: apps/insights/views.py:50 apps/rules/forms.py:184 apps/rules/forms.py:194
#: apps/rules/models.py:45 apps/rules/models.py:315
#: apps/transactions/filters.py:79 apps/transactions/forms.py:70
#: apps/transactions/forms.py:338 apps/transactions/forms.py:518
#: apps/transactions/forms.py:623 apps/transactions/forms.py:631
#: apps/transactions/forms.py:816 apps/transactions/forms.py:1068
-#: apps/transactions/models.py:363 apps/transactions/models.py:745
-#: apps/transactions/models.py:1003 apps/transactions/models.py:1312
-#: templates/includes/sidebar.html:156
+#: apps/transactions/models.py:363 apps/transactions/models.py:769
+#: apps/transactions/models.py:1027 apps/transactions/models.py:1336
+#: templates/includes/sidebar.html:162
#: templates/insights/fragments/month_by_month.html:29
#: templates/insights/fragments/month_by_month.html:32
#: templates/insights/fragments/year_by_year.html:24
@@ -95,27 +102,27 @@ msgstr "Категорія"
msgid "Tags"
msgstr "Мітки"
-#: apps/accounts/forms.py:179
+#: apps/accounts/forms.py:254
#, fuzzy
#| msgid "Transactions"
msgid "All paid transactions"
msgstr "Транзакції"
-#: apps/accounts/forms.py:180
+#: apps/accounts/forms.py:255
#, fuzzy
#| msgid "Update"
msgid "Up to date"
msgstr "Оновлення"
-#: apps/accounts/forms.py:181
+#: apps/accounts/forms.py:256
msgid "Up to reference month"
msgstr ""
-#: apps/accounts/forms.py:185
+#: apps/accounts/forms.py:260
msgid "Balance against"
msgstr ""
-#: apps/accounts/forms.py:189 apps/currencies/forms.py:70
+#: apps/accounts/forms.py:264 apps/currencies/forms.py:70
#: apps/dca/models.py:158 apps/rules/forms.py:176 apps/rules/forms.py:190
#: apps/rules/models.py:38 apps/rules/models.py:279
#: apps/transactions/forms.py:86 apps/transactions/forms.py:482
@@ -126,14 +133,15 @@ msgstr ""
msgid "Date"
msgstr "Дата"
-#: apps/accounts/forms.py:194
+#: apps/accounts/forms.py:269
msgid "Reference month"
msgstr ""
-#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
+#: apps/accounts/models.py:19 apps/accounts/models.py:36
+#: apps/accounts/models.py:84 apps/dca/models.py:13
#: apps/import_app/models.py:20 apps/rules/models.py:13
#: apps/transactions/models.py:249 apps/transactions/models.py:274
-#: apps/transactions/models.py:298 apps/transactions/models.py:1274
+#: apps/transactions/models.py:298 apps/transactions/models.py:1298
#: apps/users/models.py:577 templates/account_groups/fragments/list.html:22
#: templates/accounts/fragments/list.html:22
#: templates/categories/fragments/table.html:17
@@ -142,6 +150,7 @@ msgstr ""
#: templates/exchange_rates_services/fragments/list.html:28
#: templates/import_app/fragments/profiles/list.html:34
#: templates/installment_plans/fragments/table.html:16
+#: templates/investment_classes/fragments/list.html:27
#: templates/quick_transactions/fragments/list.html:13
#: templates/recurring_transactions/fragments/table.html:18
#: templates/rules/fragments/list.html:24
@@ -150,44 +159,127 @@ msgstr ""
msgid "Name"
msgstr "Ім'я"
-#: apps/accounts/models.py:18 apps/accounts/models.py:33
+#: apps/accounts/models.py:25 apps/accounts/models.py:88
msgid "Account Group"
msgstr "Група рахунків"
-#: apps/accounts/models.py:19 templates/account_groups/fragments/list.html:9
+#: apps/accounts/models.py:26 templates/account_groups/fragments/list.html:9
#: templates/account_groups/pages/index.html:4
-#: templates/includes/sidebar.html:176
+#: templates/includes/sidebar.html:182
msgid "Account Groups"
msgstr "Групи рахунків"
-#: apps/accounts/models.py:39 apps/currencies/models.py:44
+#: apps/accounts/models.py:43
+msgid "Target allocation (%)"
+msgstr ""
+
+#: apps/accounts/models.py:45
+msgid ""
+"Share of your investments you want in this class. Leave it empty to leave "
+"the class out of the targets."
+msgstr ""
+
+#: apps/accounts/models.py:61
+msgid "You already have an asset class with this name."
+msgstr ""
+
+#: apps/accounts/models.py:66
+#, python-format
+msgid ""
+"Targets can't add up to more than 100%%. Other classes already take "
+"%(taken)s%%."
+msgstr ""
+
+#: apps/accounts/models.py:74 apps/export_app/forms.py:131
+#: templates/includes/sidebar.html:190
+#: templates/investment_classes/fragments/list.html:9
+#: templates/investment_classes/pages/index.html:4
+#: templates/investments/fragments/rebalance.html:6
+#: templates/investments/fragments/rebalance.html:11
+msgid "Asset classes"
+msgstr ""
+
+#: apps/accounts/models.py:94 apps/currencies/models.py:44
#: templates/accounts/fragments/list.html:24
#: templates/mini_tools/simulator.html:37
#: templates/yearly_overview/pages/yearly_overview_base.html:19
msgid "Currency"
msgstr "Валюта"
-#: apps/accounts/models.py:45 apps/currencies/models.py:27
+#: apps/accounts/models.py:100 apps/currencies/models.py:27
#: templates/accounts/fragments/list.html:25
msgid "Exchange Currency"
msgstr "Валюта обміну"
-#: apps/accounts/models.py:50 apps/currencies/models.py:32
+#: apps/accounts/models.py:105 apps/currencies/models.py:32
msgid "Default currency for exchange calculations"
msgstr "Валюта за замовчуванням для обмінних розрахунків"
-#: apps/accounts/models.py:55
+#: apps/accounts/models.py:110
#, fuzzy
msgid "Asset account"
msgstr "Рахунок активу"
-#: apps/accounts/models.py:57
+#: apps/accounts/models.py:112
msgid ""
"Asset accounts count towards your Net Worth, but not towards your month."
msgstr ""
"Рахунки активів враховуються у вашій чистій вартості, але не у вашому місяці."
-#: apps/accounts/models.py:62 apps/currencies/models.py:37
+#: apps/accounts/models.py:117 templates/investments/fragments/list.html:46
+msgid "Investment"
+msgstr ""
+
+#: apps/accounts/models.py:119
+msgid ""
+"Show this account in the Investments tracker. Set an exchange currency to "
+"price it by units (stocks, ETFs, crypto); leave it empty to track it by "
+"value (fixed income, property, pensions)."
+msgstr ""
+
+#: apps/accounts/models.py:132
+msgid ""
+"Groups investments in the allocation charts, e.g. Fixed income, Stocks, Real "
+"estate."
+msgstr ""
+
+#: apps/accounts/models.py:140
+#, fuzzy
+#| msgid "Income Transaction"
+msgid "Income entities"
+msgstr "Дохідна транзакція"
+
+#: apps/accounts/models.py:142
+msgid ""
+"Transactions with any of these entities count towards this investment's "
+"return: in this account, and income paid into other accounts (dividends, "
+"rent)."
+msgstr ""
+
+#: apps/accounts/models.py:151
+#, fuzzy
+#| msgid "Categories"
+msgid "Income categories"
+msgstr "Категорії"
+
+#: apps/accounts/models.py:153
+msgid ""
+"Transactions in this account with any of these categories are its result "
+"(interest, taxes, fees). On accounts tracked by value, everything else that "
+"isn't a transfer counts as money coming in or going out."
+msgstr ""
+
+#: apps/accounts/models.py:162
+#, fuzzy
+#| msgid "Income Transaction"
+msgid "Income tags"
+msgstr "Дохідна транзакція"
+
+#: apps/accounts/models.py:164
+msgid "Same as income categories, for transactions with any of these tags."
+msgstr ""
+
+#: apps/accounts/models.py:169 apps/currencies/models.py:37
#: templates/accounts/fragments/list.html:27
#: templates/categories/fragments/list.html:20
#: templates/currencies/fragments/list.html:24
@@ -196,19 +288,19 @@ msgstr ""
msgid "Archived"
msgstr "Архівовано"
-#: apps/accounts/models.py:63
+#: apps/accounts/models.py:170
msgid "Archived accounts don't show up nor count towards your net worth"
msgstr ""
"Заархівовані рахунки не відображаються і не враховуються у вашій чистій "
"вартості"
-#: apps/accounts/models.py:75 apps/rules/forms.py:173 apps/rules/forms.py:187
+#: apps/accounts/models.py:182 apps/rules/forms.py:173 apps/rules/forms.py:187
#: apps/rules/models.py:35 apps/rules/models.py:267
#: apps/transactions/forms.py:82 apps/transactions/forms.py:350
#: apps/transactions/forms.py:477 apps/transactions/forms.py:808
#: apps/transactions/forms.py:1060 apps/transactions/models.py:329
-#: apps/transactions/models.py:703 apps/transactions/models.py:981
-#: apps/transactions/models.py:1280
+#: apps/transactions/models.py:727 apps/transactions/models.py:1005
+#: apps/transactions/models.py:1304
#: templates/installment_plans/fragments/table.html:17
#: templates/quick_transactions/fragments/list.html:14
#: templates/recurring_transactions/fragments/table.html:19
@@ -216,17 +308,17 @@ msgstr ""
msgid "Account"
msgstr "Рахунок"
-#: apps/accounts/models.py:76 apps/export_app/forms.py:19
-#: apps/export_app/forms.py:129 apps/transactions/filters.py:63
+#: apps/accounts/models.py:183 apps/export_app/forms.py:19
+#: apps/export_app/forms.py:130 apps/transactions/filters.py:63
#: templates/accounts/fragments/list.html:9
-#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:168
-#: templates/includes/sidebar.html:170
+#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:174
+#: templates/includes/sidebar.html:176
#: templates/monthly_overview/pages/overview.html:77
#: templates/transactions/pages/transactions.html:26
msgid "Accounts"
msgstr "Рахунки"
-#: apps/accounts/models.py:93
+#: apps/accounts/models.py:200
msgid "Exchange currency cannot be the same as the account's main currency."
msgstr "Валюта обміну не може збігатися з основною валютою рахунку."
@@ -244,7 +336,7 @@ msgstr "Групу рахунків успішно видалено"
#: apps/accounts/views/account_groups.py:109
#: apps/accounts/views/accounts.py:137 apps/dca/views.py:106
-#: apps/import_app/views.py:147 apps/rules/views.py:191
+#: apps/import_app/views.py:148 apps/rules/views.py:191
#: apps/transactions/views/categories.py:163
#: apps/transactions/views/entities.py:135 apps/transactions/views/tags.py:131
msgid "Item no longer shared with you"
@@ -252,7 +344,7 @@ msgstr "Елемент більше не доступний для вас"
#: apps/accounts/views/account_groups.py:132
#: apps/accounts/views/accounts.py:182 apps/dca/views.py:131
-#: apps/import_app/views.py:273 apps/rules/views.py:217
+#: apps/import_app/views.py:274 apps/rules/views.py:217
#: apps/transactions/views/categories.py:188
#: apps/transactions/views/entities.py:160 apps/transactions/views/tags.py:154
msgid "Ownership taken successfully"
@@ -260,7 +352,7 @@ msgstr "Успішно прийнято право власності"
#: apps/accounts/views/account_groups.py:152
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:151
-#: apps/import_app/views.py:294 apps/rules/views.py:238
+#: apps/import_app/views.py:295 apps/rules/views.py:238
#: apps/transactions/views/categories.py:130
#: apps/transactions/views/entities.py:180 apps/transactions/views/tags.py:174
msgid "Configuration saved successfully"
@@ -318,7 +410,7 @@ msgstr "Об'єкт з таким ідентифікатором не існує
msgid "Invalid entity data. Provide an ID or name."
msgstr "Невірні дані про об'єкт. Вкажіть ідентифікатор або ім'я."
-#: apps/api/serializers/transactions.py:141 apps/transactions/models.py:953
+#: apps/api/serializers/transactions.py:141 apps/transactions/models.py:977
msgid "This field is required."
msgstr ""
@@ -340,13 +432,14 @@ msgstr "Private"
msgid "Shared with users"
msgstr "Поділитися з користувачами"
-#: apps/common/fields/forms/dynamic_select.py:71
-#: apps/common/fields/forms/dynamic_select.py:142
+#: apps/common/fields/forms/dynamic_select.py:79
+#: apps/common/fields/forms/dynamic_select.py:150
msgid "Error creating new instance"
msgstr "Помилка створення нового екземпляра"
#: apps/common/fields/forms/grouped_select.py:24
-#: apps/common/widgets/tom_select.py:92 apps/common/widgets/tom_select.py:95
+#: apps/common/widgets/tom_select.py:94 apps/common/widgets/tom_select.py:97
+#: templates/investments/fragments/list.html:26
msgid "Ungrouped"
msgstr "Не згруповані"
@@ -500,14 +593,15 @@ msgid "Today"
msgstr "Сьогодні"
#: apps/common/widgets/datepicker.py:142
+#: templates/investments/fragments/rebalance.html:24
msgid "Now"
msgstr "Зараз"
-#: apps/common/widgets/tom_select.py:11
+#: apps/common/widgets/tom_select.py:13
msgid "Remove"
msgstr "Видалити"
-#: apps/common/widgets/tom_select.py:15 templates/mini_tools/simulator.html:187
+#: apps/common/widgets/tom_select.py:17 templates/mini_tools/simulator.html:187
#: templates/mini_tools/unit_price_calculator.html:180
#: templates/monthly_overview/pages/overview.html:286
#: templates/transactions/fragments/filter_actions.html:48
@@ -515,7 +609,7 @@ msgstr "Видалити"
msgid "Clear"
msgstr "Чисто"
-#: apps/common/widgets/tom_select.py:16
+#: apps/common/widgets/tom_select.py:18
msgid "No results..."
msgstr "Жодних результатів..."
@@ -549,10 +643,10 @@ msgid "Decimal Places"
msgstr "Десяткові знаки"
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
-#: apps/export_app/forms.py:130 apps/transactions/filters.py:70
+#: apps/export_app/forms.py:132 apps/transactions/filters.py:70
#: templates/currencies/fragments/list.html:9
-#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:182
-#: templates/includes/sidebar.html:184
+#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:196
+#: templates/includes/sidebar.html:198
#: templates/monthly_overview/pages/overview.html:63
#: templates/transactions/pages/transactions.html:12
msgid "Currencies"
@@ -584,9 +678,9 @@ msgid "Auto"
msgstr "Авто"
#: apps/currencies/models.py:82 apps/export_app/forms.py:67
-#: apps/export_app/forms.py:142 templates/exchange_rates/fragments/list.html:10
+#: apps/export_app/forms.py:144 templates/exchange_rates/fragments/list.html:10
#: templates/exchange_rates/pages/index.html:4
-#: templates/includes/sidebar.html:190
+#: templates/includes/sidebar.html:204
msgid "Exchange Rates"
msgstr "Обмінні курси"
@@ -681,32 +775,43 @@ msgstr ""
#: apps/currencies/models.py:171
#, fuzzy
#| msgid "Exchange Rate"
-msgid "Single exchange rate"
+msgid "One exchange rate per day"
msgstr "Обмінний курс"
#: apps/currencies/models.py:174
-msgid "Create one exchange rate and keep updating it. Avoids database clutter."
+msgid ""
+"Update the day's exchange rate on each fetch instead of adding a new one."
msgstr ""
-"Створіть один курс обміну та оновлюйте його постійно. Це запобігає "
-"засміченню бази даних."
-#: apps/currencies/models.py:179
+#: apps/currencies/models.py:178
+#, fuzzy
+#| msgid "Exchange Rates"
+msgid "Exchange rates to keep"
+msgstr "Обмінні курси"
+
+#: apps/currencies/models.py:183
+msgid ""
+"Keep at most this many automatic exchange rates per currency pair, deleting "
+"the oldest first. Leave empty to keep all."
+msgstr ""
+
+#: apps/currencies/models.py:188
msgid "Exchange Rate Service"
msgstr "Сервіс Курсів Обміну"
-#: apps/currencies/models.py:180
+#: apps/currencies/models.py:189
msgid "Exchange Rate Services"
msgstr "Сервіси Курсів Обміну"
-#: apps/currencies/models.py:234
+#: apps/currencies/models.py:243
msgid "'Every X hours' interval type requires a positive integer."
msgstr "Інтервал типу «Кожні X годин» потребує додатнього цілого числа."
-#: apps/currencies/models.py:243
+#: apps/currencies/models.py:252
msgid "'Every X hours' interval must be between 1 and 24."
msgstr "Інтервал типу «Кожні X годин» повинен бути між 1 та 24."
-#: apps/currencies/models.py:257
+#: apps/currencies/models.py:266
msgid ""
"Invalid hour format. Use comma-separated hours (0-23) and/or ranges (e.g., "
"'1-5,8,10-12')."
@@ -714,7 +819,7 @@ msgstr ""
"Неправильний формат годин. Використовуйте години, розділені комами (0–23) та/"
"або діапазони (наприклад, '1-5,8,10-12')."
-#: apps/currencies/models.py:268
+#: apps/currencies/models.py:277
msgid ""
"Invalid format. Please check the requirements for your selected interval "
"type."
@@ -786,21 +891,21 @@ msgstr "Введіть для пошуку транзакції, щоб пов
msgid "Income Transaction"
msgstr "Дохідна транзакція"
-#: apps/dca/forms.py:192
+#: apps/dca/forms.py:179
msgid "Link transaction"
msgstr "Пов’язати транзакцію"
-#: apps/dca/forms.py:275 apps/dca/forms.py:276 apps/dca/forms.py:281
-#: apps/dca/forms.py:285
+#: apps/dca/forms.py:262 apps/dca/forms.py:263 apps/dca/forms.py:268
+#: apps/dca/forms.py:272
msgid "You must provide an account."
msgstr "Необхідно вказати рахунок."
-#: apps/dca/forms.py:290 apps/transactions/forms.py:752
-#: apps/transactions/models.py:956
+#: apps/dca/forms.py:277 apps/transactions/forms.py:752
+#: apps/transactions/models.py:980
msgid "From and To accounts must be different."
msgstr "Рахунки «З» та «На» повинні бути різними."
-#: apps/dca/forms.py:304
+#: apps/dca/forms.py:299
#, python-format
msgid "DCA for %(strategy_name)s"
msgstr ""
@@ -816,8 +921,8 @@ msgstr "Валюта платежу"
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
#: apps/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
#: apps/transactions/forms.py:504 apps/transactions/forms.py:651
-#: apps/transactions/models.py:353 apps/transactions/models.py:752
-#: apps/transactions/models.py:1009 apps/transactions/models.py:1302
+#: apps/transactions/models.py:353 apps/transactions/models.py:776
+#: apps/transactions/models.py:1033 apps/transactions/models.py:1326
msgid "Notes"
msgstr "Примітки"
@@ -825,7 +930,7 @@ msgstr "Примітки"
msgid "DCA Strategy"
msgstr ""
-#: apps/dca/models.py:35 apps/export_app/forms.py:150
+#: apps/dca/models.py:35 apps/export_app/forms.py:152
msgid "DCA Strategies"
msgstr ""
@@ -845,7 +950,7 @@ msgstr "Отримана Сума"
msgid "DCA Entry"
msgstr ""
-#: apps/dca/models.py:187 apps/export_app/forms.py:151
+#: apps/dca/models.py:187 apps/export_app/forms.py:153
msgid "DCA Entries"
msgstr ""
@@ -873,25 +978,25 @@ msgstr "Запис успішно оновлено"
msgid "Entry deleted successfully"
msgstr "Запис успішно видалено"
-#: apps/export_app/forms.py:13 apps/export_app/forms.py:128
-#: templates/includes/sidebar.html:227 templates/users/fragments/list.html:10
+#: apps/export_app/forms.py:13 apps/export_app/forms.py:129
+#: templates/includes/sidebar.html:241 templates/users/fragments/list.html:10
#: templates/users/pages/index.html:4
msgid "Users"
msgstr "Користувачі"
-#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
+#: apps/export_app/forms.py:31 apps/export_app/forms.py:136
#: apps/transactions/models.py:422 templates/includes/sidebar.html:81
-#: templates/includes/sidebar.html:148
+#: templates/includes/sidebar.html:154
#: 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/export_app/forms.py:37 apps/export_app/forms.py:131
+#: apps/export_app/forms.py:37 apps/export_app/forms.py:133
#: apps/insights/views.py:43 apps/transactions/filters.py:74
#: templates/categories/fragments/list.html:9
-#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:150
+#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:156
#: templates/insights/fragments/month_by_month.html:18
#: templates/insights/fragments/month_by_month.html:21
#: templates/insights/fragments/year_by_year.html:13
@@ -899,16 +1004,16 @@ msgstr "Транзакції"
msgid "Categories"
msgstr "Категорії"
-#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
+#: apps/export_app/forms.py:49 apps/export_app/forms.py:135
#: apps/insights/views.py:57 apps/rules/forms.py:185 apps/rules/forms.py:195
#: apps/rules/models.py:46 apps/rules/models.py:307
#: apps/transactions/filters.py:84 apps/transactions/forms.py:78
#: apps/transactions/forms.py:346 apps/transactions/forms.py:526
#: apps/transactions/forms.py:831 apps/transactions/forms.py:1083
#: apps/transactions/models.py:312 apps/transactions/models.py:368
-#: apps/transactions/models.py:748 apps/transactions/models.py:1006
-#: apps/transactions/models.py:1317 templates/entities/fragments/list.html:9
-#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:162
+#: apps/transactions/models.py:772 apps/transactions/models.py:1030
+#: apps/transactions/models.py:1341 templates/entities/fragments/list.html:9
+#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:168
#: templates/insights/fragments/month_by_month.html:40
#: templates/insights/fragments/month_by_month.html:43
#: templates/insights/fragments/year_by_year.html:35
@@ -916,28 +1021,28 @@ msgstr "Категорії"
msgid "Entities"
msgstr ""
-#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
-#: apps/transactions/models.py:1046 templates/includes/sidebar.html:110
+#: apps/export_app/forms.py:55 apps/export_app/forms.py:139
+#: apps/transactions/models.py:1070 templates/includes/sidebar.html:110
#: templates/recurring_transactions/fragments/list.html:9
#: templates/recurring_transactions/pages/index.html:4
msgid "Recurring Transactions"
msgstr "Регулярні транзакції"
-#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
-#: apps/transactions/models.py:766 templates/includes/sidebar.html:104
+#: apps/export_app/forms.py:61 apps/export_app/forms.py:137
+#: apps/transactions/models.py:790 templates/includes/sidebar.html:104
#: templates/installment_plans/fragments/list.html:9
#: templates/installment_plans/pages/index.html:4
msgid "Installment Plans"
msgstr "Плани Розстрочки"
-#: apps/export_app/forms.py:73 apps/export_app/forms.py:140
+#: apps/export_app/forms.py:73 apps/export_app/forms.py:142
#: templates/exchange_rates_services/fragments/list.html:10
#: templates/exchange_rates_services/pages/index.html:4
-#: templates/includes/sidebar.html:218
+#: templates/includes/sidebar.html:232
msgid "Automatic Exchange Rates"
msgstr "Автоматичні Курси Обміну"
-#: apps/export_app/forms.py:79 templates/includes/sidebar.html:198
+#: apps/export_app/forms.py:79 templates/includes/sidebar.html:212
#: templates/rules/fragments/list.html:9 templates/rules/pages/index.html:4
msgid "Rules"
msgstr "Правила"
@@ -946,57 +1051,57 @@ msgstr "Правила"
msgid "DCA"
msgstr ""
-#: apps/export_app/forms.py:91 apps/export_app/forms.py:152
+#: apps/export_app/forms.py:91 apps/export_app/forms.py:154
#: templates/import_app/fragments/profiles/list.html:21
#: templates/import_app/pages/profiles_index.html:4
msgid "Import Profiles"
msgstr "Імпортувати Профілі"
-#: apps/export_app/forms.py:117 templates/export_app/fragments/export.html:5
+#: apps/export_app/forms.py:118 templates/export_app/fragments/export.html:5
#: templates/export_app/pages/index.html:13
msgid "Export"
msgstr "Експортувати"
-#: apps/export_app/forms.py:125
+#: apps/export_app/forms.py:126
msgid "Import a ZIP file exported from WYGIWYH"
msgstr "Імпортувати ZIP-файл, експортований із WYGIWYH"
-#: apps/export_app/forms.py:126
+#: apps/export_app/forms.py:127
msgid "ZIP File"
msgstr "ZIP-Файл"
-#: apps/export_app/forms.py:143 apps/rules/models.py:27
+#: apps/export_app/forms.py:145 apps/rules/models.py:27
msgid "Transaction rules"
msgstr "Правила транзакцій"
-#: apps/export_app/forms.py:145 apps/rules/models.py:68
+#: apps/export_app/forms.py:147 apps/rules/models.py:68
msgid "Edit transaction action"
msgstr ""
-#: apps/export_app/forms.py:148 apps/rules/models.py:327
+#: apps/export_app/forms.py:150 apps/rules/models.py:327
msgid "Update or create transaction actions"
msgstr ""
-#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:341
+#: apps/export_app/forms.py:183 templates/cotton/transaction/item.html:341
#: templates/cotton/ui/deleted_transactions_action_bar.html:53
#: templates/export_app/fragments/restore.html:5
#: templates/export_app/pages/index.html:19
msgid "Restore"
msgstr "Відновити"
-#: apps/export_app/forms.py:191
+#: apps/export_app/forms.py:193
msgid "Please upload either a ZIP file or at least one CSV file"
msgstr "Будь ласка, завантажте або ZIP-файл, або принаймні один CSV-файл"
-#: apps/export_app/views.py:177
+#: apps/export_app/views.py:183
msgid "You have to select at least one export"
msgstr "Ви повинні обрати принаймні один експорт"
-#: apps/export_app/views.py:197
+#: apps/export_app/views.py:203
msgid "Data restored successfully"
msgstr "Дані успішно відновлено"
-#: apps/export_app/views.py:209
+#: apps/export_app/views.py:215
msgid ""
"There was an error restoring your data. Check the logs for more details."
msgstr ""
@@ -1009,7 +1114,7 @@ msgstr "Оберіть файл"
#: apps/import_app/forms.py:55
#: templates/import_app/fragments/profiles/list.html:75
-#: templates/includes/sidebar.html:204
+#: templates/includes/sidebar.html:218
msgid "Import"
msgstr "Імпортувати"
@@ -1057,23 +1162,23 @@ msgstr ""
msgid "File name"
msgstr ""
-#: apps/import_app/views.py:75
+#: apps/import_app/views.py:76
msgid "Import Profile added successfully"
msgstr ""
-#: apps/import_app/views.py:113
+#: apps/import_app/views.py:114
msgid "Import Profile update successfully"
msgstr ""
-#: apps/import_app/views.py:142
+#: apps/import_app/views.py:143
msgid "Import Profile deleted successfully"
msgstr ""
-#: apps/import_app/views.py:222
+#: apps/import_app/views.py:223
msgid "Import Run queued successfully"
msgstr ""
-#: apps/import_app/views.py:249
+#: apps/import_app/views.py:250
msgid "Run deleted successfully"
msgstr ""
@@ -1144,7 +1249,7 @@ msgstr "Мітки"
msgid "Untagged"
msgstr ""
-#: apps/insights/views.py:53 apps/transactions/models.py:589
+#: apps/insights/views.py:53 apps/transactions/models.py:613
#: templates/tags/fragments/table.html:69
msgid "No tags"
msgstr ""
@@ -1165,6 +1270,32 @@ msgstr ""
msgid "No entities"
msgstr ""
+#: apps/investments/views.py:104
+msgid "Other"
+msgstr ""
+
+#: apps/investments/views.py:135
+msgid "Unclassified"
+msgstr ""
+
+#: apps/investments/views.py:267
+#, fuzzy
+#| msgid "Account added successfully"
+msgid "Asset class added successfully"
+msgstr "Рахунок додано успішно"
+
+#: apps/investments/views.py:275
+#, fuzzy
+#| msgid "Account updated successfully"
+msgid "Asset class updated successfully"
+msgstr "Рахунок успішно оновлено"
+
+#: apps/investments/views.py:296
+#, fuzzy
+#| msgid "Account deleted successfully"
+msgid "Asset class deleted successfully"
+msgstr "Рахунок успішно видалено"
+
#: apps/net_worth/views.py:137 templates/net_worth/net_worth.html:84
#: templates/net_worth/net_worth.html:85
msgid "Consolidated"
@@ -1218,8 +1349,8 @@ msgstr ""
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
#: apps/rules/models.py:271 apps/transactions/forms.py:468
-#: apps/transactions/models.py:336 apps/transactions/models.py:708
-#: apps/transactions/models.py:987 apps/transactions/models.py:1287
+#: apps/transactions/models.py:336 apps/transactions/models.py:732
+#: apps/transactions/models.py:1011 apps/transactions/models.py:1311
#: templates/mini_tools/simulator.html:63
msgid "Type"
msgstr ""
@@ -1227,7 +1358,7 @@ msgstr ""
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
#: apps/rules/models.py:275 apps/transactions/filters.py:22
#: apps/transactions/forms.py:472 apps/transactions/models.py:338
-#: apps/transactions/models.py:1289 templates/cotton/transaction/item.html:23
+#: apps/transactions/models.py:1313 templates/cotton/transaction/item.html:23
#: templates/cotton/transaction/item.html:34
#: templates/transactions/widgets/paid_toggle_button.html:10
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
@@ -1238,14 +1369,14 @@ msgstr ""
#: apps/rules/models.py:283 apps/transactions/forms.py:90
#: apps/transactions/forms.py:488 apps/transactions/forms.py:638
#: apps/transactions/forms.py:837 apps/transactions/models.py:340
-#: apps/transactions/models.py:726 apps/transactions/models.py:1011
+#: apps/transactions/models.py:750 apps/transactions/models.py:1035
msgid "Reference Date"
msgstr ""
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
#: apps/rules/models.py:287 apps/transactions/forms.py:495
-#: apps/transactions/models.py:346 apps/transactions/models.py:992
-#: apps/transactions/models.py:1295
+#: apps/transactions/models.py:346 apps/transactions/models.py:1016
+#: apps/transactions/models.py:1319
#: templates/insights/fragments/sankey.html:102
#: templates/installment_plans/fragments/table.html:18
#: templates/mini_tools/simulator.html:70
@@ -1257,27 +1388,27 @@ msgstr ""
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
#: apps/rules/models.py:42 apps/rules/models.py:291
#: apps/transactions/forms.py:499 apps/transactions/forms.py:642
-#: apps/transactions/models.py:351 apps/transactions/models.py:710
-#: apps/transactions/models.py:995 apps/transactions/models.py:1300
+#: apps/transactions/models.py:351 apps/transactions/models.py:734
+#: apps/transactions/models.py:1019 apps/transactions/models.py:1324
msgid "Description"
msgstr ""
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
#: apps/rules/models.py:299 apps/transactions/models.py:398
-#: apps/transactions/models.py:1322
+#: apps/transactions/models.py:1346
msgid "Internal Note"
msgstr ""
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
#: apps/rules/models.py:303 apps/transactions/models.py:400
-#: apps/transactions/models.py:1324
+#: apps/transactions/models.py:1348
msgid "Internal ID"
msgstr ""
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
#: apps/rules/models.py:319 apps/transactions/forms.py:655
#: apps/transactions/models.py:250 apps/transactions/models.py:341
-#: apps/transactions/models.py:1290
+#: apps/transactions/models.py:1314
msgid "Mute"
msgstr ""
@@ -1290,7 +1421,7 @@ msgid "Set Values"
msgstr ""
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
-#: apps/transactions/models.py:421 apps/transactions/models.py:645
+#: apps/transactions/models.py:421 apps/transactions/models.py:669
msgid "Transaction"
msgstr ""
@@ -1326,7 +1457,12 @@ msgstr ""
msgid "Field"
msgstr ""
-#: apps/rules/models.py:61
+#: apps/rules/models.py:61 templates/investments/fragments/charts.html:86
+#: templates/investments/fragments/detail.html:103
+#: templates/investments/fragments/detail.html:162
+#: templates/investments/fragments/list.html:48
+#: templates/investments/fragments/list.html:49
+#: templates/investments/fragments/rebalance.html:23
msgid "Value"
msgstr ""
@@ -1510,7 +1646,7 @@ msgid "Upload"
msgstr ""
#: apps/transactions/forms.py:355 apps/transactions/forms.py:1089
-#: apps/transactions/models.py:925
+#: apps/transactions/models.py:949
#, fuzzy
#| msgid "To Account"
msgid "To account"
@@ -1528,8 +1664,8 @@ msgstr ""
msgid "To Amount"
msgstr ""
-#: apps/transactions/forms.py:718 apps/transactions/models.py:970
-#: apps/transactions/models.py:1267 templates/cotton/transaction/item.html:52
+#: apps/transactions/forms.py:718 apps/transactions/models.py:994
+#: apps/transactions/models.py:1291 templates/cotton/transaction/item.html:52
#: templates/cotton/transaction/item.html:60
#: templates/cotton/ui/quick_transactions_buttons.html:40
#: templates/cotton/ui/transactions_fab.html:44
@@ -1589,8 +1725,8 @@ msgid ""
"transactions"
msgstr ""
-#: apps/transactions/models.py:323 apps/transactions/models.py:969
-#: apps/transactions/models.py:1266
+#: apps/transactions/models.py:323 apps/transactions/models.py:993
+#: apps/transactions/models.py:1290
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: templates/calendar_view/fragments/list.html:52
@@ -1598,13 +1734,19 @@ msgstr ""
#: templates/cotton/ui/quick_transactions_buttons.html:10
#: templates/cotton/ui/transactions_fab.html:10
#: templates/insights/fragments/overview/_results.html:12
+#: templates/investments/fragments/_totals.html:21
+#: templates/investments/fragments/_totals.html:42
+#: templates/investments/fragments/detail.html:106
+#: templates/investments/fragments/detail.html:210
+#: templates/investments/fragments/detail.html:252
+#: templates/investments/fragments/detail.html:320
#: templates/mini_tools/simulator.html:66
#: templates/monthly_overview/fragments/monthly_summary.html:42
msgid "Income"
msgstr ""
-#: apps/transactions/models.py:324 apps/transactions/models.py:971
-#: apps/transactions/models.py:1268
+#: apps/transactions/models.py:324 apps/transactions/models.py:995
+#: apps/transactions/models.py:1292
#: templates/calendar_view/fragments/list.html:46
#: templates/calendar_view/fragments/list.html:48
#: templates/calendar_view/fragments/list.html:56
@@ -1616,14 +1758,14 @@ msgstr ""
msgid "Expense"
msgstr ""
-#: apps/transactions/models.py:379 apps/transactions/models.py:765
+#: apps/transactions/models.py:379 apps/transactions/models.py:789
#: templates/cotton/transaction/item.html:291
#: templates/cotton/transaction/item.html:308
#: templates/cotton/transaction/item.html:318
msgid "Installment Plan"
msgstr ""
-#: apps/transactions/models.py:388 apps/transactions/models.py:1045
+#: apps/transactions/models.py:388 apps/transactions/models.py:1069
#: templates/cotton/transaction/item.html:281
#: templates/cotton/transaction/item.html:301
msgid "Recurring Transaction"
@@ -1643,160 +1785,172 @@ msgstr ""
msgid "Deleted At"
msgstr ""
-#: apps/transactions/models.py:591
+#: apps/transactions/models.py:517
+msgid "A transfer needs one expense and one income."
+msgstr ""
+
+#: apps/transactions/models.py:519
+msgid "A transfer needs two different accounts."
+msgstr ""
+
+#: apps/transactions/models.py:523
+msgid "This transaction is already part of another transfer."
+msgstr ""
+
+#: apps/transactions/models.py:615
msgid "No category"
msgstr ""
-#: apps/transactions/models.py:593
+#: apps/transactions/models.py:617
msgid "No description"
msgstr ""
-#: apps/transactions/models.py:650
+#: apps/transactions/models.py:674
#, fuzzy
#| msgid "ZIP File"
msgid "File"
msgstr "ZIP-Файл"
-#: apps/transactions/models.py:652
+#: apps/transactions/models.py:676
msgid "Original Name"
msgstr ""
-#: apps/transactions/models.py:654
+#: apps/transactions/models.py:678
#, fuzzy
#| msgid "Interval Type"
msgid "Content Type"
msgstr "Тип інтервалу"
-#: apps/transactions/models.py:656
+#: apps/transactions/models.py:680
msgid "Size"
msgstr ""
-#: apps/transactions/models.py:661
+#: apps/transactions/models.py:685
msgid "Uploaded By"
msgstr ""
-#: apps/transactions/models.py:666
+#: apps/transactions/models.py:690
#, fuzzy
#| msgid "Transaction rules"
msgid "Transaction Attachment"
msgstr "Правила транзакцій"
-#: apps/transactions/models.py:667
+#: apps/transactions/models.py:691
#, fuzzy
#| msgid "Transaction rules"
msgid "Transaction Attachments"
msgstr "Правила транзакцій"
-#: apps/transactions/models.py:697 templates/includes/sidebar.html:57
+#: apps/transactions/models.py:721 templates/includes/sidebar.html:57
msgid "Yearly"
msgstr ""
-#: apps/transactions/models.py:698 apps/users/models.py:469
+#: apps/transactions/models.py:722 apps/users/models.py:469
#: templates/includes/sidebar.html:51
msgid "Monthly"
msgstr ""
-#: apps/transactions/models.py:699
+#: apps/transactions/models.py:723
msgid "Weekly"
msgstr ""
-#: apps/transactions/models.py:700
+#: apps/transactions/models.py:724
msgid "Daily"
msgstr ""
-#: apps/transactions/models.py:713
+#: apps/transactions/models.py:737
msgid "Number of Installments"
msgstr ""
-#: apps/transactions/models.py:718
+#: apps/transactions/models.py:742
msgid "Installment Start"
msgstr ""
-#: apps/transactions/models.py:719
+#: apps/transactions/models.py:743
msgid "The installment number to start counting from"
msgstr ""
-#: apps/transactions/models.py:724 apps/transactions/models.py:1015
+#: apps/transactions/models.py:748 apps/transactions/models.py:1039
msgid "Start Date"
msgstr ""
-#: apps/transactions/models.py:728 apps/transactions/models.py:1016
+#: apps/transactions/models.py:752 apps/transactions/models.py:1040
msgid "End Date"
msgstr ""
-#: apps/transactions/models.py:733
+#: apps/transactions/models.py:757
msgid "Recurrence"
msgstr ""
-#: apps/transactions/models.py:736
+#: apps/transactions/models.py:760
msgid "Installment Amount"
msgstr ""
-#: apps/transactions/models.py:755 apps/transactions/models.py:1035
+#: apps/transactions/models.py:779 apps/transactions/models.py:1059
msgid "Add description to transactions"
msgstr ""
-#: apps/transactions/models.py:758 apps/transactions/models.py:1038
+#: apps/transactions/models.py:782 apps/transactions/models.py:1062
msgid "Add notes to transactions"
msgstr ""
-#: apps/transactions/models.py:932
+#: apps/transactions/models.py:956
#, fuzzy
#| msgid "To Account"
msgid "To amount"
msgstr "На рахунок"
-#: apps/transactions/models.py:933
+#: apps/transactions/models.py:957
msgid "Leave empty to use the same amount"
msgstr ""
-#: apps/transactions/models.py:974 templates/mini_tools/simulator.html:129
+#: apps/transactions/models.py:998 templates/mini_tools/simulator.html:129
msgid "day(s)"
msgstr ""
-#: apps/transactions/models.py:975 templates/mini_tools/simulator.html:130
+#: apps/transactions/models.py:999 templates/mini_tools/simulator.html:130
msgid "week(s)"
msgstr ""
-#: apps/transactions/models.py:976 templates/mini_tools/simulator.html:131
+#: apps/transactions/models.py:1000 templates/mini_tools/simulator.html:131
msgid "month(s)"
msgstr ""
-#: apps/transactions/models.py:977 templates/mini_tools/simulator.html:132
+#: apps/transactions/models.py:1001 templates/mini_tools/simulator.html:132
msgid "year(s)"
msgstr ""
-#: apps/transactions/models.py:979
+#: apps/transactions/models.py:1003
#: templates/recurring_transactions/fragments/list.html:18
msgid "Paused"
msgstr ""
-#: apps/transactions/models.py:1018
+#: apps/transactions/models.py:1042
msgid "Recurrence Type"
msgstr ""
-#: apps/transactions/models.py:1021
+#: apps/transactions/models.py:1045
msgid "Recurrence Interval"
msgstr ""
-#: apps/transactions/models.py:1024
+#: apps/transactions/models.py:1048
msgid "Keep at most"
msgstr ""
-#: apps/transactions/models.py:1028
+#: apps/transactions/models.py:1052
msgid "Last Generated Date"
msgstr ""
-#: apps/transactions/models.py:1031
+#: apps/transactions/models.py:1055
msgid "Last Generated Reference Date"
msgstr ""
-#: apps/transactions/models.py:1334 templates/cotton/transaction/item.html:274
+#: apps/transactions/models.py:1358 templates/cotton/transaction/item.html:274
#: templates/cotton/ui/transactions_fab.html:59
msgid "Quick Transaction"
msgstr ""
-#: apps/transactions/models.py:1335 templates/includes/sidebar.html:98
+#: apps/transactions/models.py:1359 templates/includes/sidebar.html:98
#: templates/quick_transactions/pages/index.html:5
#: templates/quick_transactions/pages/index.html:15
msgid "Quick Transactions"
@@ -1927,8 +2081,8 @@ msgid "Item deleted successfully"
msgstr "Рахунок успішно видалено"
#: apps/transactions/views/quick_transactions.py:164
-#: apps/transactions/views/transactions.py:143
-#: apps/transactions/views/transactions.py:328
+#: apps/transactions/views/transactions.py:156
+#: apps/transactions/views/transactions.py:343
msgid "Transaction added successfully"
msgstr ""
@@ -1964,46 +2118,46 @@ msgstr ""
msgid "Tag deleted successfully"
msgstr ""
-#: apps/transactions/views/transactions.py:61
+#: apps/transactions/views/transactions.py:65
#, fuzzy
#| msgid "Account updated successfully"
msgid "Attachment uploaded successfully"
msgstr "Рахунок успішно оновлено"
-#: apps/transactions/views/transactions.py:112
+#: apps/transactions/views/transactions.py:116
#, fuzzy
#| msgid "Account deleted successfully"
msgid "Attachment deleted successfully"
msgstr "Рахунок успішно видалено"
-#: apps/transactions/views/transactions.py:355
+#: apps/transactions/views/transactions.py:370
msgid "Transaction updated successfully"
msgstr ""
-#: apps/transactions/views/transactions.py:406
+#: apps/transactions/views/transactions.py:421
#, python-format
msgid "%(count)s transaction updated successfully"
msgid_plural "%(count)s transactions updated successfully"
msgstr[0] ""
msgstr[1] ""
-#: apps/transactions/views/transactions.py:433
+#: apps/transactions/views/transactions.py:448
msgid "Transaction duplicated successfully"
msgstr ""
-#: apps/transactions/views/transactions.py:478
+#: apps/transactions/views/transactions.py:493
msgid "Transaction deleted successfully"
msgstr ""
-#: apps/transactions/views/transactions.py:508
+#: apps/transactions/views/transactions.py:523
msgid "Transaction restored successfully"
msgstr ""
-#: apps/transactions/views/transactions.py:534
+#: apps/transactions/views/transactions.py:549
msgid "Transfer added successfully"
msgstr ""
-#: apps/transactions/views/transactions.py:562
+#: apps/transactions/views/transactions.py:579
#, fuzzy
#| msgid "Entry updated successfully"
msgid "Transfer updated successfully"
@@ -2330,6 +2484,7 @@ msgstr ""
#: templates/exchange_rates_services/fragments/table.html:20
#: templates/import_app/fragments/profiles/list.html:42
#: templates/installment_plans/fragments/table.html:25
+#: templates/investment_classes/fragments/list.html:36
#: templates/quick_transactions/fragments/list.html:22
#: templates/recurring_transactions/fragments/table.html:27
#: templates/rules/fragments/list.html:31
@@ -2352,6 +2507,9 @@ msgstr ""
#: templates/exchange_rates_services/fragments/table.html:23
#: templates/import_app/fragments/profiles/list.html:46
#: templates/installment_plans/fragments/table.html:28
+#: templates/investment_classes/fragments/list.html:39
+#: templates/investments/fragments/detail.html:305
+#: templates/investments/fragments/detail.html:331
#: templates/quick_transactions/fragments/list.html:25
#: templates/recurring_transactions/fragments/table.html:30
#: templates/rules/fragments/transaction_rule/view.html:22
@@ -2401,6 +2559,7 @@ msgstr ""
#: templates/import_app/fragments/profiles/list.html:81
#: templates/import_app/fragments/runs/list.html:100
#: templates/installment_plans/fragments/table.html:54
+#: templates/investment_classes/fragments/list.html:45
#: templates/mini_tools/simulator.html:109
#: templates/mini_tools/unit_price_calculator.html:16
#: templates/quick_transactions/fragments/list.html:32
@@ -2434,6 +2593,7 @@ msgstr ""
#: templates/import_app/fragments/runs/list.html:104
#: templates/installment_plans/fragments/table.html:47
#: templates/installment_plans/fragments/table.html:58
+#: templates/investment_classes/fragments/list.html:49
#: templates/quick_transactions/fragments/list.html:37
#: templates/recurring_transactions/fragments/table.html:52
#: templates/recurring_transactions/fragments/table.html:65
@@ -2483,6 +2643,7 @@ msgstr ""
#: templates/import_app/fragments/profiles/list.html:87
#: templates/import_app/fragments/runs/list.html:106
#: templates/installment_plans/fragments/table.html:60
+#: templates/investment_classes/fragments/list.html:51
#: templates/quick_transactions/fragments/list.html:39
#: templates/recurring_transactions/fragments/table.html:94
#: templates/rules/fragments/list.html:62
@@ -2531,6 +2692,7 @@ msgid "Add account"
msgstr ""
#: templates/accounts/fragments/edit.html:5
+#: templates/investments/fragments/detail.html:40
msgid "Edit account"
msgstr ""
@@ -2603,7 +2765,7 @@ msgstr ""
msgid "Pick a month"
msgstr ""
-#: templates/common/fragments/toasts.html:20
+#: templates/common/fragments/toasts.html:18
#: templates/cotton/components/fab.html:9
#: templates/cotton/components/sidebar_collapsible_panel.html:28
#: templates/extends/offcanvas.html:4 templates/includes/sidebar.html:44
@@ -2832,6 +2994,8 @@ msgid "Add DCA strategy"
msgstr ""
#: templates/dca/fragments/strategy/details.html:28
+#: templates/investments/fragments/detail.html:92
+#: templates/investments/fragments/list.html:65
msgid "No exchange rate available"
msgstr ""
@@ -3014,7 +3178,7 @@ msgstr ""
msgid "No services configured"
msgstr ""
-#: templates/export_app/pages/index.html:4 templates/includes/sidebar.html:211
+#: templates/export_app/pages/index.html:4 templates/includes/sidebar.html:225
msgid "Export and Restore"
msgstr ""
@@ -3185,52 +3349,63 @@ msgstr ""
msgid "Tools"
msgstr ""
-#: templates/includes/sidebar.html:118
+#: templates/includes/sidebar.html:118 templates/includes/sidebar.html:188
+#: templates/investment_classes/fragments/list.html:29
+#: templates/investments/fragments/detail.html:37
+#: templates/investments/fragments/list.html:9
+#: templates/investments/pages/detail.html:4
+#: templates/investments/pages/index.html:4
+#, fuzzy
+#| msgid "Installment Plans"
+msgid "Investments"
+msgstr "Плани Розстрочки"
+
+#: templates/includes/sidebar.html:124
msgid "Dollar Cost Average Tracker"
msgstr ""
-#: templates/includes/sidebar.html:124
+#: templates/includes/sidebar.html:130
#: templates/mini_tools/unit_price_calculator.html:4
#: templates/mini_tools/unit_price_calculator.html:9
msgid "Unit Price Calculator"
msgstr ""
-#: templates/includes/sidebar.html:130
+#: templates/includes/sidebar.html:136
#: templates/mini_tools/currency_converter/currency_converter.html:7
#: templates/mini_tools/currency_converter/currency_converter.html:14
msgid "Currency Converter"
msgstr ""
-#: templates/includes/sidebar.html:136 templates/mini_tools/simulator.html:5
+#: templates/includes/sidebar.html:142 templates/mini_tools/simulator.html:5
#: templates/mini_tools/simulator.html:21
msgid "Simulator"
msgstr ""
-#: templates/includes/sidebar.html:145
+#: templates/includes/sidebar.html:151
msgid "Management"
msgstr ""
-#: templates/includes/sidebar.html:196
+#: templates/includes/sidebar.html:210
msgid "Automation"
msgstr ""
-#: templates/includes/sidebar.html:225
+#: templates/includes/sidebar.html:239
msgid "Admin"
msgstr ""
-#: templates/includes/sidebar.html:233
+#: templates/includes/sidebar.html:247
msgid "Django Admin"
msgstr ""
-#: templates/includes/sidebar.html:234
+#: templates/includes/sidebar.html:248
msgid "Only use this if you know what you're doing"
msgstr ""
-#: templates/includes/sidebar.html:249
+#: templates/includes/sidebar.html:263
msgid "is available"
msgstr ""
-#: templates/includes/sidebar.html:257
+#: templates/includes/sidebar.html:271
msgid "Calculator"
msgstr ""
@@ -3297,6 +3472,7 @@ msgstr ""
#: templates/insights/fragments/overview/_results.html:173
#: templates/insights/fragments/year_by_year.html:59
#: templates/insights/fragments/year_by_year.html:140
+#: templates/investment_classes/fragments/list.html:65
#: templates/monthly_overview/fragments/monthly_summary.html:168
msgid "Total"
msgstr ""
@@ -3498,6 +3674,285 @@ msgstr ""
msgid "No installment plans"
msgstr ""
+#: templates/investment_classes/fragments/form.html:5
+msgid "Edit asset class"
+msgstr ""
+
+#: templates/investment_classes/fragments/form.html:5
+msgid "Add asset class"
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:19
+#: templates/investments/fragments/rebalance.html:10
+#, python-format
+msgid "Targets add up to %(total)s%%. Set them to 100%% to see the rebalance."
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:28
+#: templates/investments/fragments/rebalance.html:25
+msgid "Target"
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:50
+msgid "Its investments will be left without an asset class."
+msgstr ""
+
+#: templates/investment_classes/fragments/list.html:74
+msgid "No asset classes"
+msgstr ""
+
+#: templates/investments/fragments/_allocation.html:3
+msgid "Allocation chart"
+msgstr ""
+
+#: templates/investments/fragments/_allocation.html:11
+#, python-format
+msgid ""
+"%(share)s%% of your investments with a target, against a target of "
+"%(target)s%%"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid "Annual return, weighted by when money went in and out"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid ""
+"Return since the first contribution, weighted by when money went in and out"
+msgstr ""
+
+#: templates/investments/fragments/_mwr.html:1
+msgid "/yr"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:15
+#: templates/investments/fragments/detail.html:63
+msgid "total return"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:20
+#: templates/investments/fragments/_totals.html:41
+#: templates/investments/fragments/charts.html:96
+#: templates/investments/fragments/detail.html:163
+#: templates/investments/fragments/detail.html:223
+#: templates/investments/fragments/list.html:47
+msgid "Invested"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:24
+#: templates/investments/fragments/detail.html:74
+msgid ""
+"How much of the return came from exchange rates moving since you invested"
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:24
+#: templates/investments/fragments/_totals.html:26
+#, fuzzy
+#| msgid "Currency Code"
+msgid "Currency effect"
+msgstr "Код валюти"
+
+#: templates/investments/fragments/_totals.html:26
+#: templates/investments/fragments/detail.html:78
+msgid ""
+"Every date is converted with the same rate. Add historical exchange rates "
+"(or an automatic rate service) to see how much exchange rates moved your "
+"return."
+msgstr ""
+
+#: templates/investments/fragments/_totals.html:40
+#: templates/investments/fragments/list.html:49
+msgid "Return"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:10
+msgid "By asset class"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:17
+#: templates/net_worth/net_worth.html:52
+#: templates/yearly_overview/pages/overview_by_currency.html:6
+msgid "By currency"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:24
+msgid "Value and invested"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:31
+msgid "Return by investment"
+msgstr ""
+
+#: templates/investments/fragments/charts.html:38
+msgid "Nothing to chart yet"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:10
+msgid "Buy / Contribute"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:16
+msgid "Sell / Withdraw"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:23
+msgid "Dividend / Income"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:30
+#, fuzzy
+#| msgid "Update"
+msgid "Update value"
+msgstr "Оновлення"
+
+#: templates/investments/fragments/detail.html:47
+#: templates/investments/fragments/list.html:67
+msgid "Tracked by value"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:48
+msgid ""
+"Some incoming units have no linked payment, so their cost was estimated from "
+"the market price of that day"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:68
+#, fuzzy
+#| msgid "Info"
+msgid "In"
+msgstr "Info"
+
+#: templates/investments/fragments/detail.html:74
+#: templates/investments/fragments/detail.html:78
+#, fuzzy
+#| msgid "Currency Code"
+msgid "currency effect"
+msgstr "Код валюти"
+
+#: templates/investments/fragments/detail.html:104
+#: templates/investments/fragments/detail.html:179
+msgid "Price"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:105
+#: templates/investments/fragments/detail.html:200
+#: templates/investments/fragments/detail.html:239
+#: templates/investments/fragments/detail.html:290
+#, fuzzy
+#| msgid "Warning"
+msgid "Earnings"
+msgstr "Попередження"
+
+#: templates/investments/fragments/detail.html:180
+#: templates/investments/fragments/detail.html:227
+msgid "Average cost"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:181
+#: templates/investments/fragments/rebalance.html:41
+msgid "Buy"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:182
+#: templates/investments/fragments/rebalance.html:43
+msgid "Sell"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:227
+#: templates/investments/fragments/detail.html:257
+msgid "Put in / taken out"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:239
+msgid "Unrealized"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:248
+msgid "Realized"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:271
+msgid "Movements"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:284
+msgid "Bought / contributed"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:284
+msgid "Sold / withdrew"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:288
+msgid "Money in"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:288
+msgid "Money out"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:290
+msgid "Gain"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:290
+msgid "Loss, tax or fee"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:315
+msgid "No movements yet"
+msgstr ""
+
+#: templates/investments/fragments/detail.html:315
+msgid "Use the + button to buy or contribute"
+msgstr ""
+
+#: templates/investments/fragments/list.html:21
+msgid "Holdings"
+msgstr ""
+
+#: templates/investments/fragments/list.html:60
+msgid "Some costs were estimated from market prices"
+msgstr ""
+
+#: templates/investments/fragments/list.html:92
+msgid "Charts"
+msgstr ""
+
+#: templates/investments/fragments/list.html:97
+#, fuzzy
+#| msgid "New balance"
+msgid "Rebalance"
+msgstr "Новий баланс"
+
+#: templates/investments/fragments/list.html:105
+msgid "No investments yet"
+msgstr ""
+
+#: templates/investments/fragments/list.html:106
+msgid "Mark an account as an investment in its settings to see it here"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:4
+msgid "No allocation targets"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:5
+msgid "Set a target for your asset classes to see what to buy and sell"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:26
+msgid "To reach the target"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:34
+msgid "Off target"
+msgstr ""
+
+#: templates/investments/fragments/rebalance.html:55
+msgid "Left out: investments without an asset class or a target"
+msgstr ""
+
#: templates/layouts/base.html:38
msgid "This is a demo!"
msgstr ""
@@ -3711,11 +4166,6 @@ msgstr ""
msgid "Order by"
msgstr ""
-#: templates/net_worth/net_worth.html:52
-#: templates/yearly_overview/pages/overview_by_currency.html:6
-msgid "By currency"
-msgstr ""
-
#: templates/net_worth/net_worth.html:135
msgid "Evolution"
msgstr ""
@@ -4169,3 +4619,9 @@ msgstr "Увiйти за допомогою"
#: templates/yearly_overview/pages/yearly_overview_base.html:6
msgid "Yearly Overview"
msgstr ""
+
+#~ msgid ""
+#~ "Create one exchange rate and keep updating it. Avoids database clutter."
+#~ msgstr ""
+#~ "Створіть один курс обміну та оновлюйте його постійно. Це запобігає "
+#~ "засміченню бази даних."
diff --git a/app/locale/zh_Hant/LC_MESSAGES/django.po b/app/locale/zh_Hant/LC_MESSAGES/django.po
index 600568a6..90aeda74 100644
--- a/app/locale/zh_Hant/LC_MESSAGES/django.po
+++ b/app/locale/zh_Hant/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-11-08 06:01+0000\n"
+"POT-Creation-Date: 2026-11-08 18:27+0000\n"
"PO-Revision-Date: 2025-10-08 16:17+0000\n"
"Last-Translator: doody \n"
"Language-Team: Chinese (Traditional Han script)
-
-
-
-
- {{ message.tags | toast_title }}
-
{{ message }}
-
-
-
+
+
+ {{ message.tags | toast_title }}
+
{{ message }}
+ {# -mt-1 centers the 2rem button on the 1.5rem title line #}
+
{% endfor %}
{% endif %}
diff --git a/app/templates/crispy-daisyui/layout/switch.html b/app/templates/crispy-daisyui/layout/switch.html
index f45e63dc..48ffbe29 100644
--- a/app/templates/crispy-daisyui/layout/switch.html
+++ b/app/templates/crispy-daisyui/layout/switch.html
@@ -11,7 +11,7 @@
{% crispy_field field 'class' 'toggle toggle-sm' %}
{% endif %}
{{ field.label }}{% if field.field.required %}*{% endif %}
- {% include 'crispy-daisyui/layout/help_text_and_errors.html' %}
+ {% include 'crispy-daisyui/layout/help_text_and_errors.html' %}
{% if tag %}{{ tag }}{% else %}fieldset{% endif %}>
{% endif %}
diff --git a/app/templates/includes/sidebar.html b/app/templates/includes/sidebar.html
index 2ed7d4a3..8d398600 100644
--- a/app/templates/includes/sidebar.html
+++ b/app/templates/includes/sidebar.html
@@ -114,6 +114,12 @@
+
+
+ active="tags_index||entities_index||categories_index||accounts_index||account_groups_index||investment_classes_index||currencies_index||exchange_rates_index||rules_index||import_profiles_index||automatic_exchange_rates_index||bank_sync_index||export_index||users_index">
+
+
+
+
+ {% crispy form %}
+
+{% endblock %}
diff --git a/app/templates/investment_classes/fragments/list.html b/app/templates/investment_classes/fragments/list.html
new file mode 100644
index 00000000..e40ba58c
--- /dev/null
+++ b/app/templates/investment_classes/fragments/list.html
@@ -0,0 +1,78 @@
+{% load i18n %}
+
+
+
+
+ {% spaceless %}
+
{% translate 'Asset classes' %}
+ {% endspaceless %}
+
+
+
+
+ {% if classes %}
+ {% if total_target and total_target != 100 %}
+
+
+ {% blocktranslate with total=total_target|floatformat:"-2" %}Targets add up to {{ total }}%. Set them to 100% to see the rebalance.{% endblocktranslate %}
+
+ {% if perf.unrealized is not None %}
+
+ {% include 'investments/fragments/_percentage.html' with value=perf.unrealized_percentage css="text-xs" %}
+ {% else %}-{% endif %}
+
+
+ {% blocktranslate with total=total_target|floatformat:"-2" %}Targets add up to {{ total }}%. Set them to 100% to see the rebalance.{% endblocktranslate %}
+ {% translate 'Asset classes' %}
+
+{% else %}
+ {% for plan in plans %}
+ {% with c=plan.currency %}
+