From 706f8fea2b0af477d97861ee8fd6d8b0fd2ee9b7 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Tue, 10 Nov 2026 21:55:29 -0300 Subject: [PATCH] feat(transactions): allow filtering the transaction picker search TransactionSelect takes a filters dict sent as query params; the search endpoint applies allowlisted ones (currency, account). --- app/apps/common/widgets/tom_select.py | 6 ++++++ .../transactions/tests/test_transaction_search.py | 12 +++++++++++- app/apps/transactions/views/transactions.py | 12 ++++++++++++ frontend/src/js/select.js | 3 ++- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/apps/common/widgets/tom_select.py b/app/apps/common/widgets/tom_select.py index 0f22d106..c179e630 100644 --- a/app/apps/common/widgets/tom_select.py +++ b/app/apps/common/widgets/tom_select.py @@ -1,4 +1,5 @@ import json +from urllib.parse import urlencode from django.forms import SelectMultiple, widgets from django.urls import reverse @@ -141,6 +142,9 @@ class TransactionSelect(TomSelect): self.load_income = income self.load_expense = expense self.create = False + # Extra search filters sent as query params, e.g. {"currency": 1}; + # keys must be in SEARCH_FILTERS of the transactions_search view + self.filters = {} def build_attrs(self, base_attrs, extra_attrs=None): attrs = super().build_attrs(base_attrs, extra_attrs) @@ -156,6 +160,8 @@ class TransactionSelect(TomSelect): attrs["data-load"] = reverse( "transactions_search", kwargs={"filter_type": "expenses"} ) + if self.filters: + attrs["data-load"] += "?" + urlencode(self.filters) return attrs diff --git a/app/apps/transactions/tests/test_transaction_search.py b/app/apps/transactions/tests/test_transaction_search.py index 6b02e95b..18901e4a 100644 --- a/app/apps/transactions/tests/test_transaction_search.py +++ b/app/apps/transactions/tests/test_transaction_search.py @@ -32,7 +32,7 @@ class TransactionSearchTests(TestCase): def make(self, days, description="", amount="10", **kwargs): return Transaction.objects.create( - account=self.account, + account=kwargs.pop("account", self.account), type=Transaction.Type.EXPENSE, date=self.today + timedelta(days=days), reference_date=(self.today + timedelta(days=days)).replace(day=1), @@ -72,3 +72,13 @@ class TransactionSearchTests(TestCase): self.assertEqual(self.search("wallet"), ["Groceries", "Other"]) self.assertEqual(self.search("groceries nomatch"), []) + + def test_filters_by_currency(self): + usd = Currency.objects.create(code="USD", name="Dollar", decimal_places=2) + other = Account.objects.create(name="Bank", currency=usd) + self.make(0, "brl") + self.make(0, "usd", account=other) + response = self.client.get( + reverse("transactions_search"), {"q": "", "currency": usd.id} + ) + self.assertEqual([r["description"] for r in response.json()], ["usd"]) diff --git a/app/apps/transactions/views/transactions.py b/app/apps/transactions/views/transactions.py index 07b7f731..9ca4e68d 100644 --- a/app/apps/transactions/views/transactions.py +++ b/app/apps/transactions/views/transactions.py @@ -985,6 +985,13 @@ def transactions_trash_can_list(request): ) +# Query params the transaction picker may filter by (TransactionSelect.filters) +SEARCH_FILTERS = { + "currency": "account__currency_id", + "account": "account_id", +} + + @login_required @require_http_methods(["GET"]) def get_recent_transactions(request, filter_type=None): @@ -1002,6 +1009,11 @@ def get_recent_transactions(request, filter_type=None): elif filter_type == "income": queryset = queryset.filter(type=Transaction.Type.INCOME) + for param, lookup in SEARCH_FILTERS.items(): + value = request.GET.get(param, "") + if value.isdigit(): + queryset = queryset.filter(**{lookup: value}) + if search_term: queryset = search_transactions(queryset, search_term) diff --git a/frontend/src/js/select.js b/frontend/src/js/select.js index dc573918..b65b9236 100644 --- a/frontend/src/js/select.js +++ b/frontend/src/js/select.js @@ -136,7 +136,8 @@ window.TomSelect = function createDynamicTomSelect(element) { config.preload = 'focus'; config.shouldLoad = () => true; config.load = function (query, callback) { - let url = element.dataset.load + '?q=' + encodeURIComponent(query); + const url = new URL(element.dataset.load, window.location.origin); + url.searchParams.set('q', query); fetch(url) .then(response => response.json()) .then(json => {