667 lines
23 KiB
Python
667 lines
23 KiB
Python
import pytest
|
|
from datetime import date, timedelta
|
|
from decimal import Decimal
|
|
from django.db import connection
|
|
from django.test.utils import CaptureQueriesContext
|
|
from django.urls import reverse
|
|
from expenses.models import Account, Expense, Category, Income, Transfer
|
|
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
def test_dashboard_requires_login(client):
|
|
url = reverse('dashboard')
|
|
response = client.get(url)
|
|
assert response.status_code == 302
|
|
|
|
def test_dashboard_logged_user_can_access(auth_client):
|
|
url = reverse('dashboard')
|
|
response = auth_client.get(url)
|
|
|
|
assert response.status_code == 200
|
|
|
|
def test_dashboard_groups_by_category(auth_client, user, account):
|
|
food = Category.objects.create(name='Food', owner=user)
|
|
rent = Category.objects.create(name='Rent', owner=user)
|
|
|
|
Expense.objects.create(
|
|
owner=user,
|
|
category=food,
|
|
amount=Decimal('10'),
|
|
date=date(2024, 1, 1),
|
|
account=account,
|
|
)
|
|
Expense.objects.create(
|
|
owner=user,
|
|
category=food,
|
|
amount=Decimal('5'),
|
|
date=date(2024, 1, 2),
|
|
account=account,
|
|
)
|
|
Expense.objects.create(
|
|
owner=user,
|
|
category=rent,
|
|
amount=Decimal('20'),
|
|
date=date(2024, 1, 3),
|
|
account=account,
|
|
)
|
|
|
|
response = auth_client.get(
|
|
reverse('dashboard'),
|
|
{'year': 2024}
|
|
)
|
|
|
|
data = list(response.context['by_category'])
|
|
|
|
assert {'category__name': 'Food', 'total': Decimal('15')} in data
|
|
assert {'category__name': 'Rent', 'total': Decimal('20')} in data
|
|
|
|
def test_dashboard_filters_by_year(auth_client, user, account, category):
|
|
Expense.objects.create(
|
|
owner=user,
|
|
category=category,
|
|
amount=10,
|
|
date=date(2023, 5, 1),
|
|
account=account,
|
|
)
|
|
|
|
Expense.objects.create(
|
|
owner=user,
|
|
category=category,
|
|
amount=20,
|
|
date=date(2024, 5, 1),
|
|
account=account,
|
|
)
|
|
|
|
response = auth_client.get(
|
|
reverse('dashboard'),
|
|
{'year': 2024},
|
|
)
|
|
|
|
chart_data = response.context['chart_data']
|
|
|
|
assert len(chart_data) == 12
|
|
assert chart_data[4] == 20.0 # May = month 5 -> index 4
|
|
assert sum(chart_data) == 20.0 # only the 2024 expense contributes
|
|
|
|
|
|
def test_dashboard_preset_this_month(auth_client):
|
|
today = date.today()
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'period': 'this_month'})
|
|
|
|
assert response.context['selected_year'] == today.year
|
|
assert response.context['selected_month'] == today.month
|
|
|
|
|
|
def test_dashboard_preset_last_month(auth_client):
|
|
today = date.today()
|
|
expected_month = today.month - 1 or 12
|
|
expected_year = today.year if today.month > 1 else today.year - 1
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'period': 'last_month'})
|
|
|
|
assert response.context['selected_year'] == expected_year
|
|
assert response.context['selected_month'] == expected_month
|
|
|
|
|
|
def test_dashboard_preset_this_year_has_no_month(auth_client):
|
|
today = date.today()
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'period': 'this_year'})
|
|
|
|
assert response.context['selected_year'] == today.year
|
|
assert response.context['selected_month'] is None
|
|
|
|
|
|
def test_dashboard_filters_by_account(auth_client, user, account, category):
|
|
other = Account.objects.create(
|
|
owner=user, name="Otra", initial_balance=Decimal("100"), active=True
|
|
)
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=category,
|
|
amount=Decimal("10"), date=date(2024, 3, 1),
|
|
)
|
|
Expense.objects.create(
|
|
owner=user, account=other, category=category,
|
|
amount=Decimal("25"), date=date(2024, 3, 1),
|
|
)
|
|
|
|
response = auth_client.get(
|
|
reverse('dashboard'),
|
|
{'year': 2024, 'account': other.id},
|
|
)
|
|
|
|
assert response.context['kpi_total'] == Decimal('25')
|
|
assert response.context['kpi_count'] == 1
|
|
assert response.context['kpi_balance'] == Decimal('75') # 100 - 25
|
|
|
|
|
|
def test_dashboard_without_compare_keeps_comparison_defaults(auth_client):
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
assert response.context['compare_enabled'] is False
|
|
assert response.context['kpi_previous_total'] == 0
|
|
assert response.context['kpi_difference'] == 0
|
|
assert response.context['kpi_percentage'] == 0
|
|
assert response.context['kpi_trend'] == 'equal'
|
|
assert response.context['category_comparison'] == []
|
|
|
|
|
|
def test_dashboard_compare_fills_difference_and_trend(auth_client, user, account, category):
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=category,
|
|
amount=Decimal("20"), date=date(2024, 2, 15),
|
|
)
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=category,
|
|
amount=Decimal("30"), date=date(2024, 3, 10),
|
|
)
|
|
|
|
response = auth_client.get(
|
|
reverse('dashboard'),
|
|
{'year': 2024, 'month': 3, 'compare': '1'},
|
|
)
|
|
|
|
assert response.context['compare_enabled'] is True
|
|
assert response.context['kpi_previous_total'] == Decimal('20')
|
|
assert response.context['kpi_difference'] == Decimal('10')
|
|
assert response.context['kpi_trend'] == 'up'
|
|
assert float(response.context['kpi_percentage']) == 50.0
|
|
|
|
rows = {row['category']: row for row in response.context['category_comparison']}
|
|
assert rows[category.name]['current'] == 30.0
|
|
assert rows[category.name]['previous'] == 20.0
|
|
assert rows[category.name]['difference'] == 10.0
|
|
|
|
|
|
def test_dashboard_comparison_is_ordered_by_absolute_difference(
|
|
auth_client, user, account
|
|
):
|
|
# Las categorías se juntan con un set, y el orden de un set no es estable
|
|
# entre ejecuciones: sin ordenar, la tabla sale distinta en cada recarga.
|
|
moved_little = Category.objects.create(owner=user, name="Poco")
|
|
moved_a_lot = Category.objects.create(owner=user, name="Mucho")
|
|
|
|
for cat, previous, current in (
|
|
(moved_little, "10", "15"),
|
|
(moved_a_lot, "10", "90"),
|
|
):
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=cat,
|
|
amount=Decimal(previous), date=date(2024, 2, 15),
|
|
)
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=cat,
|
|
amount=Decimal(current), date=date(2024, 3, 10),
|
|
)
|
|
|
|
response = auth_client.get(
|
|
reverse('dashboard'),
|
|
{'year': 2024, 'month': 3, 'compare': '1'},
|
|
)
|
|
|
|
order = [row['category'] for row in response.context['category_comparison']]
|
|
assert order == ["Mucho", "Poco"]
|
|
|
|
|
|
def test_dashboard_comparison_breaks_ties_by_category_name(
|
|
auth_client, user, account
|
|
):
|
|
# Misma diferencia en las tres: sin desempate explícito por nombre
|
|
# seguirían bailando entre recargas aunque el orden principal sea estable.
|
|
for name in ("Zeta", "Alfa", "Media"):
|
|
cat = Category.objects.create(owner=user, name=name)
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=cat,
|
|
amount=Decimal("10"), date=date(2024, 2, 15),
|
|
)
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=cat,
|
|
amount=Decimal("20"), date=date(2024, 3, 10),
|
|
)
|
|
|
|
response = auth_client.get(
|
|
reverse('dashboard'),
|
|
{'year': 2024, 'month': 3, 'compare': '1'},
|
|
)
|
|
|
|
order = [row['category'] for row in response.context['category_comparison']]
|
|
assert order == ["Alfa", "Media", "Zeta"]
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Serie de ingresos contra gastos
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def _make_transfer(user, from_account, to_account, amount="400", on=None, fee=None):
|
|
transfer = Transfer(
|
|
owner=user,
|
|
date=on or date(2024, 3, 10),
|
|
amount=Decimal(amount),
|
|
from_account=from_account,
|
|
to_account=to_account,
|
|
fee_amount=Decimal(fee) if fee is not None else None,
|
|
)
|
|
transfer.sync()
|
|
return transfer
|
|
|
|
|
|
def test_dashboard_sends_both_series_on_the_same_axis(
|
|
auth_client, user, account, category
|
|
):
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=category,
|
|
amount=Decimal("30"), date=date(2024, 5, 10),
|
|
)
|
|
Income.objects.create(
|
|
owner=user, account=account, name="Nómina",
|
|
amount=Decimal("1500"), date=date(2024, 3, 10),
|
|
)
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
expenses = response.context['chart_data']
|
|
incomes = response.context['chart_income_data']
|
|
# Mismo eje para las dos: si cada una montara el suyo, se desalinearían.
|
|
assert len(incomes) == len(response.context['chart_labels']) == len(expenses)
|
|
assert incomes[2] == 1500.0 # marzo
|
|
assert expenses[4] == 30.0 # mayo
|
|
assert incomes[4] == 0 # mayo no tuvo ingresos: cero, no hueco
|
|
|
|
|
|
def test_dashboard_income_series_is_zero_filled_without_movements(auth_client):
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
assert response.context['chart_income_data'] == [0] * 12
|
|
|
|
|
|
def test_dashboard_income_series_is_zero_filled_in_a_month_without_movements(
|
|
auth_client
|
|
):
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024, 'month': 3})
|
|
|
|
assert response.context['chart_income_data'] == [0] * 31
|
|
|
|
|
|
def test_dashboard_both_series_ignore_a_transfer(user, auth_client, account):
|
|
other = Account.objects.create(
|
|
owner=user, name="Ahorro", initial_balance=Decimal("0"), active=True
|
|
)
|
|
_make_transfer(user, account, other, amount="400")
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
# Mandarse dinero a una cuenta propia no es ingreso ni gasto: si se
|
|
# excluyera solo de los gastos, el gráfico daría un pico de ingresos cada
|
|
# vez que el usuario se traspasa la nómina.
|
|
assert sum(response.context['chart_income_data']) == 0
|
|
assert sum(response.context['chart_data']) == 0
|
|
|
|
|
|
def test_dashboard_counts_the_transfer_fee_as_an_expense(user, auth_client, account):
|
|
other = Account.objects.create(
|
|
owner=user, name="Ahorro", initial_balance=Decimal("0"), active=True
|
|
)
|
|
_make_transfer(user, account, other, amount="400", fee="1.50")
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
# La comisión va con is_transfer=False: sale del patrimonio de verdad.
|
|
assert sum(response.context['chart_data']) == 1.5
|
|
assert sum(response.context['chart_income_data']) == 0
|
|
|
|
|
|
def test_dashboard_income_series_follows_the_day_axis_in_a_short_range(
|
|
auth_client, user, account
|
|
):
|
|
Income.objects.create(
|
|
owner=user, account=account, name="Nómina",
|
|
amount=Decimal("1500"), date=date(2024, 3, 5),
|
|
)
|
|
|
|
response = auth_client.get(
|
|
reverse('dashboard'),
|
|
{'date_from': '2024-03-01', 'date_to': '2024-03-30'}, # 30 días
|
|
)
|
|
|
|
assert response.context['chart_type'] == 'day'
|
|
incomes = response.context['chart_income_data']
|
|
assert len(incomes) == 30
|
|
assert incomes[4] == 1500.0
|
|
assert incomes[0] == 0
|
|
|
|
|
|
def test_dashboard_income_series_follows_the_month_axis_in_a_long_range(
|
|
auth_client, user, account
|
|
):
|
|
Income.objects.create(
|
|
owner=user, account=account, name="Nómina",
|
|
amount=Decimal("1500"), date=date(2024, 2, 5),
|
|
)
|
|
|
|
response = auth_client.get(
|
|
reverse('dashboard'),
|
|
{'date_from': '2024-01-01', 'date_to': '2024-04-29'}, # 120 días
|
|
)
|
|
|
|
assert response.context['chart_type'] == 'month'
|
|
incomes = response.context['chart_income_data']
|
|
assert len(incomes) == 4
|
|
assert incomes[1] == 1500.0
|
|
|
|
|
|
def test_dashboard_income_series_matches_the_62_day_threshold(
|
|
auth_client, user, account
|
|
):
|
|
start = date(2024, 3, 1)
|
|
at_limit = auth_client.get(
|
|
reverse('dashboard'),
|
|
{'date_from': start.isoformat(), 'date_to': (start + timedelta(days=61)).isoformat()},
|
|
)
|
|
over_limit = auth_client.get(
|
|
reverse('dashboard'),
|
|
{'date_from': start.isoformat(), 'date_to': (start + timedelta(days=62)).isoformat()},
|
|
)
|
|
|
|
# La serie de ingresos usa el mismo umbral que la de gastos, no el suyo.
|
|
assert at_limit.context['chart_type'] == 'day'
|
|
assert len(at_limit.context['chart_income_data']) == 62
|
|
assert over_limit.context['chart_type'] == 'month'
|
|
assert len(over_limit.context['chart_income_data']) == 3 # mar, abr, may
|
|
|
|
|
|
def test_dashboard_open_range_axis_covers_older_incomes(auth_client, user, account, category):
|
|
Income.objects.create(
|
|
owner=user, account=account, name="Nómina",
|
|
amount=Decimal("1500"), date=date(2024, 3, 1),
|
|
)
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=category,
|
|
amount=Decimal("30"), date=date(2024, 3, 20),
|
|
)
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'date_to': '2024-03-25'})
|
|
|
|
# Sin date_from el eje se acota con el movimiento más antiguo de las dos
|
|
# series: midiendo solo el gasto más viejo, ese ingreso se caería del eje.
|
|
assert response.context['chart_labels'][0] == "01/03"
|
|
assert response.context['chart_income_data'][0] == 1500.0
|
|
|
|
|
|
def test_dashboard_income_series_does_not_leak_between_users(
|
|
auth_client, user, account, django_user_model
|
|
):
|
|
intruder = django_user_model.objects.create_user(username="otro", password="x")
|
|
their_account = Account.objects.create(
|
|
owner=intruder, name="Ajena", initial_balance=Decimal("0"), active=True
|
|
)
|
|
Income.objects.create(
|
|
owner=intruder, account=their_account, name="Ajena",
|
|
amount=Decimal("999"), date=date(2024, 3, 10),
|
|
)
|
|
Income.objects.create(
|
|
owner=user, account=account, name="Mía",
|
|
amount=Decimal("100"), date=date(2024, 3, 10),
|
|
)
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
assert sum(response.context['chart_income_data']) == 100.0
|
|
|
|
|
|
def test_dashboard_income_series_respects_the_account_filter(
|
|
auth_client, user, account
|
|
):
|
|
other = Account.objects.create(
|
|
owner=user, name="Otra", initial_balance=Decimal("0"), active=True
|
|
)
|
|
Income.objects.create(
|
|
owner=user, account=account, name="Aquí",
|
|
amount=Decimal("100"), date=date(2024, 3, 10),
|
|
)
|
|
Income.objects.create(
|
|
owner=user, account=other, name="Allí",
|
|
amount=Decimal("500"), date=date(2024, 3, 10),
|
|
)
|
|
|
|
response = auth_client.get(
|
|
reverse('dashboard'), {'year': 2024, 'account': account.id}
|
|
)
|
|
|
|
assert sum(response.context['chart_income_data']) == 100.0
|
|
|
|
|
|
def test_dashboard_chart_type_is_day_when_a_month_is_selected(auth_client):
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024, 'month': 3})
|
|
|
|
assert response.context['chart_type'] == 'day'
|
|
|
|
|
|
def test_dashboard_chart_type_is_month_for_the_whole_year(auth_client):
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
assert response.context['chart_type'] == 'month'
|
|
|
|
|
|
def test_dashboard_category_chart_keeps_only_top_ten(auth_client, user, account):
|
|
for i in range(12):
|
|
cat = Category.objects.create(name=f"Cat {i:02d}", owner=user)
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=cat,
|
|
amount=Decimal(str(i + 1)), date=date(2024, 6, 1),
|
|
)
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
assert len(response.context['by_category_chart']) == 10
|
|
assert len(list(response.context['by_category'])) == 12
|
|
|
|
|
|
def test_dashboard_kpi_balance_matches_sum_of_current_balances(auth_client, user, account, category):
|
|
savings = Account.objects.create(
|
|
owner=user, name="Ahorro", initial_balance=Decimal("500"), active=True
|
|
)
|
|
Account.objects.create(
|
|
owner=user, name="Cerrada", initial_balance=Decimal("999"), active=False
|
|
)
|
|
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=category,
|
|
amount=Decimal("40"), date=date(2024, 4, 1),
|
|
)
|
|
Income.objects.create(
|
|
owner=user, account=savings, name="Nomina",
|
|
amount=Decimal("60"), date=date(2024, 4, 2),
|
|
)
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
expected = sum(
|
|
acc.current_balance()
|
|
for acc in Account.objects.filter(owner=user, active=True)
|
|
)
|
|
assert expected == Decimal('520') # (0 - 40) + (500 + 60), sin la inactiva
|
|
assert response.context['kpi_balance'] == expected
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Desglose por tipo de gasto
|
|
# --------------------------------------------------------------------------
|
|
|
|
def _breakdown(response):
|
|
return {row['label']: row['total'] for row in response.context['expense_type_breakdown']}
|
|
|
|
|
|
def test_expense_type_breakdown_groups_by_effective_type(auth_client, user, account):
|
|
home = Category.objects.create(
|
|
name='Casa', owner=user, expense_type=Category.EXPENSE_TYPE_FIXED_ESSENTIAL
|
|
)
|
|
power = Category.objects.create(name='Luz', owner=user, parent=home)
|
|
fun = Category.objects.create(
|
|
name='Ocio', owner=user, expense_type=Category.EXPENSE_TYPE_VARIABLE_DISCRETIONARY
|
|
)
|
|
loose = Category.objects.create(name='Varios', owner=user)
|
|
for category, amount in [(home, '500'), (power, '60'), (fun, '40'), (loose, '7')]:
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=category,
|
|
amount=Decimal(amount), date=date(2024, 3, 1),
|
|
)
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
# La luz hereda "Fijo esencial" de Casa; Varios no tiene tipo en ninguna
|
|
# parte de la cadena y va a "Sin clasificar" en vez de desaparecer.
|
|
assert _breakdown(response) == {
|
|
'Fijo esencial': Decimal('560'),
|
|
'Fijo prescindible': Decimal('0'),
|
|
'Variable esencial': Decimal('0'),
|
|
'Variable prescindible': Decimal('40'),
|
|
'Sin clasificar': Decimal('7'),
|
|
}
|
|
assert sum(_breakdown(response).values()) == response.context['kpi_total']
|
|
assert 'id="expenseTypeChart"' in response.content.decode()
|
|
|
|
|
|
def test_expense_type_breakdown_follows_the_selected_period(auth_client, user, account, category):
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=category,
|
|
amount=Decimal('10'), date=date(2024, 3, 1),
|
|
)
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=category,
|
|
amount=Decimal('99'), date=date(2024, 4, 1),
|
|
)
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024, 'month': 3})
|
|
|
|
assert _breakdown(response)['Sin clasificar'] == Decimal('10')
|
|
|
|
|
|
def test_expense_type_breakdown_ignores_transfers(auth_client, user, account):
|
|
other = Account.objects.create(
|
|
owner=user, name="Ahorro", initial_balance=Decimal("0"), active=True
|
|
)
|
|
_make_transfer(user, account, other, amount="400", fee="1.50")
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
# Solo la comisión es gasto de verdad.
|
|
assert sum(_breakdown(response).values()) == Decimal('1.50')
|
|
|
|
|
|
def _classify_two_categories(user):
|
|
Category.objects.create(
|
|
name='Alquiler', owner=user, expense_type=Category.EXPENSE_TYPE_FIXED_ESSENTIAL
|
|
)
|
|
Category.objects.create(
|
|
name='Ocio', owner=user, expense_type=Category.EXPENSE_TYPE_VARIABLE_DISCRETIONARY
|
|
)
|
|
|
|
|
|
def test_expense_type_breakdown_shows_empty_state_without_expenses(auth_client, user):
|
|
_classify_two_categories(user)
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
assert sum(_breakdown(response).values()) == 0
|
|
content = response.content.decode()
|
|
assert 'id="expenseTypeChart"' not in content
|
|
assert 'No hay gastos en este periodo' in content
|
|
assert 'Clasifica al menos dos categorías' not in content
|
|
|
|
|
|
def test_expense_type_breakdown_asks_to_classify_with_fewer_than_two(
|
|
auth_client, user, account
|
|
):
|
|
# Una sola categoría clasificada; la hija hereda pero no cuenta como
|
|
# clasificada, porque el usuario no la ha clasificado él.
|
|
parent = Category.objects.create(
|
|
name='Casa', owner=user, expense_type=Category.EXPENSE_TYPE_FIXED_ESSENTIAL
|
|
)
|
|
child = Category.objects.create(name='Luz', owner=user, parent=parent)
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=child,
|
|
amount=Decimal('60'), date=date(2024, 3, 1),
|
|
)
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
assert response.context['expense_type_classified_count'] == 1
|
|
content = response.content.decode()
|
|
assert 'Clasifica al menos dos categorías' in content
|
|
assert 'href="%s"' % reverse('category_list') in content
|
|
assert 'id="expenseTypeChart"' not in content
|
|
assert 'id="expenseTypeGrouping"' not in content
|
|
assert 'No hay gastos en este periodo' not in content
|
|
|
|
|
|
@pytest.mark.parametrize('classified, asks_to_classify', [(1, True), (2, False)])
|
|
def test_expense_type_breakdown_classification_threshold(
|
|
auth_client, user, account, classified, asks_to_classify
|
|
):
|
|
types = [Category.EXPENSE_TYPE_FIXED_ESSENTIAL, Category.EXPENSE_TYPE_VARIABLE_ESSENTIAL]
|
|
for i in range(classified):
|
|
category = Category.objects.create(name=f'Clasificada {i}', owner=user, expense_type=types[i])
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=category,
|
|
amount=Decimal('10'), date=date(2024, 3, 1),
|
|
)
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
assert response.context['expense_type_classified_count'] == classified
|
|
content = response.content.decode()
|
|
assert ('Clasifica al menos dos categorías' in content) is asks_to_classify
|
|
assert ('id="expenseTypeChart"' in content) is not asks_to_classify
|
|
|
|
|
|
def test_expense_type_breakdown_renders_grouping_and_shape_controls(
|
|
auth_client, user, account
|
|
):
|
|
_classify_two_categories(user)
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=Category.objects.get(name='Ocio'),
|
|
amount=Decimal('40'), date=date(2024, 3, 1),
|
|
)
|
|
|
|
response = auth_client.get(reverse('dashboard'), {'year': 2024})
|
|
|
|
content = response.content.decode()
|
|
assert 'id="expenseTypeGrouping"' in content
|
|
assert 'role="group" aria-label="Forma del gráfico"' in content
|
|
assert content.count('data-shape=') == 3
|
|
assert content.count('aria-pressed="true"') == 1
|
|
|
|
|
|
def _dashboard_queries(client):
|
|
with CaptureQueriesContext(connection) as ctx:
|
|
client.get(reverse('dashboard'), {'year': 2024})
|
|
return len(ctx.captured_queries)
|
|
|
|
|
|
def test_dashboard_query_count_does_not_grow_with_categories_and_expenses(
|
|
auth_client, user, account
|
|
):
|
|
root = Category.objects.create(
|
|
name='Raíz', owner=user, expense_type=Category.EXPENSE_TYPE_FIXED_ESSENTIAL
|
|
)
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=root,
|
|
amount=Decimal('1'), date=date(2024, 3, 1),
|
|
)
|
|
baseline = _dashboard_queries(auth_client)
|
|
|
|
# Más categorías anidadas y más gastos que las 7 filas de recent_expenses:
|
|
# ni la herencia del desglose ni el listado reciente deben pedir una
|
|
# consulta por fila.
|
|
parent = root
|
|
for i in range(10):
|
|
parent = Category.objects.create(name=f'Nivel {i}', owner=user, parent=parent)
|
|
Expense.objects.create(
|
|
owner=user, account=account, category=parent,
|
|
amount=Decimal('1'), date=date(2024, 3, 1),
|
|
)
|
|
|
|
assert _dashboard_queries(auth_client) == baseline
|