refactor: Style transaction items for untracked accounts

This commit extends the "Untrack Account" feature by applying a special style to transaction items that belong to an untracked account.

- The transaction item template is modified to apply a "dimmed" style to transactions from untracked accounts.
- The styling follows the precedence: Account (untracked) > Category (muted) > Transaction (hidden).
- The dropdown menu for transaction items now shows "Controlled by account" if the transaction's account is untracked.
This commit is contained in:
google-labs-jules[bot]
2025-08-09 05:47:18 +00:00
parent 9102654eab
commit 7f8261b9cc
9 changed files with 92 additions and 23 deletions

View File

@@ -62,6 +62,11 @@ class Account(SharedObject):
verbose_name=_("Archived"), verbose_name=_("Archived"),
help_text=_("Archived accounts don't show up nor count towards your net worth"), help_text=_("Archived accounts don't show up nor count towards your net worth"),
) )
untracked_by = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
related_name="untracked_accounts",
)
objects = SharedObjectManager() objects = SharedObjectManager()
all_objects = models.Manager() # Unfiltered manager all_objects = models.Manager() # Unfiltered manager
@@ -75,6 +80,9 @@ class Account(SharedObject):
def __str__(self): def __str__(self):
return self.name return self.name
def is_untracked_by(self, user):
return self.untracked_by.filter(pk=user.pk).exists()
def clean(self): def clean(self):
super().clean() super().clean()
if self.exchange_currency == self.currency: if self.exchange_currency == self.currency:

View File

@@ -31,6 +31,11 @@ urlpatterns = [
views.account_take_ownership, views.account_take_ownership,
name="account_take_ownership", name="account_take_ownership",
), ),
path(
"account/<int:pk>/toggle-untracked/",
views.account_toggle_untracked,
name="account_toggle_untracked",
),
path("account-groups/", views.account_groups_index, name="account_groups_index"), path("account-groups/", views.account_groups_index, name="account_groups_index"),
path("account-groups/list/", views.account_groups_list, name="account_groups_list"), path("account-groups/list/", views.account_groups_list, name="account_groups_list"),
path("account-groups/add/", views.account_group_add, name="account_group_add"), path("account-groups/add/", views.account_group_add, name="account_group_add"),

View File

@@ -155,6 +155,26 @@ def account_delete(request, pk):
) )
@only_htmx
@login_required
@require_http_methods(["POST"])
def account_toggle_untracked(request, pk):
account = get_object_or_404(Account, id=pk)
if account.is_untracked_by(request.user):
account.untracked_by.remove(request.user)
messages.success(request, _("Account is now tracked."))
else:
account.untracked_by.add(request.user)
messages.success(request, _("Account is now untracked."))
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated",
},
)
@only_htmx @only_htmx
@login_required @login_required
@require_http_methods(["GET"]) @require_http_methods(["GET"])

View File

@@ -95,4 +95,6 @@ def get_transactions(request, include_unpaid=True, include_silent=False):
Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True) Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True)
) )
transactions = transactions.exclude(account__in=request.user.untracked_accounts.all())
return transactions return transactions

View File

@@ -107,9 +107,15 @@ def transactions_list(request, month: int, year: int):
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def monthly_summary(request, month: int, year: int): def monthly_summary(request, month: int, year: int):
# Base queryset with all required filters # Base queryset with all required filters
base_queryset = Transaction.objects.filter( base_queryset = (
reference_date__year=year, reference_date__month=month, account__is_asset=False Transaction.objects.filter(
).exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True)) reference_date__year=year,
reference_date__month=month,
account__is_asset=False,
)
.exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
.exclude(account__in=request.user.untracked_accounts.all())
)
data = calculate_currency_totals(base_queryset, ignore_empty=True) data = calculate_currency_totals(base_queryset, ignore_empty=True)
percentages = calculate_percentage_distribution(data) percentages = calculate_percentage_distribution(data)

View File

@@ -27,10 +27,12 @@ def net_worth(request):
view_type = request.session.get("networth_view_type", "current") view_type = request.session.get("networth_view_type", "current")
if view_type == "current": if view_type == "current":
transactions_currency_queryset = Transaction.objects.filter( transactions_currency_queryset = (
is_paid=True, account__is_archived=False Transaction.objects.filter(is_paid=True, account__is_archived=False)
).order_by( .order_by(
"account__currency__name", "account__currency__name",
)
.exclude(account__in=request.user.untracked_accounts.all())
) )
transactions_account_queryset = Transaction.objects.filter( transactions_account_queryset = Transaction.objects.filter(
is_paid=True, account__is_archived=False is_paid=True, account__is_archived=False
@@ -39,10 +41,12 @@ def net_worth(request):
"account__name", "account__name",
) )
else: else:
transactions_currency_queryset = Transaction.objects.filter( transactions_currency_queryset = (
account__is_archived=False Transaction.objects.filter(account__is_archived=False)
).order_by( .order_by(
"account__currency__name", "account__currency__name",
)
.exclude(account__in=request.user.untracked_accounts.all())
) )
transactions_account_queryset = Transaction.objects.filter( transactions_account_queryset = Transaction.objects.filter(
account__is_archived=False account__is_archived=False

View File

@@ -95,6 +95,7 @@ def yearly_overview_by_currency(request, year: int):
transactions = ( transactions = (
Transaction.objects.filter(**filter_params) Transaction.objects.filter(**filter_params)
.exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True)) .exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
.exclude(account__in=request.user.untracked_accounts.all())
.order_by("account__currency__name") .order_by("account__currency__name")
) )

View File

@@ -71,6 +71,19 @@
hx-get="{% url 'account_share_settings' pk=account.id %}"> hx-get="{% url 'account_share_settings' pk=account.id %}">
<i class="fa-solid fa-share fa-fw"></i></a> <i class="fa-solid fa-share fa-fw"></i></a>
{% endif %} {% endif %}
<a class="btn btn-secondary btn-sm"
role="button"
hx-post="{% url 'account_toggle_untracked' pk=account.id %}"
hx-trigger='updated from:body'
hx-swap="none"
data-bs-toggle="tooltip"
data-bs-title="{% if account.is_untracked_by user %}{% translate "Track" %}{% else %}{% translate "Untrack" %}{% endif %}">
{% if account.is_untracked_by user %}
<i class="fa-solid fa-eye-slash fa-fw"></i>
{% else %}
<i class="fa-solid fa-eye fa-fw"></i>
{% endif %}
</a>
</div> </div>
</td> </td>
<td class="col">{{ account.name }}</td> <td class="col">{{ account.name }}</td>

View File

@@ -33,7 +33,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="col-lg col-12 {% if transaction.category.mute or transaction.mute %}tw:brightness-80{% endif %}"> <div class="col-lg col-12 {% if transaction.account.is_untracked_by user or transaction.category.mute or transaction.mute %}tw:brightness-80{% endif %}">
{# Date#} {# Date#}
<div class="row mb-2 mb-lg-1 tw:text-gray-400"> <div class="row mb-2 mb-lg-1 tw:text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-calendar fa-fw me-1 fa-xs"></i></div> <div class="col-auto pe-1"><i class="fa-solid fa-calendar fa-fw me-1 fa-xs"></i></div>
@@ -91,7 +91,7 @@
{% endwith %} {% endwith %}
</div> </div>
</div> </div>
<div class="col-lg-auto col-12 text-lg-end align-self-end {% if transaction.category.mute or transaction.mute %}tw:brightness-80{% endif %}"> <div class="col-lg-auto col-12 text-lg-end align-self-end {% if transaction.account.is_untracked_by user or transaction.category.mute or transaction.mute %}tw:brightness-80{% endif %}">
<div class="main-amount mb-2 mb-lg-0"> <div class="main-amount mb-2 mb-lg-0">
<c-amount.display <c-amount.display
:amount="transaction.amount" :amount="transaction.amount"
@@ -146,16 +146,26 @@
<i class="fa-solid fa-ellipsis fa-fw"></i> <i class="fa-solid fa-ellipsis fa-fw"></i>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-md-start"> <ul class="dropdown-menu dropdown-menu-end dropdown-menu-md-start">
{% if transaction.category.mute %} {% if transaction.account.is_untracked_by user %}
<li> <li>
<a class="dropdown-item disabled d-flex align-items-center" aria-disabled="true"> <a class="dropdown-item disabled d-flex align-items-center" aria-disabled="true">
<i class="fa-solid fa-eye fa-fw me-2"></i> <i class="fa-solid fa-eye fa-fw me-2"></i>
<div> <div>
{% translate 'Show on summaries' %} {% translate 'Show on summaries' %}
<div class="d-block text-body-secondary tw:text-xs tw:font-medium">{% translate 'Controlled by category' %}</div> <div class="d-block text-body-secondary tw:text-xs tw:font-medium">{% translate 'Controlled by account' %}</div>
</div> </div>
</a> </a>
</li> </li>
{% elif transaction.category.mute %}
<li>
<a class="dropdown-item disabled d-flex align-items-center" aria-disabled="true">
<i class="fa-solid fa-eye fa-fw me-2"></i>
<div>
{% translate 'Show on summaries' %}
<div class="d-block text-body-secondary tw:text-xs tw:font-medium">{% translate 'Controlled by category' %}</div>
</div>
</a>
</li>
{% elif transaction.mute %} {% elif transaction.mute %}
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_mute' transaction_id=transaction.id %}" hx-target="closest .transaction" hx-swap="outerHTML"><i class="fa-solid fa-eye fa-fw me-2"></i>{% translate 'Show on summaries' %}</a></li> <li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_mute' transaction_id=transaction.id %}" hx-target="closest .transaction" hx-swap="outerHTML"><i class="fa-solid fa-eye fa-fw me-2"></i>{% translate 'Show on summaries' %}</a></li>
{% else %} {% else %}