expenses_manager/expenses_manager/expenses/forms.py

299 lines
9.3 KiB
Python

from django import forms
from .csv_export import DEFAULT_DIALECT
from .models import (
Expense,
Category,
Income,
Tag,
Account,
FuelEntry,
Goal,
Transfer,
)
def _hide_is_transfer_when_editing(form):
"""Quita la casilla de traspaso al editar un movimiento que ya existe.
Al crear, la casilla es un atajo: lleva al formulario de traspaso, que es
quien crea las dos patas. Al editar no significa nada, porque convertir un
gasto suelto en traspaso no es editarlo (habría que inventarse la otra
cuenta), y marcarla solo dejaría el movimiento marcado pero sin pareja: justo
el estado a medias que Transfer viene a evitar.
"""
if form.instance.pk:
form.fields.pop("is_transfer", None)
class ExpenseForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
# Get user
user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
if user:
self.fields["category"].queryset = Category.objects.filter(owner=user)
self.fields["tags"].queryset = Tag.objects.filter(owner=user)
self.fields["account"].queryset = Account.objects.filter(
owner=user, active=True
)
self.fields["date"].input_formats = ["%Y-%m-%d"]
_hide_is_transfer_when_editing(self)
class Meta:
model = Expense
fields = [
"date",
"amount",
"description",
"category",
"account",
"tags",
"is_transfer",
]
widgets = {
"date": forms.DateInput(format="%Y-%m-%d", attrs={"type": "date"}),
"tags": forms.CheckboxSelectMultiple(),
}
class TagForm(forms.ModelForm):
class Meta:
model = Tag
fields = ["name"]
class AccountForm(forms.ModelForm):
class Meta:
model = Account
fields = ["name", "initial_balance", "active"]
class IncomeForm(forms.ModelForm):
class Meta:
model = Income
fields = ["account", "name", "amount", "date", "is_transfer"]
widgets = {"date": forms.DateInput(format="%Y-%m-%d", attrs={"type": "date"})}
def __init__(self, *args, **kwargs):
user = kwargs.pop("user")
super().__init__(*args, **kwargs)
self.fields["account"].queryset = Account.objects.filter(
owner=user,
active=True,
)
_hide_is_transfer_when_editing(self)
class TransferForm(forms.ModelForm):
"""Formulario del traspaso. Las patas no se editan aquí ni en ninguna parte:
las deriva Transfer.sync() de estos campos."""
class Meta:
model = Transfer
fields = ["date", "amount", "from_account", "to_account", "fee_amount"]
widgets = {"date": forms.DateInput(format="%Y-%m-%d", attrs={"type": "date"})}
labels = {
"from_account": "Cuenta de origen",
"to_account": "Cuenta de destino",
"fee_amount": "Comisión",
}
help_texts = {
"fee_amount": (
"Déjalo vacío si el traspaso no tiene comisión. La comisión sí "
"cuenta como gasto real en el dashboard y en los presupuestos."
),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop("user")
super().__init__(*args, **kwargs)
accounts = Account.objects.filter(owner=user, active=True)
self.fields["from_account"].queryset = accounts
self.fields["to_account"].queryset = accounts
self.fields["date"].input_formats = ["%Y-%m-%d"]
class FuelEntryForm(forms.ModelForm):
odometer = forms.DecimalField(label="Kilómetros actuales")
liters = forms.DecimalField(max_digits=8, decimal_places=2, label="Litros")
class Meta:
model = Expense
fields = ["date", "amount", "account"]
widgets = {
"date": forms.DateInput(format="%Y-%m-%d", attrs={"type":"date"}),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop("user")
super().__init__(*args, **kwargs)
if user:
self.fields["account"].queryset = user.accounts.filter(active=True)
if self.instance.pk and hasattr(self.instance, "fuel_data"):
fuel = self.instance.fuel_data
self.fields["odometer"].initial = fuel.odometer
self.fields["liters"].initial = fuel.liters
class CategoryForm(forms.ModelForm):
class Meta:
model = Category
fields = ["name", "parent"]
def __init__(self, *args, **kwargs):
user = kwargs.pop("user")
super().__init__(*args, **kwargs)
if user:
queryset = Category.objects.filter(owner=user)
if self.instance.pk:
queryset = queryset.exclude(
pk__in=self.instance.descendant_ids()
)
self.fields["parent"].queryset = queryset
def clean_parent(self):
parent = self.cleaned_data.get("parent")
if parent and self.instance.pk:
if parent.pk in self.instance.descendant_ids():
raise forms.ValidationError(
"Una categoría no puede ser su propio padre ni depender "
"de una de sus subcategorías."
)
return parent
class GoalForm(forms.ModelForm):
class Meta:
model = Goal
fields = [
"name",
"kind",
"target_amount",
"category",
"include_subcategories",
"account",
"start_date",
"period",
"show_on_home",
]
widgets = {
"start_date": forms.DateInput(
format="%Y-%m-%d", attrs={"type": "date"}
),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['category'].queryset = user.categories.all()
self.fields['account'].queryset = user.accounts.filter(active=True)
self.fields['category'].required = False
self.fields['account'].required = False
self.fields['start_date'].input_formats = ["%Y-%m-%d"]
def clean(self):
cleaned = super().clean()
kind = cleaned.get("kind")
if kind in (Goal.KIND_PAYMENT, Goal.KIND_BUDGET):
if not cleaned.get('category'):
self.add_error(
"category",
"Selecciona una categoría para este tipo de objetivo.",
)
cleaned["account"] = None
if kind == Goal.KIND_SAVING:
if not cleaned.get("account"):
self.add_error(
"account",
"Selecciona la cuenta donde se acumula el ahorro.",
)
cleaned["category"] = None
if kind == Goal.KIND_BUDGET:
if cleaned.get("period") == Goal.PERIOD_NONE:
self.add_error(
"period",
"Un presupuesto necesita un periodo: mensual o anual",
)
else:
cleaned["period"] = Goal.PERIOD_NONE
return cleaned
class ExportForm(forms.Form):
"""Filtros de la exportación a CSV. Valida la pantalla y la descarga.
Los filtros son a propósito solo rango de fechas y cuenta: la gracia de un
CSV es filtrar fuera, y duplicar aquí la fila de filtros avanzados de
expense_list serían dos UIs que divergen.
"""
KIND_CHOICES = [
("expenses", "Gastos"),
("incomes", "Ingresos"),
("transfers", "Traspasos"),
]
FMT_CHOICES = [
("excel", "Excel español — separador ; y decimales con coma"),
("std", "CSV estándar — separador , y decimales con punto"),
]
kind = forms.ChoiceField(choices=KIND_CHOICES, label="Qué exportar")
date_from = forms.DateField(
required=False,
label="Desde",
widget=forms.DateInput(format="%Y-%m-%d", attrs={"type": "date"}),
)
date_to = forms.DateField(
required=False,
label="Hasta",
widget=forms.DateInput(format="%Y-%m-%d", attrs={"type": "date"}),
)
account = forms.ModelChoiceField(
queryset=Account.objects.none(),
required=False,
label="Cuenta",
empty_label="Todas las cuentas",
)
fmt = forms.ChoiceField(
choices=FMT_CHOICES, label="Formato", initial=DEFAULT_DIALECT
)
def __init__(self, *args, **kwargs):
user = kwargs.pop("user")
super().__init__(*args, **kwargs)
# Activas e inactivas: se exporta histórico, y una cuenta cerrada tiene
# movimientos que interesan.
self.fields["account"].queryset = Account.objects.filter(owner=user)
self.fields["date_from"].input_formats = ["%Y-%m-%d"]
self.fields["date_to"].input_formats = ["%Y-%m-%d"]
def clean(self):
cleaned = super().clean()
date_from, date_to = cleaned.get("date_from"), cleaned.get("date_to")
# En un listado un rango invertido se ignora; aquí no, porque el
# resultado sería un fichero vacío descargado sin explicación.
if date_from and date_to and date_from > date_to:
self.add_error("date_to", "La fecha final es anterior a la inicial.")
return cleaned