El listado de ingresos era el unico que seguia volcando la tabla entera sin
filtros, sin totales y sin paginacion: con unos cuantos anos de datos la
pagina se vuelve inmanejable y no hay forma de responder "cuanto ingrese en
marzo".
Se reutiliza lo que ya existe en el listado de gastos en vez de inventar nada:
los mismos filtros de ano, mes y cuenta, el mismo rango de fechas libre con la
misma precedencia y el mismo aviso, la misma paginacion de diez por pagina y
el mismo query_params para que los enlaces de pagina no pierdan los filtros.
Los ingresos marcados como traspaso se siguen listando, porque son movimientos
reales de la cuenta y hay que poder verlos y corregirlos, pero el total los
excluye: de ahi la etiqueta "Total (sin traspasos)".
El estado vacio distingue entre no tener ingresos y que los filtros no
devuelvan nada, para no invitar a crear un ingreso cuando el problema es el
filtro. El colspan de esa fila es 5, que son las columnas de esta tabla.
select_related("account") mantiene el numero de consultas plano: sin el, cada
fila pedia su cuenta por separado.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
172 lines
5.8 KiB
Python
172 lines
5.8 KiB
Python
import pytest
|
|
from datetime import date
|
|
from decimal import Decimal
|
|
from django.test.utils import CaptureQueriesContext
|
|
from django.db import connection
|
|
from django.urls import reverse
|
|
from expenses.models import Income, Account
|
|
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
|
|
def make_income(user, account, amount, on, name="Ingreso", is_transfer=False):
|
|
return Income.objects.create(
|
|
owner=user,
|
|
account=account,
|
|
name=name,
|
|
amount=Decimal(amount),
|
|
date=on,
|
|
is_transfer=is_transfer,
|
|
)
|
|
|
|
|
|
def test_income_increases_account_balance(user):
|
|
general_account = Account.objects.create(name='General', owner=user, initial_balance=1000, active=True)
|
|
|
|
Income.objects.create(
|
|
owner=user,
|
|
account=general_account,
|
|
amount=500,
|
|
name='Nomina enero',
|
|
date=date.today(),
|
|
)
|
|
|
|
assert general_account.current_balance() == 1500
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Listado: filtros, paginación y totales
|
|
# --------------------------------------------------------------------------
|
|
|
|
def test_income_list_filters_by_year(auth_client, user, account):
|
|
old = make_income(user, account, "10", date(2023, 5, 10))
|
|
new = make_income(user, account, "20", date(2024, 5, 10))
|
|
|
|
response = auth_client.get(reverse("income_list"), {"year": 2024})
|
|
|
|
assert list(response.context["page_obj"]) == [new]
|
|
assert old not in list(response.context["page_obj"])
|
|
|
|
|
|
def test_income_list_filters_by_month(auth_client, user, account):
|
|
march = make_income(user, account, "10", date(2024, 3, 10))
|
|
make_income(user, account, "20", date(2024, 5, 10))
|
|
|
|
response = auth_client.get(reverse("income_list"), {"year": 2024, "month": 3})
|
|
|
|
assert list(response.context["page_obj"]) == [march]
|
|
|
|
|
|
def test_income_list_filters_by_account(auth_client, user, account):
|
|
other = Account.objects.create(
|
|
owner=user, name="Otra", initial_balance=Decimal("0"), active=True
|
|
)
|
|
make_income(user, account, "10", date(2024, 3, 10))
|
|
from_other = make_income(user, other, "20", date(2024, 3, 11))
|
|
|
|
response = auth_client.get(reverse("income_list"), {"account": other.id})
|
|
|
|
assert list(response.context["page_obj"]) == [from_other]
|
|
|
|
|
|
def test_income_list_filters_by_range(auth_client, user, account):
|
|
inside = make_income(user, account, "10", date(2024, 3, 15))
|
|
make_income(user, account, "20", date(2024, 4, 5))
|
|
|
|
response = auth_client.get(
|
|
reverse("income_list"),
|
|
{"date_from": "2024-03-01", "date_to": "2024-03-31"},
|
|
)
|
|
|
|
assert response.context["range_active"] is True
|
|
assert list(response.context["page_obj"]) == [inside]
|
|
|
|
|
|
def test_income_list_range_overrides_year(auth_client, user, account):
|
|
in_2023 = make_income(user, account, "10", date(2023, 5, 10))
|
|
make_income(user, account, "20", date(2024, 5, 10))
|
|
|
|
response = auth_client.get(
|
|
reverse("income_list"),
|
|
{"year": 2024, "date_from": "2023-01-01", "date_to": "2023-12-31"},
|
|
)
|
|
|
|
assert list(response.context["page_obj"]) == [in_2023]
|
|
|
|
|
|
def test_income_list_paginates_by_ten(auth_client, user, account):
|
|
for day in range(1, 15):
|
|
make_income(user, account, "10", date(2024, 3, day))
|
|
|
|
response = auth_client.get(reverse("income_list"))
|
|
|
|
assert len(response.context["page_obj"]) == 10
|
|
assert response.context["page_obj"].paginator.num_pages == 2
|
|
|
|
second = auth_client.get(reverse("income_list"), {"page": 2})
|
|
assert len(second.context["page_obj"]) == 4
|
|
|
|
|
|
def test_income_list_pagination_links_keep_the_filters(auth_client, user, account):
|
|
for day in range(1, 15):
|
|
make_income(user, account, "10", date(2024, 3, day))
|
|
make_income(user, account, "99", date(2023, 1, 1))
|
|
|
|
response = auth_client.get(reverse("income_list"), {"year": 2024})
|
|
|
|
# query_params alimenta los enlaces de paginación: sin él, pasar de página
|
|
# perdería el filtro y aparecería el ingreso de 2023.
|
|
assert "year=2024" in response.context["query_params"]
|
|
assert "page" not in response.context["query_params"]
|
|
assert 'href="?page=2&year=2024"' in response.content.decode()
|
|
|
|
|
|
def test_income_list_total_excludes_transfers_but_lists_them(auth_client, user, account):
|
|
make_income(user, account, "100", date(2024, 3, 10))
|
|
transfer = make_income(
|
|
user, account, "400", date(2024, 3, 11), is_transfer=True
|
|
)
|
|
|
|
response = auth_client.get(reverse("income_list"))
|
|
|
|
assert response.context["kpi_total"] == Decimal("100")
|
|
assert response.context["kpi_count"] == 1
|
|
assert transfer in list(response.context["page_obj"])
|
|
|
|
|
|
def test_income_list_empty_state_when_filters_match_nothing(auth_client, user, account):
|
|
make_income(user, account, "10", date(2024, 3, 10))
|
|
|
|
response = auth_client.get(reverse("income_list"), {"year": 1999})
|
|
|
|
assert response.status_code == 200
|
|
assert list(response.context["page_obj"]) == []
|
|
assert response.context["filters_active"] is True
|
|
body = response.content.decode()
|
|
assert "No hay ingresos que coincidan con los filtros" in body
|
|
assert "Añade el primero" not in body
|
|
|
|
|
|
def test_income_list_empty_state_without_filters(auth_client):
|
|
response = auth_client.get(reverse("income_list"))
|
|
|
|
assert response.context["filters_active"] is False
|
|
assert "Añade el primero" in response.content.decode()
|
|
|
|
|
|
def test_income_list_query_count_does_not_grow_with_rows(auth_client, user, account):
|
|
"""select_related("account"): una consulta por página, no una por fila."""
|
|
for day in range(1, 4):
|
|
make_income(user, account, "10", date(2024, 3, day))
|
|
|
|
with CaptureQueriesContext(connection) as few:
|
|
auth_client.get(reverse("income_list"))
|
|
|
|
for day in range(4, 11):
|
|
make_income(user, account, "10", date(2024, 3, day))
|
|
|
|
with CaptureQueriesContext(connection) as many:
|
|
auth_client.get(reverse("income_list"))
|
|
|
|
assert len(many) == len(few)
|