mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Add bulk operations for orders (#3548)
* Add bulk operations for orders * UI tweaks * Fix test failures * Fix filter form * Add tests * Run isort
This commit is contained in:
@@ -159,7 +159,7 @@ class ReactivateOrderForm(ForceQuotaConfirmationForm):
|
||||
pass
|
||||
|
||||
|
||||
class CancelForm(ForceQuotaConfirmationForm):
|
||||
class CancelForm(forms.Form):
|
||||
send_email = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Notify customer by email'),
|
||||
@@ -188,6 +188,7 @@ class CancelForm(ForceQuotaConfirmationForm):
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.instance = kwargs.pop("instance")
|
||||
super().__init__(*args, **kwargs)
|
||||
change_decimal_field(self.fields['cancellation_fee'], self.instance.event.currency)
|
||||
self.fields['cancellation_fee'].widget.attrs['placeholder'] = floatformat(
|
||||
@@ -205,6 +206,20 @@ class CancelForm(ForceQuotaConfirmationForm):
|
||||
return val
|
||||
|
||||
|
||||
class DenyForm(forms.Form):
|
||||
send_email = forms.BooleanField(
|
||||
required=False,
|
||||
label=_('Notify customer by email'),
|
||||
initial=True
|
||||
)
|
||||
comment = forms.CharField(
|
||||
label=_('Comment (will be sent to the user)'),
|
||||
help_text=_('Will be included in the notification email when the respective placeholder is present in the '
|
||||
'configured email text.'),
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
class MarkPaidForm(ConfirmPaymentForm):
|
||||
send_email = forms.BooleanField(
|
||||
required=False,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}
|
||||
{% trans "Deny order" %}
|
||||
{% endblock %}
|
||||
@@ -13,16 +14,7 @@
|
||||
|
||||
<form method="post" href="">
|
||||
{% csrf_token %}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="send_email" value="on" checked="checked">
|
||||
{% trans "Notify user by e-mail" %}
|
||||
</label>
|
||||
</div>
|
||||
<p>
|
||||
<label>{% trans "Comment (will be sent to the user)" %}</label>
|
||||
<textarea name="comment" class="form-control" rows="5"></textarea>
|
||||
</p>
|
||||
{% bootstrap_form form %}
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load money %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Modify orders" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Modify orders" %}</h1>
|
||||
<form action="{{ request.get_full_path }}" method="post" class="form-horizontal" data-asynctask
|
||||
data-asynctask-long="">
|
||||
{% csrf_token %}
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed with label=label allowed=allowed.count total=total %}
|
||||
The operation <strong>{{ label }}</strong> can be applied to <strong>{{ allowed }}</strong> of the
|
||||
selected <strong>{{ total }}</strong> orders.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% if allowed %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover table-orders">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Order code" %}</th>
|
||||
<th>{% trans "User" %}</th>
|
||||
<th>{% trans "Order date" %}</th>
|
||||
<th class="text-right flip">{% trans "Order total" %}</th>
|
||||
<th class="text-right flip">{% trans "Status" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for o in allowed|slice:":50" %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=o.code %}">
|
||||
{{ o.code }}</a>
|
||||
</strong>
|
||||
{% if o.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ o.email|default_if_none:"" }}
|
||||
{% if o.invoice_address.name %}
|
||||
<br>{{ o.invoice_address.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="fa fa-fw fa-{{ o.sales_channel_obj.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{% trans o.sales_channel_obj.verbose_name %}"></span>
|
||||
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{{ o.total|money:request.event.currency }}
|
||||
</td>
|
||||
<td class="text-right flip">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if allowed.count > 50 %}
|
||||
<tr>
|
||||
<td>…</td>
|
||||
<td>…</td>
|
||||
<td>…</td>
|
||||
<td>…</td>
|
||||
<td>…</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Do you want to continue?
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="alert alert-warning">
|
||||
<strong>
|
||||
{% blocktrans trimmed %}
|
||||
This operation cannot be reversed.
|
||||
{% endblocktrans %}
|
||||
</strong>
|
||||
</div>
|
||||
{% for k, l in request.POST.lists %}
|
||||
{% if "bulkactionform" not in k %}
|
||||
{% for v in l %}
|
||||
<input type="hidden" name="{{ k }}" value="{{ v }}">
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% bootstrap_form form layout="control" %}
|
||||
{% endif %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:event.orders" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
{% if allowed %}
|
||||
<button type="submit" class="btn btn-primary btn-save" value="confirm" name="operation">
|
||||
{{ label }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -17,11 +17,12 @@
|
||||
|
||||
{% if not request.event.live %}
|
||||
<a href="{% url "control:event.live" event=request.event.slug organizer=request.event.organizer.slug %}"
|
||||
class="btn btn-primary btn-lg">
|
||||
class="btn btn-primary btn-lg">
|
||||
{% trans "Take your shop live" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% eventurl request.event "presale:event.index" %}" class="btn btn-primary btn-lg" target="_blank">
|
||||
<a href="{% eventurl request.event "presale:event.index" %}" class="btn btn-primary btn-lg"
|
||||
target="_blank">
|
||||
{% trans "Go to the ticket shop" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -40,9 +41,10 @@
|
||||
</p>
|
||||
{% else %}
|
||||
<form class="form-inline"
|
||||
action="{% url "control:event.orders.go" event=request.event.slug organizer=request.event.organizer.slug %}">
|
||||
action="{% url "control:event.orders.go" event=request.event.slug organizer=request.event.organizer.slug %}">
|
||||
<p class="input-group">
|
||||
<input type="text" name="code" class="form-control" placeholder="{% trans "Order code" %}" autofocus>
|
||||
<input type="text" name="code" class="form-control" placeholder="{% trans "Order code" %}"
|
||||
autofocus>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-primary" type="submit">{% trans "Go!" %}</button>
|
||||
</span>
|
||||
@@ -82,7 +84,8 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-right flip">
|
||||
<a href="{% url "control:event.orders.search" event=request.event.slug organizer=request.event.organizer.slug %}" class="btn btn-default btn-lg">
|
||||
<a href="{% url "control:event.orders.search" event=request.event.slug organizer=request.event.organizer.slug %}"
|
||||
class="btn btn-default btn-lg">
|
||||
{% trans "Advanced search" %}
|
||||
</a>
|
||||
<button class="btn btn-primary btn-lg" type="submit">
|
||||
@@ -99,134 +102,216 @@
|
||||
{% blocktrans trimmed with question=filter_form.cleaned_data.question.question %}
|
||||
List filtered by answers to question "{{ question }}".
|
||||
{% endblocktrans %}
|
||||
<a href="?{% url_replace request 'question' '' 'answer' ''%}" class="text-muted">
|
||||
<a href="?{% url_replace request 'question' '' 'answer' '' %}" class="text-muted">
|
||||
<span class="fa fa-times"></span>
|
||||
{% trans "Remove filter" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover table-orders">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Order code" %}
|
||||
<a href="?{% url_replace request 'ordering' '-code' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'code' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "User" %}
|
||||
<a href="?{% url_replace request 'ordering' '-email' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'email' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th>{% trans "Order date" %}
|
||||
<a href="?{% url_replace request 'ordering' '-datetime' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'datetime' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th class="text-right flip">{% trans "Order paid / total" %}
|
||||
<a href="?{% url_replace request 'ordering' '-total' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'total' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
<th class="text-right flip">{% trans "Positions" %}</th>
|
||||
<th class="text-right flip">{% trans "Status" %}
|
||||
<a href="?{% url_replace request 'ordering' '-status' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'status' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for o in orders %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
<a
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=o.code %}">
|
||||
{{ o.code }}</a>
|
||||
</strong>
|
||||
{% if o.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
{% if o.custom_followup_due %}
|
||||
<span class="label label-danger">{% blocktrans with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}TODO {{ date }}{% endblocktrans %}</span>
|
||||
{% elif o.custom_followup_at %}
|
||||
<span class="label label-default">{% blocktrans with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}TODO {{ date }}{% endblocktrans %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ o.email|default_if_none:"" }}
|
||||
{% if o.invoice_address.name %}
|
||||
<br>{{ o.invoice_address.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="fa fa-fw fa-{{ o.sales_channel_obj.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{% trans o.sales_channel_obj.verbose_name %}"></span>
|
||||
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{% if o.has_cancellation_request %}
|
||||
<span class="label label-warning">{% trans "CANCELLATION REQUESTED" %}</span>
|
||||
{% endif %}
|
||||
{% if o.has_external_refund or o.has_pending_refund %}
|
||||
<span class="label label-danger">{% trans "REFUND PENDING" %}</span>
|
||||
{% elif o.has_pending_refund %}
|
||||
<span class="label label-warning">{% trans "REFUND PENDING" %}</span>
|
||||
{% endif %}
|
||||
{% if o.is_overpaid %}
|
||||
<span class="label label-warning">{% trans "OVERPAID" %}</span>
|
||||
{% elif o.is_underpaid %}
|
||||
<span class="label label-danger">{% trans "UNDERPAID" %}</span>
|
||||
{% elif o.is_pending_with_full_payment %}
|
||||
<span class="label label-danger">{% trans "FULLY PAID" %}</span>
|
||||
{% endif %}
|
||||
{% if o.computed_payment_refund_sum == o.total or o.computed_payment_refund_sum == 0 %}
|
||||
<span class="text-muted">
|
||||
{% endif %}
|
||||
{{ o.computed_payment_refund_sum|money:request.event.currency }} /
|
||||
{% if o.computed_payment_refund_sum == o.total or o.computed_payment_refund_sum == 0 %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{{ o.total|money:request.event.currency }}
|
||||
{% if o.status == "c" and o.icnt %}
|
||||
<br>
|
||||
<span class="label label-warning">{% trans "INVOICE NOT CANCELED" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">{{ o.pcnt|default_if_none:"0" }}</td>
|
||||
<td class="text-right flip">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td>
|
||||
</tr>
|
||||
<form action="{% url "control:organizer.device.bulk_edit" organizer=request.organizer.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in filter_form %}
|
||||
{{ field.as_hidden }}
|
||||
{% endfor %}
|
||||
{% for form in filter_forms %}
|
||||
{% for field in form %}
|
||||
{{ field.as_hidden }}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% if sums %}
|
||||
<tfoot>
|
||||
{% endfor %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover table-orders">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Sum over all pages" %}</th>
|
||||
<th></th>
|
||||
<th>
|
||||
{% blocktrans trimmed count s=sums.c %}
|
||||
1 order
|
||||
{% plural %}
|
||||
{{ s }} orders
|
||||
{% endblocktrans %}
|
||||
<label aria-label="{% trans "select all rows for batch-operation" %}"
|
||||
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
|
||||
</th>
|
||||
<th class="text-right flip">
|
||||
{% if not filter_form.filtered %}
|
||||
<span class="fa fa-info-circle text-muted" data-toggle="tooltip"
|
||||
title='{% trans 'This sum includes canceled orders. For your ticket revenue, look at the "order overview".' %}'></span>
|
||||
{% if sums.s|default_if_none:"none" != "none" %}
|
||||
{{ sums.s|money:request.event.currency }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<th>{% trans "Order code" %}
|
||||
<a href="?{% url_replace request 'ordering' '-code' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'code' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th class="text-right flip">
|
||||
{% if not filter_form.filtered %}
|
||||
<span class="fa fa-info-circle text-muted" data-toggle="tooltip"
|
||||
title='{% trans 'This sum includes canceled orders. For your ticket revenue, look at the "order overview".' %}'></span>
|
||||
{% if sums.pc %}
|
||||
{{ sums.pc }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<th>{% trans "User" %}
|
||||
<a href="?{% url_replace request 'ordering' '-email' %}"><i
|
||||
class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'email' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>{% trans "Order date" %}
|
||||
<a href="?{% url_replace request 'ordering' '-datetime' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'datetime' %}"><i
|
||||
class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th class="text-right flip">{% trans "Order paid / total" %}
|
||||
<a href="?{% url_replace request 'ordering' '-total' %}"><i
|
||||
class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'total' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th class="text-right flip">{% trans "Positions" %}</th>
|
||||
<th class="text-right flip">{% trans "Status" %}
|
||||
<a href="?{% url_replace request 'ordering' '-status' %}"><i
|
||||
class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'status' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<tr class="table-select-all warning hidden">
|
||||
<td>
|
||||
<input type="checkbox" name="__ALL" id="__all"
|
||||
data-results-total="{{ page_obj.paginator.count }}">
|
||||
</td>
|
||||
<td colspan="6">
|
||||
<label for="__all">
|
||||
{% trans "Select all results on other pages as well" %}
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for o in orders %}
|
||||
<tr>
|
||||
<td>
|
||||
<label aria-label="{% trans "select row for batch-operation" %}"
|
||||
class="batch-select-label"><input type="checkbox" name="order"
|
||||
class="batch-select-checkbox"
|
||||
value="{{ o.pk }}"/></label>
|
||||
</td>
|
||||
<td>
|
||||
<strong>
|
||||
<a
|
||||
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=o.code %}">
|
||||
{{ o.code }}</a>
|
||||
</strong>
|
||||
{% if o.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
{% if o.custom_followup_due %}
|
||||
<span class="label label-danger">{% blocktrans with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}
|
||||
TODO {{ date }}{% endblocktrans %}</span>
|
||||
{% elif o.custom_followup_at %}
|
||||
<span class="label label-default">{% blocktrans with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}
|
||||
TODO {{ date }}{% endblocktrans %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ o.email|default_if_none:"" }}
|
||||
{% if o.invoice_address.name %}
|
||||
<br>{{ o.invoice_address.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="fa fa-fw fa-{{ o.sales_channel_obj.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{% trans o.sales_channel_obj.verbose_name %}"></span>
|
||||
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{% if o.has_cancellation_request %}
|
||||
<span class="label label-warning">{% trans "CANCELLATION REQUESTED" %}</span>
|
||||
{% endif %}
|
||||
{% if o.has_external_refund or o.has_pending_refund %}
|
||||
<span class="label label-danger">{% trans "REFUND PENDING" %}</span>
|
||||
{% elif o.has_pending_refund %}
|
||||
<span class="label label-warning">{% trans "REFUND PENDING" %}</span>
|
||||
{% endif %}
|
||||
{% if o.is_overpaid %}
|
||||
<span class="label label-warning">{% trans "OVERPAID" %}</span>
|
||||
{% elif o.is_underpaid %}
|
||||
<span class="label label-danger">{% trans "UNDERPAID" %}</span>
|
||||
{% elif o.is_pending_with_full_payment %}
|
||||
<span class="label label-danger">{% trans "FULLY PAID" %}</span>
|
||||
{% endif %}
|
||||
{% if o.computed_payment_refund_sum == o.total or o.computed_payment_refund_sum == 0 %}
|
||||
<span class="text-muted">
|
||||
{% endif %}
|
||||
{{ o.computed_payment_refund_sum|money:request.event.currency }} /
|
||||
{% if o.computed_payment_refund_sum == o.total or o.computed_payment_refund_sum == 0 %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{{ o.total|money:request.event.currency }}
|
||||
{% if o.status == "c" and o.icnt %}
|
||||
<br>
|
||||
<span class="label label-warning">{% trans "INVOICE NOT CANCELED" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">{{ o.pcnt|default_if_none:"0" }}</td>
|
||||
<td class="text-right flip">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% if sums %}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans "Sum over all pages" %}</th>
|
||||
<th></th>
|
||||
<th>
|
||||
{% blocktrans trimmed count s=sums.c %}
|
||||
1 order
|
||||
{% plural %}
|
||||
{{ s }} orders
|
||||
{% endblocktrans %}
|
||||
</th>
|
||||
<th class="text-right flip">
|
||||
{% if not filter_form.filtered %}
|
||||
<span class="fa fa-info-circle text-muted" data-toggle="tooltip"
|
||||
title='{% trans 'This sum includes canceled orders. For your ticket revenue, look at the "order overview".' %}'></span>
|
||||
{% if sums.s|default_if_none:"none" != "none" %}
|
||||
{{ sums.s|money:request.event.currency }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</th>
|
||||
<th class="text-right flip">
|
||||
{% if not filter_form.filtered %}
|
||||
<span class="fa fa-info-circle text-muted" data-toggle="tooltip"
|
||||
title='{% trans 'This sum includes canceled orders. For your ticket revenue, look at the "order overview".' %}'></span>
|
||||
{% if sums.pc %}
|
||||
{{ sums.pc }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
<div class="batch-select-actions">
|
||||
<div class="btn-group dropup">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
{% trans "Select action" %}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<button type="submit" class="btn"
|
||||
formaction="{% url "control:event.orders.bulk.approve" organizer=request.organizer.slug event=request.event.slug %}">
|
||||
<i class="fa fa-thumbs-up fa-fw text-green"></i> {% trans "Approve" %}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="submit" class="btn"
|
||||
formaction="{% url "control:event.orders.bulk.deny" organizer=request.organizer.slug event=request.event.slug %}">
|
||||
<i class="fa fa-thumbs-down fa-fw text-danger"></i> {% trans "Deny" %}
|
||||
</button>
|
||||
</li>
|
||||
{% if not request.event.settings.payment_term_expire_automatically %}
|
||||
<li>
|
||||
<button type="submit" class="btn"
|
||||
formaction="{% url "control:event.orders.bulk.expire" organizer=request.organizer.slug event=request.event.slug %}">
|
||||
<i class="fa fa-times fa-fw"></i>{% trans "Mark as expired if overdue" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<button type="submit" class="btn"
|
||||
formaction="{% url "control:event.orders.bulk.delete" organizer=request.organizer.slug event=request.event.slug %}">
|
||||
<i class="fa fa-trash fa-fw text-danger"></i>{% trans "Delete (test mode only)" %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -414,6 +414,10 @@ urlpatterns = [
|
||||
re_path(r'^orders/refunds/$', orders.RefundList.as_view(), name='event.orders.refunds'),
|
||||
re_path(r'^orders/go$', orders.OrderGo.as_view(), name='event.orders.go'),
|
||||
re_path(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
|
||||
re_path(r'^orders/bulk/approve$', orders.OrderApproveBulkActionView.as_view(), name='event.orders.bulk.approve'),
|
||||
re_path(r'^orders/bulk/deny$', orders.OrderDenyBulkActionView.as_view(), name='event.orders.bulk.deny'),
|
||||
re_path(r'^orders/bulk/expire$', orders.OrderExpireBulkActionView.as_view(), name='event.orders.bulk.expire'),
|
||||
re_path(r'^orders/bulk/delete$', orders.OrderDeleteBulkActionView.as_view(), name='event.orders.bulk.delete'),
|
||||
re_path(r'^orders/search$', orders.OrderSearch.as_view(), name='event.orders.search'),
|
||||
re_path(r'^dangerzone/$', event.DangerZone.as_view(), name='event.dangerzone'),
|
||||
re_path(r'^cancel/$', orders.EventCancel.as_view(), name='event.cancel'),
|
||||
|
||||
@@ -192,7 +192,6 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList
|
||||
|
||||
|
||||
class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMixin, AsyncPostView):
|
||||
template_name = 'pretixcontrol/organizers/device_bulk_edit.html'
|
||||
permission = ('can_change_orders', 'can_checkin_orders')
|
||||
context_object_name = 'device'
|
||||
|
||||
|
||||
@@ -45,12 +45,12 @@ from urllib.parse import quote, urlencode
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
Count, Exists, F, IntegerField, OuterRef, Prefetch, ProtectedError, Q,
|
||||
Subquery, Sum,
|
||||
QuerySet, Subquery, Sum,
|
||||
)
|
||||
from django.forms import formset_factory
|
||||
from django.http import (
|
||||
@@ -111,16 +111,16 @@ from pretix.base.signals import (
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.base.views.mixins import OrderQuestionsViewMixin
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.base.views.tasks import AsyncAction, AsyncFormView
|
||||
from pretix.control.forms.exports import ScheduledEventExportForm
|
||||
from pretix.control.forms.filter import (
|
||||
EventOrderExpertFilterForm, EventOrderFilterForm, OverviewFilterForm,
|
||||
RefundFilterForm,
|
||||
)
|
||||
from pretix.control.forms.orders import (
|
||||
CancelForm, CommentForm, ConfirmPaymentForm, EventCancelForm, ExporterForm,
|
||||
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm,
|
||||
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
||||
CancelForm, CommentForm, ConfirmPaymentForm, DenyForm, EventCancelForm,
|
||||
ExporterForm, ExtendForm, MarkPaidForm, OrderContactForm,
|
||||
OrderFeeChangeForm, OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
||||
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
|
||||
OrderRefundForm, OtherOperationsForm, ReactivateOrderForm,
|
||||
)
|
||||
@@ -137,10 +137,17 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OrderSearchMixin:
|
||||
|
||||
@cached_property
|
||||
def request_data(self):
|
||||
if self.request.method == "POST":
|
||||
return self.request.POST
|
||||
return self.request.GET
|
||||
|
||||
def get_forms(self):
|
||||
f = [
|
||||
EventOrderExpertFilterForm(
|
||||
data=self.request.GET,
|
||||
data=self.request_data,
|
||||
event=self.request.event,
|
||||
prefix='expert',
|
||||
)
|
||||
@@ -160,6 +167,167 @@ class OrderSearch(OrderSearchMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
return ctx
|
||||
|
||||
|
||||
class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, AsyncFormView):
|
||||
template_name = 'pretixcontrol/orders/bulk_action.html'
|
||||
permission = 'can_change_orders'
|
||||
form_class = forms.Form
|
||||
|
||||
def get_queryset(self):
|
||||
qs = Order.objects.filter(
|
||||
event=self.request.event
|
||||
).select_related('invoice_address')
|
||||
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
|
||||
for f in self.get_forms():
|
||||
if any(k.startswith(f.prefix) for k in self.request.POST.keys()):
|
||||
if not f.is_valid():
|
||||
raise PermissionDenied("Invalid query") # better safe than sorry with this one
|
||||
qs = f.filter_qs(qs)
|
||||
|
||||
if 'order' in self.request_data and '__ALL' not in self.request_data:
|
||||
qs = qs.filter(
|
||||
id__in=self.request_data.getlist('order')
|
||||
)
|
||||
elif '__ALL' not in self.request_data:
|
||||
raise PermissionDenied("Invalid query") # better safe than sorry with this one
|
||||
|
||||
return qs
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return EventOrderFilterForm(data=self.request.POST, event=self.request.event)
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
def allowed_for(self, queryset: QuerySet) -> QuerySet:
|
||||
raise NotImplementedError()
|
||||
|
||||
def execute_single(self, instance, form: forms.Form):
|
||||
raise NotImplementedError()
|
||||
|
||||
def execute_bulk(self, queryset: QuerySet, form: forms.Form):
|
||||
qs = self.allowed_for(self.allowed_for(self.get_queryset()))
|
||||
total = qs.count()
|
||||
for i, o in enumerate(qs):
|
||||
self.execute_single(o, form)
|
||||
if i % 100 == 0:
|
||||
self.async_set_progress(i / total * 100)
|
||||
|
||||
def get_error_url(self):
|
||||
return self.get_success_url(None)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||
return self.get_result(request)
|
||||
return render(request, self.template_name, self.get_context_data())
|
||||
|
||||
def get_success_url(self, value):
|
||||
return reverse('control:event.orders', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['total'] = self.get_queryset().count()
|
||||
ctx['allowed'] = self.allowed_for(self.get_queryset())
|
||||
ctx['label'] = self.label
|
||||
ctx['form'] = self.get_form()
|
||||
return ctx
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = {
|
||||
"initial": self.get_initial(),
|
||||
"prefix": self.get_prefix(),
|
||||
}
|
||||
|
||||
if self.request.method in ("POST", "PUT") and self.request.POST.get("operation") == "confirm":
|
||||
kwargs.update(
|
||||
{
|
||||
"data": self.request.POST,
|
||||
"files": self.request.FILES,
|
||||
}
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Handle POST requests: instantiate a form instance with the passed
|
||||
POST variables and then check if it's valid.
|
||||
"""
|
||||
form = self.get_form()
|
||||
if self.request.POST.get("operation") == "confirm" and form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def get_prefix(self):
|
||||
return "bulkactionform"
|
||||
|
||||
@transaction.atomic()
|
||||
def async_form_valid(self, task, form):
|
||||
self.execute_bulk(self.allowed_for(self.get_queryset()), form)
|
||||
|
||||
|
||||
class OrderApproveBulkActionView(BaseOrderBulkActionView):
|
||||
label = _("Approve")
|
||||
|
||||
def allowed_for(self, queryset):
|
||||
return queryset.filter(
|
||||
status=Order.STATUS_PENDING,
|
||||
require_approval=True,
|
||||
)
|
||||
|
||||
def execute_single(self, instance, form: forms.Form):
|
||||
approve_order(instance, user=self.request.user)
|
||||
|
||||
|
||||
class OrderDenyBulkActionView(BaseOrderBulkActionView):
|
||||
label = _("Deny")
|
||||
form_class = DenyForm
|
||||
|
||||
def allowed_for(self, queryset):
|
||||
return queryset.filter(
|
||||
status=Order.STATUS_PENDING,
|
||||
require_approval=True,
|
||||
)
|
||||
|
||||
def execute_single(self, instance, form: forms.Form):
|
||||
deny_order(instance, user=self.request.user,
|
||||
comment=form.cleaned_data.get('comment') or None,
|
||||
send_mail=form.cleaned_data['send_email'])
|
||||
|
||||
|
||||
class OrderExpireBulkActionView(BaseOrderBulkActionView):
|
||||
label = _("Mark as expired if overdue")
|
||||
|
||||
def allowed_for(self, queryset):
|
||||
return queryset.filter(
|
||||
status=Order.STATUS_PENDING,
|
||||
require_approval=False,
|
||||
expires__lt=now(),
|
||||
)
|
||||
|
||||
def execute_single(self, instance, form: forms.Form):
|
||||
mark_order_expired(instance, user=self.request.user)
|
||||
|
||||
|
||||
class OrderDeleteBulkActionView(BaseOrderBulkActionView):
|
||||
label = _("Delete")
|
||||
|
||||
def allowed_for(self, queryset):
|
||||
return queryset.filter(
|
||||
testmode=True,
|
||||
)
|
||||
|
||||
def execute_single(self, instance, form: forms.Form):
|
||||
instance.gracefully_delete(user=self.request.user)
|
||||
|
||||
|
||||
class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
model = Order
|
||||
context_object_name = 'orders'
|
||||
@@ -183,6 +351,7 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['filter_form'] = self.filter_form
|
||||
ctx['filter_forms'] = self.get_forms()
|
||||
|
||||
ctx['filter_strings'] = []
|
||||
for f in self.get_forms():
|
||||
@@ -607,21 +776,26 @@ class OrderDelete(OrderView):
|
||||
class OrderDeny(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
def post(self, request, *args, **kwargs):
|
||||
if self.order.require_approval:
|
||||
try:
|
||||
deny_order(self.order, user=self.request.user,
|
||||
comment=self.request.POST.get('comment'),
|
||||
send_mail=self.request.POST.get('send_email') == 'on')
|
||||
except OrderError as e:
|
||||
messages.error(self.request, str(e))
|
||||
form = DenyForm(self.request.POST if self.request.method == "POST" else None)
|
||||
if form.is_valid():
|
||||
try:
|
||||
deny_order(self.order, user=self.request.user,
|
||||
comment=self.request.POST.get('comment'),
|
||||
send_mail=self.request.POST.get('send_email') == 'on')
|
||||
except OrderError as e:
|
||||
messages.error(self.request, str(e))
|
||||
else:
|
||||
messages.success(self.request, _('The order has been denied and is therefore now canceled.'))
|
||||
else:
|
||||
messages.success(self.request, _('The order has been denied and is therefore now canceled.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return render(self.request, 'pretixcontrol/order/deny.html', {
|
||||
'order': self.order,
|
||||
'form': DenyForm(self.request.POST if self.request.method == "POST" else None)
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -784,7 +784,7 @@ function setup_basics(el) {
|
||||
el.find("input[data-toggle-table]").each(function (ev) {
|
||||
var $toggle = $(this);
|
||||
var $actionButtons = $(".batch-select-actions button", this.form);
|
||||
var countLabels = $("<span></span>").appendTo($actionButtons);
|
||||
var countLabels = $("<span></span>").appendTo($actionButtons.filter(function () { return !$(this).closest(".dropdown-menu").length }));
|
||||
var $table = $toggle.closest("table");
|
||||
var $selectAll = $table.find(".table-select-all");
|
||||
var $rows = $table.find("tbody tr");
|
||||
|
||||
@@ -788,12 +788,32 @@ table td > .checkbox input[type="checkbox"] {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
|
||||
.batch-select-label {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 1.5em;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dropdown-menu > li > button.btn {
|
||||
display: block;
|
||||
padding: 3px 20px;
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
line-height: $line-height-base;
|
||||
color: $dropdown-link-color;
|
||||
white-space: nowrap;
|
||||
background: inherit;
|
||||
border: 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
&:hover, &:focus {
|
||||
color: $dropdown-link-hover-color;
|
||||
text-decoration: none;
|
||||
background-color: $dropdown-link-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.bulk-edit-field-group {
|
||||
|
||||
Reference in New Issue
Block a user