Fixed bugs 2 #15
31
expenses_manager/expenses/tests/conftest.py
Normal file
31
expenses_manager/expenses/tests/conftest.py
Normal file
@ -0,0 +1,31 @@
|
||||
import pytest
|
||||
from decimal import Decimal
|
||||
from expenses.models import Account, Category
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user(db, django_user_model):
|
||||
return django_user_model.objects.create_user(
|
||||
username="tester", password="testpass123"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_client(client, user):
|
||||
client.login(username=user.username, password="testpass123")
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def account(user):
|
||||
return Account.objects.create(
|
||||
owner=user,
|
||||
name="Cuenta principal",
|
||||
initial_balance=Decimal("0"),
|
||||
active=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def category(user):
|
||||
return Category.objects.create(owner=user, name="General")
|
||||
86
expenses_manager/expenses/tests/test_accounts.py
Normal file
86
expenses_manager/expenses/tests/test_accounts.py
Normal file
@ -0,0 +1,86 @@
|
||||
import pytest
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from expenses.models import Account, Expense, Income, Category
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_current_balance_reflects_income_and_expenses(user, category):
|
||||
acc = Account.objects.create(owner=user, name="Cuenta", initial_balance=Decimal("100"))
|
||||
Income.objects.create(owner=user, account=acc, name="Nomina", amount=Decimal("50"), date=date.today())
|
||||
Expense.objects.create(owner=user, account=acc, category=category, amount=Decimal("30"), date=date.today())
|
||||
|
||||
assert acc.current_balance() == Decimal("120")
|
||||
|
||||
|
||||
def test_monthly_balance_returns_12_rows_in_order(user):
|
||||
acc = Account.objects.create(owner=user, name="Cuenta", initial_balance=Decimal("100"))
|
||||
|
||||
data = acc.monthly_balance(2020)
|
||||
|
||||
assert len(data) == 12
|
||||
assert [row["month"] for row in data] == list(range(1, 13))
|
||||
assert all(row["balance"] == 100.0 for row in data)
|
||||
|
||||
|
||||
def test_monthly_balance_is_cumulative_across_months(user, category):
|
||||
acc = Account.objects.create(owner=user, name="Cuenta", initial_balance=Decimal("0"))
|
||||
year = date.today().year - 1 # avoid the current-month live-balance patch
|
||||
|
||||
Expense.objects.create(owner=user, account=acc, category=category, amount=Decimal("40"), date=date(year, 3, 1))
|
||||
Income.objects.create(owner=user, account=acc, name="Extra", amount=Decimal("100"), date=date(year, 6, 1))
|
||||
|
||||
data = acc.monthly_balance(year)
|
||||
|
||||
assert data[2]["balance"] == -40.0 # month 3
|
||||
assert data[5]["balance"] == 60.0 # month 6, cumulative
|
||||
assert data[11]["balance"] == 60.0 # carries forward to December
|
||||
|
||||
|
||||
def test_monthly_balance_respects_year_param_and_excludes_other_years(user, category):
|
||||
acc = Account.objects.create(owner=user, name="Cuenta", initial_balance=Decimal("0"))
|
||||
|
||||
Expense.objects.create(owner=user, account=acc, category=category, amount=Decimal("50"), date=date(2023, 6, 15))
|
||||
Income.objects.create(owner=user, account=acc, name="Extra", amount=Decimal("30"), date=date(2024, 3, 10))
|
||||
|
||||
data_2023 = acc.monthly_balance(2023)
|
||||
assert data_2023[5]["balance"] == -50.0 # month 6
|
||||
assert data_2023[11]["balance"] == -50.0
|
||||
|
||||
data_2024 = acc.monthly_balance(2024)
|
||||
assert data_2024[1]["balance"] == -50.0 # month 2: prior year's expense folded into start
|
||||
assert data_2024[2]["balance"] == -20.0 # month 3: 2024 income applied
|
||||
|
||||
|
||||
def test_monthly_balance_current_month_patched_with_live_balance(user, category):
|
||||
today = date.today()
|
||||
acc = Account.objects.create(owner=user, name="Cuenta", initial_balance=Decimal("100"))
|
||||
Expense.objects.create(owner=user, account=acc, category=category, amount=Decimal("30"), date=today)
|
||||
|
||||
data = acc.monthly_balance(today.year)
|
||||
|
||||
assert data[today.month - 1]["balance"] == float(acc.current_balance())
|
||||
|
||||
|
||||
def test_monthly_net_returns_income_minus_expense_per_month(user, category):
|
||||
acc = Account.objects.create(owner=user, name="Cuenta", initial_balance=Decimal("0"))
|
||||
|
||||
Income.objects.create(owner=user, account=acc, name="Nomina", amount=Decimal("100"), date=date(2024, 4, 5))
|
||||
Expense.objects.create(owner=user, account=acc, category=category, amount=Decimal("40"), date=date(2024, 4, 20))
|
||||
|
||||
data = acc.monthly_net(2024)
|
||||
|
||||
assert len(data) == 12
|
||||
assert data[3]["net"] == 60.0 # month 4
|
||||
assert data[0]["net"] == 0.0
|
||||
|
||||
|
||||
def test_balance_until_only_counts_entries_on_or_before_date(user, category):
|
||||
acc = Account.objects.create(owner=user, name="Cuenta", initial_balance=Decimal("0"))
|
||||
|
||||
Expense.objects.create(owner=user, account=acc, category=category, amount=Decimal("20"), date=date(2024, 1, 10))
|
||||
Expense.objects.create(owner=user, account=acc, category=category, amount=Decimal("5"), date=date(2024, 1, 15))
|
||||
Income.objects.create(owner=user, account=acc, name="Later", amount=Decimal("50"), date=date(2024, 1, 20))
|
||||
|
||||
assert acc.balance_until(date(2024, 1, 15)) == Decimal("-25")
|
||||
101
expenses_manager/expenses/tests/test_categories.py
Normal file
101
expenses_manager/expenses/tests/test_categories.py
Normal file
@ -0,0 +1,101 @@
|
||||
import pytest
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from django.contrib.messages import get_messages
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
from expenses.models import Category, Expense
|
||||
from expenses.forms import CategoryForm
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_slug_auto_generated_from_name_on_save(user):
|
||||
cat = Category.objects.create(name="Ropa y Calzado", owner=user)
|
||||
|
||||
assert cat.slug == slugify("Ropa y Calzado")
|
||||
|
||||
|
||||
def test_slug_not_overwritten_if_explicitly_set(user):
|
||||
cat = Category(name="Comida", slug="mi-slug-personalizado", owner=user)
|
||||
cat.save()
|
||||
|
||||
assert cat.slug == "mi-slug-personalizado"
|
||||
|
||||
|
||||
def test_descendant_ids_includes_grandchildren(user):
|
||||
parent = Category.objects.create(name="A", owner=user)
|
||||
child = Category.objects.create(name="B", owner=user, parent=parent)
|
||||
grandchild = Category.objects.create(name="C", owner=user, parent=child)
|
||||
|
||||
ids = set(parent.descendant_ids())
|
||||
|
||||
assert ids == {parent.pk, child.pk, grandchild.pk}
|
||||
|
||||
|
||||
def test_descendant_ids_can_exclude_self(user):
|
||||
parent = Category.objects.create(name="A", owner=user)
|
||||
child = Category.objects.create(name="B", owner=user, parent=parent)
|
||||
|
||||
ids = parent.descendant_ids(include_self=False)
|
||||
|
||||
assert parent.pk not in ids
|
||||
assert child.pk in ids
|
||||
|
||||
|
||||
def test_categoryform_parent_persists_on_save(auth_client, user):
|
||||
other = Category.objects.create(name="Otra", owner=user)
|
||||
|
||||
auth_client.post(reverse('category_list'), {'name': 'Nueva', 'parent': str(other.pk)})
|
||||
|
||||
created = Category.objects.get(name='Nueva', owner=user)
|
||||
assert created.parent_id == other.pk
|
||||
|
||||
|
||||
def test_categoryform_excludes_self_and_descendants_from_parent_queryset_when_editing(user):
|
||||
parent = Category.objects.create(name="P", owner=user)
|
||||
child = Category.objects.create(name="C", owner=user, parent=parent)
|
||||
|
||||
form = CategoryForm(instance=parent, user=user)
|
||||
queryset_ids = set(form.fields['parent'].queryset.values_list('pk', flat=True))
|
||||
|
||||
assert parent.pk not in queryset_ids
|
||||
assert child.pk not in queryset_ids
|
||||
|
||||
|
||||
def test_categoryform_clean_parent_rejects_self_as_parent(user):
|
||||
cat = Category.objects.create(name="X", owner=user)
|
||||
|
||||
form = CategoryForm(data={'name': 'X', 'parent': str(cat.pk)}, instance=cat, user=user)
|
||||
|
||||
assert not form.is_valid()
|
||||
assert 'parent' in form.errors
|
||||
|
||||
|
||||
def test_categoryform_rejects_descendant_as_parent(user):
|
||||
parent = Category.objects.create(name="P", owner=user)
|
||||
child = Category.objects.create(name="C", owner=user, parent=parent)
|
||||
|
||||
form = CategoryForm(data={'name': 'P', 'parent': str(child.pk)}, instance=parent, user=user)
|
||||
|
||||
assert not form.is_valid()
|
||||
|
||||
|
||||
def test_category_delete_with_expenses_is_protected(auth_client, user, account, category):
|
||||
Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("10"), date=date.today())
|
||||
|
||||
response = auth_client.post(reverse('category_delete', args=[category.pk]))
|
||||
|
||||
assert response.status_code == 302
|
||||
assert Category.objects.filter(pk=category.pk).exists()
|
||||
messages = [str(m) for m in get_messages(response.wsgi_request)]
|
||||
assert any("gastos asociados" in m for m in messages)
|
||||
|
||||
|
||||
def test_category_delete_without_expenses_succeeds(auth_client, user):
|
||||
cat = Category.objects.create(name="Vacia", owner=user)
|
||||
|
||||
response = auth_client.post(reverse('category_delete', args=[cat.pk]))
|
||||
|
||||
assert response.status_code == 302
|
||||
assert not Category.objects.filter(pk=cat.pk).exists()
|
||||
@ -2,7 +2,7 @@ import pytest
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from django.urls import reverse
|
||||
from expenses.models import Account, Expense, Category
|
||||
from expenses.models import Expense, Category
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@ -11,53 +11,39 @@ def test_dashboard_requires_login(client):
|
||||
response = client.get(url)
|
||||
assert response.status_code == 302
|
||||
|
||||
def test_dashboard_logged_user_can_access(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(
|
||||
username='test',
|
||||
password='1234'
|
||||
)
|
||||
client.login(username='test', password='1234')
|
||||
|
||||
def test_dashboard_logged_user_can_access(auth_client):
|
||||
url = reverse('dashboard')
|
||||
response = client.get(url)
|
||||
response = auth_client.get(url)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_dashboard_groups_by_category(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(
|
||||
username='test',
|
||||
password='1234'
|
||||
)
|
||||
client.login(username='test', password='1234')
|
||||
|
||||
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)
|
||||
|
||||
general_account = Account.objects.create(name='General', owner=user, initial_balance=300, active=True)
|
||||
|
||||
Expense.objects.create(
|
||||
owner=user,
|
||||
category=food,
|
||||
amount=Decimal('10'),
|
||||
date=date(2024, 1, 1),
|
||||
account=general_account,
|
||||
account=account,
|
||||
)
|
||||
Expense.objects.create(
|
||||
owner=user,
|
||||
category=food,
|
||||
amount=Decimal('5'),
|
||||
date=date(2024, 1, 2),
|
||||
account=general_account,
|
||||
account=account,
|
||||
)
|
||||
Expense.objects.create(
|
||||
owner=user,
|
||||
category=rent,
|
||||
amount=Decimal('20'),
|
||||
date=date(2024, 1, 3),
|
||||
account=general_account,
|
||||
account=account,
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
response = auth_client.get(
|
||||
reverse('dashboard'),
|
||||
{'year': 2024}
|
||||
)
|
||||
@ -67,39 +53,30 @@ def test_dashboard_groups_by_category(client, django_user_model):
|
||||
assert {'category__name': 'Food', 'total': Decimal('15')} in data
|
||||
assert {'category__name': 'Rent', 'total': Decimal('20')} in data
|
||||
|
||||
def test_dashboard_filters_by_year(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(
|
||||
username='test',
|
||||
password='1234'
|
||||
)
|
||||
client.login(username='test', password='1234')
|
||||
|
||||
cat = Category.objects.create(name='General', owner=user)
|
||||
general_account = Account.objects.create(name='General', owner=user, initial_balance=300, active=True)
|
||||
|
||||
def test_dashboard_filters_by_year(auth_client, user, account, category):
|
||||
Expense.objects.create(
|
||||
owner=user,
|
||||
category=cat,
|
||||
category=category,
|
||||
amount=10,
|
||||
date=date(2023, 5, 1),
|
||||
account=general_account,
|
||||
account=account,
|
||||
)
|
||||
|
||||
Expense.objects.create(
|
||||
owner=user,
|
||||
category=cat,
|
||||
category=category,
|
||||
amount=20,
|
||||
date=date(2024, 5, 1),
|
||||
account=general_account,
|
||||
account=account,
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
response = auth_client.get(
|
||||
reverse('dashboard'),
|
||||
{'year': 2024},
|
||||
)
|
||||
|
||||
by_month = list(response.context['by_month'])
|
||||
chart_data = response.context['chart_data']
|
||||
|
||||
totals = [row['total'] for row in by_month]
|
||||
assert Decimal('20') in totals
|
||||
assert Decimal('10') not in totals
|
||||
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
|
||||
|
||||
61
expenses_manager/expenses/tests/test_deletes.py
Normal file
61
expenses_manager/expenses/tests/test_deletes.py
Normal file
@ -0,0 +1,61 @@
|
||||
import pytest
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from django.urls import reverse
|
||||
from expenses.models import Goal, Tag, Income
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_goal_delete_get_does_not_delete(auth_client, user):
|
||||
goal = Goal.objects.create(owner=user, name="Meta", target_amount=Decimal("100"))
|
||||
|
||||
response = auth_client.get(reverse('goal_delete', args=[goal.pk]))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert Goal.objects.filter(pk=goal.pk).exists()
|
||||
|
||||
|
||||
def test_goal_delete_post_deletes(auth_client, user):
|
||||
goal = Goal.objects.create(owner=user, name="Meta", target_amount=Decimal("100"))
|
||||
|
||||
response = auth_client.post(reverse('goal_delete', args=[goal.pk]))
|
||||
|
||||
assert response.status_code == 302
|
||||
assert not Goal.objects.filter(pk=goal.pk).exists()
|
||||
|
||||
|
||||
def test_tag_delete_get_does_not_delete(auth_client, user):
|
||||
tag = Tag.objects.create(name="Food", owner=user)
|
||||
|
||||
response = auth_client.get(reverse('tag_delete', args=[tag.pk]))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert Tag.objects.filter(pk=tag.pk).exists()
|
||||
|
||||
|
||||
def test_tag_delete_post_deletes(auth_client, user):
|
||||
tag = Tag.objects.create(name="Food", owner=user)
|
||||
|
||||
response = auth_client.post(reverse('tag_delete', args=[tag.pk]))
|
||||
|
||||
assert response.status_code == 302
|
||||
assert not Tag.objects.filter(pk=tag.pk).exists()
|
||||
|
||||
|
||||
def test_income_delete_get_does_not_delete(auth_client, user, account):
|
||||
income = Income.objects.create(owner=user, account=account, name="Nomina", amount=Decimal("500"), date=date.today())
|
||||
|
||||
response = auth_client.get(reverse('income_delete', args=[income.pk]))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert Income.objects.filter(pk=income.pk).exists()
|
||||
|
||||
|
||||
def test_income_delete_post_deletes(auth_client, user, account):
|
||||
income = Income.objects.create(owner=user, account=account, name="Nomina", amount=Decimal("500"), date=date.today())
|
||||
|
||||
response = auth_client.post(reverse('income_delete', args=[income.pk]))
|
||||
|
||||
assert response.status_code == 302
|
||||
assert not Income.objects.filter(pk=income.pk).exists()
|
||||
42
expenses_manager/expenses/tests/test_expense_list.py
Normal file
42
expenses_manager/expenses/tests/test_expense_list.py
Normal file
@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from django.urls import reverse
|
||||
from expenses.models import Expense, Tag
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_tag_filter_with_non_numeric_value_returns_200(auth_client, user, account, category):
|
||||
Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("10"), date=date.today())
|
||||
|
||||
response = auth_client.get(reverse('expense_list'), {'tag': 'abc'})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert len(response.context['page_obj']) == 1
|
||||
|
||||
|
||||
def test_tag_filter_with_valid_tag_returns_only_matching_expenses(auth_client, user, account, category):
|
||||
tag = Tag.objects.create(name="Food", owner=user)
|
||||
tagged = Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("10"), date=date.today())
|
||||
tagged.tags.add(tag)
|
||||
Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("20"), date=date.today())
|
||||
|
||||
response = auth_client.get(reverse('expense_list'), {'tag': str(tag.pk)})
|
||||
|
||||
results = list(response.context['page_obj'])
|
||||
assert results == [tagged]
|
||||
|
||||
|
||||
def test_tag_filter_multiple_tags_are_ORed(auth_client, user, account, category):
|
||||
tag1 = Tag.objects.create(name="Food", owner=user)
|
||||
tag2 = Tag.objects.create(name="Transport", owner=user)
|
||||
expense1 = Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("10"), date=date.today())
|
||||
expense1.tags.add(tag1)
|
||||
expense2 = Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("20"), date=date.today())
|
||||
expense2.tags.add(tag2)
|
||||
|
||||
response = auth_client.get(reverse('expense_list'), {'tag': [str(tag1.pk), str(tag2.pk)]})
|
||||
|
||||
results = set(response.context['page_obj'])
|
||||
assert results == {expense1, expense2}
|
||||
@ -1,39 +1,27 @@
|
||||
import pytest
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from django.urls import reverse
|
||||
from expenses.models import Expense, Category, Tag, Account
|
||||
from expenses.models import Expense, Tag
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
def test_expense_can_have_tags(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(username='test', password='1234')
|
||||
client.login(username='test', password='1234')
|
||||
|
||||
def test_expense_can_have_tags(user, category, account):
|
||||
tag = Tag.objects.create(name='Food', owner=user)
|
||||
category = Category.objects.create(name='General', owner=user)
|
||||
|
||||
general_account = Account.objects.create(name='General', owner=user, initial_balance=300, active=True)
|
||||
|
||||
expense = Expense.objects.create(
|
||||
owner=user,
|
||||
category=category,
|
||||
amount=10,
|
||||
date=date.today(),
|
||||
account=general_account,
|
||||
account=account,
|
||||
)
|
||||
expense.tags.add(tag)
|
||||
|
||||
assert expense.tags.count() == 1
|
||||
assert tag in expense.tags.all()
|
||||
|
||||
def test_user_can_create_tag(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(
|
||||
username='test', password='1234'
|
||||
)
|
||||
client.login(username='test', password='1234')
|
||||
|
||||
response = client.post(
|
||||
def test_user_can_create_tag(auth_client, user):
|
||||
response = auth_client.post(
|
||||
reverse('tag_create'),
|
||||
{'name': 'Food'}
|
||||
)
|
||||
|
||||
69
expenses_manager/expenses/tests/test_fuel.py
Normal file
69
expenses_manager/expenses/tests/test_fuel.py
Normal file
@ -0,0 +1,69 @@
|
||||
import pytest
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from django.urls import reverse
|
||||
from expenses.models import Expense, FuelEntry
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def _make_fuel_entry(user, account, category, odometer, liters, amount=Decimal("50")):
|
||||
expense = Expense.objects.create(
|
||||
owner=user, account=account, category=category, amount=amount, date=date.today()
|
||||
)
|
||||
return FuelEntry.objects.create(expense=expense, odometer=odometer, liters=liters)
|
||||
|
||||
|
||||
def test_price_per_liter_divides_amount_by_liters(user, account, category):
|
||||
entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("25"), amount=Decimal("50"))
|
||||
|
||||
assert entry.price_per_liter() == Decimal("2")
|
||||
|
||||
|
||||
def test_price_per_liter_zero_when_no_liters(user, account, category):
|
||||
entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("0"))
|
||||
|
||||
assert entry.price_per_liter() == 0
|
||||
|
||||
|
||||
def test_km_since_previous_none_on_first_fillup(user, account, category):
|
||||
entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20"))
|
||||
|
||||
assert entry.km_since_previous() is None
|
||||
|
||||
|
||||
def test_km_since_previous_computes_delta_from_prior_entry(user, account, category):
|
||||
_make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20"))
|
||||
second = _make_fuel_entry(user, account, category, odometer=1300, liters=Decimal("24"))
|
||||
|
||||
assert second.km_since_previous() == 300
|
||||
|
||||
|
||||
def test_consumption_none_when_no_previous_entry(user, account, category):
|
||||
entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20"))
|
||||
|
||||
assert entry.consumption() is None
|
||||
|
||||
|
||||
def test_consumption_computes_liters_per_100km(user, account, category):
|
||||
_make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20"))
|
||||
second = _make_fuel_entry(user, account, category, odometer=1300, liters=Decimal("24"))
|
||||
|
||||
assert second.consumption() == Decimal("8")
|
||||
|
||||
|
||||
def test_fuel_create_invalid_post_returns_200_not_500(auth_client):
|
||||
response = auth_client.post(reverse('fuel_create'), {})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert FuelEntry.objects.count() == 0
|
||||
|
||||
|
||||
def test_fuel_delete_post_cascades_to_fuelentry(auth_client, user, account, category):
|
||||
entry = _make_fuel_entry(user, account, category, odometer=1000, liters=Decimal("20"))
|
||||
expense = entry.expense
|
||||
|
||||
response = auth_client.post(reverse('fuel_delete', args=[expense.pk]))
|
||||
|
||||
assert not Expense.objects.filter(pk=expense.pk).exists()
|
||||
assert not FuelEntry.objects.filter(pk=entry.pk).exists()
|
||||
196
expenses_manager/expenses/tests/test_goals.py
Normal file
196
expenses_manager/expenses/tests/test_goals.py
Normal file
@ -0,0 +1,196 @@
|
||||
import pytest
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from expenses.models import Account, Category, Expense, Goal
|
||||
from expenses.forms import GoalForm
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def _last_month_date():
|
||||
today = date.today()
|
||||
if today.month == 1:
|
||||
return date(today.year - 1, 12, 15)
|
||||
return date(today.year, today.month - 1, 15)
|
||||
|
||||
|
||||
def test_payment_goal_sums_expenses_since_start_date(user, account, category):
|
||||
start = date(2024, 6, 1)
|
||||
Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("10"), date=date(2024, 5, 1))
|
||||
Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("20"), date=date(2024, 6, 15))
|
||||
|
||||
goal = Goal.objects.create(
|
||||
owner=user, name="Pago", kind=Goal.KIND_PAYMENT, category=category,
|
||||
include_subcategories=False, start_date=start, target_amount=Decimal("100"),
|
||||
)
|
||||
|
||||
assert goal.progress() == Decimal("20")
|
||||
|
||||
|
||||
def test_payment_goal_include_subcategories_true_includes_child_expenses(user, account, category):
|
||||
child = Category.objects.create(owner=user, name="Sub", parent=category)
|
||||
Expense.objects.create(owner=user, account=account, category=child, amount=Decimal("15"), date=date.today())
|
||||
|
||||
goal = Goal.objects.create(
|
||||
owner=user, name="Pago", kind=Goal.KIND_PAYMENT, category=category,
|
||||
include_subcategories=True, target_amount=Decimal("100"),
|
||||
)
|
||||
|
||||
assert goal.progress() == Decimal("15")
|
||||
|
||||
|
||||
def test_payment_goal_include_subcategories_false_excludes_child_expenses(user, account, category):
|
||||
child = Category.objects.create(owner=user, name="Sub", parent=category)
|
||||
Expense.objects.create(owner=user, account=account, category=child, amount=Decimal("15"), date=date.today())
|
||||
|
||||
goal = Goal.objects.create(
|
||||
owner=user, name="Pago", kind=Goal.KIND_PAYMENT, category=category,
|
||||
include_subcategories=False, target_amount=Decimal("100"),
|
||||
)
|
||||
|
||||
assert goal.progress() == Decimal("0")
|
||||
|
||||
|
||||
def test_budget_goal_period_month_counts_only_current_month(user, account, category):
|
||||
Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("30"), date=date.today())
|
||||
Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("999"), date=_last_month_date())
|
||||
|
||||
goal = Goal.objects.create(
|
||||
owner=user, name="Presupuesto", kind=Goal.KIND_BUDGET, category=category,
|
||||
period=Goal.PERIOD_MONTH, target_amount=Decimal("100"),
|
||||
)
|
||||
|
||||
assert goal.progress() == Decimal("30")
|
||||
|
||||
|
||||
def test_budget_goal_is_exceeded_true_over_target(user, account, category):
|
||||
Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("150"), date=date.today())
|
||||
|
||||
goal = Goal.objects.create(
|
||||
owner=user, name="Presupuesto", kind=Goal.KIND_BUDGET, category=category,
|
||||
period=Goal.PERIOD_MONTH, target_amount=Decimal("100"),
|
||||
)
|
||||
|
||||
assert goal.is_exceeded() is True
|
||||
|
||||
|
||||
def test_budget_goal_is_exceeded_false_under_target(user, account, category):
|
||||
Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("50"), date=date.today())
|
||||
|
||||
goal = Goal.objects.create(
|
||||
owner=user, name="Presupuesto", kind=Goal.KIND_BUDGET, category=category,
|
||||
period=Goal.PERIOD_MONTH, target_amount=Decimal("100"),
|
||||
)
|
||||
|
||||
assert goal.is_exceeded() is False
|
||||
|
||||
|
||||
def test_is_exceeded_always_false_for_non_budget_kind(user, account, category):
|
||||
Expense.objects.create(owner=user, account=account, category=category, amount=Decimal("50"), date=date.today())
|
||||
|
||||
goal = Goal.objects.create(
|
||||
owner=user, name="Pago", kind=Goal.KIND_PAYMENT, category=category, target_amount=Decimal("10"),
|
||||
)
|
||||
|
||||
assert goal.progress() > goal.target_amount
|
||||
assert goal.is_exceeded() is False
|
||||
|
||||
|
||||
def test_saving_goal_progress_equals_account_balance(user):
|
||||
acc = Account.objects.create(owner=user, name="Ahorros", initial_balance=Decimal("500"))
|
||||
|
||||
goal = Goal.objects.create(
|
||||
owner=user, name="Ahorro", kind=Goal.KIND_SAVING, account=acc, target_amount=Decimal("1000"),
|
||||
)
|
||||
|
||||
assert goal.progress() == acc.current_balance()
|
||||
|
||||
|
||||
def test_bar_width_caps_at_100_above_target(user):
|
||||
acc = Account.objects.create(owner=user, name="Ahorros", initial_balance=Decimal("250"))
|
||||
goal = Goal.objects.create(owner=user, name="Ahorro", kind=Goal.KIND_SAVING, account=acc, target_amount=Decimal("100"))
|
||||
|
||||
assert goal.percentage() == Decimal("250")
|
||||
assert goal.bar_width() == 100.0
|
||||
|
||||
|
||||
def test_bar_width_uncapped_below_100(user):
|
||||
acc = Account.objects.create(owner=user, name="Ahorros", initial_balance=Decimal("40"))
|
||||
goal = Goal.objects.create(owner=user, name="Ahorro", kind=Goal.KIND_SAVING, account=acc, target_amount=Decimal("100"))
|
||||
|
||||
assert goal.bar_width() == 40.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("amount,expected_state", [
|
||||
(Decimal("50"), "ok"),
|
||||
(Decimal("90"), "warning"),
|
||||
(Decimal("150"), "danger"),
|
||||
])
|
||||
def test_progress_state_budget_thresholds(user, account, category, amount, expected_state):
|
||||
Expense.objects.create(owner=user, account=account, category=category, amount=amount, date=date.today())
|
||||
goal = Goal.objects.create(
|
||||
owner=user, name="Presupuesto", kind=Goal.KIND_BUDGET, category=category, target_amount=Decimal("100"),
|
||||
)
|
||||
|
||||
assert goal.progress_state() == expected_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize("amount,expected_state", [
|
||||
(Decimal("50"), "ok"),
|
||||
(Decimal("100"), "complete"),
|
||||
(Decimal("150"), "complete"),
|
||||
])
|
||||
def test_progress_state_non_budget_thresholds(user, account, category, amount, expected_state):
|
||||
Expense.objects.create(owner=user, account=account, category=category, amount=amount, date=date.today())
|
||||
goal = Goal.objects.create(
|
||||
owner=user, name="Pago", kind=Goal.KIND_PAYMENT, category=category, target_amount=Decimal("100"),
|
||||
)
|
||||
|
||||
assert goal.progress_state() == expected_state
|
||||
|
||||
|
||||
def test_goalform_budget_requires_period(user, category):
|
||||
data = {"name": "Presupuesto", "kind": Goal.KIND_BUDGET, "target_amount": "100", "category": str(category.pk), "period": Goal.PERIOD_NONE}
|
||||
form = GoalForm(data=data, user=user)
|
||||
|
||||
assert not form.is_valid()
|
||||
assert "period" in form.errors
|
||||
|
||||
|
||||
def test_goalform_saving_requires_account(user):
|
||||
data = {"name": "Ahorro", "kind": Goal.KIND_SAVING, "target_amount": "500", "period": Goal.PERIOD_NONE}
|
||||
form = GoalForm(data=data, user=user)
|
||||
|
||||
assert not form.is_valid()
|
||||
assert "account" in form.errors
|
||||
|
||||
|
||||
def test_goalform_payment_requires_category(user):
|
||||
data = {"name": "Pago", "kind": Goal.KIND_PAYMENT, "target_amount": "50", "period": Goal.PERIOD_NONE}
|
||||
form = GoalForm(data=data, user=user)
|
||||
|
||||
assert not form.is_valid()
|
||||
assert "category" in form.errors
|
||||
|
||||
|
||||
def test_goalform_budget_requires_category(user):
|
||||
data = {"name": "Presupuesto", "kind": Goal.KIND_BUDGET, "target_amount": "50", "period": Goal.PERIOD_MONTH}
|
||||
form = GoalForm(data=data, user=user)
|
||||
|
||||
assert not form.is_valid()
|
||||
assert "category" in form.errors
|
||||
|
||||
|
||||
def test_goalform_valid_payment_saves_goal(user, category):
|
||||
data = {
|
||||
"name": "Ahorro viaje", "kind": Goal.KIND_PAYMENT, "target_amount": "200",
|
||||
"category": str(category.pk), "period": Goal.PERIOD_NONE,
|
||||
}
|
||||
form = GoalForm(data=data, user=user)
|
||||
|
||||
assert form.is_valid(), form.errors
|
||||
goal = form.save(commit=False)
|
||||
goal.owner = user
|
||||
goal.save()
|
||||
|
||||
assert Goal.objects.filter(pk=goal.pk, name="Ahorro viaje", kind=Goal.KIND_PAYMENT, category=category).exists()
|
||||
@ -1,18 +1,10 @@
|
||||
import pytest
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from django.urls import reverse
|
||||
from expenses.models import Income, Account
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
def test_income_increases_account_balance(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(
|
||||
username='test',
|
||||
password='1234'
|
||||
)
|
||||
client.login(username='test', password='1234')
|
||||
|
||||
def test_income_increases_account_balance(user):
|
||||
general_account = Account.objects.create(name='General', owner=user, initial_balance=1000, active=True)
|
||||
|
||||
Income.objects.create(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user