diff --git a/expenses_manager/expenses/forms.py b/expenses_manager/expenses/forms.py index 0cfdb77..6442e32 100644 --- a/expenses_manager/expenses/forms.py +++ b/expenses_manager/expenses/forms.py @@ -25,7 +25,43 @@ def _hide_is_transfer_when_editing(form): form.fields.pop("is_transfer", None) -class ExpenseForm(forms.ModelForm): +class _GoalLinksMixin: + """Campo "Objetivos" en el formulario de un movimiento. + + `goals` es el lado inverso de Goal.linked_expenses / linked_incomes, y un + ModelForm no guarda relaciones inversas: hay que poblarlo al abrir y + escribirlo a mano al guardar. Lo comparten gastos e ingresos para que qué + objetivo admite cada movimiento se decida en un solo sitio + (Goal.KINDS_LINKING_*, en el modelo). + """ + + goal_kinds = () + + def setup_goal_links(self, user): + self.fields["goals"] = forms.ModelMultipleChoiceField( + queryset=Goal.objects.filter(owner=user, kind__in=self.goal_kinds), + required=False, + label="Objetivos", + widget=forms.CheckboxSelectMultiple(), + help_text="Este movimiento cuenta en los objetivos que marques.", + ) + + if self.instance.pk: + self.initial["goals"] = list( + self.instance.goals.values_list("pk", flat=True) + ) + + def _save_m2m(self): + super()._save_m2m() + + # Si el formulario se construyó sin usuario no hay campo, y entonces no + # se toca nada: un set() a ciegas borraría las vinculaciones que ya haya. + if "goals" in self.cleaned_data: + self.instance.goals.set(self.cleaned_data["goals"]) + + +class ExpenseForm(_GoalLinksMixin, forms.ModelForm): + goal_kinds = Goal.KINDS_LINKING_EXPENSES def __init__(self, *args, **kwargs): # Get user @@ -40,6 +76,7 @@ class ExpenseForm(forms.ModelForm): owner=user, active=True ) self.fields["date"].input_formats = ["%Y-%m-%d"] + self.setup_goal_links(user) _hide_is_transfer_when_editing(self) @@ -73,7 +110,9 @@ class AccountForm(forms.ModelForm): fields = ["name", "initial_balance", "active"] -class IncomeForm(forms.ModelForm): +class IncomeForm(_GoalLinksMixin, forms.ModelForm): + goal_kinds = Goal.KINDS_LINKING_INCOMES + class Meta: model = Income fields = ["account", "name", "amount", "date", "is_transfer"] @@ -86,6 +125,7 @@ class IncomeForm(forms.ModelForm): owner=user, active=True, ) + self.setup_goal_links(user) _hide_is_transfer_when_editing(self) @@ -225,6 +265,18 @@ class GoalForm(forms.ModelForm): "Selecciona la cuenta donde se acumula el ahorro.", ) cleaned["category"] = None + + if kind == Goal.KIND_COLLECTION: + # Un cobro pendiente se mide solo con los ingresos que se le + # vinculen, así que no lleva categoría ni cuenta; la fecha de inicio + # sí, porque es la que acota desde cuándo cuentan. + if not cleaned.get("start_date"): + self.add_error( + "start_date", + "Indica desde cuándo se cuentan los cobros.", + ) + cleaned["category"] = None + cleaned["account"] = None if kind == Goal.KIND_BUDGET: if cleaned.get("period") == Goal.PERIOD_NONE: diff --git a/expenses_manager/expenses/templates/expenses/_form_fields.html b/expenses_manager/expenses/templates/expenses/_form_fields.html index d4a3cd3..68792d5 100644 --- a/expenses_manager/expenses/templates/expenses/_form_fields.html +++ b/expenses_manager/expenses/templates/expenses/_form_fields.html @@ -1,6 +1,6 @@ {% for field in form %}