diff --git a/expenses_manager/expenses/models.py b/expenses_manager/expenses/models.py index 95339b9..688e0d9 100644 --- a/expenses_manager/expenses/models.py +++ b/expenses_manager/expenses/models.py @@ -3,9 +3,10 @@ from decimal import Decimal from django.db import models from django.conf import settings from django.db.models import Sum +from functools import cached_property +from django.utils.text import slugify from django.db.models.fields import related from django.db.models.functions import ExtractMonth -from django.utils.text import slugify class Category(models.Model): @@ -357,7 +358,15 @@ class Goal(models.Model): return self.start_date + @cached_property def progress(self): + """Progreso del objetivo. Memoizado: se calcula una vez por instancia. + + Ojo: es una cached_property, no un método. Se accede como `goal.progress` + (sin paréntesis), tanto en código como en plantillas. El valor se cachea + en la instancia la primera vez; si en la misma petición se modifican + gastos y se necesita recalcular, habría que crear una instancia nueva. + """ if self.kind == self.KIND_SAVING: # Provisional: saldo de la cuenta asociada. Cuando exista el # módulo de inversiones, esta rama es lo único que hay que tocar. @@ -384,7 +393,7 @@ class Goal(models.Model): def percentage(self): if not self.target_amount: return 0 - return (self.progress() / self.target_amount) * 100 + return (self.progress / self.target_amount) * 100 def bar_width(self): return min(float(self.percentage()), 100) @@ -405,7 +414,7 @@ class Goal(models.Model): """True si es un presupuesto que ya se ha pasado del límite.""" return ( self.kind == self.KIND_BUDGET - and self.progress() > self.target_amount + and self.progress > self.target_amount ) def __str__(self): diff --git a/expenses_manager/expenses/tests/test_goals.py b/expenses_manager/expenses/tests/test_goals.py index 2411c8e..e7717ae 100644 --- a/expenses_manager/expenses/tests/test_goals.py +++ b/expenses_manager/expenses/tests/test_goals.py @@ -24,7 +24,7 @@ def test_payment_goal_sums_expenses_since_start_date(user, account, category): include_subcategories=False, start_date=start, target_amount=Decimal("100"), ) - assert goal.progress() == Decimal("20") + assert goal.progress == Decimal("20") def test_payment_goal_include_subcategories_true_includes_child_expenses(user, account, category): @@ -36,7 +36,7 @@ def test_payment_goal_include_subcategories_true_includes_child_expenses(user, a include_subcategories=True, target_amount=Decimal("100"), ) - assert goal.progress() == Decimal("15") + assert goal.progress == Decimal("15") def test_payment_goal_include_subcategories_false_excludes_child_expenses(user, account, category): @@ -48,7 +48,7 @@ def test_payment_goal_include_subcategories_false_excludes_child_expenses(user, include_subcategories=False, target_amount=Decimal("100"), ) - assert goal.progress() == Decimal("0") + assert goal.progress == Decimal("0") def test_budget_goal_period_month_counts_only_current_month(user, account, category): @@ -60,7 +60,7 @@ def test_budget_goal_period_month_counts_only_current_month(user, account, categ period=Goal.PERIOD_MONTH, target_amount=Decimal("100"), ) - assert goal.progress() == Decimal("30") + assert goal.progress == Decimal("30") def test_budget_goal_is_exceeded_true_over_target(user, account, category): @@ -92,7 +92,7 @@ def test_is_exceeded_always_false_for_non_budget_kind(user, account, category): owner=user, name="Pago", kind=Goal.KIND_PAYMENT, category=category, target_amount=Decimal("10"), ) - assert goal.progress() > goal.target_amount + assert goal.progress > goal.target_amount assert goal.is_exceeded() is False @@ -103,7 +103,7 @@ def test_saving_goal_progress_equals_account_balance(user): owner=user, name="Ahorro", kind=Goal.KIND_SAVING, account=acc, target_amount=Decimal("1000"), ) - assert goal.progress() == acc.current_balance() + assert goal.progress == acc.current_balance() def test_bar_width_caps_at_100_above_target(user):