forked from CGM_Public/pretix_original
Compare commits
1 Commits
v4.20.0
...
selfcancel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04645869d5 |
@@ -12,7 +12,7 @@ import pytz
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import (
|
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.functions import Coalesce
|
||||||
from django.db.models.signals import post_delete
|
from django.db.models.signals import post_delete
|
||||||
@@ -404,14 +404,13 @@ class Order(LockModel, LoggedModel):
|
|||||||
else:
|
else:
|
||||||
return until.datetime(self.event)
|
return until.datetime(self.event)
|
||||||
|
|
||||||
@cached_property
|
def user_partial_cancel_fee(self, total: Decimal):
|
||||||
def user_cancel_fee(self):
|
|
||||||
fee = Decimal('0.00')
|
fee = Decimal('0.00')
|
||||||
if self.event.settings.cancel_allow_user_paid_keep:
|
if self.event.settings.cancel_allow_user_paid_keep:
|
||||||
fee += 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:
|
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
|
fee += self.event.settings.cancel_allow_user_paid_keep_percentage / Decimal('100.0') * total
|
||||||
if self.event.settings.cancel_allow_user_paid_keep_fees:
|
if self.event.settings.cancel_allow_user_paid_keep_fees and self.total == total:
|
||||||
fee += self.fees.filter(
|
fee += self.fees.filter(
|
||||||
fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE)
|
fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE)
|
||||||
).aggregate(
|
).aggregate(
|
||||||
@@ -419,6 +418,62 @@ class Order(LockModel, LoggedModel):
|
|||||||
)['s'] or 0
|
)['s'] or 0
|
||||||
return round_decimal(fee, self.event.currency)
|
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
|
@property
|
||||||
def user_cancel_allowed(self) -> bool:
|
def user_cancel_allowed(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -450,6 +450,10 @@ class CancelSettingsForm(SettingsForm):
|
|||||||
label=_("Do not allow cancellations after"),
|
label=_("Do not allow cancellations after"),
|
||||||
required=False
|
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(
|
cancel_allow_user_paid = forms.BooleanField(
|
||||||
label=_("Customers can cancel their paid orders"),
|
label=_("Customers can cancel their paid orders"),
|
||||||
help_text=_("Paid money will be automatically paid back if the payment method allows it. "
|
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"),
|
label=_("Do not allow cancellations after"),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
cancel_allow_user_paid_per_position = forms.BooleanField(
|
||||||
|
label=_("Customers can cancel individual products in their order"),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PaymentSettingsForm(SettingsForm):
|
class PaymentSettingsForm(SettingsForm):
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<legend>{% trans "Cancellation of unpaid or free orders" %}</legend>
|
<legend>{% trans "Cancellation of unpaid or free orders" %}</legend>
|
||||||
{% bootstrap_field form.cancel_allow_user layout="control" %}
|
{% bootstrap_field form.cancel_allow_user layout="control" %}
|
||||||
{% bootstrap_field form.cancel_allow_user_until layout="control" %}
|
{% bootstrap_field form.cancel_allow_user_until layout="control" %}
|
||||||
|
{% bootstrap_field form.cancel_allow_user_per_position layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Cancellation of paid orders" %}</legend>
|
<legend>{% trans "Cancellation of paid orders" %}</legend>
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
{% bootstrap_field form.cancel_allow_user_paid_keep_percentage layout="control" %}
|
{% 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_keep_fees layout="control" %}
|
||||||
{% bootstrap_field form.cancel_allow_user_paid_until 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 %}
|
{% if not gets_notification %}
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
|
|||||||
@@ -292,6 +292,25 @@
|
|||||||
{% trans "Cancel order" %}
|
{% trans "Cancel order" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% elif order.user_cancel_partial_positions %}
|
||||||
|
{% if order.status == "p" and order.total != 0 %}
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
You can cancel parts of this order and receive a refund to your original payment method.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
You can cancel parts of this order.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% eventurl event 'presale:event.order.cancel' secret=order.secret order=order.code %}"
|
||||||
|
class="btn btn-danger">
|
||||||
|
<span class="fa fa-remove"></span>
|
||||||
|
{% trans "Start cancellation" %}
|
||||||
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
|
|||||||
@@ -9,13 +9,28 @@
|
|||||||
Cancel order: {{ code }}
|
Cancel order: {{ code }}
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
{% if selected_positions %}
|
||||||
{% blocktrans trimmed %}
|
<p>
|
||||||
Do you really want to cancel this order? You cannot revert this action.
|
{% trans "The following positions of your order will be canceled:" %}
|
||||||
{% endblocktrans %}
|
</p>
|
||||||
{% trans "This will invalidate all of your tickets." %}
|
<ul>
|
||||||
</p>
|
{% for pos in selected_positions %}
|
||||||
{% if can_auto_refund %}
|
<li>
|
||||||
|
#{{ pos.positionid }} –
|
||||||
|
{{ pos.item }} {% if pos.variation %}– {{ pos.variation }}{% endif %}
|
||||||
|
{% if pos.attendee_name %}({{ pos.attendee_name }}){% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Do you really want to cancel this order? You cannot revert this action.
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% trans "This will invalidate all of your tickets." %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if can_auto_refund and refund_amount != 0 %}
|
||||||
<p>
|
<p>
|
||||||
<strong>
|
<strong>
|
||||||
{% blocktrans trimmed with amount=refund_amount|money:request.event.currency %}
|
{% blocktrans trimmed with amount=refund_amount|money:request.event.currency %}
|
||||||
@@ -24,7 +39,7 @@
|
|||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% elif refund_amount != 0 %}
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
{% blocktrans trimmed with amount=refund_amount|money:request.event.currency %}
|
{% blocktrans trimmed with amount=refund_amount|money:request.event.currency %}
|
||||||
With to the payment method you used, the refund amount of {{ amount }} <strong>can not be sent back to you automatically</strong>. Instead, the
|
With to the payment method you used, the refund amount of {{ amount }} <strong>can not be sent back to you automatically</strong>. Instead, the
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
{% extends "pretixpresale/event/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load money %}
|
||||||
|
{% load eventurl %}
|
||||||
|
{% block title %}{% trans "Cancel order" %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h2>
|
||||||
|
{% blocktrans trimmed with code=order.code %}
|
||||||
|
Cancel order: {{ code }}
|
||||||
|
{% endblocktrans %}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form method="post"
|
||||||
|
{% if order.status == "p" and order.total != 0 %}
|
||||||
|
action=""
|
||||||
|
{% else %}
|
||||||
|
action="{% eventurl request.event "presale:event.order.cancel.do" secret=order.secret order=order.code %}" data-asynctask
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Please choose which parts of your order you want to cancel:
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% for pos in cancellable_positions %}
|
||||||
|
<div class="checkbox">
|
||||||
|
<label for="id_pos_{{ pos.pk }}">
|
||||||
|
<input type="checkbox" name="position" value="{{ pos.pk }}"
|
||||||
|
class="scrolling-multiple-choice" checked="checked" id="id_pos_{{ pos.pk }}">
|
||||||
|
#{{ pos.positionid }} –
|
||||||
|
{{ pos.item }} {% if pos.variation %}– {{ pos.variation }}{% endif %}
|
||||||
|
{% if pos.attendee_name %}({{ pos.attendee_name }}){% endif %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if order.status == "p" and order.total != 0 %}
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Before your purchase is cancelled, you will be shown the refund amount and asked to confirm the cancellation.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row checkout-button-row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<a class="btn btn-block btn-default btn-lg"
|
||||||
|
href="{% eventurl request.event "presale:event.order" secret=order.secret order=order.code %}">
|
||||||
|
{% trans "No, take me back" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-md-offset-4">
|
||||||
|
{% if order.status == "p" and order.total != 0 %}
|
||||||
|
<button class="btn btn-block btn-primary btn-lg" type="submit">
|
||||||
|
{% trans "Calculate refund amount" %}
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-block btn-danger btn-lg" type="submit">
|
||||||
|
{% trans "Yes, cancel order" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -558,28 +558,49 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem
|
|||||||
|
|
||||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||||
class OrderCancel(EventViewMixin, OrderDetailMixin, TemplateView):
|
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):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.request = request
|
self.request = request
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
if not self.order:
|
if not self.order:
|
||||||
raise Http404(_('Unknown order code or not authorized to access this 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.'))
|
messages.error(request, _('You cannot cancel this order.'))
|
||||||
return redirect(self.get_order_url())
|
return redirect(self.get_order_url())
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['order'] = self.order
|
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)
|
proposals = self.order.propose_auto_refunds(refund_amount)
|
||||||
ctx['refund_amount'] = refund_amount
|
ctx['refund_amount'] = refund_amount
|
||||||
ctx['can_auto_refund'] = sum(proposals.values()) == 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
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user