Modificaciones en filtrado y creada opción para marcar que un gasto/ingreso es un traspaso de cuentas #34

Merged
jkuijperm merged 18 commits from dev into main 2026-09-16 07:50:18 +00:00
Showing only changes of commit 2c05318e0b - Show all commits

View File

@ -0,0 +1,242 @@
"""Traspasos entre cuentas (is_transfer).
Un traspaso no es gasto ni ingreso real, pero 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