mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-09-07 18:31:53 +02:00
Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d500bef481 | ||
|
|
f280fcb172 | ||
|
|
6c808eff38 | ||
|
|
1690a153f9 | ||
|
|
7116176c78 | ||
|
|
d9c9cbe7c3 | ||
|
|
11b03474c1 | ||
|
|
e2f26b3629 | ||
|
|
10c8fcfb97 | ||
|
|
039ad225d3 | ||
|
|
18d4ab7d11 | ||
|
|
c64a363126 | ||
|
|
68a9286ce5 | ||
|
|
1357688e7b | ||
|
|
ba1f421ad3 | ||
|
|
f60f86a5cb | ||
|
|
5588eb33b1 | ||
|
|
934e6bd8e0 | ||
|
|
7933f1c316 | ||
|
|
00f87bea3d | ||
|
|
53ed0d791a | ||
|
|
5147d939ef | ||
|
|
716bd6f57f | ||
|
|
ded4cb37d5 | ||
|
|
cd48edddab | ||
|
|
9e135560fe | ||
|
|
bdbccdec16 | ||
|
|
907c511b59 | ||
|
|
33f0904a0f | ||
|
|
1c4f23a69e | ||
|
|
8373732964 | ||
|
|
8e09f3e8d8 | ||
|
|
1516fe0281 | ||
|
|
3bd16602a0 | ||
|
|
83c2b599d7 | ||
|
|
f9eab3ce17 | ||
|
|
62a33ddd2c | ||
|
|
ea3e9fd68f | ||
|
|
d71bf3ccb3 | ||
|
|
350d78356c | ||
|
|
91dfba519c | ||
|
|
0c9b41182b | ||
|
|
7a900908dd | ||
|
|
dcd52ec189 | ||
|
|
93e73b9aa6 | ||
|
|
7947d5f874 | ||
|
|
2ab6621205 | ||
|
|
39c15eedec | ||
|
|
6dcf5a63f8 | ||
|
|
11a6b3faa6 | ||
|
|
9b1312b744 | ||
|
|
72707f46e6 | ||
|
|
cf908ae87e | ||
|
|
b145d10fbd | ||
|
|
ee39dcf9ef | ||
|
|
5d330ddfbf |
@@ -65,7 +65,6 @@ jobs:
|
|||||||
if: steps.check_changes.outputs.changes_detected == 'true'
|
if: steps.check_changes.outputs.changes_detected == 'true'
|
||||||
uses: stefanzweifel/git-auto-commit-action@v5
|
uses: stefanzweifel/git-auto-commit-action@v5
|
||||||
with:
|
with:
|
||||||
push_options: --force
|
|
||||||
commit_message: |
|
commit_message: |
|
||||||
chore(locale): update translation files
|
chore(locale): update translation files
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.accounts.forms import AccountGroupForm
|
from apps.accounts.forms import AccountGroupForm
|
||||||
from apps.accounts.models import AccountGroup
|
from apps.accounts.models import AccountGroup
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
|
from apps.common.functions.permissions import (
|
||||||
|
EDIT,
|
||||||
|
READ,
|
||||||
|
get_shared_object_or_error,
|
||||||
|
)
|
||||||
from apps.common.models import SharedObject
|
from apps.common.models import SharedObject
|
||||||
from apps.common.forms import SharedObjectForm
|
from apps.common.forms import SharedObjectForm
|
||||||
|
|
||||||
@@ -63,17 +69,7 @@ def account_group_add(request, **kwargs):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def account_group_edit(request, pk):
|
def account_group_edit(request, pk):
|
||||||
account_group = get_object_or_404(AccountGroup, id=pk)
|
account_group = get_shared_object_or_error(AccountGroup, request, id=pk, level=EDIT)
|
||||||
|
|
||||||
if account_group.owner and account_group.owner != request.user:
|
|
||||||
messages.error(request, _("Only the owner can edit this"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
status=204,
|
|
||||||
headers={
|
|
||||||
"HX-Trigger": "updated, hide_offcanvas",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = AccountGroupForm(request.POST, instance=account_group)
|
form = AccountGroupForm(request.POST, instance=account_group)
|
||||||
@@ -101,17 +97,18 @@ def account_group_edit(request, pk):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def account_group_delete(request, pk):
|
def account_group_delete(request, pk):
|
||||||
account_group = get_object_or_404(AccountGroup, id=pk)
|
account_group = get_shared_object_or_error(AccountGroup, request, id=pk, level=READ)
|
||||||
|
|
||||||
if (
|
if account_group.is_editable_by(request.user):
|
||||||
account_group.owner != request.user
|
account_group.delete()
|
||||||
and request.user in account_group.shared_with.all()
|
messages.success(request, _("Account Group deleted successfully"))
|
||||||
):
|
elif account_group.shared_with.filter(pk=request.user.pk).exists():
|
||||||
|
# Someone else's object shared with us: we can drop our own access
|
||||||
|
# to it, but never delete it.
|
||||||
account_group.shared_with.remove(request.user)
|
account_group.shared_with.remove(request.user)
|
||||||
messages.success(request, _("Item no longer shared with you"))
|
messages.success(request, _("Item no longer shared with you"))
|
||||||
else:
|
else:
|
||||||
account_group.delete()
|
raise PermissionDenied
|
||||||
messages.success(request, _("Account Group deleted successfully"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
status=204,
|
status=204,
|
||||||
@@ -125,7 +122,7 @@ def account_group_delete(request, pk):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def account_group_take_ownership(request, pk):
|
def account_group_take_ownership(request, pk):
|
||||||
account_group = get_object_or_404(AccountGroup, id=pk)
|
account_group = get_shared_object_or_error(AccountGroup, request, id=pk, level=EDIT)
|
||||||
|
|
||||||
if not account_group.owner:
|
if not account_group.owner:
|
||||||
account_group.owner = request.user
|
account_group.owner = request.user
|
||||||
@@ -146,17 +143,7 @@ def account_group_take_ownership(request, pk):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def account_group_share(request, pk):
|
def account_group_share(request, pk):
|
||||||
obj = get_object_or_404(AccountGroup, id=pk)
|
obj = get_shared_object_or_error(AccountGroup, request, id=pk, level=EDIT)
|
||||||
|
|
||||||
if obj.owner and obj.owner != request.user:
|
|
||||||
messages.error(request, _("Only the owner can edit this"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
status=204,
|
|
||||||
headers={
|
|
||||||
"HX-Trigger": "updated, hide_offcanvas",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.accounts.forms import AccountForm
|
from apps.accounts.forms import AccountForm
|
||||||
from apps.accounts.models import Account
|
from apps.accounts.models import Account
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
|
from apps.common.functions.permissions import (
|
||||||
|
EDIT,
|
||||||
|
READ,
|
||||||
|
get_shared_object_or_error,
|
||||||
|
)
|
||||||
from apps.common.models import SharedObject
|
from apps.common.models import SharedObject
|
||||||
from apps.common.forms import SharedObjectForm
|
from apps.common.forms import SharedObjectForm
|
||||||
|
|
||||||
@@ -63,16 +69,7 @@ def account_add(request, **kwargs):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def account_edit(request, pk):
|
def account_edit(request, pk):
|
||||||
account = get_object_or_404(Account, id=pk)
|
account = get_shared_object_or_error(Account, request, id=pk, level=EDIT)
|
||||||
if account.owner and account.owner != request.user:
|
|
||||||
messages.error(request, _("Only the owner can edit this"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
status=204,
|
|
||||||
headers={
|
|
||||||
"HX-Trigger": "updated, hide_offcanvas",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = AccountForm(request.POST, instance=account)
|
form = AccountForm(request.POST, instance=account)
|
||||||
@@ -100,17 +97,7 @@ def account_edit(request, pk):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def account_share(request, pk):
|
def account_share(request, pk):
|
||||||
obj = get_object_or_404(Account, id=pk)
|
obj = get_shared_object_or_error(Account, request, id=pk, level=EDIT)
|
||||||
|
|
||||||
if obj.owner and obj.owner != request.user:
|
|
||||||
messages.error(request, _("Only the owner can edit this"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
status=204,
|
|
||||||
headers={
|
|
||||||
"HX-Trigger": "updated, hide_offcanvas",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
||||||
@@ -138,14 +125,18 @@ def account_share(request, pk):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def account_delete(request, pk):
|
def account_delete(request, pk):
|
||||||
account = get_object_or_404(Account, id=pk)
|
account = get_shared_object_or_error(Account, request, id=pk, level=READ)
|
||||||
|
|
||||||
if account.owner != request.user and request.user in account.shared_with.all():
|
if account.is_editable_by(request.user):
|
||||||
|
account.delete()
|
||||||
|
messages.success(request, _("Account deleted successfully"))
|
||||||
|
elif account.shared_with.filter(pk=request.user.pk).exists():
|
||||||
|
# Someone else's object shared with us: we can drop our own access
|
||||||
|
# to it, but never delete it.
|
||||||
account.shared_with.remove(request.user)
|
account.shared_with.remove(request.user)
|
||||||
messages.success(request, _("Item no longer shared with you"))
|
messages.success(request, _("Item no longer shared with you"))
|
||||||
else:
|
else:
|
||||||
account.delete()
|
raise PermissionDenied
|
||||||
messages.success(request, _("Account deleted successfully"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
status=204,
|
status=204,
|
||||||
@@ -159,7 +150,9 @@ def account_delete(request, pk):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def account_toggle_untracked(request, pk):
|
def account_toggle_untracked(request, pk):
|
||||||
account = get_object_or_404(Account, id=pk)
|
# Only flips the calling user's own row in untracked_by, so visibility --
|
||||||
|
# not ownership -- is the right bar here.
|
||||||
|
account = get_shared_object_or_error(Account, request, id=pk, level=READ)
|
||||||
if account.is_untracked_by():
|
if account.is_untracked_by():
|
||||||
account.untracked_by.remove(request.user)
|
account.untracked_by.remove(request.user)
|
||||||
messages.success(request, _("Account is now tracked"))
|
messages.success(request, _("Account is now tracked"))
|
||||||
@@ -179,7 +172,7 @@ def account_toggle_untracked(request, pk):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def account_take_ownership(request, pk):
|
def account_take_ownership(request, pk):
|
||||||
account = get_object_or_404(Account, id=pk)
|
account = get_shared_object_or_error(Account, request, id=pk, level=EDIT)
|
||||||
|
|
||||||
if not account.owner:
|
if not account.owner:
|
||||||
account.owner = request.user
|
account.owner = request.user
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
from rest_framework.permissions import BasePermission
|
from rest_framework.permissions import (
|
||||||
|
SAFE_METHODS,
|
||||||
|
BasePermission,
|
||||||
|
DjangoModelPermissions,
|
||||||
|
)
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
@@ -8,3 +12,37 @@ class NotInDemoMode(BasePermission):
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class SharedObjectPermission(BasePermission):
|
||||||
|
"""Object-level ownership check for SharedObject-backed viewsets.
|
||||||
|
|
||||||
|
DjangoModelPermissions is model-level: a user holding ``change_account``
|
||||||
|
may write any object the viewset's queryset returns, and for SharedObject
|
||||||
|
that queryset includes other people's public and shared-with-them objects.
|
||||||
|
Sharing grants read access only, so writes are restricted to the owner
|
||||||
|
here as well.
|
||||||
|
|
||||||
|
Set ``shared_object_via`` on the viewset when the governing SharedObject is
|
||||||
|
reached through a relation (e.g. ``"strategy"`` for a DCA entry).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
if request.method in SAFE_METHODS:
|
||||||
|
return True
|
||||||
|
|
||||||
|
guard = obj
|
||||||
|
via = getattr(view, "shared_object_via", None)
|
||||||
|
for attr in via.split(".") if via else []:
|
||||||
|
guard = getattr(guard, attr)
|
||||||
|
|
||||||
|
return guard.is_editable_by(request.user)
|
||||||
|
|
||||||
|
|
||||||
|
#: Default permissions plus the object-level ownership check. Assigning
|
||||||
|
#: ``permission_classes`` replaces the defaults, so they are repeated here.
|
||||||
|
SHARED_OBJECT_PERMISSIONS = [
|
||||||
|
NotInDemoMode,
|
||||||
|
DjangoModelPermissions,
|
||||||
|
SharedObjectPermission,
|
||||||
|
]
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ from .test_imports import *
|
|||||||
from .test_accounts import *
|
from .test_accounts import *
|
||||||
from .test_data_isolation import *
|
from .test_data_isolation import *
|
||||||
from .test_shared_access import *
|
from .test_shared_access import *
|
||||||
|
from .test_object_permissions import *
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
"""Object-level ownership on the SharedObject API viewsets.
|
||||||
|
|
||||||
|
DjangoModelPermissions is model-level: a user holding ``change_account`` could
|
||||||
|
write any object the viewset's queryset returned, and for SharedObject that
|
||||||
|
queryset includes other people's public and shared-with-them objects. Ordinary
|
||||||
|
users hold no model permissions, so this was not reachable for them, but the
|
||||||
|
object-level check was missing entirely.
|
||||||
|
|
||||||
|
Reads are unaffected -- they stay governed by SharedObjectManager.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
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 import status
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from apps.accounts.models import Account
|
||||||
|
from apps.currencies.models import Currency
|
||||||
|
from apps.dca.models import DCAEntry, DCAStrategy
|
||||||
|
from apps.transactions.models import TransactionCategory
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
STORAGES={
|
||||||
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WHITENOISE_AUTOREFRESH=True,
|
||||||
|
DEMO=False,
|
||||||
|
)
|
||||||
|
class SharedObjectAPIPermissionTests(TestCase):
|
||||||
|
"""The attacker here deliberately HOLDS the Django model permissions.
|
||||||
|
|
||||||
|
Without them DjangoModelPermissions already answers 403 and the
|
||||||
|
object-level check is never consulted, so the test would pass whether or
|
||||||
|
not it exists.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
User = get_user_model()
|
||||||
|
self.owner = User.objects.create_user(
|
||||||
|
email="owner@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.attacker = User.objects.create_user(
|
||||||
|
email="attacker@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.attacker.user_permissions.set(
|
||||||
|
Permission.objects.filter(
|
||||||
|
codename__in=[
|
||||||
|
"add_account",
|
||||||
|
"change_account",
|
||||||
|
"delete_account",
|
||||||
|
"add_transactioncategory",
|
||||||
|
"change_transactioncategory",
|
||||||
|
"delete_transactioncategory",
|
||||||
|
"add_dcaentry",
|
||||||
|
"change_dcaentry",
|
||||||
|
"delete_dcaentry",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Permissions are cached on the user instance.
|
||||||
|
self.attacker = User.objects.get(pk=self.attacker.pk)
|
||||||
|
|
||||||
|
self.currency = Currency.objects.create(
|
||||||
|
code="USD", name="US Dollar", decimal_places=2
|
||||||
|
)
|
||||||
|
|
||||||
|
self.api = APIClient()
|
||||||
|
self.api.force_authenticate(user=self.attacker)
|
||||||
|
|
||||||
|
def test_cannot_modify_public_account(self):
|
||||||
|
account = Account.all_objects.create(
|
||||||
|
name="Public account",
|
||||||
|
currency=self.currency,
|
||||||
|
owner=self.owner,
|
||||||
|
visibility="public",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.api.patch(
|
||||||
|
f"/api/accounts/{account.id}/", {"name": "HIJACKED"}, format="json"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
account.refresh_from_db()
|
||||||
|
self.assertEqual(account.name, "Public account")
|
||||||
|
|
||||||
|
def test_cannot_delete_account_shared_with_them(self):
|
||||||
|
account = Account.all_objects.create(
|
||||||
|
name="Shared account",
|
||||||
|
currency=self.currency,
|
||||||
|
owner=self.owner,
|
||||||
|
visibility="private",
|
||||||
|
)
|
||||||
|
account.shared_with.add(self.attacker)
|
||||||
|
|
||||||
|
response = self.api.delete(f"/api/accounts/{account.id}/")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertTrue(Account.all_objects.filter(pk=account.pk).exists())
|
||||||
|
|
||||||
|
def test_cannot_delete_public_category(self):
|
||||||
|
category = TransactionCategory.all_objects.create(
|
||||||
|
name="Public category", owner=self.owner, visibility="public"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.api.delete(f"/api/categories/{category.id}/")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertTrue(TransactionCategory.all_objects.filter(pk=category.pk).exists())
|
||||||
|
|
||||||
|
def test_cannot_delete_entry_on_public_strategy(self):
|
||||||
|
strategy = DCAStrategy.all_objects.create(
|
||||||
|
name="Public strategy",
|
||||||
|
owner=self.owner,
|
||||||
|
visibility="public",
|
||||||
|
target_currency=self.currency,
|
||||||
|
payment_currency=self.currency,
|
||||||
|
)
|
||||||
|
entry = DCAEntry.objects.create(
|
||||||
|
strategy=strategy,
|
||||||
|
date=date(2025, 1, 1),
|
||||||
|
amount_paid=Decimal("100"),
|
||||||
|
amount_received=Decimal("1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.api.delete(f"/api/dca/entries/{entry.id}/")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertTrue(DCAEntry.objects.filter(pk=entry.pk).exists())
|
||||||
|
|
||||||
|
def test_reads_of_shared_objects_still_work(self):
|
||||||
|
account = Account.all_objects.create(
|
||||||
|
name="Public account",
|
||||||
|
currency=self.currency,
|
||||||
|
owner=self.owner,
|
||||||
|
visibility="public",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.api.get(f"/api/accounts/{account.id}/")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def test_owner_can_still_write_their_own_objects(self):
|
||||||
|
owner_client = APIClient()
|
||||||
|
self.owner.user_permissions.set(
|
||||||
|
Permission.objects.filter(codename__in=["change_account", "delete_account"])
|
||||||
|
)
|
||||||
|
owner = get_user_model().objects.get(pk=self.owner.pk)
|
||||||
|
owner_client.force_authenticate(user=owner)
|
||||||
|
|
||||||
|
account = Account.all_objects.create(
|
||||||
|
name="Own account",
|
||||||
|
currency=self.currency,
|
||||||
|
owner=owner,
|
||||||
|
visibility="private",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = owner_client.patch(
|
||||||
|
f"/api/accounts/{account.id}/", {"name": "Renamed"}, format="json"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
account.refresh_from_db()
|
||||||
|
self.assertEqual(account.name, "Renamed")
|
||||||
@@ -6,6 +6,7 @@ from rest_framework.response import Response
|
|||||||
|
|
||||||
from apps.accounts.models import AccountGroup, Account
|
from apps.accounts.models import AccountGroup, Account
|
||||||
from apps.accounts.services import get_account_balance
|
from apps.accounts.services import get_account_balance
|
||||||
|
from apps.api.permissions import SHARED_OBJECT_PERMISSIONS
|
||||||
from apps.api.serializers import (
|
from apps.api.serializers import (
|
||||||
AccountGroupSerializer,
|
AccountGroupSerializer,
|
||||||
AccountSerializer,
|
AccountSerializer,
|
||||||
@@ -16,6 +17,7 @@ from apps.api.serializers import (
|
|||||||
class AccountGroupViewSet(viewsets.ModelViewSet):
|
class AccountGroupViewSet(viewsets.ModelViewSet):
|
||||||
"""ViewSet for managing account groups."""
|
"""ViewSet for managing account groups."""
|
||||||
|
|
||||||
|
permission_classes = SHARED_OBJECT_PERMISSIONS
|
||||||
queryset = AccountGroup.objects.all()
|
queryset = AccountGroup.objects.all()
|
||||||
serializer_class = AccountGroupSerializer
|
serializer_class = AccountGroupSerializer
|
||||||
filterset_fields = {
|
filterset_fields = {
|
||||||
@@ -40,6 +42,7 @@ class AccountGroupViewSet(viewsets.ModelViewSet):
|
|||||||
class AccountViewSet(viewsets.ModelViewSet):
|
class AccountViewSet(viewsets.ModelViewSet):
|
||||||
"""ViewSet for managing accounts."""
|
"""ViewSet for managing accounts."""
|
||||||
|
|
||||||
|
permission_classes = SHARED_OBJECT_PERMISSIONS
|
||||||
queryset = Account.objects.all()
|
queryset = Account.objects.all()
|
||||||
serializer_class = AccountSerializer
|
serializer_class = AccountSerializer
|
||||||
filterset_fields = {
|
filterset_fields = {
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ from rest_framework import viewsets
|
|||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from apps.dca.models import DCAStrategy, DCAEntry
|
from apps.dca.models import DCAStrategy, DCAEntry
|
||||||
|
from apps.api.permissions import SHARED_OBJECT_PERMISSIONS
|
||||||
from apps.api.serializers import DCAStrategySerializer, DCAEntrySerializer
|
from apps.api.serializers import DCAStrategySerializer, DCAEntrySerializer
|
||||||
|
|
||||||
|
|
||||||
class DCAStrategyViewSet(viewsets.ModelViewSet):
|
class DCAStrategyViewSet(viewsets.ModelViewSet):
|
||||||
|
permission_classes = SHARED_OBJECT_PERMISSIONS
|
||||||
queryset = DCAStrategy.objects.all()
|
queryset = DCAStrategy.objects.all()
|
||||||
serializer_class = DCAStrategySerializer
|
serializer_class = DCAStrategySerializer
|
||||||
filterset_fields = {
|
filterset_fields = {
|
||||||
@@ -43,6 +45,8 @@ class DCAStrategyViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class DCAEntryViewSet(viewsets.ModelViewSet):
|
class DCAEntryViewSet(viewsets.ModelViewSet):
|
||||||
|
permission_classes = SHARED_OBJECT_PERMISSIONS
|
||||||
|
shared_object_via = "strategy"
|
||||||
queryset = DCAEntry.objects.all()
|
queryset = DCAEntry.objects.all()
|
||||||
serializer_class = DCAEntrySerializer
|
serializer_class = DCAEntrySerializer
|
||||||
filterset_fields = {
|
filterset_fields = {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from apps.transactions.models import (
|
|||||||
RecurringTransaction,
|
RecurringTransaction,
|
||||||
)
|
)
|
||||||
from apps.rules.signals import transaction_updated, transaction_created
|
from apps.rules.signals import transaction_updated, transaction_created
|
||||||
|
from apps.api.permissions import SHARED_OBJECT_PERMISSIONS
|
||||||
|
|
||||||
|
|
||||||
class TransactionViewSet(viewsets.ModelViewSet):
|
class TransactionViewSet(viewsets.ModelViewSet):
|
||||||
@@ -68,6 +69,7 @@ class TransactionViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class TransactionCategoryViewSet(viewsets.ModelViewSet):
|
class TransactionCategoryViewSet(viewsets.ModelViewSet):
|
||||||
|
permission_classes = SHARED_OBJECT_PERMISSIONS
|
||||||
queryset = TransactionCategory.objects.all()
|
queryset = TransactionCategory.objects.all()
|
||||||
serializer_class = TransactionCategorySerializer
|
serializer_class = TransactionCategorySerializer
|
||||||
filterset_fields = {
|
filterset_fields = {
|
||||||
@@ -85,6 +87,7 @@ class TransactionCategoryViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class TransactionTagViewSet(viewsets.ModelViewSet):
|
class TransactionTagViewSet(viewsets.ModelViewSet):
|
||||||
|
permission_classes = SHARED_OBJECT_PERMISSIONS
|
||||||
queryset = TransactionTag.objects.all()
|
queryset = TransactionTag.objects.all()
|
||||||
serializer_class = TransactionTagSerializer
|
serializer_class = TransactionTagSerializer
|
||||||
filterset_fields = {
|
filterset_fields = {
|
||||||
@@ -101,6 +104,7 @@ class TransactionTagViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class TransactionEntityViewSet(viewsets.ModelViewSet):
|
class TransactionEntityViewSet(viewsets.ModelViewSet):
|
||||||
|
permission_classes = SHARED_OBJECT_PERMISSIONS
|
||||||
queryset = TransactionEntity.objects.all()
|
queryset = TransactionEntity.objects.all()
|
||||||
serializer_class = TransactionEntitySerializer
|
serializer_class = TransactionEntitySerializer
|
||||||
filterset_fields = {
|
filterset_fields = {
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.http import Http404
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
|
READ = "read"
|
||||||
|
EDIT = "edit"
|
||||||
|
|
||||||
|
|
||||||
|
def get_shared_object_or_error(klass, request, *, level=EDIT, via=None, **kwargs):
|
||||||
|
"""Fetch an object like ``get_object_or_404`` while enforcing access control.
|
||||||
|
|
||||||
|
``SharedObjectManager`` scopes querysets to what a user may *see*, which is
|
||||||
|
not the same as what they may *change*. Views that resolve an object from a
|
||||||
|
URL id must state which of the two they need, otherwise a shared or public
|
||||||
|
object becomes writable by anyone who can see it.
|
||||||
|
|
||||||
|
``level`` selects the check applied to the governing ``SharedObject``:
|
||||||
|
|
||||||
|
``READ``
|
||||||
|
The object must be visible to the user. Denial raises :class:`Http404`
|
||||||
|
so the response does not confirm that the id exists.
|
||||||
|
``EDIT``
|
||||||
|
The object must be owned by the user. Denial raises
|
||||||
|
:class:`~django.core.exceptions.PermissionDenied` (HTTP 403), which the
|
||||||
|
frontend surfaces as an "Access Denied" dialog. An object the user
|
||||||
|
cannot even see raises :class:`Http404` instead, so 403 never confirms
|
||||||
|
the existence of an object they were not allowed to know about.
|
||||||
|
|
||||||
|
Objects with no owner stay accessible to everyone, preserving the existing
|
||||||
|
behaviour for legacy/unowned objects.
|
||||||
|
|
||||||
|
``via`` is a dotted path to the ``SharedObject`` that governs access, for
|
||||||
|
models owned through a relation, e.g. a rule action governed by its parent
|
||||||
|
rule::
|
||||||
|
|
||||||
|
get_shared_object_or_error(
|
||||||
|
TransactionRuleAction, request, id=pk, level=EDIT, via="rule"
|
||||||
|
)
|
||||||
|
|
||||||
|
The path is resolved with a plain ``getattr``, so a path that does not
|
||||||
|
resolve raises ``AttributeError`` rather than silently granting access.
|
||||||
|
"""
|
||||||
|
obj = get_object_or_404(klass, **kwargs)
|
||||||
|
|
||||||
|
guard = obj
|
||||||
|
for attr in via.split(".") if via else []:
|
||||||
|
guard = getattr(guard, attr)
|
||||||
|
|
||||||
|
if level not in (READ, EDIT):
|
||||||
|
raise ValueError(f"Unknown access level: {level!r}")
|
||||||
|
|
||||||
|
if not guard.is_visible_to(request.user):
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
if level == EDIT and not guard.is_editable_by(request.user):
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
return obj
|
||||||
@@ -58,13 +58,35 @@ class SharedObject(models.Model):
|
|||||||
models.Index(fields=["visibility"]),
|
models.Index(fields=["visibility"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
def is_accessible_by(self, user):
|
# NOTE: these two predicates must stay in sync with the ``Q`` objects built
|
||||||
"""Check if a user can access this object"""
|
# by ``SharedObjectManager.get_queryset`` above. The manager filters at the
|
||||||
return (
|
# queryset level and these check a single instance, so they cannot share an
|
||||||
self.visibility == "public"
|
# implementation; ``SharedObjectPredicateParityTests`` asserts they agree.
|
||||||
or self.owner == user
|
def is_visible_to(self, user):
|
||||||
or (self.visibility == "shared" and user in self.shared_with.all())
|
"""Whether ``user`` may read this object.
|
||||||
)
|
|
||||||
|
Mirrors ``SharedObjectManager``: public objects, objects with no owner,
|
||||||
|
the owner's own objects, and objects explicitly shared with the user.
|
||||||
|
"""
|
||||||
|
if self.owner is None or self.visibility == "public":
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not user or not user.is_authenticated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.owner_id == user.pk or self.shared_with.filter(pk=user.pk).exists()
|
||||||
|
|
||||||
|
def is_editable_by(self, user):
|
||||||
|
"""Whether ``user`` may mutate this object.
|
||||||
|
|
||||||
|
Sharing grants read access only; mutation stays with the owner. Objects
|
||||||
|
with no owner remain editable by everyone, preserving the behaviour of
|
||||||
|
legacy/unowned objects.
|
||||||
|
"""
|
||||||
|
if self.owner is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return bool(user and user.is_authenticated and self.owner_id == user.pk)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.pk and not self.owner:
|
if not self.pk and not self.owner:
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.http import Http404
|
||||||
|
from django.test import RequestFactory, TestCase, override_settings
|
||||||
|
|
||||||
|
from apps.common.functions.permissions import (
|
||||||
|
EDIT,
|
||||||
|
READ,
|
||||||
|
get_shared_object_or_error,
|
||||||
|
)
|
||||||
|
from apps.common.middleware.thread_local import delete_current_user, write_current_user
|
||||||
|
from apps.rules.models import TransactionRule, TransactionRuleAction
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
STORAGES={
|
||||||
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WHITENOISE_AUTOREFRESH=True,
|
||||||
|
)
|
||||||
|
class SharedObjectPredicateTests(TestCase):
|
||||||
|
"""Unit tests for is_visible_to / is_editable_by on SharedObject."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
User = get_user_model()
|
||||||
|
self.owner = User.objects.create_user(
|
||||||
|
email="owner@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.shared_user = User.objects.create_user(
|
||||||
|
email="shared@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.stranger = User.objects.create_user(
|
||||||
|
email="stranger@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _rule(self, **kwargs):
|
||||||
|
kwargs.setdefault("name", "Rule")
|
||||||
|
kwargs.setdefault("trigger", "True")
|
||||||
|
return TransactionRule.all_objects.create(**kwargs)
|
||||||
|
|
||||||
|
def test_owner_can_read_and_edit_own_private_rule(self):
|
||||||
|
rule = self._rule(owner=self.owner, visibility="private")
|
||||||
|
|
||||||
|
self.assertTrue(rule.is_visible_to(self.owner))
|
||||||
|
self.assertTrue(rule.is_editable_by(self.owner))
|
||||||
|
|
||||||
|
def test_private_rule_is_invisible_to_stranger(self):
|
||||||
|
rule = self._rule(owner=self.owner, visibility="private")
|
||||||
|
|
||||||
|
self.assertFalse(rule.is_visible_to(self.stranger))
|
||||||
|
self.assertFalse(rule.is_editable_by(self.stranger))
|
||||||
|
|
||||||
|
def test_shared_rule_is_readable_but_not_editable(self):
|
||||||
|
rule = self._rule(owner=self.owner, visibility="private")
|
||||||
|
rule.shared_with.add(self.shared_user)
|
||||||
|
|
||||||
|
self.assertTrue(rule.is_visible_to(self.shared_user))
|
||||||
|
self.assertFalse(rule.is_editable_by(self.shared_user))
|
||||||
|
|
||||||
|
def test_public_rule_is_readable_but_not_editable(self):
|
||||||
|
rule = self._rule(owner=self.owner, visibility="public")
|
||||||
|
|
||||||
|
self.assertTrue(rule.is_visible_to(self.stranger))
|
||||||
|
self.assertFalse(rule.is_editable_by(self.stranger))
|
||||||
|
|
||||||
|
def test_unowned_rule_stays_readable_and_editable_by_everyone(self):
|
||||||
|
rule = self._rule(owner=None, visibility="private")
|
||||||
|
|
||||||
|
self.assertTrue(rule.is_visible_to(self.stranger))
|
||||||
|
self.assertTrue(rule.is_editable_by(self.stranger))
|
||||||
|
|
||||||
|
def test_anonymous_user_gets_no_access_to_owned_rules(self):
|
||||||
|
rule = self._rule(owner=self.owner, visibility="private")
|
||||||
|
|
||||||
|
self.assertFalse(rule.is_visible_to(AnonymousUser()))
|
||||||
|
self.assertFalse(rule.is_editable_by(AnonymousUser()))
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
STORAGES={
|
||||||
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WHITENOISE_AUTOREFRESH=True,
|
||||||
|
)
|
||||||
|
class SharedObjectPredicateParityTests(TestCase):
|
||||||
|
"""is_visible_to must agree with what SharedObjectManager returns.
|
||||||
|
|
||||||
|
The manager filters at the queryset level and the predicate checks a single
|
||||||
|
instance, so the two cannot share an implementation. This asserts they do
|
||||||
|
not drift apart.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
User = get_user_model()
|
||||||
|
self.owner = User.objects.create_user(
|
||||||
|
email="owner@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.other = User.objects.create_user(
|
||||||
|
email="other@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.addCleanup(self._clear_current_user)
|
||||||
|
|
||||||
|
def _clear_current_user(self):
|
||||||
|
try:
|
||||||
|
delete_current_user()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_manager_and_predicate_agree_over_every_combination(self):
|
||||||
|
combinations = []
|
||||||
|
for owner in (self.owner, self.other, None):
|
||||||
|
for visibility in ("private", "public"):
|
||||||
|
for shared in (True, False):
|
||||||
|
rule = TransactionRule.all_objects.create(
|
||||||
|
name=f"{owner}-{visibility}-{shared}",
|
||||||
|
trigger="True",
|
||||||
|
owner=owner,
|
||||||
|
visibility=visibility,
|
||||||
|
)
|
||||||
|
if shared:
|
||||||
|
rule.shared_with.add(self.owner)
|
||||||
|
combinations.append(rule)
|
||||||
|
|
||||||
|
write_current_user(self.owner)
|
||||||
|
visible_ids = set(TransactionRule.objects.values_list("id", flat=True))
|
||||||
|
|
||||||
|
for rule in combinations:
|
||||||
|
with self.subTest(rule=rule.name):
|
||||||
|
self.assertEqual(
|
||||||
|
rule.id in visible_ids,
|
||||||
|
rule.is_visible_to(self.owner),
|
||||||
|
f"manager and is_visible_to disagree for {rule.name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
STORAGES={
|
||||||
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WHITENOISE_AUTOREFRESH=True,
|
||||||
|
)
|
||||||
|
class GetSharedObjectOrErrorTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
User = get_user_model()
|
||||||
|
self.owner = User.objects.create_user(
|
||||||
|
email="owner@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.stranger = User.objects.create_user(
|
||||||
|
email="stranger@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
self.public_rule = TransactionRule.all_objects.create(
|
||||||
|
name="Public", trigger="True", owner=self.owner, visibility="public"
|
||||||
|
)
|
||||||
|
self.private_rule = TransactionRule.all_objects.create(
|
||||||
|
name="Private", trigger="True", owner=self.owner, visibility="private"
|
||||||
|
)
|
||||||
|
self.public_action = TransactionRuleAction.objects.create(
|
||||||
|
rule=self.public_rule, field="notes", value="x"
|
||||||
|
)
|
||||||
|
self.private_action = TransactionRuleAction.objects.create(
|
||||||
|
rule=self.private_rule, field="notes", value="x"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.addCleanup(self._clear_current_user)
|
||||||
|
|
||||||
|
def _clear_current_user(self):
|
||||||
|
try:
|
||||||
|
delete_current_user()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _request(self, user):
|
||||||
|
request = self.factory.get("/")
|
||||||
|
request.user = user
|
||||||
|
write_current_user(user)
|
||||||
|
return request
|
||||||
|
|
||||||
|
def test_read_allows_visible_object(self):
|
||||||
|
request = self._request(self.stranger)
|
||||||
|
|
||||||
|
rule = get_shared_object_or_error(
|
||||||
|
TransactionRule, request, id=self.public_rule.id, level=READ
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(rule, self.public_rule)
|
||||||
|
|
||||||
|
def test_edit_denies_visible_but_unowned_object_with_403(self):
|
||||||
|
request = self._request(self.stranger)
|
||||||
|
|
||||||
|
with self.assertRaises(PermissionDenied):
|
||||||
|
get_shared_object_or_error(
|
||||||
|
TransactionRule, request, id=self.public_rule.id, level=EDIT
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_edit_allows_owner(self):
|
||||||
|
request = self._request(self.owner)
|
||||||
|
|
||||||
|
rule = get_shared_object_or_error(
|
||||||
|
TransactionRule, request, id=self.public_rule.id, level=EDIT
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(rule, self.public_rule)
|
||||||
|
|
||||||
|
def test_invisible_object_raises_404_not_403(self):
|
||||||
|
"""403 must not confirm the existence of an object the user cannot see."""
|
||||||
|
request = self._request(self.stranger)
|
||||||
|
|
||||||
|
with self.assertRaises(Http404):
|
||||||
|
get_shared_object_or_error(
|
||||||
|
TransactionRule, request, id=self.private_rule.id, level=EDIT
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_via_traverses_to_the_governing_object(self):
|
||||||
|
request = self._request(self.stranger)
|
||||||
|
|
||||||
|
with self.assertRaises(PermissionDenied):
|
||||||
|
get_shared_object_or_error(
|
||||||
|
TransactionRuleAction,
|
||||||
|
request,
|
||||||
|
id=self.public_action.id,
|
||||||
|
level=EDIT,
|
||||||
|
via="rule",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_via_hides_children_of_invisible_parents_behind_404(self):
|
||||||
|
"""TransactionRuleAction has an unscoped manager, so the id is reachable."""
|
||||||
|
request = self._request(self.stranger)
|
||||||
|
|
||||||
|
with self.assertRaises(Http404):
|
||||||
|
get_shared_object_or_error(
|
||||||
|
TransactionRuleAction,
|
||||||
|
request,
|
||||||
|
id=self.private_action.id,
|
||||||
|
level=EDIT,
|
||||||
|
via="rule",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unresolvable_via_path_fails_closed(self):
|
||||||
|
"""A typo'd path must raise, never silently grant access."""
|
||||||
|
request = self._request(self.stranger)
|
||||||
|
|
||||||
|
with self.assertRaises(AttributeError):
|
||||||
|
get_shared_object_or_error(
|
||||||
|
TransactionRuleAction,
|
||||||
|
request,
|
||||||
|
id=self.public_action.id,
|
||||||
|
level=EDIT,
|
||||||
|
via="rulee",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unknown_level_is_rejected(self):
|
||||||
|
request = self._request(self.owner)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
get_shared_object_or_error(
|
||||||
|
TransactionRule, request, id=self.public_rule.id, level="write"
|
||||||
|
)
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
"""Delete semantics for every SharedObject-backed model.
|
||||||
|
|
||||||
|
Each of these views carried the same inverted condition::
|
||||||
|
|
||||||
|
if obj.owner != request.user and request.user in obj.shared_with.all():
|
||||||
|
obj.shared_with.remove(request.user) # unshare
|
||||||
|
else:
|
||||||
|
obj.delete() # <- public objects landed here
|
||||||
|
|
||||||
|
so an object its owner had made public could be destroyed by any authenticated
|
||||||
|
user. The rule is: the owner deletes, a shared user revokes only their own
|
||||||
|
access, and nobody else may do either.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from apps.accounts.models import Account, AccountGroup
|
||||||
|
from apps.currencies.models import Currency
|
||||||
|
from apps.dca.models import DCAStrategy
|
||||||
|
from apps.rules.models import TransactionRule
|
||||||
|
from apps.transactions.models import (
|
||||||
|
TransactionCategory,
|
||||||
|
TransactionEntity,
|
||||||
|
TransactionTag,
|
||||||
|
)
|
||||||
|
|
||||||
|
HTMX = {"HTTP_HX_REQUEST": "true"}
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
STORAGES={
|
||||||
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WHITENOISE_AUTOREFRESH=True,
|
||||||
|
DEMO=False,
|
||||||
|
)
|
||||||
|
class SharedObjectDeletionTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
User = get_user_model()
|
||||||
|
self.owner = User.objects.create_user(
|
||||||
|
email="owner@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.shared_user = User.objects.create_user(
|
||||||
|
email="shared@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.stranger = User.objects.create_user(
|
||||||
|
email="stranger@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.currency = Currency.objects.create(
|
||||||
|
code="USD", name="US Dollar", decimal_places=2
|
||||||
|
)
|
||||||
|
|
||||||
|
def cases(self):
|
||||||
|
"""(label, model, delete url name, url kwarg, extra create kwargs)."""
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
"account",
|
||||||
|
Account,
|
||||||
|
"account_delete",
|
||||||
|
"pk",
|
||||||
|
{"currency": self.currency},
|
||||||
|
),
|
||||||
|
("account group", AccountGroup, "account_group_delete", "pk", {}),
|
||||||
|
(
|
||||||
|
"category",
|
||||||
|
TransactionCategory,
|
||||||
|
"category_delete",
|
||||||
|
"category_id",
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
("tag", TransactionTag, "tag_delete", "tag_id", {}),
|
||||||
|
("entity", TransactionEntity, "entity_delete", "entity_id", {}),
|
||||||
|
(
|
||||||
|
"rule",
|
||||||
|
TransactionRule,
|
||||||
|
"transaction_rule_delete",
|
||||||
|
"transaction_rule_id",
|
||||||
|
{"trigger": "True"},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"dca strategy",
|
||||||
|
DCAStrategy,
|
||||||
|
"dca_strategy_delete",
|
||||||
|
"strategy_id",
|
||||||
|
{
|
||||||
|
"target_currency": self.currency,
|
||||||
|
"payment_currency": self.currency,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def make(self, model, visibility, extra):
|
||||||
|
return model.all_objects.create(
|
||||||
|
name="Target", owner=self.owner, visibility=visibility, **extra
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, url_name, kwarg, obj):
|
||||||
|
return self.client.delete(reverse(url_name, kwargs={kwarg: obj.id}), **HTMX)
|
||||||
|
|
||||||
|
def test_stranger_cannot_delete_a_public_object(self):
|
||||||
|
for label, model, url_name, kwarg, extra in self.cases():
|
||||||
|
with self.subTest(model=label):
|
||||||
|
obj = self.make(model, "public", extra)
|
||||||
|
self.client.force_login(self.stranger)
|
||||||
|
|
||||||
|
response = self.delete(url_name, kwarg, obj)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403, label)
|
||||||
|
self.assertTrue(
|
||||||
|
model.all_objects.filter(pk=obj.pk).exists(),
|
||||||
|
f"{label} was deleted by a non-owner",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_shared_user_deleting_only_revokes_their_own_access(self):
|
||||||
|
for label, model, url_name, kwarg, extra in self.cases():
|
||||||
|
with self.subTest(model=label):
|
||||||
|
obj = self.make(model, "private", extra)
|
||||||
|
obj.shared_with.add(self.shared_user)
|
||||||
|
self.client.force_login(self.shared_user)
|
||||||
|
|
||||||
|
response = self.delete(url_name, kwarg, obj)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 204, label)
|
||||||
|
self.assertTrue(
|
||||||
|
model.all_objects.filter(pk=obj.pk).exists(),
|
||||||
|
f"{label} was deleted by a shared user",
|
||||||
|
)
|
||||||
|
self.assertNotIn(self.shared_user, obj.shared_with.all())
|
||||||
|
|
||||||
|
def test_owner_can_delete(self):
|
||||||
|
for label, model, url_name, kwarg, extra in self.cases():
|
||||||
|
with self.subTest(model=label):
|
||||||
|
obj = self.make(model, "private", extra)
|
||||||
|
self.client.force_login(self.owner)
|
||||||
|
|
||||||
|
response = self.delete(url_name, kwarg, obj)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 204, label)
|
||||||
|
self.assertFalse(
|
||||||
|
model.all_objects.filter(pk=obj.pk).exists(),
|
||||||
|
f"{label} was not deleted by its owner",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unowned_objects_stay_deletable(self):
|
||||||
|
"""Legacy objects with no owner are editable by everyone by design."""
|
||||||
|
for label, model, url_name, kwarg, extra in self.cases():
|
||||||
|
with self.subTest(model=label):
|
||||||
|
obj = model.all_objects.create(
|
||||||
|
name="Legacy", owner=None, visibility="private", **extra
|
||||||
|
)
|
||||||
|
self.client.force_login(self.stranger)
|
||||||
|
|
||||||
|
response = self.delete(url_name, kwarg, obj)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 204, label)
|
||||||
|
self.assertFalse(model.all_objects.filter(pk=obj.pk).exists(), label)
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
"""Object-level authorization tests for the DCA views.
|
||||||
|
|
||||||
|
DCAEntry has an unscoped default manager, so filtering an entry by
|
||||||
|
``strategy__id`` alone reached entries belonging to strategies the caller could
|
||||||
|
not see at all -- a strictly wider hole than the one reported for rules in
|
||||||
|
GHSA-83g9-vjqf-2j5q, since it needs no public or shared strategy.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
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 apps.currencies.models import Currency
|
||||||
|
from apps.dca.models import DCAEntry, DCAStrategy
|
||||||
|
|
||||||
|
HTMX = {"HTTP_HX_REQUEST": "true"}
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
STORAGES={
|
||||||
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WHITENOISE_AUTOREFRESH=True,
|
||||||
|
DEMO=False,
|
||||||
|
)
|
||||||
|
class DCAObjectPermissionTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
User = get_user_model()
|
||||||
|
self.owner = User.objects.create_user(
|
||||||
|
email="owner@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.shared_user = User.objects.create_user(
|
||||||
|
email="shared@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.stranger = User.objects.create_user(
|
||||||
|
email="stranger@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.currency = Currency.objects.create(
|
||||||
|
code="USD", name="US Dollar", decimal_places=2
|
||||||
|
)
|
||||||
|
|
||||||
|
self.private_strategy = self._strategy("Private", visibility="private")
|
||||||
|
self.public_strategy = self._strategy("Public", visibility="public")
|
||||||
|
self.shared_strategy = self._strategy("Shared", visibility="private")
|
||||||
|
self.shared_strategy.shared_with.add(self.shared_user)
|
||||||
|
|
||||||
|
self.private_entry = self._entry(self.private_strategy)
|
||||||
|
self.public_entry = self._entry(self.public_strategy)
|
||||||
|
self.shared_entry = self._entry(self.shared_strategy)
|
||||||
|
|
||||||
|
def _strategy(self, name, visibility):
|
||||||
|
return DCAStrategy.all_objects.create(
|
||||||
|
name=name,
|
||||||
|
owner=self.owner,
|
||||||
|
visibility=visibility,
|
||||||
|
target_currency=self.currency,
|
||||||
|
payment_currency=self.currency,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _entry(self, strategy):
|
||||||
|
return DCAEntry.objects.create(
|
||||||
|
strategy=strategy,
|
||||||
|
date=date(2025, 1, 1),
|
||||||
|
amount_paid=Decimal("100"),
|
||||||
|
amount_received=Decimal("1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# entries on an invisible strategy: must not even confirm they exist
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_stranger_cannot_delete_entry_on_private_strategy(self):
|
||||||
|
self.client.force_login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"dca_entry_delete",
|
||||||
|
kwargs={
|
||||||
|
"strategy_id": self.private_strategy.id,
|
||||||
|
"entry_id": self.private_entry.id,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
self.assertTrue(DCAEntry.objects.filter(pk=self.private_entry.pk).exists())
|
||||||
|
|
||||||
|
def test_stranger_cannot_open_entry_edit_form_on_private_strategy(self):
|
||||||
|
self.client.force_login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"dca_entry_edit",
|
||||||
|
kwargs={
|
||||||
|
"strategy_id": self.private_strategy.id,
|
||||||
|
"entry_id": self.private_entry.id,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_stranger_cannot_edit_entry_on_private_strategy(self):
|
||||||
|
self.client.force_login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"dca_entry_edit",
|
||||||
|
kwargs={
|
||||||
|
"strategy_id": self.private_strategy.id,
|
||||||
|
"entry_id": self.private_entry.id,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
data={
|
||||||
|
"date": "2030-01-01",
|
||||||
|
"amount_paid": "999",
|
||||||
|
"amount_received": "999",
|
||||||
|
},
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
self.private_entry.refresh_from_db()
|
||||||
|
self.assertEqual(self.private_entry.amount_paid, Decimal("100"))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# entries on a visible-but-unowned strategy: 403, not 404
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_stranger_cannot_delete_entry_on_public_strategy(self):
|
||||||
|
self.client.force_login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"dca_entry_delete",
|
||||||
|
kwargs={
|
||||||
|
"strategy_id": self.public_strategy.id,
|
||||||
|
"entry_id": self.public_entry.id,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertTrue(DCAEntry.objects.filter(pk=self.public_entry.pk).exists())
|
||||||
|
|
||||||
|
def test_shared_user_cannot_add_entry_to_shared_strategy(self):
|
||||||
|
self.client.force_login(self.shared_user)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("dca_entry_add", kwargs={"strategy_id": self.shared_strategy.id}),
|
||||||
|
data={
|
||||||
|
"date": "2030-01-01",
|
||||||
|
"amount_paid": "5",
|
||||||
|
"amount_received": "5",
|
||||||
|
},
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(self.shared_strategy.entries.count(), 1)
|
||||||
|
|
||||||
|
def test_owner_can_still_manage_own_entries(self):
|
||||||
|
self.client.force_login(self.owner)
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"dca_entry_delete",
|
||||||
|
kwargs={
|
||||||
|
"strategy_id": self.private_strategy.id,
|
||||||
|
"entry_id": self.private_entry.id,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
self.assertFalse(DCAEntry.objects.filter(pk=self.private_entry.pk).exists())
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# reads stay open to shared users
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_shared_user_can_still_view_strategy_detail(self):
|
||||||
|
self.client.force_login(self.shared_user)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"dca_strategy_detail", kwargs={"strategy_id": self.shared_strategy.id}
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# strategy delete: the same inversion the rules views had
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_stranger_cannot_delete_public_strategy(self):
|
||||||
|
self.client.force_login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"dca_strategy_delete", kwargs={"strategy_id": self.public_strategy.id}
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertTrue(
|
||||||
|
DCAStrategy.all_objects.filter(pk=self.public_strategy.pk).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_shared_user_deleting_only_revokes_their_own_access(self):
|
||||||
|
self.client.force_login(self.shared_user)
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"dca_strategy_delete", kwargs={"strategy_id": self.shared_strategy.id}
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
self.assertTrue(
|
||||||
|
DCAStrategy.all_objects.filter(pk=self.shared_strategy.pk).exists()
|
||||||
|
)
|
||||||
|
self.assertNotIn(self.shared_user, self.shared_strategy.shared_with.all())
|
||||||
|
|
||||||
|
def test_owner_can_delete_own_strategy(self):
|
||||||
|
self.client.force_login(self.owner)
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"dca_strategy_delete", kwargs={"strategy_id": self.public_strategy.id}
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
self.assertFalse(
|
||||||
|
DCAStrategy.all_objects.filter(pk=self.public_strategy.pk).exists()
|
||||||
|
)
|
||||||
+48
-35
@@ -1,13 +1,19 @@
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db.models import Sum, Avg
|
from django.db.models import Sum, Avg
|
||||||
from django.db.models.functions import TruncMonth
|
from django.db.models.functions import TruncMonth
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
|
from apps.common.functions.permissions import (
|
||||||
|
EDIT,
|
||||||
|
READ,
|
||||||
|
get_shared_object_or_error,
|
||||||
|
)
|
||||||
from apps.dca.forms import DCAEntryForm, DCAStrategyForm
|
from apps.dca.forms import DCAEntryForm, DCAStrategyForm
|
||||||
from apps.dca.models import DCAStrategy, DCAEntry
|
from apps.dca.models import DCAStrategy, DCAEntry
|
||||||
from apps.common.models import SharedObject
|
from apps.common.models import SharedObject
|
||||||
@@ -56,16 +62,8 @@ def strategy_add(request):
|
|||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
def strategy_edit(request, strategy_id):
|
def strategy_edit(request, strategy_id):
|
||||||
dca_strategy = get_object_or_404(DCAStrategy, id=strategy_id)
|
dca_strategy = get_shared_object_or_error(
|
||||||
|
DCAStrategy, request, id=strategy_id, level=EDIT
|
||||||
if dca_strategy.owner and dca_strategy.owner != request.user:
|
|
||||||
messages.error(request, _("Only the owner can edit this"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
status=204,
|
|
||||||
headers={
|
|
||||||
"HX-Trigger": "updated, hide_offcanvas",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@@ -94,17 +92,20 @@ def strategy_edit(request, strategy_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def strategy_delete(request, strategy_id):
|
def strategy_delete(request, strategy_id):
|
||||||
dca_strategy = get_object_or_404(DCAStrategy, id=strategy_id)
|
dca_strategy = get_shared_object_or_error(
|
||||||
|
DCAStrategy, request, id=strategy_id, level=READ
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if dca_strategy.is_editable_by(request.user):
|
||||||
dca_strategy.owner != request.user
|
dca_strategy.delete()
|
||||||
and request.user in dca_strategy.shared_with.all()
|
messages.success(request, _("DCA strategy deleted successfully"))
|
||||||
):
|
elif dca_strategy.shared_with.filter(pk=request.user.pk).exists():
|
||||||
|
# Someone else's object shared with us: we can drop our own access
|
||||||
|
# to it, but never delete it.
|
||||||
dca_strategy.shared_with.remove(request.user)
|
dca_strategy.shared_with.remove(request.user)
|
||||||
messages.success(request, _("Item no longer shared with you"))
|
messages.success(request, _("Item no longer shared with you"))
|
||||||
else:
|
else:
|
||||||
dca_strategy.delete()
|
raise PermissionDenied
|
||||||
messages.success(request, _("DCA strategy deleted successfully"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
status=204,
|
status=204,
|
||||||
@@ -118,7 +119,9 @@ def strategy_delete(request, strategy_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def strategy_take_ownership(request, strategy_id):
|
def strategy_take_ownership(request, strategy_id):
|
||||||
dca_strategy = get_object_or_404(DCAStrategy, id=strategy_id)
|
dca_strategy = get_shared_object_or_error(
|
||||||
|
DCAStrategy, request, id=strategy_id, level=EDIT
|
||||||
|
)
|
||||||
|
|
||||||
if not dca_strategy.owner:
|
if not dca_strategy.owner:
|
||||||
dca_strategy.owner = request.user
|
dca_strategy.owner = request.user
|
||||||
@@ -139,17 +142,7 @@ def strategy_take_ownership(request, strategy_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def strategy_share(request, pk):
|
def strategy_share(request, pk):
|
||||||
obj = get_object_or_404(DCAStrategy, id=pk)
|
obj = get_shared_object_or_error(DCAStrategy, request, id=pk, level=EDIT)
|
||||||
|
|
||||||
if obj.owner and obj.owner != request.user:
|
|
||||||
messages.error(request, _("Only the owner can edit this"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
status=204,
|
|
||||||
headers={
|
|
||||||
"HX-Trigger": "updated, hide_offcanvas",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
||||||
@@ -175,7 +168,9 @@ def strategy_share(request, pk):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def strategy_detail_index(request, strategy_id):
|
def strategy_detail_index(request, strategy_id):
|
||||||
strategy = get_object_or_404(DCAStrategy, id=strategy_id)
|
strategy = get_shared_object_or_error(
|
||||||
|
DCAStrategy, request, id=strategy_id, level=READ
|
||||||
|
)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@@ -187,7 +182,9 @@ def strategy_detail_index(request, strategy_id):
|
|||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
def strategy_detail(request, strategy_id):
|
def strategy_detail(request, strategy_id):
|
||||||
strategy = get_object_or_404(DCAStrategy, id=strategy_id)
|
strategy = get_shared_object_or_error(
|
||||||
|
DCAStrategy, request, id=strategy_id, level=READ
|
||||||
|
)
|
||||||
entries = strategy.entries.all()
|
entries = strategy.entries.all()
|
||||||
|
|
||||||
# Calculate monthly aggregates
|
# Calculate monthly aggregates
|
||||||
@@ -229,7 +226,9 @@ def strategy_detail(request, strategy_id):
|
|||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
def strategy_entry_add(request, strategy_id):
|
def strategy_entry_add(request, strategy_id):
|
||||||
strategy = get_object_or_404(DCAStrategy, id=strategy_id)
|
strategy = get_shared_object_or_error(
|
||||||
|
DCAStrategy, request, id=strategy_id, level=EDIT
|
||||||
|
)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = DCAEntryForm(request.POST, strategy=strategy)
|
form = DCAEntryForm(request.POST, strategy=strategy)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
@@ -255,7 +254,14 @@ def strategy_entry_add(request, strategy_id):
|
|||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
def strategy_entry_edit(request, strategy_id, entry_id):
|
def strategy_entry_edit(request, strategy_id, entry_id):
|
||||||
dca_entry = get_object_or_404(DCAEntry, id=entry_id, strategy__id=strategy_id)
|
dca_entry = get_shared_object_or_error(
|
||||||
|
DCAEntry,
|
||||||
|
request,
|
||||||
|
id=entry_id,
|
||||||
|
strategy__id=strategy_id,
|
||||||
|
level=EDIT,
|
||||||
|
via="strategy",
|
||||||
|
)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = DCAEntryForm(request.POST, instance=dca_entry)
|
form = DCAEntryForm(request.POST, instance=dca_entry)
|
||||||
@@ -283,7 +289,14 @@ def strategy_entry_edit(request, strategy_id, entry_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def strategy_entry_delete(request, entry_id, strategy_id):
|
def strategy_entry_delete(request, entry_id, strategy_id):
|
||||||
dca_entry = get_object_or_404(DCAEntry, id=entry_id, strategy__id=strategy_id)
|
dca_entry = get_shared_object_or_error(
|
||||||
|
DCAEntry,
|
||||||
|
request,
|
||||||
|
id=entry_id,
|
||||||
|
strategy__id=strategy_id,
|
||||||
|
level=EDIT,
|
||||||
|
via="strategy",
|
||||||
|
)
|
||||||
|
|
||||||
dca_entry.delete()
|
dca_entry.delete()
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,25 @@ class MonthlySummaryFilterBehaviorTests(TestCase):
|
|||||||
return data
|
return data
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _create_asset_income(self):
|
||||||
|
asset_account = Account.objects.create(
|
||||||
|
name="Asset Account",
|
||||||
|
group=self.account_group,
|
||||||
|
currency=self.currency,
|
||||||
|
is_asset=True,
|
||||||
|
)
|
||||||
|
Transaction.objects.create(
|
||||||
|
account=asset_account,
|
||||||
|
type=Transaction.Type.INCOME,
|
||||||
|
is_paid=True,
|
||||||
|
date=date(2025, 12, 25),
|
||||||
|
reference_date=date(2025, 12, 1),
|
||||||
|
amount=Decimal("50.00"),
|
||||||
|
description="Asset Income",
|
||||||
|
owner=self.user,
|
||||||
|
)
|
||||||
|
return asset_account
|
||||||
|
|
||||||
# --- monthly_summary view tests ---
|
# --- monthly_summary view tests ---
|
||||||
|
|
||||||
def test_monthly_summary_no_filter_returns_200(self):
|
def test_monthly_summary_no_filter_returns_200(self):
|
||||||
@@ -304,6 +323,15 @@ class MonthlySummaryFilterBehaviorTests(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_monthly_account_summary_includes_asset_accounts(self):
|
||||||
|
asset_account = self._create_asset_income()
|
||||||
|
response = self.client.get(
|
||||||
|
"/monthly/12/2025/summary/accounts/",
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn(asset_account.id, response.context["account_data"])
|
||||||
|
|
||||||
def test_monthly_account_summary_with_filter_returns_200(self):
|
def test_monthly_account_summary_with_filter_returns_200(self):
|
||||||
"""Test that monthly_account_summary returns 200 with filter"""
|
"""Test that monthly_account_summary returns 200 with filter"""
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
@@ -322,6 +350,16 @@ class MonthlySummaryFilterBehaviorTests(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_monthly_currency_summary_includes_asset_account_transactions(self):
|
||||||
|
self._create_asset_income()
|
||||||
|
response = self.client.get(
|
||||||
|
"/monthly/12/2025/summary/currencies/",
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
)
|
||||||
|
|
||||||
|
usd_data = self._get_currency_data(response.context["currency_data"])
|
||||||
|
self.assertEqual(usd_data["income_current"], Decimal("1050.00"))
|
||||||
|
|
||||||
def test_monthly_currency_summary_with_filter_returns_200(self):
|
def test_monthly_currency_summary_with_filter_returns_200(self):
|
||||||
"""Test that monthly_currency_summary returns 200 with filter"""
|
"""Test that monthly_currency_summary returns 200 with filter"""
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from apps.monthly_overview.utils.daily_spending_allowance import (
|
|||||||
calculate_daily_allowance_currency,
|
calculate_daily_allowance_currency,
|
||||||
)
|
)
|
||||||
from apps.transactions.filters import TransactionsFilter
|
from apps.transactions.filters import TransactionsFilter
|
||||||
from apps.transactions.models import Transaction
|
from apps.transactions.models import FilterPreset, Transaction
|
||||||
from apps.transactions.utils.calculations import (
|
from apps.transactions.utils.calculations import (
|
||||||
calculate_currency_totals,
|
calculate_currency_totals,
|
||||||
calculate_percentage_distribution,
|
calculate_percentage_distribution,
|
||||||
@@ -58,6 +58,8 @@ def monthly_overview(request, month: int, year: int):
|
|||||||
"previous_month": previous_month,
|
"previous_month": previous_month,
|
||||||
"previous_year": previous_year,
|
"previous_year": previous_year,
|
||||||
"filter": f,
|
"filter": f,
|
||||||
|
"filter_is_active": f.has_active_filters,
|
||||||
|
"filter_presets": FilterPreset.objects.filter(owner=request.user),
|
||||||
"order": order,
|
"order": order,
|
||||||
"summary_tab": summary_tab,
|
"summary_tab": summary_tab,
|
||||||
},
|
},
|
||||||
@@ -226,7 +228,6 @@ def monthly_account_summary(request, month: int, year: int):
|
|||||||
Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True)
|
Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True)
|
||||||
)
|
)
|
||||||
.exclude(account__in=request.user.untracked_accounts.all())
|
.exclude(account__in=request.user.untracked_accounts.all())
|
||||||
.exclude(account__is_asset=True)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
account_data = calculate_account_totals(transactions_queryset=queryset.all())
|
account_data = calculate_account_totals(transactions_queryset=queryset.all())
|
||||||
@@ -280,7 +281,6 @@ def monthly_currency_summary(request, month: int, year: int):
|
|||||||
Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True)
|
Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True)
|
||||||
)
|
)
|
||||||
.exclude(account__in=request.user.untracked_accounts.all())
|
.exclude(account__in=request.user.untracked_accounts.all())
|
||||||
.exclude(account__is_asset=True)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
currency_data = calculate_currency_totals(queryset.all(), ignore_empty=True)
|
currency_data = calculate_currency_totals(queryset.all(), ignore_empty=True)
|
||||||
|
|||||||
@@ -1,3 +1,82 @@
|
|||||||
from django.test import TestCase
|
import json
|
||||||
|
import tempfile
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
# Create your tests here.
|
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
|
||||||
|
from apps.currencies.models import Currency, ExchangeRate
|
||||||
|
from apps.transactions.models import Transaction
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
STATIC_ROOT=tempfile.gettempdir(),
|
||||||
|
STORAGES={
|
||||||
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
class NetWorthCurrencyChartTests(TestCase):
|
||||||
|
def test_consolidated_currency_is_a_selectable_dashed_matching_color_line(self):
|
||||||
|
user = get_user_model().objects.create_user(
|
||||||
|
email="chart@example.com", password="password"
|
||||||
|
)
|
||||||
|
usd = Currency.objects.create(code="USD", name="US Dollar", prefix="$ ")
|
||||||
|
eur = Currency.objects.create(
|
||||||
|
code="EUR", name="Euro", prefix="€ ", exchange_currency=usd
|
||||||
|
)
|
||||||
|
usd_account = Account.all_objects.create(
|
||||||
|
name="USD account", currency=usd, owner=user
|
||||||
|
)
|
||||||
|
eur_account = Account.all_objects.create(
|
||||||
|
name="EUR account", currency=eur, owner=user
|
||||||
|
)
|
||||||
|
ExchangeRate.objects.create(
|
||||||
|
from_currency=eur,
|
||||||
|
to_currency=usd,
|
||||||
|
rate=Decimal("1.234567"),
|
||||||
|
date=timezone.now(),
|
||||||
|
)
|
||||||
|
for account, amount in ((usd_account, "100"), (eur_account, "50")):
|
||||||
|
Transaction.userless_all_objects.create(
|
||||||
|
account=account,
|
||||||
|
owner=user,
|
||||||
|
type=Transaction.Type.INCOME,
|
||||||
|
amount=Decimal(amount),
|
||||||
|
date=date(2026, 1, 15),
|
||||||
|
reference_date=date(2026, 1, 1),
|
||||||
|
is_paid=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(user)
|
||||||
|
response = self.client.get(reverse("net_worth"))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
chart_data = json.loads(response.context["chart_data_currency_json"])
|
||||||
|
datasets = {dataset["label"]: dataset for dataset in chart_data["datasets"]}
|
||||||
|
self.assertIn("US Dollar Consolidated", datasets)
|
||||||
|
regular = datasets["US Dollar"]
|
||||||
|
consolidated = datasets["US Dollar Consolidated"]
|
||||||
|
self.assertEqual(consolidated["data"], [161.73])
|
||||||
|
self.assertNotIn("borderColor", regular)
|
||||||
|
self.assertNotIn("borderColor", consolidated)
|
||||||
|
self.assertEqual(consolidated["colorSource"], "US Dollar")
|
||||||
|
self.assertEqual(consolidated["borderDash"], [12, 6])
|
||||||
|
self.assertEqual(consolidated["pointRadius"], 0)
|
||||||
|
self.assertEqual(consolidated["pointHitRadius"], 8)
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
"showOnlyCurrencyDataset('US Dollar Consolidated', 'US Dollar')",
|
||||||
|
html=False,
|
||||||
|
)
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
'<span class="text-start shrink">Consolidated</span>',
|
||||||
|
html=False,
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import json
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
|
from django.utils.translation import gettext
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
|
from apps.currencies.models import Currency
|
||||||
|
from apps.currencies.utils.convert import convert
|
||||||
from apps.net_worth.utils.calculate_net_worth import (
|
from apps.net_worth.utils.calculate_net_worth import (
|
||||||
calculate_historical_currency_net_worth,
|
calculate_historical_currency_net_worth,
|
||||||
calculate_historical_account_balance,
|
calculate_historical_account_balance,
|
||||||
@@ -78,6 +81,17 @@ def net_worth(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
datasets = []
|
datasets = []
|
||||||
|
currency_models = {
|
||||||
|
currency.name: currency
|
||||||
|
for currency in Currency.objects.filter(name__in=currencies).select_related(
|
||||||
|
"exchange_currency"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
consolidated_currencies = {
|
||||||
|
data["currency"]["name"]
|
||||||
|
for data in currency_net_worth.values()
|
||||||
|
if data["consolidated"]["total_final"] != data["total_final"]
|
||||||
|
}
|
||||||
for i, currency in enumerate(currencies):
|
for i, currency in enumerate(currencies):
|
||||||
data = [
|
data = [
|
||||||
float(month_data[currency])
|
float(month_data[currency])
|
||||||
@@ -93,6 +107,45 @@ def net_worth(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if currency in consolidated_currencies:
|
||||||
|
target = currency_models[currency]
|
||||||
|
sources = [
|
||||||
|
source
|
||||||
|
for source in currency_models.values()
|
||||||
|
if source.exchange_currency_id == target.id
|
||||||
|
]
|
||||||
|
rates = {}
|
||||||
|
for source in sources:
|
||||||
|
converted, _, _, _ = convert(1, source, target)
|
||||||
|
if converted is not None:
|
||||||
|
rates[source.name] = converted
|
||||||
|
|
||||||
|
consolidated_data = [
|
||||||
|
float(
|
||||||
|
round(
|
||||||
|
month_data[currency]
|
||||||
|
+ sum(
|
||||||
|
month_data[source] * rate for source, rate in rates.items()
|
||||||
|
),
|
||||||
|
target.decimal_places,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for month_data in historical_currency_net_worth.values()
|
||||||
|
]
|
||||||
|
datasets.append(
|
||||||
|
{
|
||||||
|
"label": f"{currency} {gettext('Consolidated')}",
|
||||||
|
"data": consolidated_data,
|
||||||
|
"yAxisID": f"y{i}",
|
||||||
|
"fill": False,
|
||||||
|
"tension": 0.1,
|
||||||
|
"colorSource": currency,
|
||||||
|
"borderDash": [12, 6],
|
||||||
|
"pointRadius": 0,
|
||||||
|
"pointHitRadius": 8,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
chart_data_currency = {"labels": labels, "datasets": datasets}
|
chart_data_currency = {"labels": labels, "datasets": datasets}
|
||||||
|
|
||||||
chart_data_currency_json = json.dumps(chart_data_currency, cls=DjangoJSONEncoder)
|
chart_data_currency_json = json.dumps(chart_data_currency, cls=DjangoJSONEncoder)
|
||||||
|
|||||||
@@ -0,0 +1,473 @@
|
|||||||
|
"""Object-level authorization tests for the transaction-rule endpoints.
|
||||||
|
|
||||||
|
Regression coverage for GHSA-83g9-vjqf-2j5q: SharedObjectManager scopes rules to
|
||||||
|
what a user may *see*, which included other people's public and shared-with-them
|
||||||
|
rules. Several mutating endpoints treated that visibility as permission to write.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from apps.rules.models import (
|
||||||
|
TransactionRule,
|
||||||
|
TransactionRuleAction,
|
||||||
|
UpdateOrCreateTransactionRuleAction,
|
||||||
|
)
|
||||||
|
|
||||||
|
HTMX = {"HTTP_HX_REQUEST": "true"}
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
STORAGES={
|
||||||
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WHITENOISE_AUTOREFRESH=True,
|
||||||
|
DEMO=False,
|
||||||
|
)
|
||||||
|
class TransactionRuleObjectPermissionTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
User = get_user_model()
|
||||||
|
self.owner = User.objects.create_user(
|
||||||
|
email="owner@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.shared_user = User.objects.create_user(
|
||||||
|
email="shared@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.stranger = User.objects.create_user(
|
||||||
|
email="stranger@test.com", password="testpass123"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Public: visible to everyone through SharedObjectManager.
|
||||||
|
self.public_rule = TransactionRule.all_objects.create(
|
||||||
|
name="Public rule",
|
||||||
|
trigger="True",
|
||||||
|
owner=self.owner,
|
||||||
|
visibility="public",
|
||||||
|
active=True,
|
||||||
|
)
|
||||||
|
# Private but shared with shared_user: visible to them, not to stranger.
|
||||||
|
self.shared_rule = TransactionRule.all_objects.create(
|
||||||
|
name="Shared rule",
|
||||||
|
trigger="True",
|
||||||
|
owner=self.owner,
|
||||||
|
visibility="private",
|
||||||
|
active=True,
|
||||||
|
)
|
||||||
|
self.shared_rule.shared_with.add(self.shared_user)
|
||||||
|
# Private and unshared: invisible to everyone but the owner.
|
||||||
|
self.private_rule = TransactionRule.all_objects.create(
|
||||||
|
name="Private rule",
|
||||||
|
trigger="True",
|
||||||
|
owner=self.owner,
|
||||||
|
visibility="private",
|
||||||
|
active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.public_action = TransactionRuleAction.objects.create(
|
||||||
|
rule=self.public_rule, field="notes", value="owned by owner"
|
||||||
|
)
|
||||||
|
self.private_action = TransactionRuleAction.objects.create(
|
||||||
|
rule=self.private_rule, field="notes", value="owned by owner"
|
||||||
|
)
|
||||||
|
self.public_linked_action = UpdateOrCreateTransactionRuleAction.objects.create(
|
||||||
|
rule=self.public_rule
|
||||||
|
)
|
||||||
|
self.private_linked_action = UpdateOrCreateTransactionRuleAction.objects.create(
|
||||||
|
rule=self.private_rule
|
||||||
|
)
|
||||||
|
|
||||||
|
def login(self, user):
|
||||||
|
self.client.force_login(user)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# toggle-active
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_stranger_cannot_toggle_public_rule(self):
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_toggle_activity",
|
||||||
|
kwargs={"transaction_rule_id": self.public_rule.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.public_rule.refresh_from_db()
|
||||||
|
self.assertTrue(self.public_rule.active)
|
||||||
|
|
||||||
|
def test_shared_user_cannot_toggle_shared_rule(self):
|
||||||
|
self.login(self.shared_user)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_toggle_activity",
|
||||||
|
kwargs={"transaction_rule_id": self.shared_rule.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.shared_rule.refresh_from_db()
|
||||||
|
self.assertTrue(self.shared_rule.active)
|
||||||
|
|
||||||
|
def test_owner_can_toggle_own_rule(self):
|
||||||
|
self.login(self.owner)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_toggle_activity",
|
||||||
|
kwargs={"transaction_rule_id": self.public_rule.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
self.public_rule.refresh_from_db()
|
||||||
|
self.assertFalse(self.public_rule.active)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# rule actions
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_stranger_cannot_add_action_to_public_rule(self):
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_action_add",
|
||||||
|
kwargs={"transaction_rule_id": self.public_rule.id},
|
||||||
|
),
|
||||||
|
data={"field": "notes", "value": "injected", "order": 0},
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertFalse(
|
||||||
|
TransactionRuleAction.objects.filter(value="injected").exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stranger_cannot_edit_action_on_public_rule(self):
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_action_edit",
|
||||||
|
kwargs={"transaction_rule_action_id": self.public_action.id},
|
||||||
|
),
|
||||||
|
data={"field": "notes", "value": "rewritten", "order": 0},
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.public_action.refresh_from_db()
|
||||||
|
self.assertEqual(self.public_action.value, "owned by owner")
|
||||||
|
|
||||||
|
def test_stranger_cannot_delete_action_on_public_rule(self):
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_action_delete",
|
||||||
|
kwargs={"transaction_rule_action_id": self.public_action.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertTrue(
|
||||||
|
TransactionRuleAction.objects.filter(pk=self.public_action.pk).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stranger_cannot_delete_action_on_invisible_rule(self):
|
||||||
|
"""The child manager is unscoped, so the id is reachable by guessing.
|
||||||
|
|
||||||
|
The parent rule is invisible to the stranger, so the response must be a
|
||||||
|
404 rather than a 403 that confirms the action exists.
|
||||||
|
"""
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_action_delete",
|
||||||
|
kwargs={"transaction_rule_action_id": self.private_action.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
self.assertTrue(
|
||||||
|
TransactionRuleAction.objects.filter(pk=self.private_action.pk).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_owner_can_delete_own_action(self):
|
||||||
|
self.login(self.owner)
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_action_delete",
|
||||||
|
kwargs={"transaction_rule_action_id": self.public_action.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
self.assertFalse(
|
||||||
|
TransactionRuleAction.objects.filter(pk=self.public_action.pk).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# update-or-create rule actions
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_stranger_cannot_add_linked_action_to_public_rule(self):
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"update_or_create_transaction_rule_action_add",
|
||||||
|
kwargs={"transaction_rule_id": self.public_rule.id},
|
||||||
|
),
|
||||||
|
data={},
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(
|
||||||
|
UpdateOrCreateTransactionRuleAction.objects.filter(
|
||||||
|
rule=self.public_rule
|
||||||
|
).count(),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stranger_cannot_edit_linked_action_on_public_rule(self):
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"update_or_create_transaction_rule_action_edit",
|
||||||
|
kwargs={"pk": self.public_linked_action.id},
|
||||||
|
),
|
||||||
|
data={"filter": "injected"},
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.public_linked_action.refresh_from_db()
|
||||||
|
self.assertEqual(self.public_linked_action.filter, "")
|
||||||
|
|
||||||
|
def test_stranger_cannot_delete_linked_action_on_public_rule(self):
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"update_or_create_transaction_rule_action_delete",
|
||||||
|
kwargs={"pk": self.public_linked_action.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertTrue(
|
||||||
|
UpdateOrCreateTransactionRuleAction.objects.filter(
|
||||||
|
pk=self.public_linked_action.pk
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_stranger_cannot_delete_linked_action_on_invisible_rule(self):
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"update_or_create_transaction_rule_action_delete",
|
||||||
|
kwargs={"pk": self.private_linked_action.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
self.assertTrue(
|
||||||
|
UpdateOrCreateTransactionRuleAction.objects.filter(
|
||||||
|
pk=self.private_linked_action.pk
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# edit / share / dry-run
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_stranger_cannot_edit_public_rule(self):
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_edit",
|
||||||
|
kwargs={"transaction_rule_id": self.public_rule.id},
|
||||||
|
),
|
||||||
|
data={"name": "hijacked", "trigger": "True", "order": 0},
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.public_rule.refresh_from_db()
|
||||||
|
self.assertEqual(self.public_rule.name, "Public rule")
|
||||||
|
|
||||||
|
def test_stranger_cannot_change_sharing_of_public_rule(self):
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_share_settings", kwargs={"pk": self.public_rule.id}
|
||||||
|
),
|
||||||
|
data={"visibility": "private"},
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.public_rule.refresh_from_db()
|
||||||
|
self.assertEqual(self.public_rule.visibility, "public")
|
||||||
|
|
||||||
|
def test_stranger_cannot_dry_run_public_rule(self):
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_dry_run_created", kwargs={"pk": self.public_rule.id}
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# delete: sharing may be revoked, ownership may not be overridden
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_stranger_cannot_delete_public_rule(self):
|
||||||
|
"""The original condition fell through to delete() for public rules."""
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_delete",
|
||||||
|
kwargs={"transaction_rule_id": self.public_rule.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertTrue(
|
||||||
|
TransactionRule.all_objects.filter(pk=self.public_rule.pk).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_shared_user_deleting_only_revokes_their_own_access(self):
|
||||||
|
self.login(self.shared_user)
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_delete",
|
||||||
|
kwargs={"transaction_rule_id": self.shared_rule.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
self.assertTrue(
|
||||||
|
TransactionRule.all_objects.filter(pk=self.shared_rule.pk).exists()
|
||||||
|
)
|
||||||
|
self.assertNotIn(self.shared_user, self.shared_rule.shared_with.all())
|
||||||
|
|
||||||
|
def test_owner_can_delete_own_rule(self):
|
||||||
|
self.login(self.owner)
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_delete",
|
||||||
|
kwargs={"transaction_rule_id": self.public_rule.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
self.assertFalse(
|
||||||
|
TransactionRule.all_objects.filter(pk=self.public_rule.pk).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# reads must keep working for shared users
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_shared_user_can_still_view_shared_rule(self):
|
||||||
|
self.login(self.shared_user)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_view",
|
||||||
|
kwargs={"transaction_rule_id": self.shared_rule.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_stranger_can_view_public_rule(self):
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_view",
|
||||||
|
kwargs={"transaction_rule_id": self.public_rule.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_stranger_cannot_view_private_rule(self):
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_view",
|
||||||
|
kwargs={"transaction_rule_id": self.private_rule.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# take ownership stays available for unowned rules only
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_take_ownership_of_unowned_rule_still_works(self):
|
||||||
|
unowned = TransactionRule.all_objects.create(
|
||||||
|
name="Legacy rule", trigger="True", owner=None, visibility="private"
|
||||||
|
)
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_take_ownership",
|
||||||
|
kwargs={"transaction_rule_id": unowned.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
unowned.refresh_from_db()
|
||||||
|
self.assertEqual(unowned.owner, self.stranger)
|
||||||
|
|
||||||
|
def test_cannot_take_ownership_of_someone_elses_public_rule(self):
|
||||||
|
self.login(self.stranger)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
"transaction_rule_take_ownership",
|
||||||
|
kwargs={"transaction_rule_id": self.public_rule.id},
|
||||||
|
),
|
||||||
|
**HTMX,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.public_rule.refresh_from_db()
|
||||||
|
self.assertEqual(self.public_rule.owner, self.owner)
|
||||||
+57
-46
@@ -4,13 +4,19 @@ from copy import deepcopy
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
|
from apps.common.functions.permissions import (
|
||||||
|
EDIT,
|
||||||
|
READ,
|
||||||
|
get_shared_object_or_error,
|
||||||
|
)
|
||||||
from apps.rules.forms import (
|
from apps.rules.forms import (
|
||||||
TransactionRuleForm,
|
TransactionRuleForm,
|
||||||
TransactionRuleActionForm,
|
TransactionRuleActionForm,
|
||||||
@@ -62,7 +68,9 @@ def rules_list(request):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def transaction_rule_toggle_activity(request, transaction_rule_id, **kwargs):
|
def transaction_rule_toggle_activity(request, transaction_rule_id, **kwargs):
|
||||||
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
|
transaction_rule = get_shared_object_or_error(
|
||||||
|
TransactionRule, request, id=transaction_rule_id, level=EDIT
|
||||||
|
)
|
||||||
current_active = transaction_rule.active
|
current_active = transaction_rule.active
|
||||||
transaction_rule.active = not current_active
|
transaction_rule.active = not current_active
|
||||||
transaction_rule.save(update_fields=["active"])
|
transaction_rule.save(update_fields=["active"])
|
||||||
@@ -112,16 +120,8 @@ def transaction_rule_add(request, **kwargs):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def transaction_rule_edit(request, transaction_rule_id):
|
def transaction_rule_edit(request, transaction_rule_id):
|
||||||
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
|
transaction_rule = get_shared_object_or_error(
|
||||||
|
TransactionRule, request, id=transaction_rule_id, level=EDIT
|
||||||
if transaction_rule.owner and transaction_rule.owner != request.user:
|
|
||||||
messages.error(request, _("Only the owner can edit this"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
status=204,
|
|
||||||
headers={
|
|
||||||
"HX-Trigger": "updated, hide_offcanvas",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@@ -151,7 +151,9 @@ def transaction_rule_edit(request, transaction_rule_id):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def transaction_rule_view(request, transaction_rule_id):
|
def transaction_rule_view(request, transaction_rule_id):
|
||||||
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
|
transaction_rule = get_shared_object_or_error(
|
||||||
|
TransactionRule, request, id=transaction_rule_id, level=READ
|
||||||
|
)
|
||||||
|
|
||||||
edit_actions = transaction_rule.transaction_actions.all()
|
edit_actions = transaction_rule.transaction_actions.all()
|
||||||
update_or_create_actions = (
|
update_or_create_actions = (
|
||||||
@@ -175,17 +177,20 @@ def transaction_rule_view(request, transaction_rule_id):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def transaction_rule_delete(request, transaction_rule_id):
|
def transaction_rule_delete(request, transaction_rule_id):
|
||||||
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
|
transaction_rule = get_shared_object_or_error(
|
||||||
|
TransactionRule, request, id=transaction_rule_id, level=READ
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if transaction_rule.is_editable_by(request.user):
|
||||||
transaction_rule.owner != request.user
|
transaction_rule.delete()
|
||||||
and request.user in transaction_rule.shared_with.all()
|
messages.success(request, _("Rule deleted successfully"))
|
||||||
):
|
elif transaction_rule.shared_with.filter(pk=request.user.pk).exists():
|
||||||
|
# Someone else's rule shared with us: we can drop our own access to it,
|
||||||
|
# but never delete it.
|
||||||
transaction_rule.shared_with.remove(request.user)
|
transaction_rule.shared_with.remove(request.user)
|
||||||
messages.success(request, _("Item no longer shared with you"))
|
messages.success(request, _("Item no longer shared with you"))
|
||||||
else:
|
else:
|
||||||
transaction_rule.delete()
|
raise PermissionDenied
|
||||||
messages.success(request, _("Rule deleted successfully"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
status=204,
|
status=204,
|
||||||
@@ -200,7 +205,9 @@ def transaction_rule_delete(request, transaction_rule_id):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def transaction_rule_take_ownership(request, transaction_rule_id):
|
def transaction_rule_take_ownership(request, transaction_rule_id):
|
||||||
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
|
transaction_rule = get_shared_object_or_error(
|
||||||
|
TransactionRule, request, id=transaction_rule_id, level=EDIT
|
||||||
|
)
|
||||||
|
|
||||||
if not transaction_rule.owner:
|
if not transaction_rule.owner:
|
||||||
transaction_rule.owner = request.user
|
transaction_rule.owner = request.user
|
||||||
@@ -222,17 +229,7 @@ def transaction_rule_take_ownership(request, transaction_rule_id):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def transaction_rule_share(request, pk):
|
def transaction_rule_share(request, pk):
|
||||||
obj = get_object_or_404(TransactionRule, id=pk)
|
obj = get_shared_object_or_error(TransactionRule, request, id=pk, level=EDIT)
|
||||||
|
|
||||||
if obj.owner and obj.owner != request.user:
|
|
||||||
messages.error(request, _("Only the owner can edit this"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
status=204,
|
|
||||||
headers={
|
|
||||||
"HX-Trigger": "updated, hide_offcanvas",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
||||||
@@ -261,7 +258,9 @@ def transaction_rule_share(request, pk):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def transaction_rule_action_add(request, transaction_rule_id):
|
def transaction_rule_action_add(request, transaction_rule_id):
|
||||||
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
|
transaction_rule = get_shared_object_or_error(
|
||||||
|
TransactionRule, request, id=transaction_rule_id, level=EDIT
|
||||||
|
)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = TransactionRuleActionForm(request.POST, rule=transaction_rule)
|
form = TransactionRuleActionForm(request.POST, rule=transaction_rule)
|
||||||
@@ -289,12 +288,14 @@ def transaction_rule_action_add(request, transaction_rule_id):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def transaction_rule_action_edit(request, transaction_rule_action_id):
|
def transaction_rule_action_edit(request, transaction_rule_action_id):
|
||||||
transaction_rule_action = get_object_or_404(
|
transaction_rule_action = get_shared_object_or_error(
|
||||||
TransactionRuleAction, id=transaction_rule_action_id
|
TransactionRuleAction,
|
||||||
)
|
request,
|
||||||
transaction_rule = get_object_or_404(
|
id=transaction_rule_action_id,
|
||||||
TransactionRule, id=transaction_rule_action.rule.id
|
level=EDIT,
|
||||||
|
via="rule",
|
||||||
)
|
)
|
||||||
|
transaction_rule = transaction_rule_action.rule
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = TransactionRuleActionForm(
|
form = TransactionRuleActionForm(
|
||||||
@@ -327,8 +328,12 @@ def transaction_rule_action_edit(request, transaction_rule_action_id):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def transaction_rule_action_delete(request, transaction_rule_action_id):
|
def transaction_rule_action_delete(request, transaction_rule_action_id):
|
||||||
transaction_rule_action = get_object_or_404(
|
transaction_rule_action = get_shared_object_or_error(
|
||||||
TransactionRuleAction, id=transaction_rule_action_id
|
TransactionRuleAction,
|
||||||
|
request,
|
||||||
|
id=transaction_rule_action_id,
|
||||||
|
level=EDIT,
|
||||||
|
via="rule",
|
||||||
)
|
)
|
||||||
|
|
||||||
transaction_rule_action.delete()
|
transaction_rule_action.delete()
|
||||||
@@ -348,7 +353,9 @@ def transaction_rule_action_delete(request, transaction_rule_action_id):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def update_or_create_transaction_rule_action_add(request, transaction_rule_id):
|
def update_or_create_transaction_rule_action_add(request, transaction_rule_id):
|
||||||
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
|
transaction_rule = get_shared_object_or_error(
|
||||||
|
TransactionRule, request, id=transaction_rule_id, level=EDIT
|
||||||
|
)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = UpdateOrCreateTransactionRuleActionForm(
|
form = UpdateOrCreateTransactionRuleActionForm(
|
||||||
@@ -380,7 +387,9 @@ def update_or_create_transaction_rule_action_add(request, transaction_rule_id):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def update_or_create_transaction_rule_action_edit(request, pk):
|
def update_or_create_transaction_rule_action_edit(request, pk):
|
||||||
linked_action = get_object_or_404(UpdateOrCreateTransactionRuleAction, id=pk)
|
linked_action = get_shared_object_or_error(
|
||||||
|
UpdateOrCreateTransactionRuleAction, request, id=pk, level=EDIT, via="rule"
|
||||||
|
)
|
||||||
transaction_rule = linked_action.rule
|
transaction_rule = linked_action.rule
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@@ -415,7 +424,9 @@ def update_or_create_transaction_rule_action_edit(request, pk):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def update_or_create_transaction_rule_action_delete(request, pk):
|
def update_or_create_transaction_rule_action_delete(request, pk):
|
||||||
linked_action = get_object_or_404(UpdateOrCreateTransactionRuleAction, id=pk)
|
linked_action = get_shared_object_or_error(
|
||||||
|
UpdateOrCreateTransactionRuleAction, request, id=pk, level=EDIT, via="rule"
|
||||||
|
)
|
||||||
|
|
||||||
linked_action.delete()
|
linked_action.delete()
|
||||||
|
|
||||||
@@ -436,7 +447,7 @@ def update_or_create_transaction_rule_action_delete(request, pk):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def dry_run_rule_created(request, pk):
|
def dry_run_rule_created(request, pk):
|
||||||
rule = get_object_or_404(TransactionRule, id=pk)
|
rule = get_shared_object_or_error(TransactionRule, request, id=pk, level=EDIT)
|
||||||
logs = None
|
logs = None
|
||||||
results = None
|
results = None
|
||||||
|
|
||||||
@@ -481,7 +492,7 @@ def dry_run_rule_created(request, pk):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def dry_run_rule_deleted(request, pk):
|
def dry_run_rule_deleted(request, pk):
|
||||||
rule = get_object_or_404(TransactionRule, id=pk)
|
rule = get_shared_object_or_error(TransactionRule, request, id=pk, level=EDIT)
|
||||||
logs = None
|
logs = None
|
||||||
results = None
|
results = None
|
||||||
|
|
||||||
@@ -526,7 +537,7 @@ def dry_run_rule_deleted(request, pk):
|
|||||||
@disabled_on_demo
|
@disabled_on_demo
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def dry_run_rule_updated(request, pk):
|
def dry_run_rule_updated(request, pk):
|
||||||
rule = get_object_or_404(TransactionRule, id=pk)
|
rule = get_shared_object_or_error(TransactionRule, request, id=pk, level=EDIT)
|
||||||
logs = None
|
logs = None
|
||||||
results = None
|
results = None
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ class MonthYearFilter(Filter):
|
|||||||
|
|
||||||
|
|
||||||
class TransactionsFilter(django_filters.FilterSet):
|
class TransactionsFilter(django_filters.FilterSet):
|
||||||
|
default_filter_values = {
|
||||||
|
"type": {"IN", "EX"},
|
||||||
|
"is_paid": {"1", "0"},
|
||||||
|
"mute_status": {"active", "muted"},
|
||||||
|
}
|
||||||
|
|
||||||
description = django_filters.CharFilter(
|
description = django_filters.CharFilter(
|
||||||
label=_("Content"),
|
label=_("Content"),
|
||||||
method=content_filter,
|
method=content_filter,
|
||||||
@@ -217,6 +223,23 @@ class TransactionsFilter(django_filters.FilterSet):
|
|||||||
]
|
]
|
||||||
self.form.fields["entities"].choices = custom_entity_choices + entity_choices
|
self.form.fields["entities"].choices = custom_entity_choices + entity_choices
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_active_filters(self):
|
||||||
|
for name in self.base_filters:
|
||||||
|
if hasattr(self.form.data, "getlist"):
|
||||||
|
values = self.form.data.getlist(name)
|
||||||
|
else:
|
||||||
|
value = self.form.data.get(name)
|
||||||
|
values = value if isinstance(value, (list, tuple)) else [value]
|
||||||
|
|
||||||
|
values = [str(value) for value in values if value not in (None, "")]
|
||||||
|
if not values:
|
||||||
|
continue
|
||||||
|
if set(values) == self.default_filter_values.get(name):
|
||||||
|
continue
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def filter_category(queryset, name, value):
|
def filter_category(queryset, name, value):
|
||||||
if not value:
|
if not value:
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("transactions", "0049_transactionattachment"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="FilterPreset",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=100)),
|
||||||
|
("parameters", models.JSONField(default=dict)),
|
||||||
|
(
|
||||||
|
"owner",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="filter_presets",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"ordering": ["name", "id"]},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -30,12 +30,27 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
transaction_created = Signal()
|
transaction_created = Signal()
|
||||||
transaction_updated = Signal()
|
transaction_updated = Signal()
|
||||||
transaction_deleted = Signal()
|
transaction_deleted = Signal()
|
||||||
|
|
||||||
|
|
||||||
|
class FilterPreset(models.Model):
|
||||||
|
owner = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="filter_presets",
|
||||||
|
)
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
parameters = models.JSONField(default=dict)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["name", "id"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
def transaction_attachment_path(instance, filename):
|
def transaction_attachment_path(instance, filename):
|
||||||
extension = Path(filename).suffix
|
extension = Path(filename).suffix
|
||||||
return f"transaction_attachments/{instance.transaction_id}/{instance.id}{extension}"
|
return f"transaction_attachments/{instance.transaction_id}/{instance.id}{extension}"
|
||||||
@@ -535,6 +550,7 @@ class Transaction(OwnedObject):
|
|||||||
|
|
||||||
return new_obj
|
return new_obj
|
||||||
|
|
||||||
|
|
||||||
class TransactionAttachment(models.Model):
|
class TransactionAttachment(models.Model):
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
transaction = models.ForeignKey(
|
transaction = models.ForeignKey(
|
||||||
@@ -590,6 +606,7 @@ def delete_transaction_attachment_file(sender, instance, **kwargs):
|
|||||||
if storage.exists(instance.file.name):
|
if storage.exists(instance.file.name):
|
||||||
storage.delete(instance.file.name)
|
storage.delete(instance.file.name)
|
||||||
|
|
||||||
|
|
||||||
class InstallmentPlan(models.Model):
|
class InstallmentPlan(models.Model):
|
||||||
class Recurrence(models.TextChoices):
|
class Recurrence(models.TextChoices):
|
||||||
YEARLY = "yearly", _("Yearly")
|
YEARLY = "yearly", _("Yearly")
|
||||||
@@ -937,8 +954,10 @@ class RecurringTransaction(models.Model):
|
|||||||
notes=self.notes if self.add_notes_to_transaction else "",
|
notes=self.notes if self.add_notes_to_transaction else "",
|
||||||
owner=self.account.owner,
|
owner=self.account.owner,
|
||||||
)
|
)
|
||||||
created_transaction.tags.set(self.tags.all())
|
# Unfiltered managers: generation also runs without a current user, or with a
|
||||||
created_transaction.entities.set(self.entities.all())
|
# different one, and the scoped default manager would hide private rows.
|
||||||
|
created_transaction.tags.set(self.tags(manager="all_objects").all())
|
||||||
|
created_transaction.entities.set(self.entities(manager="all_objects").all())
|
||||||
|
|
||||||
def get_recurrence_delta(self):
|
def get_recurrence_delta(self):
|
||||||
if self.recurrence_type == self.RecurrenceType.DAY:
|
if self.recurrence_type == self.RecurrenceType.DAY:
|
||||||
@@ -1030,9 +1049,11 @@ class RecurringTransaction(models.Model):
|
|||||||
self.notes if self.add_notes_to_transaction else ""
|
self.notes if self.add_notes_to_transaction else ""
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update many-to-many relationships
|
# Update many-to-many relationships (see create_transaction)
|
||||||
existing_transaction.tags.set(self.tags.all())
|
existing_transaction.tags.set(self.tags(manager="all_objects").all())
|
||||||
existing_transaction.entities.set(self.entities.all())
|
existing_transaction.entities.set(
|
||||||
|
self.entities(manager="all_objects").all()
|
||||||
|
)
|
||||||
|
|
||||||
# Save updated transaction
|
# Save updated transaction
|
||||||
existing_transaction.save()
|
existing_transaction.save()
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from apps.transactions.models import FilterPreset
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
STORAGES={
|
||||||
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
WHITENOISE_AUTOREFRESH=True,
|
||||||
|
)
|
||||||
|
class FilterPresetViewTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
user_model = get_user_model()
|
||||||
|
self.user = user_model.objects.create_user(
|
||||||
|
email="preset-owner@example.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.other_user = user_model.objects.create_user(
|
||||||
|
email="other-user@example.com", password="testpass123"
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
self.preset = FilterPreset.objects.create(
|
||||||
|
owner=self.user,
|
||||||
|
name="Unpaid",
|
||||||
|
parameters={"is_paid": ["0"], "type": ["IN", "EX"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_create_stores_only_transaction_filter_fields(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("filter_preset_create"),
|
||||||
|
{
|
||||||
|
"name": "Account X Unpaid",
|
||||||
|
"account": ["Account X"],
|
||||||
|
"is_paid": ["0"],
|
||||||
|
"order": "newer",
|
||||||
|
},
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
)
|
||||||
|
|
||||||
|
preset = FilterPreset.objects.get(
|
||||||
|
owner=self.user, name="Account X Unpaid"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
preset.parameters,
|
||||||
|
{"account": ["Account X"], "is_paid": ["0"]},
|
||||||
|
)
|
||||||
|
self.assertContains(response, "Account X Unpaid")
|
||||||
|
|
||||||
|
def test_create_rejects_a_blank_name(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("filter_preset_create"),
|
||||||
|
{"name": " ", "is_paid": ["0"]},
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(FilterPreset.objects.filter(owner=self.user).count(), 1)
|
||||||
|
|
||||||
|
def test_apply_returns_the_saved_filter_form_without_changing_the_url(self):
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("filter_preset_apply", args=[self.preset.pk]),
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, 'id="filter"')
|
||||||
|
self.assertEqual(response.context["filter"].data.getlist("is_paid"), ["0"])
|
||||||
|
self.assertEqual(
|
||||||
|
response.context["filter"].data.getlist("type"), ["IN", "EX"]
|
||||||
|
)
|
||||||
|
self.assertNotIn("HX-Push-Url", response.headers)
|
||||||
|
self.assertNotIn("HX-Replace-Url", response.headers)
|
||||||
|
self.assertIs(response.context.get("filter_is_active"), True)
|
||||||
|
self.assertContains(response, 'hx-swap-oob="outerHTML"')
|
||||||
|
self.assertEqual(
|
||||||
|
response.headers["HX-Trigger-After-Settle"],
|
||||||
|
"updated",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_clear_returns_the_default_filter_form_without_changing_the_url(self):
|
||||||
|
response = self.client.get(
|
||||||
|
"/transactions/filter/clear/",
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, 'id="filter"')
|
||||||
|
self.assertEqual(
|
||||||
|
response.context["filter"].data.getlist("type"), ["IN", "EX"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response.context["filter"].data.getlist("is_paid"), ["1", "0"]
|
||||||
|
)
|
||||||
|
self.assertNotIn("HX-Push-Url", response.headers)
|
||||||
|
self.assertNotIn("HX-Replace-Url", response.headers)
|
||||||
|
self.assertIs(response.context.get("filter_is_active"), False)
|
||||||
|
self.assertContains(response, 'hx-swap-oob="outerHTML"')
|
||||||
|
self.assertEqual(
|
||||||
|
response.headers["HX-Trigger-After-Settle"],
|
||||||
|
"updated",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_other_users_cannot_apply_or_delete_a_preset(self):
|
||||||
|
self.client.force_login(self.other_user)
|
||||||
|
|
||||||
|
apply_response = self.client.get(
|
||||||
|
reverse("filter_preset_apply", args=[self.preset.pk]),
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
)
|
||||||
|
delete_response = self.client.post(
|
||||||
|
reverse("filter_preset_delete", args=[self.preset.pk]),
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(apply_response.status_code, 404)
|
||||||
|
self.assertEqual(delete_response.status_code, 404)
|
||||||
|
self.assertTrue(FilterPreset.objects.filter(pk=self.preset.pk).exists())
|
||||||
|
|
||||||
|
def test_delete_removes_the_current_users_preset(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("filter_preset_delete", args=[self.preset.pk]),
|
||||||
|
HTTP_HX_REQUEST="true",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertFalse(FilterPreset.objects.filter(pk=self.preset.pk).exists())
|
||||||
|
self.assertNotContains(response, self.preset.name)
|
||||||
|
|
||||||
|
def test_all_transactions_page_only_offers_the_current_users_presets(self):
|
||||||
|
FilterPreset.objects.create(
|
||||||
|
owner=self.other_user,
|
||||||
|
name="Other User Preset",
|
||||||
|
parameters={"type": ["IN"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("transactions_all_index"))
|
||||||
|
|
||||||
|
self.assertContains(response, self.preset.name)
|
||||||
|
self.assertNotContains(response, "Other User Preset")
|
||||||
|
self.assertContains(response, 'id="filter-presets"')
|
||||||
|
self.assertNotContains(response, 'href="./?')
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
reverse("filter_preset_apply", args=[self.preset.pk]),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_all_transactions_page_marks_a_filtered_query_active(self):
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("transactions_all_index"),
|
||||||
|
{"type": ["IN", "EX"], "is_paid": ["0"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(response.context["filter_is_active"], True)
|
||||||
|
|
||||||
|
def test_all_transactions_page_does_not_mark_default_query_active(self):
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("transactions_all_index"),
|
||||||
|
{
|
||||||
|
"type": ["IN", "EX"],
|
||||||
|
"is_paid": ["1", "0"],
|
||||||
|
"mute_status": ["active", "muted"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIs(response.context["filter_is_active"], False)
|
||||||
|
|
||||||
|
def test_monthly_page_renders_preset_controls(self):
|
||||||
|
other_preset = FilterPreset.objects.create(
|
||||||
|
owner=self.other_user,
|
||||||
|
name="Other Monthly Preset",
|
||||||
|
parameters={"type": ["IN"]},
|
||||||
|
)
|
||||||
|
response = self.client.get(reverse("monthly_overview", args=[8, 2026]))
|
||||||
|
|
||||||
|
self.assertContains(response, 'id="filter-presets"')
|
||||||
|
self.assertNotContains(response, 'href="./?')
|
||||||
|
self.assertContains(response, reverse("filter_preset_create"))
|
||||||
|
self.assertNotContains(response, other_preset.name)
|
||||||
@@ -7,6 +7,7 @@ from django.utils import timezone
|
|||||||
from apps.transactions.models import (
|
from apps.transactions.models import (
|
||||||
TransactionCategory,
|
TransactionCategory,
|
||||||
TransactionTag,
|
TransactionTag,
|
||||||
|
TransactionEntity,
|
||||||
Transaction,
|
Transaction,
|
||||||
InstallmentPlan,
|
InstallmentPlan,
|
||||||
RecurringTransaction,
|
RecurringTransaction,
|
||||||
@@ -240,3 +241,27 @@ class RecurringTransactionTests(TestCase):
|
|||||||
self.assertFalse(recurring.is_paused)
|
self.assertFalse(recurring.is_paused)
|
||||||
self.assertEqual(recurring.recurrence_interval, 1)
|
self.assertEqual(recurring.recurrence_interval, 1)
|
||||||
self.assertEqual(recurring.account.currency.code, "USD")
|
self.assertEqual(recurring.account.currency.code, "USD")
|
||||||
|
|
||||||
|
def test_generate_upcoming_transactions_keeps_tags_and_entities(self):
|
||||||
|
"""Generation must copy tags/entities even with no current user"""
|
||||||
|
tag = TransactionTag.objects.create(name="Essential")
|
||||||
|
entity = TransactionEntity.objects.create(name="Landlord")
|
||||||
|
recurring = RecurringTransaction.objects.create(
|
||||||
|
account=self.account,
|
||||||
|
type=Transaction.Type.EXPENSE,
|
||||||
|
amount=Decimal("100.00"),
|
||||||
|
description="Monthly Payment",
|
||||||
|
start_date=timezone.now().date(),
|
||||||
|
recurrence_type=RecurringTransaction.RecurrenceType.MONTH,
|
||||||
|
recurrence_interval=1,
|
||||||
|
)
|
||||||
|
recurring.tags.set([tag])
|
||||||
|
recurring.entities.set([entity])
|
||||||
|
|
||||||
|
RecurringTransaction.generate_upcoming_transactions()
|
||||||
|
|
||||||
|
generated = Transaction.all_objects.filter(recurring_transaction=recurring)
|
||||||
|
self.assertTrue(generated.exists())
|
||||||
|
for transaction in generated:
|
||||||
|
self.assertIn(tag, transaction.tags(manager="all_objects").all())
|
||||||
|
self.assertIn(entity, transaction.entities(manager="all_objects").all())
|
||||||
|
|||||||
@@ -6,6 +6,26 @@ urlpatterns = [
|
|||||||
path(
|
path(
|
||||||
"transactions/list/", views.transaction_all_list, name="transactions_all_list"
|
"transactions/list/", views.transaction_all_list, name="transactions_all_list"
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"transactions/filter-presets/create/",
|
||||||
|
views.filter_preset_create,
|
||||||
|
name="filter_preset_create",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"transactions/filter-presets/<int:preset_id>/apply/",
|
||||||
|
views.filter_preset_apply,
|
||||||
|
name="filter_preset_apply",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"transactions/filter-presets/<int:preset_id>/delete/",
|
||||||
|
views.filter_preset_delete,
|
||||||
|
name="filter_preset_delete",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"transactions/filter/clear/",
|
||||||
|
views.transaction_filter_clear,
|
||||||
|
name="transaction_filter_clear",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"transactions/trash/",
|
"transactions/trash/",
|
||||||
views.transactions_trash_can_index,
|
views.transactions_trash_can_index,
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
|
from apps.common.functions.permissions import (
|
||||||
|
EDIT,
|
||||||
|
READ,
|
||||||
|
get_shared_object_or_error,
|
||||||
|
)
|
||||||
from apps.transactions.forms import TransactionCategoryForm
|
from apps.transactions.forms import TransactionCategoryForm
|
||||||
from apps.transactions.models import TransactionCategory
|
from apps.transactions.models import TransactionCategory
|
||||||
from apps.common.models import SharedObject
|
from apps.common.models import SharedObject
|
||||||
@@ -85,16 +91,8 @@ def category_add(request, **kwargs):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def category_edit(request, category_id):
|
def category_edit(request, category_id):
|
||||||
category = get_object_or_404(TransactionCategory, id=category_id)
|
category = get_shared_object_or_error(
|
||||||
|
TransactionCategory, request, id=category_id, level=EDIT
|
||||||
if category.owner and category.owner != request.user:
|
|
||||||
messages.error(request, _("Only the owner can edit this"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
status=204,
|
|
||||||
headers={
|
|
||||||
"HX-Trigger": "updated, hide_offcanvas",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@@ -123,17 +121,7 @@ def category_edit(request, category_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def category_share(request, pk):
|
def category_share(request, pk):
|
||||||
obj = get_object_or_404(TransactionCategory, id=pk)
|
obj = get_shared_object_or_error(TransactionCategory, request, id=pk, level=EDIT)
|
||||||
|
|
||||||
if obj.owner and obj.owner != request.user:
|
|
||||||
messages.error(request, _("Only the owner can edit this"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
status=204,
|
|
||||||
headers={
|
|
||||||
"HX-Trigger": "updated, hide_offcanvas",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
||||||
@@ -161,14 +149,20 @@ def category_share(request, pk):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def category_delete(request, category_id):
|
def category_delete(request, category_id):
|
||||||
category = get_object_or_404(TransactionCategory, id=category_id)
|
category = get_shared_object_or_error(
|
||||||
|
TransactionCategory, request, id=category_id, level=READ
|
||||||
|
)
|
||||||
|
|
||||||
if category.owner != request.user and request.user in category.shared_with.all():
|
if category.is_editable_by(request.user):
|
||||||
|
category.delete()
|
||||||
|
messages.success(request, _("Category deleted successfully"))
|
||||||
|
elif category.shared_with.filter(pk=request.user.pk).exists():
|
||||||
|
# Someone else's object shared with us: we can drop our own access
|
||||||
|
# to it, but never delete it.
|
||||||
category.shared_with.remove(request.user)
|
category.shared_with.remove(request.user)
|
||||||
messages.success(request, _("Item no longer shared with you"))
|
messages.success(request, _("Item no longer shared with you"))
|
||||||
else:
|
else:
|
||||||
category.delete()
|
raise PermissionDenied
|
||||||
messages.success(request, _("Category deleted successfully"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
status=204,
|
status=204,
|
||||||
@@ -182,7 +176,9 @@ def category_delete(request, category_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def category_take_ownership(request, category_id):
|
def category_take_ownership(request, category_id):
|
||||||
category = get_object_or_404(TransactionCategory, id=category_id)
|
category = get_shared_object_or_error(
|
||||||
|
TransactionCategory, request, id=category_id, level=EDIT
|
||||||
|
)
|
||||||
|
|
||||||
if not category.owner:
|
if not category.owner:
|
||||||
category.owner = request.user
|
category.owner = request.user
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
|
from apps.common.functions.permissions import (
|
||||||
|
EDIT,
|
||||||
|
READ,
|
||||||
|
get_shared_object_or_error,
|
||||||
|
)
|
||||||
from apps.transactions.forms import TransactionEntityForm
|
from apps.transactions.forms import TransactionEntityForm
|
||||||
from apps.transactions.models import TransactionEntity
|
from apps.transactions.models import TransactionEntity
|
||||||
from apps.common.models import SharedObject
|
from apps.common.models import SharedObject
|
||||||
@@ -85,16 +91,8 @@ def entity_add(request, **kwargs):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def entity_edit(request, entity_id):
|
def entity_edit(request, entity_id):
|
||||||
entity = get_object_or_404(TransactionEntity, id=entity_id)
|
entity = get_shared_object_or_error(
|
||||||
|
TransactionEntity, request, id=entity_id, level=EDIT
|
||||||
if entity.owner and entity.owner != request.user:
|
|
||||||
messages.error(request, _("Only the owner can edit this"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
status=204,
|
|
||||||
headers={
|
|
||||||
"HX-Trigger": "updated, hide_offcanvas",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@@ -123,14 +121,20 @@ def entity_edit(request, entity_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def entity_delete(request, entity_id):
|
def entity_delete(request, entity_id):
|
||||||
entity = get_object_or_404(TransactionEntity, id=entity_id)
|
entity = get_shared_object_or_error(
|
||||||
|
TransactionEntity, request, id=entity_id, level=READ
|
||||||
|
)
|
||||||
|
|
||||||
if entity.owner != request.user and request.user in entity.shared_with.all():
|
if entity.is_editable_by(request.user):
|
||||||
|
entity.delete()
|
||||||
|
messages.success(request, _("Entity deleted successfully"))
|
||||||
|
elif entity.shared_with.filter(pk=request.user.pk).exists():
|
||||||
|
# Someone else's object shared with us: we can drop our own access
|
||||||
|
# to it, but never delete it.
|
||||||
entity.shared_with.remove(request.user)
|
entity.shared_with.remove(request.user)
|
||||||
messages.success(request, _("Item no longer shared with you"))
|
messages.success(request, _("Item no longer shared with you"))
|
||||||
else:
|
else:
|
||||||
entity.delete()
|
raise PermissionDenied
|
||||||
messages.success(request, _("Entity deleted successfully"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
status=204,
|
status=204,
|
||||||
@@ -144,7 +148,9 @@ def entity_delete(request, entity_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def entity_take_ownership(request, entity_id):
|
def entity_take_ownership(request, entity_id):
|
||||||
entity = get_object_or_404(TransactionEntity, id=entity_id)
|
entity = get_shared_object_or_error(
|
||||||
|
TransactionEntity, request, id=entity_id, level=EDIT
|
||||||
|
)
|
||||||
|
|
||||||
if not entity.owner:
|
if not entity.owner:
|
||||||
entity.owner = request.user
|
entity.owner = request.user
|
||||||
@@ -165,17 +171,7 @@ def entity_take_ownership(request, entity_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def entity_share(request, pk):
|
def entity_share(request, pk):
|
||||||
obj = get_object_or_404(TransactionEntity, id=pk)
|
obj = get_shared_object_or_error(TransactionEntity, request, id=pk, level=EDIT)
|
||||||
|
|
||||||
if obj.owner and obj.owner != request.user:
|
|
||||||
messages.error(request, _("Only the owner can edit this"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
status=204,
|
|
||||||
headers={
|
|
||||||
"HX-Trigger": "updated, hide_offcanvas",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from apps.common.decorators.htmx import only_htmx
|
from apps.common.decorators.htmx import only_htmx
|
||||||
|
from apps.common.functions.permissions import (
|
||||||
|
EDIT,
|
||||||
|
READ,
|
||||||
|
get_shared_object_or_error,
|
||||||
|
)
|
||||||
from apps.transactions.forms import TransactionTagForm
|
from apps.transactions.forms import TransactionTagForm
|
||||||
from apps.transactions.models import TransactionTag
|
from apps.transactions.models import TransactionTag
|
||||||
from apps.common.models import SharedObject
|
from apps.common.models import SharedObject
|
||||||
@@ -85,17 +91,7 @@ def tag_add(request, **kwargs):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def tag_edit(request, tag_id):
|
def tag_edit(request, tag_id):
|
||||||
tag = get_object_or_404(TransactionTag, id=tag_id)
|
tag = get_shared_object_or_error(TransactionTag, request, id=tag_id, level=EDIT)
|
||||||
|
|
||||||
if tag.owner and tag.owner != request.user:
|
|
||||||
messages.error(request, _("Only the owner can edit this"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
status=204,
|
|
||||||
headers={
|
|
||||||
"HX-Trigger": "updated, hide_offcanvas",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = TransactionTagForm(request.POST, instance=tag)
|
form = TransactionTagForm(request.POST, instance=tag)
|
||||||
@@ -123,14 +119,18 @@ def tag_edit(request, tag_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["DELETE"])
|
@require_http_methods(["DELETE"])
|
||||||
def tag_delete(request, tag_id):
|
def tag_delete(request, tag_id):
|
||||||
tag = get_object_or_404(TransactionTag, id=tag_id)
|
tag = get_shared_object_or_error(TransactionTag, request, id=tag_id, level=READ)
|
||||||
|
|
||||||
if tag.owner != request.user and request.user in tag.shared_with.all():
|
if tag.is_editable_by(request.user):
|
||||||
|
tag.delete()
|
||||||
|
messages.success(request, _("Tag deleted successfully"))
|
||||||
|
elif tag.shared_with.filter(pk=request.user.pk).exists():
|
||||||
|
# Someone else's object shared with us: we can drop our own access
|
||||||
|
# to it, but never delete it.
|
||||||
tag.shared_with.remove(request.user)
|
tag.shared_with.remove(request.user)
|
||||||
messages.success(request, _("Item no longer shared with you"))
|
messages.success(request, _("Item no longer shared with you"))
|
||||||
else:
|
else:
|
||||||
tag.delete()
|
raise PermissionDenied
|
||||||
messages.success(request, _("Tag deleted successfully"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
status=204,
|
status=204,
|
||||||
@@ -144,7 +144,7 @@ def tag_delete(request, tag_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def tag_take_ownership(request, tag_id):
|
def tag_take_ownership(request, tag_id):
|
||||||
tag = get_object_or_404(TransactionTag, id=tag_id)
|
tag = get_shared_object_or_error(TransactionTag, request, id=tag_id, level=EDIT)
|
||||||
|
|
||||||
if not tag.owner:
|
if not tag.owner:
|
||||||
tag.owner = request.user
|
tag.owner = request.user
|
||||||
@@ -165,17 +165,7 @@ def tag_take_ownership(request, tag_id):
|
|||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
def tag_share(request, pk):
|
def tag_share(request, pk):
|
||||||
obj = get_object_or_404(TransactionTag, id=pk)
|
obj = get_shared_object_or_error(TransactionTag, request, id=pk, level=EDIT)
|
||||||
|
|
||||||
if obj.owner and obj.owner != request.user:
|
|
||||||
messages.error(request, _("Only the owner can edit this"))
|
|
||||||
|
|
||||||
return HttpResponse(
|
|
||||||
status=204,
|
|
||||||
headers={
|
|
||||||
"HX-Trigger": "updated, hide_offcanvas",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from apps.transactions.forms import (
|
|||||||
TransactionForm,
|
TransactionForm,
|
||||||
TransferForm,
|
TransferForm,
|
||||||
)
|
)
|
||||||
from apps.transactions.models import Transaction, TransactionAttachment
|
from apps.transactions.models import FilterPreset, Transaction, TransactionAttachment
|
||||||
from apps.transactions.utils.calculations import (
|
from apps.transactions.utils.calculations import (
|
||||||
calculate_account_totals,
|
calculate_account_totals,
|
||||||
calculate_currency_totals,
|
calculate_currency_totals,
|
||||||
@@ -23,7 +23,7 @@ from django.contrib import messages
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models import Case, IntegerField, Q, Value, When
|
from django.db.models import Case, IntegerField, Q, Value, When
|
||||||
from django.http import FileResponse, Http404, HttpResponse, JsonResponse
|
from django.http import FileResponse, Http404, HttpResponse, JsonResponse, QueryDict
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -635,7 +635,92 @@ def transaction_all_index(request):
|
|||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"transactions/pages/transactions.html",
|
"transactions/pages/transactions.html",
|
||||||
{"filter": f, "order": order, "summary_tab": summary_tab},
|
{
|
||||||
|
"filter": f,
|
||||||
|
"filter_is_active": f.has_active_filters,
|
||||||
|
"filter_presets": FilterPreset.objects.filter(owner=request.user),
|
||||||
|
"order": order,
|
||||||
|
"summary_tab": summary_tab,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
def filter_preset_create(request):
|
||||||
|
name = request.POST.get("name", "").strip()
|
||||||
|
if not name or len(name) > 100:
|
||||||
|
return HttpResponse(status=400)
|
||||||
|
|
||||||
|
parameters = {
|
||||||
|
key: request.POST.getlist(key)
|
||||||
|
for key in TransactionsFilter.base_filters
|
||||||
|
if key in request.POST
|
||||||
|
}
|
||||||
|
FilterPreset.objects.create(
|
||||||
|
owner=request.user,
|
||||||
|
name=name,
|
||||||
|
parameters=parameters,
|
||||||
|
)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"transactions/fragments/filter_presets.html",
|
||||||
|
{"filter_presets": FilterPreset.objects.filter(owner=request.user)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def filter_preset_apply(request, preset_id):
|
||||||
|
preset = get_object_or_404(FilterPreset, pk=preset_id, owner=request.user)
|
||||||
|
data = QueryDict(mutable=True)
|
||||||
|
for key, values in preset.parameters.items():
|
||||||
|
if key in TransactionsFilter.base_filters:
|
||||||
|
data.setlist(key, values)
|
||||||
|
|
||||||
|
transaction_filter = TransactionsFilter(data)
|
||||||
|
response = render(
|
||||||
|
request,
|
||||||
|
"transactions/fragments/filter_form.html",
|
||||||
|
{
|
||||||
|
"filter": transaction_filter,
|
||||||
|
"filter_is_active": transaction_filter.has_active_filters,
|
||||||
|
"swap_filter_indicator": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.headers["HX-Trigger-After-Settle"] = "updated"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def transaction_filter_clear(request):
|
||||||
|
transaction_filter = TransactionsFilter(QueryDict())
|
||||||
|
response = render(
|
||||||
|
request,
|
||||||
|
"transactions/fragments/filter_form.html",
|
||||||
|
{
|
||||||
|
"filter": transaction_filter,
|
||||||
|
"filter_is_active": False,
|
||||||
|
"swap_filter_indicator": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.headers["HX-Trigger-After-Settle"] = "updated"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
def filter_preset_delete(request, preset_id):
|
||||||
|
get_object_or_404(FilterPreset, pk=preset_id, owner=request.user).delete()
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"transactions/fragments/filter_presets.html",
|
||||||
|
{"filter_presets": FilterPreset.objects.filter(owner=request.user)},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+235
-195
File diff suppressed because it is too large
Load Diff
+220
-192
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-18 18:43+0000\n"
|
"POT-Creation-Date: 2026-09-06 19:42+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -65,9 +65,9 @@ msgstr ""
|
|||||||
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
|
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
|
||||||
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
|
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
|
||||||
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
|
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
|
||||||
#: apps/transactions/forms.py:1007 apps/transactions/models.py:331
|
#: apps/transactions/forms.py:1007 apps/transactions/models.py:346
|
||||||
#: apps/transactions/models.py:641 apps/transactions/models.py:841
|
#: apps/transactions/models.py:658 apps/transactions/models.py:858
|
||||||
#: apps/transactions/models.py:1089
|
#: apps/transactions/models.py:1110
|
||||||
#: templates/insights/fragments/category_overview/index.html:86
|
#: templates/insights/fragments/category_overview/index.html:86
|
||||||
#: templates/insights/fragments/category_overview/index.html:542
|
#: templates/insights/fragments/category_overview/index.html:542
|
||||||
#: templates/insights/fragments/month_by_month.html:84
|
#: templates/insights/fragments/month_by_month.html:84
|
||||||
@@ -78,13 +78,13 @@ msgstr ""
|
|||||||
#: apps/accounts/forms.py:132 apps/dca/forms.py:95 apps/dca/forms.py:103
|
#: apps/accounts/forms.py:132 apps/dca/forms.py:95 apps/dca/forms.py:103
|
||||||
#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
|
#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
|
||||||
#: apps/rules/forms.py:184 apps/rules/forms.py:194 apps/rules/models.py:45
|
#: apps/rules/forms.py:184 apps/rules/forms.py:194 apps/rules/models.py:45
|
||||||
#: apps/rules/models.py:315 apps/transactions/filters.py:73
|
#: apps/rules/models.py:315 apps/transactions/filters.py:79
|
||||||
#: apps/transactions/forms.py:69 apps/transactions/forms.py:315
|
#: apps/transactions/forms.py:69 apps/transactions/forms.py:315
|
||||||
#: apps/transactions/forms.py:483 apps/transactions/forms.py:588
|
#: apps/transactions/forms.py:483 apps/transactions/forms.py:588
|
||||||
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
|
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
|
||||||
#: apps/transactions/forms.py:1000 apps/transactions/models.py:337
|
#: apps/transactions/forms.py:1000 apps/transactions/models.py:352
|
||||||
#: apps/transactions/models.py:643 apps/transactions/models.py:845
|
#: apps/transactions/models.py:660 apps/transactions/models.py:862
|
||||||
#: apps/transactions/models.py:1095 templates/includes/sidebar.html:150
|
#: apps/transactions/models.py:1116 templates/includes/sidebar.html:150
|
||||||
#: templates/insights/fragments/category_overview/index.html:40
|
#: templates/insights/fragments/category_overview/index.html:40
|
||||||
#: templates/insights/fragments/month_by_month.html:29
|
#: templates/insights/fragments/month_by_month.html:29
|
||||||
#: templates/insights/fragments/month_by_month.html:32
|
#: templates/insights/fragments/month_by_month.html:32
|
||||||
@@ -96,8 +96,8 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
|
#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
|
||||||
#: apps/import_app/models.py:14 apps/rules/models.py:13
|
#: apps/import_app/models.py:14 apps/rules/models.py:13
|
||||||
#: apps/transactions/models.py:223 apps/transactions/models.py:248
|
#: apps/transactions/models.py:238 apps/transactions/models.py:263
|
||||||
#: apps/transactions/models.py:272 apps/transactions/models.py:1057
|
#: apps/transactions/models.py:287 apps/transactions/models.py:1078
|
||||||
#: apps/users/models.py:572 templates/account_groups/fragments/list.html:22
|
#: apps/users/models.py:572 templates/account_groups/fragments/list.html:22
|
||||||
#: templates/accounts/fragments/list.html:22
|
#: templates/accounts/fragments/list.html:22
|
||||||
#: templates/categories/fragments/table.html:17
|
#: templates/categories/fragments/table.html:17
|
||||||
@@ -165,9 +165,9 @@ msgstr ""
|
|||||||
#: apps/rules/models.py:35 apps/rules/models.py:267
|
#: apps/rules/models.py:35 apps/rules/models.py:267
|
||||||
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
|
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
|
||||||
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
|
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
|
||||||
#: apps/transactions/forms.py:992 apps/transactions/models.py:303
|
#: apps/transactions/forms.py:992 apps/transactions/models.py:318
|
||||||
#: apps/transactions/models.py:601 apps/transactions/models.py:823
|
#: apps/transactions/models.py:618 apps/transactions/models.py:840
|
||||||
#: apps/transactions/models.py:1063
|
#: apps/transactions/models.py:1084
|
||||||
#: templates/installment_plans/fragments/table.html:17
|
#: templates/installment_plans/fragments/table.html:17
|
||||||
#: templates/quick_transactions/fragments/list.html:14
|
#: templates/quick_transactions/fragments/list.html:14
|
||||||
#: templates/recurring_transactions/fragments/table.html:19
|
#: templates/recurring_transactions/fragments/table.html:19
|
||||||
@@ -176,7 +176,7 @@ msgid "Account"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/models.py:76 apps/export_app/forms.py:19
|
#: apps/accounts/models.py:76 apps/export_app/forms.py:19
|
||||||
#: apps/export_app/forms.py:129 apps/transactions/filters.py:57
|
#: apps/export_app/forms.py:129 apps/transactions/filters.py:63
|
||||||
#: templates/accounts/fragments/list.html:9
|
#: templates/accounts/fragments/list.html:9
|
||||||
#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:162
|
#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:162
|
||||||
#: templates/includes/sidebar.html:164
|
#: templates/includes/sidebar.html:164
|
||||||
@@ -189,68 +189,56 @@ msgstr ""
|
|||||||
msgid "Exchange currency cannot be the same as the account's main currency."
|
msgid "Exchange currency cannot be the same as the account's main currency."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:44
|
#: apps/accounts/views/account_groups.py:50
|
||||||
msgid "Account Group added successfully"
|
msgid "Account Group added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:69
|
#: apps/accounts/views/account_groups.py:78
|
||||||
#: apps/accounts/views/account_groups.py:152 apps/accounts/views/accounts.py:68
|
|
||||||
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:62
|
|
||||||
#: apps/dca/views.py:145 apps/rules/views.py:118 apps/rules/views.py:228
|
|
||||||
#: apps/transactions/views/categories.py:91
|
|
||||||
#: apps/transactions/views/categories.py:129
|
|
||||||
#: apps/transactions/views/entities.py:91
|
|
||||||
#: apps/transactions/views/entities.py:171 apps/transactions/views/tags.py:91
|
|
||||||
#: apps/transactions/views/tags.py:171
|
|
||||||
msgid "Only the owner can edit this"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:82
|
|
||||||
msgid "Account Group updated successfully"
|
msgid "Account Group updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:111
|
#: apps/accounts/views/account_groups.py:104
|
||||||
#: apps/accounts/views/accounts.py:145 apps/dca/views.py:104
|
|
||||||
#: apps/rules/views.py:185 apps/transactions/views/categories.py:168
|
|
||||||
#: apps/transactions/views/entities.py:130 apps/transactions/views/tags.py:130
|
|
||||||
msgid "Item no longer shared with you"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:114
|
|
||||||
msgid "Account Group deleted successfully"
|
msgid "Account Group deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:135
|
#: apps/accounts/views/account_groups.py:109
|
||||||
#: apps/accounts/views/accounts.py:189 apps/dca/views.py:128
|
#: apps/accounts/views/accounts.py:137 apps/dca/views.py:106
|
||||||
#: apps/rules/views.py:210 apps/transactions/views/categories.py:192
|
#: apps/rules/views.py:191 apps/transactions/views/categories.py:163
|
||||||
#: apps/transactions/views/entities.py:154 apps/transactions/views/tags.py:154
|
#: apps/transactions/views/entities.py:135 apps/transactions/views/tags.py:131
|
||||||
|
msgid "Item no longer shared with you"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/accounts/views/account_groups.py:132
|
||||||
|
#: apps/accounts/views/accounts.py:182 apps/dca/views.py:131
|
||||||
|
#: 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"
|
msgid "Ownership taken successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:165
|
#: apps/accounts/views/account_groups.py:152
|
||||||
#: apps/accounts/views/accounts.py:119 apps/dca/views.py:158
|
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:151
|
||||||
#: apps/rules/views.py:241 apps/transactions/views/categories.py:142
|
#: apps/rules/views.py:238 apps/transactions/views/categories.py:130
|
||||||
#: apps/transactions/views/entities.py:184 apps/transactions/views/tags.py:184
|
#: apps/transactions/views/entities.py:180 apps/transactions/views/tags.py:174
|
||||||
msgid "Configuration saved successfully"
|
msgid "Configuration saved successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:44
|
#: apps/accounts/views/accounts.py:50
|
||||||
msgid "Account added successfully"
|
msgid "Account added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:81
|
#: apps/accounts/views/accounts.py:78
|
||||||
msgid "Account updated successfully"
|
msgid "Account updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:148
|
#: apps/accounts/views/accounts.py:132
|
||||||
msgid "Account deleted successfully"
|
msgid "Account deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:165
|
#: apps/accounts/views/accounts.py:158
|
||||||
msgid "Account is now tracked"
|
msgid "Account is now tracked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:168
|
#: apps/accounts/views/accounts.py:161
|
||||||
msgid "Account is now untracked"
|
msgid "Account is now untracked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -345,6 +333,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/common/forms.py:76 apps/users/forms.py:172
|
#: apps/common/forms.py:76 apps/users/forms.py:172
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:5
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -459,10 +448,9 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/common/widgets/tom_select.py:15
|
#: apps/common/widgets/tom_select.py:15
|
||||||
#: templates/mini_tools/unit_price_calculator.html:180
|
#: templates/mini_tools/unit_price_calculator.html:180
|
||||||
#: templates/monthly_overview/pages/overview.html:275
|
#: templates/monthly_overview/pages/overview.html:284
|
||||||
#: templates/monthly_overview/pages/overview.html:287
|
#: templates/transactions/fragments/filter_actions.html:48
|
||||||
#: templates/transactions/pages/transactions.html:225
|
#: templates/transactions/pages/transactions.html:233
|
||||||
#: templates/transactions/pages/transactions.html:237
|
|
||||||
msgid "Clear"
|
msgid "Clear"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -481,7 +469,7 @@ msgstr ""
|
|||||||
#: apps/currencies/forms.py:66 apps/dca/models.py:158 apps/rules/forms.py:176
|
#: apps/currencies/forms.py:66 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/rules/forms.py:190 apps/rules/models.py:38 apps/rules/models.py:279
|
||||||
#: apps/transactions/forms.py:85 apps/transactions/forms.py:447
|
#: apps/transactions/forms.py:85 apps/transactions/forms.py:447
|
||||||
#: apps/transactions/forms.py:600 apps/transactions/models.py:313
|
#: apps/transactions/forms.py:600 apps/transactions/models.py:328
|
||||||
#: templates/dca/fragments/strategy/details.html:49
|
#: templates/dca/fragments/strategy/details.html:49
|
||||||
#: templates/exchange_rates/fragments/table.html:10
|
#: templates/exchange_rates/fragments/table.html:10
|
||||||
#: templates/exchange_rates_services/fragments/table.html:11
|
#: templates/exchange_rates_services/fragments/table.html:11
|
||||||
@@ -501,7 +489,7 @@ msgid "Decimal Places"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
|
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
|
||||||
#: apps/export_app/forms.py:130 apps/transactions/filters.py:64
|
#: apps/export_app/forms.py:130 apps/transactions/filters.py:70
|
||||||
#: templates/currencies/fragments/list.html:9
|
#: templates/currencies/fragments/list.html:9
|
||||||
#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:176
|
#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:176
|
||||||
#: templates/includes/sidebar.html:178
|
#: templates/includes/sidebar.html:178
|
||||||
@@ -567,8 +555,8 @@ msgid "Service Type"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/currencies/models.py:119 apps/transactions/filters.py:27
|
#: apps/currencies/models.py:119 apps/transactions/filters.py:27
|
||||||
#: apps/transactions/models.py:227 apps/transactions/models.py:251
|
#: apps/transactions/models.py:242 apps/transactions/models.py:266
|
||||||
#: apps/transactions/models.py:275 templates/categories/fragments/list.html:16
|
#: apps/transactions/models.py:290 templates/categories/fragments/list.html:16
|
||||||
#: templates/entities/fragments/list.html:16
|
#: templates/entities/fragments/list.html:16
|
||||||
#: templates/installment_plans/fragments/list.html:16
|
#: templates/installment_plans/fragments/list.html:16
|
||||||
#: templates/recurring_transactions/fragments/list.html:16
|
#: templates/recurring_transactions/fragments/list.html:16
|
||||||
@@ -747,8 +735,8 @@ msgstr ""
|
|||||||
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
|
#: 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/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
|
||||||
#: apps/transactions/forms.py:469 apps/transactions/forms.py:616
|
#: apps/transactions/forms.py:469 apps/transactions/forms.py:616
|
||||||
#: apps/transactions/models.py:327 apps/transactions/models.py:650
|
#: apps/transactions/models.py:342 apps/transactions/models.py:667
|
||||||
#: apps/transactions/models.py:851 apps/transactions/models.py:1085
|
#: apps/transactions/models.py:868 apps/transactions/models.py:1106
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -780,27 +768,27 @@ msgstr ""
|
|||||||
msgid "DCA Entries"
|
msgid "DCA Entries"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:38
|
#: apps/dca/views.py:44
|
||||||
msgid "DCA Strategy added successfully"
|
msgid "DCA Strategy added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:75
|
#: apps/dca/views.py:73
|
||||||
msgid "DCA Strategy updated successfully"
|
msgid "DCA Strategy updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:107
|
#: apps/dca/views.py:101
|
||||||
msgid "DCA strategy deleted successfully"
|
msgid "DCA strategy deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:237
|
#: apps/dca/views.py:236
|
||||||
msgid "Entry added successfully"
|
msgid "Entry added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:264
|
#: apps/dca/views.py:270
|
||||||
msgid "Entry updated successfully"
|
msgid "Entry updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:290
|
#: apps/dca/views.py:303
|
||||||
msgid "Entry deleted successfully"
|
msgid "Entry deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -811,7 +799,7 @@ msgid "Users"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
|
#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
|
||||||
#: apps/transactions/models.py:388 templates/includes/sidebar.html:81
|
#: apps/transactions/models.py:403 templates/includes/sidebar.html:81
|
||||||
#: templates/includes/sidebar.html:142
|
#: templates/includes/sidebar.html:142
|
||||||
#: templates/recurring_transactions/fragments/list_transactions.html:5
|
#: templates/recurring_transactions/fragments/list_transactions.html:5
|
||||||
#: templates/recurring_transactions/fragments/table.html:37
|
#: templates/recurring_transactions/fragments/table.html:37
|
||||||
@@ -820,7 +808,7 @@ msgid "Transactions"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:37 apps/export_app/forms.py:131
|
#: apps/export_app/forms.py:37 apps/export_app/forms.py:131
|
||||||
#: apps/transactions/filters.py:68 templates/categories/fragments/list.html:9
|
#: apps/transactions/filters.py:74 templates/categories/fragments/list.html:9
|
||||||
#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:144
|
#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:144
|
||||||
#: templates/insights/fragments/month_by_month.html:18
|
#: templates/insights/fragments/month_by_month.html:18
|
||||||
#: templates/insights/fragments/month_by_month.html:21
|
#: templates/insights/fragments/month_by_month.html:21
|
||||||
@@ -831,12 +819,12 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
|
#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
|
||||||
#: apps/rules/forms.py:185 apps/rules/forms.py:195 apps/rules/models.py:46
|
#: apps/rules/forms.py:185 apps/rules/forms.py:195 apps/rules/models.py:46
|
||||||
#: apps/rules/models.py:307 apps/transactions/filters.py:78
|
#: apps/rules/models.py:307 apps/transactions/filters.py:84
|
||||||
#: apps/transactions/forms.py:77 apps/transactions/forms.py:323
|
#: apps/transactions/forms.py:77 apps/transactions/forms.py:323
|
||||||
#: apps/transactions/forms.py:491 apps/transactions/forms.py:771
|
#: apps/transactions/forms.py:491 apps/transactions/forms.py:771
|
||||||
#: apps/transactions/forms.py:1015 apps/transactions/models.py:286
|
#: apps/transactions/forms.py:1015 apps/transactions/models.py:301
|
||||||
#: apps/transactions/models.py:342 apps/transactions/models.py:646
|
#: apps/transactions/models.py:357 apps/transactions/models.py:663
|
||||||
#: apps/transactions/models.py:848 apps/transactions/models.py:1100
|
#: apps/transactions/models.py:865 apps/transactions/models.py:1121
|
||||||
#: templates/entities/fragments/list.html:9
|
#: templates/entities/fragments/list.html:9
|
||||||
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
|
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
|
||||||
#: templates/insights/fragments/category_overview/index.html:54
|
#: templates/insights/fragments/category_overview/index.html:54
|
||||||
@@ -848,14 +836,14 @@ msgid "Entities"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
|
#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
|
||||||
#: apps/transactions/models.py:888 templates/includes/sidebar.html:110
|
#: apps/transactions/models.py:905 templates/includes/sidebar.html:110
|
||||||
#: templates/recurring_transactions/fragments/list.html:9
|
#: templates/recurring_transactions/fragments/list.html:9
|
||||||
#: templates/recurring_transactions/pages/index.html:4
|
#: templates/recurring_transactions/pages/index.html:4
|
||||||
msgid "Recurring Transactions"
|
msgid "Recurring Transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
|
#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
|
||||||
#: apps/transactions/models.py:664 templates/includes/sidebar.html:104
|
#: apps/transactions/models.py:681 templates/includes/sidebar.html:104
|
||||||
#: templates/installment_plans/fragments/list.html:9
|
#: templates/installment_plans/fragments/list.html:9
|
||||||
#: templates/installment_plans/pages/index.html:4
|
#: templates/installment_plans/pages/index.html:4
|
||||||
msgid "Installment Plans"
|
msgid "Installment Plans"
|
||||||
@@ -1007,7 +995,7 @@ msgid "Run deleted successfully"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/insights/forms.py:118 apps/insights/utils/sankey.py:36
|
#: apps/insights/forms.py:118 apps/insights/utils/sankey.py:36
|
||||||
#: apps/insights/utils/sankey.py:167 apps/transactions/filters.py:203
|
#: apps/insights/utils/sankey.py:167 apps/transactions/filters.py:209
|
||||||
#: templates/insights/fragments/category_overview/index.html:96
|
#: templates/insights/fragments/category_overview/index.html:96
|
||||||
#: templates/insights/fragments/category_overview/index.html:407
|
#: templates/insights/fragments/category_overview/index.html:407
|
||||||
#: templates/insights/fragments/category_overview/index.html:436
|
#: templates/insights/fragments/category_overview/index.html:436
|
||||||
@@ -1105,15 +1093,15 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
|
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
|
||||||
#: apps/rules/models.py:271 apps/transactions/forms.py:433
|
#: apps/rules/models.py:271 apps/transactions/forms.py:433
|
||||||
#: apps/transactions/models.py:310 apps/transactions/models.py:606
|
#: apps/transactions/models.py:325 apps/transactions/models.py:623
|
||||||
#: apps/transactions/models.py:829 apps/transactions/models.py:1070
|
#: apps/transactions/models.py:846 apps/transactions/models.py:1091
|
||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
|
#: 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/rules/models.py:275 apps/transactions/filters.py:22
|
||||||
#: apps/transactions/forms.py:437 apps/transactions/models.py:312
|
#: apps/transactions/forms.py:437 apps/transactions/models.py:327
|
||||||
#: apps/transactions/models.py:1072 templates/cotton/transaction/item.html:20
|
#: apps/transactions/models.py:1093 templates/cotton/transaction/item.html:20
|
||||||
#: templates/cotton/transaction/item.html:31
|
#: templates/cotton/transaction/item.html:31
|
||||||
#: templates/transactions/widgets/paid_toggle_button.html:10
|
#: templates/transactions/widgets/paid_toggle_button.html:10
|
||||||
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
|
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
|
||||||
@@ -1123,15 +1111,15 @@ msgstr ""
|
|||||||
#: apps/rules/forms.py:177 apps/rules/forms.py:191 apps/rules/models.py:39
|
#: apps/rules/forms.py:177 apps/rules/forms.py:191 apps/rules/models.py:39
|
||||||
#: apps/rules/models.py:283 apps/transactions/forms.py:89
|
#: apps/rules/models.py:283 apps/transactions/forms.py:89
|
||||||
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
|
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
|
||||||
#: apps/transactions/forms.py:777 apps/transactions/models.py:314
|
#: apps/transactions/forms.py:777 apps/transactions/models.py:329
|
||||||
#: apps/transactions/models.py:624 apps/transactions/models.py:853
|
#: apps/transactions/models.py:641 apps/transactions/models.py:870
|
||||||
msgid "Reference Date"
|
msgid "Reference Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
|
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
|
||||||
#: apps/rules/models.py:287 apps/transactions/forms.py:460
|
#: apps/rules/models.py:287 apps/transactions/forms.py:460
|
||||||
#: apps/transactions/models.py:320 apps/transactions/models.py:834
|
#: apps/transactions/models.py:335 apps/transactions/models.py:851
|
||||||
#: apps/transactions/models.py:1078
|
#: apps/transactions/models.py:1099
|
||||||
#: templates/insights/fragments/sankey.html:102
|
#: templates/insights/fragments/sankey.html:102
|
||||||
#: templates/installment_plans/fragments/table.html:18
|
#: templates/installment_plans/fragments/table.html:18
|
||||||
#: templates/quick_transactions/fragments/list.html:15
|
#: templates/quick_transactions/fragments/list.html:15
|
||||||
@@ -1142,27 +1130,27 @@ msgstr ""
|
|||||||
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
|
#: 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/rules/models.py:42 apps/rules/models.py:291
|
||||||
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
|
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
|
||||||
#: apps/transactions/models.py:325 apps/transactions/models.py:608
|
#: apps/transactions/models.py:340 apps/transactions/models.py:625
|
||||||
#: apps/transactions/models.py:837 apps/transactions/models.py:1083
|
#: apps/transactions/models.py:854 apps/transactions/models.py:1104
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
|
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
|
||||||
#: apps/rules/models.py:299 apps/transactions/models.py:364
|
#: apps/rules/models.py:299 apps/transactions/models.py:379
|
||||||
#: apps/transactions/models.py:1105
|
#: apps/transactions/models.py:1126
|
||||||
msgid "Internal Note"
|
msgid "Internal Note"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
|
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
|
||||||
#: apps/rules/models.py:303 apps/transactions/models.py:366
|
#: apps/rules/models.py:303 apps/transactions/models.py:381
|
||||||
#: apps/transactions/models.py:1107
|
#: apps/transactions/models.py:1128
|
||||||
msgid "Internal ID"
|
msgid "Internal ID"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
|
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
|
||||||
#: apps/rules/models.py:319 apps/transactions/forms.py:620
|
#: apps/rules/models.py:319 apps/transactions/forms.py:620
|
||||||
#: apps/transactions/models.py:224 apps/transactions/models.py:315
|
#: apps/transactions/models.py:239 apps/transactions/models.py:330
|
||||||
#: apps/transactions/models.py:1073
|
#: apps/transactions/models.py:1094
|
||||||
msgid "Mute"
|
msgid "Mute"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1175,7 +1163,7 @@ msgid "Set Values"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
|
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
|
||||||
#: apps/transactions/models.py:387 apps/transactions/models.py:544
|
#: apps/transactions/models.py:402 apps/transactions/models.py:560
|
||||||
msgid "Transaction"
|
msgid "Transaction"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1269,15 +1257,15 @@ msgstr ""
|
|||||||
msgid "Update or create transaction action"
|
msgid "Update or create transaction action"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:71
|
#: apps/rules/views.py:79
|
||||||
msgid "Rule deactivated successfully"
|
msgid "Rule deactivated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:73
|
#: apps/rules/views.py:81
|
||||||
msgid "Rule activated successfully"
|
msgid "Rule activated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:92
|
#: apps/rules/views.py:100
|
||||||
msgid "Rule added successfully"
|
msgid "Rule added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1285,27 +1273,27 @@ msgstr ""
|
|||||||
msgid "Rule updated successfully"
|
msgid "Rule updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:188
|
#: apps/rules/views.py:186
|
||||||
msgid "Rule deleted successfully"
|
msgid "Rule deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:305
|
#: apps/rules/views.py:306
|
||||||
msgid "Action updated successfully"
|
msgid "Action updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:336
|
#: apps/rules/views.py:341
|
||||||
msgid "Action deleted successfully"
|
msgid "Action deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:360
|
#: apps/rules/views.py:367
|
||||||
msgid "Update or Create Transaction action added successfully"
|
msgid "Update or Create Transaction action added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:393
|
#: apps/rules/views.py:402
|
||||||
msgid "Update or Create Transaction action updated successfully"
|
msgid "Update or Create Transaction action updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:423
|
#: apps/rules/views.py:434
|
||||||
msgid "Update or Create Transaction action deleted successfully"
|
msgid "Update or Create Transaction action deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1322,58 +1310,58 @@ msgstr ""
|
|||||||
msgid "Muted"
|
msgid "Muted"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:45
|
#: apps/transactions/filters.py:51
|
||||||
msgid "Content"
|
msgid "Content"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:51
|
#: apps/transactions/filters.py:57
|
||||||
msgid "Transaction Type"
|
msgid "Transaction Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:89
|
#: apps/transactions/filters.py:95
|
||||||
msgid "Mute Status"
|
msgid "Mute Status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:94
|
#: apps/transactions/filters.py:100
|
||||||
msgid "Date from"
|
msgid "Date from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:99 apps/transactions/filters.py:109
|
#: apps/transactions/filters.py:105 apps/transactions/filters.py:115
|
||||||
msgid "Until"
|
msgid "Until"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:104
|
#: apps/transactions/filters.py:110
|
||||||
msgid "Reference date from"
|
msgid "Reference date from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:114
|
#: apps/transactions/filters.py:120
|
||||||
msgid "Amount min"
|
msgid "Amount min"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:119
|
#: apps/transactions/filters.py:125
|
||||||
msgid "Amount max"
|
msgid "Amount max"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:202
|
#: apps/transactions/filters.py:208
|
||||||
msgid "Categorized"
|
msgid "Categorized"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:209
|
#: apps/transactions/filters.py:215
|
||||||
msgid "Tagged"
|
msgid "Tagged"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:209
|
#: apps/transactions/filters.py:215
|
||||||
#: templates/insights/fragments/category_overview/index.html:189
|
#: templates/insights/fragments/category_overview/index.html:189
|
||||||
#: templates/insights/fragments/month_by_month.html:121
|
#: templates/insights/fragments/month_by_month.html:121
|
||||||
#: templates/insights/fragments/year_by_year.html:75
|
#: templates/insights/fragments/year_by_year.html:75
|
||||||
msgid "Untagged"
|
msgid "Untagged"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:215
|
#: apps/transactions/filters.py:221
|
||||||
msgid "Any entity"
|
msgid "Any entity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:216
|
#: apps/transactions/filters.py:222
|
||||||
#: templates/insights/fragments/category_overview/index.html:282
|
#: templates/insights/fragments/category_overview/index.html:282
|
||||||
#: templates/insights/fragments/month_by_month.html:123
|
#: templates/insights/fragments/month_by_month.html:123
|
||||||
#: templates/insights/fragments/year_by_year.html:77
|
#: templates/insights/fragments/year_by_year.html:77
|
||||||
@@ -1448,42 +1436,42 @@ msgstr ""
|
|||||||
msgid "End date should be after the start date"
|
msgid "End date should be after the start date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:229
|
#: apps/transactions/models.py:244
|
||||||
msgid ""
|
msgid ""
|
||||||
"Deactivated categories won't be able to be selected when creating new "
|
"Deactivated categories won't be able to be selected when creating new "
|
||||||
"transactions"
|
"transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:237
|
#: apps/transactions/models.py:252
|
||||||
msgid "Transaction Category"
|
msgid "Transaction Category"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:238
|
#: apps/transactions/models.py:253
|
||||||
msgid "Transaction Categories"
|
msgid "Transaction Categories"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:253
|
#: apps/transactions/models.py:268
|
||||||
msgid ""
|
msgid ""
|
||||||
"Deactivated tags won't be able to be selected when creating new transactions"
|
"Deactivated tags won't be able to be selected when creating new transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:261 apps/transactions/models.py:262
|
#: apps/transactions/models.py:276 apps/transactions/models.py:277
|
||||||
msgid "Transaction Tags"
|
msgid "Transaction Tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:277
|
#: apps/transactions/models.py:292
|
||||||
msgid ""
|
msgid ""
|
||||||
"Deactivated entities won't be able to be selected when creating new "
|
"Deactivated entities won't be able to be selected when creating new "
|
||||||
"transactions"
|
"transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:285
|
#: apps/transactions/models.py:300
|
||||||
#: templates/insights/fragments/month_by_month.html:88
|
#: templates/insights/fragments/month_by_month.html:88
|
||||||
#: templates/insights/fragments/year_by_year.html:56
|
#: templates/insights/fragments/year_by_year.html:56
|
||||||
msgid "Entity"
|
msgid "Entity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:297 apps/transactions/models.py:1050
|
#: apps/transactions/models.py:312 apps/transactions/models.py:1071
|
||||||
#: templates/calendar_view/fragments/list.html:42
|
#: templates/calendar_view/fragments/list.html:42
|
||||||
#: templates/calendar_view/fragments/list.html:44
|
#: templates/calendar_view/fragments/list.html:44
|
||||||
#: templates/calendar_view/fragments/list.html:52
|
#: templates/calendar_view/fragments/list.html:52
|
||||||
@@ -1495,7 +1483,7 @@ msgstr ""
|
|||||||
msgid "Income"
|
msgid "Income"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:298 apps/transactions/models.py:1051
|
#: apps/transactions/models.py:313 apps/transactions/models.py:1072
|
||||||
#: templates/calendar_view/fragments/list.html:46
|
#: templates/calendar_view/fragments/list.html:46
|
||||||
#: templates/calendar_view/fragments/list.html:48
|
#: templates/calendar_view/fragments/list.html:48
|
||||||
#: templates/calendar_view/fragments/list.html:56
|
#: templates/calendar_view/fragments/list.html:56
|
||||||
@@ -1506,157 +1494,157 @@ msgstr ""
|
|||||||
msgid "Expense"
|
msgid "Expense"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:353 apps/transactions/models.py:663
|
#: apps/transactions/models.py:368 apps/transactions/models.py:680
|
||||||
msgid "Installment Plan"
|
msgid "Installment Plan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:362 apps/transactions/models.py:887
|
#: apps/transactions/models.py:377 apps/transactions/models.py:904
|
||||||
msgid "Recurring Transaction"
|
msgid "Recurring Transaction"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:370
|
#: apps/transactions/models.py:385
|
||||||
msgid "Deleted"
|
msgid "Deleted"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:375
|
#: apps/transactions/models.py:390
|
||||||
msgid "Deleted At"
|
msgid "Deleted At"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:489 templates/tags/fragments/table.html:69
|
#: apps/transactions/models.py:504 templates/tags/fragments/table.html:69
|
||||||
msgid "No tags"
|
msgid "No tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:491
|
#: apps/transactions/models.py:506
|
||||||
msgid "No category"
|
msgid "No category"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:493
|
#: apps/transactions/models.py:508
|
||||||
msgid "No description"
|
msgid "No description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:549
|
#: apps/transactions/models.py:565
|
||||||
msgid "File"
|
msgid "File"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:551
|
#: apps/transactions/models.py:567
|
||||||
msgid "Original Name"
|
msgid "Original Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:553
|
#: apps/transactions/models.py:569
|
||||||
msgid "Content Type"
|
msgid "Content Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:555
|
#: apps/transactions/models.py:571
|
||||||
msgid "Size"
|
msgid "Size"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:560
|
#: apps/transactions/models.py:576
|
||||||
msgid "Uploaded By"
|
msgid "Uploaded By"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:565
|
#: apps/transactions/models.py:581
|
||||||
msgid "Transaction Attachment"
|
msgid "Transaction Attachment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:566
|
#: apps/transactions/models.py:582
|
||||||
msgid "Transaction Attachments"
|
msgid "Transaction Attachments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:595 templates/includes/sidebar.html:57
|
#: apps/transactions/models.py:612 templates/includes/sidebar.html:57
|
||||||
msgid "Yearly"
|
msgid "Yearly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:596 apps/users/models.py:469
|
#: apps/transactions/models.py:613 apps/users/models.py:469
|
||||||
#: templates/includes/sidebar.html:51
|
#: templates/includes/sidebar.html:51
|
||||||
msgid "Monthly"
|
msgid "Monthly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:597
|
#: apps/transactions/models.py:614
|
||||||
msgid "Weekly"
|
msgid "Weekly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:598
|
#: apps/transactions/models.py:615
|
||||||
msgid "Daily"
|
msgid "Daily"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:611
|
#: apps/transactions/models.py:628
|
||||||
msgid "Number of Installments"
|
msgid "Number of Installments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:616
|
#: apps/transactions/models.py:633
|
||||||
msgid "Installment Start"
|
msgid "Installment Start"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:617
|
#: apps/transactions/models.py:634
|
||||||
msgid "The installment number to start counting from"
|
msgid "The installment number to start counting from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:622 apps/transactions/models.py:857
|
#: apps/transactions/models.py:639 apps/transactions/models.py:874
|
||||||
msgid "Start Date"
|
msgid "Start Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:626 apps/transactions/models.py:858
|
#: apps/transactions/models.py:643 apps/transactions/models.py:875
|
||||||
msgid "End Date"
|
msgid "End Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:631
|
#: apps/transactions/models.py:648
|
||||||
msgid "Recurrence"
|
msgid "Recurrence"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:634
|
#: apps/transactions/models.py:651
|
||||||
msgid "Installment Amount"
|
msgid "Installment Amount"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:653 apps/transactions/models.py:877
|
#: apps/transactions/models.py:670 apps/transactions/models.py:894
|
||||||
msgid "Add description to transactions"
|
msgid "Add description to transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:656 apps/transactions/models.py:880
|
#: apps/transactions/models.py:673 apps/transactions/models.py:897
|
||||||
msgid "Add notes to transactions"
|
msgid "Add notes to transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:816
|
#: apps/transactions/models.py:833
|
||||||
msgid "day(s)"
|
msgid "day(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:817
|
#: apps/transactions/models.py:834
|
||||||
msgid "week(s)"
|
msgid "week(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:818
|
#: apps/transactions/models.py:835
|
||||||
msgid "month(s)"
|
msgid "month(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:819
|
#: apps/transactions/models.py:836
|
||||||
msgid "year(s)"
|
msgid "year(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:821
|
#: apps/transactions/models.py:838
|
||||||
#: templates/recurring_transactions/fragments/list.html:18
|
#: templates/recurring_transactions/fragments/list.html:18
|
||||||
msgid "Paused"
|
msgid "Paused"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:860
|
#: apps/transactions/models.py:877
|
||||||
msgid "Recurrence Type"
|
msgid "Recurrence Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:863
|
#: apps/transactions/models.py:880
|
||||||
msgid "Recurrence Interval"
|
msgid "Recurrence Interval"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:866
|
#: apps/transactions/models.py:883
|
||||||
msgid "Keep at most"
|
msgid "Keep at most"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:870
|
#: apps/transactions/models.py:887
|
||||||
msgid "Last Generated Date"
|
msgid "Last Generated Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:873
|
#: apps/transactions/models.py:890
|
||||||
msgid "Last Generated Reference Date"
|
msgid "Last Generated Reference Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:1117
|
#: apps/transactions/models.py:1138
|
||||||
#: apps/transactions/views/quick_transactions.py:178
|
#: apps/transactions/views/quick_transactions.py:178
|
||||||
#: apps/transactions/views/quick_transactions.py:187
|
#: apps/transactions/views/quick_transactions.py:187
|
||||||
#: apps/transactions/views/quick_transactions.py:189
|
#: apps/transactions/views/quick_transactions.py:189
|
||||||
@@ -1665,7 +1653,7 @@ msgstr ""
|
|||||||
msgid "Quick Transaction"
|
msgid "Quick Transaction"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:1118 templates/includes/sidebar.html:98
|
#: apps/transactions/models.py:1139 templates/includes/sidebar.html:98
|
||||||
#: templates/quick_transactions/pages/index.html:5
|
#: templates/quick_transactions/pages/index.html:5
|
||||||
#: templates/quick_transactions/pages/index.html:15
|
#: templates/quick_transactions/pages/index.html:15
|
||||||
msgid "Quick Transactions"
|
msgid "Quick Transactions"
|
||||||
@@ -1716,27 +1704,27 @@ msgid_plural "%(count)s transactions duplicated successfully"
|
|||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: apps/transactions/views/categories.py:66
|
#: apps/transactions/views/categories.py:72
|
||||||
msgid "Category added successfully"
|
msgid "Category added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/categories.py:104
|
#: apps/transactions/views/categories.py:102
|
||||||
msgid "Category updated successfully"
|
msgid "Category updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/categories.py:171
|
#: apps/transactions/views/categories.py:158
|
||||||
msgid "Category deleted successfully"
|
msgid "Category deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/entities.py:66
|
#: apps/transactions/views/entities.py:72
|
||||||
msgid "Entity added successfully"
|
msgid "Entity added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/entities.py:104
|
#: apps/transactions/views/entities.py:102
|
||||||
msgid "Entity updated successfully"
|
msgid "Entity updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/entities.py:133
|
#: apps/transactions/views/entities.py:130
|
||||||
msgid "Entity deleted successfully"
|
msgid "Entity deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1799,15 +1787,15 @@ msgstr ""
|
|||||||
msgid "Recurring Transaction deleted successfully"
|
msgid "Recurring Transaction deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/tags.py:66
|
#: apps/transactions/views/tags.py:72
|
||||||
msgid "Tag added successfully"
|
msgid "Tag added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/tags.py:104
|
#: apps/transactions/views/tags.py:100
|
||||||
msgid "Tag updated successfully"
|
msgid "Tag updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/tags.py:133
|
#: apps/transactions/views/tags.py:126
|
||||||
msgid "Tag deleted successfully"
|
msgid "Tag deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1889,9 +1877,9 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/users/forms.py:68 apps/users/forms.py:81 apps/users/forms.py:103
|
#: apps/users/forms.py:68 apps/users/forms.py:81 apps/users/forms.py:103
|
||||||
#: templates/monthly_overview/pages/overview.html:98
|
#: templates/monthly_overview/pages/overview.html:98
|
||||||
#: templates/monthly_overview/pages/overview.html:245
|
#: templates/monthly_overview/pages/overview.html:247
|
||||||
#: templates/transactions/pages/transactions.html:47
|
#: templates/transactions/pages/transactions.html:47
|
||||||
#: templates/transactions/pages/transactions.html:195
|
#: templates/transactions/pages/transactions.html:196
|
||||||
msgid "Default"
|
msgid "Default"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2212,6 +2200,7 @@ msgstr ""
|
|||||||
#: templates/rules/fragments/transaction_rule/view.html:92
|
#: templates/rules/fragments/transaction_rule/view.html:92
|
||||||
#: templates/rules/fragments/transaction_rule/view.html:133
|
#: templates/rules/fragments/transaction_rule/view.html:133
|
||||||
#: templates/tags/fragments/table.html:51
|
#: templates/tags/fragments/table.html:51
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:28
|
||||||
#: templates/users/fragments/api_tokens.html:93
|
#: templates/users/fragments/api_tokens.html:93
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2316,8 +2305,8 @@ msgid "Current balance"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/accounts/fragments/account_reconciliation.html:36
|
#: templates/accounts/fragments/account_reconciliation.html:36
|
||||||
#: templates/net_worth/net_worth.html:146
|
#: templates/net_worth/net_worth.html:147
|
||||||
#: templates/net_worth/net_worth.html:396
|
#: templates/net_worth/net_worth.html:407
|
||||||
msgid "Difference"
|
msgid "Difference"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2697,19 +2686,19 @@ msgstr ""
|
|||||||
msgid "Amount Bought"
|
msgid "Amount Bought"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:384
|
#: templates/dca/fragments/strategy/details.html:409
|
||||||
msgid "Entry Price vs Current Price"
|
msgid "Entry Price vs Current Price"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:398
|
#: templates/dca/fragments/strategy/details.html:423
|
||||||
msgid "Days Between Investments"
|
msgid "Days Between Investments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:444
|
#: templates/dca/fragments/strategy/details.html:469
|
||||||
msgid "Investment Frequency"
|
msgid "Investment Frequency"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:446
|
#: templates/dca/fragments/strategy/details.html:471
|
||||||
msgid "The straighter the blue line, the more consistent your DCA strategy is."
|
msgid "The straighter the blue line, the more consistent your DCA strategy is."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2946,6 +2935,7 @@ msgid "Reload"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/includes/scripts/hyperscript/swal.html:13
|
#: templates/includes/scripts/hyperscript/swal.html:13
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:4
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3360,16 +3350,16 @@ msgid "Summary"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/monthly_overview/pages/overview.html:99
|
#: templates/monthly_overview/pages/overview.html:99
|
||||||
#: templates/monthly_overview/pages/overview.html:254
|
#: templates/monthly_overview/pages/overview.html:256
|
||||||
#: templates/transactions/pages/transactions.html:48
|
#: templates/transactions/pages/transactions.html:48
|
||||||
#: templates/transactions/pages/transactions.html:204
|
#: templates/transactions/pages/transactions.html:205
|
||||||
msgid "Oldest first"
|
msgid "Oldest first"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/monthly_overview/pages/overview.html:100
|
#: templates/monthly_overview/pages/overview.html:100
|
||||||
#: templates/monthly_overview/pages/overview.html:263
|
#: templates/monthly_overview/pages/overview.html:265
|
||||||
#: templates/transactions/pages/transactions.html:49
|
#: templates/transactions/pages/transactions.html:49
|
||||||
#: templates/transactions/pages/transactions.html:213
|
#: templates/transactions/pages/transactions.html:214
|
||||||
msgid "Newest first"
|
msgid "Newest first"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3378,8 +3368,8 @@ msgstr ""
|
|||||||
msgid "Filter transactions"
|
msgid "Filter transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/monthly_overview/pages/overview.html:235
|
#: templates/monthly_overview/pages/overview.html:237
|
||||||
#: templates/transactions/pages/transactions.html:185
|
#: templates/transactions/pages/transactions.html:186
|
||||||
msgid "Order by"
|
msgid "Order by"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3388,24 +3378,24 @@ msgstr ""
|
|||||||
msgid "By currency"
|
msgid "By currency"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:84
|
#: templates/net_worth/net_worth.html:84 templates/net_worth/net_worth.html:85
|
||||||
msgid "Consolidated"
|
msgid "Consolidated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:134
|
#: templates/net_worth/net_worth.html:135
|
||||||
msgid "Evolution"
|
msgid "Evolution"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:162
|
#: templates/net_worth/net_worth.html:163
|
||||||
#: templates/yearly_overview/pages/overview_by_account.html:4
|
#: templates/yearly_overview/pages/overview_by_account.html:4
|
||||||
msgid "By account"
|
msgid "By account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:270
|
#: templates/net_worth/net_worth.html:271
|
||||||
msgid "Evolution by currency"
|
msgid "Evolution by currency"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:334
|
#: templates/net_worth/net_worth.html:345
|
||||||
msgid "Evolution by account"
|
msgid "Evolution by account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3485,15 +3475,19 @@ msgstr ""
|
|||||||
msgid "View"
|
msgid "View"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/rules/fragments/list.html:70
|
#: templates/rules/fragments/list.html:71
|
||||||
msgid "Deactivate"
|
msgid "Deactivate"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/rules/fragments/list.html:70
|
#: templates/rules/fragments/list.html:71
|
||||||
msgid "Activate"
|
msgid "Activate"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/rules/fragments/list.html:89
|
#: templates/rules/fragments/list.html:77
|
||||||
|
msgid "Only the owner can change this"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/rules/fragments/list.html:96
|
||||||
msgid "No rules"
|
msgid "No rules"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3636,6 +3630,40 @@ msgstr ""
|
|||||||
msgid "transactions"
|
msgid "transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:2
|
||||||
|
msgid "Save filter preset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:3
|
||||||
|
msgid "Preset name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:40
|
||||||
|
msgid "Save preset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:6
|
||||||
|
msgid "Filter presets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:21
|
||||||
|
#, python-format
|
||||||
|
msgid "Delete %(name)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:26
|
||||||
|
msgid "Delete filter preset?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:27
|
||||||
|
#, python-format
|
||||||
|
msgid "Delete “%(name)s”?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:34
|
||||||
|
msgid "No saved presets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: templates/transactions/fragments/list_all.html:58
|
#: templates/transactions/fragments/list_all.html:58
|
||||||
msgid "No transactions found"
|
msgid "No transactions found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
+230
-192
File diff suppressed because it is too large
Load Diff
+295
-270
File diff suppressed because it is too large
Load Diff
+226
-192
File diff suppressed because it is too large
Load Diff
+220
-192
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-18 18:43+0000\n"
|
"POT-Creation-Date: 2026-09-06 19:42+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: Automatically generated\n"
|
"Last-Translator: Automatically generated\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
@@ -64,9 +64,9 @@ msgstr ""
|
|||||||
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
|
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
|
||||||
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
|
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
|
||||||
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
|
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
|
||||||
#: apps/transactions/forms.py:1007 apps/transactions/models.py:331
|
#: apps/transactions/forms.py:1007 apps/transactions/models.py:346
|
||||||
#: apps/transactions/models.py:641 apps/transactions/models.py:841
|
#: apps/transactions/models.py:658 apps/transactions/models.py:858
|
||||||
#: apps/transactions/models.py:1089
|
#: apps/transactions/models.py:1110
|
||||||
#: templates/insights/fragments/category_overview/index.html:86
|
#: templates/insights/fragments/category_overview/index.html:86
|
||||||
#: templates/insights/fragments/category_overview/index.html:542
|
#: templates/insights/fragments/category_overview/index.html:542
|
||||||
#: templates/insights/fragments/month_by_month.html:84
|
#: templates/insights/fragments/month_by_month.html:84
|
||||||
@@ -77,13 +77,13 @@ msgstr ""
|
|||||||
#: apps/accounts/forms.py:132 apps/dca/forms.py:95 apps/dca/forms.py:103
|
#: apps/accounts/forms.py:132 apps/dca/forms.py:95 apps/dca/forms.py:103
|
||||||
#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
|
#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
|
||||||
#: apps/rules/forms.py:184 apps/rules/forms.py:194 apps/rules/models.py:45
|
#: apps/rules/forms.py:184 apps/rules/forms.py:194 apps/rules/models.py:45
|
||||||
#: apps/rules/models.py:315 apps/transactions/filters.py:73
|
#: apps/rules/models.py:315 apps/transactions/filters.py:79
|
||||||
#: apps/transactions/forms.py:69 apps/transactions/forms.py:315
|
#: apps/transactions/forms.py:69 apps/transactions/forms.py:315
|
||||||
#: apps/transactions/forms.py:483 apps/transactions/forms.py:588
|
#: apps/transactions/forms.py:483 apps/transactions/forms.py:588
|
||||||
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
|
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
|
||||||
#: apps/transactions/forms.py:1000 apps/transactions/models.py:337
|
#: apps/transactions/forms.py:1000 apps/transactions/models.py:352
|
||||||
#: apps/transactions/models.py:643 apps/transactions/models.py:845
|
#: apps/transactions/models.py:660 apps/transactions/models.py:862
|
||||||
#: apps/transactions/models.py:1095 templates/includes/sidebar.html:150
|
#: apps/transactions/models.py:1116 templates/includes/sidebar.html:150
|
||||||
#: templates/insights/fragments/category_overview/index.html:40
|
#: templates/insights/fragments/category_overview/index.html:40
|
||||||
#: templates/insights/fragments/month_by_month.html:29
|
#: templates/insights/fragments/month_by_month.html:29
|
||||||
#: templates/insights/fragments/month_by_month.html:32
|
#: templates/insights/fragments/month_by_month.html:32
|
||||||
@@ -95,8 +95,8 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
|
#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
|
||||||
#: apps/import_app/models.py:14 apps/rules/models.py:13
|
#: apps/import_app/models.py:14 apps/rules/models.py:13
|
||||||
#: apps/transactions/models.py:223 apps/transactions/models.py:248
|
#: apps/transactions/models.py:238 apps/transactions/models.py:263
|
||||||
#: apps/transactions/models.py:272 apps/transactions/models.py:1057
|
#: apps/transactions/models.py:287 apps/transactions/models.py:1078
|
||||||
#: apps/users/models.py:572 templates/account_groups/fragments/list.html:22
|
#: apps/users/models.py:572 templates/account_groups/fragments/list.html:22
|
||||||
#: templates/accounts/fragments/list.html:22
|
#: templates/accounts/fragments/list.html:22
|
||||||
#: templates/categories/fragments/table.html:17
|
#: templates/categories/fragments/table.html:17
|
||||||
@@ -164,9 +164,9 @@ msgstr ""
|
|||||||
#: apps/rules/models.py:35 apps/rules/models.py:267
|
#: apps/rules/models.py:35 apps/rules/models.py:267
|
||||||
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
|
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
|
||||||
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
|
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
|
||||||
#: apps/transactions/forms.py:992 apps/transactions/models.py:303
|
#: apps/transactions/forms.py:992 apps/transactions/models.py:318
|
||||||
#: apps/transactions/models.py:601 apps/transactions/models.py:823
|
#: apps/transactions/models.py:618 apps/transactions/models.py:840
|
||||||
#: apps/transactions/models.py:1063
|
#: apps/transactions/models.py:1084
|
||||||
#: templates/installment_plans/fragments/table.html:17
|
#: templates/installment_plans/fragments/table.html:17
|
||||||
#: templates/quick_transactions/fragments/list.html:14
|
#: templates/quick_transactions/fragments/list.html:14
|
||||||
#: templates/recurring_transactions/fragments/table.html:19
|
#: templates/recurring_transactions/fragments/table.html:19
|
||||||
@@ -175,7 +175,7 @@ msgid "Account"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/models.py:76 apps/export_app/forms.py:19
|
#: apps/accounts/models.py:76 apps/export_app/forms.py:19
|
||||||
#: apps/export_app/forms.py:129 apps/transactions/filters.py:57
|
#: apps/export_app/forms.py:129 apps/transactions/filters.py:63
|
||||||
#: templates/accounts/fragments/list.html:9
|
#: templates/accounts/fragments/list.html:9
|
||||||
#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:162
|
#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:162
|
||||||
#: templates/includes/sidebar.html:164
|
#: templates/includes/sidebar.html:164
|
||||||
@@ -188,68 +188,56 @@ msgstr ""
|
|||||||
msgid "Exchange currency cannot be the same as the account's main currency."
|
msgid "Exchange currency cannot be the same as the account's main currency."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:44
|
#: apps/accounts/views/account_groups.py:50
|
||||||
msgid "Account Group added successfully"
|
msgid "Account Group added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:69
|
#: apps/accounts/views/account_groups.py:78
|
||||||
#: apps/accounts/views/account_groups.py:152 apps/accounts/views/accounts.py:68
|
|
||||||
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:62
|
|
||||||
#: apps/dca/views.py:145 apps/rules/views.py:118 apps/rules/views.py:228
|
|
||||||
#: apps/transactions/views/categories.py:91
|
|
||||||
#: apps/transactions/views/categories.py:129
|
|
||||||
#: apps/transactions/views/entities.py:91
|
|
||||||
#: apps/transactions/views/entities.py:171 apps/transactions/views/tags.py:91
|
|
||||||
#: apps/transactions/views/tags.py:171
|
|
||||||
msgid "Only the owner can edit this"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:82
|
|
||||||
msgid "Account Group updated successfully"
|
msgid "Account Group updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:111
|
#: apps/accounts/views/account_groups.py:104
|
||||||
#: apps/accounts/views/accounts.py:145 apps/dca/views.py:104
|
|
||||||
#: apps/rules/views.py:185 apps/transactions/views/categories.py:168
|
|
||||||
#: apps/transactions/views/entities.py:130 apps/transactions/views/tags.py:130
|
|
||||||
msgid "Item no longer shared with you"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:114
|
|
||||||
msgid "Account Group deleted successfully"
|
msgid "Account Group deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:135
|
#: apps/accounts/views/account_groups.py:109
|
||||||
#: apps/accounts/views/accounts.py:189 apps/dca/views.py:128
|
#: apps/accounts/views/accounts.py:137 apps/dca/views.py:106
|
||||||
#: apps/rules/views.py:210 apps/transactions/views/categories.py:192
|
#: apps/rules/views.py:191 apps/transactions/views/categories.py:163
|
||||||
#: apps/transactions/views/entities.py:154 apps/transactions/views/tags.py:154
|
#: apps/transactions/views/entities.py:135 apps/transactions/views/tags.py:131
|
||||||
|
msgid "Item no longer shared with you"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/accounts/views/account_groups.py:132
|
||||||
|
#: apps/accounts/views/accounts.py:182 apps/dca/views.py:131
|
||||||
|
#: 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"
|
msgid "Ownership taken successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:165
|
#: apps/accounts/views/account_groups.py:152
|
||||||
#: apps/accounts/views/accounts.py:119 apps/dca/views.py:158
|
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:151
|
||||||
#: apps/rules/views.py:241 apps/transactions/views/categories.py:142
|
#: apps/rules/views.py:238 apps/transactions/views/categories.py:130
|
||||||
#: apps/transactions/views/entities.py:184 apps/transactions/views/tags.py:184
|
#: apps/transactions/views/entities.py:180 apps/transactions/views/tags.py:174
|
||||||
msgid "Configuration saved successfully"
|
msgid "Configuration saved successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:44
|
#: apps/accounts/views/accounts.py:50
|
||||||
msgid "Account added successfully"
|
msgid "Account added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:81
|
#: apps/accounts/views/accounts.py:78
|
||||||
msgid "Account updated successfully"
|
msgid "Account updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:148
|
#: apps/accounts/views/accounts.py:132
|
||||||
msgid "Account deleted successfully"
|
msgid "Account deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:165
|
#: apps/accounts/views/accounts.py:158
|
||||||
msgid "Account is now tracked"
|
msgid "Account is now tracked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:168
|
#: apps/accounts/views/accounts.py:161
|
||||||
msgid "Account is now untracked"
|
msgid "Account is now untracked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -344,6 +332,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/common/forms.py:76 apps/users/forms.py:172
|
#: apps/common/forms.py:76 apps/users/forms.py:172
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:5
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -458,10 +447,9 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/common/widgets/tom_select.py:15
|
#: apps/common/widgets/tom_select.py:15
|
||||||
#: templates/mini_tools/unit_price_calculator.html:180
|
#: templates/mini_tools/unit_price_calculator.html:180
|
||||||
#: templates/monthly_overview/pages/overview.html:275
|
#: templates/monthly_overview/pages/overview.html:284
|
||||||
#: templates/monthly_overview/pages/overview.html:287
|
#: templates/transactions/fragments/filter_actions.html:48
|
||||||
#: templates/transactions/pages/transactions.html:225
|
#: templates/transactions/pages/transactions.html:233
|
||||||
#: templates/transactions/pages/transactions.html:237
|
|
||||||
msgid "Clear"
|
msgid "Clear"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -480,7 +468,7 @@ msgstr ""
|
|||||||
#: apps/currencies/forms.py:66 apps/dca/models.py:158 apps/rules/forms.py:176
|
#: apps/currencies/forms.py:66 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/rules/forms.py:190 apps/rules/models.py:38 apps/rules/models.py:279
|
||||||
#: apps/transactions/forms.py:85 apps/transactions/forms.py:447
|
#: apps/transactions/forms.py:85 apps/transactions/forms.py:447
|
||||||
#: apps/transactions/forms.py:600 apps/transactions/models.py:313
|
#: apps/transactions/forms.py:600 apps/transactions/models.py:328
|
||||||
#: templates/dca/fragments/strategy/details.html:49
|
#: templates/dca/fragments/strategy/details.html:49
|
||||||
#: templates/exchange_rates/fragments/table.html:10
|
#: templates/exchange_rates/fragments/table.html:10
|
||||||
#: templates/exchange_rates_services/fragments/table.html:11
|
#: templates/exchange_rates_services/fragments/table.html:11
|
||||||
@@ -500,7 +488,7 @@ msgid "Decimal Places"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
|
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
|
||||||
#: apps/export_app/forms.py:130 apps/transactions/filters.py:64
|
#: apps/export_app/forms.py:130 apps/transactions/filters.py:70
|
||||||
#: templates/currencies/fragments/list.html:9
|
#: templates/currencies/fragments/list.html:9
|
||||||
#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:176
|
#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:176
|
||||||
#: templates/includes/sidebar.html:178
|
#: templates/includes/sidebar.html:178
|
||||||
@@ -566,8 +554,8 @@ msgid "Service Type"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/currencies/models.py:119 apps/transactions/filters.py:27
|
#: apps/currencies/models.py:119 apps/transactions/filters.py:27
|
||||||
#: apps/transactions/models.py:227 apps/transactions/models.py:251
|
#: apps/transactions/models.py:242 apps/transactions/models.py:266
|
||||||
#: apps/transactions/models.py:275 templates/categories/fragments/list.html:16
|
#: apps/transactions/models.py:290 templates/categories/fragments/list.html:16
|
||||||
#: templates/entities/fragments/list.html:16
|
#: templates/entities/fragments/list.html:16
|
||||||
#: templates/installment_plans/fragments/list.html:16
|
#: templates/installment_plans/fragments/list.html:16
|
||||||
#: templates/recurring_transactions/fragments/list.html:16
|
#: templates/recurring_transactions/fragments/list.html:16
|
||||||
@@ -746,8 +734,8 @@ msgstr ""
|
|||||||
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
|
#: 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/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
|
||||||
#: apps/transactions/forms.py:469 apps/transactions/forms.py:616
|
#: apps/transactions/forms.py:469 apps/transactions/forms.py:616
|
||||||
#: apps/transactions/models.py:327 apps/transactions/models.py:650
|
#: apps/transactions/models.py:342 apps/transactions/models.py:667
|
||||||
#: apps/transactions/models.py:851 apps/transactions/models.py:1085
|
#: apps/transactions/models.py:868 apps/transactions/models.py:1106
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -779,27 +767,27 @@ msgstr ""
|
|||||||
msgid "DCA Entries"
|
msgid "DCA Entries"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:38
|
#: apps/dca/views.py:44
|
||||||
msgid "DCA Strategy added successfully"
|
msgid "DCA Strategy added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:75
|
#: apps/dca/views.py:73
|
||||||
msgid "DCA Strategy updated successfully"
|
msgid "DCA Strategy updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:107
|
#: apps/dca/views.py:101
|
||||||
msgid "DCA strategy deleted successfully"
|
msgid "DCA strategy deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:237
|
#: apps/dca/views.py:236
|
||||||
msgid "Entry added successfully"
|
msgid "Entry added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:264
|
#: apps/dca/views.py:270
|
||||||
msgid "Entry updated successfully"
|
msgid "Entry updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:290
|
#: apps/dca/views.py:303
|
||||||
msgid "Entry deleted successfully"
|
msgid "Entry deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -810,7 +798,7 @@ msgid "Users"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
|
#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
|
||||||
#: apps/transactions/models.py:388 templates/includes/sidebar.html:81
|
#: apps/transactions/models.py:403 templates/includes/sidebar.html:81
|
||||||
#: templates/includes/sidebar.html:142
|
#: templates/includes/sidebar.html:142
|
||||||
#: templates/recurring_transactions/fragments/list_transactions.html:5
|
#: templates/recurring_transactions/fragments/list_transactions.html:5
|
||||||
#: templates/recurring_transactions/fragments/table.html:37
|
#: templates/recurring_transactions/fragments/table.html:37
|
||||||
@@ -819,7 +807,7 @@ msgid "Transactions"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:37 apps/export_app/forms.py:131
|
#: apps/export_app/forms.py:37 apps/export_app/forms.py:131
|
||||||
#: apps/transactions/filters.py:68 templates/categories/fragments/list.html:9
|
#: apps/transactions/filters.py:74 templates/categories/fragments/list.html:9
|
||||||
#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:144
|
#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:144
|
||||||
#: templates/insights/fragments/month_by_month.html:18
|
#: templates/insights/fragments/month_by_month.html:18
|
||||||
#: templates/insights/fragments/month_by_month.html:21
|
#: templates/insights/fragments/month_by_month.html:21
|
||||||
@@ -830,12 +818,12 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
|
#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
|
||||||
#: apps/rules/forms.py:185 apps/rules/forms.py:195 apps/rules/models.py:46
|
#: apps/rules/forms.py:185 apps/rules/forms.py:195 apps/rules/models.py:46
|
||||||
#: apps/rules/models.py:307 apps/transactions/filters.py:78
|
#: apps/rules/models.py:307 apps/transactions/filters.py:84
|
||||||
#: apps/transactions/forms.py:77 apps/transactions/forms.py:323
|
#: apps/transactions/forms.py:77 apps/transactions/forms.py:323
|
||||||
#: apps/transactions/forms.py:491 apps/transactions/forms.py:771
|
#: apps/transactions/forms.py:491 apps/transactions/forms.py:771
|
||||||
#: apps/transactions/forms.py:1015 apps/transactions/models.py:286
|
#: apps/transactions/forms.py:1015 apps/transactions/models.py:301
|
||||||
#: apps/transactions/models.py:342 apps/transactions/models.py:646
|
#: apps/transactions/models.py:357 apps/transactions/models.py:663
|
||||||
#: apps/transactions/models.py:848 apps/transactions/models.py:1100
|
#: apps/transactions/models.py:865 apps/transactions/models.py:1121
|
||||||
#: templates/entities/fragments/list.html:9
|
#: templates/entities/fragments/list.html:9
|
||||||
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
|
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
|
||||||
#: templates/insights/fragments/category_overview/index.html:54
|
#: templates/insights/fragments/category_overview/index.html:54
|
||||||
@@ -847,14 +835,14 @@ msgid "Entities"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
|
#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
|
||||||
#: apps/transactions/models.py:888 templates/includes/sidebar.html:110
|
#: apps/transactions/models.py:905 templates/includes/sidebar.html:110
|
||||||
#: templates/recurring_transactions/fragments/list.html:9
|
#: templates/recurring_transactions/fragments/list.html:9
|
||||||
#: templates/recurring_transactions/pages/index.html:4
|
#: templates/recurring_transactions/pages/index.html:4
|
||||||
msgid "Recurring Transactions"
|
msgid "Recurring Transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
|
#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
|
||||||
#: apps/transactions/models.py:664 templates/includes/sidebar.html:104
|
#: apps/transactions/models.py:681 templates/includes/sidebar.html:104
|
||||||
#: templates/installment_plans/fragments/list.html:9
|
#: templates/installment_plans/fragments/list.html:9
|
||||||
#: templates/installment_plans/pages/index.html:4
|
#: templates/installment_plans/pages/index.html:4
|
||||||
msgid "Installment Plans"
|
msgid "Installment Plans"
|
||||||
@@ -1006,7 +994,7 @@ msgid "Run deleted successfully"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/insights/forms.py:118 apps/insights/utils/sankey.py:36
|
#: apps/insights/forms.py:118 apps/insights/utils/sankey.py:36
|
||||||
#: apps/insights/utils/sankey.py:167 apps/transactions/filters.py:203
|
#: apps/insights/utils/sankey.py:167 apps/transactions/filters.py:209
|
||||||
#: templates/insights/fragments/category_overview/index.html:96
|
#: templates/insights/fragments/category_overview/index.html:96
|
||||||
#: templates/insights/fragments/category_overview/index.html:407
|
#: templates/insights/fragments/category_overview/index.html:407
|
||||||
#: templates/insights/fragments/category_overview/index.html:436
|
#: templates/insights/fragments/category_overview/index.html:436
|
||||||
@@ -1104,15 +1092,15 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
|
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
|
||||||
#: apps/rules/models.py:271 apps/transactions/forms.py:433
|
#: apps/rules/models.py:271 apps/transactions/forms.py:433
|
||||||
#: apps/transactions/models.py:310 apps/transactions/models.py:606
|
#: apps/transactions/models.py:325 apps/transactions/models.py:623
|
||||||
#: apps/transactions/models.py:829 apps/transactions/models.py:1070
|
#: apps/transactions/models.py:846 apps/transactions/models.py:1091
|
||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
|
#: 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/rules/models.py:275 apps/transactions/filters.py:22
|
||||||
#: apps/transactions/forms.py:437 apps/transactions/models.py:312
|
#: apps/transactions/forms.py:437 apps/transactions/models.py:327
|
||||||
#: apps/transactions/models.py:1072 templates/cotton/transaction/item.html:20
|
#: apps/transactions/models.py:1093 templates/cotton/transaction/item.html:20
|
||||||
#: templates/cotton/transaction/item.html:31
|
#: templates/cotton/transaction/item.html:31
|
||||||
#: templates/transactions/widgets/paid_toggle_button.html:10
|
#: templates/transactions/widgets/paid_toggle_button.html:10
|
||||||
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
|
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
|
||||||
@@ -1122,15 +1110,15 @@ msgstr ""
|
|||||||
#: apps/rules/forms.py:177 apps/rules/forms.py:191 apps/rules/models.py:39
|
#: apps/rules/forms.py:177 apps/rules/forms.py:191 apps/rules/models.py:39
|
||||||
#: apps/rules/models.py:283 apps/transactions/forms.py:89
|
#: apps/rules/models.py:283 apps/transactions/forms.py:89
|
||||||
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
|
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
|
||||||
#: apps/transactions/forms.py:777 apps/transactions/models.py:314
|
#: apps/transactions/forms.py:777 apps/transactions/models.py:329
|
||||||
#: apps/transactions/models.py:624 apps/transactions/models.py:853
|
#: apps/transactions/models.py:641 apps/transactions/models.py:870
|
||||||
msgid "Reference Date"
|
msgid "Reference Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
|
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
|
||||||
#: apps/rules/models.py:287 apps/transactions/forms.py:460
|
#: apps/rules/models.py:287 apps/transactions/forms.py:460
|
||||||
#: apps/transactions/models.py:320 apps/transactions/models.py:834
|
#: apps/transactions/models.py:335 apps/transactions/models.py:851
|
||||||
#: apps/transactions/models.py:1078
|
#: apps/transactions/models.py:1099
|
||||||
#: templates/insights/fragments/sankey.html:102
|
#: templates/insights/fragments/sankey.html:102
|
||||||
#: templates/installment_plans/fragments/table.html:18
|
#: templates/installment_plans/fragments/table.html:18
|
||||||
#: templates/quick_transactions/fragments/list.html:15
|
#: templates/quick_transactions/fragments/list.html:15
|
||||||
@@ -1141,27 +1129,27 @@ msgstr ""
|
|||||||
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
|
#: 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/rules/models.py:42 apps/rules/models.py:291
|
||||||
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
|
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
|
||||||
#: apps/transactions/models.py:325 apps/transactions/models.py:608
|
#: apps/transactions/models.py:340 apps/transactions/models.py:625
|
||||||
#: apps/transactions/models.py:837 apps/transactions/models.py:1083
|
#: apps/transactions/models.py:854 apps/transactions/models.py:1104
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
|
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
|
||||||
#: apps/rules/models.py:299 apps/transactions/models.py:364
|
#: apps/rules/models.py:299 apps/transactions/models.py:379
|
||||||
#: apps/transactions/models.py:1105
|
#: apps/transactions/models.py:1126
|
||||||
msgid "Internal Note"
|
msgid "Internal Note"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
|
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
|
||||||
#: apps/rules/models.py:303 apps/transactions/models.py:366
|
#: apps/rules/models.py:303 apps/transactions/models.py:381
|
||||||
#: apps/transactions/models.py:1107
|
#: apps/transactions/models.py:1128
|
||||||
msgid "Internal ID"
|
msgid "Internal ID"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
|
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
|
||||||
#: apps/rules/models.py:319 apps/transactions/forms.py:620
|
#: apps/rules/models.py:319 apps/transactions/forms.py:620
|
||||||
#: apps/transactions/models.py:224 apps/transactions/models.py:315
|
#: apps/transactions/models.py:239 apps/transactions/models.py:330
|
||||||
#: apps/transactions/models.py:1073
|
#: apps/transactions/models.py:1094
|
||||||
msgid "Mute"
|
msgid "Mute"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1174,7 +1162,7 @@ msgid "Set Values"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
|
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
|
||||||
#: apps/transactions/models.py:387 apps/transactions/models.py:544
|
#: apps/transactions/models.py:402 apps/transactions/models.py:560
|
||||||
msgid "Transaction"
|
msgid "Transaction"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1268,15 +1256,15 @@ msgstr ""
|
|||||||
msgid "Update or create transaction action"
|
msgid "Update or create transaction action"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:71
|
#: apps/rules/views.py:79
|
||||||
msgid "Rule deactivated successfully"
|
msgid "Rule deactivated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:73
|
#: apps/rules/views.py:81
|
||||||
msgid "Rule activated successfully"
|
msgid "Rule activated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:92
|
#: apps/rules/views.py:100
|
||||||
msgid "Rule added successfully"
|
msgid "Rule added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1284,27 +1272,27 @@ msgstr ""
|
|||||||
msgid "Rule updated successfully"
|
msgid "Rule updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:188
|
#: apps/rules/views.py:186
|
||||||
msgid "Rule deleted successfully"
|
msgid "Rule deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:305
|
#: apps/rules/views.py:306
|
||||||
msgid "Action updated successfully"
|
msgid "Action updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:336
|
#: apps/rules/views.py:341
|
||||||
msgid "Action deleted successfully"
|
msgid "Action deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:360
|
#: apps/rules/views.py:367
|
||||||
msgid "Update or Create Transaction action added successfully"
|
msgid "Update or Create Transaction action added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:393
|
#: apps/rules/views.py:402
|
||||||
msgid "Update or Create Transaction action updated successfully"
|
msgid "Update or Create Transaction action updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:423
|
#: apps/rules/views.py:434
|
||||||
msgid "Update or Create Transaction action deleted successfully"
|
msgid "Update or Create Transaction action deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1321,58 +1309,58 @@ msgstr ""
|
|||||||
msgid "Muted"
|
msgid "Muted"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:45
|
#: apps/transactions/filters.py:51
|
||||||
msgid "Content"
|
msgid "Content"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:51
|
#: apps/transactions/filters.py:57
|
||||||
msgid "Transaction Type"
|
msgid "Transaction Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:89
|
#: apps/transactions/filters.py:95
|
||||||
msgid "Mute Status"
|
msgid "Mute Status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:94
|
#: apps/transactions/filters.py:100
|
||||||
msgid "Date from"
|
msgid "Date from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:99 apps/transactions/filters.py:109
|
#: apps/transactions/filters.py:105 apps/transactions/filters.py:115
|
||||||
msgid "Until"
|
msgid "Until"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:104
|
#: apps/transactions/filters.py:110
|
||||||
msgid "Reference date from"
|
msgid "Reference date from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:114
|
#: apps/transactions/filters.py:120
|
||||||
msgid "Amount min"
|
msgid "Amount min"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:119
|
#: apps/transactions/filters.py:125
|
||||||
msgid "Amount max"
|
msgid "Amount max"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:202
|
#: apps/transactions/filters.py:208
|
||||||
msgid "Categorized"
|
msgid "Categorized"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:209
|
#: apps/transactions/filters.py:215
|
||||||
msgid "Tagged"
|
msgid "Tagged"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:209
|
#: apps/transactions/filters.py:215
|
||||||
#: templates/insights/fragments/category_overview/index.html:189
|
#: templates/insights/fragments/category_overview/index.html:189
|
||||||
#: templates/insights/fragments/month_by_month.html:121
|
#: templates/insights/fragments/month_by_month.html:121
|
||||||
#: templates/insights/fragments/year_by_year.html:75
|
#: templates/insights/fragments/year_by_year.html:75
|
||||||
msgid "Untagged"
|
msgid "Untagged"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:215
|
#: apps/transactions/filters.py:221
|
||||||
msgid "Any entity"
|
msgid "Any entity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:216
|
#: apps/transactions/filters.py:222
|
||||||
#: templates/insights/fragments/category_overview/index.html:282
|
#: templates/insights/fragments/category_overview/index.html:282
|
||||||
#: templates/insights/fragments/month_by_month.html:123
|
#: templates/insights/fragments/month_by_month.html:123
|
||||||
#: templates/insights/fragments/year_by_year.html:77
|
#: templates/insights/fragments/year_by_year.html:77
|
||||||
@@ -1447,42 +1435,42 @@ msgstr ""
|
|||||||
msgid "End date should be after the start date"
|
msgid "End date should be after the start date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:229
|
#: apps/transactions/models.py:244
|
||||||
msgid ""
|
msgid ""
|
||||||
"Deactivated categories won't be able to be selected when creating new "
|
"Deactivated categories won't be able to be selected when creating new "
|
||||||
"transactions"
|
"transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:237
|
#: apps/transactions/models.py:252
|
||||||
msgid "Transaction Category"
|
msgid "Transaction Category"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:238
|
#: apps/transactions/models.py:253
|
||||||
msgid "Transaction Categories"
|
msgid "Transaction Categories"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:253
|
#: apps/transactions/models.py:268
|
||||||
msgid ""
|
msgid ""
|
||||||
"Deactivated tags won't be able to be selected when creating new transactions"
|
"Deactivated tags won't be able to be selected when creating new transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:261 apps/transactions/models.py:262
|
#: apps/transactions/models.py:276 apps/transactions/models.py:277
|
||||||
msgid "Transaction Tags"
|
msgid "Transaction Tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:277
|
#: apps/transactions/models.py:292
|
||||||
msgid ""
|
msgid ""
|
||||||
"Deactivated entities won't be able to be selected when creating new "
|
"Deactivated entities won't be able to be selected when creating new "
|
||||||
"transactions"
|
"transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:285
|
#: apps/transactions/models.py:300
|
||||||
#: templates/insights/fragments/month_by_month.html:88
|
#: templates/insights/fragments/month_by_month.html:88
|
||||||
#: templates/insights/fragments/year_by_year.html:56
|
#: templates/insights/fragments/year_by_year.html:56
|
||||||
msgid "Entity"
|
msgid "Entity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:297 apps/transactions/models.py:1050
|
#: apps/transactions/models.py:312 apps/transactions/models.py:1071
|
||||||
#: templates/calendar_view/fragments/list.html:42
|
#: templates/calendar_view/fragments/list.html:42
|
||||||
#: templates/calendar_view/fragments/list.html:44
|
#: templates/calendar_view/fragments/list.html:44
|
||||||
#: templates/calendar_view/fragments/list.html:52
|
#: templates/calendar_view/fragments/list.html:52
|
||||||
@@ -1494,7 +1482,7 @@ msgstr ""
|
|||||||
msgid "Income"
|
msgid "Income"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:298 apps/transactions/models.py:1051
|
#: apps/transactions/models.py:313 apps/transactions/models.py:1072
|
||||||
#: templates/calendar_view/fragments/list.html:46
|
#: templates/calendar_view/fragments/list.html:46
|
||||||
#: templates/calendar_view/fragments/list.html:48
|
#: templates/calendar_view/fragments/list.html:48
|
||||||
#: templates/calendar_view/fragments/list.html:56
|
#: templates/calendar_view/fragments/list.html:56
|
||||||
@@ -1505,157 +1493,157 @@ msgstr ""
|
|||||||
msgid "Expense"
|
msgid "Expense"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:353 apps/transactions/models.py:663
|
#: apps/transactions/models.py:368 apps/transactions/models.py:680
|
||||||
msgid "Installment Plan"
|
msgid "Installment Plan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:362 apps/transactions/models.py:887
|
#: apps/transactions/models.py:377 apps/transactions/models.py:904
|
||||||
msgid "Recurring Transaction"
|
msgid "Recurring Transaction"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:370
|
#: apps/transactions/models.py:385
|
||||||
msgid "Deleted"
|
msgid "Deleted"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:375
|
#: apps/transactions/models.py:390
|
||||||
msgid "Deleted At"
|
msgid "Deleted At"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:489 templates/tags/fragments/table.html:69
|
#: apps/transactions/models.py:504 templates/tags/fragments/table.html:69
|
||||||
msgid "No tags"
|
msgid "No tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:491
|
#: apps/transactions/models.py:506
|
||||||
msgid "No category"
|
msgid "No category"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:493
|
#: apps/transactions/models.py:508
|
||||||
msgid "No description"
|
msgid "No description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:549
|
#: apps/transactions/models.py:565
|
||||||
msgid "File"
|
msgid "File"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:551
|
#: apps/transactions/models.py:567
|
||||||
msgid "Original Name"
|
msgid "Original Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:553
|
#: apps/transactions/models.py:569
|
||||||
msgid "Content Type"
|
msgid "Content Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:555
|
#: apps/transactions/models.py:571
|
||||||
msgid "Size"
|
msgid "Size"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:560
|
#: apps/transactions/models.py:576
|
||||||
msgid "Uploaded By"
|
msgid "Uploaded By"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:565
|
#: apps/transactions/models.py:581
|
||||||
msgid "Transaction Attachment"
|
msgid "Transaction Attachment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:566
|
#: apps/transactions/models.py:582
|
||||||
msgid "Transaction Attachments"
|
msgid "Transaction Attachments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:595 templates/includes/sidebar.html:57
|
#: apps/transactions/models.py:612 templates/includes/sidebar.html:57
|
||||||
msgid "Yearly"
|
msgid "Yearly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:596 apps/users/models.py:469
|
#: apps/transactions/models.py:613 apps/users/models.py:469
|
||||||
#: templates/includes/sidebar.html:51
|
#: templates/includes/sidebar.html:51
|
||||||
msgid "Monthly"
|
msgid "Monthly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:597
|
#: apps/transactions/models.py:614
|
||||||
msgid "Weekly"
|
msgid "Weekly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:598
|
#: apps/transactions/models.py:615
|
||||||
msgid "Daily"
|
msgid "Daily"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:611
|
#: apps/transactions/models.py:628
|
||||||
msgid "Number of Installments"
|
msgid "Number of Installments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:616
|
#: apps/transactions/models.py:633
|
||||||
msgid "Installment Start"
|
msgid "Installment Start"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:617
|
#: apps/transactions/models.py:634
|
||||||
msgid "The installment number to start counting from"
|
msgid "The installment number to start counting from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:622 apps/transactions/models.py:857
|
#: apps/transactions/models.py:639 apps/transactions/models.py:874
|
||||||
msgid "Start Date"
|
msgid "Start Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:626 apps/transactions/models.py:858
|
#: apps/transactions/models.py:643 apps/transactions/models.py:875
|
||||||
msgid "End Date"
|
msgid "End Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:631
|
#: apps/transactions/models.py:648
|
||||||
msgid "Recurrence"
|
msgid "Recurrence"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:634
|
#: apps/transactions/models.py:651
|
||||||
msgid "Installment Amount"
|
msgid "Installment Amount"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:653 apps/transactions/models.py:877
|
#: apps/transactions/models.py:670 apps/transactions/models.py:894
|
||||||
msgid "Add description to transactions"
|
msgid "Add description to transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:656 apps/transactions/models.py:880
|
#: apps/transactions/models.py:673 apps/transactions/models.py:897
|
||||||
msgid "Add notes to transactions"
|
msgid "Add notes to transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:816
|
#: apps/transactions/models.py:833
|
||||||
msgid "day(s)"
|
msgid "day(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:817
|
#: apps/transactions/models.py:834
|
||||||
msgid "week(s)"
|
msgid "week(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:818
|
#: apps/transactions/models.py:835
|
||||||
msgid "month(s)"
|
msgid "month(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:819
|
#: apps/transactions/models.py:836
|
||||||
msgid "year(s)"
|
msgid "year(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:821
|
#: apps/transactions/models.py:838
|
||||||
#: templates/recurring_transactions/fragments/list.html:18
|
#: templates/recurring_transactions/fragments/list.html:18
|
||||||
msgid "Paused"
|
msgid "Paused"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:860
|
#: apps/transactions/models.py:877
|
||||||
msgid "Recurrence Type"
|
msgid "Recurrence Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:863
|
#: apps/transactions/models.py:880
|
||||||
msgid "Recurrence Interval"
|
msgid "Recurrence Interval"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:866
|
#: apps/transactions/models.py:883
|
||||||
msgid "Keep at most"
|
msgid "Keep at most"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:870
|
#: apps/transactions/models.py:887
|
||||||
msgid "Last Generated Date"
|
msgid "Last Generated Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:873
|
#: apps/transactions/models.py:890
|
||||||
msgid "Last Generated Reference Date"
|
msgid "Last Generated Reference Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:1117
|
#: apps/transactions/models.py:1138
|
||||||
#: apps/transactions/views/quick_transactions.py:178
|
#: apps/transactions/views/quick_transactions.py:178
|
||||||
#: apps/transactions/views/quick_transactions.py:187
|
#: apps/transactions/views/quick_transactions.py:187
|
||||||
#: apps/transactions/views/quick_transactions.py:189
|
#: apps/transactions/views/quick_transactions.py:189
|
||||||
@@ -1664,7 +1652,7 @@ msgstr ""
|
|||||||
msgid "Quick Transaction"
|
msgid "Quick Transaction"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:1118 templates/includes/sidebar.html:98
|
#: apps/transactions/models.py:1139 templates/includes/sidebar.html:98
|
||||||
#: templates/quick_transactions/pages/index.html:5
|
#: templates/quick_transactions/pages/index.html:5
|
||||||
#: templates/quick_transactions/pages/index.html:15
|
#: templates/quick_transactions/pages/index.html:15
|
||||||
msgid "Quick Transactions"
|
msgid "Quick Transactions"
|
||||||
@@ -1715,27 +1703,27 @@ msgid_plural "%(count)s transactions duplicated successfully"
|
|||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: apps/transactions/views/categories.py:66
|
#: apps/transactions/views/categories.py:72
|
||||||
msgid "Category added successfully"
|
msgid "Category added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/categories.py:104
|
#: apps/transactions/views/categories.py:102
|
||||||
msgid "Category updated successfully"
|
msgid "Category updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/categories.py:171
|
#: apps/transactions/views/categories.py:158
|
||||||
msgid "Category deleted successfully"
|
msgid "Category deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/entities.py:66
|
#: apps/transactions/views/entities.py:72
|
||||||
msgid "Entity added successfully"
|
msgid "Entity added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/entities.py:104
|
#: apps/transactions/views/entities.py:102
|
||||||
msgid "Entity updated successfully"
|
msgid "Entity updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/entities.py:133
|
#: apps/transactions/views/entities.py:130
|
||||||
msgid "Entity deleted successfully"
|
msgid "Entity deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1798,15 +1786,15 @@ msgstr ""
|
|||||||
msgid "Recurring Transaction deleted successfully"
|
msgid "Recurring Transaction deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/tags.py:66
|
#: apps/transactions/views/tags.py:72
|
||||||
msgid "Tag added successfully"
|
msgid "Tag added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/tags.py:104
|
#: apps/transactions/views/tags.py:100
|
||||||
msgid "Tag updated successfully"
|
msgid "Tag updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/tags.py:133
|
#: apps/transactions/views/tags.py:126
|
||||||
msgid "Tag deleted successfully"
|
msgid "Tag deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1888,9 +1876,9 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/users/forms.py:68 apps/users/forms.py:81 apps/users/forms.py:103
|
#: apps/users/forms.py:68 apps/users/forms.py:81 apps/users/forms.py:103
|
||||||
#: templates/monthly_overview/pages/overview.html:98
|
#: templates/monthly_overview/pages/overview.html:98
|
||||||
#: templates/monthly_overview/pages/overview.html:245
|
#: templates/monthly_overview/pages/overview.html:247
|
||||||
#: templates/transactions/pages/transactions.html:47
|
#: templates/transactions/pages/transactions.html:47
|
||||||
#: templates/transactions/pages/transactions.html:195
|
#: templates/transactions/pages/transactions.html:196
|
||||||
msgid "Default"
|
msgid "Default"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2211,6 +2199,7 @@ msgstr ""
|
|||||||
#: templates/rules/fragments/transaction_rule/view.html:92
|
#: templates/rules/fragments/transaction_rule/view.html:92
|
||||||
#: templates/rules/fragments/transaction_rule/view.html:133
|
#: templates/rules/fragments/transaction_rule/view.html:133
|
||||||
#: templates/tags/fragments/table.html:51
|
#: templates/tags/fragments/table.html:51
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:28
|
||||||
#: templates/users/fragments/api_tokens.html:93
|
#: templates/users/fragments/api_tokens.html:93
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2315,8 +2304,8 @@ msgid "Current balance"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/accounts/fragments/account_reconciliation.html:36
|
#: templates/accounts/fragments/account_reconciliation.html:36
|
||||||
#: templates/net_worth/net_worth.html:146
|
#: templates/net_worth/net_worth.html:147
|
||||||
#: templates/net_worth/net_worth.html:396
|
#: templates/net_worth/net_worth.html:407
|
||||||
msgid "Difference"
|
msgid "Difference"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2696,19 +2685,19 @@ msgstr ""
|
|||||||
msgid "Amount Bought"
|
msgid "Amount Bought"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:384
|
#: templates/dca/fragments/strategy/details.html:409
|
||||||
msgid "Entry Price vs Current Price"
|
msgid "Entry Price vs Current Price"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:398
|
#: templates/dca/fragments/strategy/details.html:423
|
||||||
msgid "Days Between Investments"
|
msgid "Days Between Investments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:444
|
#: templates/dca/fragments/strategy/details.html:469
|
||||||
msgid "Investment Frequency"
|
msgid "Investment Frequency"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:446
|
#: templates/dca/fragments/strategy/details.html:471
|
||||||
msgid "The straighter the blue line, the more consistent your DCA strategy is."
|
msgid "The straighter the blue line, the more consistent your DCA strategy is."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2944,6 +2933,7 @@ msgid "Reload"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/includes/scripts/hyperscript/swal.html:13
|
#: templates/includes/scripts/hyperscript/swal.html:13
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:4
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3358,16 +3348,16 @@ msgid "Summary"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/monthly_overview/pages/overview.html:99
|
#: templates/monthly_overview/pages/overview.html:99
|
||||||
#: templates/monthly_overview/pages/overview.html:254
|
#: templates/monthly_overview/pages/overview.html:256
|
||||||
#: templates/transactions/pages/transactions.html:48
|
#: templates/transactions/pages/transactions.html:48
|
||||||
#: templates/transactions/pages/transactions.html:204
|
#: templates/transactions/pages/transactions.html:205
|
||||||
msgid "Oldest first"
|
msgid "Oldest first"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/monthly_overview/pages/overview.html:100
|
#: templates/monthly_overview/pages/overview.html:100
|
||||||
#: templates/monthly_overview/pages/overview.html:263
|
#: templates/monthly_overview/pages/overview.html:265
|
||||||
#: templates/transactions/pages/transactions.html:49
|
#: templates/transactions/pages/transactions.html:49
|
||||||
#: templates/transactions/pages/transactions.html:213
|
#: templates/transactions/pages/transactions.html:214
|
||||||
msgid "Newest first"
|
msgid "Newest first"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3376,8 +3366,8 @@ msgstr ""
|
|||||||
msgid "Filter transactions"
|
msgid "Filter transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/monthly_overview/pages/overview.html:235
|
#: templates/monthly_overview/pages/overview.html:237
|
||||||
#: templates/transactions/pages/transactions.html:185
|
#: templates/transactions/pages/transactions.html:186
|
||||||
msgid "Order by"
|
msgid "Order by"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3386,24 +3376,24 @@ msgstr ""
|
|||||||
msgid "By currency"
|
msgid "By currency"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:84
|
#: templates/net_worth/net_worth.html:84 templates/net_worth/net_worth.html:85
|
||||||
msgid "Consolidated"
|
msgid "Consolidated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:134
|
#: templates/net_worth/net_worth.html:135
|
||||||
msgid "Evolution"
|
msgid "Evolution"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:162
|
#: templates/net_worth/net_worth.html:163
|
||||||
#: templates/yearly_overview/pages/overview_by_account.html:4
|
#: templates/yearly_overview/pages/overview_by_account.html:4
|
||||||
msgid "By account"
|
msgid "By account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:270
|
#: templates/net_worth/net_worth.html:271
|
||||||
msgid "Evolution by currency"
|
msgid "Evolution by currency"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:334
|
#: templates/net_worth/net_worth.html:345
|
||||||
msgid "Evolution by account"
|
msgid "Evolution by account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3483,15 +3473,19 @@ msgstr ""
|
|||||||
msgid "View"
|
msgid "View"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/rules/fragments/list.html:70
|
#: templates/rules/fragments/list.html:71
|
||||||
msgid "Deactivate"
|
msgid "Deactivate"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/rules/fragments/list.html:70
|
#: templates/rules/fragments/list.html:71
|
||||||
msgid "Activate"
|
msgid "Activate"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/rules/fragments/list.html:89
|
#: templates/rules/fragments/list.html:77
|
||||||
|
msgid "Only the owner can change this"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/rules/fragments/list.html:96
|
||||||
msgid "No rules"
|
msgid "No rules"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3634,6 +3628,40 @@ msgstr ""
|
|||||||
msgid "transactions"
|
msgid "transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:2
|
||||||
|
msgid "Save filter preset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:3
|
||||||
|
msgid "Preset name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:40
|
||||||
|
msgid "Save preset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:6
|
||||||
|
msgid "Filter presets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:21
|
||||||
|
#, python-format
|
||||||
|
msgid "Delete %(name)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:26
|
||||||
|
msgid "Delete filter preset?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:27
|
||||||
|
#, python-format
|
||||||
|
msgid "Delete “%(name)s”?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:34
|
||||||
|
msgid "No saved presets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: templates/transactions/fragments/list_all.html:58
|
#: templates/transactions/fragments/list_all.html:58
|
||||||
msgid "No transactions found"
|
msgid "No transactions found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
+230
-192
File diff suppressed because it is too large
Load Diff
+225
-195
File diff suppressed because it is too large
Load Diff
+226
-192
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+226
-192
File diff suppressed because it is too large
Load Diff
+220
-192
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-18 18:43+0000\n"
|
"POT-Creation-Date: 2026-09-06 19:42+0000\n"
|
||||||
"PO-Revision-Date: 2025-04-14 06:16+0000\n"
|
"PO-Revision-Date: 2025-04-14 06:16+0000\n"
|
||||||
"Last-Translator: Emil <emil.bjorkroth@gmail.com>\n"
|
"Last-Translator: Emil <emil.bjorkroth@gmail.com>\n"
|
||||||
"Language-Team: Swedish <https://translations.herculino.com/projects/wygiwyh/"
|
"Language-Team: Swedish <https://translations.herculino.com/projects/wygiwyh/"
|
||||||
@@ -66,9 +66,9 @@ msgstr ""
|
|||||||
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
|
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
|
||||||
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
|
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
|
||||||
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
|
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
|
||||||
#: apps/transactions/forms.py:1007 apps/transactions/models.py:331
|
#: apps/transactions/forms.py:1007 apps/transactions/models.py:346
|
||||||
#: apps/transactions/models.py:641 apps/transactions/models.py:841
|
#: apps/transactions/models.py:658 apps/transactions/models.py:858
|
||||||
#: apps/transactions/models.py:1089
|
#: apps/transactions/models.py:1110
|
||||||
#: templates/insights/fragments/category_overview/index.html:86
|
#: templates/insights/fragments/category_overview/index.html:86
|
||||||
#: templates/insights/fragments/category_overview/index.html:542
|
#: templates/insights/fragments/category_overview/index.html:542
|
||||||
#: templates/insights/fragments/month_by_month.html:84
|
#: templates/insights/fragments/month_by_month.html:84
|
||||||
@@ -79,13 +79,13 @@ msgstr ""
|
|||||||
#: apps/accounts/forms.py:132 apps/dca/forms.py:95 apps/dca/forms.py:103
|
#: apps/accounts/forms.py:132 apps/dca/forms.py:95 apps/dca/forms.py:103
|
||||||
#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
|
#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
|
||||||
#: apps/rules/forms.py:184 apps/rules/forms.py:194 apps/rules/models.py:45
|
#: apps/rules/forms.py:184 apps/rules/forms.py:194 apps/rules/models.py:45
|
||||||
#: apps/rules/models.py:315 apps/transactions/filters.py:73
|
#: apps/rules/models.py:315 apps/transactions/filters.py:79
|
||||||
#: apps/transactions/forms.py:69 apps/transactions/forms.py:315
|
#: apps/transactions/forms.py:69 apps/transactions/forms.py:315
|
||||||
#: apps/transactions/forms.py:483 apps/transactions/forms.py:588
|
#: apps/transactions/forms.py:483 apps/transactions/forms.py:588
|
||||||
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
|
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
|
||||||
#: apps/transactions/forms.py:1000 apps/transactions/models.py:337
|
#: apps/transactions/forms.py:1000 apps/transactions/models.py:352
|
||||||
#: apps/transactions/models.py:643 apps/transactions/models.py:845
|
#: apps/transactions/models.py:660 apps/transactions/models.py:862
|
||||||
#: apps/transactions/models.py:1095 templates/includes/sidebar.html:150
|
#: apps/transactions/models.py:1116 templates/includes/sidebar.html:150
|
||||||
#: templates/insights/fragments/category_overview/index.html:40
|
#: templates/insights/fragments/category_overview/index.html:40
|
||||||
#: templates/insights/fragments/month_by_month.html:29
|
#: templates/insights/fragments/month_by_month.html:29
|
||||||
#: templates/insights/fragments/month_by_month.html:32
|
#: templates/insights/fragments/month_by_month.html:32
|
||||||
@@ -97,8 +97,8 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
|
#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
|
||||||
#: apps/import_app/models.py:14 apps/rules/models.py:13
|
#: apps/import_app/models.py:14 apps/rules/models.py:13
|
||||||
#: apps/transactions/models.py:223 apps/transactions/models.py:248
|
#: apps/transactions/models.py:238 apps/transactions/models.py:263
|
||||||
#: apps/transactions/models.py:272 apps/transactions/models.py:1057
|
#: apps/transactions/models.py:287 apps/transactions/models.py:1078
|
||||||
#: apps/users/models.py:572 templates/account_groups/fragments/list.html:22
|
#: apps/users/models.py:572 templates/account_groups/fragments/list.html:22
|
||||||
#: templates/accounts/fragments/list.html:22
|
#: templates/accounts/fragments/list.html:22
|
||||||
#: templates/categories/fragments/table.html:17
|
#: templates/categories/fragments/table.html:17
|
||||||
@@ -166,9 +166,9 @@ msgstr ""
|
|||||||
#: apps/rules/models.py:35 apps/rules/models.py:267
|
#: apps/rules/models.py:35 apps/rules/models.py:267
|
||||||
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
|
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
|
||||||
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
|
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
|
||||||
#: apps/transactions/forms.py:992 apps/transactions/models.py:303
|
#: apps/transactions/forms.py:992 apps/transactions/models.py:318
|
||||||
#: apps/transactions/models.py:601 apps/transactions/models.py:823
|
#: apps/transactions/models.py:618 apps/transactions/models.py:840
|
||||||
#: apps/transactions/models.py:1063
|
#: apps/transactions/models.py:1084
|
||||||
#: templates/installment_plans/fragments/table.html:17
|
#: templates/installment_plans/fragments/table.html:17
|
||||||
#: templates/quick_transactions/fragments/list.html:14
|
#: templates/quick_transactions/fragments/list.html:14
|
||||||
#: templates/recurring_transactions/fragments/table.html:19
|
#: templates/recurring_transactions/fragments/table.html:19
|
||||||
@@ -177,7 +177,7 @@ msgid "Account"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/models.py:76 apps/export_app/forms.py:19
|
#: apps/accounts/models.py:76 apps/export_app/forms.py:19
|
||||||
#: apps/export_app/forms.py:129 apps/transactions/filters.py:57
|
#: apps/export_app/forms.py:129 apps/transactions/filters.py:63
|
||||||
#: templates/accounts/fragments/list.html:9
|
#: templates/accounts/fragments/list.html:9
|
||||||
#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:162
|
#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:162
|
||||||
#: templates/includes/sidebar.html:164
|
#: templates/includes/sidebar.html:164
|
||||||
@@ -190,68 +190,56 @@ msgstr ""
|
|||||||
msgid "Exchange currency cannot be the same as the account's main currency."
|
msgid "Exchange currency cannot be the same as the account's main currency."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:44
|
#: apps/accounts/views/account_groups.py:50
|
||||||
msgid "Account Group added successfully"
|
msgid "Account Group added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:69
|
#: apps/accounts/views/account_groups.py:78
|
||||||
#: apps/accounts/views/account_groups.py:152 apps/accounts/views/accounts.py:68
|
|
||||||
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:62
|
|
||||||
#: apps/dca/views.py:145 apps/rules/views.py:118 apps/rules/views.py:228
|
|
||||||
#: apps/transactions/views/categories.py:91
|
|
||||||
#: apps/transactions/views/categories.py:129
|
|
||||||
#: apps/transactions/views/entities.py:91
|
|
||||||
#: apps/transactions/views/entities.py:171 apps/transactions/views/tags.py:91
|
|
||||||
#: apps/transactions/views/tags.py:171
|
|
||||||
msgid "Only the owner can edit this"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:82
|
|
||||||
msgid "Account Group updated successfully"
|
msgid "Account Group updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:111
|
#: apps/accounts/views/account_groups.py:104
|
||||||
#: apps/accounts/views/accounts.py:145 apps/dca/views.py:104
|
|
||||||
#: apps/rules/views.py:185 apps/transactions/views/categories.py:168
|
|
||||||
#: apps/transactions/views/entities.py:130 apps/transactions/views/tags.py:130
|
|
||||||
msgid "Item no longer shared with you"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:114
|
|
||||||
msgid "Account Group deleted successfully"
|
msgid "Account Group deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:135
|
#: apps/accounts/views/account_groups.py:109
|
||||||
#: apps/accounts/views/accounts.py:189 apps/dca/views.py:128
|
#: apps/accounts/views/accounts.py:137 apps/dca/views.py:106
|
||||||
#: apps/rules/views.py:210 apps/transactions/views/categories.py:192
|
#: apps/rules/views.py:191 apps/transactions/views/categories.py:163
|
||||||
#: apps/transactions/views/entities.py:154 apps/transactions/views/tags.py:154
|
#: apps/transactions/views/entities.py:135 apps/transactions/views/tags.py:131
|
||||||
|
msgid "Item no longer shared with you"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/accounts/views/account_groups.py:132
|
||||||
|
#: apps/accounts/views/accounts.py:182 apps/dca/views.py:131
|
||||||
|
#: 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"
|
msgid "Ownership taken successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:165
|
#: apps/accounts/views/account_groups.py:152
|
||||||
#: apps/accounts/views/accounts.py:119 apps/dca/views.py:158
|
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:151
|
||||||
#: apps/rules/views.py:241 apps/transactions/views/categories.py:142
|
#: apps/rules/views.py:238 apps/transactions/views/categories.py:130
|
||||||
#: apps/transactions/views/entities.py:184 apps/transactions/views/tags.py:184
|
#: apps/transactions/views/entities.py:180 apps/transactions/views/tags.py:174
|
||||||
msgid "Configuration saved successfully"
|
msgid "Configuration saved successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:44
|
#: apps/accounts/views/accounts.py:50
|
||||||
msgid "Account added successfully"
|
msgid "Account added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:81
|
#: apps/accounts/views/accounts.py:78
|
||||||
msgid "Account updated successfully"
|
msgid "Account updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:148
|
#: apps/accounts/views/accounts.py:132
|
||||||
msgid "Account deleted successfully"
|
msgid "Account deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:165
|
#: apps/accounts/views/accounts.py:158
|
||||||
msgid "Account is now tracked"
|
msgid "Account is now tracked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:168
|
#: apps/accounts/views/accounts.py:161
|
||||||
msgid "Account is now untracked"
|
msgid "Account is now untracked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -346,6 +334,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/common/forms.py:76 apps/users/forms.py:172
|
#: apps/common/forms.py:76 apps/users/forms.py:172
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:5
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -460,10 +449,9 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/common/widgets/tom_select.py:15
|
#: apps/common/widgets/tom_select.py:15
|
||||||
#: templates/mini_tools/unit_price_calculator.html:180
|
#: templates/mini_tools/unit_price_calculator.html:180
|
||||||
#: templates/monthly_overview/pages/overview.html:275
|
#: templates/monthly_overview/pages/overview.html:284
|
||||||
#: templates/monthly_overview/pages/overview.html:287
|
#: templates/transactions/fragments/filter_actions.html:48
|
||||||
#: templates/transactions/pages/transactions.html:225
|
#: templates/transactions/pages/transactions.html:233
|
||||||
#: templates/transactions/pages/transactions.html:237
|
|
||||||
msgid "Clear"
|
msgid "Clear"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -482,7 +470,7 @@ msgstr ""
|
|||||||
#: apps/currencies/forms.py:66 apps/dca/models.py:158 apps/rules/forms.py:176
|
#: apps/currencies/forms.py:66 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/rules/forms.py:190 apps/rules/models.py:38 apps/rules/models.py:279
|
||||||
#: apps/transactions/forms.py:85 apps/transactions/forms.py:447
|
#: apps/transactions/forms.py:85 apps/transactions/forms.py:447
|
||||||
#: apps/transactions/forms.py:600 apps/transactions/models.py:313
|
#: apps/transactions/forms.py:600 apps/transactions/models.py:328
|
||||||
#: templates/dca/fragments/strategy/details.html:49
|
#: templates/dca/fragments/strategy/details.html:49
|
||||||
#: templates/exchange_rates/fragments/table.html:10
|
#: templates/exchange_rates/fragments/table.html:10
|
||||||
#: templates/exchange_rates_services/fragments/table.html:11
|
#: templates/exchange_rates_services/fragments/table.html:11
|
||||||
@@ -502,7 +490,7 @@ msgid "Decimal Places"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
|
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
|
||||||
#: apps/export_app/forms.py:130 apps/transactions/filters.py:64
|
#: apps/export_app/forms.py:130 apps/transactions/filters.py:70
|
||||||
#: templates/currencies/fragments/list.html:9
|
#: templates/currencies/fragments/list.html:9
|
||||||
#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:176
|
#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:176
|
||||||
#: templates/includes/sidebar.html:178
|
#: templates/includes/sidebar.html:178
|
||||||
@@ -568,8 +556,8 @@ msgid "Service Type"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/currencies/models.py:119 apps/transactions/filters.py:27
|
#: apps/currencies/models.py:119 apps/transactions/filters.py:27
|
||||||
#: apps/transactions/models.py:227 apps/transactions/models.py:251
|
#: apps/transactions/models.py:242 apps/transactions/models.py:266
|
||||||
#: apps/transactions/models.py:275 templates/categories/fragments/list.html:16
|
#: apps/transactions/models.py:290 templates/categories/fragments/list.html:16
|
||||||
#: templates/entities/fragments/list.html:16
|
#: templates/entities/fragments/list.html:16
|
||||||
#: templates/installment_plans/fragments/list.html:16
|
#: templates/installment_plans/fragments/list.html:16
|
||||||
#: templates/recurring_transactions/fragments/list.html:16
|
#: templates/recurring_transactions/fragments/list.html:16
|
||||||
@@ -748,8 +736,8 @@ msgstr ""
|
|||||||
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
|
#: 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/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
|
||||||
#: apps/transactions/forms.py:469 apps/transactions/forms.py:616
|
#: apps/transactions/forms.py:469 apps/transactions/forms.py:616
|
||||||
#: apps/transactions/models.py:327 apps/transactions/models.py:650
|
#: apps/transactions/models.py:342 apps/transactions/models.py:667
|
||||||
#: apps/transactions/models.py:851 apps/transactions/models.py:1085
|
#: apps/transactions/models.py:868 apps/transactions/models.py:1106
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -781,27 +769,27 @@ msgstr ""
|
|||||||
msgid "DCA Entries"
|
msgid "DCA Entries"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:38
|
#: apps/dca/views.py:44
|
||||||
msgid "DCA Strategy added successfully"
|
msgid "DCA Strategy added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:75
|
#: apps/dca/views.py:73
|
||||||
msgid "DCA Strategy updated successfully"
|
msgid "DCA Strategy updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:107
|
#: apps/dca/views.py:101
|
||||||
msgid "DCA strategy deleted successfully"
|
msgid "DCA strategy deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:237
|
#: apps/dca/views.py:236
|
||||||
msgid "Entry added successfully"
|
msgid "Entry added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:264
|
#: apps/dca/views.py:270
|
||||||
msgid "Entry updated successfully"
|
msgid "Entry updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:290
|
#: apps/dca/views.py:303
|
||||||
msgid "Entry deleted successfully"
|
msgid "Entry deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -812,7 +800,7 @@ msgid "Users"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
|
#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
|
||||||
#: apps/transactions/models.py:388 templates/includes/sidebar.html:81
|
#: apps/transactions/models.py:403 templates/includes/sidebar.html:81
|
||||||
#: templates/includes/sidebar.html:142
|
#: templates/includes/sidebar.html:142
|
||||||
#: templates/recurring_transactions/fragments/list_transactions.html:5
|
#: templates/recurring_transactions/fragments/list_transactions.html:5
|
||||||
#: templates/recurring_transactions/fragments/table.html:37
|
#: templates/recurring_transactions/fragments/table.html:37
|
||||||
@@ -821,7 +809,7 @@ msgid "Transactions"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:37 apps/export_app/forms.py:131
|
#: apps/export_app/forms.py:37 apps/export_app/forms.py:131
|
||||||
#: apps/transactions/filters.py:68 templates/categories/fragments/list.html:9
|
#: apps/transactions/filters.py:74 templates/categories/fragments/list.html:9
|
||||||
#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:144
|
#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:144
|
||||||
#: templates/insights/fragments/month_by_month.html:18
|
#: templates/insights/fragments/month_by_month.html:18
|
||||||
#: templates/insights/fragments/month_by_month.html:21
|
#: templates/insights/fragments/month_by_month.html:21
|
||||||
@@ -832,12 +820,12 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
|
#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
|
||||||
#: apps/rules/forms.py:185 apps/rules/forms.py:195 apps/rules/models.py:46
|
#: apps/rules/forms.py:185 apps/rules/forms.py:195 apps/rules/models.py:46
|
||||||
#: apps/rules/models.py:307 apps/transactions/filters.py:78
|
#: apps/rules/models.py:307 apps/transactions/filters.py:84
|
||||||
#: apps/transactions/forms.py:77 apps/transactions/forms.py:323
|
#: apps/transactions/forms.py:77 apps/transactions/forms.py:323
|
||||||
#: apps/transactions/forms.py:491 apps/transactions/forms.py:771
|
#: apps/transactions/forms.py:491 apps/transactions/forms.py:771
|
||||||
#: apps/transactions/forms.py:1015 apps/transactions/models.py:286
|
#: apps/transactions/forms.py:1015 apps/transactions/models.py:301
|
||||||
#: apps/transactions/models.py:342 apps/transactions/models.py:646
|
#: apps/transactions/models.py:357 apps/transactions/models.py:663
|
||||||
#: apps/transactions/models.py:848 apps/transactions/models.py:1100
|
#: apps/transactions/models.py:865 apps/transactions/models.py:1121
|
||||||
#: templates/entities/fragments/list.html:9
|
#: templates/entities/fragments/list.html:9
|
||||||
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
|
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
|
||||||
#: templates/insights/fragments/category_overview/index.html:54
|
#: templates/insights/fragments/category_overview/index.html:54
|
||||||
@@ -849,14 +837,14 @@ msgid "Entities"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
|
#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
|
||||||
#: apps/transactions/models.py:888 templates/includes/sidebar.html:110
|
#: apps/transactions/models.py:905 templates/includes/sidebar.html:110
|
||||||
#: templates/recurring_transactions/fragments/list.html:9
|
#: templates/recurring_transactions/fragments/list.html:9
|
||||||
#: templates/recurring_transactions/pages/index.html:4
|
#: templates/recurring_transactions/pages/index.html:4
|
||||||
msgid "Recurring Transactions"
|
msgid "Recurring Transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
|
#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
|
||||||
#: apps/transactions/models.py:664 templates/includes/sidebar.html:104
|
#: apps/transactions/models.py:681 templates/includes/sidebar.html:104
|
||||||
#: templates/installment_plans/fragments/list.html:9
|
#: templates/installment_plans/fragments/list.html:9
|
||||||
#: templates/installment_plans/pages/index.html:4
|
#: templates/installment_plans/pages/index.html:4
|
||||||
msgid "Installment Plans"
|
msgid "Installment Plans"
|
||||||
@@ -1008,7 +996,7 @@ msgid "Run deleted successfully"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/insights/forms.py:118 apps/insights/utils/sankey.py:36
|
#: apps/insights/forms.py:118 apps/insights/utils/sankey.py:36
|
||||||
#: apps/insights/utils/sankey.py:167 apps/transactions/filters.py:203
|
#: apps/insights/utils/sankey.py:167 apps/transactions/filters.py:209
|
||||||
#: templates/insights/fragments/category_overview/index.html:96
|
#: templates/insights/fragments/category_overview/index.html:96
|
||||||
#: templates/insights/fragments/category_overview/index.html:407
|
#: templates/insights/fragments/category_overview/index.html:407
|
||||||
#: templates/insights/fragments/category_overview/index.html:436
|
#: templates/insights/fragments/category_overview/index.html:436
|
||||||
@@ -1106,15 +1094,15 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
|
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
|
||||||
#: apps/rules/models.py:271 apps/transactions/forms.py:433
|
#: apps/rules/models.py:271 apps/transactions/forms.py:433
|
||||||
#: apps/transactions/models.py:310 apps/transactions/models.py:606
|
#: apps/transactions/models.py:325 apps/transactions/models.py:623
|
||||||
#: apps/transactions/models.py:829 apps/transactions/models.py:1070
|
#: apps/transactions/models.py:846 apps/transactions/models.py:1091
|
||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
|
#: 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/rules/models.py:275 apps/transactions/filters.py:22
|
||||||
#: apps/transactions/forms.py:437 apps/transactions/models.py:312
|
#: apps/transactions/forms.py:437 apps/transactions/models.py:327
|
||||||
#: apps/transactions/models.py:1072 templates/cotton/transaction/item.html:20
|
#: apps/transactions/models.py:1093 templates/cotton/transaction/item.html:20
|
||||||
#: templates/cotton/transaction/item.html:31
|
#: templates/cotton/transaction/item.html:31
|
||||||
#: templates/transactions/widgets/paid_toggle_button.html:10
|
#: templates/transactions/widgets/paid_toggle_button.html:10
|
||||||
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
|
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
|
||||||
@@ -1124,15 +1112,15 @@ msgstr ""
|
|||||||
#: apps/rules/forms.py:177 apps/rules/forms.py:191 apps/rules/models.py:39
|
#: apps/rules/forms.py:177 apps/rules/forms.py:191 apps/rules/models.py:39
|
||||||
#: apps/rules/models.py:283 apps/transactions/forms.py:89
|
#: apps/rules/models.py:283 apps/transactions/forms.py:89
|
||||||
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
|
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
|
||||||
#: apps/transactions/forms.py:777 apps/transactions/models.py:314
|
#: apps/transactions/forms.py:777 apps/transactions/models.py:329
|
||||||
#: apps/transactions/models.py:624 apps/transactions/models.py:853
|
#: apps/transactions/models.py:641 apps/transactions/models.py:870
|
||||||
msgid "Reference Date"
|
msgid "Reference Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
|
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
|
||||||
#: apps/rules/models.py:287 apps/transactions/forms.py:460
|
#: apps/rules/models.py:287 apps/transactions/forms.py:460
|
||||||
#: apps/transactions/models.py:320 apps/transactions/models.py:834
|
#: apps/transactions/models.py:335 apps/transactions/models.py:851
|
||||||
#: apps/transactions/models.py:1078
|
#: apps/transactions/models.py:1099
|
||||||
#: templates/insights/fragments/sankey.html:102
|
#: templates/insights/fragments/sankey.html:102
|
||||||
#: templates/installment_plans/fragments/table.html:18
|
#: templates/installment_plans/fragments/table.html:18
|
||||||
#: templates/quick_transactions/fragments/list.html:15
|
#: templates/quick_transactions/fragments/list.html:15
|
||||||
@@ -1143,27 +1131,27 @@ msgstr ""
|
|||||||
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
|
#: 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/rules/models.py:42 apps/rules/models.py:291
|
||||||
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
|
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
|
||||||
#: apps/transactions/models.py:325 apps/transactions/models.py:608
|
#: apps/transactions/models.py:340 apps/transactions/models.py:625
|
||||||
#: apps/transactions/models.py:837 apps/transactions/models.py:1083
|
#: apps/transactions/models.py:854 apps/transactions/models.py:1104
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
|
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
|
||||||
#: apps/rules/models.py:299 apps/transactions/models.py:364
|
#: apps/rules/models.py:299 apps/transactions/models.py:379
|
||||||
#: apps/transactions/models.py:1105
|
#: apps/transactions/models.py:1126
|
||||||
msgid "Internal Note"
|
msgid "Internal Note"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
|
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
|
||||||
#: apps/rules/models.py:303 apps/transactions/models.py:366
|
#: apps/rules/models.py:303 apps/transactions/models.py:381
|
||||||
#: apps/transactions/models.py:1107
|
#: apps/transactions/models.py:1128
|
||||||
msgid "Internal ID"
|
msgid "Internal ID"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
|
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
|
||||||
#: apps/rules/models.py:319 apps/transactions/forms.py:620
|
#: apps/rules/models.py:319 apps/transactions/forms.py:620
|
||||||
#: apps/transactions/models.py:224 apps/transactions/models.py:315
|
#: apps/transactions/models.py:239 apps/transactions/models.py:330
|
||||||
#: apps/transactions/models.py:1073
|
#: apps/transactions/models.py:1094
|
||||||
msgid "Mute"
|
msgid "Mute"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1176,7 +1164,7 @@ msgid "Set Values"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
|
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
|
||||||
#: apps/transactions/models.py:387 apps/transactions/models.py:544
|
#: apps/transactions/models.py:402 apps/transactions/models.py:560
|
||||||
msgid "Transaction"
|
msgid "Transaction"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1270,15 +1258,15 @@ msgstr ""
|
|||||||
msgid "Update or create transaction action"
|
msgid "Update or create transaction action"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:71
|
#: apps/rules/views.py:79
|
||||||
msgid "Rule deactivated successfully"
|
msgid "Rule deactivated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:73
|
#: apps/rules/views.py:81
|
||||||
msgid "Rule activated successfully"
|
msgid "Rule activated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:92
|
#: apps/rules/views.py:100
|
||||||
msgid "Rule added successfully"
|
msgid "Rule added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1286,27 +1274,27 @@ msgstr ""
|
|||||||
msgid "Rule updated successfully"
|
msgid "Rule updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:188
|
#: apps/rules/views.py:186
|
||||||
msgid "Rule deleted successfully"
|
msgid "Rule deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:305
|
#: apps/rules/views.py:306
|
||||||
msgid "Action updated successfully"
|
msgid "Action updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:336
|
#: apps/rules/views.py:341
|
||||||
msgid "Action deleted successfully"
|
msgid "Action deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:360
|
#: apps/rules/views.py:367
|
||||||
msgid "Update or Create Transaction action added successfully"
|
msgid "Update or Create Transaction action added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:393
|
#: apps/rules/views.py:402
|
||||||
msgid "Update or Create Transaction action updated successfully"
|
msgid "Update or Create Transaction action updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:423
|
#: apps/rules/views.py:434
|
||||||
msgid "Update or Create Transaction action deleted successfully"
|
msgid "Update or Create Transaction action deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1323,58 +1311,58 @@ msgstr ""
|
|||||||
msgid "Muted"
|
msgid "Muted"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:45
|
#: apps/transactions/filters.py:51
|
||||||
msgid "Content"
|
msgid "Content"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:51
|
#: apps/transactions/filters.py:57
|
||||||
msgid "Transaction Type"
|
msgid "Transaction Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:89
|
#: apps/transactions/filters.py:95
|
||||||
msgid "Mute Status"
|
msgid "Mute Status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:94
|
#: apps/transactions/filters.py:100
|
||||||
msgid "Date from"
|
msgid "Date from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:99 apps/transactions/filters.py:109
|
#: apps/transactions/filters.py:105 apps/transactions/filters.py:115
|
||||||
msgid "Until"
|
msgid "Until"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:104
|
#: apps/transactions/filters.py:110
|
||||||
msgid "Reference date from"
|
msgid "Reference date from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:114
|
#: apps/transactions/filters.py:120
|
||||||
msgid "Amount min"
|
msgid "Amount min"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:119
|
#: apps/transactions/filters.py:125
|
||||||
msgid "Amount max"
|
msgid "Amount max"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:202
|
#: apps/transactions/filters.py:208
|
||||||
msgid "Categorized"
|
msgid "Categorized"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:209
|
#: apps/transactions/filters.py:215
|
||||||
msgid "Tagged"
|
msgid "Tagged"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:209
|
#: apps/transactions/filters.py:215
|
||||||
#: templates/insights/fragments/category_overview/index.html:189
|
#: templates/insights/fragments/category_overview/index.html:189
|
||||||
#: templates/insights/fragments/month_by_month.html:121
|
#: templates/insights/fragments/month_by_month.html:121
|
||||||
#: templates/insights/fragments/year_by_year.html:75
|
#: templates/insights/fragments/year_by_year.html:75
|
||||||
msgid "Untagged"
|
msgid "Untagged"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:215
|
#: apps/transactions/filters.py:221
|
||||||
msgid "Any entity"
|
msgid "Any entity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:216
|
#: apps/transactions/filters.py:222
|
||||||
#: templates/insights/fragments/category_overview/index.html:282
|
#: templates/insights/fragments/category_overview/index.html:282
|
||||||
#: templates/insights/fragments/month_by_month.html:123
|
#: templates/insights/fragments/month_by_month.html:123
|
||||||
#: templates/insights/fragments/year_by_year.html:77
|
#: templates/insights/fragments/year_by_year.html:77
|
||||||
@@ -1449,42 +1437,42 @@ msgstr ""
|
|||||||
msgid "End date should be after the start date"
|
msgid "End date should be after the start date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:229
|
#: apps/transactions/models.py:244
|
||||||
msgid ""
|
msgid ""
|
||||||
"Deactivated categories won't be able to be selected when creating new "
|
"Deactivated categories won't be able to be selected when creating new "
|
||||||
"transactions"
|
"transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:237
|
#: apps/transactions/models.py:252
|
||||||
msgid "Transaction Category"
|
msgid "Transaction Category"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:238
|
#: apps/transactions/models.py:253
|
||||||
msgid "Transaction Categories"
|
msgid "Transaction Categories"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:253
|
#: apps/transactions/models.py:268
|
||||||
msgid ""
|
msgid ""
|
||||||
"Deactivated tags won't be able to be selected when creating new transactions"
|
"Deactivated tags won't be able to be selected when creating new transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:261 apps/transactions/models.py:262
|
#: apps/transactions/models.py:276 apps/transactions/models.py:277
|
||||||
msgid "Transaction Tags"
|
msgid "Transaction Tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:277
|
#: apps/transactions/models.py:292
|
||||||
msgid ""
|
msgid ""
|
||||||
"Deactivated entities won't be able to be selected when creating new "
|
"Deactivated entities won't be able to be selected when creating new "
|
||||||
"transactions"
|
"transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:285
|
#: apps/transactions/models.py:300
|
||||||
#: templates/insights/fragments/month_by_month.html:88
|
#: templates/insights/fragments/month_by_month.html:88
|
||||||
#: templates/insights/fragments/year_by_year.html:56
|
#: templates/insights/fragments/year_by_year.html:56
|
||||||
msgid "Entity"
|
msgid "Entity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:297 apps/transactions/models.py:1050
|
#: apps/transactions/models.py:312 apps/transactions/models.py:1071
|
||||||
#: templates/calendar_view/fragments/list.html:42
|
#: templates/calendar_view/fragments/list.html:42
|
||||||
#: templates/calendar_view/fragments/list.html:44
|
#: templates/calendar_view/fragments/list.html:44
|
||||||
#: templates/calendar_view/fragments/list.html:52
|
#: templates/calendar_view/fragments/list.html:52
|
||||||
@@ -1496,7 +1484,7 @@ msgstr ""
|
|||||||
msgid "Income"
|
msgid "Income"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:298 apps/transactions/models.py:1051
|
#: apps/transactions/models.py:313 apps/transactions/models.py:1072
|
||||||
#: templates/calendar_view/fragments/list.html:46
|
#: templates/calendar_view/fragments/list.html:46
|
||||||
#: templates/calendar_view/fragments/list.html:48
|
#: templates/calendar_view/fragments/list.html:48
|
||||||
#: templates/calendar_view/fragments/list.html:56
|
#: templates/calendar_view/fragments/list.html:56
|
||||||
@@ -1507,157 +1495,157 @@ msgstr ""
|
|||||||
msgid "Expense"
|
msgid "Expense"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:353 apps/transactions/models.py:663
|
#: apps/transactions/models.py:368 apps/transactions/models.py:680
|
||||||
msgid "Installment Plan"
|
msgid "Installment Plan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:362 apps/transactions/models.py:887
|
#: apps/transactions/models.py:377 apps/transactions/models.py:904
|
||||||
msgid "Recurring Transaction"
|
msgid "Recurring Transaction"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:370
|
#: apps/transactions/models.py:385
|
||||||
msgid "Deleted"
|
msgid "Deleted"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:375
|
#: apps/transactions/models.py:390
|
||||||
msgid "Deleted At"
|
msgid "Deleted At"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:489 templates/tags/fragments/table.html:69
|
#: apps/transactions/models.py:504 templates/tags/fragments/table.html:69
|
||||||
msgid "No tags"
|
msgid "No tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:491
|
#: apps/transactions/models.py:506
|
||||||
msgid "No category"
|
msgid "No category"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:493
|
#: apps/transactions/models.py:508
|
||||||
msgid "No description"
|
msgid "No description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:549
|
#: apps/transactions/models.py:565
|
||||||
msgid "File"
|
msgid "File"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:551
|
#: apps/transactions/models.py:567
|
||||||
msgid "Original Name"
|
msgid "Original Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:553
|
#: apps/transactions/models.py:569
|
||||||
msgid "Content Type"
|
msgid "Content Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:555
|
#: apps/transactions/models.py:571
|
||||||
msgid "Size"
|
msgid "Size"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:560
|
#: apps/transactions/models.py:576
|
||||||
msgid "Uploaded By"
|
msgid "Uploaded By"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:565
|
#: apps/transactions/models.py:581
|
||||||
msgid "Transaction Attachment"
|
msgid "Transaction Attachment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:566
|
#: apps/transactions/models.py:582
|
||||||
msgid "Transaction Attachments"
|
msgid "Transaction Attachments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:595 templates/includes/sidebar.html:57
|
#: apps/transactions/models.py:612 templates/includes/sidebar.html:57
|
||||||
msgid "Yearly"
|
msgid "Yearly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:596 apps/users/models.py:469
|
#: apps/transactions/models.py:613 apps/users/models.py:469
|
||||||
#: templates/includes/sidebar.html:51
|
#: templates/includes/sidebar.html:51
|
||||||
msgid "Monthly"
|
msgid "Monthly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:597
|
#: apps/transactions/models.py:614
|
||||||
msgid "Weekly"
|
msgid "Weekly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:598
|
#: apps/transactions/models.py:615
|
||||||
msgid "Daily"
|
msgid "Daily"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:611
|
#: apps/transactions/models.py:628
|
||||||
msgid "Number of Installments"
|
msgid "Number of Installments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:616
|
#: apps/transactions/models.py:633
|
||||||
msgid "Installment Start"
|
msgid "Installment Start"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:617
|
#: apps/transactions/models.py:634
|
||||||
msgid "The installment number to start counting from"
|
msgid "The installment number to start counting from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:622 apps/transactions/models.py:857
|
#: apps/transactions/models.py:639 apps/transactions/models.py:874
|
||||||
msgid "Start Date"
|
msgid "Start Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:626 apps/transactions/models.py:858
|
#: apps/transactions/models.py:643 apps/transactions/models.py:875
|
||||||
msgid "End Date"
|
msgid "End Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:631
|
#: apps/transactions/models.py:648
|
||||||
msgid "Recurrence"
|
msgid "Recurrence"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:634
|
#: apps/transactions/models.py:651
|
||||||
msgid "Installment Amount"
|
msgid "Installment Amount"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:653 apps/transactions/models.py:877
|
#: apps/transactions/models.py:670 apps/transactions/models.py:894
|
||||||
msgid "Add description to transactions"
|
msgid "Add description to transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:656 apps/transactions/models.py:880
|
#: apps/transactions/models.py:673 apps/transactions/models.py:897
|
||||||
msgid "Add notes to transactions"
|
msgid "Add notes to transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:816
|
#: apps/transactions/models.py:833
|
||||||
msgid "day(s)"
|
msgid "day(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:817
|
#: apps/transactions/models.py:834
|
||||||
msgid "week(s)"
|
msgid "week(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:818
|
#: apps/transactions/models.py:835
|
||||||
msgid "month(s)"
|
msgid "month(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:819
|
#: apps/transactions/models.py:836
|
||||||
msgid "year(s)"
|
msgid "year(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:821
|
#: apps/transactions/models.py:838
|
||||||
#: templates/recurring_transactions/fragments/list.html:18
|
#: templates/recurring_transactions/fragments/list.html:18
|
||||||
msgid "Paused"
|
msgid "Paused"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:860
|
#: apps/transactions/models.py:877
|
||||||
msgid "Recurrence Type"
|
msgid "Recurrence Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:863
|
#: apps/transactions/models.py:880
|
||||||
msgid "Recurrence Interval"
|
msgid "Recurrence Interval"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:866
|
#: apps/transactions/models.py:883
|
||||||
msgid "Keep at most"
|
msgid "Keep at most"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:870
|
#: apps/transactions/models.py:887
|
||||||
msgid "Last Generated Date"
|
msgid "Last Generated Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:873
|
#: apps/transactions/models.py:890
|
||||||
msgid "Last Generated Reference Date"
|
msgid "Last Generated Reference Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:1117
|
#: apps/transactions/models.py:1138
|
||||||
#: apps/transactions/views/quick_transactions.py:178
|
#: apps/transactions/views/quick_transactions.py:178
|
||||||
#: apps/transactions/views/quick_transactions.py:187
|
#: apps/transactions/views/quick_transactions.py:187
|
||||||
#: apps/transactions/views/quick_transactions.py:189
|
#: apps/transactions/views/quick_transactions.py:189
|
||||||
@@ -1666,7 +1654,7 @@ msgstr ""
|
|||||||
msgid "Quick Transaction"
|
msgid "Quick Transaction"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:1118 templates/includes/sidebar.html:98
|
#: apps/transactions/models.py:1139 templates/includes/sidebar.html:98
|
||||||
#: templates/quick_transactions/pages/index.html:5
|
#: templates/quick_transactions/pages/index.html:5
|
||||||
#: templates/quick_transactions/pages/index.html:15
|
#: templates/quick_transactions/pages/index.html:15
|
||||||
msgid "Quick Transactions"
|
msgid "Quick Transactions"
|
||||||
@@ -1717,27 +1705,27 @@ msgid_plural "%(count)s transactions duplicated successfully"
|
|||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: apps/transactions/views/categories.py:66
|
#: apps/transactions/views/categories.py:72
|
||||||
msgid "Category added successfully"
|
msgid "Category added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/categories.py:104
|
#: apps/transactions/views/categories.py:102
|
||||||
msgid "Category updated successfully"
|
msgid "Category updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/categories.py:171
|
#: apps/transactions/views/categories.py:158
|
||||||
msgid "Category deleted successfully"
|
msgid "Category deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/entities.py:66
|
#: apps/transactions/views/entities.py:72
|
||||||
msgid "Entity added successfully"
|
msgid "Entity added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/entities.py:104
|
#: apps/transactions/views/entities.py:102
|
||||||
msgid "Entity updated successfully"
|
msgid "Entity updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/entities.py:133
|
#: apps/transactions/views/entities.py:130
|
||||||
msgid "Entity deleted successfully"
|
msgid "Entity deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1800,15 +1788,15 @@ msgstr ""
|
|||||||
msgid "Recurring Transaction deleted successfully"
|
msgid "Recurring Transaction deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/tags.py:66
|
#: apps/transactions/views/tags.py:72
|
||||||
msgid "Tag added successfully"
|
msgid "Tag added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/tags.py:104
|
#: apps/transactions/views/tags.py:100
|
||||||
msgid "Tag updated successfully"
|
msgid "Tag updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/tags.py:133
|
#: apps/transactions/views/tags.py:126
|
||||||
msgid "Tag deleted successfully"
|
msgid "Tag deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1890,9 +1878,9 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/users/forms.py:68 apps/users/forms.py:81 apps/users/forms.py:103
|
#: apps/users/forms.py:68 apps/users/forms.py:81 apps/users/forms.py:103
|
||||||
#: templates/monthly_overview/pages/overview.html:98
|
#: templates/monthly_overview/pages/overview.html:98
|
||||||
#: templates/monthly_overview/pages/overview.html:245
|
#: templates/monthly_overview/pages/overview.html:247
|
||||||
#: templates/transactions/pages/transactions.html:47
|
#: templates/transactions/pages/transactions.html:47
|
||||||
#: templates/transactions/pages/transactions.html:195
|
#: templates/transactions/pages/transactions.html:196
|
||||||
msgid "Default"
|
msgid "Default"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2215,6 +2203,7 @@ msgstr ""
|
|||||||
#: templates/rules/fragments/transaction_rule/view.html:92
|
#: templates/rules/fragments/transaction_rule/view.html:92
|
||||||
#: templates/rules/fragments/transaction_rule/view.html:133
|
#: templates/rules/fragments/transaction_rule/view.html:133
|
||||||
#: templates/tags/fragments/table.html:51
|
#: templates/tags/fragments/table.html:51
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:28
|
||||||
#: templates/users/fragments/api_tokens.html:93
|
#: templates/users/fragments/api_tokens.html:93
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2319,8 +2308,8 @@ msgid "Current balance"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/accounts/fragments/account_reconciliation.html:36
|
#: templates/accounts/fragments/account_reconciliation.html:36
|
||||||
#: templates/net_worth/net_worth.html:146
|
#: templates/net_worth/net_worth.html:147
|
||||||
#: templates/net_worth/net_worth.html:396
|
#: templates/net_worth/net_worth.html:407
|
||||||
msgid "Difference"
|
msgid "Difference"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2700,19 +2689,19 @@ msgstr ""
|
|||||||
msgid "Amount Bought"
|
msgid "Amount Bought"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:384
|
#: templates/dca/fragments/strategy/details.html:409
|
||||||
msgid "Entry Price vs Current Price"
|
msgid "Entry Price vs Current Price"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:398
|
#: templates/dca/fragments/strategy/details.html:423
|
||||||
msgid "Days Between Investments"
|
msgid "Days Between Investments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:444
|
#: templates/dca/fragments/strategy/details.html:469
|
||||||
msgid "Investment Frequency"
|
msgid "Investment Frequency"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:446
|
#: templates/dca/fragments/strategy/details.html:471
|
||||||
msgid "The straighter the blue line, the more consistent your DCA strategy is."
|
msgid "The straighter the blue line, the more consistent your DCA strategy is."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2949,6 +2938,7 @@ msgid "Reload"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/includes/scripts/hyperscript/swal.html:13
|
#: templates/includes/scripts/hyperscript/swal.html:13
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:4
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3363,16 +3353,16 @@ msgid "Summary"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/monthly_overview/pages/overview.html:99
|
#: templates/monthly_overview/pages/overview.html:99
|
||||||
#: templates/monthly_overview/pages/overview.html:254
|
#: templates/monthly_overview/pages/overview.html:256
|
||||||
#: templates/transactions/pages/transactions.html:48
|
#: templates/transactions/pages/transactions.html:48
|
||||||
#: templates/transactions/pages/transactions.html:204
|
#: templates/transactions/pages/transactions.html:205
|
||||||
msgid "Oldest first"
|
msgid "Oldest first"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/monthly_overview/pages/overview.html:100
|
#: templates/monthly_overview/pages/overview.html:100
|
||||||
#: templates/monthly_overview/pages/overview.html:263
|
#: templates/monthly_overview/pages/overview.html:265
|
||||||
#: templates/transactions/pages/transactions.html:49
|
#: templates/transactions/pages/transactions.html:49
|
||||||
#: templates/transactions/pages/transactions.html:213
|
#: templates/transactions/pages/transactions.html:214
|
||||||
msgid "Newest first"
|
msgid "Newest first"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3381,8 +3371,8 @@ msgstr ""
|
|||||||
msgid "Filter transactions"
|
msgid "Filter transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/monthly_overview/pages/overview.html:235
|
#: templates/monthly_overview/pages/overview.html:237
|
||||||
#: templates/transactions/pages/transactions.html:185
|
#: templates/transactions/pages/transactions.html:186
|
||||||
msgid "Order by"
|
msgid "Order by"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3391,24 +3381,24 @@ msgstr ""
|
|||||||
msgid "By currency"
|
msgid "By currency"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:84
|
#: templates/net_worth/net_worth.html:84 templates/net_worth/net_worth.html:85
|
||||||
msgid "Consolidated"
|
msgid "Consolidated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:134
|
#: templates/net_worth/net_worth.html:135
|
||||||
msgid "Evolution"
|
msgid "Evolution"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:162
|
#: templates/net_worth/net_worth.html:163
|
||||||
#: templates/yearly_overview/pages/overview_by_account.html:4
|
#: templates/yearly_overview/pages/overview_by_account.html:4
|
||||||
msgid "By account"
|
msgid "By account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:270
|
#: templates/net_worth/net_worth.html:271
|
||||||
msgid "Evolution by currency"
|
msgid "Evolution by currency"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:334
|
#: templates/net_worth/net_worth.html:345
|
||||||
msgid "Evolution by account"
|
msgid "Evolution by account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3488,15 +3478,19 @@ msgstr ""
|
|||||||
msgid "View"
|
msgid "View"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/rules/fragments/list.html:70
|
#: templates/rules/fragments/list.html:71
|
||||||
msgid "Deactivate"
|
msgid "Deactivate"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/rules/fragments/list.html:70
|
#: templates/rules/fragments/list.html:71
|
||||||
msgid "Activate"
|
msgid "Activate"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/rules/fragments/list.html:89
|
#: templates/rules/fragments/list.html:77
|
||||||
|
msgid "Only the owner can change this"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/rules/fragments/list.html:96
|
||||||
msgid "No rules"
|
msgid "No rules"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3639,6 +3633,40 @@ msgstr ""
|
|||||||
msgid "transactions"
|
msgid "transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:2
|
||||||
|
msgid "Save filter preset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:3
|
||||||
|
msgid "Preset name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:40
|
||||||
|
msgid "Save preset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:6
|
||||||
|
msgid "Filter presets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:21
|
||||||
|
#, python-format
|
||||||
|
msgid "Delete %(name)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:26
|
||||||
|
msgid "Delete filter preset?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:27
|
||||||
|
#, python-format
|
||||||
|
msgid "Delete “%(name)s”?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:34
|
||||||
|
msgid "No saved presets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: templates/transactions/fragments/list_all.html:58
|
#: templates/transactions/fragments/list_all.html:58
|
||||||
msgid "No transactions found"
|
msgid "No transactions found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
+220
-192
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-18 18:43+0000\n"
|
"POT-Creation-Date: 2026-09-06 19:42+0000\n"
|
||||||
"PO-Revision-Date: 2026-01-18 19:24+0000\n"
|
"PO-Revision-Date: 2026-01-18 19:24+0000\n"
|
||||||
"Last-Translator: Ebrahim Tayabali <ebrahimhakimuddin@gmail.com>\n"
|
"Last-Translator: Ebrahim Tayabali <ebrahimhakimuddin@gmail.com>\n"
|
||||||
"Language-Team: Swahili <https://translations.herculino.com/projects/wygiwyh/"
|
"Language-Team: Swahili <https://translations.herculino.com/projects/wygiwyh/"
|
||||||
@@ -67,9 +67,9 @@ msgstr "Salio Mpya"
|
|||||||
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
|
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
|
||||||
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
|
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
|
||||||
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
|
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
|
||||||
#: apps/transactions/forms.py:1007 apps/transactions/models.py:331
|
#: apps/transactions/forms.py:1007 apps/transactions/models.py:346
|
||||||
#: apps/transactions/models.py:641 apps/transactions/models.py:841
|
#: apps/transactions/models.py:658 apps/transactions/models.py:858
|
||||||
#: apps/transactions/models.py:1089
|
#: apps/transactions/models.py:1110
|
||||||
#: templates/insights/fragments/category_overview/index.html:86
|
#: templates/insights/fragments/category_overview/index.html:86
|
||||||
#: templates/insights/fragments/category_overview/index.html:542
|
#: templates/insights/fragments/category_overview/index.html:542
|
||||||
#: templates/insights/fragments/month_by_month.html:84
|
#: templates/insights/fragments/month_by_month.html:84
|
||||||
@@ -80,13 +80,13 @@ msgstr ""
|
|||||||
#: apps/accounts/forms.py:132 apps/dca/forms.py:95 apps/dca/forms.py:103
|
#: apps/accounts/forms.py:132 apps/dca/forms.py:95 apps/dca/forms.py:103
|
||||||
#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
|
#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
|
||||||
#: apps/rules/forms.py:184 apps/rules/forms.py:194 apps/rules/models.py:45
|
#: apps/rules/forms.py:184 apps/rules/forms.py:194 apps/rules/models.py:45
|
||||||
#: apps/rules/models.py:315 apps/transactions/filters.py:73
|
#: apps/rules/models.py:315 apps/transactions/filters.py:79
|
||||||
#: apps/transactions/forms.py:69 apps/transactions/forms.py:315
|
#: apps/transactions/forms.py:69 apps/transactions/forms.py:315
|
||||||
#: apps/transactions/forms.py:483 apps/transactions/forms.py:588
|
#: apps/transactions/forms.py:483 apps/transactions/forms.py:588
|
||||||
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
|
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
|
||||||
#: apps/transactions/forms.py:1000 apps/transactions/models.py:337
|
#: apps/transactions/forms.py:1000 apps/transactions/models.py:352
|
||||||
#: apps/transactions/models.py:643 apps/transactions/models.py:845
|
#: apps/transactions/models.py:660 apps/transactions/models.py:862
|
||||||
#: apps/transactions/models.py:1095 templates/includes/sidebar.html:150
|
#: apps/transactions/models.py:1116 templates/includes/sidebar.html:150
|
||||||
#: templates/insights/fragments/category_overview/index.html:40
|
#: templates/insights/fragments/category_overview/index.html:40
|
||||||
#: templates/insights/fragments/month_by_month.html:29
|
#: templates/insights/fragments/month_by_month.html:29
|
||||||
#: templates/insights/fragments/month_by_month.html:32
|
#: templates/insights/fragments/month_by_month.html:32
|
||||||
@@ -98,8 +98,8 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
|
#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
|
||||||
#: apps/import_app/models.py:14 apps/rules/models.py:13
|
#: apps/import_app/models.py:14 apps/rules/models.py:13
|
||||||
#: apps/transactions/models.py:223 apps/transactions/models.py:248
|
#: apps/transactions/models.py:238 apps/transactions/models.py:263
|
||||||
#: apps/transactions/models.py:272 apps/transactions/models.py:1057
|
#: apps/transactions/models.py:287 apps/transactions/models.py:1078
|
||||||
#: apps/users/models.py:572 templates/account_groups/fragments/list.html:22
|
#: apps/users/models.py:572 templates/account_groups/fragments/list.html:22
|
||||||
#: templates/accounts/fragments/list.html:22
|
#: templates/accounts/fragments/list.html:22
|
||||||
#: templates/categories/fragments/table.html:17
|
#: templates/categories/fragments/table.html:17
|
||||||
@@ -170,9 +170,9 @@ msgstr ""
|
|||||||
#: apps/rules/models.py:35 apps/rules/models.py:267
|
#: apps/rules/models.py:35 apps/rules/models.py:267
|
||||||
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
|
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
|
||||||
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
|
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
|
||||||
#: apps/transactions/forms.py:992 apps/transactions/models.py:303
|
#: apps/transactions/forms.py:992 apps/transactions/models.py:318
|
||||||
#: apps/transactions/models.py:601 apps/transactions/models.py:823
|
#: apps/transactions/models.py:618 apps/transactions/models.py:840
|
||||||
#: apps/transactions/models.py:1063
|
#: apps/transactions/models.py:1084
|
||||||
#: templates/installment_plans/fragments/table.html:17
|
#: templates/installment_plans/fragments/table.html:17
|
||||||
#: templates/quick_transactions/fragments/list.html:14
|
#: templates/quick_transactions/fragments/list.html:14
|
||||||
#: templates/recurring_transactions/fragments/table.html:19
|
#: templates/recurring_transactions/fragments/table.html:19
|
||||||
@@ -181,7 +181,7 @@ msgid "Account"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/models.py:76 apps/export_app/forms.py:19
|
#: apps/accounts/models.py:76 apps/export_app/forms.py:19
|
||||||
#: apps/export_app/forms.py:129 apps/transactions/filters.py:57
|
#: apps/export_app/forms.py:129 apps/transactions/filters.py:63
|
||||||
#: templates/accounts/fragments/list.html:9
|
#: templates/accounts/fragments/list.html:9
|
||||||
#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:162
|
#: templates/accounts/pages/index.html:4 templates/includes/sidebar.html:162
|
||||||
#: templates/includes/sidebar.html:164
|
#: templates/includes/sidebar.html:164
|
||||||
@@ -194,68 +194,56 @@ msgstr ""
|
|||||||
msgid "Exchange currency cannot be the same as the account's main currency."
|
msgid "Exchange currency cannot be the same as the account's main currency."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:44
|
#: apps/accounts/views/account_groups.py:50
|
||||||
msgid "Account Group added successfully"
|
msgid "Account Group added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:69
|
#: apps/accounts/views/account_groups.py:78
|
||||||
#: apps/accounts/views/account_groups.py:152 apps/accounts/views/accounts.py:68
|
|
||||||
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:62
|
|
||||||
#: apps/dca/views.py:145 apps/rules/views.py:118 apps/rules/views.py:228
|
|
||||||
#: apps/transactions/views/categories.py:91
|
|
||||||
#: apps/transactions/views/categories.py:129
|
|
||||||
#: apps/transactions/views/entities.py:91
|
|
||||||
#: apps/transactions/views/entities.py:171 apps/transactions/views/tags.py:91
|
|
||||||
#: apps/transactions/views/tags.py:171
|
|
||||||
msgid "Only the owner can edit this"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:82
|
|
||||||
msgid "Account Group updated successfully"
|
msgid "Account Group updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:111
|
#: apps/accounts/views/account_groups.py:104
|
||||||
#: apps/accounts/views/accounts.py:145 apps/dca/views.py:104
|
|
||||||
#: apps/rules/views.py:185 apps/transactions/views/categories.py:168
|
|
||||||
#: apps/transactions/views/entities.py:130 apps/transactions/views/tags.py:130
|
|
||||||
msgid "Item no longer shared with you"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:114
|
|
||||||
msgid "Account Group deleted successfully"
|
msgid "Account Group deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:135
|
#: apps/accounts/views/account_groups.py:109
|
||||||
#: apps/accounts/views/accounts.py:189 apps/dca/views.py:128
|
#: apps/accounts/views/accounts.py:137 apps/dca/views.py:106
|
||||||
#: apps/rules/views.py:210 apps/transactions/views/categories.py:192
|
#: apps/rules/views.py:191 apps/transactions/views/categories.py:163
|
||||||
#: apps/transactions/views/entities.py:154 apps/transactions/views/tags.py:154
|
#: apps/transactions/views/entities.py:135 apps/transactions/views/tags.py:131
|
||||||
|
msgid "Item no longer shared with you"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/accounts/views/account_groups.py:132
|
||||||
|
#: apps/accounts/views/accounts.py:182 apps/dca/views.py:131
|
||||||
|
#: 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"
|
msgid "Ownership taken successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/account_groups.py:165
|
#: apps/accounts/views/account_groups.py:152
|
||||||
#: apps/accounts/views/accounts.py:119 apps/dca/views.py:158
|
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:151
|
||||||
#: apps/rules/views.py:241 apps/transactions/views/categories.py:142
|
#: apps/rules/views.py:238 apps/transactions/views/categories.py:130
|
||||||
#: apps/transactions/views/entities.py:184 apps/transactions/views/tags.py:184
|
#: apps/transactions/views/entities.py:180 apps/transactions/views/tags.py:174
|
||||||
msgid "Configuration saved successfully"
|
msgid "Configuration saved successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:44
|
#: apps/accounts/views/accounts.py:50
|
||||||
msgid "Account added successfully"
|
msgid "Account added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:81
|
#: apps/accounts/views/accounts.py:78
|
||||||
msgid "Account updated successfully"
|
msgid "Account updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:148
|
#: apps/accounts/views/accounts.py:132
|
||||||
msgid "Account deleted successfully"
|
msgid "Account deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:165
|
#: apps/accounts/views/accounts.py:158
|
||||||
msgid "Account is now tracked"
|
msgid "Account is now tracked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/accounts/views/accounts.py:168
|
#: apps/accounts/views/accounts.py:161
|
||||||
msgid "Account is now untracked"
|
msgid "Account is now untracked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -350,6 +338,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/common/forms.py:76 apps/users/forms.py:172
|
#: apps/common/forms.py:76 apps/users/forms.py:172
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:5
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -464,10 +453,9 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/common/widgets/tom_select.py:15
|
#: apps/common/widgets/tom_select.py:15
|
||||||
#: templates/mini_tools/unit_price_calculator.html:180
|
#: templates/mini_tools/unit_price_calculator.html:180
|
||||||
#: templates/monthly_overview/pages/overview.html:275
|
#: templates/monthly_overview/pages/overview.html:284
|
||||||
#: templates/monthly_overview/pages/overview.html:287
|
#: templates/transactions/fragments/filter_actions.html:48
|
||||||
#: templates/transactions/pages/transactions.html:225
|
#: templates/transactions/pages/transactions.html:233
|
||||||
#: templates/transactions/pages/transactions.html:237
|
|
||||||
msgid "Clear"
|
msgid "Clear"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -486,7 +474,7 @@ msgstr ""
|
|||||||
#: apps/currencies/forms.py:66 apps/dca/models.py:158 apps/rules/forms.py:176
|
#: apps/currencies/forms.py:66 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/rules/forms.py:190 apps/rules/models.py:38 apps/rules/models.py:279
|
||||||
#: apps/transactions/forms.py:85 apps/transactions/forms.py:447
|
#: apps/transactions/forms.py:85 apps/transactions/forms.py:447
|
||||||
#: apps/transactions/forms.py:600 apps/transactions/models.py:313
|
#: apps/transactions/forms.py:600 apps/transactions/models.py:328
|
||||||
#: templates/dca/fragments/strategy/details.html:49
|
#: templates/dca/fragments/strategy/details.html:49
|
||||||
#: templates/exchange_rates/fragments/table.html:10
|
#: templates/exchange_rates/fragments/table.html:10
|
||||||
#: templates/exchange_rates_services/fragments/table.html:11
|
#: templates/exchange_rates_services/fragments/table.html:11
|
||||||
@@ -506,7 +494,7 @@ msgid "Decimal Places"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
|
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
|
||||||
#: apps/export_app/forms.py:130 apps/transactions/filters.py:64
|
#: apps/export_app/forms.py:130 apps/transactions/filters.py:70
|
||||||
#: templates/currencies/fragments/list.html:9
|
#: templates/currencies/fragments/list.html:9
|
||||||
#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:176
|
#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:176
|
||||||
#: templates/includes/sidebar.html:178
|
#: templates/includes/sidebar.html:178
|
||||||
@@ -572,8 +560,8 @@ msgid "Service Type"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/currencies/models.py:119 apps/transactions/filters.py:27
|
#: apps/currencies/models.py:119 apps/transactions/filters.py:27
|
||||||
#: apps/transactions/models.py:227 apps/transactions/models.py:251
|
#: apps/transactions/models.py:242 apps/transactions/models.py:266
|
||||||
#: apps/transactions/models.py:275 templates/categories/fragments/list.html:16
|
#: apps/transactions/models.py:290 templates/categories/fragments/list.html:16
|
||||||
#: templates/entities/fragments/list.html:16
|
#: templates/entities/fragments/list.html:16
|
||||||
#: templates/installment_plans/fragments/list.html:16
|
#: templates/installment_plans/fragments/list.html:16
|
||||||
#: templates/recurring_transactions/fragments/list.html:16
|
#: templates/recurring_transactions/fragments/list.html:16
|
||||||
@@ -752,8 +740,8 @@ msgstr ""
|
|||||||
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
|
#: 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/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
|
||||||
#: apps/transactions/forms.py:469 apps/transactions/forms.py:616
|
#: apps/transactions/forms.py:469 apps/transactions/forms.py:616
|
||||||
#: apps/transactions/models.py:327 apps/transactions/models.py:650
|
#: apps/transactions/models.py:342 apps/transactions/models.py:667
|
||||||
#: apps/transactions/models.py:851 apps/transactions/models.py:1085
|
#: apps/transactions/models.py:868 apps/transactions/models.py:1106
|
||||||
msgid "Notes"
|
msgid "Notes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -785,27 +773,27 @@ msgstr ""
|
|||||||
msgid "DCA Entries"
|
msgid "DCA Entries"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:38
|
#: apps/dca/views.py:44
|
||||||
msgid "DCA Strategy added successfully"
|
msgid "DCA Strategy added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:75
|
#: apps/dca/views.py:73
|
||||||
msgid "DCA Strategy updated successfully"
|
msgid "DCA Strategy updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:107
|
#: apps/dca/views.py:101
|
||||||
msgid "DCA strategy deleted successfully"
|
msgid "DCA strategy deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:237
|
#: apps/dca/views.py:236
|
||||||
msgid "Entry added successfully"
|
msgid "Entry added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:264
|
#: apps/dca/views.py:270
|
||||||
msgid "Entry updated successfully"
|
msgid "Entry updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/dca/views.py:290
|
#: apps/dca/views.py:303
|
||||||
msgid "Entry deleted successfully"
|
msgid "Entry deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -816,7 +804,7 @@ msgid "Users"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
|
#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
|
||||||
#: apps/transactions/models.py:388 templates/includes/sidebar.html:81
|
#: apps/transactions/models.py:403 templates/includes/sidebar.html:81
|
||||||
#: templates/includes/sidebar.html:142
|
#: templates/includes/sidebar.html:142
|
||||||
#: templates/recurring_transactions/fragments/list_transactions.html:5
|
#: templates/recurring_transactions/fragments/list_transactions.html:5
|
||||||
#: templates/recurring_transactions/fragments/table.html:37
|
#: templates/recurring_transactions/fragments/table.html:37
|
||||||
@@ -825,7 +813,7 @@ msgid "Transactions"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:37 apps/export_app/forms.py:131
|
#: apps/export_app/forms.py:37 apps/export_app/forms.py:131
|
||||||
#: apps/transactions/filters.py:68 templates/categories/fragments/list.html:9
|
#: apps/transactions/filters.py:74 templates/categories/fragments/list.html:9
|
||||||
#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:144
|
#: templates/categories/pages/index.html:4 templates/includes/sidebar.html:144
|
||||||
#: templates/insights/fragments/month_by_month.html:18
|
#: templates/insights/fragments/month_by_month.html:18
|
||||||
#: templates/insights/fragments/month_by_month.html:21
|
#: templates/insights/fragments/month_by_month.html:21
|
||||||
@@ -836,12 +824,12 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
|
#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
|
||||||
#: apps/rules/forms.py:185 apps/rules/forms.py:195 apps/rules/models.py:46
|
#: apps/rules/forms.py:185 apps/rules/forms.py:195 apps/rules/models.py:46
|
||||||
#: apps/rules/models.py:307 apps/transactions/filters.py:78
|
#: apps/rules/models.py:307 apps/transactions/filters.py:84
|
||||||
#: apps/transactions/forms.py:77 apps/transactions/forms.py:323
|
#: apps/transactions/forms.py:77 apps/transactions/forms.py:323
|
||||||
#: apps/transactions/forms.py:491 apps/transactions/forms.py:771
|
#: apps/transactions/forms.py:491 apps/transactions/forms.py:771
|
||||||
#: apps/transactions/forms.py:1015 apps/transactions/models.py:286
|
#: apps/transactions/forms.py:1015 apps/transactions/models.py:301
|
||||||
#: apps/transactions/models.py:342 apps/transactions/models.py:646
|
#: apps/transactions/models.py:357 apps/transactions/models.py:663
|
||||||
#: apps/transactions/models.py:848 apps/transactions/models.py:1100
|
#: apps/transactions/models.py:865 apps/transactions/models.py:1121
|
||||||
#: templates/entities/fragments/list.html:9
|
#: templates/entities/fragments/list.html:9
|
||||||
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
|
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
|
||||||
#: templates/insights/fragments/category_overview/index.html:54
|
#: templates/insights/fragments/category_overview/index.html:54
|
||||||
@@ -853,14 +841,14 @@ msgid "Entities"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
|
#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
|
||||||
#: apps/transactions/models.py:888 templates/includes/sidebar.html:110
|
#: apps/transactions/models.py:905 templates/includes/sidebar.html:110
|
||||||
#: templates/recurring_transactions/fragments/list.html:9
|
#: templates/recurring_transactions/fragments/list.html:9
|
||||||
#: templates/recurring_transactions/pages/index.html:4
|
#: templates/recurring_transactions/pages/index.html:4
|
||||||
msgid "Recurring Transactions"
|
msgid "Recurring Transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
|
#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
|
||||||
#: apps/transactions/models.py:664 templates/includes/sidebar.html:104
|
#: apps/transactions/models.py:681 templates/includes/sidebar.html:104
|
||||||
#: templates/installment_plans/fragments/list.html:9
|
#: templates/installment_plans/fragments/list.html:9
|
||||||
#: templates/installment_plans/pages/index.html:4
|
#: templates/installment_plans/pages/index.html:4
|
||||||
msgid "Installment Plans"
|
msgid "Installment Plans"
|
||||||
@@ -1012,7 +1000,7 @@ msgid "Run deleted successfully"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/insights/forms.py:118 apps/insights/utils/sankey.py:36
|
#: apps/insights/forms.py:118 apps/insights/utils/sankey.py:36
|
||||||
#: apps/insights/utils/sankey.py:167 apps/transactions/filters.py:203
|
#: apps/insights/utils/sankey.py:167 apps/transactions/filters.py:209
|
||||||
#: templates/insights/fragments/category_overview/index.html:96
|
#: templates/insights/fragments/category_overview/index.html:96
|
||||||
#: templates/insights/fragments/category_overview/index.html:407
|
#: templates/insights/fragments/category_overview/index.html:407
|
||||||
#: templates/insights/fragments/category_overview/index.html:436
|
#: templates/insights/fragments/category_overview/index.html:436
|
||||||
@@ -1110,15 +1098,15 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
|
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
|
||||||
#: apps/rules/models.py:271 apps/transactions/forms.py:433
|
#: apps/rules/models.py:271 apps/transactions/forms.py:433
|
||||||
#: apps/transactions/models.py:310 apps/transactions/models.py:606
|
#: apps/transactions/models.py:325 apps/transactions/models.py:623
|
||||||
#: apps/transactions/models.py:829 apps/transactions/models.py:1070
|
#: apps/transactions/models.py:846 apps/transactions/models.py:1091
|
||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
|
#: 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/rules/models.py:275 apps/transactions/filters.py:22
|
||||||
#: apps/transactions/forms.py:437 apps/transactions/models.py:312
|
#: apps/transactions/forms.py:437 apps/transactions/models.py:327
|
||||||
#: apps/transactions/models.py:1072 templates/cotton/transaction/item.html:20
|
#: apps/transactions/models.py:1093 templates/cotton/transaction/item.html:20
|
||||||
#: templates/cotton/transaction/item.html:31
|
#: templates/cotton/transaction/item.html:31
|
||||||
#: templates/transactions/widgets/paid_toggle_button.html:10
|
#: templates/transactions/widgets/paid_toggle_button.html:10
|
||||||
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
|
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
|
||||||
@@ -1128,15 +1116,15 @@ msgstr ""
|
|||||||
#: apps/rules/forms.py:177 apps/rules/forms.py:191 apps/rules/models.py:39
|
#: apps/rules/forms.py:177 apps/rules/forms.py:191 apps/rules/models.py:39
|
||||||
#: apps/rules/models.py:283 apps/transactions/forms.py:89
|
#: apps/rules/models.py:283 apps/transactions/forms.py:89
|
||||||
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
|
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
|
||||||
#: apps/transactions/forms.py:777 apps/transactions/models.py:314
|
#: apps/transactions/forms.py:777 apps/transactions/models.py:329
|
||||||
#: apps/transactions/models.py:624 apps/transactions/models.py:853
|
#: apps/transactions/models.py:641 apps/transactions/models.py:870
|
||||||
msgid "Reference Date"
|
msgid "Reference Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
|
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
|
||||||
#: apps/rules/models.py:287 apps/transactions/forms.py:460
|
#: apps/rules/models.py:287 apps/transactions/forms.py:460
|
||||||
#: apps/transactions/models.py:320 apps/transactions/models.py:834
|
#: apps/transactions/models.py:335 apps/transactions/models.py:851
|
||||||
#: apps/transactions/models.py:1078
|
#: apps/transactions/models.py:1099
|
||||||
#: templates/insights/fragments/sankey.html:102
|
#: templates/insights/fragments/sankey.html:102
|
||||||
#: templates/installment_plans/fragments/table.html:18
|
#: templates/installment_plans/fragments/table.html:18
|
||||||
#: templates/quick_transactions/fragments/list.html:15
|
#: templates/quick_transactions/fragments/list.html:15
|
||||||
@@ -1147,27 +1135,27 @@ msgstr ""
|
|||||||
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
|
#: 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/rules/models.py:42 apps/rules/models.py:291
|
||||||
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
|
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
|
||||||
#: apps/transactions/models.py:325 apps/transactions/models.py:608
|
#: apps/transactions/models.py:340 apps/transactions/models.py:625
|
||||||
#: apps/transactions/models.py:837 apps/transactions/models.py:1083
|
#: apps/transactions/models.py:854 apps/transactions/models.py:1104
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
|
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
|
||||||
#: apps/rules/models.py:299 apps/transactions/models.py:364
|
#: apps/rules/models.py:299 apps/transactions/models.py:379
|
||||||
#: apps/transactions/models.py:1105
|
#: apps/transactions/models.py:1126
|
||||||
msgid "Internal Note"
|
msgid "Internal Note"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
|
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
|
||||||
#: apps/rules/models.py:303 apps/transactions/models.py:366
|
#: apps/rules/models.py:303 apps/transactions/models.py:381
|
||||||
#: apps/transactions/models.py:1107
|
#: apps/transactions/models.py:1128
|
||||||
msgid "Internal ID"
|
msgid "Internal ID"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
|
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
|
||||||
#: apps/rules/models.py:319 apps/transactions/forms.py:620
|
#: apps/rules/models.py:319 apps/transactions/forms.py:620
|
||||||
#: apps/transactions/models.py:224 apps/transactions/models.py:315
|
#: apps/transactions/models.py:239 apps/transactions/models.py:330
|
||||||
#: apps/transactions/models.py:1073
|
#: apps/transactions/models.py:1094
|
||||||
msgid "Mute"
|
msgid "Mute"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1180,7 +1168,7 @@ msgid "Set Values"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
|
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
|
||||||
#: apps/transactions/models.py:387 apps/transactions/models.py:544
|
#: apps/transactions/models.py:402 apps/transactions/models.py:560
|
||||||
msgid "Transaction"
|
msgid "Transaction"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1274,15 +1262,15 @@ msgstr ""
|
|||||||
msgid "Update or create transaction action"
|
msgid "Update or create transaction action"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:71
|
#: apps/rules/views.py:79
|
||||||
msgid "Rule deactivated successfully"
|
msgid "Rule deactivated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:73
|
#: apps/rules/views.py:81
|
||||||
msgid "Rule activated successfully"
|
msgid "Rule activated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:92
|
#: apps/rules/views.py:100
|
||||||
msgid "Rule added successfully"
|
msgid "Rule added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1290,27 +1278,27 @@ msgstr ""
|
|||||||
msgid "Rule updated successfully"
|
msgid "Rule updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:188
|
#: apps/rules/views.py:186
|
||||||
msgid "Rule deleted successfully"
|
msgid "Rule deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:305
|
#: apps/rules/views.py:306
|
||||||
msgid "Action updated successfully"
|
msgid "Action updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:336
|
#: apps/rules/views.py:341
|
||||||
msgid "Action deleted successfully"
|
msgid "Action deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:360
|
#: apps/rules/views.py:367
|
||||||
msgid "Update or Create Transaction action added successfully"
|
msgid "Update or Create Transaction action added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:393
|
#: apps/rules/views.py:402
|
||||||
msgid "Update or Create Transaction action updated successfully"
|
msgid "Update or Create Transaction action updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/rules/views.py:423
|
#: apps/rules/views.py:434
|
||||||
msgid "Update or Create Transaction action deleted successfully"
|
msgid "Update or Create Transaction action deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1327,58 +1315,58 @@ msgstr ""
|
|||||||
msgid "Muted"
|
msgid "Muted"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:45
|
#: apps/transactions/filters.py:51
|
||||||
msgid "Content"
|
msgid "Content"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:51
|
#: apps/transactions/filters.py:57
|
||||||
msgid "Transaction Type"
|
msgid "Transaction Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:89
|
#: apps/transactions/filters.py:95
|
||||||
msgid "Mute Status"
|
msgid "Mute Status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:94
|
#: apps/transactions/filters.py:100
|
||||||
msgid "Date from"
|
msgid "Date from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:99 apps/transactions/filters.py:109
|
#: apps/transactions/filters.py:105 apps/transactions/filters.py:115
|
||||||
msgid "Until"
|
msgid "Until"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:104
|
#: apps/transactions/filters.py:110
|
||||||
msgid "Reference date from"
|
msgid "Reference date from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:114
|
#: apps/transactions/filters.py:120
|
||||||
msgid "Amount min"
|
msgid "Amount min"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:119
|
#: apps/transactions/filters.py:125
|
||||||
msgid "Amount max"
|
msgid "Amount max"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:202
|
#: apps/transactions/filters.py:208
|
||||||
msgid "Categorized"
|
msgid "Categorized"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:209
|
#: apps/transactions/filters.py:215
|
||||||
msgid "Tagged"
|
msgid "Tagged"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:209
|
#: apps/transactions/filters.py:215
|
||||||
#: templates/insights/fragments/category_overview/index.html:189
|
#: templates/insights/fragments/category_overview/index.html:189
|
||||||
#: templates/insights/fragments/month_by_month.html:121
|
#: templates/insights/fragments/month_by_month.html:121
|
||||||
#: templates/insights/fragments/year_by_year.html:75
|
#: templates/insights/fragments/year_by_year.html:75
|
||||||
msgid "Untagged"
|
msgid "Untagged"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:215
|
#: apps/transactions/filters.py:221
|
||||||
msgid "Any entity"
|
msgid "Any entity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/filters.py:216
|
#: apps/transactions/filters.py:222
|
||||||
#: templates/insights/fragments/category_overview/index.html:282
|
#: templates/insights/fragments/category_overview/index.html:282
|
||||||
#: templates/insights/fragments/month_by_month.html:123
|
#: templates/insights/fragments/month_by_month.html:123
|
||||||
#: templates/insights/fragments/year_by_year.html:77
|
#: templates/insights/fragments/year_by_year.html:77
|
||||||
@@ -1453,42 +1441,42 @@ msgstr ""
|
|||||||
msgid "End date should be after the start date"
|
msgid "End date should be after the start date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:229
|
#: apps/transactions/models.py:244
|
||||||
msgid ""
|
msgid ""
|
||||||
"Deactivated categories won't be able to be selected when creating new "
|
"Deactivated categories won't be able to be selected when creating new "
|
||||||
"transactions"
|
"transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:237
|
#: apps/transactions/models.py:252
|
||||||
msgid "Transaction Category"
|
msgid "Transaction Category"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:238
|
#: apps/transactions/models.py:253
|
||||||
msgid "Transaction Categories"
|
msgid "Transaction Categories"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:253
|
#: apps/transactions/models.py:268
|
||||||
msgid ""
|
msgid ""
|
||||||
"Deactivated tags won't be able to be selected when creating new transactions"
|
"Deactivated tags won't be able to be selected when creating new transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:261 apps/transactions/models.py:262
|
#: apps/transactions/models.py:276 apps/transactions/models.py:277
|
||||||
msgid "Transaction Tags"
|
msgid "Transaction Tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:277
|
#: apps/transactions/models.py:292
|
||||||
msgid ""
|
msgid ""
|
||||||
"Deactivated entities won't be able to be selected when creating new "
|
"Deactivated entities won't be able to be selected when creating new "
|
||||||
"transactions"
|
"transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:285
|
#: apps/transactions/models.py:300
|
||||||
#: templates/insights/fragments/month_by_month.html:88
|
#: templates/insights/fragments/month_by_month.html:88
|
||||||
#: templates/insights/fragments/year_by_year.html:56
|
#: templates/insights/fragments/year_by_year.html:56
|
||||||
msgid "Entity"
|
msgid "Entity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:297 apps/transactions/models.py:1050
|
#: apps/transactions/models.py:312 apps/transactions/models.py:1071
|
||||||
#: templates/calendar_view/fragments/list.html:42
|
#: templates/calendar_view/fragments/list.html:42
|
||||||
#: templates/calendar_view/fragments/list.html:44
|
#: templates/calendar_view/fragments/list.html:44
|
||||||
#: templates/calendar_view/fragments/list.html:52
|
#: templates/calendar_view/fragments/list.html:52
|
||||||
@@ -1500,7 +1488,7 @@ msgstr ""
|
|||||||
msgid "Income"
|
msgid "Income"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:298 apps/transactions/models.py:1051
|
#: apps/transactions/models.py:313 apps/transactions/models.py:1072
|
||||||
#: templates/calendar_view/fragments/list.html:46
|
#: templates/calendar_view/fragments/list.html:46
|
||||||
#: templates/calendar_view/fragments/list.html:48
|
#: templates/calendar_view/fragments/list.html:48
|
||||||
#: templates/calendar_view/fragments/list.html:56
|
#: templates/calendar_view/fragments/list.html:56
|
||||||
@@ -1511,157 +1499,157 @@ msgstr ""
|
|||||||
msgid "Expense"
|
msgid "Expense"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:353 apps/transactions/models.py:663
|
#: apps/transactions/models.py:368 apps/transactions/models.py:680
|
||||||
msgid "Installment Plan"
|
msgid "Installment Plan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:362 apps/transactions/models.py:887
|
#: apps/transactions/models.py:377 apps/transactions/models.py:904
|
||||||
msgid "Recurring Transaction"
|
msgid "Recurring Transaction"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:370
|
#: apps/transactions/models.py:385
|
||||||
msgid "Deleted"
|
msgid "Deleted"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:375
|
#: apps/transactions/models.py:390
|
||||||
msgid "Deleted At"
|
msgid "Deleted At"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:489 templates/tags/fragments/table.html:69
|
#: apps/transactions/models.py:504 templates/tags/fragments/table.html:69
|
||||||
msgid "No tags"
|
msgid "No tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:491
|
#: apps/transactions/models.py:506
|
||||||
msgid "No category"
|
msgid "No category"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:493
|
#: apps/transactions/models.py:508
|
||||||
msgid "No description"
|
msgid "No description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:549
|
#: apps/transactions/models.py:565
|
||||||
msgid "File"
|
msgid "File"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:551
|
#: apps/transactions/models.py:567
|
||||||
msgid "Original Name"
|
msgid "Original Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:553
|
#: apps/transactions/models.py:569
|
||||||
msgid "Content Type"
|
msgid "Content Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:555
|
#: apps/transactions/models.py:571
|
||||||
msgid "Size"
|
msgid "Size"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:560
|
#: apps/transactions/models.py:576
|
||||||
msgid "Uploaded By"
|
msgid "Uploaded By"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:565
|
#: apps/transactions/models.py:581
|
||||||
msgid "Transaction Attachment"
|
msgid "Transaction Attachment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:566
|
#: apps/transactions/models.py:582
|
||||||
msgid "Transaction Attachments"
|
msgid "Transaction Attachments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:595 templates/includes/sidebar.html:57
|
#: apps/transactions/models.py:612 templates/includes/sidebar.html:57
|
||||||
msgid "Yearly"
|
msgid "Yearly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:596 apps/users/models.py:469
|
#: apps/transactions/models.py:613 apps/users/models.py:469
|
||||||
#: templates/includes/sidebar.html:51
|
#: templates/includes/sidebar.html:51
|
||||||
msgid "Monthly"
|
msgid "Monthly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:597
|
#: apps/transactions/models.py:614
|
||||||
msgid "Weekly"
|
msgid "Weekly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:598
|
#: apps/transactions/models.py:615
|
||||||
msgid "Daily"
|
msgid "Daily"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:611
|
#: apps/transactions/models.py:628
|
||||||
msgid "Number of Installments"
|
msgid "Number of Installments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:616
|
#: apps/transactions/models.py:633
|
||||||
msgid "Installment Start"
|
msgid "Installment Start"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:617
|
#: apps/transactions/models.py:634
|
||||||
msgid "The installment number to start counting from"
|
msgid "The installment number to start counting from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:622 apps/transactions/models.py:857
|
#: apps/transactions/models.py:639 apps/transactions/models.py:874
|
||||||
msgid "Start Date"
|
msgid "Start Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:626 apps/transactions/models.py:858
|
#: apps/transactions/models.py:643 apps/transactions/models.py:875
|
||||||
msgid "End Date"
|
msgid "End Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:631
|
#: apps/transactions/models.py:648
|
||||||
msgid "Recurrence"
|
msgid "Recurrence"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:634
|
#: apps/transactions/models.py:651
|
||||||
msgid "Installment Amount"
|
msgid "Installment Amount"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:653 apps/transactions/models.py:877
|
#: apps/transactions/models.py:670 apps/transactions/models.py:894
|
||||||
msgid "Add description to transactions"
|
msgid "Add description to transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:656 apps/transactions/models.py:880
|
#: apps/transactions/models.py:673 apps/transactions/models.py:897
|
||||||
msgid "Add notes to transactions"
|
msgid "Add notes to transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:816
|
#: apps/transactions/models.py:833
|
||||||
msgid "day(s)"
|
msgid "day(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:817
|
#: apps/transactions/models.py:834
|
||||||
msgid "week(s)"
|
msgid "week(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:818
|
#: apps/transactions/models.py:835
|
||||||
msgid "month(s)"
|
msgid "month(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:819
|
#: apps/transactions/models.py:836
|
||||||
msgid "year(s)"
|
msgid "year(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:821
|
#: apps/transactions/models.py:838
|
||||||
#: templates/recurring_transactions/fragments/list.html:18
|
#: templates/recurring_transactions/fragments/list.html:18
|
||||||
msgid "Paused"
|
msgid "Paused"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:860
|
#: apps/transactions/models.py:877
|
||||||
msgid "Recurrence Type"
|
msgid "Recurrence Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:863
|
#: apps/transactions/models.py:880
|
||||||
msgid "Recurrence Interval"
|
msgid "Recurrence Interval"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:866
|
#: apps/transactions/models.py:883
|
||||||
msgid "Keep at most"
|
msgid "Keep at most"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:870
|
#: apps/transactions/models.py:887
|
||||||
msgid "Last Generated Date"
|
msgid "Last Generated Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:873
|
#: apps/transactions/models.py:890
|
||||||
msgid "Last Generated Reference Date"
|
msgid "Last Generated Reference Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:1117
|
#: apps/transactions/models.py:1138
|
||||||
#: apps/transactions/views/quick_transactions.py:178
|
#: apps/transactions/views/quick_transactions.py:178
|
||||||
#: apps/transactions/views/quick_transactions.py:187
|
#: apps/transactions/views/quick_transactions.py:187
|
||||||
#: apps/transactions/views/quick_transactions.py:189
|
#: apps/transactions/views/quick_transactions.py:189
|
||||||
@@ -1670,7 +1658,7 @@ msgstr ""
|
|||||||
msgid "Quick Transaction"
|
msgid "Quick Transaction"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/models.py:1118 templates/includes/sidebar.html:98
|
#: apps/transactions/models.py:1139 templates/includes/sidebar.html:98
|
||||||
#: templates/quick_transactions/pages/index.html:5
|
#: templates/quick_transactions/pages/index.html:5
|
||||||
#: templates/quick_transactions/pages/index.html:15
|
#: templates/quick_transactions/pages/index.html:15
|
||||||
msgid "Quick Transactions"
|
msgid "Quick Transactions"
|
||||||
@@ -1721,27 +1709,27 @@ msgid_plural "%(count)s transactions duplicated successfully"
|
|||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: apps/transactions/views/categories.py:66
|
#: apps/transactions/views/categories.py:72
|
||||||
msgid "Category added successfully"
|
msgid "Category added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/categories.py:104
|
#: apps/transactions/views/categories.py:102
|
||||||
msgid "Category updated successfully"
|
msgid "Category updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/categories.py:171
|
#: apps/transactions/views/categories.py:158
|
||||||
msgid "Category deleted successfully"
|
msgid "Category deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/entities.py:66
|
#: apps/transactions/views/entities.py:72
|
||||||
msgid "Entity added successfully"
|
msgid "Entity added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/entities.py:104
|
#: apps/transactions/views/entities.py:102
|
||||||
msgid "Entity updated successfully"
|
msgid "Entity updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/entities.py:133
|
#: apps/transactions/views/entities.py:130
|
||||||
msgid "Entity deleted successfully"
|
msgid "Entity deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1804,15 +1792,15 @@ msgstr ""
|
|||||||
msgid "Recurring Transaction deleted successfully"
|
msgid "Recurring Transaction deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/tags.py:66
|
#: apps/transactions/views/tags.py:72
|
||||||
msgid "Tag added successfully"
|
msgid "Tag added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/tags.py:104
|
#: apps/transactions/views/tags.py:100
|
||||||
msgid "Tag updated successfully"
|
msgid "Tag updated successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/transactions/views/tags.py:133
|
#: apps/transactions/views/tags.py:126
|
||||||
msgid "Tag deleted successfully"
|
msgid "Tag deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1894,9 +1882,9 @@ msgstr ""
|
|||||||
|
|
||||||
#: apps/users/forms.py:68 apps/users/forms.py:81 apps/users/forms.py:103
|
#: apps/users/forms.py:68 apps/users/forms.py:81 apps/users/forms.py:103
|
||||||
#: templates/monthly_overview/pages/overview.html:98
|
#: templates/monthly_overview/pages/overview.html:98
|
||||||
#: templates/monthly_overview/pages/overview.html:245
|
#: templates/monthly_overview/pages/overview.html:247
|
||||||
#: templates/transactions/pages/transactions.html:47
|
#: templates/transactions/pages/transactions.html:47
|
||||||
#: templates/transactions/pages/transactions.html:195
|
#: templates/transactions/pages/transactions.html:196
|
||||||
msgid "Default"
|
msgid "Default"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2223,6 +2211,7 @@ msgstr ""
|
|||||||
#: templates/rules/fragments/transaction_rule/view.html:92
|
#: templates/rules/fragments/transaction_rule/view.html:92
|
||||||
#: templates/rules/fragments/transaction_rule/view.html:133
|
#: templates/rules/fragments/transaction_rule/view.html:133
|
||||||
#: templates/tags/fragments/table.html:51
|
#: templates/tags/fragments/table.html:51
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:28
|
||||||
#: templates/users/fragments/api_tokens.html:93
|
#: templates/users/fragments/api_tokens.html:93
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2327,8 +2316,8 @@ msgid "Current balance"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/accounts/fragments/account_reconciliation.html:36
|
#: templates/accounts/fragments/account_reconciliation.html:36
|
||||||
#: templates/net_worth/net_worth.html:146
|
#: templates/net_worth/net_worth.html:147
|
||||||
#: templates/net_worth/net_worth.html:396
|
#: templates/net_worth/net_worth.html:407
|
||||||
msgid "Difference"
|
msgid "Difference"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2708,19 +2697,19 @@ msgstr ""
|
|||||||
msgid "Amount Bought"
|
msgid "Amount Bought"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:384
|
#: templates/dca/fragments/strategy/details.html:409
|
||||||
msgid "Entry Price vs Current Price"
|
msgid "Entry Price vs Current Price"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:398
|
#: templates/dca/fragments/strategy/details.html:423
|
||||||
msgid "Days Between Investments"
|
msgid "Days Between Investments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:444
|
#: templates/dca/fragments/strategy/details.html:469
|
||||||
msgid "Investment Frequency"
|
msgid "Investment Frequency"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/dca/fragments/strategy/details.html:446
|
#: templates/dca/fragments/strategy/details.html:471
|
||||||
msgid "The straighter the blue line, the more consistent your DCA strategy is."
|
msgid "The straighter the blue line, the more consistent your DCA strategy is."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2957,6 +2946,7 @@ msgid "Reload"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/includes/scripts/hyperscript/swal.html:13
|
#: templates/includes/scripts/hyperscript/swal.html:13
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:4
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3371,16 +3361,16 @@ msgid "Summary"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/monthly_overview/pages/overview.html:99
|
#: templates/monthly_overview/pages/overview.html:99
|
||||||
#: templates/monthly_overview/pages/overview.html:254
|
#: templates/monthly_overview/pages/overview.html:256
|
||||||
#: templates/transactions/pages/transactions.html:48
|
#: templates/transactions/pages/transactions.html:48
|
||||||
#: templates/transactions/pages/transactions.html:204
|
#: templates/transactions/pages/transactions.html:205
|
||||||
msgid "Oldest first"
|
msgid "Oldest first"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/monthly_overview/pages/overview.html:100
|
#: templates/monthly_overview/pages/overview.html:100
|
||||||
#: templates/monthly_overview/pages/overview.html:263
|
#: templates/monthly_overview/pages/overview.html:265
|
||||||
#: templates/transactions/pages/transactions.html:49
|
#: templates/transactions/pages/transactions.html:49
|
||||||
#: templates/transactions/pages/transactions.html:213
|
#: templates/transactions/pages/transactions.html:214
|
||||||
msgid "Newest first"
|
msgid "Newest first"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3389,8 +3379,8 @@ msgstr ""
|
|||||||
msgid "Filter transactions"
|
msgid "Filter transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/monthly_overview/pages/overview.html:235
|
#: templates/monthly_overview/pages/overview.html:237
|
||||||
#: templates/transactions/pages/transactions.html:185
|
#: templates/transactions/pages/transactions.html:186
|
||||||
msgid "Order by"
|
msgid "Order by"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3399,24 +3389,24 @@ msgstr ""
|
|||||||
msgid "By currency"
|
msgid "By currency"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:84
|
#: templates/net_worth/net_worth.html:84 templates/net_worth/net_worth.html:85
|
||||||
msgid "Consolidated"
|
msgid "Consolidated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:134
|
#: templates/net_worth/net_worth.html:135
|
||||||
msgid "Evolution"
|
msgid "Evolution"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:162
|
#: templates/net_worth/net_worth.html:163
|
||||||
#: templates/yearly_overview/pages/overview_by_account.html:4
|
#: templates/yearly_overview/pages/overview_by_account.html:4
|
||||||
msgid "By account"
|
msgid "By account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:270
|
#: templates/net_worth/net_worth.html:271
|
||||||
msgid "Evolution by currency"
|
msgid "Evolution by currency"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/net_worth/net_worth.html:334
|
#: templates/net_worth/net_worth.html:345
|
||||||
msgid "Evolution by account"
|
msgid "Evolution by account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3496,15 +3486,19 @@ msgstr ""
|
|||||||
msgid "View"
|
msgid "View"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/rules/fragments/list.html:70
|
#: templates/rules/fragments/list.html:71
|
||||||
msgid "Deactivate"
|
msgid "Deactivate"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/rules/fragments/list.html:70
|
#: templates/rules/fragments/list.html:71
|
||||||
msgid "Activate"
|
msgid "Activate"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/rules/fragments/list.html:89
|
#: templates/rules/fragments/list.html:77
|
||||||
|
msgid "Only the owner can change this"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/rules/fragments/list.html:96
|
||||||
msgid "No rules"
|
msgid "No rules"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3647,6 +3641,40 @@ msgstr ""
|
|||||||
msgid "transactions"
|
msgid "transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:2
|
||||||
|
msgid "Save filter preset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:3
|
||||||
|
msgid "Preset name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_actions.html:40
|
||||||
|
msgid "Save preset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:6
|
||||||
|
msgid "Filter presets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:21
|
||||||
|
#, python-format
|
||||||
|
msgid "Delete %(name)s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:26
|
||||||
|
msgid "Delete filter preset?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:27
|
||||||
|
#, python-format
|
||||||
|
msgid "Delete “%(name)s”?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/transactions/fragments/filter_presets.html:34
|
||||||
|
msgid "No saved presets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: templates/transactions/fragments/list_all.html:58
|
#: templates/transactions/fragments/list_all.html:58
|
||||||
msgid "No transactions found"
|
msgid "No transactions found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
+222
-192
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -331,6 +331,8 @@
|
|||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
|
min: Math.max(0, priceData.labels.length - 20),
|
||||||
|
max: Math.max(0, priceData.labels.length - 1),
|
||||||
grid: {
|
grid: {
|
||||||
display: false
|
display: false
|
||||||
},
|
},
|
||||||
@@ -369,6 +371,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
|
zoom: {
|
||||||
|
limits: {
|
||||||
|
x: {
|
||||||
|
min: 0,
|
||||||
|
max: Math.max(0, priceData.labels.length - 1),
|
||||||
|
minRange: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pan: {
|
||||||
|
enabled: true,
|
||||||
|
mode: 'x'
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
wheel: {
|
||||||
|
enabled: true,
|
||||||
|
modifierKey: 'ctrl'
|
||||||
|
},
|
||||||
|
pinch: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
mode: 'x'
|
||||||
|
}
|
||||||
|
},
|
||||||
legend: {
|
legend: {
|
||||||
position: 'top'
|
position: 'top'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
<i class="fa-solid fa-grip"></i>
|
<i class="fa-solid fa-grip"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
<input type="search"
|
<input type="search"
|
||||||
class="input w-full"
|
class="input w-full"
|
||||||
id="calculator-input"
|
id="calculator-input"
|
||||||
@@ -90,14 +91,15 @@
|
|||||||
then remove .swing-out-top-bck from #calculator-result-container
|
then remove .swing-out-top-bck from #calculator-result-container
|
||||||
end"
|
end"
|
||||||
placeholder="2 + 2">
|
placeholder="2 + 2">
|
||||||
|
<div class="btn btn-error btn-circle absolute cursor-pointer top-0 right-0 translate-x-1/2 -translate-y-1/2 p-0 flex items-center justify-center w-5 h-5"
|
||||||
|
_="on click trigger show on #calculator">
|
||||||
|
<i class="fa-solid fa-xmark flex items-center justify-center w-full h-full content-center text-xs"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="hidden" id="calculator-result-container">
|
<div class="hidden" id="calculator-result-container">
|
||||||
<div class="flex flex-row py-3 px-5 justify-between">
|
<div class="flex flex-row py-3 px-5 justify-between">
|
||||||
<div class="text-subtle">=</div>
|
<div class="text-subtle">=</div>
|
||||||
<div id="calculator-result" class="select-all"></div>
|
<div id="calculator-result" class="select-all"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn btn-error btn-circle absolute cursor-pointer top-0 left-full start-100 -translate-x-1/2 -translate-y-1/2 p-0 flex items-center justify-center w-5 h-5"
|
|
||||||
_="on click trigger show on #calculator">
|
|
||||||
<i class="fa-solid fa-xmark flex items-center justify-center w-full h-full content-center text-xs"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
@click="filterOpen = !filterOpen"
|
@click="filterOpen = !filterOpen"
|
||||||
:aria-expanded="filterOpen" id="filter-button"
|
:aria-expanded="filterOpen" id="filter-button"
|
||||||
title="{% translate 'Filter transactions' %}"
|
title="{% translate 'Filter transactions' %}"
|
||||||
_="on load or change from #filter
|
_="on change from #filter-container
|
||||||
-- Check if any filter has a non-default value
|
-- Check if any filter has a non-default value
|
||||||
set hasActiveFilter to false
|
set hasActiveFilter to false
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
add .hidden to #filter-active-indicator
|
add .hidden to #filter-active-indicator
|
||||||
end">
|
end">
|
||||||
<i class="fa-solid fa-filter fa-fw"></i>
|
<i class="fa-solid fa-filter fa-fw"></i>
|
||||||
<span id="filter-active-indicator" class="absolute -top-1 -right-1 w-3 h-3 bg-error rounded-full hidden z-10"></span>
|
<span id="filter-active-indicator" class="absolute -top-1 -right-1 w-3 h-3 bg-error rounded-full {% if not filter_is_active %}hidden {% endif %}z-10"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{# Search box #}
|
{# Search box #}
|
||||||
@@ -228,6 +228,8 @@
|
|||||||
show <.transaction/> in <#transactions-list/>
|
show <.transaction/> in <#transactions-list/>
|
||||||
when its textContent.toLowerCase() contains my value.toLowerCase()">
|
when its textContent.toLowerCase() contains my value.toLowerCase()">
|
||||||
|
|
||||||
|
{% include "transactions/fragments/filter_presets.html" %}
|
||||||
|
|
||||||
{# Order by icon dropdown #}
|
{# Order by icon dropdown #}
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-secondary join-item" type="button"
|
<button class="btn btn-secondary join-item" type="button"
|
||||||
@@ -270,21 +272,16 @@
|
|||||||
{# Filter transactions form #}
|
{# Filter transactions form #}
|
||||||
<div class="z-1" x-show="filterOpen" x-collapse x-cloak>
|
<div class="z-1" x-show="filterOpen" x-collapse x-cloak>
|
||||||
<div class="card card-body bg-base-200 mt-2">
|
<div class="card card-body bg-base-200 mt-2">
|
||||||
<div class="text-right">
|
{% include "transactions/fragments/filter_actions.html" %}
|
||||||
<button class="btn btn-outline btn-error btn-sm w-fit"
|
|
||||||
_="on click call #filter.reset() then trigger change on #filter">{% translate 'Clear' %}</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form _="on change or submit or search trigger updated on window
|
{% include "transactions/fragments/filter_form.html" %}
|
||||||
install init_tom_select
|
|
||||||
install init_datepicker"
|
|
||||||
id="filter" class="mt-3">
|
|
||||||
{% crispy filter.form %}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<button class="btn btn-outline btn-error btn-sm w-fit"
|
<a class="btn btn-outline btn-error btn-sm w-fit"
|
||||||
_="on click call #filter.reset() then trigger change on #filter">{% translate 'Clear' %}</button>
|
href="{{ request.path }}"
|
||||||
|
hx-get="{% url 'transaction_filter_clear' %}"
|
||||||
|
hx-target="#filter"
|
||||||
|
hx-swap="outerHTML">{% translate 'Clear' %}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
<li>
|
<li>
|
||||||
{% if currency.consolidated and currency.consolidated.total_final != currency.total_final %}
|
{% if currency.consolidated and currency.consolidated.total_final != currency.total_final %}
|
||||||
<a class="cursor-pointer select-auto flex justify-between items-center w-full"
|
<a class="cursor-pointer select-auto flex justify-between items-center w-full"
|
||||||
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }}')">
|
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }}', '{{ currency.currency.name }}')">
|
||||||
<span
|
<span
|
||||||
class="currency-name text-start font-mono shrink text-ellipsis">{{ currency.currency.name }}</span>
|
class="currency-name text-start font-mono shrink text-ellipsis">{{ currency.currency.name }}</span>
|
||||||
<span class="text-end shrink-0">
|
<span class="text-end shrink-0">
|
||||||
@@ -80,7 +80,8 @@
|
|||||||
</a>
|
</a>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a class="text-base-content/60 select-auto flex justify-between items-center w-full">
|
<a class="cursor-pointer text-base-content/60 select-auto flex justify-between items-center w-full"
|
||||||
|
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }} {% trans "Consolidated" %}', '{{ currency.currency.name }}')">
|
||||||
<span class="text-start shrink">{% trans "Consolidated" %}</span>
|
<span class="text-start shrink">{% trans "Consolidated" %}</span>
|
||||||
<span class="text-end shrink-0">
|
<span class="text-end shrink-0">
|
||||||
<c-amount.display :amount="currency.consolidated.total_final"
|
<c-amount.display :amount="currency.consolidated.total_final"
|
||||||
@@ -95,7 +96,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="cursor-pointer select-auto flex justify-between items-center w-full"
|
<a class="cursor-pointer select-auto flex justify-between items-center w-full"
|
||||||
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }}')">
|
_="on click showOnlyCurrencyDataset('{{ currency.currency.name }}', '{{ currency.currency.name }}')">
|
||||||
<span class="currency-name text-start font-mono shrink">{{ currency.currency.name }}</span>
|
<span class="currency-name text-start font-mono shrink">{{ currency.currency.name }}</span>
|
||||||
<span class="text-end shrink-0">
|
<span class="text-end shrink-0">
|
||||||
<div>
|
<div>
|
||||||
@@ -302,6 +303,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const dataset of currencyChart.data.datasets) {
|
||||||
|
if (!dataset.colorSource) continue;
|
||||||
|
const source = currencyChart.data.datasets.find(
|
||||||
|
candidate => candidate.label === dataset.colorSource
|
||||||
|
);
|
||||||
|
dataset.borderColor = source.borderColor;
|
||||||
|
dataset.backgroundColor = source.backgroundColor;
|
||||||
|
}
|
||||||
|
currencyChart.update('none');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script id="accountBalanceChartScript">
|
<script id="accountBalanceChartScript">
|
||||||
@@ -448,7 +459,7 @@
|
|||||||
call accountChart.update()
|
call accountChart.update()
|
||||||
end
|
end
|
||||||
|
|
||||||
def showOnlyCurrencyDataset(datasetName)
|
def showOnlyCurrencyDataset(datasetName, differenceDatasetName)
|
||||||
for dataset in currencyChart.data.datasets
|
for dataset in currencyChart.data.datasets
|
||||||
set isMatch to dataset.label is datasetName
|
set isMatch to dataset.label is datasetName
|
||||||
call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), isMatch)
|
call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), isMatch)
|
||||||
@@ -456,7 +467,7 @@
|
|||||||
call currencyChart.update()
|
call currencyChart.update()
|
||||||
|
|
||||||
for dataset in monthlyDifferenceChart.data.datasets
|
for dataset in monthlyDifferenceChart.data.datasets
|
||||||
set isMatch to dataset.label is datasetName
|
set isMatch to dataset.label is differenceDatasetName
|
||||||
call monthlyDifferenceChart.setDatasetVisibility(monthlyDifferenceChart.data.datasets.indexOf(dataset), isMatch)
|
call monthlyDifferenceChart.setDatasetVisibility(monthlyDifferenceChart.data.datasets.indexOf(dataset), isMatch)
|
||||||
end
|
end
|
||||||
call monthlyDifferenceChart.update()
|
call monthlyDifferenceChart.update()
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-col-auto">
|
<td class="table-col-auto">
|
||||||
|
{% if not rule.owner or user == rule.owner %}
|
||||||
<a class="no-underline cursor-pointer"
|
<a class="no-underline cursor-pointer"
|
||||||
role="button"
|
role="button"
|
||||||
data-tippy-content="
|
data-tippy-content="
|
||||||
@@ -72,6 +73,12 @@
|
|||||||
{% if rule.active %}<i class="fa-solid fa-toggle-on text-success"></i>{% else %}
|
{% if rule.active %}<i class="fa-solid fa-toggle-on text-success"></i>{% else %}
|
||||||
<i class="fa-solid fa-toggle-off text-error"></i>{% endif %}
|
<i class="fa-solid fa-toggle-off text-error"></i>{% endif %}
|
||||||
</a>
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span data-tippy-content="{% translate "Only the owner can change this" %}">
|
||||||
|
{% if rule.active %}<i class="fa-solid fa-toggle-on text-success opacity-50"></i>{% else %}
|
||||||
|
<i class="fa-solid fa-toggle-off text-error opacity-50"></i>{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="table-col-auto text-center">
|
<td class="table-col-auto text-center">
|
||||||
<div>{{ rule.order }}</div>
|
<div>{{ rule.order }}</div>
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% translate "Save filter preset" as save_filter_preset_title %}
|
||||||
|
{% translate "Preset name" as preset_name_label %}
|
||||||
|
{% translate "Cancel" as cancel_label %}
|
||||||
|
{% translate "Save" as save_label %}
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<form id="save-filter-preset"
|
||||||
|
hx-post="{% url 'filter_preset_create' %}"
|
||||||
|
hx-include="#filter"
|
||||||
|
hx-target="#filter-presets"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<input id="filter-preset-name" type="hidden" name="name">
|
||||||
|
<button class="btn btn-outline btn-primary btn-sm" type="button"
|
||||||
|
data-title="{{ save_filter_preset_title }}"
|
||||||
|
data-input-label="{{ preset_name_label }}"
|
||||||
|
data-cancel-text="{{ cancel_label }}"
|
||||||
|
data-confirm-text="{{ save_label }}"
|
||||||
|
_="on click
|
||||||
|
call Swal.fire({title: @data-title,
|
||||||
|
input: 'text',
|
||||||
|
inputLabel: @data-input-label,
|
||||||
|
inputAttributes: {maxlength: '100'},
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonText: @data-cancel-text,
|
||||||
|
confirmButtonText: @data-confirm-text,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-primary me-3',
|
||||||
|
cancelButton: 'btn btn-error'
|
||||||
|
},
|
||||||
|
buttonsStyling: false})
|
||||||
|
if result.isConfirmed
|
||||||
|
set presetName to result.value.trim()
|
||||||
|
if presetName is not ''
|
||||||
|
set #filter-preset-name.value to presetName
|
||||||
|
trigger submit on #save-filter-preset
|
||||||
|
end
|
||||||
|
end">
|
||||||
|
<i class="fa-solid fa-bookmark fa-fw"></i>
|
||||||
|
{% translate 'Save preset' %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<a class="btn btn-outline btn-error btn-sm"
|
||||||
|
href="{{ request.path }}"
|
||||||
|
hx-get="{% url 'transaction_filter_clear' %}"
|
||||||
|
hx-target="#filter"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
{% translate 'Clear' %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
<form _="on change or submit or search
|
||||||
|
if event.type is 'submit'
|
||||||
|
halt the event
|
||||||
|
end
|
||||||
|
trigger updated on window
|
||||||
|
end
|
||||||
|
install init_tom_select
|
||||||
|
install init_datepicker"
|
||||||
|
id="filter" class="mt-3">
|
||||||
|
{% crispy filter.form %}
|
||||||
|
</form>
|
||||||
|
{% if swap_filter_indicator %}
|
||||||
|
<span id="filter-active-indicator"
|
||||||
|
class="absolute -top-1 -right-1 w-3 h-3 bg-error rounded-full {% if not filter_is_active %}hidden {% endif %}z-10"
|
||||||
|
hx-swap-oob="outerHTML"></span>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div id="filter-presets">
|
||||||
|
<button class="btn btn-secondary join-item" type="button"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false"
|
||||||
|
title="{% translate 'Filter presets' %}">
|
||||||
|
<i class="fa-solid fa-bookmark fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end menu min-w-52">
|
||||||
|
{% for preset in filter_presets %}
|
||||||
|
<li class="group flex-row items-center">
|
||||||
|
<button class="grow justify-start"
|
||||||
|
type="button"
|
||||||
|
hx-get="{% url 'filter_preset_apply' preset.id %}"
|
||||||
|
hx-target="#filter"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
{{ preset.name }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost btn-xs opacity-0 group-hover:opacity-100 focus:opacity-100"
|
||||||
|
type="button"
|
||||||
|
aria-label="{% blocktranslate with name=preset.name %}Delete {{ name }}{% endblocktranslate %}"
|
||||||
|
hx-post="{% url 'filter_preset_delete' preset.id %}"
|
||||||
|
hx-trigger="confirmed"
|
||||||
|
hx-target="#filter-presets"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
data-title="{% translate 'Delete filter preset?' %}"
|
||||||
|
data-text="{% blocktranslate with name=preset.name %}Delete “{{ name }}”?{% endblocktranslate %}"
|
||||||
|
data-confirm-text="{% translate 'Delete' %}"
|
||||||
|
_="install prompt_swal">
|
||||||
|
<i class="fa-solid fa-xmark fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<li class="menu-disabled"><span>{% translate 'No saved presets' %}</span></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
@click="filterOpen = !filterOpen"
|
@click="filterOpen = !filterOpen"
|
||||||
:aria-expanded="filterOpen" id="filter-button"
|
:aria-expanded="filterOpen" id="filter-button"
|
||||||
title="{% translate 'Filter transactions' %}"
|
title="{% translate 'Filter transactions' %}"
|
||||||
_="on load or change from #filter
|
_="on change from #filter-container
|
||||||
-- Check if any filter has a non-default value
|
-- Check if any filter has a non-default value
|
||||||
set hasActiveFilter to false
|
set hasActiveFilter to false
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
add .hidden to #filter-active-indicator
|
add .hidden to #filter-active-indicator
|
||||||
end">
|
end">
|
||||||
<i class="fa-solid fa-filter fa-fw"></i>
|
<i class="fa-solid fa-filter fa-fw"></i>
|
||||||
<span id="filter-active-indicator" class="absolute -top-1 -right-1 w-3 h-3 bg-error rounded-full hidden z-10"></span>
|
<span id="filter-active-indicator" class="absolute -top-1 -right-1 w-3 h-3 bg-error rounded-full {% if not filter_is_active %}hidden {% endif %}z-10"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{# Search box #}
|
{# Search box #}
|
||||||
@@ -177,7 +177,8 @@
|
|||||||
show <.transaction/> in <#transactions-list/>
|
show <.transaction/> in <#transactions-list/>
|
||||||
when its textContent.toLowerCase() contains my value.toLowerCase()">
|
when its textContent.toLowerCase() contains my value.toLowerCase()">
|
||||||
|
|
||||||
{# Order by icon dropdown #}
|
{% include "transactions/fragments/filter_presets.html" %}
|
||||||
|
|
||||||
{# Order by icon dropdown #}
|
{# Order by icon dropdown #}
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-secondary join-item" type="button"
|
<button class="btn btn-secondary join-item" type="button"
|
||||||
@@ -220,21 +221,16 @@
|
|||||||
{# Filter transactions form #}
|
{# Filter transactions form #}
|
||||||
<div class="z-1" x-show="filterOpen" x-collapse x-cloak>
|
<div class="z-1" x-show="filterOpen" x-collapse x-cloak>
|
||||||
<div class="card card-body bg-base-200 mt-2">
|
<div class="card card-body bg-base-200 mt-2">
|
||||||
<div class="text-right">
|
{% include "transactions/fragments/filter_actions.html" %}
|
||||||
<button class="btn btn-outline btn-error btn-sm w-fit"
|
|
||||||
_="on click call #filter.reset() then trigger change on #filter">{% translate 'Clear' %}</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form _="on change or submit or search trigger updated on window
|
{% include "transactions/fragments/filter_form.html" %}
|
||||||
install init_tom_select
|
|
||||||
install init_datepicker"
|
|
||||||
id="filter" class="mt-3">
|
|
||||||
{% crispy filter.form %}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<button class="btn btn-outline btn-error btn-sm w-fit"
|
<a class="btn btn-outline btn-error btn-sm w-fit"
|
||||||
_="on click call #filter.reset() then trigger change on #filter">{% translate 'Clear' %}</button>
|
href="{{ request.path }}"
|
||||||
|
hx-get="{% url 'transaction_filter_clear' %}"
|
||||||
|
hx-target="#filter"
|
||||||
|
hx-swap="outerHTML">{% translate 'Clear' %}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ WORKDIR /usr/src/frontend
|
|||||||
|
|
||||||
COPY ./frontend/package.json ./frontend/package-lock.json ./
|
COPY ./frontend/package.json ./frontend/package-lock.json ./
|
||||||
COPY ./docker/dev/vite/entrypoint.sh /usr/local/bin/vite-entrypoint
|
COPY ./docker/dev/vite/entrypoint.sh /usr/local/bin/vite-entrypoint
|
||||||
RUN chmod +x /usr/local/bin/vite-entrypoint
|
RUN sed -i 's/\r$//g' /usr/local/bin/vite-entrypoint && \
|
||||||
|
chmod +x /usr/local/bin/vite-entrypoint
|
||||||
|
|
||||||
ENV PATH ./node_modules/.bin/:$PATH
|
ENV PATH ./node_modules/.bin/:$PATH
|
||||||
|
|
||||||
|
|||||||
Generated
+63
-34
@@ -23,12 +23,13 @@
|
|||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"chartjs-chart-sankey": "^0.14.3",
|
"chartjs-chart-sankey": "^0.14.3",
|
||||||
|
"chartjs-plugin-zoom": "^2.2.0",
|
||||||
"cuelume": "^0.1.2",
|
"cuelume": "^0.1.2",
|
||||||
"daisyui": "5.5.20",
|
"daisyui": "5.5.20",
|
||||||
"htmx.org": "^2.0.10",
|
"htmx.org": "^2.0.10",
|
||||||
"hyperscript.org": "^0.9.91",
|
"hyperscript.org": "^0.9.91",
|
||||||
"mathjs": "^15.2.0",
|
"mathjs": "^15.2.0",
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.26",
|
||||||
"pulltorefreshjs": "^0.1.22",
|
"pulltorefreshjs": "^0.1.22",
|
||||||
"sass": "^1.100.0",
|
"sass": "^1.100.0",
|
||||||
"sweetalert2": "^11.26.25",
|
"sweetalert2": "^11.26.25",
|
||||||
@@ -1077,6 +1078,12 @@
|
|||||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/hammerjs": {
|
||||||
|
"version": "2.0.46",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz",
|
||||||
|
"integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.1.5",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
|
||||||
@@ -1150,9 +1157,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.34",
|
"version": "2.11.20",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz",
|
||||||
"integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==",
|
"integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"baseline-browser-mapping": "dist/cli.cjs"
|
"baseline-browser-mapping": "dist/cli.cjs"
|
||||||
@@ -1181,9 +1188,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.28.2",
|
"version": "4.28.8",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
|
||||||
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
|
"integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1200,11 +1207,11 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.10.12",
|
"baseline-browser-mapping": "^2.11.12",
|
||||||
"caniuse-lite": "^1.0.30001782",
|
"caniuse-lite": "^1.0.30001809",
|
||||||
"electron-to-chromium": "^1.5.328",
|
"electron-to-chromium": "^1.5.402",
|
||||||
"node-releases": "^2.0.36",
|
"node-releases": "^2.0.53",
|
||||||
"update-browserslist-db": "^1.2.3"
|
"update-browserslist-db": "^1.3.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"browserslist": "cli.js"
|
"browserslist": "cli.js"
|
||||||
@@ -1214,9 +1221,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001797",
|
"version": "1.0.30001810",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
|
||||||
"integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==",
|
"integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1254,6 +1261,19 @@
|
|||||||
"chart.js": ">=3.3.0"
|
"chart.js": ">=3.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chartjs-plugin-zoom": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-in6kcdiTlP6npIVLMd4zXZ08PDUXC52gZ4FAy5oyjk1zX3gKarXMAof7B9eFiisf9WOC3bh2saHg+J5WtLXZeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/hammerjs": "^2.0.45",
|
||||||
|
"hammerjs": "^2.0.8"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": ">=3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||||
@@ -1319,9 +1339,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.368",
|
"version": "1.5.420",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz",
|
||||||
"integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==",
|
"integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
@@ -1408,6 +1428,15 @@
|
|||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/hammerjs": {
|
||||||
|
"version": "2.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
|
||||||
|
"integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/htmx.org": {
|
"node_modules/htmx.org": {
|
||||||
"version": "2.0.10",
|
"version": "2.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.10.tgz",
|
||||||
@@ -1424,9 +1453,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/immutable": {
|
"node_modules/immutable": {
|
||||||
"version": "5.1.6",
|
"version": "5.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
|
||||||
"integrity": "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==",
|
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
@@ -1758,9 +1787,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.12",
|
"version": "3.3.18",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -1783,9 +1812,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.47",
|
"version": "2.0.54",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
|
||||||
"integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
|
"integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -1810,9 +1839,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.15",
|
"version": "8.5.26",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1829,7 +1858,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.12",
|
"nanoid": "^3.3.17",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -2040,9 +2069,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.2.3",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
|
||||||
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
"integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|||||||
@@ -30,12 +30,13 @@
|
|||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"chartjs-chart-sankey": "^0.14.3",
|
"chartjs-chart-sankey": "^0.14.3",
|
||||||
|
"chartjs-plugin-zoom": "^2.2.0",
|
||||||
"cuelume": "^0.1.2",
|
"cuelume": "^0.1.2",
|
||||||
"daisyui": "5.5.20",
|
"daisyui": "5.5.20",
|
||||||
"htmx.org": "^2.0.10",
|
"htmx.org": "^2.0.10",
|
||||||
"hyperscript.org": "^0.9.91",
|
"hyperscript.org": "^0.9.91",
|
||||||
"mathjs": "^15.2.0",
|
"mathjs": "^15.2.0",
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.26",
|
||||||
"pulltorefreshjs": "^0.1.22",
|
"pulltorefreshjs": "^0.1.22",
|
||||||
"sass": "^1.100.0",
|
"sass": "^1.100.0",
|
||||||
"sweetalert2": "^11.26.25",
|
"sweetalert2": "^11.26.25",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Chart from 'chart.js/auto';
|
import Chart from 'chart.js/auto';
|
||||||
import {SankeyController, Flow} from 'chartjs-chart-sankey';
|
import {SankeyController, Flow} from 'chartjs-chart-sankey';
|
||||||
|
import zoomPlugin from 'chartjs-plugin-zoom';
|
||||||
|
|
||||||
Chart.register(SankeyController, Flow);
|
Chart.register(SankeyController, Flow, zoomPlugin);
|
||||||
window.Chart = Chart;
|
window.Chart = Chart;
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ readme = "README.md"
|
|||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crispy-bootstrap5==2026.3",
|
"crispy-bootstrap5==2026.3",
|
||||||
"django~=5.2.15",
|
"django~=5.2.16",
|
||||||
"django-allauth[socialaccount]~=65.18.0",
|
"django-allauth[socialaccount]~=65.18.0",
|
||||||
"django-browser-reload==1.21.0",
|
"django-browser-reload==1.21.0",
|
||||||
"django-cachalot~=2.9.0",
|
"django-cachalot~=2.9.0",
|
||||||
|
|||||||
@@ -309,61 +309,58 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cryptography"
|
name = "cryptography"
|
||||||
version = "48.0.1"
|
version = "50.0.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" },
|
{ url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" },
|
{ url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" },
|
{ url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" },
|
{ url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" },
|
{ url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" },
|
{ url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" },
|
{ url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" },
|
{ url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" },
|
{ url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" },
|
{ url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" },
|
{ url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" },
|
{ url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" },
|
{ url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830, upload-time = "2026-06-09T22:31:07.8Z" },
|
{ url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" },
|
{ url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" },
|
{ url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" },
|
{ url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385, upload-time = "2026-06-09T22:31:15.212Z" },
|
{ url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" },
|
{ url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" },
|
{ url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" },
|
{ url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046, upload-time = "2026-06-09T22:31:25.774Z" },
|
{ url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" },
|
{ url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" },
|
{ url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" },
|
{ url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498, upload-time = "2026-06-09T22:31:29.154Z" },
|
{ url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790, upload-time = "2026-06-09T22:31:56.697Z" },
|
{ url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" },
|
{ url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" },
|
{ url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" },
|
{ url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" },
|
{ url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" },
|
{ url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" },
|
{ url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" },
|
{ url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" },
|
{ url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" },
|
{ url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" },
|
{ url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" },
|
{ url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" },
|
{ url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/d3/eb4e394e587341fdad09a09101fa76478ead3a78b0ad63e55c22f0d75c02/cryptography-48.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a", size = 3951747, upload-time = "2026-06-09T22:31:23.871Z" },
|
{ url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/4a/3f43451b4f858bfceaaaffc649e6e787e8d4fb332a1d443af39ab02cc8f1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd", size = 4641226, upload-time = "2026-06-09T22:31:02.532Z" },
|
{ url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/4e/855584c2c23b09e4ce2d3b9c30e983e679cd60b068c513c6bbdb91e11782/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c", size = 4668958, upload-time = "2026-06-09T22:32:06.213Z" },
|
{ url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/3b/d35750e41d803d1e516fd6d6011f065424924da7af1748cef4cc9cb3ede1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9", size = 4640793, upload-time = "2026-06-09T22:32:26.331Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/aa/cdb7181fe865285e87e96825aaab239400f1de0c3bfba9bd9769b79f1a92/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92", size = 4668505, upload-time = "2026-06-09T22:31:27.534Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/8c/ce3823c06c2804f194f9e64f0d67fa3f4094a39f2bb1a990cd03603af8fc/cryptography-48.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a", size = 3742204, upload-time = "2026-06-09T22:31:34.773Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -410,16 +407,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django"
|
name = "django"
|
||||||
version = "5.2.15"
|
version = "5.2.16"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "asgiref" },
|
{ name = "asgiref" },
|
||||||
{ name = "sqlparse" },
|
{ name = "sqlparse" },
|
||||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/2b/e3/31722f7284c9f43333daff9aee9184678e4487adcb5506af0db8cea09ce1/django-5.2.15.tar.gz", hash = "sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7", size = 10873669, upload-time = "2026-06-03T13:03:35.892Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/a9/26/889449d521ae508b26de715954faecd8bcf3f740affb81b2d146a83b42a5/django-5.2.16.tar.gz", hash = "sha256:59ea02020c3136fce14bef0bbece21a10a4febef5eed1c51c22ae468efa22200", size = 10890894, upload-time = "2026-07-07T13:52:17.005Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/b5/38140b1643c00d5c46ce69c78e6980fd285aee223100319631bedee4f5e7/django-5.2.15-py3-none-any.whl", hash = "sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970", size = 8311957, upload-time = "2026-06-03T13:03:31.329Z" },
|
{ url = "https://files.pythonhosted.org/packages/4e/13/1e5e3e4c15dcecb04281b3cb2a46a4670e1cef131068e202f6040df19224/django-5.2.16-py3-none-any.whl", hash = "sha256:04f354bf9d807a86ad1a8392fe3808d362358a8eafc322848e0e43e59b24371d", size = 8311943, upload-time = "2026-07-07T13:52:11.223Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -583,14 +580,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "djangorestframework"
|
name = "djangorestframework"
|
||||||
version = "3.17.1"
|
version = "3.17.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/3b/35/c96055e700fdff25da3a7b7756cfd1d4dc54f38b9bc6d6c5e19e3a0fdc20/djangorestframework-3.17.2.tar.gz", hash = "sha256:89ed713b6dc83e1539f214b7d10808ae19bb8511004beba886225da6d5c9dafa", size = 906683, upload-time = "2026-08-05T07:47:22.5Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", size = 898844, upload-time = "2026-03-24T16:58:31.845Z" },
|
{ url = "https://files.pythonhosted.org/packages/a2/46/c14108e400b208c394325eb63fbae06c81341b6447fa1a6f9da718b17fe7/djangorestframework-3.17.2-py3-none-any.whl", hash = "sha256:cb0546a7415d5b46c04e0f4fe0a54b2109f4fdd5e83ca773c8c6183a6493d042", size = 899109, upload-time = "2026-08-05T07:47:20.853Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -712,11 +709,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mistune"
|
name = "mistune"
|
||||||
version = "3.3.0"
|
version = "3.3.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/84/9c/1939635275ec7258e2b43b00dafabc36d89ad11aa7838d375dc1b0e561cb/mistune-3.3.0.tar.gz", hash = "sha256:3074ec4c61b384abe725128e4dcbb483f5a09cc4632012505cdee655d3a113b9", size = 110936, upload-time = "2026-06-21T13:11:39.458Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/7b/a5/2dab368d6950e6808904dec98f54c7e726ee7be4a0c6afe00e6e011bd52d/mistune-3.3.3.tar.gz", hash = "sha256:c4c6c0c840b8637a2e9b8b6d607eb7c8f00888bf14c754409bcd339e848c2477", size = 115363, upload-time = "2026-07-09T06:18:05.268Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/76/b90f9d48d43fbd80a79a20d3eab2e5109859c7a56dc663b23187385898f3/mistune-3.3.0-py3-none-any.whl", hash = "sha256:a758e578acda49d8195f9a860b132dae2cf7bf409381393b1c4e6e489a65397b", size = 61250, upload-time = "2026-06-21T13:11:37.938Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/70/b1e4737b84163db5bb1dfde6f216dbfbf32783330a9989c965e121172830/mistune-3.3.3-py3-none-any.whl", hash = "sha256:99de1585e42dcbd826faa9e11a202727a5e202e4e4722a4c69ac1ff615793dd7", size = 63569, upload-time = "2026-07-09T06:18:03.839Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1535,20 +1532,20 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlparse"
|
name = "sqlparse"
|
||||||
version = "0.5.5"
|
version = "0.6.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/5f/d3/3f06a1006f2261d1342aefb3c71eed02f5d4ca5bdbecd86ebc12ad38306e/sqlparse-0.6.0.tar.gz", hash = "sha256:113c35c75365ab9cc9c7231d68c6428fb11c085fc8e9eb1ad659b7ddbf6cd2b9", size = 178477, upload-time = "2026-08-13T19:16:06.396Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
|
{ url = "https://files.pythonhosted.org/packages/d9/50/f00935da0ec7cbf325f8dc4f772ae46fbc7b672dd62876e73f0a94adda57/sqlparse-0.6.0-py3-none-any.whl", hash = "sha256:b861c0288ce2fa56209a9a6412d2e066ac664b3873b89c26c9d8415e8e32996f", size = 50070, upload-time = "2026-08-13T19:16:04.062Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tablib"
|
name = "tablib"
|
||||||
version = "3.9.0"
|
version = "3.10.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/11/00/416d2ba54d7d58a7f7c61bf62dfeb48fd553cf49614daf83312f2d2c156e/tablib-3.9.0.tar.gz", hash = "sha256:1b6abd8edb0f35601e04c6161d79660fdcde4abb4a54f66cc9f9054bd55d5fe2", size = 125565, upload-time = "2025-10-15T18:21:56.263Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/96/b9/59d9c8b9c34642e7a718e8a1a7a91af3eb9b48f79bac794263702d41519c/tablib-3.10.0.tar.gz", hash = "sha256:781cfa9a42de8f33c52aff857fd0a04e483de4fd1c53eaaa9a6aeac62bad11de", size = 127128, upload-time = "2026-07-31T17:30:15.577Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/66/6b/32e51d847148b299088fc42d3d896845fd09c5247190133ea69dbe71ba51/tablib-3.9.0-py3-none-any.whl", hash = "sha256:eda17cd0d4dda614efc0e710227654c60ddbeb1ca92cdcfc5c3bd1fc5f5a6e4a", size = 49580, upload-time = "2025-10-15T18:21:44.185Z" },
|
{ url = "https://files.pythonhosted.org/packages/fc/0f/3ae17545ff976687f679ed3a101df7017135ee04769e858a4ccea0cc3f3e/tablib-3.10.0-py3-none-any.whl", hash = "sha256:5721bf3a52d491f8f843628a3a3200db164f6ad9e92fb6eaa64dc3f945f30ad9", size = 50170, upload-time = "2026-07-31T17:30:04.925Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1856,7 +1853,7 @@ dependencies = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "crispy-bootstrap5", specifier = "==2026.3" },
|
{ name = "crispy-bootstrap5", specifier = "==2026.3" },
|
||||||
{ name = "django", specifier = "~=5.2.15" },
|
{ name = "django", specifier = "~=5.2.16" },
|
||||||
{ name = "django-allauth", extras = ["socialaccount"], specifier = "~=65.18.0" },
|
{ name = "django-allauth", extras = ["socialaccount"], specifier = "~=65.18.0" },
|
||||||
{ name = "django-browser-reload", specifier = "==1.21.0" },
|
{ name = "django-browser-reload", specifier = "==1.21.0" },
|
||||||
{ name = "django-cachalot", specifier = "~=2.9.0" },
|
{ name = "django-cachalot", specifier = "~=2.9.0" },
|
||||||
|
|||||||
Reference in New Issue
Block a user