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 from apps.common.widgets.decimal import ArbitraryDecimalDisplayNumberInput from apps.common.widgets.tom_select import TomSelect, TransactionSelect from apps.dca.models import DCAEntry, DCAStrategy from apps.transactions.models import Transaction, TransactionCategory, TransactionTag from crispy_forms.bootstrap import AccordionGroup, FormActions, Accordion from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Column, Layout, Row from django import forms from django.utils.translation import gettext_lazy as _ class DCAStrategyForm(forms.ModelForm): class Meta: model = DCAStrategy fields = ["name", "target_currency", "payment_currency", "notes"] widgets = { "target_currency": TomSelect(clear_button=False), "payment_currency": TomSelect(clear_button=False), "notes": forms.Textarea(attrs={"rows": 3}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_tag = False self.helper.layout = Layout( "name", Row( Column("payment_currency"), Column("target_currency"), ), "notes", ) 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 DCAEntryForm(forms.ModelForm): create_transaction = forms.BooleanField( label=_("Create transaction"), initial=False, required=False ) from_account = forms.ModelChoiceField( queryset=Account.objects.filter(is_archived=False), label=_("From Account"), widget=TomSelect(clear_button=False, group_by="group"), required=False, ) to_account = forms.ModelChoiceField( queryset=Account.objects.filter(is_archived=False), label=_("To Account"), widget=TomSelect(clear_button=False, group_by="group"), 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), ) expense_transaction = DynamicModelChoiceField( model=Transaction, to_field_name="id", label=_("Expense Transaction"), required=False, queryset=Transaction.objects.none(), widget=TransactionSelect(clear_button=True, income=False, expense=True), help_text=_("Type to search for a transaction to link to this entry"), ) income_transaction = DynamicModelChoiceField( model=Transaction, to_field_name="id", label=_("Income Transaction"), required=False, queryset=Transaction.objects.none(), widget=TransactionSelect(clear_button=True, income=True, expense=False), help_text=_("Type to search for a transaction to link to this entry"), ) class Meta: model = DCAEntry fields = [ "date", "amount_paid", "amount_received", "notes", "expense_transaction", "income_transaction", ] widgets = { "notes": forms.Textarea(attrs={"rows": 3}), } def __init__(self, *args, **kwargs): strategy = kwargs.pop("strategy", None) super().__init__(*args, **kwargs) self.strategy = strategy if strategy else self.instance.strategy self.helper = FormHelper() self.helper.form_tag = False self.helper.layout = Layout( "date", Row( Column("amount_paid"), Column("amount_received"), ), "notes", Accordion( AccordionGroup( _("Create transaction"), Switch("create_transaction"), Row( Column( Row( Column( "from_account", ), ), Row( Column("from_category"), Column("from_tags"), ), ), css_class="p-1 mx-1 my-3 border rounded-3", ), Row( Column( Row( Column( "to_account", css_class="form-group", ), ), Row( Column("to_category"), Column("to_tags"), ), ), css_class="p-1 mx-1 my-3 border rounded-3", ), active=False, ), AccordionGroup( _("Link transaction"), "income_transaction", "expense_transaction", ), flush=False, always_open=False, css_class="mb-3", ), ) 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"), ), ) self.fields["amount_paid"].widget = ArbitraryDecimalDisplayNumberInput() self.fields["amount_received"].widget = ArbitraryDecimalDisplayNumberInput() self.fields["date"].widget = AirDatePickerInput(clear_button=False) expense_transaction = None income_transaction = None if self.instance and self.instance.pk: # Edit mode - get from instance expense_transaction = self.instance.expense_transaction income_transaction = self.instance.income_transaction elif self.data.get("expense_transaction"): # Form validation - get from submitted data try: expense_transaction = Transaction.objects.get( id=self.data["expense_transaction"] ) income_transaction = Transaction.objects.get( id=self.data["income_transaction"] ) except Transaction.DoesNotExist: pass # If we have a current transaction, ensure it's in the queryset if income_transaction: self.fields["income_transaction"].queryset = Transaction.objects.filter( id=income_transaction.id ) if expense_transaction: self.fields["expense_transaction"].queryset = Transaction.objects.filter( id=expense_transaction.id ) 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() if cleaned_data.get("create_transaction"): from_account = cleaned_data.get("from_account") to_account = cleaned_data.get("to_account") if not from_account and not to_account: raise forms.ValidationError( { "from_account": _("You must provide an account."), "to_account": _("You must provide an account."), } ) elif not from_account and to_account: raise forms.ValidationError( {"from_account": _("You must provide an account.")} ) elif not to_account and from_account: raise forms.ValidationError( {"to_account": _("You must provide an account.")} ) if from_account == to_account: raise forms.ValidationError( _("From and To accounts must be different.") ) return cleaned_data def save(self, **kwargs): instance = super().save(commit=False) if self.cleaned_data.get("create_transaction"): from_account = self.cleaned_data["from_account"] to_account = self.cleaned_data["to_account"] from_amount = instance.amount_paid to_amount = instance.amount_received date = instance.date description = _("DCA for %(strategy_name)s") % { "strategy_name": self.strategy.name } 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, amount=from_amount, description=description, category=from_category, notes=notes, ) 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, amount=to_amount, description=description, category=to_category, notes=notes, ) to_transaction.tags.set(self.cleaned_data.get("to_tags", [])) instance.expense_transaction = from_transaction instance.income_transaction = to_transaction else: if instance.expense_transaction: instance.expense_transaction.amount = instance.amount_paid instance.expense_transaction.save() if instance.income_transaction: instance.income_transaction.amount = instance.amount_received instance.income_transaction.save() instance.strategy = self.strategy instance.save() return instance