Compare commits

..
Author SHA1 Message Date
Herculino Trotta d500bef481 ci(translations): drop force push to protected main
Branch protection rejects force pushes to main (GH006), so the
translation commit never landed. The push is a fast-forward anyway.
2026-09-06 16:53:57 -03:00
eitchtee f280fcb172 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2026-09-06 19:42:46 +00:00
Herculino Trotta 6c808eff38 Merge pull request #593 from eitchtee/dependabot/uv/djangorestframework-3.17.2
build(deps): bump djangorestframework from 3.17.1 to 3.17.2
2026-09-06 16:41:50 -03:00
Herculino Trotta 1690a153f9 Merge pull request #594 from eitchtee/dependabot/npm_and_yarn/frontend/browserslist-4.28.8
build(deps): bump browserslist from 4.28.2 to 4.28.8 in /frontend
2026-09-06 16:41:41 -03:00
Herculino Trotta 7116176c78 Merge pull request #595 from eitchtee/dependabot/uv/mistune-3.3.3
build(deps): bump mistune from 3.3.0 to 3.3.3
2026-09-06 16:41:15 -03:00
Herculino Trotta d9c9cbe7c3 Merge pull request #592 from hackking007/fix/owner-scoped-transaction-rule-endpoints
fix: BOLA on transaction-rule endpoints via get_owned_object_or_403
2026-09-06 16:41:00 -03:00
dependabot[bot] 11b03474c1 build(deps): bump mistune from 3.3.0 to 3.3.3
Bumps [mistune](https://github.com/lepture/mistune) from 3.3.0 to 3.3.3.
- [Release notes](https://github.com/lepture/mistune/releases)
- [Changelog](https://github.com/lepture/mistune/blob/main/docs/changes.rst)
- [Commits](https://github.com/lepture/mistune/compare/v3.3.0...v3.3.3)

---
updated-dependencies:
- dependency-name: mistune
  dependency-version: 3.3.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-02 16:45:07 +00:00
Herculino Trotta e2f26b3629 fix(sharing): enforce object-level authorization outside the rules app
The rules endpoints were one instance of a pattern repeated across every
SharedObject-backed app. Route the rest through the same helper.

DCA entries were the worst case and are strictly wider than the reported
rules bug: DCAEntry has an unscoped default manager, so filtering by
strategy__id alone reached entries on strategies the caller could not see
at all. No public or shared strategy was needed -- only a guessable
integer. strategy_entry_add/edit/delete now resolve through the parent
strategy with via="strategy".

Every SharedObject delete view carried an inverted condition:

    if obj.owner != request.user and request.user in obj.shared_with.all():
        obj.shared_with.remove(request.user)
    else:
        obj.delete()

An object its owner had made public matched neither branch's intent and
fell through to delete(), so any authenticated user could destroy it.
Confirmed reachable for accounts, account groups, categories, tags,
entities and DCA strategies. The owner now deletes, a shared user revokes
only their own access, and anyone else gets a 403.

The API viewsets had no object-level check at all. DjangoModelPermissions
gated them shut for ordinary users, who hold no model permissions, so this
was not reachable in a default install -- but the check belongs there
regardless, and a user granted change_account in the admin could write any
visible account. SharedObjectPermission adds it for the SharedObject
viewsets, leaving reads to SharedObjectManager.

account_toggle_untracked only flips the calling user's own row in the
untracked_by m2m, so it takes READ rather than EDIT.

Refs GHSA-83g9-vjqf-2j5q
2026-09-01 23:15:41 -03:00
dependabot[bot] 10c8fcfb97 build(deps): bump browserslist from 4.28.2 to 4.28.8 in /frontend
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.28.2 to 4.28.8.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.28.2...4.28.8)

---
updated-dependencies:
- dependency-name: browserslist
  dependency-version: 4.28.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-02 01:27:54 +00:00
Herculino Trotta 039ad225d3 test(rules): cover object-level authorization for transaction rules
Regression tests for GHSA-83g9-vjqf-2j5q, one per endpoint the advisory
named plus the delete and view paths found alongside them: a non-owner
gets 403 on every mutation of a public or shared rule, and the object is
asserted unchanged afterwards.

Also covers the parts that are easy to regress in the other direction:
shared users keep read access, a shared user deleting only revokes their
own access, unowned rules stay claimable, and children of invisible rules
answer 404 rather than 403.

SharedObjectPredicateParityTests asserts is_visible_to agrees with
SharedObjectManager across every owner/visibility/shared combination.
The manager builds a Q and the predicate tests an instance, so they
cannot share an implementation and can otherwise drift apart.
2026-09-01 21:43:13 -03:00
Herculino Trotta 18d4ab7d11 fix(rules): enforce object-level authorization on rule endpoints
SharedObjectManager scopes querysets to what a user may see, which
includes other people's public and shared-with-them objects. Several
mutating rule endpoints treated that visibility as permission to write.

Generalise get_owned_object_or_403 into get_shared_object_or_error, which
takes an explicit access level instead of inferring one:

- READ requires the object to be visible; denial is 404 so the response
  does not confirm that an id exists.
- EDIT requires ownership; denial is 403, but only after the visibility
  check, so 403 never leaks the existence of an invisible object. This
  matters for TransactionRuleAction, whose manager is unscoped.

The previous owner_path resolved to a User and discarded the object, so
it could not express visibility at all. via= now points at the governing
SharedObject and is resolved with a plain getattr, so an unresolvable
path raises instead of silently granting access.

is_visible_to/is_editable_by replace is_accessible_by, which was never
called and tested visibility == "shared", a value that does not exist in
Visibility.

Also fixes two further holes in the same module:

- transaction_rule_delete fell through to delete() whenever the caller
  was not in shared_with, so any user could delete a public rule. Now
  only the owner deletes; a shared user revokes their own access.
- transaction_rule_view was read-only but is now explicitly READ, so
  rules shared with a user stay viewable.

The activate/deactivate control is hidden for rules the user does not
own, instead of rendering a button that always fails.

Refs GHSA-83g9-vjqf-2j5q
2026-09-01 21:25:46 -03:00
dependabot[bot] c64a363126 build(deps): bump djangorestframework from 3.17.1 to 3.17.2
Bumps [djangorestframework](https://github.com/encode/django-rest-framework) from 3.17.1 to 3.17.2.
- [Release notes](https://github.com/encode/django-rest-framework/releases)
- [Commits](https://github.com/encode/django-rest-framework/compare/3.17.1...3.17.2)

---
updated-dependencies:
- dependency-name: djangorestframework
  dependency-version: 3.17.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-02 00:14:06 +00:00
Moshe Levi 68a9286ce5 Fix BOLA on transaction-rule endpoints via get_owned_object_or_403
Add apps.common.functions.get_owned_object_or_403, an ownership-enforcing
variant of get_object_or_404, and apply it across every rules handler that
resolved a TransactionRule / (UpdateOrCreate)TransactionRuleAction from a URL id
without an owner check. Mirrors the check in transaction_rule_edit
(obj.owner and obj.owner != request.user); objects with no owner remain
accessible, preserving existing behaviour. Nested ownership (actions owned via
their parent rule) is handled with owner_path='rule.owner'.
2026-08-31 20:14:40 +03:00
Herculino Trotta 1357688e7b Merge pull request #591 from eitchtee/dependabot/uv/sqlparse-0.6.0
build(deps): bump sqlparse from 0.5.5 to 0.6.0
2026-08-30 15:13:33 -03:00
Herculino Trotta ba1f421ad3 Merge pull request #590 from eitchtee/weblate
Translations update from Weblate
2026-08-30 15:13:24 -03:00
renardspark f60f86a5cb locale(French): update translation
Currently translated at 98.8% (776 of 785 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/fr/
2026-08-23 16:57:26 +00:00
renardspark 5588eb33b1 locale(French): update translation
Currently translated at 98.8% (776 of 785 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/fr/
2026-08-23 15:57:24 +00:00
renardspark 934e6bd8e0 locale(French): update translation
Currently translated at 98.8% (776 of 785 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/fr/
2026-08-23 14:57:25 +00:00
dependabot[bot] 7933f1c316 build(deps): bump sqlparse from 0.5.5 to 0.6.0
Bumps [sqlparse](https://github.com/andialbrecht/sqlparse) from 0.5.5 to 0.6.0.
- [Changelog](https://github.com/andialbrecht/sqlparse/blob/master/CHANGELOG)
- [Commits](https://github.com/andialbrecht/sqlparse/compare/0.5.5...0.6.0)

---
updated-dependencies:
- dependency-name: sqlparse
  dependency-version: 0.6.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 21:31:28 +00:00
Dimitri Decrock 00f87bea3d locale(Dutch): update translation
Currently translated at 100.0% (785 of 785 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/nl/
2026-08-17 15:57:28 +00:00
Herculino Trotta 53ed0d791a Merge pull request #589 from eitchtee/weblate
Translations update from Weblate
2026-08-16 19:03:53 -03:00
Herculino Trotta 5147d939ef locale(Portuguese (Brazil)): update translation
Currently translated at 100.0% (785 of 785 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pt_BR/
2026-08-16 22:03:32 +00:00
eitchtee 716bd6f57f chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2026-08-16 04:46:49 +00:00
Herculino Trotta ded4cb37d5 Merge pull request #588 from eitchtee/feat/filter-presets
feat(transactions): add filter presets
2026-08-16 01:46:18 -03:00
Herculino Trotta cd48edddab feat(transactions): add htmx filter presets 2026-08-16 01:40:11 -03:00
Herculino Trotta 9e135560fe feat(transactions): add filter preset model 2026-08-16 01:40:03 -03:00
eitchtee bdbccdec16 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2026-08-15 18:12:40 +00:00
Herculino Trotta 907c511b59 Merge pull request #587
feat(dca): add chart zoom navigation
2026-08-15 15:12:14 -03:00
Herculino Trotta 33f0904a0f feat(dca): add chart zoom navigation 2026-08-15 15:10:10 -03:00
eitchtee 1c4f23a69e chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2026-08-15 17:43:26 +00:00
Herculino Trotta 8373732964 Merge pull request #586
Add consolidated currency chart lines
2026-08-15 14:42:54 -03:00
Herculino Trotta 8e09f3e8d8 feat(net-worth): add consolidated chart lines 2026-08-15 14:36:13 -03:00
Herculino Trotta 1516fe0281 Merge pull request #585
fix(tools:calculator): prevent close button from drifiting to the right
2026-08-15 11:41:44 -03:00
Herculino Trotta 3bd16602a0 fix(tools:calculator): prevent close button from drifiting to the right 2026-08-15 11:41:15 -03:00
Herculino Trotta 83c2b599d7 Merge pull request #584
fix(docker:dev): handle CRLF endings on vite entrypoint
2026-08-15 01:32:50 -03:00
Herculino Trotta f9eab3ce17 fix(docker:dev): handle CRLF endings on vite entrypoint 2026-08-15 01:32:02 -03:00
Herculino Trotta 62a33ddd2c Merge pull request #583
feat(monthly): show asset accounts/currencies on specific sumaries
2026-08-15 01:31:30 -03:00
Herculino Trotta ea3e9fd68f feat(monthly): show asset accounts/currencies on specific sumaries 2026-08-15 01:30:57 -03:00
Herculino Trotta d71bf3ccb3 Merge pull request #570 from eitchtee/dependabot/npm_and_yarn/frontend/immutable-5.1.9
build(deps): bump immutable from 5.1.6 to 5.1.9 in /frontend
2026-08-13 00:05:53 -03:00
Herculino Trotta 350d78356c Merge pull request #576 from eitchtee/dependabot/npm_and_yarn/frontend/postcss-8.5.26
build(deps): bump postcss from 8.5.15 to 8.5.26 in /frontend
2026-08-13 00:04:37 -03:00
Herculino Trotta 91dfba519c Merge branch 'main' into dependabot/npm_and_yarn/frontend/postcss-8.5.26 2026-08-13 00:04:24 -03:00
Herculino Trotta 0c9b41182b Merge pull request #575 from eitchtee/dependabot/uv/cryptography-50.0.0
build(deps): bump cryptography from 48.0.1 to 50.0.0
2026-08-13 00:03:51 -03:00
Herculino Trotta 7a900908dd Merge pull request #578 from eitchtee/dependabot/uv/django-5.2.16
build(deps): bump django from 5.2.15 to 5.2.16
2026-08-13 00:03:28 -03:00
Herculino Trotta dcd52ec189 Merge pull request #580 from eitchtee/dependabot/npm_and_yarn/frontend/nanoid-3.3.18
build(deps): bump nanoid from 3.3.12 to 3.3.18 in /frontend
2026-08-13 00:03:18 -03:00
Herculino Trotta 93e73b9aa6 Merge pull request #582 from eitchtee/dependabot/uv/tablib-3.10.0
build(deps): bump tablib from 3.9.0 to 3.10.0
2026-08-13 00:03:02 -03:00
eitchtee 7947d5f874 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2026-08-13 03:01:16 +00:00
Herculino Trotta 2ab6621205 Merge pull request #581 from obervinov/fix/recurring-tags-scheduler
fix: recurring tags and entities dropped when generated for another user
2026-08-13 00:00:48 -03:00
Herculino Trotta 39c15eedec Merge pull request #579 from eitchtee/weblate
Translations update from Weblate
2026-08-12 23:59:05 -03:00
dependabot[bot] 6dcf5a63f8 build(deps): bump tablib from 3.9.0 to 3.10.0
Bumps [tablib](https://github.com/jazzband/tablib) from 3.9.0 to 3.10.0.
- [Release notes](https://github.com/jazzband/tablib/releases)
- [Changelog](https://github.com/jazzband/tablib/blob/master/HISTORY.md)
- [Commits](https://github.com/jazzband/tablib/compare/v3.9.0...v3.10.0)

---
updated-dependencies:
- dependency-name: tablib
  dependency-version: 3.10.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-12 19:48:53 +00:00
obervinov 11a6b3faa6 fix(transactions): recurring tags and entities dropped when generated for another user 2026-08-12 22:33:01 +04:00
dependabot[bot] 9b1312b744 build(deps): bump nanoid from 3.3.12 to 3.3.18 in /frontend
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.12 to 3.3.18.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/3.3.18/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.12...3.3.18)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-version: 3.3.18
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-11 23:14:09 +00:00
GitEbu 72707f46e6 locale(German): update translation
Currently translated at 86.2% (670 of 777 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/de/
2026-08-10 12:57:27 +00:00
dependabot[bot] cf908ae87e build(deps): bump django from 5.2.15 to 5.2.16
Bumps [django](https://github.com/django/django) from 5.2.15 to 5.2.16.
- [Commits](https://github.com/django/django/compare/5.2.15...5.2.16)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 5.2.16
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-10 03:07:57 +00:00
dependabot[bot] b145d10fbd build(deps): bump postcss from 8.5.15 to 8.5.26 in /frontend
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.15 to 8.5.26.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.15...8.5.26)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.26
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-07 07:51:02 +00:00
dependabot[bot] ee39dcf9ef build(deps): bump cryptography from 48.0.1 to 50.0.0
Bumps [cryptography](https://github.com/pyca/cryptography) from 48.0.1 to 50.0.0.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/48.0.1...50.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 50.0.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-05 03:39:39 +00:00
dependabot[bot] 5d330ddfbf build(deps): bump immutable from 5.1.6 to 5.1.9 in /frontend
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 5.1.6 to 5.1.9.
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v5.1.6...v5.1.9)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 5.1.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 06:23:28 +00:00
64 changed files with 6028 additions and 3375 deletions
-1
View File
@@ -65,7 +65,6 @@ jobs:
if: steps.check_changes.outputs.changes_detected == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
with:
push_options: --force
commit_message: |
chore(locale): update translation files
+18 -31
View File
@@ -1,13 +1,19 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
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.views.decorators.http import require_http_methods
from apps.accounts.forms import AccountGroupForm
from apps.accounts.models import AccountGroup
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.forms import SharedObjectForm
@@ -63,17 +69,7 @@ def account_group_add(request, **kwargs):
@login_required
@require_http_methods(["GET", "POST"])
def account_group_edit(request, pk):
account_group = get_object_or_404(AccountGroup, id=pk)
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",
},
)
account_group = get_shared_object_or_error(AccountGroup, request, id=pk, level=EDIT)
if request.method == "POST":
form = AccountGroupForm(request.POST, instance=account_group)
@@ -101,17 +97,18 @@ def account_group_edit(request, pk):
@login_required
@require_http_methods(["DELETE"])
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 (
account_group.owner != request.user
and request.user in account_group.shared_with.all()
):
if account_group.is_editable_by(request.user):
account_group.delete()
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)
messages.success(request, _("Item no longer shared with you"))
else:
account_group.delete()
messages.success(request, _("Account Group deleted successfully"))
raise PermissionDenied
return HttpResponse(
status=204,
@@ -125,7 +122,7 @@ def account_group_delete(request, pk):
@login_required
@require_http_methods(["GET"])
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:
account_group.owner = request.user
@@ -146,17 +143,7 @@ def account_group_take_ownership(request, pk):
@login_required
@require_http_methods(["GET", "POST"])
def account_group_share(request, pk):
obj = get_object_or_404(AccountGroup, id=pk)
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",
},
)
obj = get_shared_object_or_error(AccountGroup, request, id=pk, level=EDIT)
if request.method == "POST":
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
+21 -28
View File
@@ -1,13 +1,19 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
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.views.decorators.http import require_http_methods
from apps.accounts.forms import AccountForm
from apps.accounts.models import Account
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.forms import SharedObjectForm
@@ -63,16 +69,7 @@ def account_add(request, **kwargs):
@login_required
@require_http_methods(["GET", "POST"])
def account_edit(request, pk):
account = get_object_or_404(Account, id=pk)
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",
},
)
account = get_shared_object_or_error(Account, request, id=pk, level=EDIT)
if request.method == "POST":
form = AccountForm(request.POST, instance=account)
@@ -100,17 +97,7 @@ def account_edit(request, pk):
@login_required
@require_http_methods(["GET", "POST"])
def account_share(request, pk):
obj = get_object_or_404(Account, id=pk)
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",
},
)
obj = get_shared_object_or_error(Account, request, id=pk, level=EDIT)
if request.method == "POST":
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
@@ -138,14 +125,18 @@ def account_share(request, pk):
@login_required
@require_http_methods(["DELETE"])
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)
messages.success(request, _("Item no longer shared with you"))
else:
account.delete()
messages.success(request, _("Account deleted successfully"))
raise PermissionDenied
return HttpResponse(
status=204,
@@ -159,7 +150,9 @@ def account_delete(request, pk):
@login_required
@require_http_methods(["GET"])
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():
account.untracked_by.remove(request.user)
messages.success(request, _("Account is now tracked"))
@@ -179,7 +172,7 @@ def account_toggle_untracked(request, pk):
@login_required
@require_http_methods(["GET"])
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:
account.owner = request.user
+39 -1
View File
@@ -1,4 +1,8 @@
from rest_framework.permissions import BasePermission
from rest_framework.permissions import (
SAFE_METHODS,
BasePermission,
DjangoModelPermissions,
)
from django.conf import settings
@@ -8,3 +12,37 @@ class NotInDemoMode(BasePermission):
return False
else:
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,
]
+1
View File
@@ -3,3 +3,4 @@ from .test_imports import *
from .test_accounts import *
from .test_data_isolation 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")
+3
View File
@@ -6,6 +6,7 @@ from rest_framework.response import Response
from apps.accounts.models import AccountGroup, Account
from apps.accounts.services import get_account_balance
from apps.api.permissions import SHARED_OBJECT_PERMISSIONS
from apps.api.serializers import (
AccountGroupSerializer,
AccountSerializer,
@@ -16,6 +17,7 @@ from apps.api.serializers import (
class AccountGroupViewSet(viewsets.ModelViewSet):
"""ViewSet for managing account groups."""
permission_classes = SHARED_OBJECT_PERMISSIONS
queryset = AccountGroup.objects.all()
serializer_class = AccountGroupSerializer
filterset_fields = {
@@ -40,6 +42,7 @@ class AccountGroupViewSet(viewsets.ModelViewSet):
class AccountViewSet(viewsets.ModelViewSet):
"""ViewSet for managing accounts."""
permission_classes = SHARED_OBJECT_PERMISSIONS
queryset = Account.objects.all()
serializer_class = AccountSerializer
filterset_fields = {
+4
View File
@@ -2,10 +2,12 @@ from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from apps.dca.models import DCAStrategy, DCAEntry
from apps.api.permissions import SHARED_OBJECT_PERMISSIONS
from apps.api.serializers import DCAStrategySerializer, DCAEntrySerializer
class DCAStrategyViewSet(viewsets.ModelViewSet):
permission_classes = SHARED_OBJECT_PERMISSIONS
queryset = DCAStrategy.objects.all()
serializer_class = DCAStrategySerializer
filterset_fields = {
@@ -43,6 +45,8 @@ class DCAStrategyViewSet(viewsets.ModelViewSet):
class DCAEntryViewSet(viewsets.ModelViewSet):
permission_classes = SHARED_OBJECT_PERMISSIONS
shared_object_via = "strategy"
queryset = DCAEntry.objects.all()
serializer_class = DCAEntrySerializer
filterset_fields = {
+4
View File
@@ -19,6 +19,7 @@ from apps.transactions.models import (
RecurringTransaction,
)
from apps.rules.signals import transaction_updated, transaction_created
from apps.api.permissions import SHARED_OBJECT_PERMISSIONS
class TransactionViewSet(viewsets.ModelViewSet):
@@ -68,6 +69,7 @@ class TransactionViewSet(viewsets.ModelViewSet):
class TransactionCategoryViewSet(viewsets.ModelViewSet):
permission_classes = SHARED_OBJECT_PERMISSIONS
queryset = TransactionCategory.objects.all()
serializer_class = TransactionCategorySerializer
filterset_fields = {
@@ -85,6 +87,7 @@ class TransactionCategoryViewSet(viewsets.ModelViewSet):
class TransactionTagViewSet(viewsets.ModelViewSet):
permission_classes = SHARED_OBJECT_PERMISSIONS
queryset = TransactionTag.objects.all()
serializer_class = TransactionTagSerializer
filterset_fields = {
@@ -101,6 +104,7 @@ class TransactionTagViewSet(viewsets.ModelViewSet):
class TransactionEntityViewSet(viewsets.ModelViewSet):
permission_classes = SHARED_OBJECT_PERMISSIONS
queryset = TransactionEntity.objects.all()
serializer_class = TransactionEntitySerializer
filterset_fields = {
+58
View File
@@ -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
+29 -7
View File
@@ -58,13 +58,35 @@ class SharedObject(models.Model):
models.Index(fields=["visibility"]),
]
def is_accessible_by(self, user):
"""Check if a user can access this object"""
return (
self.visibility == "public"
or self.owner == user
or (self.visibility == "shared" and user in self.shared_with.all())
)
# NOTE: these two predicates must stay in sync with the ``Q`` objects built
# by ``SharedObjectManager.get_queryset`` above. The manager filters at the
# queryset level and these check a single instance, so they cannot share an
# implementation; ``SharedObjectPredicateParityTests`` asserts they agree.
def is_visible_to(self, user):
"""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):
if not self.pk and not self.owner:
+5 -6
View File
@@ -1,6 +1,5 @@
import hmac
import json
import logging
import time
from secrets import token_urlsafe
@@ -13,7 +12,6 @@ from oauth2_provider.models import get_application_model
Application = get_application_model()
logger = logging.getLogger(__name__)
SUPPORTED_TOKEN_ENDPOINT_AUTH_METHODS = {
"none": Application.CLIENT_PUBLIC,
@@ -147,8 +145,7 @@ def dynamic_client_registration(request):
default=["code"],
)
except ValueError as exc:
logger.warning("Invalid dynamic client registration payload: %s", exc)
return _json_error("invalid_client_metadata", "Client metadata is invalid.")
return _json_error("invalid_client_metadata", str(exc))
unsupported_grant_types = sorted(set(grant_types) - SUPPORTED_GRANT_TYPES)
if unsupported_grant_types:
@@ -226,10 +223,12 @@ def dynamic_client_registration(request):
try:
application.full_clean()
except ValidationError as exc:
logger.warning("Dynamic client registration validation failed: %s", exc)
errors = []
for field, messages in exc.message_dict.items():
errors.extend(f"{field}: {message}" for message in messages)
return _json_error(
"invalid_client_metadata",
"Client metadata is invalid.",
"; ".join(errors),
)
application.save()
+269
View File
@@ -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)
-3
View File
@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.
View File
+248
View File
@@ -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()
)
+49 -36
View File
@@ -1,13 +1,19 @@
from django.contrib import messages
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.functions import TruncMonth
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.views.decorators.http import require_http_methods
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.models import DCAStrategy, DCAEntry
from apps.common.models import SharedObject
@@ -56,17 +62,9 @@ def strategy_add(request):
@only_htmx
@login_required
def strategy_edit(request, strategy_id):
dca_strategy = get_object_or_404(DCAStrategy, id=strategy_id)
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",
},
)
dca_strategy = get_shared_object_or_error(
DCAStrategy, request, id=strategy_id, level=EDIT
)
if request.method == "POST":
form = DCAStrategyForm(request.POST, instance=dca_strategy)
@@ -94,17 +92,20 @@ def strategy_edit(request, strategy_id):
@login_required
@require_http_methods(["DELETE"])
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 (
dca_strategy.owner != request.user
and request.user in dca_strategy.shared_with.all()
):
if dca_strategy.is_editable_by(request.user):
dca_strategy.delete()
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)
messages.success(request, _("Item no longer shared with you"))
else:
dca_strategy.delete()
messages.success(request, _("DCA strategy deleted successfully"))
raise PermissionDenied
return HttpResponse(
status=204,
@@ -118,7 +119,9 @@ def strategy_delete(request, strategy_id):
@login_required
@require_http_methods(["GET"])
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:
dca_strategy.owner = request.user
@@ -139,17 +142,7 @@ def strategy_take_ownership(request, strategy_id):
@login_required
@require_http_methods(["GET", "POST"])
def strategy_share(request, pk):
obj = get_object_or_404(DCAStrategy, id=pk)
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",
},
)
obj = get_shared_object_or_error(DCAStrategy, request, id=pk, level=EDIT)
if request.method == "POST":
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
@@ -175,7 +168,9 @@ def strategy_share(request, pk):
@login_required
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(
request,
@@ -187,7 +182,9 @@ def strategy_detail_index(request, strategy_id):
@only_htmx
@login_required
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()
# Calculate monthly aggregates
@@ -229,7 +226,9 @@ def strategy_detail(request, strategy_id):
@only_htmx
@login_required
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":
form = DCAEntryForm(request.POST, strategy=strategy)
if form.is_valid():
@@ -255,7 +254,14 @@ def strategy_entry_add(request, strategy_id):
@only_htmx
@login_required
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":
form = DCAEntryForm(request.POST, instance=dca_entry)
@@ -283,7 +289,14 @@ def strategy_entry_edit(request, strategy_id, entry_id):
@login_required
@require_http_methods(["DELETE"])
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()
@@ -107,6 +107,25 @@ class MonthlySummaryFilterBehaviorTests(TestCase):
return data
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 ---
def test_monthly_summary_no_filter_returns_200(self):
@@ -304,6 +323,15 @@ class MonthlySummaryFilterBehaviorTests(TestCase):
)
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):
"""Test that monthly_account_summary returns 200 with filter"""
response = self.client.get(
@@ -322,6 +350,16 @@ class MonthlySummaryFilterBehaviorTests(TestCase):
)
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):
"""Test that monthly_currency_summary returns 200 with filter"""
response = self.client.get(
+3 -3
View File
@@ -14,7 +14,7 @@ from apps.monthly_overview.utils.daily_spending_allowance import (
calculate_daily_allowance_currency,
)
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 (
calculate_currency_totals,
calculate_percentage_distribution,
@@ -58,6 +58,8 @@ def monthly_overview(request, month: int, year: int):
"previous_month": previous_month,
"previous_year": previous_year,
"filter": f,
"filter_is_active": f.has_active_filters,
"filter_presets": FilterPreset.objects.filter(owner=request.user),
"order": order,
"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)
)
.exclude(account__in=request.user.untracked_accounts.all())
.exclude(account__is_asset=True)
)
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)
)
.exclude(account__in=request.user.untracked_accounts.all())
.exclude(account__is_asset=True)
)
currency_data = calculate_currency_totals(queryset.all(), ignore_empty=True)
+81 -2
View File
@@ -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,
)
+53
View File
@@ -3,8 +3,11 @@ import json
from django.contrib.auth.decorators import login_required
from django.core.serializers.json import DjangoJSONEncoder
from django.shortcuts import render, redirect
from django.utils.translation import gettext
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 (
calculate_historical_currency_net_worth,
calculate_historical_account_balance,
@@ -78,6 +81,17 @@ def net_worth(request):
)
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):
data = [
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_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)
+58 -47
View File
@@ -4,13 +4,19 @@ from copy import deepcopy
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.db import transaction
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.views.decorators.http import require_http_methods
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 (
TransactionRuleForm,
TransactionRuleActionForm,
@@ -62,7 +68,9 @@ def rules_list(request):
@disabled_on_demo
@require_http_methods(["GET", "POST"])
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
transaction_rule.active = not current_active
transaction_rule.save(update_fields=["active"])
@@ -112,17 +120,9 @@ def transaction_rule_add(request, **kwargs):
@disabled_on_demo
@require_http_methods(["GET", "POST"])
def transaction_rule_edit(request, transaction_rule_id):
transaction_rule = get_object_or_404(TransactionRule, id=transaction_rule_id)
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",
},
)
transaction_rule = get_shared_object_or_error(
TransactionRule, request, id=transaction_rule_id, level=EDIT
)
if request.method == "POST":
form = TransactionRuleForm(request.POST, instance=transaction_rule)
@@ -151,7 +151,9 @@ def transaction_rule_edit(request, transaction_rule_id):
@disabled_on_demo
@require_http_methods(["GET", "POST"])
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()
update_or_create_actions = (
@@ -175,17 +177,20 @@ def transaction_rule_view(request, transaction_rule_id):
@disabled_on_demo
@require_http_methods(["DELETE"])
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 (
transaction_rule.owner != request.user
and request.user in transaction_rule.shared_with.all()
):
if transaction_rule.is_editable_by(request.user):
transaction_rule.delete()
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)
messages.success(request, _("Item no longer shared with you"))
else:
transaction_rule.delete()
messages.success(request, _("Rule deleted successfully"))
raise PermissionDenied
return HttpResponse(
status=204,
@@ -200,7 +205,9 @@ def transaction_rule_delete(request, transaction_rule_id):
@disabled_on_demo
@require_http_methods(["GET"])
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:
transaction_rule.owner = request.user
@@ -222,17 +229,7 @@ def transaction_rule_take_ownership(request, transaction_rule_id):
@disabled_on_demo
@require_http_methods(["GET", "POST"])
def transaction_rule_share(request, pk):
obj = get_object_or_404(TransactionRule, id=pk)
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",
},
)
obj = get_shared_object_or_error(TransactionRule, request, id=pk, level=EDIT)
if request.method == "POST":
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
@@ -261,7 +258,9 @@ def transaction_rule_share(request, pk):
@disabled_on_demo
@require_http_methods(["GET", "POST"])
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":
form = TransactionRuleActionForm(request.POST, rule=transaction_rule)
@@ -289,12 +288,14 @@ def transaction_rule_action_add(request, transaction_rule_id):
@disabled_on_demo
@require_http_methods(["GET", "POST"])
def transaction_rule_action_edit(request, transaction_rule_action_id):
transaction_rule_action = get_object_or_404(
TransactionRuleAction, id=transaction_rule_action_id
)
transaction_rule = get_object_or_404(
TransactionRule, id=transaction_rule_action.rule.id
transaction_rule_action = get_shared_object_or_error(
TransactionRuleAction,
request,
id=transaction_rule_action_id,
level=EDIT,
via="rule",
)
transaction_rule = transaction_rule_action.rule
if request.method == "POST":
form = TransactionRuleActionForm(
@@ -327,8 +328,12 @@ def transaction_rule_action_edit(request, transaction_rule_action_id):
@disabled_on_demo
@require_http_methods(["DELETE"])
def transaction_rule_action_delete(request, transaction_rule_action_id):
transaction_rule_action = get_object_or_404(
TransactionRuleAction, id=transaction_rule_action_id
transaction_rule_action = get_shared_object_or_error(
TransactionRuleAction,
request,
id=transaction_rule_action_id,
level=EDIT,
via="rule",
)
transaction_rule_action.delete()
@@ -348,7 +353,9 @@ def transaction_rule_action_delete(request, transaction_rule_action_id):
@disabled_on_demo
@require_http_methods(["GET", "POST"])
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":
form = UpdateOrCreateTransactionRuleActionForm(
@@ -380,7 +387,9 @@ def update_or_create_transaction_rule_action_add(request, transaction_rule_id):
@disabled_on_demo
@require_http_methods(["GET", "POST"])
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
if request.method == "POST":
@@ -415,7 +424,9 @@ def update_or_create_transaction_rule_action_edit(request, pk):
@disabled_on_demo
@require_http_methods(["DELETE"])
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()
@@ -436,7 +447,7 @@ def update_or_create_transaction_rule_action_delete(request, pk):
@disabled_on_demo
@require_http_methods(["GET", "POST"])
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
results = None
@@ -481,7 +492,7 @@ def dry_run_rule_created(request, pk):
@disabled_on_demo
@require_http_methods(["GET", "POST"])
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
results = None
@@ -526,7 +537,7 @@ def dry_run_rule_deleted(request, pk):
@disabled_on_demo
@require_http_methods(["GET", "POST"])
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
results = None
+23
View File
@@ -41,6 +41,12 @@ class MonthYearFilter(Filter):
class TransactionsFilter(django_filters.FilterSet):
default_filter_values = {
"type": {"IN", "EX"},
"is_paid": {"1", "0"},
"mute_status": {"active", "muted"},
}
description = django_filters.CharFilter(
label=_("Content"),
method=content_filter,
@@ -217,6 +223,23 @@ class TransactionsFilter(django_filters.FilterSet):
]
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
def filter_category(queryset, name, 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"]},
),
]
+27 -6
View File
@@ -30,12 +30,27 @@ from django.utils.translation import gettext_lazy as _
logger = logging.getLogger()
transaction_created = Signal()
transaction_updated = 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):
extension = Path(filename).suffix
return f"transaction_attachments/{instance.transaction_id}/{instance.id}{extension}"
@@ -535,6 +550,7 @@ class Transaction(OwnedObject):
return new_obj
class TransactionAttachment(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
transaction = models.ForeignKey(
@@ -590,6 +606,7 @@ def delete_transaction_attachment_file(sender, instance, **kwargs):
if storage.exists(instance.file.name):
storage.delete(instance.file.name)
class InstallmentPlan(models.Model):
class Recurrence(models.TextChoices):
YEARLY = "yearly", _("Yearly")
@@ -937,8 +954,10 @@ class RecurringTransaction(models.Model):
notes=self.notes if self.add_notes_to_transaction else "",
owner=self.account.owner,
)
created_transaction.tags.set(self.tags.all())
created_transaction.entities.set(self.entities.all())
# Unfiltered managers: generation also runs without a current user, or with a
# 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):
if self.recurrence_type == self.RecurrenceType.DAY:
@@ -1030,9 +1049,11 @@ class RecurringTransaction(models.Model):
self.notes if self.add_notes_to_transaction else ""
)
# Update many-to-many relationships
existing_transaction.tags.set(self.tags.all())
existing_transaction.entities.set(self.entities.all())
# Update many-to-many relationships (see create_transaction)
existing_transaction.tags.set(self.tags(manager="all_objects").all())
existing_transaction.entities.set(
self.entities(manager="all_objects").all()
)
# Save updated transaction
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 (
TransactionCategory,
TransactionTag,
TransactionEntity,
Transaction,
InstallmentPlan,
RecurringTransaction,
@@ -240,3 +241,27 @@ class RecurringTransactionTests(TestCase):
self.assertFalse(recurring.is_paused)
self.assertEqual(recurring.recurrence_interval, 1)
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())
+20
View File
@@ -6,6 +6,26 @@ urlpatterns = [
path(
"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(
"transactions/trash/",
views.transactions_trash_can_index,
+24 -28
View File
@@ -1,11 +1,17 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
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.views.decorators.http import require_http_methods
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.models import TransactionCategory
from apps.common.models import SharedObject
@@ -85,17 +91,9 @@ def category_add(request, **kwargs):
@login_required
@require_http_methods(["GET", "POST"])
def category_edit(request, category_id):
category = get_object_or_404(TransactionCategory, id=category_id)
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",
},
)
category = get_shared_object_or_error(
TransactionCategory, request, id=category_id, level=EDIT
)
if request.method == "POST":
form = TransactionCategoryForm(request.POST, instance=category)
@@ -123,17 +121,7 @@ def category_edit(request, category_id):
@login_required
@require_http_methods(["GET", "POST"])
def category_share(request, pk):
obj = get_object_or_404(TransactionCategory, id=pk)
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",
},
)
obj = get_shared_object_or_error(TransactionCategory, request, id=pk, level=EDIT)
if request.method == "POST":
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
@@ -161,14 +149,20 @@ def category_share(request, pk):
@login_required
@require_http_methods(["DELETE"])
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)
messages.success(request, _("Item no longer shared with you"))
else:
category.delete()
messages.success(request, _("Category deleted successfully"))
raise PermissionDenied
return HttpResponse(
status=204,
@@ -182,7 +176,9 @@ def category_delete(request, category_id):
@login_required
@require_http_methods(["GET"])
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:
category.owner = request.user
+24 -28
View File
@@ -1,11 +1,17 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
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.views.decorators.http import require_http_methods
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.models import TransactionEntity
from apps.common.models import SharedObject
@@ -85,17 +91,9 @@ def entity_add(request, **kwargs):
@login_required
@require_http_methods(["GET", "POST"])
def entity_edit(request, entity_id):
entity = get_object_or_404(TransactionEntity, id=entity_id)
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",
},
)
entity = get_shared_object_or_error(
TransactionEntity, request, id=entity_id, level=EDIT
)
if request.method == "POST":
form = TransactionEntityForm(request.POST, instance=entity)
@@ -123,14 +121,20 @@ def entity_edit(request, entity_id):
@login_required
@require_http_methods(["DELETE"])
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)
messages.success(request, _("Item no longer shared with you"))
else:
entity.delete()
messages.success(request, _("Entity deleted successfully"))
raise PermissionDenied
return HttpResponse(
status=204,
@@ -144,7 +148,9 @@ def entity_delete(request, entity_id):
@login_required
@require_http_methods(["GET"])
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:
entity.owner = request.user
@@ -165,17 +171,7 @@ def entity_take_ownership(request, entity_id):
@login_required
@require_http_methods(["GET", "POST"])
def entity_share(request, pk):
obj = get_object_or_404(TransactionEntity, id=pk)
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",
},
)
obj = get_shared_object_or_error(TransactionEntity, request, id=pk, level=EDIT)
if request.method == "POST":
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
+18 -28
View File
@@ -1,11 +1,17 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
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.views.decorators.http import require_http_methods
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.models import TransactionTag
from apps.common.models import SharedObject
@@ -85,17 +91,7 @@ def tag_add(request, **kwargs):
@login_required
@require_http_methods(["GET", "POST"])
def tag_edit(request, tag_id):
tag = get_object_or_404(TransactionTag, id=tag_id)
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",
},
)
tag = get_shared_object_or_error(TransactionTag, request, id=tag_id, level=EDIT)
if request.method == "POST":
form = TransactionTagForm(request.POST, instance=tag)
@@ -123,14 +119,18 @@ def tag_edit(request, tag_id):
@login_required
@require_http_methods(["DELETE"])
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)
messages.success(request, _("Item no longer shared with you"))
else:
tag.delete()
messages.success(request, _("Tag deleted successfully"))
raise PermissionDenied
return HttpResponse(
status=204,
@@ -144,7 +144,7 @@ def tag_delete(request, tag_id):
@login_required
@require_http_methods(["GET"])
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:
tag.owner = request.user
@@ -165,17 +165,7 @@ def tag_take_ownership(request, tag_id):
@login_required
@require_http_methods(["GET", "POST"])
def tag_share(request, pk):
obj = get_object_or_404(TransactionTag, id=pk)
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",
},
)
obj = get_shared_object_or_error(TransactionTag, request, id=pk, level=EDIT)
if request.method == "POST":
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
+88 -3
View File
@@ -11,7 +11,7 @@ from apps.transactions.forms import (
TransactionForm,
TransferForm,
)
from apps.transactions.models import Transaction, TransactionAttachment
from apps.transactions.models import FilterPreset, Transaction, TransactionAttachment
from apps.transactions.utils.calculations import (
calculate_account_totals,
calculate_currency_totals,
@@ -23,7 +23,7 @@ from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Case, IntegerField, Q, Value, When
from django.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.utils import timezone
from django.utils.translation import gettext_lazy as _
@@ -635,7 +635,92 @@ def transaction_all_index(request):
return render(
request,
"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)},
)
File diff suppressed because it is too large Load Diff
+220 -192
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\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:475 apps/transactions/forms.py:572
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
#: apps/transactions/forms.py:1007 apps/transactions/models.py:331
#: apps/transactions/models.py:641 apps/transactions/models.py:841
#: apps/transactions/models.py:1089
#: apps/transactions/forms.py:1007 apps/transactions/models.py:346
#: apps/transactions/models.py:658 apps/transactions/models.py:858
#: apps/transactions/models.py:1110
#: templates/insights/fragments/category_overview/index.html:86
#: templates/insights/fragments/category_overview/index.html:542
#: 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/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/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:483 apps/transactions/forms.py:588
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
#: apps/transactions/forms.py:1000 apps/transactions/models.py:337
#: apps/transactions/models.py:643 apps/transactions/models.py:845
#: apps/transactions/models.py:1095 templates/includes/sidebar.html:150
#: apps/transactions/forms.py:1000 apps/transactions/models.py:352
#: apps/transactions/models.py:660 apps/transactions/models.py:862
#: apps/transactions/models.py:1116 templates/includes/sidebar.html:150
#: templates/insights/fragments/category_overview/index.html:40
#: templates/insights/fragments/month_by_month.html:29
#: 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/import_app/models.py:14 apps/rules/models.py:13
#: apps/transactions/models.py:223 apps/transactions/models.py:248
#: apps/transactions/models.py:272 apps/transactions/models.py:1057
#: apps/transactions/models.py:238 apps/transactions/models.py:263
#: apps/transactions/models.py:287 apps/transactions/models.py:1078
#: apps/users/models.py:572 templates/account_groups/fragments/list.html:22
#: templates/accounts/fragments/list.html:22
#: templates/categories/fragments/table.html:17
@@ -165,9 +165,9 @@ msgstr ""
#: apps/rules/models.py:35 apps/rules/models.py:267
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
#: apps/transactions/forms.py:992 apps/transactions/models.py:303
#: apps/transactions/models.py:601 apps/transactions/models.py:823
#: apps/transactions/models.py:1063
#: apps/transactions/forms.py:992 apps/transactions/models.py:318
#: apps/transactions/models.py:618 apps/transactions/models.py:840
#: apps/transactions/models.py:1084
#: templates/installment_plans/fragments/table.html:17
#: templates/quick_transactions/fragments/list.html:14
#: templates/recurring_transactions/fragments/table.html:19
@@ -176,7 +176,7 @@ msgid "Account"
msgstr ""
#: 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/pages/index.html:4 templates/includes/sidebar.html:162
#: templates/includes/sidebar.html:164
@@ -189,68 +189,56 @@ msgstr ""
msgid "Exchange currency cannot be the same as the account's main currency."
msgstr ""
#: apps/accounts/views/account_groups.py:44
#: apps/accounts/views/account_groups.py:50
msgid "Account Group added successfully"
msgstr ""
#: apps/accounts/views/account_groups.py:69
#: 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
#: apps/accounts/views/account_groups.py:78
msgid "Account Group updated successfully"
msgstr ""
#: apps/accounts/views/account_groups.py:111
#: 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
#: apps/accounts/views/account_groups.py:104
msgid "Account Group deleted successfully"
msgstr ""
#: apps/accounts/views/account_groups.py:135
#: apps/accounts/views/accounts.py:189 apps/dca/views.py:128
#: apps/rules/views.py:210 apps/transactions/views/categories.py:192
#: apps/transactions/views/entities.py:154 apps/transactions/views/tags.py:154
#: apps/accounts/views/account_groups.py:109
#: apps/accounts/views/accounts.py:137 apps/dca/views.py:106
#: apps/rules/views.py:191 apps/transactions/views/categories.py:163
#: apps/transactions/views/entities.py:135 apps/transactions/views/tags.py:131
msgid "Item no longer shared with you"
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"
msgstr ""
#: apps/accounts/views/account_groups.py:165
#: apps/accounts/views/accounts.py:119 apps/dca/views.py:158
#: apps/rules/views.py:241 apps/transactions/views/categories.py:142
#: apps/transactions/views/entities.py:184 apps/transactions/views/tags.py:184
#: apps/accounts/views/account_groups.py:152
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:151
#: apps/rules/views.py:238 apps/transactions/views/categories.py:130
#: apps/transactions/views/entities.py:180 apps/transactions/views/tags.py:174
msgid "Configuration saved successfully"
msgstr ""
#: apps/accounts/views/accounts.py:44
#: apps/accounts/views/accounts.py:50
msgid "Account added successfully"
msgstr ""
#: apps/accounts/views/accounts.py:81
#: apps/accounts/views/accounts.py:78
msgid "Account updated successfully"
msgstr ""
#: apps/accounts/views/accounts.py:148
#: apps/accounts/views/accounts.py:132
msgid "Account deleted successfully"
msgstr ""
#: apps/accounts/views/accounts.py:165
#: apps/accounts/views/accounts.py:158
msgid "Account is now tracked"
msgstr ""
#: apps/accounts/views/accounts.py:168
#: apps/accounts/views/accounts.py:161
msgid "Account is now untracked"
msgstr ""
@@ -345,6 +333,7 @@ msgid ""
msgstr ""
#: apps/common/forms.py:76 apps/users/forms.py:172
#: templates/transactions/fragments/filter_actions.html:5
msgid "Save"
msgstr ""
@@ -459,10 +448,9 @@ msgstr ""
#: apps/common/widgets/tom_select.py:15
#: templates/mini_tools/unit_price_calculator.html:180
#: templates/monthly_overview/pages/overview.html:275
#: templates/monthly_overview/pages/overview.html:287
#: templates/transactions/pages/transactions.html:225
#: templates/transactions/pages/transactions.html:237
#: templates/monthly_overview/pages/overview.html:284
#: templates/transactions/fragments/filter_actions.html:48
#: templates/transactions/pages/transactions.html:233
msgid "Clear"
msgstr ""
@@ -481,7 +469,7 @@ msgstr ""
#: 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/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/exchange_rates/fragments/table.html:10
#: templates/exchange_rates_services/fragments/table.html:11
@@ -501,7 +489,7 @@ msgid "Decimal Places"
msgstr ""
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
#: apps/export_app/forms.py:130 apps/transactions/filters.py:64
#: apps/export_app/forms.py:130 apps/transactions/filters.py:70
#: templates/currencies/fragments/list.html:9
#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:176
#: templates/includes/sidebar.html:178
@@ -567,8 +555,8 @@ msgid "Service Type"
msgstr ""
#: apps/currencies/models.py:119 apps/transactions/filters.py:27
#: apps/transactions/models.py:227 apps/transactions/models.py:251
#: apps/transactions/models.py:275 templates/categories/fragments/list.html:16
#: apps/transactions/models.py:242 apps/transactions/models.py:266
#: apps/transactions/models.py:290 templates/categories/fragments/list.html:16
#: templates/entities/fragments/list.html:16
#: templates/installment_plans/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/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/models.py:327 apps/transactions/models.py:650
#: apps/transactions/models.py:851 apps/transactions/models.py:1085
#: apps/transactions/models.py:342 apps/transactions/models.py:667
#: apps/transactions/models.py:868 apps/transactions/models.py:1106
msgid "Notes"
msgstr ""
@@ -780,27 +768,27 @@ msgstr ""
msgid "DCA Entries"
msgstr ""
#: apps/dca/views.py:38
#: apps/dca/views.py:44
msgid "DCA Strategy added successfully"
msgstr ""
#: apps/dca/views.py:75
#: apps/dca/views.py:73
msgid "DCA Strategy updated successfully"
msgstr ""
#: apps/dca/views.py:107
#: apps/dca/views.py:101
msgid "DCA strategy deleted successfully"
msgstr ""
#: apps/dca/views.py:237
#: apps/dca/views.py:236
msgid "Entry added successfully"
msgstr ""
#: apps/dca/views.py:264
#: apps/dca/views.py:270
msgid "Entry updated successfully"
msgstr ""
#: apps/dca/views.py:290
#: apps/dca/views.py:303
msgid "Entry deleted successfully"
msgstr ""
@@ -811,7 +799,7 @@ msgid "Users"
msgstr ""
#: 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/recurring_transactions/fragments/list_transactions.html:5
#: templates/recurring_transactions/fragments/table.html:37
@@ -820,7 +808,7 @@ msgid "Transactions"
msgstr ""
#: 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/insights/fragments/month_by_month.html:18
#: 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/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:491 apps/transactions/forms.py:771
#: apps/transactions/forms.py:1015 apps/transactions/models.py:286
#: apps/transactions/models.py:342 apps/transactions/models.py:646
#: apps/transactions/models.py:848 apps/transactions/models.py:1100
#: apps/transactions/forms.py:1015 apps/transactions/models.py:301
#: apps/transactions/models.py:357 apps/transactions/models.py:663
#: apps/transactions/models.py:865 apps/transactions/models.py:1121
#: templates/entities/fragments/list.html:9
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
#: templates/insights/fragments/category_overview/index.html:54
@@ -848,14 +836,14 @@ msgid "Entities"
msgstr ""
#: 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/pages/index.html:4
msgid "Recurring Transactions"
msgstr ""
#: 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/pages/index.html:4
msgid "Installment Plans"
@@ -1007,7 +995,7 @@ msgid "Run deleted successfully"
msgstr ""
#: 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:407
#: 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/models.py:271 apps/transactions/forms.py:433
#: apps/transactions/models.py:310 apps/transactions/models.py:606
#: apps/transactions/models.py:829 apps/transactions/models.py:1070
#: apps/transactions/models.py:325 apps/transactions/models.py:623
#: apps/transactions/models.py:846 apps/transactions/models.py:1091
msgid "Type"
msgstr ""
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
#: apps/rules/models.py:275 apps/transactions/filters.py:22
#: apps/transactions/forms.py:437 apps/transactions/models.py:312
#: apps/transactions/models.py:1072 templates/cotton/transaction/item.html:20
#: apps/transactions/forms.py:437 apps/transactions/models.py:327
#: apps/transactions/models.py:1093 templates/cotton/transaction/item.html:20
#: templates/cotton/transaction/item.html:31
#: templates/transactions/widgets/paid_toggle_button.html:10
#: 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/models.py:283 apps/transactions/forms.py:89
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
#: apps/transactions/forms.py:777 apps/transactions/models.py:314
#: apps/transactions/models.py:624 apps/transactions/models.py:853
#: apps/transactions/forms.py:777 apps/transactions/models.py:329
#: apps/transactions/models.py:641 apps/transactions/models.py:870
msgid "Reference Date"
msgstr ""
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
#: apps/rules/models.py:287 apps/transactions/forms.py:460
#: apps/transactions/models.py:320 apps/transactions/models.py:834
#: apps/transactions/models.py:1078
#: apps/transactions/models.py:335 apps/transactions/models.py:851
#: apps/transactions/models.py:1099
#: templates/insights/fragments/sankey.html:102
#: templates/installment_plans/fragments/table.html:18
#: 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/models.py:42 apps/rules/models.py:291
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
#: apps/transactions/models.py:325 apps/transactions/models.py:608
#: apps/transactions/models.py:837 apps/transactions/models.py:1083
#: apps/transactions/models.py:340 apps/transactions/models.py:625
#: apps/transactions/models.py:854 apps/transactions/models.py:1104
msgid "Description"
msgstr ""
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
#: apps/rules/models.py:299 apps/transactions/models.py:364
#: apps/transactions/models.py:1105
#: apps/rules/models.py:299 apps/transactions/models.py:379
#: apps/transactions/models.py:1126
msgid "Internal Note"
msgstr ""
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
#: apps/rules/models.py:303 apps/transactions/models.py:366
#: apps/transactions/models.py:1107
#: apps/rules/models.py:303 apps/transactions/models.py:381
#: apps/transactions/models.py:1128
msgid "Internal ID"
msgstr ""
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
#: apps/rules/models.py:319 apps/transactions/forms.py:620
#: apps/transactions/models.py:224 apps/transactions/models.py:315
#: apps/transactions/models.py:1073
#: apps/transactions/models.py:239 apps/transactions/models.py:330
#: apps/transactions/models.py:1094
msgid "Mute"
msgstr ""
@@ -1175,7 +1163,7 @@ msgid "Set Values"
msgstr ""
#: 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"
msgstr ""
@@ -1269,15 +1257,15 @@ msgstr ""
msgid "Update or create transaction action"
msgstr ""
#: apps/rules/views.py:71
#: apps/rules/views.py:79
msgid "Rule deactivated successfully"
msgstr ""
#: apps/rules/views.py:73
#: apps/rules/views.py:81
msgid "Rule activated successfully"
msgstr ""
#: apps/rules/views.py:92
#: apps/rules/views.py:100
msgid "Rule added successfully"
msgstr ""
@@ -1285,27 +1273,27 @@ msgstr ""
msgid "Rule updated successfully"
msgstr ""
#: apps/rules/views.py:188
#: apps/rules/views.py:186
msgid "Rule deleted successfully"
msgstr ""
#: apps/rules/views.py:305
#: apps/rules/views.py:306
msgid "Action updated successfully"
msgstr ""
#: apps/rules/views.py:336
#: apps/rules/views.py:341
msgid "Action deleted successfully"
msgstr ""
#: apps/rules/views.py:360
#: apps/rules/views.py:367
msgid "Update or Create Transaction action added successfully"
msgstr ""
#: apps/rules/views.py:393
#: apps/rules/views.py:402
msgid "Update or Create Transaction action updated successfully"
msgstr ""
#: apps/rules/views.py:423
#: apps/rules/views.py:434
msgid "Update or Create Transaction action deleted successfully"
msgstr ""
@@ -1322,58 +1310,58 @@ msgstr ""
msgid "Muted"
msgstr ""
#: apps/transactions/filters.py:45
#: apps/transactions/filters.py:51
msgid "Content"
msgstr ""
#: apps/transactions/filters.py:51
#: apps/transactions/filters.py:57
msgid "Transaction Type"
msgstr ""
#: apps/transactions/filters.py:89
#: apps/transactions/filters.py:95
msgid "Mute Status"
msgstr ""
#: apps/transactions/filters.py:94
#: apps/transactions/filters.py:100
msgid "Date from"
msgstr ""
#: apps/transactions/filters.py:99 apps/transactions/filters.py:109
#: apps/transactions/filters.py:105 apps/transactions/filters.py:115
msgid "Until"
msgstr ""
#: apps/transactions/filters.py:104
#: apps/transactions/filters.py:110
msgid "Reference date from"
msgstr ""
#: apps/transactions/filters.py:114
#: apps/transactions/filters.py:120
msgid "Amount min"
msgstr ""
#: apps/transactions/filters.py:119
#: apps/transactions/filters.py:125
msgid "Amount max"
msgstr ""
#: apps/transactions/filters.py:202
#: apps/transactions/filters.py:208
msgid "Categorized"
msgstr ""
#: apps/transactions/filters.py:209
#: apps/transactions/filters.py:215
msgid "Tagged"
msgstr ""
#: apps/transactions/filters.py:209
#: apps/transactions/filters.py:215
#: templates/insights/fragments/category_overview/index.html:189
#: templates/insights/fragments/month_by_month.html:121
#: templates/insights/fragments/year_by_year.html:75
msgid "Untagged"
msgstr ""
#: apps/transactions/filters.py:215
#: apps/transactions/filters.py:221
msgid "Any entity"
msgstr ""
#: apps/transactions/filters.py:216
#: apps/transactions/filters.py:222
#: templates/insights/fragments/category_overview/index.html:282
#: templates/insights/fragments/month_by_month.html:123
#: templates/insights/fragments/year_by_year.html:77
@@ -1448,42 +1436,42 @@ msgstr ""
msgid "End date should be after the start date"
msgstr ""
#: apps/transactions/models.py:229
#: apps/transactions/models.py:244
msgid ""
"Deactivated categories won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:237
#: apps/transactions/models.py:252
msgid "Transaction Category"
msgstr ""
#: apps/transactions/models.py:238
#: apps/transactions/models.py:253
msgid "Transaction Categories"
msgstr ""
#: apps/transactions/models.py:253
#: apps/transactions/models.py:268
msgid ""
"Deactivated tags won't be able to be selected when creating new transactions"
msgstr ""
#: apps/transactions/models.py:261 apps/transactions/models.py:262
#: apps/transactions/models.py:276 apps/transactions/models.py:277
msgid "Transaction Tags"
msgstr ""
#: apps/transactions/models.py:277
#: apps/transactions/models.py:292
msgid ""
"Deactivated entities won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:285
#: apps/transactions/models.py:300
#: templates/insights/fragments/month_by_month.html:88
#: templates/insights/fragments/year_by_year.html:56
msgid "Entity"
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:44
#: templates/calendar_view/fragments/list.html:52
@@ -1495,7 +1483,7 @@ msgstr ""
msgid "Income"
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:48
#: templates/calendar_view/fragments/list.html:56
@@ -1506,157 +1494,157 @@ msgstr ""
msgid "Expense"
msgstr ""
#: apps/transactions/models.py:353 apps/transactions/models.py:663
#: apps/transactions/models.py:368 apps/transactions/models.py:680
msgid "Installment Plan"
msgstr ""
#: apps/transactions/models.py:362 apps/transactions/models.py:887
#: apps/transactions/models.py:377 apps/transactions/models.py:904
msgid "Recurring Transaction"
msgstr ""
#: apps/transactions/models.py:370
#: apps/transactions/models.py:385
msgid "Deleted"
msgstr ""
#: apps/transactions/models.py:375
#: apps/transactions/models.py:390
msgid "Deleted At"
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"
msgstr ""
#: apps/transactions/models.py:491
#: apps/transactions/models.py:506
msgid "No category"
msgstr ""
#: apps/transactions/models.py:493
#: apps/transactions/models.py:508
msgid "No description"
msgstr ""
#: apps/transactions/models.py:549
#: apps/transactions/models.py:565
msgid "File"
msgstr ""
#: apps/transactions/models.py:551
#: apps/transactions/models.py:567
msgid "Original Name"
msgstr ""
#: apps/transactions/models.py:553
#: apps/transactions/models.py:569
msgid "Content Type"
msgstr ""
#: apps/transactions/models.py:555
#: apps/transactions/models.py:571
msgid "Size"
msgstr ""
#: apps/transactions/models.py:560
#: apps/transactions/models.py:576
msgid "Uploaded By"
msgstr ""
#: apps/transactions/models.py:565
#: apps/transactions/models.py:581
msgid "Transaction Attachment"
msgstr ""
#: apps/transactions/models.py:566
#: apps/transactions/models.py:582
msgid "Transaction Attachments"
msgstr ""
#: apps/transactions/models.py:595 templates/includes/sidebar.html:57
#: apps/transactions/models.py:612 templates/includes/sidebar.html:57
msgid "Yearly"
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
msgid "Monthly"
msgstr ""
#: apps/transactions/models.py:597
#: apps/transactions/models.py:614
msgid "Weekly"
msgstr ""
#: apps/transactions/models.py:598
#: apps/transactions/models.py:615
msgid "Daily"
msgstr ""
#: apps/transactions/models.py:611
#: apps/transactions/models.py:628
msgid "Number of Installments"
msgstr ""
#: apps/transactions/models.py:616
#: apps/transactions/models.py:633
msgid "Installment Start"
msgstr ""
#: apps/transactions/models.py:617
#: apps/transactions/models.py:634
msgid "The installment number to start counting from"
msgstr ""
#: apps/transactions/models.py:622 apps/transactions/models.py:857
#: apps/transactions/models.py:639 apps/transactions/models.py:874
msgid "Start Date"
msgstr ""
#: apps/transactions/models.py:626 apps/transactions/models.py:858
#: apps/transactions/models.py:643 apps/transactions/models.py:875
msgid "End Date"
msgstr ""
#: apps/transactions/models.py:631
#: apps/transactions/models.py:648
msgid "Recurrence"
msgstr ""
#: apps/transactions/models.py:634
#: apps/transactions/models.py:651
msgid "Installment Amount"
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"
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"
msgstr ""
#: apps/transactions/models.py:816
#: apps/transactions/models.py:833
msgid "day(s)"
msgstr ""
#: apps/transactions/models.py:817
#: apps/transactions/models.py:834
msgid "week(s)"
msgstr ""
#: apps/transactions/models.py:818
#: apps/transactions/models.py:835
msgid "month(s)"
msgstr ""
#: apps/transactions/models.py:819
#: apps/transactions/models.py:836
msgid "year(s)"
msgstr ""
#: apps/transactions/models.py:821
#: apps/transactions/models.py:838
#: templates/recurring_transactions/fragments/list.html:18
msgid "Paused"
msgstr ""
#: apps/transactions/models.py:860
#: apps/transactions/models.py:877
msgid "Recurrence Type"
msgstr ""
#: apps/transactions/models.py:863
#: apps/transactions/models.py:880
msgid "Recurrence Interval"
msgstr ""
#: apps/transactions/models.py:866
#: apps/transactions/models.py:883
msgid "Keep at most"
msgstr ""
#: apps/transactions/models.py:870
#: apps/transactions/models.py:887
msgid "Last Generated Date"
msgstr ""
#: apps/transactions/models.py:873
#: apps/transactions/models.py:890
msgid "Last Generated Reference Date"
msgstr ""
#: apps/transactions/models.py:1117
#: apps/transactions/models.py:1138
#: apps/transactions/views/quick_transactions.py:178
#: apps/transactions/views/quick_transactions.py:187
#: apps/transactions/views/quick_transactions.py:189
@@ -1665,7 +1653,7 @@ msgstr ""
msgid "Quick Transaction"
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:15
msgid "Quick Transactions"
@@ -1716,27 +1704,27 @@ msgid_plural "%(count)s transactions duplicated successfully"
msgstr[0] ""
msgstr[1] ""
#: apps/transactions/views/categories.py:66
#: apps/transactions/views/categories.py:72
msgid "Category added successfully"
msgstr ""
#: apps/transactions/views/categories.py:104
#: apps/transactions/views/categories.py:102
msgid "Category updated successfully"
msgstr ""
#: apps/transactions/views/categories.py:171
#: apps/transactions/views/categories.py:158
msgid "Category deleted successfully"
msgstr ""
#: apps/transactions/views/entities.py:66
#: apps/transactions/views/entities.py:72
msgid "Entity added successfully"
msgstr ""
#: apps/transactions/views/entities.py:104
#: apps/transactions/views/entities.py:102
msgid "Entity updated successfully"
msgstr ""
#: apps/transactions/views/entities.py:133
#: apps/transactions/views/entities.py:130
msgid "Entity deleted successfully"
msgstr ""
@@ -1799,15 +1787,15 @@ msgstr ""
msgid "Recurring Transaction deleted successfully"
msgstr ""
#: apps/transactions/views/tags.py:66
#: apps/transactions/views/tags.py:72
msgid "Tag added successfully"
msgstr ""
#: apps/transactions/views/tags.py:104
#: apps/transactions/views/tags.py:100
msgid "Tag updated successfully"
msgstr ""
#: apps/transactions/views/tags.py:133
#: apps/transactions/views/tags.py:126
msgid "Tag deleted successfully"
msgstr ""
@@ -1889,9 +1877,9 @@ msgstr ""
#: 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:245
#: templates/monthly_overview/pages/overview.html:247
#: templates/transactions/pages/transactions.html:47
#: templates/transactions/pages/transactions.html:195
#: templates/transactions/pages/transactions.html:196
msgid "Default"
msgstr ""
@@ -2212,6 +2200,7 @@ msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:92
#: templates/rules/fragments/transaction_rule/view.html:133
#: templates/tags/fragments/table.html:51
#: templates/transactions/fragments/filter_presets.html:28
#: templates/users/fragments/api_tokens.html:93
msgid "Delete"
msgstr ""
@@ -2316,8 +2305,8 @@ msgid "Current balance"
msgstr ""
#: templates/accounts/fragments/account_reconciliation.html:36
#: templates/net_worth/net_worth.html:146
#: templates/net_worth/net_worth.html:396
#: templates/net_worth/net_worth.html:147
#: templates/net_worth/net_worth.html:407
msgid "Difference"
msgstr ""
@@ -2697,19 +2686,19 @@ msgstr ""
msgid "Amount Bought"
msgstr ""
#: templates/dca/fragments/strategy/details.html:384
#: templates/dca/fragments/strategy/details.html:409
msgid "Entry Price vs Current Price"
msgstr ""
#: templates/dca/fragments/strategy/details.html:398
#: templates/dca/fragments/strategy/details.html:423
msgid "Days Between Investments"
msgstr ""
#: templates/dca/fragments/strategy/details.html:444
#: templates/dca/fragments/strategy/details.html:469
msgid "Investment Frequency"
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."
msgstr ""
@@ -2946,6 +2935,7 @@ msgid "Reload"
msgstr ""
#: templates/includes/scripts/hyperscript/swal.html:13
#: templates/transactions/fragments/filter_actions.html:4
msgid "Cancel"
msgstr ""
@@ -3360,16 +3350,16 @@ msgid "Summary"
msgstr ""
#: 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:204
#: templates/transactions/pages/transactions.html:205
msgid "Oldest first"
msgstr ""
#: 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:213
#: templates/transactions/pages/transactions.html:214
msgid "Newest first"
msgstr ""
@@ -3378,8 +3368,8 @@ msgstr ""
msgid "Filter transactions"
msgstr ""
#: templates/monthly_overview/pages/overview.html:235
#: templates/transactions/pages/transactions.html:185
#: templates/monthly_overview/pages/overview.html:237
#: templates/transactions/pages/transactions.html:186
msgid "Order by"
msgstr ""
@@ -3388,24 +3378,24 @@ msgstr ""
msgid "By currency"
msgstr ""
#: templates/net_worth/net_worth.html:84
#: templates/net_worth/net_worth.html:84 templates/net_worth/net_worth.html:85
msgid "Consolidated"
msgstr ""
#: templates/net_worth/net_worth.html:134
#: templates/net_worth/net_worth.html:135
msgid "Evolution"
msgstr ""
#: templates/net_worth/net_worth.html:162
#: templates/net_worth/net_worth.html:163
#: templates/yearly_overview/pages/overview_by_account.html:4
msgid "By account"
msgstr ""
#: templates/net_worth/net_worth.html:270
#: templates/net_worth/net_worth.html:271
msgid "Evolution by currency"
msgstr ""
#: templates/net_worth/net_worth.html:334
#: templates/net_worth/net_worth.html:345
msgid "Evolution by account"
msgstr ""
@@ -3485,15 +3475,19 @@ msgstr ""
msgid "View"
msgstr ""
#: templates/rules/fragments/list.html:70
#: templates/rules/fragments/list.html:71
msgid "Deactivate"
msgstr ""
#: templates/rules/fragments/list.html:70
#: templates/rules/fragments/list.html:71
msgid "Activate"
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"
msgstr ""
@@ -3636,6 +3630,40 @@ msgstr ""
msgid "transactions"
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
msgid "No transactions found"
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+220 -192
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -64,9 +64,9 @@ msgstr ""
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
#: apps/transactions/forms.py:1007 apps/transactions/models.py:331
#: apps/transactions/models.py:641 apps/transactions/models.py:841
#: apps/transactions/models.py:1089
#: apps/transactions/forms.py:1007 apps/transactions/models.py:346
#: apps/transactions/models.py:658 apps/transactions/models.py:858
#: apps/transactions/models.py:1110
#: templates/insights/fragments/category_overview/index.html:86
#: templates/insights/fragments/category_overview/index.html:542
#: 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/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/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:483 apps/transactions/forms.py:588
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
#: apps/transactions/forms.py:1000 apps/transactions/models.py:337
#: apps/transactions/models.py:643 apps/transactions/models.py:845
#: apps/transactions/models.py:1095 templates/includes/sidebar.html:150
#: apps/transactions/forms.py:1000 apps/transactions/models.py:352
#: apps/transactions/models.py:660 apps/transactions/models.py:862
#: apps/transactions/models.py:1116 templates/includes/sidebar.html:150
#: templates/insights/fragments/category_overview/index.html:40
#: templates/insights/fragments/month_by_month.html:29
#: 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/import_app/models.py:14 apps/rules/models.py:13
#: apps/transactions/models.py:223 apps/transactions/models.py:248
#: apps/transactions/models.py:272 apps/transactions/models.py:1057
#: apps/transactions/models.py:238 apps/transactions/models.py:263
#: apps/transactions/models.py:287 apps/transactions/models.py:1078
#: apps/users/models.py:572 templates/account_groups/fragments/list.html:22
#: templates/accounts/fragments/list.html:22
#: templates/categories/fragments/table.html:17
@@ -164,9 +164,9 @@ msgstr ""
#: apps/rules/models.py:35 apps/rules/models.py:267
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
#: apps/transactions/forms.py:992 apps/transactions/models.py:303
#: apps/transactions/models.py:601 apps/transactions/models.py:823
#: apps/transactions/models.py:1063
#: apps/transactions/forms.py:992 apps/transactions/models.py:318
#: apps/transactions/models.py:618 apps/transactions/models.py:840
#: apps/transactions/models.py:1084
#: templates/installment_plans/fragments/table.html:17
#: templates/quick_transactions/fragments/list.html:14
#: templates/recurring_transactions/fragments/table.html:19
@@ -175,7 +175,7 @@ msgid "Account"
msgstr ""
#: 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/pages/index.html:4 templates/includes/sidebar.html:162
#: templates/includes/sidebar.html:164
@@ -188,68 +188,56 @@ msgstr ""
msgid "Exchange currency cannot be the same as the account's main currency."
msgstr ""
#: apps/accounts/views/account_groups.py:44
#: apps/accounts/views/account_groups.py:50
msgid "Account Group added successfully"
msgstr ""
#: apps/accounts/views/account_groups.py:69
#: 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
#: apps/accounts/views/account_groups.py:78
msgid "Account Group updated successfully"
msgstr ""
#: apps/accounts/views/account_groups.py:111
#: 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
#: apps/accounts/views/account_groups.py:104
msgid "Account Group deleted successfully"
msgstr ""
#: apps/accounts/views/account_groups.py:135
#: apps/accounts/views/accounts.py:189 apps/dca/views.py:128
#: apps/rules/views.py:210 apps/transactions/views/categories.py:192
#: apps/transactions/views/entities.py:154 apps/transactions/views/tags.py:154
#: apps/accounts/views/account_groups.py:109
#: apps/accounts/views/accounts.py:137 apps/dca/views.py:106
#: apps/rules/views.py:191 apps/transactions/views/categories.py:163
#: apps/transactions/views/entities.py:135 apps/transactions/views/tags.py:131
msgid "Item no longer shared with you"
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"
msgstr ""
#: apps/accounts/views/account_groups.py:165
#: apps/accounts/views/accounts.py:119 apps/dca/views.py:158
#: apps/rules/views.py:241 apps/transactions/views/categories.py:142
#: apps/transactions/views/entities.py:184 apps/transactions/views/tags.py:184
#: apps/accounts/views/account_groups.py:152
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:151
#: apps/rules/views.py:238 apps/transactions/views/categories.py:130
#: apps/transactions/views/entities.py:180 apps/transactions/views/tags.py:174
msgid "Configuration saved successfully"
msgstr ""
#: apps/accounts/views/accounts.py:44
#: apps/accounts/views/accounts.py:50
msgid "Account added successfully"
msgstr ""
#: apps/accounts/views/accounts.py:81
#: apps/accounts/views/accounts.py:78
msgid "Account updated successfully"
msgstr ""
#: apps/accounts/views/accounts.py:148
#: apps/accounts/views/accounts.py:132
msgid "Account deleted successfully"
msgstr ""
#: apps/accounts/views/accounts.py:165
#: apps/accounts/views/accounts.py:158
msgid "Account is now tracked"
msgstr ""
#: apps/accounts/views/accounts.py:168
#: apps/accounts/views/accounts.py:161
msgid "Account is now untracked"
msgstr ""
@@ -344,6 +332,7 @@ msgid ""
msgstr ""
#: apps/common/forms.py:76 apps/users/forms.py:172
#: templates/transactions/fragments/filter_actions.html:5
msgid "Save"
msgstr ""
@@ -458,10 +447,9 @@ msgstr ""
#: apps/common/widgets/tom_select.py:15
#: templates/mini_tools/unit_price_calculator.html:180
#: templates/monthly_overview/pages/overview.html:275
#: templates/monthly_overview/pages/overview.html:287
#: templates/transactions/pages/transactions.html:225
#: templates/transactions/pages/transactions.html:237
#: templates/monthly_overview/pages/overview.html:284
#: templates/transactions/fragments/filter_actions.html:48
#: templates/transactions/pages/transactions.html:233
msgid "Clear"
msgstr ""
@@ -480,7 +468,7 @@ msgstr ""
#: 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/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/exchange_rates/fragments/table.html:10
#: templates/exchange_rates_services/fragments/table.html:11
@@ -500,7 +488,7 @@ msgid "Decimal Places"
msgstr ""
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
#: apps/export_app/forms.py:130 apps/transactions/filters.py:64
#: apps/export_app/forms.py:130 apps/transactions/filters.py:70
#: templates/currencies/fragments/list.html:9
#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:176
#: templates/includes/sidebar.html:178
@@ -566,8 +554,8 @@ msgid "Service Type"
msgstr ""
#: apps/currencies/models.py:119 apps/transactions/filters.py:27
#: apps/transactions/models.py:227 apps/transactions/models.py:251
#: apps/transactions/models.py:275 templates/categories/fragments/list.html:16
#: apps/transactions/models.py:242 apps/transactions/models.py:266
#: apps/transactions/models.py:290 templates/categories/fragments/list.html:16
#: templates/entities/fragments/list.html:16
#: templates/installment_plans/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/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/models.py:327 apps/transactions/models.py:650
#: apps/transactions/models.py:851 apps/transactions/models.py:1085
#: apps/transactions/models.py:342 apps/transactions/models.py:667
#: apps/transactions/models.py:868 apps/transactions/models.py:1106
msgid "Notes"
msgstr ""
@@ -779,27 +767,27 @@ msgstr ""
msgid "DCA Entries"
msgstr ""
#: apps/dca/views.py:38
#: apps/dca/views.py:44
msgid "DCA Strategy added successfully"
msgstr ""
#: apps/dca/views.py:75
#: apps/dca/views.py:73
msgid "DCA Strategy updated successfully"
msgstr ""
#: apps/dca/views.py:107
#: apps/dca/views.py:101
msgid "DCA strategy deleted successfully"
msgstr ""
#: apps/dca/views.py:237
#: apps/dca/views.py:236
msgid "Entry added successfully"
msgstr ""
#: apps/dca/views.py:264
#: apps/dca/views.py:270
msgid "Entry updated successfully"
msgstr ""
#: apps/dca/views.py:290
#: apps/dca/views.py:303
msgid "Entry deleted successfully"
msgstr ""
@@ -810,7 +798,7 @@ msgid "Users"
msgstr ""
#: 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/recurring_transactions/fragments/list_transactions.html:5
#: templates/recurring_transactions/fragments/table.html:37
@@ -819,7 +807,7 @@ msgid "Transactions"
msgstr ""
#: 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/insights/fragments/month_by_month.html:18
#: 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/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:491 apps/transactions/forms.py:771
#: apps/transactions/forms.py:1015 apps/transactions/models.py:286
#: apps/transactions/models.py:342 apps/transactions/models.py:646
#: apps/transactions/models.py:848 apps/transactions/models.py:1100
#: apps/transactions/forms.py:1015 apps/transactions/models.py:301
#: apps/transactions/models.py:357 apps/transactions/models.py:663
#: apps/transactions/models.py:865 apps/transactions/models.py:1121
#: templates/entities/fragments/list.html:9
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
#: templates/insights/fragments/category_overview/index.html:54
@@ -847,14 +835,14 @@ msgid "Entities"
msgstr ""
#: 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/pages/index.html:4
msgid "Recurring Transactions"
msgstr ""
#: 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/pages/index.html:4
msgid "Installment Plans"
@@ -1006,7 +994,7 @@ msgid "Run deleted successfully"
msgstr ""
#: 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:407
#: 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/models.py:271 apps/transactions/forms.py:433
#: apps/transactions/models.py:310 apps/transactions/models.py:606
#: apps/transactions/models.py:829 apps/transactions/models.py:1070
#: apps/transactions/models.py:325 apps/transactions/models.py:623
#: apps/transactions/models.py:846 apps/transactions/models.py:1091
msgid "Type"
msgstr ""
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
#: apps/rules/models.py:275 apps/transactions/filters.py:22
#: apps/transactions/forms.py:437 apps/transactions/models.py:312
#: apps/transactions/models.py:1072 templates/cotton/transaction/item.html:20
#: apps/transactions/forms.py:437 apps/transactions/models.py:327
#: apps/transactions/models.py:1093 templates/cotton/transaction/item.html:20
#: templates/cotton/transaction/item.html:31
#: templates/transactions/widgets/paid_toggle_button.html:10
#: 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/models.py:283 apps/transactions/forms.py:89
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
#: apps/transactions/forms.py:777 apps/transactions/models.py:314
#: apps/transactions/models.py:624 apps/transactions/models.py:853
#: apps/transactions/forms.py:777 apps/transactions/models.py:329
#: apps/transactions/models.py:641 apps/transactions/models.py:870
msgid "Reference Date"
msgstr ""
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
#: apps/rules/models.py:287 apps/transactions/forms.py:460
#: apps/transactions/models.py:320 apps/transactions/models.py:834
#: apps/transactions/models.py:1078
#: apps/transactions/models.py:335 apps/transactions/models.py:851
#: apps/transactions/models.py:1099
#: templates/insights/fragments/sankey.html:102
#: templates/installment_plans/fragments/table.html:18
#: 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/models.py:42 apps/rules/models.py:291
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
#: apps/transactions/models.py:325 apps/transactions/models.py:608
#: apps/transactions/models.py:837 apps/transactions/models.py:1083
#: apps/transactions/models.py:340 apps/transactions/models.py:625
#: apps/transactions/models.py:854 apps/transactions/models.py:1104
msgid "Description"
msgstr ""
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
#: apps/rules/models.py:299 apps/transactions/models.py:364
#: apps/transactions/models.py:1105
#: apps/rules/models.py:299 apps/transactions/models.py:379
#: apps/transactions/models.py:1126
msgid "Internal Note"
msgstr ""
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
#: apps/rules/models.py:303 apps/transactions/models.py:366
#: apps/transactions/models.py:1107
#: apps/rules/models.py:303 apps/transactions/models.py:381
#: apps/transactions/models.py:1128
msgid "Internal ID"
msgstr ""
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
#: apps/rules/models.py:319 apps/transactions/forms.py:620
#: apps/transactions/models.py:224 apps/transactions/models.py:315
#: apps/transactions/models.py:1073
#: apps/transactions/models.py:239 apps/transactions/models.py:330
#: apps/transactions/models.py:1094
msgid "Mute"
msgstr ""
@@ -1174,7 +1162,7 @@ msgid "Set Values"
msgstr ""
#: 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"
msgstr ""
@@ -1268,15 +1256,15 @@ msgstr ""
msgid "Update or create transaction action"
msgstr ""
#: apps/rules/views.py:71
#: apps/rules/views.py:79
msgid "Rule deactivated successfully"
msgstr ""
#: apps/rules/views.py:73
#: apps/rules/views.py:81
msgid "Rule activated successfully"
msgstr ""
#: apps/rules/views.py:92
#: apps/rules/views.py:100
msgid "Rule added successfully"
msgstr ""
@@ -1284,27 +1272,27 @@ msgstr ""
msgid "Rule updated successfully"
msgstr ""
#: apps/rules/views.py:188
#: apps/rules/views.py:186
msgid "Rule deleted successfully"
msgstr ""
#: apps/rules/views.py:305
#: apps/rules/views.py:306
msgid "Action updated successfully"
msgstr ""
#: apps/rules/views.py:336
#: apps/rules/views.py:341
msgid "Action deleted successfully"
msgstr ""
#: apps/rules/views.py:360
#: apps/rules/views.py:367
msgid "Update or Create Transaction action added successfully"
msgstr ""
#: apps/rules/views.py:393
#: apps/rules/views.py:402
msgid "Update or Create Transaction action updated successfully"
msgstr ""
#: apps/rules/views.py:423
#: apps/rules/views.py:434
msgid "Update or Create Transaction action deleted successfully"
msgstr ""
@@ -1321,58 +1309,58 @@ msgstr ""
msgid "Muted"
msgstr ""
#: apps/transactions/filters.py:45
#: apps/transactions/filters.py:51
msgid "Content"
msgstr ""
#: apps/transactions/filters.py:51
#: apps/transactions/filters.py:57
msgid "Transaction Type"
msgstr ""
#: apps/transactions/filters.py:89
#: apps/transactions/filters.py:95
msgid "Mute Status"
msgstr ""
#: apps/transactions/filters.py:94
#: apps/transactions/filters.py:100
msgid "Date from"
msgstr ""
#: apps/transactions/filters.py:99 apps/transactions/filters.py:109
#: apps/transactions/filters.py:105 apps/transactions/filters.py:115
msgid "Until"
msgstr ""
#: apps/transactions/filters.py:104
#: apps/transactions/filters.py:110
msgid "Reference date from"
msgstr ""
#: apps/transactions/filters.py:114
#: apps/transactions/filters.py:120
msgid "Amount min"
msgstr ""
#: apps/transactions/filters.py:119
#: apps/transactions/filters.py:125
msgid "Amount max"
msgstr ""
#: apps/transactions/filters.py:202
#: apps/transactions/filters.py:208
msgid "Categorized"
msgstr ""
#: apps/transactions/filters.py:209
#: apps/transactions/filters.py:215
msgid "Tagged"
msgstr ""
#: apps/transactions/filters.py:209
#: apps/transactions/filters.py:215
#: templates/insights/fragments/category_overview/index.html:189
#: templates/insights/fragments/month_by_month.html:121
#: templates/insights/fragments/year_by_year.html:75
msgid "Untagged"
msgstr ""
#: apps/transactions/filters.py:215
#: apps/transactions/filters.py:221
msgid "Any entity"
msgstr ""
#: apps/transactions/filters.py:216
#: apps/transactions/filters.py:222
#: templates/insights/fragments/category_overview/index.html:282
#: templates/insights/fragments/month_by_month.html:123
#: templates/insights/fragments/year_by_year.html:77
@@ -1447,42 +1435,42 @@ msgstr ""
msgid "End date should be after the start date"
msgstr ""
#: apps/transactions/models.py:229
#: apps/transactions/models.py:244
msgid ""
"Deactivated categories won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:237
#: apps/transactions/models.py:252
msgid "Transaction Category"
msgstr ""
#: apps/transactions/models.py:238
#: apps/transactions/models.py:253
msgid "Transaction Categories"
msgstr ""
#: apps/transactions/models.py:253
#: apps/transactions/models.py:268
msgid ""
"Deactivated tags won't be able to be selected when creating new transactions"
msgstr ""
#: apps/transactions/models.py:261 apps/transactions/models.py:262
#: apps/transactions/models.py:276 apps/transactions/models.py:277
msgid "Transaction Tags"
msgstr ""
#: apps/transactions/models.py:277
#: apps/transactions/models.py:292
msgid ""
"Deactivated entities won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:285
#: apps/transactions/models.py:300
#: templates/insights/fragments/month_by_month.html:88
#: templates/insights/fragments/year_by_year.html:56
msgid "Entity"
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:44
#: templates/calendar_view/fragments/list.html:52
@@ -1494,7 +1482,7 @@ msgstr ""
msgid "Income"
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:48
#: templates/calendar_view/fragments/list.html:56
@@ -1505,157 +1493,157 @@ msgstr ""
msgid "Expense"
msgstr ""
#: apps/transactions/models.py:353 apps/transactions/models.py:663
#: apps/transactions/models.py:368 apps/transactions/models.py:680
msgid "Installment Plan"
msgstr ""
#: apps/transactions/models.py:362 apps/transactions/models.py:887
#: apps/transactions/models.py:377 apps/transactions/models.py:904
msgid "Recurring Transaction"
msgstr ""
#: apps/transactions/models.py:370
#: apps/transactions/models.py:385
msgid "Deleted"
msgstr ""
#: apps/transactions/models.py:375
#: apps/transactions/models.py:390
msgid "Deleted At"
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"
msgstr ""
#: apps/transactions/models.py:491
#: apps/transactions/models.py:506
msgid "No category"
msgstr ""
#: apps/transactions/models.py:493
#: apps/transactions/models.py:508
msgid "No description"
msgstr ""
#: apps/transactions/models.py:549
#: apps/transactions/models.py:565
msgid "File"
msgstr ""
#: apps/transactions/models.py:551
#: apps/transactions/models.py:567
msgid "Original Name"
msgstr ""
#: apps/transactions/models.py:553
#: apps/transactions/models.py:569
msgid "Content Type"
msgstr ""
#: apps/transactions/models.py:555
#: apps/transactions/models.py:571
msgid "Size"
msgstr ""
#: apps/transactions/models.py:560
#: apps/transactions/models.py:576
msgid "Uploaded By"
msgstr ""
#: apps/transactions/models.py:565
#: apps/transactions/models.py:581
msgid "Transaction Attachment"
msgstr ""
#: apps/transactions/models.py:566
#: apps/transactions/models.py:582
msgid "Transaction Attachments"
msgstr ""
#: apps/transactions/models.py:595 templates/includes/sidebar.html:57
#: apps/transactions/models.py:612 templates/includes/sidebar.html:57
msgid "Yearly"
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
msgid "Monthly"
msgstr ""
#: apps/transactions/models.py:597
#: apps/transactions/models.py:614
msgid "Weekly"
msgstr ""
#: apps/transactions/models.py:598
#: apps/transactions/models.py:615
msgid "Daily"
msgstr ""
#: apps/transactions/models.py:611
#: apps/transactions/models.py:628
msgid "Number of Installments"
msgstr ""
#: apps/transactions/models.py:616
#: apps/transactions/models.py:633
msgid "Installment Start"
msgstr ""
#: apps/transactions/models.py:617
#: apps/transactions/models.py:634
msgid "The installment number to start counting from"
msgstr ""
#: apps/transactions/models.py:622 apps/transactions/models.py:857
#: apps/transactions/models.py:639 apps/transactions/models.py:874
msgid "Start Date"
msgstr ""
#: apps/transactions/models.py:626 apps/transactions/models.py:858
#: apps/transactions/models.py:643 apps/transactions/models.py:875
msgid "End Date"
msgstr ""
#: apps/transactions/models.py:631
#: apps/transactions/models.py:648
msgid "Recurrence"
msgstr ""
#: apps/transactions/models.py:634
#: apps/transactions/models.py:651
msgid "Installment Amount"
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"
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"
msgstr ""
#: apps/transactions/models.py:816
#: apps/transactions/models.py:833
msgid "day(s)"
msgstr ""
#: apps/transactions/models.py:817
#: apps/transactions/models.py:834
msgid "week(s)"
msgstr ""
#: apps/transactions/models.py:818
#: apps/transactions/models.py:835
msgid "month(s)"
msgstr ""
#: apps/transactions/models.py:819
#: apps/transactions/models.py:836
msgid "year(s)"
msgstr ""
#: apps/transactions/models.py:821
#: apps/transactions/models.py:838
#: templates/recurring_transactions/fragments/list.html:18
msgid "Paused"
msgstr ""
#: apps/transactions/models.py:860
#: apps/transactions/models.py:877
msgid "Recurrence Type"
msgstr ""
#: apps/transactions/models.py:863
#: apps/transactions/models.py:880
msgid "Recurrence Interval"
msgstr ""
#: apps/transactions/models.py:866
#: apps/transactions/models.py:883
msgid "Keep at most"
msgstr ""
#: apps/transactions/models.py:870
#: apps/transactions/models.py:887
msgid "Last Generated Date"
msgstr ""
#: apps/transactions/models.py:873
#: apps/transactions/models.py:890
msgid "Last Generated Reference Date"
msgstr ""
#: apps/transactions/models.py:1117
#: apps/transactions/models.py:1138
#: apps/transactions/views/quick_transactions.py:178
#: apps/transactions/views/quick_transactions.py:187
#: apps/transactions/views/quick_transactions.py:189
@@ -1664,7 +1652,7 @@ msgstr ""
msgid "Quick Transaction"
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:15
msgid "Quick Transactions"
@@ -1715,27 +1703,27 @@ msgid_plural "%(count)s transactions duplicated successfully"
msgstr[0] ""
msgstr[1] ""
#: apps/transactions/views/categories.py:66
#: apps/transactions/views/categories.py:72
msgid "Category added successfully"
msgstr ""
#: apps/transactions/views/categories.py:104
#: apps/transactions/views/categories.py:102
msgid "Category updated successfully"
msgstr ""
#: apps/transactions/views/categories.py:171
#: apps/transactions/views/categories.py:158
msgid "Category deleted successfully"
msgstr ""
#: apps/transactions/views/entities.py:66
#: apps/transactions/views/entities.py:72
msgid "Entity added successfully"
msgstr ""
#: apps/transactions/views/entities.py:104
#: apps/transactions/views/entities.py:102
msgid "Entity updated successfully"
msgstr ""
#: apps/transactions/views/entities.py:133
#: apps/transactions/views/entities.py:130
msgid "Entity deleted successfully"
msgstr ""
@@ -1798,15 +1786,15 @@ msgstr ""
msgid "Recurring Transaction deleted successfully"
msgstr ""
#: apps/transactions/views/tags.py:66
#: apps/transactions/views/tags.py:72
msgid "Tag added successfully"
msgstr ""
#: apps/transactions/views/tags.py:104
#: apps/transactions/views/tags.py:100
msgid "Tag updated successfully"
msgstr ""
#: apps/transactions/views/tags.py:133
#: apps/transactions/views/tags.py:126
msgid "Tag deleted successfully"
msgstr ""
@@ -1888,9 +1876,9 @@ msgstr ""
#: 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:245
#: templates/monthly_overview/pages/overview.html:247
#: templates/transactions/pages/transactions.html:47
#: templates/transactions/pages/transactions.html:195
#: templates/transactions/pages/transactions.html:196
msgid "Default"
msgstr ""
@@ -2211,6 +2199,7 @@ msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:92
#: templates/rules/fragments/transaction_rule/view.html:133
#: templates/tags/fragments/table.html:51
#: templates/transactions/fragments/filter_presets.html:28
#: templates/users/fragments/api_tokens.html:93
msgid "Delete"
msgstr ""
@@ -2315,8 +2304,8 @@ msgid "Current balance"
msgstr ""
#: templates/accounts/fragments/account_reconciliation.html:36
#: templates/net_worth/net_worth.html:146
#: templates/net_worth/net_worth.html:396
#: templates/net_worth/net_worth.html:147
#: templates/net_worth/net_worth.html:407
msgid "Difference"
msgstr ""
@@ -2696,19 +2685,19 @@ msgstr ""
msgid "Amount Bought"
msgstr ""
#: templates/dca/fragments/strategy/details.html:384
#: templates/dca/fragments/strategy/details.html:409
msgid "Entry Price vs Current Price"
msgstr ""
#: templates/dca/fragments/strategy/details.html:398
#: templates/dca/fragments/strategy/details.html:423
msgid "Days Between Investments"
msgstr ""
#: templates/dca/fragments/strategy/details.html:444
#: templates/dca/fragments/strategy/details.html:469
msgid "Investment Frequency"
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."
msgstr ""
@@ -2944,6 +2933,7 @@ msgid "Reload"
msgstr ""
#: templates/includes/scripts/hyperscript/swal.html:13
#: templates/transactions/fragments/filter_actions.html:4
msgid "Cancel"
msgstr ""
@@ -3358,16 +3348,16 @@ msgid "Summary"
msgstr ""
#: 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:204
#: templates/transactions/pages/transactions.html:205
msgid "Oldest first"
msgstr ""
#: 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:213
#: templates/transactions/pages/transactions.html:214
msgid "Newest first"
msgstr ""
@@ -3376,8 +3366,8 @@ msgstr ""
msgid "Filter transactions"
msgstr ""
#: templates/monthly_overview/pages/overview.html:235
#: templates/transactions/pages/transactions.html:185
#: templates/monthly_overview/pages/overview.html:237
#: templates/transactions/pages/transactions.html:186
msgid "Order by"
msgstr ""
@@ -3386,24 +3376,24 @@ msgstr ""
msgid "By currency"
msgstr ""
#: templates/net_worth/net_worth.html:84
#: templates/net_worth/net_worth.html:84 templates/net_worth/net_worth.html:85
msgid "Consolidated"
msgstr ""
#: templates/net_worth/net_worth.html:134
#: templates/net_worth/net_worth.html:135
msgid "Evolution"
msgstr ""
#: templates/net_worth/net_worth.html:162
#: templates/net_worth/net_worth.html:163
#: templates/yearly_overview/pages/overview_by_account.html:4
msgid "By account"
msgstr ""
#: templates/net_worth/net_worth.html:270
#: templates/net_worth/net_worth.html:271
msgid "Evolution by currency"
msgstr ""
#: templates/net_worth/net_worth.html:334
#: templates/net_worth/net_worth.html:345
msgid "Evolution by account"
msgstr ""
@@ -3483,15 +3473,19 @@ msgstr ""
msgid "View"
msgstr ""
#: templates/rules/fragments/list.html:70
#: templates/rules/fragments/list.html:71
msgid "Deactivate"
msgstr ""
#: templates/rules/fragments/list.html:70
#: templates/rules/fragments/list.html:71
msgid "Activate"
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"
msgstr ""
@@ -3634,6 +3628,40 @@ msgstr ""
msgid "transactions"
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
msgid "No transactions found"
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+220 -192
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: Emil <emil.bjorkroth@gmail.com>\n"
"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:475 apps/transactions/forms.py:572
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
#: apps/transactions/forms.py:1007 apps/transactions/models.py:331
#: apps/transactions/models.py:641 apps/transactions/models.py:841
#: apps/transactions/models.py:1089
#: apps/transactions/forms.py:1007 apps/transactions/models.py:346
#: apps/transactions/models.py:658 apps/transactions/models.py:858
#: apps/transactions/models.py:1110
#: templates/insights/fragments/category_overview/index.html:86
#: templates/insights/fragments/category_overview/index.html:542
#: 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/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/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:483 apps/transactions/forms.py:588
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
#: apps/transactions/forms.py:1000 apps/transactions/models.py:337
#: apps/transactions/models.py:643 apps/transactions/models.py:845
#: apps/transactions/models.py:1095 templates/includes/sidebar.html:150
#: apps/transactions/forms.py:1000 apps/transactions/models.py:352
#: apps/transactions/models.py:660 apps/transactions/models.py:862
#: apps/transactions/models.py:1116 templates/includes/sidebar.html:150
#: templates/insights/fragments/category_overview/index.html:40
#: templates/insights/fragments/month_by_month.html:29
#: 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/import_app/models.py:14 apps/rules/models.py:13
#: apps/transactions/models.py:223 apps/transactions/models.py:248
#: apps/transactions/models.py:272 apps/transactions/models.py:1057
#: apps/transactions/models.py:238 apps/transactions/models.py:263
#: apps/transactions/models.py:287 apps/transactions/models.py:1078
#: apps/users/models.py:572 templates/account_groups/fragments/list.html:22
#: templates/accounts/fragments/list.html:22
#: templates/categories/fragments/table.html:17
@@ -166,9 +166,9 @@ msgstr ""
#: apps/rules/models.py:35 apps/rules/models.py:267
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
#: apps/transactions/forms.py:992 apps/transactions/models.py:303
#: apps/transactions/models.py:601 apps/transactions/models.py:823
#: apps/transactions/models.py:1063
#: apps/transactions/forms.py:992 apps/transactions/models.py:318
#: apps/transactions/models.py:618 apps/transactions/models.py:840
#: apps/transactions/models.py:1084
#: templates/installment_plans/fragments/table.html:17
#: templates/quick_transactions/fragments/list.html:14
#: templates/recurring_transactions/fragments/table.html:19
@@ -177,7 +177,7 @@ msgid "Account"
msgstr ""
#: 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/pages/index.html:4 templates/includes/sidebar.html:162
#: templates/includes/sidebar.html:164
@@ -190,68 +190,56 @@ msgstr ""
msgid "Exchange currency cannot be the same as the account's main currency."
msgstr ""
#: apps/accounts/views/account_groups.py:44
#: apps/accounts/views/account_groups.py:50
msgid "Account Group added successfully"
msgstr ""
#: apps/accounts/views/account_groups.py:69
#: 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
#: apps/accounts/views/account_groups.py:78
msgid "Account Group updated successfully"
msgstr ""
#: apps/accounts/views/account_groups.py:111
#: 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
#: apps/accounts/views/account_groups.py:104
msgid "Account Group deleted successfully"
msgstr ""
#: apps/accounts/views/account_groups.py:135
#: apps/accounts/views/accounts.py:189 apps/dca/views.py:128
#: apps/rules/views.py:210 apps/transactions/views/categories.py:192
#: apps/transactions/views/entities.py:154 apps/transactions/views/tags.py:154
#: apps/accounts/views/account_groups.py:109
#: apps/accounts/views/accounts.py:137 apps/dca/views.py:106
#: apps/rules/views.py:191 apps/transactions/views/categories.py:163
#: apps/transactions/views/entities.py:135 apps/transactions/views/tags.py:131
msgid "Item no longer shared with you"
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"
msgstr ""
#: apps/accounts/views/account_groups.py:165
#: apps/accounts/views/accounts.py:119 apps/dca/views.py:158
#: apps/rules/views.py:241 apps/transactions/views/categories.py:142
#: apps/transactions/views/entities.py:184 apps/transactions/views/tags.py:184
#: apps/accounts/views/account_groups.py:152
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:151
#: apps/rules/views.py:238 apps/transactions/views/categories.py:130
#: apps/transactions/views/entities.py:180 apps/transactions/views/tags.py:174
msgid "Configuration saved successfully"
msgstr ""
#: apps/accounts/views/accounts.py:44
#: apps/accounts/views/accounts.py:50
msgid "Account added successfully"
msgstr ""
#: apps/accounts/views/accounts.py:81
#: apps/accounts/views/accounts.py:78
msgid "Account updated successfully"
msgstr ""
#: apps/accounts/views/accounts.py:148
#: apps/accounts/views/accounts.py:132
msgid "Account deleted successfully"
msgstr ""
#: apps/accounts/views/accounts.py:165
#: apps/accounts/views/accounts.py:158
msgid "Account is now tracked"
msgstr ""
#: apps/accounts/views/accounts.py:168
#: apps/accounts/views/accounts.py:161
msgid "Account is now untracked"
msgstr ""
@@ -346,6 +334,7 @@ msgid ""
msgstr ""
#: apps/common/forms.py:76 apps/users/forms.py:172
#: templates/transactions/fragments/filter_actions.html:5
msgid "Save"
msgstr ""
@@ -460,10 +449,9 @@ msgstr ""
#: apps/common/widgets/tom_select.py:15
#: templates/mini_tools/unit_price_calculator.html:180
#: templates/monthly_overview/pages/overview.html:275
#: templates/monthly_overview/pages/overview.html:287
#: templates/transactions/pages/transactions.html:225
#: templates/transactions/pages/transactions.html:237
#: templates/monthly_overview/pages/overview.html:284
#: templates/transactions/fragments/filter_actions.html:48
#: templates/transactions/pages/transactions.html:233
msgid "Clear"
msgstr ""
@@ -482,7 +470,7 @@ msgstr ""
#: 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/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/exchange_rates/fragments/table.html:10
#: templates/exchange_rates_services/fragments/table.html:11
@@ -502,7 +490,7 @@ msgid "Decimal Places"
msgstr ""
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
#: apps/export_app/forms.py:130 apps/transactions/filters.py:64
#: apps/export_app/forms.py:130 apps/transactions/filters.py:70
#: templates/currencies/fragments/list.html:9
#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:176
#: templates/includes/sidebar.html:178
@@ -568,8 +556,8 @@ msgid "Service Type"
msgstr ""
#: apps/currencies/models.py:119 apps/transactions/filters.py:27
#: apps/transactions/models.py:227 apps/transactions/models.py:251
#: apps/transactions/models.py:275 templates/categories/fragments/list.html:16
#: apps/transactions/models.py:242 apps/transactions/models.py:266
#: apps/transactions/models.py:290 templates/categories/fragments/list.html:16
#: templates/entities/fragments/list.html:16
#: templates/installment_plans/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/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/models.py:327 apps/transactions/models.py:650
#: apps/transactions/models.py:851 apps/transactions/models.py:1085
#: apps/transactions/models.py:342 apps/transactions/models.py:667
#: apps/transactions/models.py:868 apps/transactions/models.py:1106
msgid "Notes"
msgstr ""
@@ -781,27 +769,27 @@ msgstr ""
msgid "DCA Entries"
msgstr ""
#: apps/dca/views.py:38
#: apps/dca/views.py:44
msgid "DCA Strategy added successfully"
msgstr ""
#: apps/dca/views.py:75
#: apps/dca/views.py:73
msgid "DCA Strategy updated successfully"
msgstr ""
#: apps/dca/views.py:107
#: apps/dca/views.py:101
msgid "DCA strategy deleted successfully"
msgstr ""
#: apps/dca/views.py:237
#: apps/dca/views.py:236
msgid "Entry added successfully"
msgstr ""
#: apps/dca/views.py:264
#: apps/dca/views.py:270
msgid "Entry updated successfully"
msgstr ""
#: apps/dca/views.py:290
#: apps/dca/views.py:303
msgid "Entry deleted successfully"
msgstr ""
@@ -812,7 +800,7 @@ msgid "Users"
msgstr ""
#: 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/recurring_transactions/fragments/list_transactions.html:5
#: templates/recurring_transactions/fragments/table.html:37
@@ -821,7 +809,7 @@ msgid "Transactions"
msgstr ""
#: 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/insights/fragments/month_by_month.html:18
#: 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/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:491 apps/transactions/forms.py:771
#: apps/transactions/forms.py:1015 apps/transactions/models.py:286
#: apps/transactions/models.py:342 apps/transactions/models.py:646
#: apps/transactions/models.py:848 apps/transactions/models.py:1100
#: apps/transactions/forms.py:1015 apps/transactions/models.py:301
#: apps/transactions/models.py:357 apps/transactions/models.py:663
#: apps/transactions/models.py:865 apps/transactions/models.py:1121
#: templates/entities/fragments/list.html:9
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
#: templates/insights/fragments/category_overview/index.html:54
@@ -849,14 +837,14 @@ msgid "Entities"
msgstr ""
#: 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/pages/index.html:4
msgid "Recurring Transactions"
msgstr ""
#: 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/pages/index.html:4
msgid "Installment Plans"
@@ -1008,7 +996,7 @@ msgid "Run deleted successfully"
msgstr ""
#: 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:407
#: 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/models.py:271 apps/transactions/forms.py:433
#: apps/transactions/models.py:310 apps/transactions/models.py:606
#: apps/transactions/models.py:829 apps/transactions/models.py:1070
#: apps/transactions/models.py:325 apps/transactions/models.py:623
#: apps/transactions/models.py:846 apps/transactions/models.py:1091
msgid "Type"
msgstr ""
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
#: apps/rules/models.py:275 apps/transactions/filters.py:22
#: apps/transactions/forms.py:437 apps/transactions/models.py:312
#: apps/transactions/models.py:1072 templates/cotton/transaction/item.html:20
#: apps/transactions/forms.py:437 apps/transactions/models.py:327
#: apps/transactions/models.py:1093 templates/cotton/transaction/item.html:20
#: templates/cotton/transaction/item.html:31
#: templates/transactions/widgets/paid_toggle_button.html:10
#: 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/models.py:283 apps/transactions/forms.py:89
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
#: apps/transactions/forms.py:777 apps/transactions/models.py:314
#: apps/transactions/models.py:624 apps/transactions/models.py:853
#: apps/transactions/forms.py:777 apps/transactions/models.py:329
#: apps/transactions/models.py:641 apps/transactions/models.py:870
msgid "Reference Date"
msgstr ""
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
#: apps/rules/models.py:287 apps/transactions/forms.py:460
#: apps/transactions/models.py:320 apps/transactions/models.py:834
#: apps/transactions/models.py:1078
#: apps/transactions/models.py:335 apps/transactions/models.py:851
#: apps/transactions/models.py:1099
#: templates/insights/fragments/sankey.html:102
#: templates/installment_plans/fragments/table.html:18
#: 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/models.py:42 apps/rules/models.py:291
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
#: apps/transactions/models.py:325 apps/transactions/models.py:608
#: apps/transactions/models.py:837 apps/transactions/models.py:1083
#: apps/transactions/models.py:340 apps/transactions/models.py:625
#: apps/transactions/models.py:854 apps/transactions/models.py:1104
msgid "Description"
msgstr ""
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
#: apps/rules/models.py:299 apps/transactions/models.py:364
#: apps/transactions/models.py:1105
#: apps/rules/models.py:299 apps/transactions/models.py:379
#: apps/transactions/models.py:1126
msgid "Internal Note"
msgstr ""
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
#: apps/rules/models.py:303 apps/transactions/models.py:366
#: apps/transactions/models.py:1107
#: apps/rules/models.py:303 apps/transactions/models.py:381
#: apps/transactions/models.py:1128
msgid "Internal ID"
msgstr ""
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
#: apps/rules/models.py:319 apps/transactions/forms.py:620
#: apps/transactions/models.py:224 apps/transactions/models.py:315
#: apps/transactions/models.py:1073
#: apps/transactions/models.py:239 apps/transactions/models.py:330
#: apps/transactions/models.py:1094
msgid "Mute"
msgstr ""
@@ -1176,7 +1164,7 @@ msgid "Set Values"
msgstr ""
#: 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"
msgstr ""
@@ -1270,15 +1258,15 @@ msgstr ""
msgid "Update or create transaction action"
msgstr ""
#: apps/rules/views.py:71
#: apps/rules/views.py:79
msgid "Rule deactivated successfully"
msgstr ""
#: apps/rules/views.py:73
#: apps/rules/views.py:81
msgid "Rule activated successfully"
msgstr ""
#: apps/rules/views.py:92
#: apps/rules/views.py:100
msgid "Rule added successfully"
msgstr ""
@@ -1286,27 +1274,27 @@ msgstr ""
msgid "Rule updated successfully"
msgstr ""
#: apps/rules/views.py:188
#: apps/rules/views.py:186
msgid "Rule deleted successfully"
msgstr ""
#: apps/rules/views.py:305
#: apps/rules/views.py:306
msgid "Action updated successfully"
msgstr ""
#: apps/rules/views.py:336
#: apps/rules/views.py:341
msgid "Action deleted successfully"
msgstr ""
#: apps/rules/views.py:360
#: apps/rules/views.py:367
msgid "Update or Create Transaction action added successfully"
msgstr ""
#: apps/rules/views.py:393
#: apps/rules/views.py:402
msgid "Update or Create Transaction action updated successfully"
msgstr ""
#: apps/rules/views.py:423
#: apps/rules/views.py:434
msgid "Update or Create Transaction action deleted successfully"
msgstr ""
@@ -1323,58 +1311,58 @@ msgstr ""
msgid "Muted"
msgstr ""
#: apps/transactions/filters.py:45
#: apps/transactions/filters.py:51
msgid "Content"
msgstr ""
#: apps/transactions/filters.py:51
#: apps/transactions/filters.py:57
msgid "Transaction Type"
msgstr ""
#: apps/transactions/filters.py:89
#: apps/transactions/filters.py:95
msgid "Mute Status"
msgstr ""
#: apps/transactions/filters.py:94
#: apps/transactions/filters.py:100
msgid "Date from"
msgstr ""
#: apps/transactions/filters.py:99 apps/transactions/filters.py:109
#: apps/transactions/filters.py:105 apps/transactions/filters.py:115
msgid "Until"
msgstr ""
#: apps/transactions/filters.py:104
#: apps/transactions/filters.py:110
msgid "Reference date from"
msgstr ""
#: apps/transactions/filters.py:114
#: apps/transactions/filters.py:120
msgid "Amount min"
msgstr ""
#: apps/transactions/filters.py:119
#: apps/transactions/filters.py:125
msgid "Amount max"
msgstr ""
#: apps/transactions/filters.py:202
#: apps/transactions/filters.py:208
msgid "Categorized"
msgstr ""
#: apps/transactions/filters.py:209
#: apps/transactions/filters.py:215
msgid "Tagged"
msgstr ""
#: apps/transactions/filters.py:209
#: apps/transactions/filters.py:215
#: templates/insights/fragments/category_overview/index.html:189
#: templates/insights/fragments/month_by_month.html:121
#: templates/insights/fragments/year_by_year.html:75
msgid "Untagged"
msgstr ""
#: apps/transactions/filters.py:215
#: apps/transactions/filters.py:221
msgid "Any entity"
msgstr ""
#: apps/transactions/filters.py:216
#: apps/transactions/filters.py:222
#: templates/insights/fragments/category_overview/index.html:282
#: templates/insights/fragments/month_by_month.html:123
#: templates/insights/fragments/year_by_year.html:77
@@ -1449,42 +1437,42 @@ msgstr ""
msgid "End date should be after the start date"
msgstr ""
#: apps/transactions/models.py:229
#: apps/transactions/models.py:244
msgid ""
"Deactivated categories won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:237
#: apps/transactions/models.py:252
msgid "Transaction Category"
msgstr ""
#: apps/transactions/models.py:238
#: apps/transactions/models.py:253
msgid "Transaction Categories"
msgstr ""
#: apps/transactions/models.py:253
#: apps/transactions/models.py:268
msgid ""
"Deactivated tags won't be able to be selected when creating new transactions"
msgstr ""
#: apps/transactions/models.py:261 apps/transactions/models.py:262
#: apps/transactions/models.py:276 apps/transactions/models.py:277
msgid "Transaction Tags"
msgstr ""
#: apps/transactions/models.py:277
#: apps/transactions/models.py:292
msgid ""
"Deactivated entities won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:285
#: apps/transactions/models.py:300
#: templates/insights/fragments/month_by_month.html:88
#: templates/insights/fragments/year_by_year.html:56
msgid "Entity"
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:44
#: templates/calendar_view/fragments/list.html:52
@@ -1496,7 +1484,7 @@ msgstr ""
msgid "Income"
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:48
#: templates/calendar_view/fragments/list.html:56
@@ -1507,157 +1495,157 @@ msgstr ""
msgid "Expense"
msgstr ""
#: apps/transactions/models.py:353 apps/transactions/models.py:663
#: apps/transactions/models.py:368 apps/transactions/models.py:680
msgid "Installment Plan"
msgstr ""
#: apps/transactions/models.py:362 apps/transactions/models.py:887
#: apps/transactions/models.py:377 apps/transactions/models.py:904
msgid "Recurring Transaction"
msgstr ""
#: apps/transactions/models.py:370
#: apps/transactions/models.py:385
msgid "Deleted"
msgstr ""
#: apps/transactions/models.py:375
#: apps/transactions/models.py:390
msgid "Deleted At"
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"
msgstr ""
#: apps/transactions/models.py:491
#: apps/transactions/models.py:506
msgid "No category"
msgstr ""
#: apps/transactions/models.py:493
#: apps/transactions/models.py:508
msgid "No description"
msgstr ""
#: apps/transactions/models.py:549
#: apps/transactions/models.py:565
msgid "File"
msgstr ""
#: apps/transactions/models.py:551
#: apps/transactions/models.py:567
msgid "Original Name"
msgstr ""
#: apps/transactions/models.py:553
#: apps/transactions/models.py:569
msgid "Content Type"
msgstr ""
#: apps/transactions/models.py:555
#: apps/transactions/models.py:571
msgid "Size"
msgstr ""
#: apps/transactions/models.py:560
#: apps/transactions/models.py:576
msgid "Uploaded By"
msgstr ""
#: apps/transactions/models.py:565
#: apps/transactions/models.py:581
msgid "Transaction Attachment"
msgstr ""
#: apps/transactions/models.py:566
#: apps/transactions/models.py:582
msgid "Transaction Attachments"
msgstr ""
#: apps/transactions/models.py:595 templates/includes/sidebar.html:57
#: apps/transactions/models.py:612 templates/includes/sidebar.html:57
msgid "Yearly"
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
msgid "Monthly"
msgstr ""
#: apps/transactions/models.py:597
#: apps/transactions/models.py:614
msgid "Weekly"
msgstr ""
#: apps/transactions/models.py:598
#: apps/transactions/models.py:615
msgid "Daily"
msgstr ""
#: apps/transactions/models.py:611
#: apps/transactions/models.py:628
msgid "Number of Installments"
msgstr ""
#: apps/transactions/models.py:616
#: apps/transactions/models.py:633
msgid "Installment Start"
msgstr ""
#: apps/transactions/models.py:617
#: apps/transactions/models.py:634
msgid "The installment number to start counting from"
msgstr ""
#: apps/transactions/models.py:622 apps/transactions/models.py:857
#: apps/transactions/models.py:639 apps/transactions/models.py:874
msgid "Start Date"
msgstr ""
#: apps/transactions/models.py:626 apps/transactions/models.py:858
#: apps/transactions/models.py:643 apps/transactions/models.py:875
msgid "End Date"
msgstr ""
#: apps/transactions/models.py:631
#: apps/transactions/models.py:648
msgid "Recurrence"
msgstr ""
#: apps/transactions/models.py:634
#: apps/transactions/models.py:651
msgid "Installment Amount"
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"
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"
msgstr ""
#: apps/transactions/models.py:816
#: apps/transactions/models.py:833
msgid "day(s)"
msgstr ""
#: apps/transactions/models.py:817
#: apps/transactions/models.py:834
msgid "week(s)"
msgstr ""
#: apps/transactions/models.py:818
#: apps/transactions/models.py:835
msgid "month(s)"
msgstr ""
#: apps/transactions/models.py:819
#: apps/transactions/models.py:836
msgid "year(s)"
msgstr ""
#: apps/transactions/models.py:821
#: apps/transactions/models.py:838
#: templates/recurring_transactions/fragments/list.html:18
msgid "Paused"
msgstr ""
#: apps/transactions/models.py:860
#: apps/transactions/models.py:877
msgid "Recurrence Type"
msgstr ""
#: apps/transactions/models.py:863
#: apps/transactions/models.py:880
msgid "Recurrence Interval"
msgstr ""
#: apps/transactions/models.py:866
#: apps/transactions/models.py:883
msgid "Keep at most"
msgstr ""
#: apps/transactions/models.py:870
#: apps/transactions/models.py:887
msgid "Last Generated Date"
msgstr ""
#: apps/transactions/models.py:873
#: apps/transactions/models.py:890
msgid "Last Generated Reference Date"
msgstr ""
#: apps/transactions/models.py:1117
#: apps/transactions/models.py:1138
#: apps/transactions/views/quick_transactions.py:178
#: apps/transactions/views/quick_transactions.py:187
#: apps/transactions/views/quick_transactions.py:189
@@ -1666,7 +1654,7 @@ msgstr ""
msgid "Quick Transaction"
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:15
msgid "Quick Transactions"
@@ -1717,27 +1705,27 @@ msgid_plural "%(count)s transactions duplicated successfully"
msgstr[0] ""
msgstr[1] ""
#: apps/transactions/views/categories.py:66
#: apps/transactions/views/categories.py:72
msgid "Category added successfully"
msgstr ""
#: apps/transactions/views/categories.py:104
#: apps/transactions/views/categories.py:102
msgid "Category updated successfully"
msgstr ""
#: apps/transactions/views/categories.py:171
#: apps/transactions/views/categories.py:158
msgid "Category deleted successfully"
msgstr ""
#: apps/transactions/views/entities.py:66
#: apps/transactions/views/entities.py:72
msgid "Entity added successfully"
msgstr ""
#: apps/transactions/views/entities.py:104
#: apps/transactions/views/entities.py:102
msgid "Entity updated successfully"
msgstr ""
#: apps/transactions/views/entities.py:133
#: apps/transactions/views/entities.py:130
msgid "Entity deleted successfully"
msgstr ""
@@ -1800,15 +1788,15 @@ msgstr ""
msgid "Recurring Transaction deleted successfully"
msgstr ""
#: apps/transactions/views/tags.py:66
#: apps/transactions/views/tags.py:72
msgid "Tag added successfully"
msgstr ""
#: apps/transactions/views/tags.py:104
#: apps/transactions/views/tags.py:100
msgid "Tag updated successfully"
msgstr ""
#: apps/transactions/views/tags.py:133
#: apps/transactions/views/tags.py:126
msgid "Tag deleted successfully"
msgstr ""
@@ -1890,9 +1878,9 @@ msgstr ""
#: 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:245
#: templates/monthly_overview/pages/overview.html:247
#: templates/transactions/pages/transactions.html:47
#: templates/transactions/pages/transactions.html:195
#: templates/transactions/pages/transactions.html:196
msgid "Default"
msgstr ""
@@ -2215,6 +2203,7 @@ msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:92
#: templates/rules/fragments/transaction_rule/view.html:133
#: templates/tags/fragments/table.html:51
#: templates/transactions/fragments/filter_presets.html:28
#: templates/users/fragments/api_tokens.html:93
msgid "Delete"
msgstr ""
@@ -2319,8 +2308,8 @@ msgid "Current balance"
msgstr ""
#: templates/accounts/fragments/account_reconciliation.html:36
#: templates/net_worth/net_worth.html:146
#: templates/net_worth/net_worth.html:396
#: templates/net_worth/net_worth.html:147
#: templates/net_worth/net_worth.html:407
msgid "Difference"
msgstr ""
@@ -2700,19 +2689,19 @@ msgstr ""
msgid "Amount Bought"
msgstr ""
#: templates/dca/fragments/strategy/details.html:384
#: templates/dca/fragments/strategy/details.html:409
msgid "Entry Price vs Current Price"
msgstr ""
#: templates/dca/fragments/strategy/details.html:398
#: templates/dca/fragments/strategy/details.html:423
msgid "Days Between Investments"
msgstr ""
#: templates/dca/fragments/strategy/details.html:444
#: templates/dca/fragments/strategy/details.html:469
msgid "Investment Frequency"
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."
msgstr ""
@@ -2949,6 +2938,7 @@ msgid "Reload"
msgstr ""
#: templates/includes/scripts/hyperscript/swal.html:13
#: templates/transactions/fragments/filter_actions.html:4
msgid "Cancel"
msgstr ""
@@ -3363,16 +3353,16 @@ msgid "Summary"
msgstr ""
#: 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:204
#: templates/transactions/pages/transactions.html:205
msgid "Oldest first"
msgstr ""
#: 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:213
#: templates/transactions/pages/transactions.html:214
msgid "Newest first"
msgstr ""
@@ -3381,8 +3371,8 @@ msgstr ""
msgid "Filter transactions"
msgstr ""
#: templates/monthly_overview/pages/overview.html:235
#: templates/transactions/pages/transactions.html:185
#: templates/monthly_overview/pages/overview.html:237
#: templates/transactions/pages/transactions.html:186
msgid "Order by"
msgstr ""
@@ -3391,24 +3381,24 @@ msgstr ""
msgid "By currency"
msgstr ""
#: templates/net_worth/net_worth.html:84
#: templates/net_worth/net_worth.html:84 templates/net_worth/net_worth.html:85
msgid "Consolidated"
msgstr ""
#: templates/net_worth/net_worth.html:134
#: templates/net_worth/net_worth.html:135
msgid "Evolution"
msgstr ""
#: templates/net_worth/net_worth.html:162
#: templates/net_worth/net_worth.html:163
#: templates/yearly_overview/pages/overview_by_account.html:4
msgid "By account"
msgstr ""
#: templates/net_worth/net_worth.html:270
#: templates/net_worth/net_worth.html:271
msgid "Evolution by currency"
msgstr ""
#: templates/net_worth/net_worth.html:334
#: templates/net_worth/net_worth.html:345
msgid "Evolution by account"
msgstr ""
@@ -3488,15 +3478,19 @@ msgstr ""
msgid "View"
msgstr ""
#: templates/rules/fragments/list.html:70
#: templates/rules/fragments/list.html:71
msgid "Deactivate"
msgstr ""
#: templates/rules/fragments/list.html:70
#: templates/rules/fragments/list.html:71
msgid "Activate"
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"
msgstr ""
@@ -3639,6 +3633,40 @@ msgstr ""
msgid "transactions"
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
msgid "No transactions found"
msgstr ""
+220 -192
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: Ebrahim Tayabali <ebrahimhakimuddin@gmail.com>\n"
"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:475 apps/transactions/forms.py:572
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
#: apps/transactions/forms.py:1007 apps/transactions/models.py:331
#: apps/transactions/models.py:641 apps/transactions/models.py:841
#: apps/transactions/models.py:1089
#: apps/transactions/forms.py:1007 apps/transactions/models.py:346
#: apps/transactions/models.py:658 apps/transactions/models.py:858
#: apps/transactions/models.py:1110
#: templates/insights/fragments/category_overview/index.html:86
#: templates/insights/fragments/category_overview/index.html:542
#: 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/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/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:483 apps/transactions/forms.py:588
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
#: apps/transactions/forms.py:1000 apps/transactions/models.py:337
#: apps/transactions/models.py:643 apps/transactions/models.py:845
#: apps/transactions/models.py:1095 templates/includes/sidebar.html:150
#: apps/transactions/forms.py:1000 apps/transactions/models.py:352
#: apps/transactions/models.py:660 apps/transactions/models.py:862
#: apps/transactions/models.py:1116 templates/includes/sidebar.html:150
#: templates/insights/fragments/category_overview/index.html:40
#: templates/insights/fragments/month_by_month.html:29
#: 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/import_app/models.py:14 apps/rules/models.py:13
#: apps/transactions/models.py:223 apps/transactions/models.py:248
#: apps/transactions/models.py:272 apps/transactions/models.py:1057
#: apps/transactions/models.py:238 apps/transactions/models.py:263
#: apps/transactions/models.py:287 apps/transactions/models.py:1078
#: apps/users/models.py:572 templates/account_groups/fragments/list.html:22
#: templates/accounts/fragments/list.html:22
#: templates/categories/fragments/table.html:17
@@ -170,9 +170,9 @@ msgstr ""
#: apps/rules/models.py:35 apps/rules/models.py:267
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
#: apps/transactions/forms.py:992 apps/transactions/models.py:303
#: apps/transactions/models.py:601 apps/transactions/models.py:823
#: apps/transactions/models.py:1063
#: apps/transactions/forms.py:992 apps/transactions/models.py:318
#: apps/transactions/models.py:618 apps/transactions/models.py:840
#: apps/transactions/models.py:1084
#: templates/installment_plans/fragments/table.html:17
#: templates/quick_transactions/fragments/list.html:14
#: templates/recurring_transactions/fragments/table.html:19
@@ -181,7 +181,7 @@ msgid "Account"
msgstr ""
#: 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/pages/index.html:4 templates/includes/sidebar.html:162
#: templates/includes/sidebar.html:164
@@ -194,68 +194,56 @@ msgstr ""
msgid "Exchange currency cannot be the same as the account's main currency."
msgstr ""
#: apps/accounts/views/account_groups.py:44
#: apps/accounts/views/account_groups.py:50
msgid "Account Group added successfully"
msgstr ""
#: apps/accounts/views/account_groups.py:69
#: 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
#: apps/accounts/views/account_groups.py:78
msgid "Account Group updated successfully"
msgstr ""
#: apps/accounts/views/account_groups.py:111
#: 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
#: apps/accounts/views/account_groups.py:104
msgid "Account Group deleted successfully"
msgstr ""
#: apps/accounts/views/account_groups.py:135
#: apps/accounts/views/accounts.py:189 apps/dca/views.py:128
#: apps/rules/views.py:210 apps/transactions/views/categories.py:192
#: apps/transactions/views/entities.py:154 apps/transactions/views/tags.py:154
#: apps/accounts/views/account_groups.py:109
#: apps/accounts/views/accounts.py:137 apps/dca/views.py:106
#: apps/rules/views.py:191 apps/transactions/views/categories.py:163
#: apps/transactions/views/entities.py:135 apps/transactions/views/tags.py:131
msgid "Item no longer shared with you"
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"
msgstr ""
#: apps/accounts/views/account_groups.py:165
#: apps/accounts/views/accounts.py:119 apps/dca/views.py:158
#: apps/rules/views.py:241 apps/transactions/views/categories.py:142
#: apps/transactions/views/entities.py:184 apps/transactions/views/tags.py:184
#: apps/accounts/views/account_groups.py:152
#: apps/accounts/views/accounts.py:106 apps/dca/views.py:151
#: apps/rules/views.py:238 apps/transactions/views/categories.py:130
#: apps/transactions/views/entities.py:180 apps/transactions/views/tags.py:174
msgid "Configuration saved successfully"
msgstr ""
#: apps/accounts/views/accounts.py:44
#: apps/accounts/views/accounts.py:50
msgid "Account added successfully"
msgstr ""
#: apps/accounts/views/accounts.py:81
#: apps/accounts/views/accounts.py:78
msgid "Account updated successfully"
msgstr ""
#: apps/accounts/views/accounts.py:148
#: apps/accounts/views/accounts.py:132
msgid "Account deleted successfully"
msgstr ""
#: apps/accounts/views/accounts.py:165
#: apps/accounts/views/accounts.py:158
msgid "Account is now tracked"
msgstr ""
#: apps/accounts/views/accounts.py:168
#: apps/accounts/views/accounts.py:161
msgid "Account is now untracked"
msgstr ""
@@ -350,6 +338,7 @@ msgid ""
msgstr ""
#: apps/common/forms.py:76 apps/users/forms.py:172
#: templates/transactions/fragments/filter_actions.html:5
msgid "Save"
msgstr ""
@@ -464,10 +453,9 @@ msgstr ""
#: apps/common/widgets/tom_select.py:15
#: templates/mini_tools/unit_price_calculator.html:180
#: templates/monthly_overview/pages/overview.html:275
#: templates/monthly_overview/pages/overview.html:287
#: templates/transactions/pages/transactions.html:225
#: templates/transactions/pages/transactions.html:237
#: templates/monthly_overview/pages/overview.html:284
#: templates/transactions/fragments/filter_actions.html:48
#: templates/transactions/pages/transactions.html:233
msgid "Clear"
msgstr ""
@@ -486,7 +474,7 @@ msgstr ""
#: 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/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/exchange_rates/fragments/table.html:10
#: templates/exchange_rates_services/fragments/table.html:11
@@ -506,7 +494,7 @@ msgid "Decimal Places"
msgstr ""
#: apps/currencies/models.py:45 apps/export_app/forms.py:25
#: apps/export_app/forms.py:130 apps/transactions/filters.py:64
#: apps/export_app/forms.py:130 apps/transactions/filters.py:70
#: templates/currencies/fragments/list.html:9
#: templates/currencies/pages/index.html:4 templates/includes/sidebar.html:176
#: templates/includes/sidebar.html:178
@@ -572,8 +560,8 @@ msgid "Service Type"
msgstr ""
#: apps/currencies/models.py:119 apps/transactions/filters.py:27
#: apps/transactions/models.py:227 apps/transactions/models.py:251
#: apps/transactions/models.py:275 templates/categories/fragments/list.html:16
#: apps/transactions/models.py:242 apps/transactions/models.py:266
#: apps/transactions/models.py:290 templates/categories/fragments/list.html:16
#: templates/entities/fragments/list.html:16
#: templates/installment_plans/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/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/models.py:327 apps/transactions/models.py:650
#: apps/transactions/models.py:851 apps/transactions/models.py:1085
#: apps/transactions/models.py:342 apps/transactions/models.py:667
#: apps/transactions/models.py:868 apps/transactions/models.py:1106
msgid "Notes"
msgstr ""
@@ -785,27 +773,27 @@ msgstr ""
msgid "DCA Entries"
msgstr ""
#: apps/dca/views.py:38
#: apps/dca/views.py:44
msgid "DCA Strategy added successfully"
msgstr ""
#: apps/dca/views.py:75
#: apps/dca/views.py:73
msgid "DCA Strategy updated successfully"
msgstr ""
#: apps/dca/views.py:107
#: apps/dca/views.py:101
msgid "DCA strategy deleted successfully"
msgstr ""
#: apps/dca/views.py:237
#: apps/dca/views.py:236
msgid "Entry added successfully"
msgstr ""
#: apps/dca/views.py:264
#: apps/dca/views.py:270
msgid "Entry updated successfully"
msgstr ""
#: apps/dca/views.py:290
#: apps/dca/views.py:303
msgid "Entry deleted successfully"
msgstr ""
@@ -816,7 +804,7 @@ msgid "Users"
msgstr ""
#: 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/recurring_transactions/fragments/list_transactions.html:5
#: templates/recurring_transactions/fragments/table.html:37
@@ -825,7 +813,7 @@ msgid "Transactions"
msgstr ""
#: 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/insights/fragments/month_by_month.html:18
#: 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/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:491 apps/transactions/forms.py:771
#: apps/transactions/forms.py:1015 apps/transactions/models.py:286
#: apps/transactions/models.py:342 apps/transactions/models.py:646
#: apps/transactions/models.py:848 apps/transactions/models.py:1100
#: apps/transactions/forms.py:1015 apps/transactions/models.py:301
#: apps/transactions/models.py:357 apps/transactions/models.py:663
#: apps/transactions/models.py:865 apps/transactions/models.py:1121
#: templates/entities/fragments/list.html:9
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
#: templates/insights/fragments/category_overview/index.html:54
@@ -853,14 +841,14 @@ msgid "Entities"
msgstr ""
#: 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/pages/index.html:4
msgid "Recurring Transactions"
msgstr ""
#: 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/pages/index.html:4
msgid "Installment Plans"
@@ -1012,7 +1000,7 @@ msgid "Run deleted successfully"
msgstr ""
#: 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:407
#: 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/models.py:271 apps/transactions/forms.py:433
#: apps/transactions/models.py:310 apps/transactions/models.py:606
#: apps/transactions/models.py:829 apps/transactions/models.py:1070
#: apps/transactions/models.py:325 apps/transactions/models.py:623
#: apps/transactions/models.py:846 apps/transactions/models.py:1091
msgid "Type"
msgstr ""
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
#: apps/rules/models.py:275 apps/transactions/filters.py:22
#: apps/transactions/forms.py:437 apps/transactions/models.py:312
#: apps/transactions/models.py:1072 templates/cotton/transaction/item.html:20
#: apps/transactions/forms.py:437 apps/transactions/models.py:327
#: apps/transactions/models.py:1093 templates/cotton/transaction/item.html:20
#: templates/cotton/transaction/item.html:31
#: templates/transactions/widgets/paid_toggle_button.html:10
#: 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/models.py:283 apps/transactions/forms.py:89
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
#: apps/transactions/forms.py:777 apps/transactions/models.py:314
#: apps/transactions/models.py:624 apps/transactions/models.py:853
#: apps/transactions/forms.py:777 apps/transactions/models.py:329
#: apps/transactions/models.py:641 apps/transactions/models.py:870
msgid "Reference Date"
msgstr ""
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
#: apps/rules/models.py:287 apps/transactions/forms.py:460
#: apps/transactions/models.py:320 apps/transactions/models.py:834
#: apps/transactions/models.py:1078
#: apps/transactions/models.py:335 apps/transactions/models.py:851
#: apps/transactions/models.py:1099
#: templates/insights/fragments/sankey.html:102
#: templates/installment_plans/fragments/table.html:18
#: 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/models.py:42 apps/rules/models.py:291
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
#: apps/transactions/models.py:325 apps/transactions/models.py:608
#: apps/transactions/models.py:837 apps/transactions/models.py:1083
#: apps/transactions/models.py:340 apps/transactions/models.py:625
#: apps/transactions/models.py:854 apps/transactions/models.py:1104
msgid "Description"
msgstr ""
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
#: apps/rules/models.py:299 apps/transactions/models.py:364
#: apps/transactions/models.py:1105
#: apps/rules/models.py:299 apps/transactions/models.py:379
#: apps/transactions/models.py:1126
msgid "Internal Note"
msgstr ""
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
#: apps/rules/models.py:303 apps/transactions/models.py:366
#: apps/transactions/models.py:1107
#: apps/rules/models.py:303 apps/transactions/models.py:381
#: apps/transactions/models.py:1128
msgid "Internal ID"
msgstr ""
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
#: apps/rules/models.py:319 apps/transactions/forms.py:620
#: apps/transactions/models.py:224 apps/transactions/models.py:315
#: apps/transactions/models.py:1073
#: apps/transactions/models.py:239 apps/transactions/models.py:330
#: apps/transactions/models.py:1094
msgid "Mute"
msgstr ""
@@ -1180,7 +1168,7 @@ msgid "Set Values"
msgstr ""
#: 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"
msgstr ""
@@ -1274,15 +1262,15 @@ msgstr ""
msgid "Update or create transaction action"
msgstr ""
#: apps/rules/views.py:71
#: apps/rules/views.py:79
msgid "Rule deactivated successfully"
msgstr ""
#: apps/rules/views.py:73
#: apps/rules/views.py:81
msgid "Rule activated successfully"
msgstr ""
#: apps/rules/views.py:92
#: apps/rules/views.py:100
msgid "Rule added successfully"
msgstr ""
@@ -1290,27 +1278,27 @@ msgstr ""
msgid "Rule updated successfully"
msgstr ""
#: apps/rules/views.py:188
#: apps/rules/views.py:186
msgid "Rule deleted successfully"
msgstr ""
#: apps/rules/views.py:305
#: apps/rules/views.py:306
msgid "Action updated successfully"
msgstr ""
#: apps/rules/views.py:336
#: apps/rules/views.py:341
msgid "Action deleted successfully"
msgstr ""
#: apps/rules/views.py:360
#: apps/rules/views.py:367
msgid "Update or Create Transaction action added successfully"
msgstr ""
#: apps/rules/views.py:393
#: apps/rules/views.py:402
msgid "Update or Create Transaction action updated successfully"
msgstr ""
#: apps/rules/views.py:423
#: apps/rules/views.py:434
msgid "Update or Create Transaction action deleted successfully"
msgstr ""
@@ -1327,58 +1315,58 @@ msgstr ""
msgid "Muted"
msgstr ""
#: apps/transactions/filters.py:45
#: apps/transactions/filters.py:51
msgid "Content"
msgstr ""
#: apps/transactions/filters.py:51
#: apps/transactions/filters.py:57
msgid "Transaction Type"
msgstr ""
#: apps/transactions/filters.py:89
#: apps/transactions/filters.py:95
msgid "Mute Status"
msgstr ""
#: apps/transactions/filters.py:94
#: apps/transactions/filters.py:100
msgid "Date from"
msgstr ""
#: apps/transactions/filters.py:99 apps/transactions/filters.py:109
#: apps/transactions/filters.py:105 apps/transactions/filters.py:115
msgid "Until"
msgstr ""
#: apps/transactions/filters.py:104
#: apps/transactions/filters.py:110
msgid "Reference date from"
msgstr ""
#: apps/transactions/filters.py:114
#: apps/transactions/filters.py:120
msgid "Amount min"
msgstr ""
#: apps/transactions/filters.py:119
#: apps/transactions/filters.py:125
msgid "Amount max"
msgstr ""
#: apps/transactions/filters.py:202
#: apps/transactions/filters.py:208
msgid "Categorized"
msgstr ""
#: apps/transactions/filters.py:209
#: apps/transactions/filters.py:215
msgid "Tagged"
msgstr ""
#: apps/transactions/filters.py:209
#: apps/transactions/filters.py:215
#: templates/insights/fragments/category_overview/index.html:189
#: templates/insights/fragments/month_by_month.html:121
#: templates/insights/fragments/year_by_year.html:75
msgid "Untagged"
msgstr ""
#: apps/transactions/filters.py:215
#: apps/transactions/filters.py:221
msgid "Any entity"
msgstr ""
#: apps/transactions/filters.py:216
#: apps/transactions/filters.py:222
#: templates/insights/fragments/category_overview/index.html:282
#: templates/insights/fragments/month_by_month.html:123
#: templates/insights/fragments/year_by_year.html:77
@@ -1453,42 +1441,42 @@ msgstr ""
msgid "End date should be after the start date"
msgstr ""
#: apps/transactions/models.py:229
#: apps/transactions/models.py:244
msgid ""
"Deactivated categories won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:237
#: apps/transactions/models.py:252
msgid "Transaction Category"
msgstr ""
#: apps/transactions/models.py:238
#: apps/transactions/models.py:253
msgid "Transaction Categories"
msgstr ""
#: apps/transactions/models.py:253
#: apps/transactions/models.py:268
msgid ""
"Deactivated tags won't be able to be selected when creating new transactions"
msgstr ""
#: apps/transactions/models.py:261 apps/transactions/models.py:262
#: apps/transactions/models.py:276 apps/transactions/models.py:277
msgid "Transaction Tags"
msgstr ""
#: apps/transactions/models.py:277
#: apps/transactions/models.py:292
msgid ""
"Deactivated entities won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:285
#: apps/transactions/models.py:300
#: templates/insights/fragments/month_by_month.html:88
#: templates/insights/fragments/year_by_year.html:56
msgid "Entity"
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:44
#: templates/calendar_view/fragments/list.html:52
@@ -1500,7 +1488,7 @@ msgstr ""
msgid "Income"
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:48
#: templates/calendar_view/fragments/list.html:56
@@ -1511,157 +1499,157 @@ msgstr ""
msgid "Expense"
msgstr ""
#: apps/transactions/models.py:353 apps/transactions/models.py:663
#: apps/transactions/models.py:368 apps/transactions/models.py:680
msgid "Installment Plan"
msgstr ""
#: apps/transactions/models.py:362 apps/transactions/models.py:887
#: apps/transactions/models.py:377 apps/transactions/models.py:904
msgid "Recurring Transaction"
msgstr ""
#: apps/transactions/models.py:370
#: apps/transactions/models.py:385
msgid "Deleted"
msgstr ""
#: apps/transactions/models.py:375
#: apps/transactions/models.py:390
msgid "Deleted At"
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"
msgstr ""
#: apps/transactions/models.py:491
#: apps/transactions/models.py:506
msgid "No category"
msgstr ""
#: apps/transactions/models.py:493
#: apps/transactions/models.py:508
msgid "No description"
msgstr ""
#: apps/transactions/models.py:549
#: apps/transactions/models.py:565
msgid "File"
msgstr ""
#: apps/transactions/models.py:551
#: apps/transactions/models.py:567
msgid "Original Name"
msgstr ""
#: apps/transactions/models.py:553
#: apps/transactions/models.py:569
msgid "Content Type"
msgstr ""
#: apps/transactions/models.py:555
#: apps/transactions/models.py:571
msgid "Size"
msgstr ""
#: apps/transactions/models.py:560
#: apps/transactions/models.py:576
msgid "Uploaded By"
msgstr ""
#: apps/transactions/models.py:565
#: apps/transactions/models.py:581
msgid "Transaction Attachment"
msgstr ""
#: apps/transactions/models.py:566
#: apps/transactions/models.py:582
msgid "Transaction Attachments"
msgstr ""
#: apps/transactions/models.py:595 templates/includes/sidebar.html:57
#: apps/transactions/models.py:612 templates/includes/sidebar.html:57
msgid "Yearly"
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
msgid "Monthly"
msgstr ""
#: apps/transactions/models.py:597
#: apps/transactions/models.py:614
msgid "Weekly"
msgstr ""
#: apps/transactions/models.py:598
#: apps/transactions/models.py:615
msgid "Daily"
msgstr ""
#: apps/transactions/models.py:611
#: apps/transactions/models.py:628
msgid "Number of Installments"
msgstr ""
#: apps/transactions/models.py:616
#: apps/transactions/models.py:633
msgid "Installment Start"
msgstr ""
#: apps/transactions/models.py:617
#: apps/transactions/models.py:634
msgid "The installment number to start counting from"
msgstr ""
#: apps/transactions/models.py:622 apps/transactions/models.py:857
#: apps/transactions/models.py:639 apps/transactions/models.py:874
msgid "Start Date"
msgstr ""
#: apps/transactions/models.py:626 apps/transactions/models.py:858
#: apps/transactions/models.py:643 apps/transactions/models.py:875
msgid "End Date"
msgstr ""
#: apps/transactions/models.py:631
#: apps/transactions/models.py:648
msgid "Recurrence"
msgstr ""
#: apps/transactions/models.py:634
#: apps/transactions/models.py:651
msgid "Installment Amount"
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"
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"
msgstr ""
#: apps/transactions/models.py:816
#: apps/transactions/models.py:833
msgid "day(s)"
msgstr ""
#: apps/transactions/models.py:817
#: apps/transactions/models.py:834
msgid "week(s)"
msgstr ""
#: apps/transactions/models.py:818
#: apps/transactions/models.py:835
msgid "month(s)"
msgstr ""
#: apps/transactions/models.py:819
#: apps/transactions/models.py:836
msgid "year(s)"
msgstr ""
#: apps/transactions/models.py:821
#: apps/transactions/models.py:838
#: templates/recurring_transactions/fragments/list.html:18
msgid "Paused"
msgstr ""
#: apps/transactions/models.py:860
#: apps/transactions/models.py:877
msgid "Recurrence Type"
msgstr ""
#: apps/transactions/models.py:863
#: apps/transactions/models.py:880
msgid "Recurrence Interval"
msgstr ""
#: apps/transactions/models.py:866
#: apps/transactions/models.py:883
msgid "Keep at most"
msgstr ""
#: apps/transactions/models.py:870
#: apps/transactions/models.py:887
msgid "Last Generated Date"
msgstr ""
#: apps/transactions/models.py:873
#: apps/transactions/models.py:890
msgid "Last Generated Reference Date"
msgstr ""
#: apps/transactions/models.py:1117
#: apps/transactions/models.py:1138
#: apps/transactions/views/quick_transactions.py:178
#: apps/transactions/views/quick_transactions.py:187
#: apps/transactions/views/quick_transactions.py:189
@@ -1670,7 +1658,7 @@ msgstr ""
msgid "Quick Transaction"
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:15
msgid "Quick Transactions"
@@ -1721,27 +1709,27 @@ msgid_plural "%(count)s transactions duplicated successfully"
msgstr[0] ""
msgstr[1] ""
#: apps/transactions/views/categories.py:66
#: apps/transactions/views/categories.py:72
msgid "Category added successfully"
msgstr ""
#: apps/transactions/views/categories.py:104
#: apps/transactions/views/categories.py:102
msgid "Category updated successfully"
msgstr ""
#: apps/transactions/views/categories.py:171
#: apps/transactions/views/categories.py:158
msgid "Category deleted successfully"
msgstr ""
#: apps/transactions/views/entities.py:66
#: apps/transactions/views/entities.py:72
msgid "Entity added successfully"
msgstr ""
#: apps/transactions/views/entities.py:104
#: apps/transactions/views/entities.py:102
msgid "Entity updated successfully"
msgstr ""
#: apps/transactions/views/entities.py:133
#: apps/transactions/views/entities.py:130
msgid "Entity deleted successfully"
msgstr ""
@@ -1804,15 +1792,15 @@ msgstr ""
msgid "Recurring Transaction deleted successfully"
msgstr ""
#: apps/transactions/views/tags.py:66
#: apps/transactions/views/tags.py:72
msgid "Tag added successfully"
msgstr ""
#: apps/transactions/views/tags.py:104
#: apps/transactions/views/tags.py:100
msgid "Tag updated successfully"
msgstr ""
#: apps/transactions/views/tags.py:133
#: apps/transactions/views/tags.py:126
msgid "Tag deleted successfully"
msgstr ""
@@ -1894,9 +1882,9 @@ msgstr ""
#: 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:245
#: templates/monthly_overview/pages/overview.html:247
#: templates/transactions/pages/transactions.html:47
#: templates/transactions/pages/transactions.html:195
#: templates/transactions/pages/transactions.html:196
msgid "Default"
msgstr ""
@@ -2223,6 +2211,7 @@ msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:92
#: templates/rules/fragments/transaction_rule/view.html:133
#: templates/tags/fragments/table.html:51
#: templates/transactions/fragments/filter_presets.html:28
#: templates/users/fragments/api_tokens.html:93
msgid "Delete"
msgstr ""
@@ -2327,8 +2316,8 @@ msgid "Current balance"
msgstr ""
#: templates/accounts/fragments/account_reconciliation.html:36
#: templates/net_worth/net_worth.html:146
#: templates/net_worth/net_worth.html:396
#: templates/net_worth/net_worth.html:147
#: templates/net_worth/net_worth.html:407
msgid "Difference"
msgstr ""
@@ -2708,19 +2697,19 @@ msgstr ""
msgid "Amount Bought"
msgstr ""
#: templates/dca/fragments/strategy/details.html:384
#: templates/dca/fragments/strategy/details.html:409
msgid "Entry Price vs Current Price"
msgstr ""
#: templates/dca/fragments/strategy/details.html:398
#: templates/dca/fragments/strategy/details.html:423
msgid "Days Between Investments"
msgstr ""
#: templates/dca/fragments/strategy/details.html:444
#: templates/dca/fragments/strategy/details.html:469
msgid "Investment Frequency"
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."
msgstr ""
@@ -2957,6 +2946,7 @@ msgid "Reload"
msgstr ""
#: templates/includes/scripts/hyperscript/swal.html:13
#: templates/transactions/fragments/filter_actions.html:4
msgid "Cancel"
msgstr ""
@@ -3371,16 +3361,16 @@ msgid "Summary"
msgstr ""
#: 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:204
#: templates/transactions/pages/transactions.html:205
msgid "Oldest first"
msgstr ""
#: 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:213
#: templates/transactions/pages/transactions.html:214
msgid "Newest first"
msgstr ""
@@ -3389,8 +3379,8 @@ msgstr ""
msgid "Filter transactions"
msgstr ""
#: templates/monthly_overview/pages/overview.html:235
#: templates/transactions/pages/transactions.html:185
#: templates/monthly_overview/pages/overview.html:237
#: templates/transactions/pages/transactions.html:186
msgid "Order by"
msgstr ""
@@ -3399,24 +3389,24 @@ msgstr ""
msgid "By currency"
msgstr ""
#: templates/net_worth/net_worth.html:84
#: templates/net_worth/net_worth.html:84 templates/net_worth/net_worth.html:85
msgid "Consolidated"
msgstr ""
#: templates/net_worth/net_worth.html:134
#: templates/net_worth/net_worth.html:135
msgid "Evolution"
msgstr ""
#: templates/net_worth/net_worth.html:162
#: templates/net_worth/net_worth.html:163
#: templates/yearly_overview/pages/overview_by_account.html:4
msgid "By account"
msgstr ""
#: templates/net_worth/net_worth.html:270
#: templates/net_worth/net_worth.html:271
msgid "Evolution by currency"
msgstr ""
#: templates/net_worth/net_worth.html:334
#: templates/net_worth/net_worth.html:345
msgid "Evolution by account"
msgstr ""
@@ -3496,15 +3486,19 @@ msgstr ""
msgid "View"
msgstr ""
#: templates/rules/fragments/list.html:70
#: templates/rules/fragments/list.html:71
msgid "Deactivate"
msgstr ""
#: templates/rules/fragments/list.html:70
#: templates/rules/fragments/list.html:71
msgid "Activate"
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"
msgstr ""
@@ -3647,6 +3641,40 @@ msgstr ""
msgid "transactions"
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
msgid "No transactions found"
msgstr ""
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: {
x: {
min: Math.max(0, priceData.labels.length - 20),
max: Math.max(0, priceData.labels.length - 1),
grid: {
display: false
},
@@ -369,6 +371,29 @@
}
},
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: {
position: 'top'
},
+7 -5
View File
@@ -52,7 +52,8 @@
<i class="fa-solid fa-grip"></i>
</div>
<input type="search"
<div class="relative">
<input type="search"
class="input w-full"
id="calculator-input"
_="on click me.focus()
@@ -90,14 +91,15 @@
then remove .swing-out-top-bck from #calculator-result-container
end"
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="flex flex-row py-3 px-5 justify-between">
<div class="text-subtle">=</div>
<div id="calculator-result" class="select-all"></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>
@@ -107,7 +107,7 @@
@click="filterOpen = !filterOpen"
:aria-expanded="filterOpen" id="filter-button"
title="{% translate 'Filter transactions' %}"
_="on load or change from #filter
_="on change from #filter-container
-- Check if any filter has a non-default value
set hasActiveFilter to false
@@ -208,7 +208,7 @@
add .hidden to #filter-active-indicator
end">
<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>
{# Search box #}
@@ -228,6 +228,8 @@
show <.transaction/> in <#transactions-list/>
when its textContent.toLowerCase() contains my value.toLowerCase()">
{% include "transactions/fragments/filter_presets.html" %}
{# Order by icon dropdown #}
<div>
<button class="btn btn-secondary join-item" type="button"
@@ -270,21 +272,16 @@
{# Filter transactions form #}
<div class="z-1" x-show="filterOpen" x-collapse x-cloak>
<div class="card card-body bg-base-200 mt-2">
<div class="text-right">
<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>
{% include "transactions/fragments/filter_actions.html" %}
<form _="on change or submit or search trigger updated on window
install init_tom_select
install init_datepicker"
id="filter" class="mt-3">
{% crispy filter.form %}
</form>
{% include "transactions/fragments/filter_form.html" %}
<div class="text-right">
<button class="btn btn-outline btn-error btn-sm w-fit"
_="on click call #filter.reset() then trigger change on #filter">{% translate 'Clear' %}</button>
<a class="btn btn-outline btn-error btn-sm w-fit"
href="{{ request.path }}"
hx-get="{% url 'transaction_filter_clear' %}"
hx-target="#filter"
hx-swap="outerHTML">{% translate 'Clear' %}</a>
</div>
</div>
</div>
+16 -5
View File
@@ -56,7 +56,7 @@
<li>
{% if currency.consolidated and currency.consolidated.total_final != currency.total_final %}
<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 text-ellipsis">{{ currency.currency.name }}</span>
<span class="text-end shrink-0">
@@ -80,7 +80,8 @@
</a>
<ul>
<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-end shrink-0">
<c-amount.display :amount="currency.consolidated.total_final"
@@ -95,7 +96,7 @@
</ul>
{% else %}
<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="text-end shrink-0">
<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 id="accountBalanceChartScript">
@@ -448,7 +459,7 @@
call accountChart.update()
end
def showOnlyCurrencyDataset(datasetName)
def showOnlyCurrencyDataset(datasetName, differenceDatasetName)
for dataset in currencyChart.data.datasets
set isMatch to dataset.label is datasetName
call currencyChart.setDatasetVisibility(currencyChart.data.datasets.indexOf(dataset), isMatch)
@@ -456,7 +467,7 @@
call currencyChart.update()
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)
end
call monthlyDifferenceChart.update()
+15 -8
View File
@@ -64,14 +64,21 @@
</div>
</td>
<td class="table-col-auto">
<a class="no-underline cursor-pointer"
role="button"
data-tippy-content="
{% if rule.active %}{% translate "Deactivate" %}{% else %}{% translate "Activate" %}{% endif %}"
hx-get="{% url 'transaction_rule_toggle_activity' transaction_rule_id=rule.id %}">
{% 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 %}
</a>
{% if not rule.owner or user == rule.owner %}
<a class="no-underline cursor-pointer"
role="button"
data-tippy-content="
{% if rule.active %}{% translate "Deactivate" %}{% else %}{% translate "Activate" %}{% endif %}"
hx-get="{% url 'transaction_rule_toggle_activity' transaction_rule_id=rule.id %}">
{% 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 %}
</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 class="table-col-auto text-center">
<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"
:aria-expanded="filterOpen" id="filter-button"
title="{% translate 'Filter transactions' %}"
_="on load or change from #filter
_="on change from #filter-container
-- Check if any filter has a non-default value
set hasActiveFilter to false
@@ -157,7 +157,7 @@
add .hidden to #filter-active-indicator
end">
<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>
{# Search box #}
@@ -177,7 +177,8 @@
show <.transaction/> in <#transactions-list/>
when its textContent.toLowerCase() contains my value.toLowerCase()">
{# Order by icon dropdown #}
{% include "transactions/fragments/filter_presets.html" %}
{# Order by icon dropdown #}
<div>
<button class="btn btn-secondary join-item" type="button"
@@ -220,21 +221,16 @@
{# Filter transactions form #}
<div class="z-1" x-show="filterOpen" x-collapse x-cloak>
<div class="card card-body bg-base-200 mt-2">
<div class="text-right">
<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>
{% include "transactions/fragments/filter_actions.html" %}
<form _="on change or submit or search trigger updated on window
install init_tom_select
install init_datepicker"
id="filter" class="mt-3">
{% crispy filter.form %}
</form>
{% include "transactions/fragments/filter_form.html" %}
<div class="text-right">
<button class="btn btn-outline btn-error btn-sm w-fit"
_="on click call #filter.reset() then trigger change on #filter">{% translate 'Clear' %}</button>
<a class="btn btn-outline btn-error btn-sm w-fit"
href="{{ request.path }}"
hx-get="{% url 'transaction_filter_clear' %}"
hx-target="#filter"
hx-swap="outerHTML">{% translate 'Clear' %}</a>
</div>
</div>
</div>
+2 -1
View File
@@ -4,7 +4,8 @@ WORKDIR /usr/src/frontend
COPY ./frontend/package.json ./frontend/package-lock.json ./
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
+63 -34
View File
@@ -23,12 +23,13 @@
"bootstrap": "^5.3.8",
"chart.js": "^4.5.1",
"chartjs-chart-sankey": "^0.14.3",
"chartjs-plugin-zoom": "^2.2.0",
"cuelume": "^0.1.2",
"daisyui": "5.5.20",
"htmx.org": "^2.0.10",
"hyperscript.org": "^0.9.91",
"mathjs": "^15.2.0",
"postcss": "^8.5.15",
"postcss": "^8.5.26",
"pulltorefreshjs": "^0.1.22",
"sass": "^1.100.0",
"sweetalert2": "^11.26.25",
@@ -1077,6 +1078,12 @@
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"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": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
@@ -1150,9 +1157,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.34",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz",
"integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==",
"version": "2.11.20",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz",
"integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
@@ -1181,9 +1188,9 @@
}
},
"node_modules/browserslist": {
"version": "4.28.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
"version": "4.28.8",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
"integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
"funding": [
{
"type": "opencollective",
@@ -1200,11 +1207,11 @@
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
"electron-to-chromium": "^1.5.328",
"node-releases": "^2.0.36",
"update-browserslist-db": "^1.2.3"
"baseline-browser-mapping": "^2.11.12",
"caniuse-lite": "^1.0.30001809",
"electron-to-chromium": "^1.5.402",
"node-releases": "^2.0.53",
"update-browserslist-db": "^1.3.0"
},
"bin": {
"browserslist": "cli.js"
@@ -1214,9 +1221,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001797",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz",
"integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==",
"version": "1.0.30001810",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
"integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
"funding": [
{
"type": "opencollective",
@@ -1254,6 +1261,19 @@
"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": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
@@ -1319,9 +1339,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.368",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz",
"integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==",
"version": "1.5.420",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz",
"integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==",
"license": "ISC"
},
"node_modules/enhanced-resolve": {
@@ -1408,6 +1428,15 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"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": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.10.tgz",
@@ -1424,9 +1453,9 @@
}
},
"node_modules/immutable": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.6.tgz",
"integrity": "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==",
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
"license": "MIT"
},
"node_modules/is-extglob": {
@@ -1758,9 +1787,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"funding": [
{
"type": "github",
@@ -1783,9 +1812,9 @@
"optional": true
},
"node_modules/node-releases": {
"version": "2.0.47",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
"integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
"version": "2.0.54",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
"integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -1810,9 +1839,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"version": "8.5.26",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
"funding": [
{
"type": "opencollective",
@@ -1829,7 +1858,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.12",
"nanoid": "^3.3.17",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -2040,9 +2069,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
"integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
"funding": [
{
"type": "opencollective",
+2 -1
View File
@@ -30,12 +30,13 @@
"bootstrap": "^5.3.8",
"chart.js": "^4.5.1",
"chartjs-chart-sankey": "^0.14.3",
"chartjs-plugin-zoom": "^2.2.0",
"cuelume": "^0.1.2",
"daisyui": "5.5.20",
"htmx.org": "^2.0.10",
"hyperscript.org": "^0.9.91",
"mathjs": "^15.2.0",
"postcss": "^8.5.15",
"postcss": "^8.5.26",
"pulltorefreshjs": "^0.1.22",
"sass": "^1.100.0",
"sweetalert2": "^11.26.25",
+2 -1
View File
@@ -1,5 +1,6 @@
import Chart from 'chart.js/auto';
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;
+1 -1
View File
@@ -6,7 +6,7 @@ readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"crispy-bootstrap5==2026.3",
"django~=5.2.15",
"django~=5.2.16",
"django-allauth[socialaccount]~=65.18.0",
"django-browser-reload==1.21.0",
"django-cachalot~=2.9.0",
Generated
+63 -66
View File
@@ -309,61 +309,58 @@ wheels = [
[[package]]
name = "cryptography"
version = "48.0.1"
version = "50.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ 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 = [
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" },
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" },
]
[[package]]
@@ -410,16 +407,16 @@ wheels = [
[[package]]
name = "django"
version = "5.2.15"
version = "5.2.16"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ 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 = [
{ 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]]
@@ -583,14 +580,14 @@ wheels = [
[[package]]
name = "djangorestframework"
version = "3.17.1"
version = "3.17.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ 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 = [
{ 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]]
@@ -712,11 +709,11 @@ wheels = [
[[package]]
name = "mistune"
version = "3.3.0"
version = "3.3.3"
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 = [
{ 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]]
@@ -1535,20 +1532,20 @@ wheels = [
[[package]]
name = "sqlparse"
version = "0.5.5"
version = "0.6.0"
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 = [
{ 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]]
name = "tablib"
version = "3.9.0"
version = "3.10.0"
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 = [
{ 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]]
@@ -1856,7 +1853,7 @@ dependencies = [
[package.metadata]
requires-dist = [
{ 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-browser-reload", specifier = "==1.21.0" },
{ name = "django-cachalot", specifier = "~=2.9.0" },