diff --git a/app/apps/transactions/forms.py b/app/apps/transactions/forms.py index 8499ca5..8dc1d69 100644 --- a/app/apps/transactions/forms.py +++ b/app/apps/transactions/forms.py @@ -5,8 +5,12 @@ from django.utils.translation import gettext_lazy as _ from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Submit, Row, Column, Div, Field, Hidden -from .models import Transaction -from apps.transactions.widgets import ArbitraryDecimalDisplayNumberInput +from apps.accounts.models import Account +from .models import Transaction, TransactionCategory, TransactionTag +from apps.transactions.widgets import ( + ArbitraryDecimalDisplayNumberInput, + MonthYearWidget, +) class TransactionForm(forms.ModelForm): @@ -73,3 +77,135 @@ class TransactionForm(forms.ModelForm): self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput( decimal_places=decimal_places ) + + +class TransferForm(forms.Form): + from_account = forms.ModelChoiceField( + queryset=Account.objects.all(), label="From Account" + ) + to_account = forms.ModelChoiceField( + queryset=Account.objects.all(), label="To Account" + ) + + from_amount = forms.DecimalField( + max_digits=42, decimal_places=30, label="From Amount", step_size=1 + ) + to_amount = forms.DecimalField( + max_digits=42, decimal_places=30, label="To Amount", required=False, step_size=1 + ) + + from_category = forms.ModelChoiceField( + queryset=TransactionCategory.objects.all(), + required=False, + label="From Category", + ) + to_category = forms.ModelChoiceField( + queryset=TransactionCategory.objects.all(), required=False, label="To Category" + ) + + from_tags = forms.ModelMultipleChoiceField( + queryset=TransactionTag.objects.all(), required=False, label="From Tags" + ) + to_tags = forms.ModelMultipleChoiceField( + queryset=TransactionTag.objects.all(), required=False, label="To Tags" + ) + + date = forms.DateField( + label="Date", widget=forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d") + ) + reference_date = forms.CharField(label="Reference Date", widget=MonthYearWidget()) + description = forms.CharField(max_length=500, label="Description") + + 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("date", css_class="form-group col-md-6 mb-0"), + Column("reference_date", css_class="form-group col-md-6 mb-0"), + css_class="form-row", + ), + "description", + Row( + Column( + Row( + Column("from_account", css_class="form-group col-md-6 mb-0"), + Column("from_amount", css_class="form-group col-md-6 mb-0"), + css_class="form-row", + ), + Row( + Column("from_category", css_class="form-group col-md-6 mb-0"), + Column("from_tags", css_class="form-group col-md-6 mb-0"), + css_class="form-row", + ), + ), + css_class="p-1 mx-1 my-3 border rounded-3", + ), + Row( + Column( + Row( + Column("to_account", css_class="form-group col-md-6 mb-0"), + Column("to_amount", css_class="form-group col-md-6 mb-0"), + css_class="form-row", + ), + Row( + Column("to_category", css_class="form-group col-md-6 mb-0"), + Column("to_tags", css_class="form-group col-md-6 mb-0"), + css_class="form-row", + ), + ), + css_class="p-1 mx-1 my-3 border rounded-3", + ), + Submit("submit", "Save", css_class="btn btn-primary"), + ) + + 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): + 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"] + + # 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=f"Transfer to {to_account}: {description}", + category=self.cleaned_data.get("from_category"), + ) + 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=f"Transfer from {from_account}: {description}", + category=self.cleaned_data.get("to_category"), + ) + to_transaction.tags.set(self.cleaned_data.get("to_tags", [])) + + return from_transaction, to_transaction diff --git a/app/apps/transactions/urls.py b/app/apps/transactions/urls.py index fed2088..218718d 100644 --- a/app/apps/transactions/urls.py +++ b/app/apps/transactions/urls.py @@ -44,4 +44,9 @@ urlpatterns = [ views.month_year_picker, name="available_dates", ), + path( + "transactions/transfer", + views.transactions_transfer, + name="transactions_transfer", + ), ] diff --git a/app/apps/transactions/views.py b/app/apps/transactions/views.py index ab56678..e01c544 100644 --- a/app/apps/transactions/views.py +++ b/app/apps/transactions/views.py @@ -13,7 +13,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from unicodedata import category -from apps.transactions.forms import TransactionForm +from apps.transactions.forms import TransactionForm, TransferForm from apps.transactions.models import Transaction @@ -170,6 +170,23 @@ def transaction_delete(request, transaction_id, **kwargs): ) +@login_required +def transactions_transfer(request): + if request.method == "POST": + form = TransferForm(request.POST) + if form.is_valid(): + from_transaction, to_transaction = form.save() + messages.success(request, "Transfer completed successfully.") + return HttpResponse( + status=204, + headers={"HX-Trigger": "transaction_updated, toast"}, + ) + else: + form = TransferForm() + + return render(request, "transactions/fragments/transfer.html", {"form": form}) + + @login_required def transaction_pay(request, transaction_id): transaction = get_object_or_404(Transaction, pk=transaction_id) diff --git a/app/templates/transactions/fragments/transfer.html b/app/templates/transactions/fragments/transfer.html new file mode 100644 index 0000000..8b9c186 --- /dev/null +++ b/app/templates/transactions/fragments/transfer.html @@ -0,0 +1,12 @@ +{% extends 'extends/offcanvas.html' %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block title %}{% translate 'Adding transaction' %}{% endblock %} + +{% block body %} +
+{% endblock %}