Compare commits

...

1 Commits

Author SHA1 Message Date
Raphael Michel
04645869d5 first steps 2019-02-28 18:26:40 +01:00
7 changed files with 207 additions and 17 deletions

View File

@@ -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:
"""

View File

@@ -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):

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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

View File

@@ -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 %}

View File

@@ -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