feat: bulk edit selected transactions

This commit is contained in:
Herculino Trotta
2025-01-25 12:41:55 -03:00
parent c474b6cda9
commit 1ef7a780fb
7 changed files with 183 additions and 3 deletions

View File

@@ -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),

View File

@@ -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/<int:transaction_id>/clone/",
views.transaction_clone,

View File

@@ -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"])

View File

@@ -0,0 +1,17 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Bulk Editing' %}{% endblock %}
{% block body %}
<p>{% trans 'Editing' %} {{ transactions|length }} {% trans 'transactions' %}</p>
<div class="editing-transactions">
{% for transaction in transactions %}
<input type="hidden" name="transactions" value="{{ transaction.id }}"/>
{% endfor %}
</div>
<form hx-post="{% url 'transactions_bulk_edit' %}" hx-target="#generic-offcanvas" hx-include=".editing-transactions" novalidate>
{% crispy form %}
</form>
{% endblock %}

View File

@@ -15,4 +15,4 @@
{% if field.help_text %}
<div class="help-text">{{ field.help_text|safe }}</div>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,40 @@
{% load i18n %}
{% load crispy_forms_field %}
<div class="form-group mb-3">
<div class="btn-group w-100" role="group" aria-label="{{ field.label }}">
<input type="radio"
class="btn-check"
name="{{ field.html_name }}"
id="{{ field.html_name }}_none_tr"
value=""
{% if field.value is None %}checked{% endif %}>
<label class="btn btn-outline-secondary {% if field.errors %}is-invalid{% endif %} w-100"
for="{{ field.html_name }}_none_tr">
{% trans 'Unchanged' %}
</label>
{% for choice in field.field.choices %}
<input type="radio"
class="btn-check"
name="{{ field.html_name }}"
id="{{ field.html_name }}_{{ forloop.counter }}_tr"
value="{{ choice.0 }}"
{% if choice.0 == field.value %}checked{% endif %}>
<label class="btn {% if forloop.first %}btn-outline-success{% elif forloop.last %}btn-outline-danger{% else %}btn-outline-primary{% endif %} {% if field.errors %}is-invalid{% endif %} w-100"
for="{{ field.html_name }}_{{ forloop.counter }}_tr">
{{ choice.1 }}
</label>
{% endfor %}
</div>
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
{% if field.help_text %}
<small class="form-text text-muted">{{ field.help_text }}</small>
{% endif %}
</div>

View File

@@ -0,0 +1,22 @@
{% load i18n %}
{% load crispy_forms_field %}
<div class="form-group mb-3">
<div class="btn-group w-100" role="group" aria-label="{{ field.label }}">
<input type="radio" class="btn-check" name="{{ field.name }}" id="{{ field.id_for_label }}_null"
value="" {% if field.value is None %}checked{% endif %}>
<label class="btn btn-outline-secondary w-100" for="{{ field.id_for_label }}_null">{% trans 'Unchaged' %}</label>
<input type="radio" class="btn-check" name="{{ field.name }}" id="{{ field.id_for_label }}_false"
value="false" {% if field.value is False %}checked{% endif %}">
<label class="btn btn-outline-primary w-100" for="{{ field.id_for_label }}_false">{% trans 'Projected' %}</label>
<input type="radio" class="btn-check" name="{{ field.name }}" id="{{ field.id_for_label }}_true"
value="true" {% if field.value is True %}checked{% endif %}>
<label class="btn btn-outline-success w-100" for="{{ field.id_for_label }}_true">{% trans 'Paid' %}</label>
</div>
{% if field.help_text %}
<div class="help-text">{{ field.help_text|safe }}</div>
{% endif %}
</div>