diff --git a/app/apps/transactions/forms.py b/app/apps/transactions/forms.py index 50ba2f1..27842a9 100644 --- a/app/apps/transactions/forms.py +++ b/app/apps/transactions/forms.py @@ -223,6 +223,43 @@ class TransactionForm(forms.ModelForm): return instance +class BulkEditTransactionForm(TransactionForm): + is_paid = forms.NullBooleanField(required=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Make all fields optional + for field_name, field in self.fields.items(): + field.required = False + + del self.helper.layout[-1] # Remove button + del self.helper.layout[0:2] # Remove type, is_paid field + + self.helper.layout.insert( + 0, + Field( + "type", + template="transactions/widgets/unselectable_income_expense_toggle_buttons.html", + ), + ) + + self.helper.layout.insert( + 1, + Field( + "is_paid", + template="transactions/widgets/unselectable_paid_toggle_button.html", + ), + ) + + self.helper.layout.append( + FormActions( + NoClassSubmit( + "submit", _("Update"), css_class="btn btn-outline-primary w-100" + ), + ), + ) + + class TransferForm(forms.Form): from_account = forms.ModelChoiceField( queryset=Account.objects.filter(is_archived=False), diff --git a/app/apps/transactions/urls.py b/app/apps/transactions/urls.py index cefe36c..76c5a70 100644 --- a/app/apps/transactions/urls.py +++ b/app/apps/transactions/urls.py @@ -41,6 +41,11 @@ urlpatterns = [ views.transaction_edit, name="transaction_edit", ), + path( + "transactions/bulk-edit/", + views.transactions_bulk_edit, + name="transactions_bulk_edit", + ), path( "transaction//clone/", views.transaction_clone, diff --git a/app/apps/transactions/views/transactions.py b/app/apps/transactions/views/transactions.py index f07442e..80ac759 100644 --- a/app/apps/transactions/views/transactions.py +++ b/app/apps/transactions/views/transactions.py @@ -12,9 +12,13 @@ from django.views.decorators.http import require_http_methods from apps.common.decorators.htmx import only_htmx from apps.common.utils.dicts import remove_falsey_entries -from apps.rules.signals import transaction_created +from apps.rules.signals import transaction_created, transaction_updated from apps.transactions.filters import TransactionsFilter -from apps.transactions.forms import TransactionForm, TransferForm +from apps.transactions.forms import ( + TransactionForm, + TransferForm, + BulkEditTransactionForm, +) from apps.transactions.models import Transaction from apps.transactions.utils.calculations import ( calculate_currency_totals, @@ -135,6 +139,61 @@ def transaction_edit(request, transaction_id, **kwargs): ) +@only_htmx +@login_required +@require_http_methods(["GET", "POST"]) +def transactions_bulk_edit(request): + # Get selected transaction IDs from the URL parameter + transaction_ids = request.GET.getlist("transactions") or request.POST.getlist( + "transactions" + ) + print(transaction_ids) + # Load the selected transactions + transactions = Transaction.objects.filter(id__in=transaction_ids) + + if request.method == "POST": + form = BulkEditTransactionForm(request.POST, user=request.user) + if form.is_valid(): + print(form.cleaned_data) + # Apply changes from the form to all selected transactions + for transaction in transactions: + for field_name, value in form.cleaned_data.items(): + if value or isinstance( + value, bool + ): # Only update fields that have been filled in the form + if field_name == "tags": + transaction.tags.set(value) + elif field_name == "entities": + transaction.entities.set(value) + else: + setattr(transaction, field_name, value) + + print(transaction.is_paid) + transaction.save() + transaction_updated.send(sender=transaction) + + messages.success( + request, + _("{count} transactions updated successfully").format( + count=len(transaction_ids) + ), + ) + return HttpResponse( + status=204, + headers={"HX-Trigger": "updated, hide_offcanvas"}, + ) + else: + form = BulkEditTransactionForm( + initial={"is_paid": None, "type": None}, user=request.user + ) + + context = { + "form": form, + "transactions": transactions, + } + return render(request, "transactions/fragments/bulk_edit.html", context) + + @only_htmx @login_required @require_http_methods(["GET", "POST"]) diff --git a/app/templates/transactions/fragments/bulk_edit.html b/app/templates/transactions/fragments/bulk_edit.html new file mode 100644 index 0000000..be219e1 --- /dev/null +++ b/app/templates/transactions/fragments/bulk_edit.html @@ -0,0 +1,17 @@ +{% extends 'extends/offcanvas.html' %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block title %}{% translate 'Bulk Editing' %}{% endblock %} + +{% block body %} +

{% trans 'Editing' %} {{ transactions|length }} {% trans 'transactions' %}

+
+ {% for transaction in transactions %} + + {% endfor %} +
+
+ {% crispy form %} +
+{% endblock %} diff --git a/app/templates/transactions/widgets/paid_toggle_button.html b/app/templates/transactions/widgets/paid_toggle_button.html index bcd8877..9b27b3a 100644 --- a/app/templates/transactions/widgets/paid_toggle_button.html +++ b/app/templates/transactions/widgets/paid_toggle_button.html @@ -15,4 +15,4 @@ {% if field.help_text %}
{{ field.help_text|safe }}
{% endif %} - \ No newline at end of file + diff --git a/app/templates/transactions/widgets/unselectable_income_expense_toggle_buttons.html b/app/templates/transactions/widgets/unselectable_income_expense_toggle_buttons.html new file mode 100644 index 0000000..cec779c --- /dev/null +++ b/app/templates/transactions/widgets/unselectable_income_expense_toggle_buttons.html @@ -0,0 +1,40 @@ +{% load i18n %} +{% load crispy_forms_field %} + +
+
+ + + + {% for choice in field.field.choices %} + + + {% endfor %} +
+ {% if field.errors %} +
+ {% for error in field.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + {% if field.help_text %} + {{ field.help_text }} + {% endif %} +
diff --git a/app/templates/transactions/widgets/unselectable_paid_toggle_button.html b/app/templates/transactions/widgets/unselectable_paid_toggle_button.html new file mode 100644 index 0000000..847a147 --- /dev/null +++ b/app/templates/transactions/widgets/unselectable_paid_toggle_button.html @@ -0,0 +1,22 @@ +{% load i18n %} +{% load crispy_forms_field %} + +
+
+ + + + + + + + +
+ + {% if field.help_text %} +
{{ field.help_text|safe }}
+ {% endif %} +