mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-03-17 23:13:57 +01:00
feat: bulk edit selected transactions
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"])
|
||||
|
||||
17
app/templates/transactions/fragments/bulk_edit.html
Normal file
17
app/templates/transactions/fragments/bulk_edit.html
Normal 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 %}
|
||||
@@ -15,4 +15,4 @@
|
||||
{% if field.help_text %}
|
||||
<div class="help-text">{{ field.help_text|safe }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user