Anade tests de los traspasos
El campo is_transfer tiene dos comportamientos opuestos y uno de ellos falla en silencio: si el filtro se cuela en un calculo de saldo, los numeros siguen apareciendo, solo que mal. Los tests se organizan en dos bloques que cubren justo esa frontera. - Analisis: el traspaso no cuenta en los KPIs de home ni del dashboard, ni en el total del listado, ni en la comparativa de periodos, ni en el progreso de los objetivos de pago y presupuesto. - Saldo: el traspaso SI cuenta en current_balance, monthly_balance, balance_until, monthly_net y en el objetivo de ahorro. Tambien se cubre que el listado de gastos siga mostrando el traspaso aunque su total lo descuente, que es la parte que un filtro puesto de mas romperia. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2d7cd8cb68
commit
2c05318e0b
242
expenses_manager/expenses/tests/test_transfers.py
Normal file
242
expenses_manager/expenses/tests/test_transfers.py
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
"""Traspasos entre cuentas (is_transfer).
|
||||||
|
|
||||||
|
Un traspaso no es gasto ni ingreso real, pero sí mueve dinero entre cuentas.
|
||||||
|
De ahí los dos bloques de este fichero, que comprueban lo contrario el uno del
|
||||||
|
otro: los análisis excluyen los traspasos, los saldos los incluyen.
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.urls import reverse
|
||||||
|
from expenses.models import Account, Category, Expense, Goal, Income
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def savings(user):
|
||||||
|
return Account.objects.create(
|
||||||
|
owner=user, name="Ahorro", initial_balance=Decimal("0"), active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_expense(user, account, category, amount, on, is_transfer=False):
|
||||||
|
return Expense.objects.create(
|
||||||
|
owner=user,
|
||||||
|
account=account,
|
||||||
|
category=category,
|
||||||
|
amount=Decimal(amount),
|
||||||
|
date=on,
|
||||||
|
is_transfer=is_transfer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Análisis: los traspasos NO cuentan
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_dashboard_kpi_total_ignores_transfer(auth_client, user, account, category):
|
||||||
|
make_expense(user, account, category, "100", date(2024, 5, 10))
|
||||||
|
make_expense(user, account, category, "400", date(2024, 5, 11), is_transfer=True)
|
||||||
|
|
||||||
|
response = auth_client.get(reverse("dashboard"), {"year": 2024})
|
||||||
|
|
||||||
|
assert response.context["kpi_total"] == Decimal("100")
|
||||||
|
assert response.context["kpi_count"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_by_category_ignores_transfer(auth_client, user, account, category):
|
||||||
|
other = Category.objects.create(owner=user, name="Traspasos")
|
||||||
|
make_expense(user, account, category, "100", date(2024, 5, 10))
|
||||||
|
make_expense(user, account, other, "400", date(2024, 5, 11), is_transfer=True)
|
||||||
|
|
||||||
|
response = auth_client.get(reverse("dashboard"), {"year": 2024})
|
||||||
|
|
||||||
|
rows = {row["category__name"]: row["total"] for row in response.context["by_category"]}
|
||||||
|
assert rows == {category.name: Decimal("100")}
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_comparison_ignores_transfer(auth_client, user, account, category):
|
||||||
|
make_expense(user, account, category, "30", date(2024, 3, 10))
|
||||||
|
make_expense(user, account, category, "20", date(2024, 2, 15))
|
||||||
|
make_expense(user, account, category, "500", date(2024, 2, 16), is_transfer=True)
|
||||||
|
|
||||||
|
response = auth_client.get(
|
||||||
|
reverse("dashboard"), {"year": 2024, "month": 3, "compare": "1"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.context["kpi_previous_total"] == Decimal("20")
|
||||||
|
assert response.context["kpi_difference"] == Decimal("10")
|
||||||
|
|
||||||
|
|
||||||
|
def test_home_kpi_total_ignores_transfer(auth_client, user, account, category):
|
||||||
|
today = date.today()
|
||||||
|
make_expense(user, account, category, "100", today)
|
||||||
|
make_expense(user, account, category, "400", today, is_transfer=True)
|
||||||
|
|
||||||
|
response = auth_client.get(reverse("home"))
|
||||||
|
|
||||||
|
assert response.context["kpi_total"] == Decimal("100")
|
||||||
|
assert response.context["kpi_count"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_home_previous_month_comparison_ignores_transfer(
|
||||||
|
auth_client, user, account, category
|
||||||
|
):
|
||||||
|
today = date.today()
|
||||||
|
# Día 1 para que reste un mes sin caer en un día inexistente.
|
||||||
|
first = date(today.year, today.month, 1)
|
||||||
|
prev = date(first.year - 1, 12, 1) if first.month == 1 else date(
|
||||||
|
first.year, first.month - 1, 1
|
||||||
|
)
|
||||||
|
|
||||||
|
make_expense(user, account, category, "10", prev)
|
||||||
|
make_expense(user, account, category, "900", prev, is_transfer=True)
|
||||||
|
|
||||||
|
response = auth_client.get(reverse("home"))
|
||||||
|
|
||||||
|
assert response.context["prev_total"] == Decimal("10")
|
||||||
|
|
||||||
|
|
||||||
|
def test_expense_list_total_ignores_transfer_but_row_is_listed(
|
||||||
|
auth_client, user, account, category
|
||||||
|
):
|
||||||
|
make_expense(user, account, category, "100", date(2024, 5, 10))
|
||||||
|
transfer = make_expense(
|
||||||
|
user, account, category, "400", date(2024, 5, 11), is_transfer=True
|
||||||
|
)
|
||||||
|
|
||||||
|
response = auth_client.get(reverse("expense_list"), {"year": 2024})
|
||||||
|
|
||||||
|
assert response.context["kpi_total"] == Decimal("100")
|
||||||
|
assert response.context["kpi_count"] == 1
|
||||||
|
# El traspaso sigue en la tabla aunque no sume en el total.
|
||||||
|
assert transfer in list(response.context["page_obj"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_budget_goal_progress_ignores_transfer(user, account, category):
|
||||||
|
today = date.today()
|
||||||
|
make_expense(user, account, category, "50", today)
|
||||||
|
make_expense(user, account, category, "300", today, is_transfer=True)
|
||||||
|
|
||||||
|
goal = Goal.objects.create(
|
||||||
|
owner=user,
|
||||||
|
name="Presupuesto mensual",
|
||||||
|
target_amount=Decimal("200"),
|
||||||
|
kind=Goal.KIND_BUDGET,
|
||||||
|
category=category,
|
||||||
|
period=Goal.PERIOD_MONTH,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert goal.progress == Decimal("50")
|
||||||
|
assert not goal.is_exceeded()
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Saldos: los traspasos SÍ cuentan
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_transfer_expense_still_lowers_source_balance(user, account, category):
|
||||||
|
account.initial_balance = Decimal("1000")
|
||||||
|
account.save()
|
||||||
|
make_expense(user, account, category, "400", date(2024, 5, 11), is_transfer=True)
|
||||||
|
|
||||||
|
assert account.current_balance() == Decimal("600")
|
||||||
|
|
||||||
|
|
||||||
|
def test_transfer_income_still_raises_target_balance(user, savings):
|
||||||
|
make_income(user, savings, "400", date(2024, 5, 11), is_transfer=True)
|
||||||
|
|
||||||
|
assert savings.current_balance() == Decimal("400")
|
||||||
|
|
||||||
|
|
||||||
|
def test_transfer_keeps_total_balance_unchanged(auth_client, user, account, category, savings):
|
||||||
|
account.initial_balance = Decimal("1000")
|
||||||
|
account.save()
|
||||||
|
make_expense(user, account, category, "400", date(2024, 5, 11), is_transfer=True)
|
||||||
|
make_income(user, savings, "400", date(2024, 5, 11), is_transfer=True)
|
||||||
|
|
||||||
|
response = auth_client.get(reverse("home"))
|
||||||
|
|
||||||
|
# El dinero cambia de sitio, el patrimonio no se mueve.
|
||||||
|
assert response.context["total_balance"] == Decimal("1000")
|
||||||
|
|
||||||
|
|
||||||
|
def test_saving_goal_progress_counts_the_transfer(user, savings):
|
||||||
|
"""El punto más delicado: un traspaso a la cuenta de ahorro ES el progreso."""
|
||||||
|
make_income(user, savings, "400", date(2024, 5, 11), is_transfer=True)
|
||||||
|
|
||||||
|
goal = Goal.objects.create(
|
||||||
|
owner=user,
|
||||||
|
name="Fondo de emergencia",
|
||||||
|
target_amount=Decimal("1000"),
|
||||||
|
kind=Goal.KIND_SAVING,
|
||||||
|
account=savings,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert goal.progress == Decimal("400")
|
||||||
|
|
||||||
|
|
||||||
|
def test_account_list_balance_counts_the_transfer(auth_client, user, account, category):
|
||||||
|
account.initial_balance = Decimal("1000")
|
||||||
|
account.save()
|
||||||
|
make_expense(user, account, category, "400", date(2024, 5, 11), is_transfer=True)
|
||||||
|
|
||||||
|
response = auth_client.get(reverse("account_list"))
|
||||||
|
|
||||||
|
rows = {row["account"].id: row["balance"] for row in response.context["account_rows"]}
|
||||||
|
assert rows[account.id] == Decimal("600")
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Formularios
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_expense_form_can_mark_a_transfer(auth_client, user, account, category):
|
||||||
|
response = auth_client.post(
|
||||||
|
reverse("expense_create"),
|
||||||
|
{
|
||||||
|
"date": "2024-05-11",
|
||||||
|
"amount": "400",
|
||||||
|
"description": "",
|
||||||
|
"category": category.id,
|
||||||
|
"account": account.id,
|
||||||
|
"is_transfer": "on",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert Expense.objects.get(owner=user).is_transfer is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_income_form_can_mark_a_transfer(auth_client, user, account):
|
||||||
|
response = auth_client.post(
|
||||||
|
reverse("income_create"),
|
||||||
|
{
|
||||||
|
"account": account.id,
|
||||||
|
"name": "Traspaso desde nómina",
|
||||||
|
"amount": "400",
|
||||||
|
"date": "2024-05-11",
|
||||||
|
"is_transfer": "on",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert Income.objects.get(owner=user).is_transfer is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_expense_defaults_to_not_a_transfer(user, account, category):
|
||||||
|
expense = make_expense(user, account, category, "10", date(2024, 5, 11))
|
||||||
|
|
||||||
|
assert expense.is_transfer is False
|
||||||
Loading…
Reference in New Issue
Block a user