Files
WYGIWYH/app/apps/transactions/forms.py
2025-11-11 20:21:01 -03:00

1096 lines
35 KiB
Python

from copy import deepcopy
from apps.accounts.models import Account
from apps.common.fields.forms.dynamic_select import (
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
)
from apps.common.widgets.crispy.daisyui import Switch
from apps.common.widgets.crispy.submit import NoClassSubmit
from apps.common.widgets.datepicker import AirDatePickerInput, AirMonthYearPickerInput
from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput
from apps.common.widgets.tom_select import TomSelect
from apps.rules.signals import transaction_created, transaction_updated
from apps.transactions.models import (
InstallmentPlan,
QuickTransaction,
RecurringTransaction,
Transaction,
TransactionCategory,
TransactionEntity,
TransactionTag,
)
from crispy_forms.bootstrap import AccordionGroup, AppendedText, FormActions, Accordion
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (
HTML,
Column,
Div,
Field,
Layout,
Row,
)
from django import forms
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
class TransactionForm(forms.ModelForm):
category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory,
required=False,
label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
)
tags = DynamicModelMultipleChoiceField(
model=TransactionTag,
to_field_name="name",
create_field="name",
required=False,
label=_("Tags"),
queryset=TransactionTag.objects.filter(active=True),
)
entities = DynamicModelMultipleChoiceField(
model=TransactionEntity,
to_field_name="name",
create_field="name",
required=False,
label=_("Entities"),
)
account = forms.ModelChoiceField(
queryset=Account.objects.filter(is_archived=False),
label=_("Account"),
widget=TomSelect(clear_button=False, group_by="group"),
)
date = forms.DateField(label=_("Date"))
reference_date = forms.DateField(
widget=AirMonthYearPickerInput(),
label=_("Reference Date"),
required=False,
)
class Meta:
model = Transaction
fields = [
"account",
"type",
"is_paid",
"date",
"reference_date",
"amount",
"description",
"notes",
"category",
"tags",
"entities",
]
widgets = {
"notes": forms.Textarea(attrs={"rows": 3}),
"account": TomSelect(clear_button=False, group_by="group"),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# if editing a transaction display non-archived items and it's own item even if it's archived
if self.instance.id:
self.fields["account"].queryset = Account.objects.filter(
Q(is_archived=False) | Q(transactions=self.instance.id),
)
self.fields["category"].queryset = TransactionCategory.objects.filter(
Q(active=True) | Q(transaction=self.instance.id)
)
self.fields["tags"].queryset = TransactionTag.objects.filter(
Q(active=True) | Q(transaction=self.instance.id)
)
self.fields["entities"].queryset = TransactionEntity.objects.filter(
Q(active=True) | Q(transactions=self.instance.id)
)
else:
self.fields["account"].queryset = Account.objects.filter(
is_archived=False,
)
self.fields["category"].queryset = TransactionCategory.objects.filter(
active=True
)
self.fields["tags"].queryset = TransactionTag.objects.filter(active=True)
self.fields["entities"].queryset = TransactionEntity.objects.all()
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.form_method = "post"
self.helper.layout = Layout(
Field(
"type",
template="transactions/widgets/income_expense_toggle_buttons.html",
),
Field("is_paid", template="transactions/widgets/paid_toggle_button.html"),
Row(
Column("account"),
Column("entities"),
),
Row(
Column(Field("date")),
Column(Field("reference_date")),
),
"description",
Field("amount", inputmode="decimal"),
Row(
Column("category"),
Column("tags"),
),
"notes",
)
self.helper_simple = FormHelper()
self.helper_simple.form_tag = False
self.helper_simple.form_method = "post"
self.helper_simple.layout = Layout(
Field(
"type",
template="transactions/widgets/income_expense_toggle_buttons.html",
),
Field("is_paid", template="transactions/widgets/paid_toggle_button.html"),
"account",
Row(
Column(Field("date")),
Column(Field("reference_date")),
),
"description",
Field("amount", inputmode="decimal"),
Accordion(
AccordionGroup(
_("More"),
"entities",
Row(
Column("category"),
Column("tags"),
),
"notes",
active=False,
),
flush=False,
always_open=False,
css_class="mb-3",
),
FormActions(
NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"),
),
)
self.fields["date"].widget = AirDatePickerInput(clear_button=False)
if self.instance and self.instance.pk:
decimal_places = self.instance.account.currency.decimal_places
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput(
decimal_places=decimal_places
)
self.helper.layout.append(
FormActions(
NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"),
),
)
else:
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.helper.layout.append(
Div(
NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"),
NoClassSubmit(
"submit_and_similar",
_("Save and add similar"),
css_class="btn btn-primary btn-soft",
),
NoClassSubmit(
"submit_and_another",
_("Save and add another"),
css_class="btn btn-primary btn-soft",
),
css_class="flex flex-col gap-2 mt-3",
),
)
def clean(self):
cleaned_data = super().clean()
date = cleaned_data.get("date")
reference_date = cleaned_data.get("reference_date")
if date and not reference_date:
cleaned_data["reference_date"] = date.replace(day=1)
return cleaned_data
def save(self, **kwargs):
is_new = not self.instance.id
if not is_new:
old_data = deepcopy(Transaction.objects.get(pk=self.instance.id))
else:
old_data = None
instance = super().save(**kwargs)
if is_new:
transaction_created.send(sender=instance)
else:
transaction_updated.send(sender=instance, old_data=old_data)
return instance
class QuickTransactionForm(forms.ModelForm):
category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory,
required=False,
label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
)
tags = DynamicModelMultipleChoiceField(
model=TransactionTag,
to_field_name="name",
create_field="name",
required=False,
label=_("Tags"),
queryset=TransactionTag.objects.filter(active=True),
)
entities = DynamicModelMultipleChoiceField(
model=TransactionEntity,
to_field_name="name",
create_field="name",
required=False,
label=_("Entities"),
)
account = forms.ModelChoiceField(
queryset=Account.objects.filter(is_archived=False),
label=_("Account"),
widget=TomSelect(clear_button=False, group_by="group"),
)
class Meta:
model = QuickTransaction
fields = [
"name",
"account",
"type",
"is_paid",
"amount",
"description",
"notes",
"category",
"tags",
"entities",
"mute",
]
widgets = {
"notes": forms.Textarea(attrs={"rows": 3}),
"account": TomSelect(clear_button=False, group_by="group"),
}
help_texts = {
"mute": _("Muted transactions won't be displayed on monthly summaries")
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# if editing a transaction display non-archived items and it's own item even if it's archived
if self.instance.id:
self.fields["account"].queryset = Account.objects.filter(
Q(is_archived=False) | Q(transactions=self.instance.id),
)
self.fields["category"].queryset = TransactionCategory.objects.filter(
Q(active=True) | Q(transaction=self.instance.id)
)
self.fields["tags"].queryset = TransactionTag.objects.filter(
Q(active=True) | Q(transaction=self.instance.id)
)
self.fields["entities"].queryset = TransactionEntity.objects.filter(
Q(active=True) | Q(transactions=self.instance.id)
)
else:
self.fields["account"].queryset = Account.objects.filter(
is_archived=False,
)
self.fields["category"].queryset = TransactionCategory.objects.filter(
active=True
)
self.fields["tags"].queryset = TransactionTag.objects.filter(active=True)
self.fields["entities"].queryset = TransactionEntity.objects.all()
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.form_method = "post"
self.helper.layout = Layout(
Field(
"type",
template="transactions/widgets/income_expense_toggle_buttons.html",
),
Field("is_paid", template="transactions/widgets/paid_toggle_button.html"),
"name",
HTML('<hr class="hr my-3" />'),
Row(
Column("account"),
Column("entities"),
),
"description",
Field("amount", inputmode="decimal"),
Row(
Column("category"),
Column("tags"),
),
"notes",
Switch("mute"),
)
if self.instance and self.instance.pk:
decimal_places = self.instance.account.currency.decimal_places
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput(
decimal_places=decimal_places
)
self.helper.layout.append(
FormActions(
NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"),
),
)
else:
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.helper.layout.append(
FormActions(
NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"),
),
)
class BulkEditTransactionForm(forms.Form):
type = forms.ChoiceField(
choices=(Transaction.Type.choices),
required=False,
label=_("Type"),
)
is_paid = forms.NullBooleanField(
required=False,
label=_("Paid"),
)
account = DynamicModelChoiceField(
model=Account,
required=False,
label=_("Account"),
queryset=Account.objects.filter(is_archived=False),
widget=TomSelect(clear_button=False, group_by="group"),
)
date = forms.DateField(
label=_("Date"),
required=False,
widget=AirDatePickerInput(clear_button=False),
)
reference_date = forms.DateField(
widget=AirMonthYearPickerInput(),
label=_("Reference Date"),
required=False,
)
amount = forms.DecimalField(
max_digits=42,
decimal_places=30,
required=False,
label=_("Amount"),
widget=ArbitraryDecimalDisplayNumberInput(),
)
description = forms.CharField(
max_length=500, required=False, label=_("Description")
)
notes = forms.CharField(
required=False,
widget=forms.Textarea(attrs={"rows": 3}),
label=_("Notes"),
)
category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory,
required=False,
label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
)
tags = DynamicModelMultipleChoiceField(
model=TransactionTag,
to_field_name="name",
create_field="name",
required=False,
label=_("Tags"),
queryset=TransactionTag.objects.filter(active=True),
)
entities = DynamicModelMultipleChoiceField(
model=TransactionEntity,
to_field_name="name",
create_field="name",
required=False,
label=_("Entities"),
queryset=TransactionEntity.objects.all(),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["account"].queryset = Account.objects.filter(
is_archived=False,
)
self.fields["category"].queryset = TransactionCategory.objects.filter(
active=True
)
self.fields["tags"].queryset = TransactionTag.objects.filter(active=True)
self.fields["entities"].queryset = TransactionEntity.objects.all()
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.form_method = "post"
self.helper.layout = Layout(
Field(
"type",
template="transactions/widgets/unselectable_income_expense_toggle_buttons.html",
),
Field(
"is_paid",
template="transactions/widgets/unselectable_paid_toggle_button.html",
),
Row(
Column("account"),
Column("entities"),
),
Row(
Column(Field("date")),
Column(Field("reference_date")),
),
"description",
Field("amount", inputmode="decimal"),
Row(
Column("category"),
Column("tags"),
),
"notes",
FormActions(
NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"),
),
)
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["date"].widget = AirDatePickerInput(clear_button=False)
class TransferForm(forms.Form):
from_account = forms.ModelChoiceField(
queryset=Account.objects.filter(is_archived=False),
label=_("From Account"),
widget=TomSelect(clear_button=False, group_by="group"),
)
to_account = forms.ModelChoiceField(
queryset=Account.objects.filter(is_archived=False),
label=_("To Account"),
widget=TomSelect(clear_button=False, group_by="group"),
)
from_amount = forms.DecimalField(
max_digits=42,
decimal_places=30,
label=_("From Amount"),
)
to_amount = forms.DecimalField(
max_digits=42,
decimal_places=30,
label=_("To Amount"),
required=False,
)
from_category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory,
required=False,
label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
)
to_category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory,
required=False,
label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
)
from_tags = DynamicModelMultipleChoiceField(
model=TransactionTag,
to_field_name="name",
create_field="name",
required=False,
label=_("Tags"),
queryset=TransactionTag.objects.filter(active=True),
)
to_tags = DynamicModelMultipleChoiceField(
model=TransactionTag,
to_field_name="name",
create_field="name",
required=False,
label=_("Tags"),
queryset=TransactionTag.objects.filter(active=True),
)
date = forms.DateField(label=_("Date"))
reference_date = forms.DateField(
widget=AirMonthYearPickerInput(), label=_("Reference Date"), required=False
)
description = forms.CharField(
max_length=500, label=_("Description"), required=False
)
notes = forms.CharField(
required=False,
widget=forms.Textarea(
attrs={
"rows": 3,
}
),
label=_("Notes"),
)
mute = forms.BooleanField(
label=_("Mute"),
initial=True,
required=False,
help_text=_("Muted transactions won't be displayed on monthly summaries"),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.form_method = "post"
self.helper.layout = Layout(
Row(
Column(Field("date")),
Column(
Field("reference_date"),
),
),
Field("description"),
Field("notes"),
Switch("mute"),
Row(
Column("from_account"),
Column(Field("from_amount")),
Column("from_category"),
Column("from_tags"),
css_class="bg-base-100 rounded-box p-4 border-base-content/60 border my-3",
),
Row(
Column(
"to_account",
),
Column(
Field("to_amount"),
),
Column("to_category"),
Column("to_tags"),
css_class="bg-base-100 rounded-box p-4 border-base-content/60 border",
),
FormActions(
NoClassSubmit("submit", _("Transfer"), css_class="btn btn-primary"),
),
)
self.fields["from_amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["to_amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["date"].widget = AirDatePickerInput(clear_button=False)
self.fields["from_account"].queryset = Account.objects.filter(
is_archived=False,
)
self.fields["from_category"].queryset = TransactionCategory.objects.filter(
active=True
)
self.fields["from_tags"].queryset = TransactionTag.objects.filter(active=True)
self.fields["to_account"].queryset = Account.objects.filter(
is_archived=False,
)
self.fields["to_category"].queryset = TransactionCategory.objects.filter(
active=True
)
self.fields["to_tags"].queryset = TransactionTag.objects.filter(active=True)
def clean(self):
cleaned_data = super().clean()
from_account = cleaned_data.get("from_account")
to_account = cleaned_data.get("to_account")
if from_account == to_account:
raise forms.ValidationError(_("From and To accounts must be different."))
return cleaned_data
def save(self):
mute = self.cleaned_data["mute"]
from_account = self.cleaned_data["from_account"]
to_account = self.cleaned_data["to_account"]
from_amount = self.cleaned_data["from_amount"]
to_amount = self.cleaned_data["to_amount"] or from_amount
date = self.cleaned_data["date"]
reference_date = self.cleaned_data["reference_date"]
description = self.cleaned_data["description"]
from_category = self.cleaned_data.get("from_category")
to_category = self.cleaned_data.get("to_category")
notes = self.cleaned_data.get("notes")
# Create "From" transaction
from_transaction = Transaction.objects.create(
account=from_account,
type=Transaction.Type.EXPENSE,
is_paid=True,
date=date,
reference_date=reference_date,
amount=from_amount,
description=description,
category=from_category,
notes=notes,
mute=mute,
)
from_transaction.tags.set(self.cleaned_data.get("from_tags", []))
# Create "To" transaction
to_transaction = Transaction.objects.create(
account=to_account,
type=Transaction.Type.INCOME,
is_paid=True,
date=date,
reference_date=reference_date,
amount=to_amount,
description=description,
category=to_category,
notes=notes,
mute=mute,
)
to_transaction.tags.set(self.cleaned_data.get("to_tags", []))
return from_transaction, to_transaction
class InstallmentPlanForm(forms.ModelForm):
account = forms.ModelChoiceField(
queryset=Account.objects.filter(is_archived=False),
label=_("Account"),
widget=TomSelect(clear_button=False, group_by="group"),
)
tags = DynamicModelMultipleChoiceField(
model=TransactionTag,
to_field_name="name",
create_field="name",
required=False,
label=_("Tags"),
queryset=TransactionTag.objects.filter(active=True),
)
category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory,
required=False,
label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
)
entities = DynamicModelMultipleChoiceField(
model=TransactionEntity,
to_field_name="name",
create_field="name",
required=False,
label=_("Entities"),
queryset=TransactionEntity.objects.filter(active=True),
)
type = forms.ChoiceField(choices=Transaction.Type.choices)
reference_date = forms.DateField(
widget=AirMonthYearPickerInput(), label=_("Reference Date"), required=False
)
class Meta:
model = InstallmentPlan
fields = [
"type",
"account",
"start_date",
"reference_date",
"description",
"number_of_installments",
"recurrence",
"installment_amount",
"category",
"tags",
"notes",
"installment_start",
"entities",
"add_description_to_transaction",
"add_notes_to_transaction",
]
widgets = {
"account": TomSelect(),
"recurrence": TomSelect(clear_button=False),
"notes": forms.Textarea(attrs={"rows": 3}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# if editing display non-archived items and it's own item even if it's archived
if self.instance.id:
self.fields["account"].queryset = Account.objects.filter(
Q(is_archived=False) | Q(installmentplan=self.instance.id)
).distinct()
self.fields["category"].queryset = TransactionCategory.objects.filter(
Q(active=True) | Q(installmentplan=self.instance.id)
).distinct()
self.fields["tags"].queryset = TransactionTag.objects.filter(
Q(active=True) | Q(installmentplan=self.instance.id)
).distinct()
self.fields["entities"].queryset = TransactionEntity.objects.filter(
Q(active=True) | Q(installmentplan=self.instance.id)
).distinct()
else:
self.fields["account"].queryset = Account.objects.filter(is_archived=False)
self.fields["category"].queryset = TransactionCategory.objects.filter(
active=True
)
self.fields["tags"].queryset = TransactionTag.objects.filter(active=True)
self.fields["entities"].queryset = TransactionEntity.objects.filter(
active=True
)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.form_method = "post"
self.helper.layout = Layout(
Field(
"type",
template="transactions/widgets/income_expense_toggle_buttons.html",
),
Row(
Column("account"),
Column("entities"),
),
"description",
Switch("add_description_to_transaction"),
"notes",
Switch("add_notes_to_transaction"),
Row(
Column("number_of_installments"),
Column("installment_start"),
),
Row(
Column("start_date", css_class="col-span-12 md:col-span-4"),
Column("reference_date", css_class="col-span-12 md:col-span-4"),
Column("recurrence", css_class="col-span-12 md:col-span-4"),
),
"installment_amount",
Row(
Column("category"),
Column("tags"),
),
)
self.fields["installment_amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["start_date"].widget = AirDatePickerInput(clear_button=False)
if self.instance and self.instance.pk:
self.helper.layout.append(
FormActions(
NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"),
),
)
else:
self.helper.layout.append(
FormActions(
NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"),
),
)
def save(self, **kwargs):
is_new = not self.instance.id
instance = super().save(**kwargs)
if is_new:
instance.create_transactions()
else:
instance.update_transactions()
return instance
class TransactionTagForm(forms.ModelForm):
class Meta:
model = TransactionTag
fields = ["name", "active"]
labels = {"name": _("Tag name")}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.form_method = "post"
self.helper.layout = Layout(Field("name", css_class="mb-3"), Switch("active"))
if self.instance and self.instance.pk:
self.helper.layout.append(
FormActions(
NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"),
),
)
else:
self.helper.layout.append(
FormActions(
NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"),
),
)
class TransactionEntityForm(forms.ModelForm):
class Meta:
model = TransactionEntity
fields = ["name", "active"]
labels = {"name": _("Entity name")}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.form_method = "post"
self.helper.layout = Layout(Field("name"), Switch("active"))
if self.instance and self.instance.pk:
self.helper.layout.append(
FormActions(
NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"),
),
)
else:
self.helper.layout.append(
FormActions(
NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"),
),
)
class TransactionCategoryForm(forms.ModelForm):
class Meta:
model = TransactionCategory
fields = ["name", "mute", "active"]
labels = {"name": _("Category name")}
help_texts = {
"mute": _("Muted categories won't be displayed on monthly summaries")
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.form_method = "post"
self.helper.layout = Layout(Field("name"), Switch("mute"), Switch("active"))
if self.instance and self.instance.pk:
self.helper.layout.append(
FormActions(
NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"),
),
)
else:
self.helper.layout.append(
FormActions(
NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"),
),
)
class RecurringTransactionForm(forms.ModelForm):
account = forms.ModelChoiceField(
queryset=Account.objects.filter(is_archived=False),
label=_("Account"),
widget=TomSelect(clear_button=False, group_by="group"),
)
tags = DynamicModelMultipleChoiceField(
model=TransactionTag,
to_field_name="name",
create_field="name",
required=False,
label=_("Tags"),
queryset=TransactionTag.objects.filter(active=True),
)
category = DynamicModelChoiceField(
create_field="name",
model=TransactionCategory,
required=False,
label=_("Category"),
queryset=TransactionCategory.objects.filter(active=True),
)
entities = DynamicModelMultipleChoiceField(
model=TransactionEntity,
to_field_name="name",
create_field="name",
required=False,
label=_("Entities"),
queryset=TransactionEntity.objects.filter(active=True),
)
type = forms.ChoiceField(choices=Transaction.Type.choices)
class Meta:
model = RecurringTransaction
fields = [
"account",
"type",
"amount",
"description",
"add_description_to_transaction",
"category",
"tags",
"start_date",
"reference_date",
"end_date",
"recurrence_type",
"recurrence_interval",
"notes",
"add_notes_to_transaction",
"entities",
"keep_at_most",
]
widgets = {
"reference_date": AirMonthYearPickerInput(),
"recurrence_type": TomSelect(clear_button=False),
"notes": forms.Textarea(
attrs={
"rows": 3,
}
),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# if editing display non-archived items and it's own item even if it's archived
if self.instance.id:
self.fields["account"].queryset = Account.objects.filter(
Q(is_archived=False) | Q(recurringtransaction=self.instance.id)
).distinct()
self.fields["category"].queryset = TransactionCategory.objects.filter(
Q(active=True) | Q(recurringtransaction=self.instance.id)
).distinct()
self.fields["tags"].queryset = TransactionTag.objects.filter(
Q(active=True) | Q(recurringtransaction=self.instance.id)
).distinct()
self.fields["entities"].queryset = TransactionEntity.objects.filter(
Q(active=True) | Q(recurringtransaction=self.instance.id)
).distinct()
else:
self.fields["account"].queryset = Account.objects.filter(is_archived=False)
self.fields["category"].queryset = TransactionCategory.objects.filter(
active=True
)
self.fields["tags"].queryset = TransactionTag.objects.filter(active=True)
self.fields["entities"].queryset = TransactionEntity.objects.filter(
active=True
)
self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_tag = False
self.helper.layout = Layout(
Field(
"type",
template="transactions/widgets/income_expense_toggle_buttons.html",
),
Row(
Column("account"),
Column("entities"),
),
"description",
Switch("add_description_to_transaction"),
"amount",
Row(
Column("category"),
Column("tags"),
),
"notes",
Switch("add_notes_to_transaction"),
Row(
Column("start_date"),
Column("reference_date"),
),
Row(
Column("recurrence_interval", css_class="col-span-12 md:col-span-4"),
Column("recurrence_type", css_class="col-span-12 md:col-span-4"),
Column("end_date", css_class="col-span-12 md:col-span-4"),
),
AppendedText("keep_at_most", _("future transactions")),
)
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
self.fields["start_date"].widget = AirDatePickerInput(clear_button=False)
self.fields["end_date"].widget = AirDatePickerInput()
if self.instance and self.instance.pk:
self.helper.layout.append(
FormActions(
NoClassSubmit("submit", _("Update"), css_class="btn btn-primary"),
),
)
else:
self.helper.layout.append(
FormActions(
NoClassSubmit("submit", _("Add"), css_class="btn btn-primary"),
),
)
def clean(self):
cleaned_data = super().clean()
start_date = cleaned_data.get("start_date")
end_date = cleaned_data.get("end_date")
if start_date and end_date and start_date > end_date:
raise forms.ValidationError(_("End date should be after the start date"))
return cleaned_data
def save(self, **kwargs):
is_new = not self.instance.id
instance = super().save(**kwargs)
if is_new:
instance.create_upcoming_transactions()
else:
instance.update_unpaid_transactions()
instance.generate_upcoming_transactions()
return instance