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 %}
- {% if field.name == "tags" %} + {% if field.name == "tags" or field.name == "goals" %} {{ field.label_tag }}
{% for checkbox in field %} diff --git a/expenses_manager/expenses/templates/goals/form.html b/expenses_manager/expenses/templates/goals/form.html index 531e076..11c5d62 100644 --- a/expenses_manager/expenses/templates/goals/form.html +++ b/expenses_manager/expenses/templates/goals/form.html @@ -25,7 +25,8 @@ Para ponerte un límite de gasto que no quieres superar. El progreso se reinicia cada mes o cada año, según el periodo que elijas. Aquí acercarse al 100% es un aviso, no un logro: la barra pasa a - naranja al llegar al 80% y a rojo si te pasas del límite. + naranja al llegar al 80% y a rojo si te pasas del límite. Cuenta + los gastos de su categoría y además los que le vincules a mano.

Ahorro

@@ -35,6 +36,46 @@ categoría.

+

Cobro pendiente

+

+ El inverso del de pago: para dinero que te tienen que devolver y vas + cobrando a plazos. El progreso es la suma de los ingresos que le + vincules desde la fecha de inicio, y llegar al 100% es haberlo + cobrado todo. No lleva categoría ni cuenta, porque los ingresos no + tienen categoría: se mide solo con los ingresos que marques. +

+ +

Vincular movimientos

+

+ Además de contar una categoría entera, un objetivo puede contar + gastos o ingresos concretos. Se marcan de dos maneras: con el campo + Objetivos del formulario del gasto o del ingreso, o + desde Vincular en la lista de objetivos, que permite + marcar varios de golpe. +

+

+ Lo admiten tres tipos, y cada uno vincula lo que mide: +

+ +

+ La categoría y las vinculaciones conviven: un presupuesto de ocio + puede contar su categoría y además un gasto suelto de otra que + quieras que compute. Un gasto que esté en la categoría y también + vinculado a mano cuenta una sola vez. +

+

+ Los dos movimientos de un traspaso no se pueden vincular (mover + dinero entre tus cuentas no es un gasto ni un cobro), pero su + comisión sí, porque es gasto real. +

+

Los demás campos