Anade columna con el numero de gastos de cada categoria

El listado de categorias no daba ninguna pista de cuales estan en uso.
Anade la columna con el mismo criterio que el de etiquetas: annotate en la
vista para no disparar una query por fila.

Cuenta solo los gastos directos de cada categoria, no los de sus
descendientes. Si el numero incluyera a los hijos, un padre sin gastos
propios mostraria una cifra que no corresponde a ninguna fila suya y que no
cuadraria con lo que se ve al filtrar por esa categoria.

Con Category.Meta.ordering = [Lower("name")], Django anade la expresion al
GROUP BY del annotate. Verificado que no multiplica filas ni fusiona
categorias cuyo nombre solo difiere en mayusculas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
JKuijperM 2026-09-09 16:21:35 +02:00
parent f154127f9e
commit 814487a390
3 changed files with 31 additions and 2 deletions

View File

@ -23,6 +23,7 @@
<thead> <thead>
<tr> <tr>
<th>Categoría</th> <th>Categoría</th>
<th>Gastos</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -30,6 +31,7 @@
{% for row in category_rows %} {% for row in category_rows %}
<tr> <tr>
<td class="cat-depth-{{ row.depth }}">{{ row.category.name }}</td> <td class="cat-depth-{{ row.depth }}">{{ row.category.name }}</td>
<td>{{ row.category.expense_count }}</td>
<td class="table-actions"> <td class="table-actions">
<a href="{% url 'category_edit' row.category.id %}">Editar</a> <a href="{% url 'category_edit' row.category.id %}">Editar</a>
<a href="{% url 'category_delete' row.category.id %}" class="danger">Eliminar</a> <a href="{% url 'category_delete' row.category.id %}" class="danger">Eliminar</a>
@ -37,7 +39,7 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="2" class="empty-state"> <td colspan="3" class="empty-state">
<p>No hay categorías</p> <p>No hay categorías</p>
</td> </td>
</tr> </tr>

View File

@ -116,3 +116,26 @@ def test_category_list_view_returns_tree_ordered_with_depth(auth_client, user):
("Nieto", 2), ("Nieto", 2),
("B raiz", 0), ("B raiz", 0),
] ]
def test_category_list_counts_only_direct_expenses(auth_client, user, account):
parent = Category.objects.create(name="Padre", owner=user)
child = Category.objects.create(name="Hija", owner=user, parent=parent)
Expense.objects.create(
owner=user, account=account, category=parent,
amount=Decimal("10"), date=date.today(),
)
for _ in range(2):
Expense.objects.create(
owner=user, account=account, category=child,
amount=Decimal("5"), date=date.today(),
)
response = auth_client.get(reverse('category_list'))
counts = {
row["category"].name: row["category"].expense_count
for row in response.context["category_rows"]
}
assert counts == {"Padre": 1, "Hija": 2}

View File

@ -990,7 +990,11 @@ def fuel_delete(request, pk):
@login_required @login_required
def category_list(request): def category_list(request):
categories = Category.objects.filter(owner=request.user) categories = list(
Category.objects.filter(owner=request.user)
.annotate(expense_count=Count("expenses"))
.select_related("parent")
)
if request.method == "POST": if request.method == "POST":
form = CategoryForm(request.POST, user=request.user) form = CategoryForm(request.POST, user=request.user)