Introduce cancellation requests (#1627)

* Allow to adjust the cancellation fee without JS

* Introduce cancellation requests

* ignore→delete

* Change a few things after Martin's review

* Add a few tests
This commit is contained in:
Raphael Michel
2020-03-25 14:13:55 +01:00
committed by GitHub
parent 173a23722a
commit 8a6334bd86
24 changed files with 352 additions and 33 deletions

View File

@@ -32,6 +32,7 @@ from pretix.control.signals import (
)
from pretix.helpers.daterange import daterange
from ...base.models.orders import CancellationRequest
from ..logdisplay import OVERVIEW_BANLIST
NUM_WIDGET = '<div class="numwidget"><span class="num">{num}</span><span class="text">{text}</span></div>'
@@ -344,6 +345,9 @@ def event_index(request, organizer, event):
status=Order.STATUS_PENDING,
require_approval=True
).exists()
ctx['has_cancellation_requests'] = CancellationRequest.objects.filter(
order__event=request.event
).exists()
for a in ctx['actions']:
a.display = a.display(request)

View File

@@ -13,7 +13,8 @@ from django.contrib import messages
from django.core.files import File
from django.db import transaction
from django.db.models import (
Count, IntegerField, OuterRef, Prefetch, ProtectedError, Q, Subquery, Sum,
Count, Exists, IntegerField, OuterRef, Prefetch, ProtectedError, Q,
Subquery, Sum,
)
from django.forms import formset_factory
from django.http import (
@@ -33,6 +34,7 @@ from django.views.generic import (
from i18nfield.strings import LazyI18nString
from pretix.base.channels import get_all_sales_channels
from pretix.base.decimal import round_decimal
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import (
@@ -41,7 +43,7 @@ from pretix.base.models import (
generate_position_secret, generate_secret,
)
from pretix.base.models.orders import (
OrderFee, OrderPayment, OrderPosition, OrderRefund,
CancellationRequest, OrderFee, OrderPayment, OrderPosition, OrderRefund,
)
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
from pretix.base.payment import PaymentException
@@ -116,10 +118,11 @@ class OrderList(EventPermissionRequiredMixin, PaginationMixin, ListView):
Order.annotate_overpayments(Order.objects).filter(
pk__in=[o.pk for o in ctx['orders']]
).annotate(
pcnt=Subquery(s, output_field=IntegerField())
pcnt=Subquery(s, output_field=IntegerField()),
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef('pk')))
).values(
'pk', 'pcnt', 'is_overpaid', 'is_underpaid', 'is_pending_with_full_payment', 'has_external_refund',
'has_pending_refund'
'has_pending_refund', 'has_cancellation_request'
)
}
@@ -132,6 +135,7 @@ class OrderList(EventPermissionRequiredMixin, PaginationMixin, ListView):
o.is_pending_with_full_payment = annotated.get(o.pk)['is_pending_with_full_payment']
o.has_external_refund = annotated.get(o.pk)['has_external_refund']
o.has_pending_refund = annotated.get(o.pk)['has_pending_refund']
o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request']
if ctx['page_obj'].paginator.count < 1000:
# Performance safeguard: Only count positions if the data set is small
@@ -607,6 +611,40 @@ class OrderRefundDone(OrderView):
})
class OrderCancellationRequestDelete(OrderView):
permission = 'can_change_orders'
@cached_property
def req(self):
return get_object_or_404(self.order.cancellation_requests, pk=self.kwargs['req'])
def post(self, *args, **kwargs):
with transaction.atomic():
self.req.delete()
self.order.log_action('pretix.event.order.cancellationrequest.deleted', {
}, user=self.request.user)
messages.success(self.request, _('The request has been removed. If you want, you can now inform the user.'))
with language(self.order.locale):
return redirect(reverse('control:event.order.sendmail', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'code': self.order.code
}) + '?' + urlencode({
'subject': _('Your cancellation request'),
'message': _('Hello,\n\nunfortunately, we were unable to accommodate your request and cancel your '
'order.\n\n'
'Your {event} team').format(
event="{event}",
)
}))
def get(self, *args, **kwargs):
return render(self.request, 'pretixcontrol/order/cancellation_request_delete.html', {
'order': self.order,
})
class OrderPaymentConfirm(OrderView):
permission = 'can_change_orders'
@@ -679,7 +717,14 @@ class OrderRefundView(OrderView):
full_refund = self.order.payment_refund_sum
else:
full_refund = self.start_form.cleaned_data.get('partial_amount')
proposals = self.order.propose_auto_refunds(full_refund, payments=payments)
if self.request.GET.get('giftcard', 'false') == 'true':
proposals = {
None: full_refund
}
giftcard_proposal = full_refund
else:
proposals = self.order.propose_auto_refunds(full_refund, payments=payments)
giftcard_proposal = Decimal('0.00')
to_refund = full_refund - sum(proposals.values())
for p in payments:
p.propose_refund = proposals.get(p, 0)
@@ -873,6 +918,7 @@ class OrderRefundView(OrderView):
'payments': payments,
'remainder': to_refund,
'order': self.order,
'giftcard_proposal': giftcard_proposal,
'partial_amount': (
self.request.POST.get('start-partial_amount') if self.request.method == 'POST'
else self.request.GET.get('start-partial_amount')
@@ -897,6 +943,12 @@ class OrderRefundView(OrderView):
class OrderTransition(OrderView):
permission = 'can_change_orders'
@cached_property
def req(self):
if 'req' not in self.request.GET:
return None
return get_object_or_404(self.order.cancellation_requests, pk=self.request.GET.get('req'))
@cached_property
def mark_paid_form(self):
return MarkPaidForm(
@@ -909,6 +961,9 @@ class OrderTransition(OrderView):
return CancelForm(
instance=self.order,
data=self.request.POST if self.request.method == "POST" else None,
initial={
'cancellation_fee': self.req.cancellation_fee if self.req else Decimal('0.00')
}
)
def post(self, *args, **kwargs):
@@ -997,8 +1052,9 @@ class OrderTransition(OrderView):
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'code': self.order.code
}) + '?start-action=do_nothing&start-mode=partial&start-partial_amount={}'.format(
self.order.pending_sum * -1
}) + '?start-action=do_nothing&start-mode=partial&start-partial_amount={}&giftcard={}'.format(
round_decimal(self.order.pending_sum * -1),
'true' if self.req and self.req.refund_as_giftcard else 'false'
))
messages.success(self.request, _('The order has been canceled.'))