From 04645869d5b56951837b3832dd90d133562c2b95 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 28 Feb 2019 18:26:40 +0100 Subject: [PATCH] first steps --- src/pretix/base/models/orders.py | 65 +++++++++++++++-- src/pretix/control/forms/event.py | 8 +++ .../templates/pretixcontrol/event/cancel.html | 2 + .../templates/pretixpresale/event/order.html | 19 +++++ .../pretixpresale/event/order_cancel.html | 31 +++++--- .../event/order_cancel_choose.html | 70 +++++++++++++++++++ src/pretix/presale/views/order.py | 29 ++++++-- 7 files changed, 207 insertions(+), 17 deletions(-) create mode 100644 src/pretix/presale/templates/pretixpresale/event/order_cancel_choose.html diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 6a8d22a0a4..1592e8d429 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -12,7 +12,7 @@ import pytz from django.conf import settings from django.db import models, transaction from django.db.models import ( - Case, Exists, F, Max, OuterRef, Q, Subquery, Sum, Value, When, + Case, Exists, F, Max, OuterRef, Prefetch, Q, Subquery, Sum, Value, When, ) from django.db.models.functions import Coalesce from django.db.models.signals import post_delete @@ -404,14 +404,13 @@ class Order(LockModel, LoggedModel): else: return until.datetime(self.event) - @cached_property - def user_cancel_fee(self): + def user_partial_cancel_fee(self, total: Decimal): fee = Decimal('0.00') if self.event.settings.cancel_allow_user_paid_keep: fee += self.event.settings.cancel_allow_user_paid_keep if self.event.settings.cancel_allow_user_paid_keep_percentage: - fee += self.event.settings.cancel_allow_user_paid_keep_percentage / Decimal('100.0') * self.total - if self.event.settings.cancel_allow_user_paid_keep_fees: + fee += self.event.settings.cancel_allow_user_paid_keep_percentage / Decimal('100.0') * total + if self.event.settings.cancel_allow_user_paid_keep_fees and self.total == total: fee += self.fees.filter( fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE) ).aggregate( @@ -419,6 +418,62 @@ class Order(LockModel, LoggedModel): )['s'] or 0 return round_decimal(fee, self.event.currency) + @cached_property + def user_cancel_fee(self): + return self.user_partial_cancel_fee(self.total) + + @property + def user_cancel_partial_positions(self) -> list: + """ + Returns a list of positions in this order that can be cancelled individually. + """ + from .checkin import Checkin + + if self.user_cancel_deadline and now() > self.user_cancel_deadline: + return [] + + if self.status == Order.STATUS_PENDING: + if not self.event.settings.cancel_allow_user or not self.event.settings.cancel_allow_user_per_position: + return [] + elif self.status == Order.STATUS_PAID: + if not self.event.settings.cancel_allow_user_paid or not self.event.settings.cancel_allow_user_paid_per_position: + return [] + if self.total == Decimal('0.00'): + if not self.event.settings.cancel_allow_user or not self.event.settings.cancel_allow_user_per_position: + return [] + else: + return [] + + pos = list( + self.positions.annotate( + has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'))) + ).filter( + has_checkin=False, + item__allow_cancel=True + ).select_related('item', 'addon_to').prefetch_related( + 'item__addons', + Prefetch( + 'addon_to__addons', + to_attr='siblings' + ) + ).distinct() + ) + allowed = [] + for p in pos: + if p.addon_to_id: + addonconf = [a for a in p.item.addons.all() if a.category_id == p.item.category_id] + if len(addonconf) > 0: + addonconf = addonconf[0] + if addonconf.min_count > 0 and len(p.siblings) <= addonconf.min_count: + continue + + allowed.append(p) + + if len(allowed) == self.positions.count(): + return [] + + return pos + @property def user_cancel_allowed(self) -> bool: """ diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 5f7f300bde..2d4aa4cc6c 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -450,6 +450,10 @@ class CancelSettingsForm(SettingsForm): label=_("Do not allow cancellations after"), required=False ) + cancel_allow_user_per_position = forms.BooleanField( + label=_("Customers can cancel individual products in their order"), + required=False + ) cancel_allow_user_paid = forms.BooleanField( label=_("Customers can cancel their paid orders"), help_text=_("Paid money will be automatically paid back if the payment method allows it. " @@ -472,6 +476,10 @@ class CancelSettingsForm(SettingsForm): label=_("Do not allow cancellations after"), required=False ) + cancel_allow_user_paid_per_position = forms.BooleanField( + label=_("Customers can cancel individual products in their order"), + required=False + ) class PaymentSettingsForm(SettingsForm): diff --git a/src/pretix/control/templates/pretixcontrol/event/cancel.html b/src/pretix/control/templates/pretixcontrol/event/cancel.html index 004e701b42..c71eb31e23 100644 --- a/src/pretix/control/templates/pretixcontrol/event/cancel.html +++ b/src/pretix/control/templates/pretixcontrol/event/cancel.html @@ -10,6 +10,7 @@ {% trans "Cancellation of unpaid or free orders" %} {% bootstrap_field form.cancel_allow_user layout="control" %} {% bootstrap_field form.cancel_allow_user_until layout="control" %} + {% bootstrap_field form.cancel_allow_user_per_position layout="control" %}
{% trans "Cancellation of paid orders" %} @@ -18,6 +19,7 @@ {% bootstrap_field form.cancel_allow_user_paid_keep_percentage layout="control" %} {% bootstrap_field form.cancel_allow_user_paid_keep_fees layout="control" %} {% bootstrap_field form.cancel_allow_user_paid_until layout="control" %} + {% bootstrap_field form.cancel_allow_user_paid_per_position layout="control" %} {% if not gets_notification %}
{% blocktrans trimmed %} diff --git a/src/pretix/presale/templates/pretixpresale/event/order.html b/src/pretix/presale/templates/pretixpresale/event/order.html index 5f0290cb13..3ad0654f2d 100644 --- a/src/pretix/presale/templates/pretixpresale/event/order.html +++ b/src/pretix/presale/templates/pretixpresale/event/order.html @@ -292,6 +292,25 @@ {% trans "Cancel order" %} {% endif %} + {% elif order.user_cancel_partial_positions %} + {% if order.status == "p" and order.total != 0 %} +

+ {% blocktrans trimmed %} + You can cancel parts of this order and receive a refund to your original payment method. + {% endblocktrans %} +

+ {% else %} +

+ {% blocktrans trimmed %} + You can cancel parts of this order. + {% endblocktrans %} +

+ {% endif %} + + + {% trans "Start cancellation" %} + {% else %}

{% blocktrans trimmed %} diff --git a/src/pretix/presale/templates/pretixpresale/event/order_cancel.html b/src/pretix/presale/templates/pretixpresale/event/order_cancel.html index b110891b3e..86c32c5706 100644 --- a/src/pretix/presale/templates/pretixpresale/event/order_cancel.html +++ b/src/pretix/presale/templates/pretixpresale/event/order_cancel.html @@ -9,13 +9,28 @@ Cancel order: {{ code }} {% endblocktrans %} -

- {% blocktrans trimmed %} - Do you really want to cancel this order? You cannot revert this action. - {% endblocktrans %} - {% trans "This will invalidate all of your tickets." %} -

- {% if can_auto_refund %} + {% if selected_positions %} +

+ {% trans "The following positions of your order will be canceled:" %} +

+
    + {% for pos in selected_positions %} +
  • + #{{ pos.positionid }} – + {{ pos.item }} {% if pos.variation %}– {{ pos.variation }}{% endif %} + {% if pos.attendee_name %}({{ pos.attendee_name }}){% endif %} +
  • + {% endfor %} +
+ {% else %} +

+ {% blocktrans trimmed %} + Do you really want to cancel this order? You cannot revert this action. + {% endblocktrans %} + {% trans "This will invalidate all of your tickets." %} +

+ {% endif %} + {% if can_auto_refund and refund_amount != 0 %}

{% blocktrans trimmed with amount=refund_amount|money:request.event.currency %} @@ -24,7 +39,7 @@ {% endblocktrans %}

- {% else %} + {% elif refund_amount != 0 %}
{% blocktrans trimmed with amount=refund_amount|money:request.event.currency %} With to the payment method you used, the refund amount of {{ amount }} can not be sent back to you automatically. Instead, the diff --git a/src/pretix/presale/templates/pretixpresale/event/order_cancel_choose.html b/src/pretix/presale/templates/pretixpresale/event/order_cancel_choose.html new file mode 100644 index 0000000000..5aef11cc8d --- /dev/null +++ b/src/pretix/presale/templates/pretixpresale/event/order_cancel_choose.html @@ -0,0 +1,70 @@ +{% extends "pretixpresale/event/base.html" %} +{% load i18n %} +{% load money %} +{% load eventurl %} +{% block title %}{% trans "Cancel order" %}{% endblock %} +{% block content %} +

+ {% blocktrans trimmed with code=order.code %} + Cancel order: {{ code }} + {% endblocktrans %} +

+ +
+ {% csrf_token %} + +

+ {% blocktrans trimmed %} + Please choose which parts of your order you want to cancel: + {% endblocktrans %} +

+ + {% for pos in cancellable_positions %} +
+ +
+ {% endfor %} + + {% if order.status == "p" and order.total != 0 %} +

+ {% blocktrans trimmed %} + Before your purchase is cancelled, you will be shown the refund amount and asked to confirm the cancellation. + {% endblocktrans %} +

+ {% endif %} + +
+ +
+ {% if order.status == "p" and order.total != 0 %} + + {% else %} + + {% endif %} +
+
+
+
+ +{% endblock %} diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index 2a1f52cee5..b4a3fc9f34 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -558,28 +558,49 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem @method_decorator(xframe_options_exempt, 'dispatch') class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView): - template_name = "pretixpresale/event/order_cancel.html" + + def get_template_names(self): + if self.cancellable_positions and not self.selected_positions: + return ["pretixpresale/event/order_cancel_choose.html"] + else: + return ["pretixpresale/event/order_cancel.html"] + + @cached_property + def selected_positions(self): + if self.request.method == "POST": + return [ + p for p in self.order.user_cancel_partial_positions + if str(p.pk) in self.request.POST.getlist("position") + ] + return [] + + @cached_property + def cancellable_positions(self): + return self.order.user_cancel_partial_positions def dispatch(self, request, *args, **kwargs): self.request = request self.kwargs = kwargs if not self.order: raise Http404(_('Unknown order code or not authorized to access this order.')) - if not self.order.user_cancel_allowed: + if not self.order.user_cancel_allowed and not self.cancellable_positions: messages.error(request, _('You cannot cancel this order.')) return redirect(self.get_order_url()) return super().dispatch(request, *args, **kwargs) - def get(self, request, *args, **kwargs): + def post(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['order'] = self.order - refund_amount = self.order.total - self.order.user_cancel_fee + total = sum(p.price for p in self.selected_positions) if self.selected_positions else self.order.total - self.order.pending_sum + refund_amount = total - self.order.user_partial_cancel_fee(total) proposals = self.order.propose_auto_refunds(refund_amount) ctx['refund_amount'] = refund_amount ctx['can_auto_refund'] = sum(proposals.values()) == refund_amount + ctx['cancellable_positions'] = self.cancellable_positions + ctx['selected_positions'] = self.selected_positions return ctx