Merge pull request #321

feat(accounts): add option for untracking accounts on a per user basis
This commit is contained in:
Herculino Trotta
2025-08-09 03:55:59 -03:00
committed by GitHub
12 changed files with 141 additions and 36 deletions
@@ -0,0 +1,20 @@
# Generated by Django 5.2.4 on 2025-08-09 05:52
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0015_alter_account_owner_alter_account_shared_with_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='account',
name='untracked_by',
field=models.ManyToManyField(blank=True, related_name='untracked_accounts', to=settings.AUTH_USER_MODEL),
),
]
+11 -2
View File
@@ -1,11 +1,11 @@
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from apps.transactions.models import Transaction from apps.common.middleware.thread_local import get_current_user
from apps.common.models import SharedObject, SharedObjectManager from apps.common.models import SharedObject, SharedObjectManager
from apps.transactions.models import Transaction
class AccountGroup(SharedObject): class AccountGroup(SharedObject):
@@ -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,10 @@ class Account(SharedObject):
def __str__(self): def __str__(self):
return self.name return self.name
def is_untracked_by(self):
user = get_current_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:
+5
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"),
+20
View File
@@ -155,6 +155,26 @@ def account_delete(request, pk):
) )
@only_htmx
@login_required
@require_http_methods(["GET"])
def account_toggle_untracked(request, pk):
account = get_object_or_404(Account, id=pk)
if account.is_untracked_by():
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"])
+8 -1
View File
@@ -13,7 +13,9 @@ from apps.insights.forms import (
) )
def get_transactions(request, include_unpaid=True, include_silent=False): def get_transactions(
request, include_unpaid=True, include_silent=False, include_untracked_accounts=False
):
transactions = Transaction.objects.all() transactions = Transaction.objects.all()
filter_type = request.GET.get("type", None) filter_type = request.GET.get("type", None)
@@ -95,4 +97,9 @@ 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)
) )
if not include_untracked_accounts:
transactions = transactions.exclude(
account__in=request.user.untracked_accounts.all()
)
return transactions return transactions
+10 -5
View File
@@ -74,7 +74,7 @@ def index(request):
def sankey_by_account(request): def sankey_by_account(request):
# Get filtered transactions # Get filtered transactions
transactions = get_transactions(request) transactions = get_transactions(request, include_untracked_accounts=True)
# Generate Sankey data # Generate Sankey data
sankey_data = generate_sankey_data_by_account(transactions) sankey_data = generate_sankey_data_by_account(transactions)
@@ -239,10 +239,14 @@ def late_transactions(request):
@login_required @login_required
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def emergency_fund(request): def emergency_fund(request):
transactions_currency_queryset = Transaction.objects.filter( transactions_currency_queryset = (
is_paid=True, account__is_archived=False, account__is_asset=False Transaction.objects.filter(
).order_by( is_paid=True, account__is_archived=False, account__is_asset=False
"account__currency__name", )
.exclude(account__in=request.user.untracked_accounts.all())
.order_by(
"account__currency__name",
)
) )
currency_net_worth = calculate_currency_totals( currency_net_worth = calculate_currency_totals(
transactions_queryset=transactions_currency_queryset, ignore_empty=False transactions_queryset=transactions_currency_queryset, ignore_empty=False
@@ -262,6 +266,7 @@ def emergency_fund(request):
category__mute=False, category__mute=False,
mute=False, mute=False,
) )
.exclude(account__in=request.user.untracked_accounts.all())
.values("reference_date", "account__currency") .values("reference_date", "account__currency")
.annotate(monthly_total=Sum("amount")) .annotate(monthly_total=Sum("amount"))
) )
+17 -7
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)
@@ -165,10 +171,14 @@ def monthly_account_summary(request, month: int, year: int):
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def monthly_currency_summary(request, month: int, year: int): def monthly_currency_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, Transaction.objects.filter(
reference_date__month=month, reference_date__year=year,
).exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True)) reference_date__month=month,
)
.exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
.exclude(account__in=request.user.untracked_accounts.all())
)
currency_data = calculate_currency_totals(base_queryset.all(), ignore_empty=True) currency_data = calculate_currency_totals(base_queryset.all(), ignore_empty=True)
currency_percentages = calculate_percentage_distribution(currency_data) currency_percentages = calculate_percentage_distribution(currency_data)
+12 -8
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
+4 -1
View File
@@ -589,7 +589,10 @@ def transaction_all_currency_summary(request):
f = TransactionsFilter(request.GET, queryset=transactions) f = TransactionsFilter(request.GET, queryset=transactions)
currency_data = calculate_currency_totals(f.qs.all(), ignore_empty=True) currency_data = calculate_currency_totals(
f.qs.exclude(account__in=request.user.untracked_accounts.all()),
ignore_empty=True,
)
currency_percentages = calculate_percentage_distribution(currency_data) currency_percentages = calculate_percentage_distribution(currency_data)
context = { context = {
+1
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")
) )
@@ -71,6 +71,17 @@
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-get="{% url 'account_toggle_untracked' pk=account.id %}"
data-bs-toggle="tooltip"
data-bs-title="{% if account.is_untracked_by %}{% translate "Track" %}{% else %}{% translate "Untrack" %}{% endif %}">
{% if account.is_untracked_by %}
<i class="fa-solid fa-eye fa-fw"></i>
{% else %}
<i class="fa-solid fa-eye-slash fa-fw"></i>
{% endif %}
</a>
</div> </div>
</td> </td>
<td class="col">{{ account.name }}</td> <td class="col">{{ account.name }}</td>
+22 -12
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 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 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 %}
<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 %}