mirror of
https://github.com/pretix/pretix.git
synced 2025-12-05 21:32:28 +00:00
Compare commits
1 Commits
fix-datasy
...
selfcancel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04645869d5 |
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<legend>{% trans "Cancellation of unpaid or free orders" %}</legend>
|
||||
{% 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" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<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_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 %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
@@ -292,6 +292,25 @@
|
||||
{% trans "Cancel order" %}
|
||||
</a>
|
||||
{% 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 %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
@@ -9,13 +9,28 @@
|
||||
Cancel order: {{ code }}
|
||||
{% endblocktrans %}
|
||||
</h2>
|
||||
<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>
|
||||
{% if can_auto_refund %}
|
||||
{% if selected_positions %}
|
||||
<p>
|
||||
{% trans "The following positions of your order will be canceled:" %}
|
||||
</p>
|
||||
<ul>
|
||||
{% for pos in selected_positions %}
|
||||
<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>
|
||||
<strong>
|
||||
{% blocktrans trimmed with amount=refund_amount|money:request.event.currency %}
|
||||
@@ -24,7 +39,7 @@
|
||||
{% endblocktrans %}
|
||||
</strong>
|
||||
</p>
|
||||
{% else %}
|
||||
{% elif refund_amount != 0 %}
|
||||
<div class="alert alert-warning">
|
||||
{% 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
|
||||
|
||||
@@ -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')
|
||||
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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user