Orders: Add bulk action to refund overpaid amount (#3721)

* add bulk action to refund overpaid amount

* display number of successful actions, use existing annotate method

* add tests, address review comments

* lint
This commit is contained in:
Julian Baumann
2023-11-23 09:48:28 +01:00
committed by GitHub
parent b639ac850f
commit 8c80200fc0
4 changed files with 93 additions and 2 deletions

View File

@@ -299,6 +299,13 @@
{% trans "Deny" %}
</button>
</li>
<li>
<button type="submit" class="btn"
formaction="{% url "control:event.orders.bulk.refund_overpaid" organizer=request.organizer.slug event=request.event.slug %}">
<i class="fa fa-money fa-fw text-danger"></i>
{% trans "Refund overpaid amount" %}
</button>
</li>
{% if not request.event.settings.payment_term_expire_automatically %}
<li>
<button type="submit" class="btn"

View File

@@ -423,6 +423,7 @@ urlpatterns = [
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/refund_overpaid$', orders.OrderOverpaidRefundBulkActionView.as_view(), name='event.orders.bulk.refund_overpaid'),
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'),

View File

@@ -213,10 +213,14 @@ class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, As
def execute_bulk(self, queryset: QuerySet, form: forms.Form):
qs = self.allowed_for(self.allowed_for(self.get_queryset()))
total = qs.count()
orders_with_successful_action = 0
for i, o in enumerate(qs):
self.execute_single(o, form)
res = self.execute_single(o, form)
if res:
orders_with_successful_action += 1
if i % 100 == 0:
self.async_set_progress(i / total * 100)
return orders_with_successful_action, total
def get_error_url(self):
return self.get_success_url(None)
@@ -232,6 +236,9 @@ class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, As
'organizer': self.request.event.organizer.slug,
})
def get_success_message(self, value):
return _("Succesfully executed the action \"{label}\" on {success} of {total} orders.").format(success=value[0], label=self.label, total=value[1])
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['total'] = self.get_queryset().count()
@@ -271,7 +278,7 @@ class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, As
@transaction.atomic()
def async_form_valid(self, task, form):
self.execute_bulk(self.allowed_for(self.get_queryset()), form)
return self.execute_bulk(self.allowed_for(self.get_queryset()), form)
class OrderApproveBulkActionView(BaseOrderBulkActionView):
@@ -285,6 +292,7 @@ class OrderApproveBulkActionView(BaseOrderBulkActionView):
def execute_single(self, instance, form: forms.Form):
approve_order(instance, user=self.request.user)
return True
class OrderDenyBulkActionView(BaseOrderBulkActionView):
@@ -301,6 +309,7 @@ class OrderDenyBulkActionView(BaseOrderBulkActionView):
deny_order(instance, user=self.request.user,
comment=form.cleaned_data.get('comment') or None,
send_mail=form.cleaned_data['send_email'])
return True
class OrderExpireBulkActionView(BaseOrderBulkActionView):
@@ -315,6 +324,33 @@ class OrderExpireBulkActionView(BaseOrderBulkActionView):
def execute_single(self, instance, form: forms.Form):
mark_order_expired(instance, user=self.request.user)
return True
class OrderOverpaidRefundBulkActionView(BaseOrderBulkActionView):
label = _("Refund overpaid amount")
def allowed_for(self, queryset):
return Order.annotate_overpayments(queryset).filter(is_overpaid=True)
def execute_single(self, instance: Order, form: forms.Form):
if instance.pending_sum < 0:
try:
proposals = instance.propose_auto_refunds(instance.pending_sum * -1)
for payment, amount in proposals.items():
refund = OrderRefund.objects.create(
order=instance,
payment=payment,
source=OrderRefund.REFUND_SOURCE_ADMIN,
state=OrderRefund.REFUND_STATE_CREATED,
amount=amount,
comment=_("Refund for overpayment"),
provider=payment.provider
)
payment.payment_provider.execute_refund(refund)
return True
except (ValueError, PaymentException):
return False
class OrderDeleteBulkActionView(BaseOrderBulkActionView):