Cubren las reglas que no se ven leyendo la vista: que los extremos son inclusivos, que cada fecha puede ir sola, que un rango invertido o una fecha ilegible se ignoran en vez de vaciar el listado, y que el rango tiene precedencia sobre los selectores de ano y mes. En el dashboard se comprueba ademas que la comparativa queda desactivada y avisada cuando hay rango, que el eje del grafico cambia de dia a mes al pasar el umbral, y que los huecos del eje salen a cero en lugar de desaparecer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
272 lines
9.7 KiB
Python
272 lines
9.7 KiB
Python
"""Rango de fechas libre (date_from / date_to) en expense_list y dashboard.
|
|
|
|
La regla que estructura todo: si hay rango activo, manda el rango y los
|
|
selectores de año y mes no se aplican. Un solo criterio de periodo a la vez.
|
|
"""
|
|
import pytest
|
|
from datetime import date, timedelta
|
|
from decimal import Decimal
|
|
from django.urls import reverse
|
|
from expenses.models import Expense
|
|
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
|
|
def make_expense(user, account, category, amount, on):
|
|
return Expense.objects.create(
|
|
owner=user,
|
|
account=account,
|
|
category=category,
|
|
amount=Decimal(amount),
|
|
date=on,
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# expense_list
|
|
# --------------------------------------------------------------------------
|
|
|
|
def test_expense_list_filters_by_range(auth_client, user, account, category):
|
|
inside = make_expense(user, account, category, "10", date(2024, 3, 15))
|
|
before = make_expense(user, account, category, "20", date(2024, 2, 20))
|
|
after = make_expense(user, account, category, "30", date(2024, 4, 5))
|
|
|
|
response = auth_client.get(
|
|
reverse("expense_list"),
|
|
{"date_from": "2024-03-01", "date_to": "2024-03-31"},
|
|
)
|
|
|
|
rows = list(response.context["page_obj"])
|
|
assert rows == [inside]
|
|
assert before not in rows and after not in rows
|
|
assert response.context["range_active"] is True
|
|
assert response.context["kpi_total"] == Decimal("10")
|
|
|
|
|
|
def test_expense_list_open_ended_range_reaches_the_end(auth_client, user, account, category):
|
|
make_expense(user, account, category, "10", date(2024, 2, 20))
|
|
march = make_expense(user, account, category, "20", date(2024, 3, 15))
|
|
december = make_expense(user, account, category, "30", date(2024, 12, 31))
|
|
|
|
response = auth_client.get(reverse("expense_list"), {"date_from": "2024-03-01"})
|
|
|
|
rows = list(response.context["page_obj"])
|
|
assert set(rows) == {march, december}
|
|
|
|
|
|
def test_expense_list_open_ended_range_reaches_the_start(auth_client, user, account, category):
|
|
old = make_expense(user, account, category, "10", date(2020, 1, 1))
|
|
february = make_expense(user, account, category, "20", date(2024, 2, 20))
|
|
make_expense(user, account, category, "30", date(2024, 4, 5))
|
|
|
|
response = auth_client.get(reverse("expense_list"), {"date_to": "2024-03-01"})
|
|
|
|
rows = list(response.context["page_obj"])
|
|
assert set(rows) == {old, february}
|
|
|
|
|
|
def test_expense_list_inverted_range_is_ignored_entirely(auth_client, user, account, category):
|
|
"""Un rango invertido no debe devolver una lista vacía sin explicación."""
|
|
expenses = [
|
|
make_expense(user, account, category, "10", date(2024, 2, 20)),
|
|
make_expense(user, account, category, "20", date(2024, 3, 15)),
|
|
]
|
|
|
|
response = auth_client.get(
|
|
reverse("expense_list"),
|
|
{"date_from": "2024-12-01", "date_to": "2024-01-01"},
|
|
)
|
|
|
|
assert response.context["range_active"] is False
|
|
assert set(response.context["page_obj"]) == set(expenses)
|
|
|
|
|
|
def test_expense_list_malformed_date_is_ignored(auth_client, user, account, category):
|
|
expense = make_expense(user, account, category, "10", date(2024, 3, 15))
|
|
|
|
response = auth_client.get(reverse("expense_list"), {"date_from": "loquesea"})
|
|
|
|
assert response.status_code == 200
|
|
assert response.context["range_active"] is False
|
|
assert list(response.context["page_obj"]) == [expense]
|
|
|
|
|
|
def test_expense_list_range_overrides_year_and_month(auth_client, user, account, category):
|
|
"""El año viene en la URL pero no debe aplicarse: manda el rango."""
|
|
in_2023 = make_expense(user, account, category, "10", date(2023, 5, 10))
|
|
make_expense(user, account, category, "20", date(2024, 5, 10))
|
|
|
|
response = auth_client.get(
|
|
reverse("expense_list"),
|
|
{"year": 2024, "month": 5, "date_from": "2023-01-01", "date_to": "2023-12-31"},
|
|
)
|
|
|
|
assert list(response.context["page_obj"]) == [in_2023]
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# dashboard
|
|
# --------------------------------------------------------------------------
|
|
|
|
def test_dashboard_filters_by_range(auth_client, user, account, category):
|
|
make_expense(user, account, category, "10", date(2024, 3, 15))
|
|
make_expense(user, account, category, "20", date(2024, 4, 5))
|
|
|
|
response = auth_client.get(
|
|
reverse("dashboard"),
|
|
{"date_from": "2024-03-01", "date_to": "2024-03-31"},
|
|
)
|
|
|
|
assert response.context["range_active"] is True
|
|
assert response.context["kpi_total"] == Decimal("10")
|
|
assert response.context["kpi_count"] == 1
|
|
|
|
|
|
def test_dashboard_range_overrides_year(auth_client, user, account, category):
|
|
make_expense(user, account, category, "10", date(2023, 5, 10))
|
|
make_expense(user, account, category, "20", date(2024, 5, 10))
|
|
|
|
response = auth_client.get(
|
|
reverse("dashboard"),
|
|
{"year": 2024, "date_from": "2023-01-01", "date_to": "2023-12-31"},
|
|
)
|
|
|
|
assert response.context["kpi_total"] == Decimal("10")
|
|
|
|
|
|
def test_dashboard_inverted_range_falls_back_to_the_normal_period(
|
|
auth_client, user, account, category
|
|
):
|
|
make_expense(user, account, category, "20", date(2024, 5, 10))
|
|
|
|
response = auth_client.get(
|
|
reverse("dashboard"),
|
|
{"year": 2024, "date_from": "2024-12-01", "date_to": "2024-01-01"},
|
|
)
|
|
|
|
assert response.context["range_active"] is False
|
|
assert response.context["kpi_total"] == Decimal("20")
|
|
|
|
|
|
def test_dashboard_short_range_charts_by_day(auth_client, user, account, category):
|
|
make_expense(user, account, category, "10", 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"
|
|
labels = response.context["chart_labels"]
|
|
assert len(labels) == 30
|
|
assert labels[0] == "01/03"
|
|
assert labels[4] == "05/03"
|
|
# El eje es denso: el día con gasto lleva su importe, el resto va a cero.
|
|
assert response.context["chart_data"][4] == 10.0
|
|
assert response.context["chart_data"][0] == 0
|
|
|
|
|
|
def test_dashboard_long_range_charts_by_month(auth_client, user, account, category):
|
|
make_expense(user, account, category, "10", 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"
|
|
labels = response.context["chart_labels"]
|
|
assert labels == ["01/2024", "02/2024", "03/2024", "04/2024"]
|
|
assert response.context["chart_data"][1] == 10.0
|
|
|
|
|
|
def test_dashboard_range_threshold_is_62_days(auth_client, user, account, category):
|
|
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()},
|
|
)
|
|
|
|
assert at_limit.context["chart_type"] == "day" # 62 días justos
|
|
assert over_limit.context["chart_type"] == "month" # 63 días
|
|
|
|
|
|
def test_dashboard_range_disables_comparison(auth_client, user, account, category):
|
|
make_expense(user, account, category, "10", date(2024, 3, 15))
|
|
|
|
response = auth_client.get(
|
|
reverse("dashboard"),
|
|
{"compare": "1", "date_from": "2024-03-01", "date_to": "2024-03-31"},
|
|
)
|
|
|
|
assert response.context["compare_enabled"] is False
|
|
assert response.context["compare_suppressed"] is True
|
|
assert response.context["kpi_previous_total"] == 0
|
|
assert response.context["category_comparison"] == []
|
|
|
|
|
|
def test_dashboard_range_skips_the_end_of_month_projection(
|
|
auth_client, user, account, category
|
|
):
|
|
today = date.today()
|
|
make_expense(user, account, category, "10", today)
|
|
|
|
response = auth_client.get(
|
|
reverse("dashboard"),
|
|
{"date_from": today.replace(day=1).isoformat()},
|
|
)
|
|
|
|
assert response.context["projected_end_of_month"] == 0
|
|
|
|
|
|
def test_dashboard_range_daily_average_uses_the_range_length(
|
|
auth_client, user, account, category
|
|
):
|
|
make_expense(user, account, category, "100", date(2024, 3, 5))
|
|
|
|
response = auth_client.get(
|
|
reverse("dashboard"),
|
|
{"date_from": "2024-03-01", "date_to": "2024-03-10"}, # 10 días
|
|
)
|
|
|
|
assert response.context["daily_average"] == Decimal("10")
|
|
|
|
|
|
def test_dashboard_empty_open_range_does_not_break(auth_client, user):
|
|
"""Sin date_from y sin gastos no hay longitud que medir: eje mensual vacío."""
|
|
response = auth_client.get(reverse("dashboard"), {"date_to": "2024-03-31"})
|
|
|
|
assert response.status_code == 200
|
|
assert response.context["chart_type"] == "month"
|
|
assert response.context["chart_labels"] == []
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Sin rango no cambia nada
|
|
# --------------------------------------------------------------------------
|
|
|
|
def test_dashboard_without_range_keeps_the_old_day_axis(auth_client, user, account, category):
|
|
make_expense(user, account, category, "10", date(2024, 3, 5))
|
|
|
|
response = auth_client.get(reverse("dashboard"), {"year": 2024, "month": 3})
|
|
|
|
assert response.context["range_active"] is False
|
|
assert response.context["chart_type"] == "day"
|
|
# Números de día, no fechas.
|
|
assert response.context["chart_labels"] == list(range(1, 32))
|
|
|
|
|
|
def test_dashboard_without_range_keeps_the_old_month_axis(auth_client, user, account, category):
|
|
make_expense(user, account, category, "10", date(2024, 3, 5))
|
|
|
|
response = auth_client.get(reverse("dashboard"), {"year": 2024})
|
|
|
|
assert response.context["chart_type"] == "month"
|
|
assert response.context["chart_labels"][0] == "Ene"
|
|
assert len(response.context["chart_labels"]) == 12
|