mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-04-28 19:47:14 +02:00
feat: bulk edit selected transactions
This commit is contained in:
@@ -223,6 +223,43 @@ class TransactionForm(forms.ModelForm):
|
|||||||
return instance
|
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):
|
class TransferForm(forms.Form):
|
||||||
from_account = forms.ModelChoiceField(
|
from_account = forms.ModelChoiceField(
|
||||||
queryset=Account.objects.filter(is_archived=False),
|
queryset=Account.objects.filter(is_archived=False),
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ urlpatterns = [
|
|||||||
views.transaction_edit,
|
views.transaction_edit,
|
||||||
name="transaction_edit",
|
name="transaction_edit",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"transactions/bulk-edit/",
|
||||||
|
views.transactions_bulk_edit,
|
||||||
|
name="transactions_bulk_edit",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"transaction/<int:transaction_id>/clone/",
|
"transaction/<int:transaction_id>/clone/",
|
||||||
views.transaction_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.decorators.htmx import only_htmx
|
||||||
from apps.common.utils.dicts import remove_falsey_entries
|
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.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.models import Transaction
|
||||||
from apps.transactions.utils.calculations import (
|
from apps.transactions.utils.calculations import (
|
||||||
calculate_currency_totals,
|
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
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@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 %}
|
||||||
@@ -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