diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py
index 4cbcd57570..6280d9a1c0 100644
--- a/src/pretix/api/serializers/event.py
+++ b/src/pretix/api/serializers/event.py
@@ -760,6 +760,9 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_logo_image',
'cancel_allow_user',
'cancel_allow_user_until',
+ 'cancel_allow_user_unpaid_keep',
+ 'cancel_allow_user_unpaid_keep_fees',
+ 'cancel_allow_user_unpaid_keep_percentage',
'cancel_allow_user_paid',
'cancel_allow_user_paid_until',
'cancel_allow_user_paid_keep',
diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py
index 84370c5497..40a87c19de 100644
--- a/src/pretix/base/models/orders.py
+++ b/src/pretix/base/models/orders.py
@@ -564,17 +564,30 @@ class Order(LockModel, LoggedModel):
@cached_property
def user_cancel_fee(self):
fee = Decimal('0.00')
- if self.event.settings.cancel_allow_user_paid_keep_fees:
- fee += self.fees.filter(
- fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE,
- OrderFee.FEE_TYPE_CANCELLATION)
- ).aggregate(
- s=Sum('value')
- )['s'] or 0
- 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)
- if self.event.settings.cancel_allow_user_paid_keep:
- fee += self.event.settings.cancel_allow_user_paid_keep
+ if self.status == Order.STATUS_PAID:
+ if self.event.settings.cancel_allow_user_paid_keep_fees:
+ fee += self.fees.filter(
+ fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE,
+ OrderFee.FEE_TYPE_CANCELLATION)
+ ).aggregate(
+ s=Sum('value')
+ )['s'] or 0
+ 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)
+ if self.event.settings.cancel_allow_user_paid_keep:
+ fee += self.event.settings.cancel_allow_user_paid_keep
+ else:
+ if self.event.settings.cancel_allow_user_unpaid_keep_fees:
+ fee += self.fees.filter(
+ fee_type__in=(OrderFee.FEE_TYPE_PAYMENT, OrderFee.FEE_TYPE_SHIPPING, OrderFee.FEE_TYPE_SERVICE,
+ OrderFee.FEE_TYPE_CANCELLATION)
+ ).aggregate(
+ s=Sum('value')
+ )['s'] or 0
+ if self.event.settings.cancel_allow_user_unpaid_keep_percentage:
+ fee += self.event.settings.cancel_allow_user_unpaid_keep_percentage / Decimal('100.0') * (self.total - fee)
+ if self.event.settings.cancel_allow_user_unpaid_keep:
+ fee += self.event.settings.cancel_allow_user_unpaid_keep
return round_decimal(min(fee, self.total), self.event.currency)
@property
@@ -642,10 +655,12 @@ class Order(LockModel, LoggedModel):
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
return False
- if self.status == Order.STATUS_PAID or self.payment_refund_sum > Decimal('0.00'):
+ if self.status == Order.STATUS_PAID:
if self.total == Decimal('0.00'):
return self.event.settings.cancel_allow_user
return self.event.settings.cancel_allow_user_paid
+ elif self.payment_refund_sum > Decimal('0.00'):
+ return False
elif self.status == Order.STATUS_PENDING:
return self.event.settings.cancel_allow_user
return False
diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py
index c7167b190e..2fec21d91c 100644
--- a/src/pretix/base/services/orders.py
+++ b/src/pretix/base/services/orders.py
@@ -462,9 +462,13 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
f._calculate_tax()
f.save()
- if order.payment_refund_sum < cancellation_fee:
- raise OrderError(_('The cancellation fee cannot be higher than the payment credit of this order.'))
- order.status = Order.STATUS_PAID
+ if cancellation_fee > order.total:
+ raise OrderError(_('The cancellation fee cannot be higher than the total amount of this order.'))
+ elif order.payment_refund_sum < cancellation_fee:
+ order.status = Order.STATUS_PENDING
+ order.set_expires()
+ else:
+ order.status = Order.STATUS_PAID
order.total = cancellation_fee
order.cancellation_date = now()
order.save(update_fields=['status', 'cancellation_date', 'total'])
@@ -1097,8 +1101,16 @@ def expire_orders(sender, **kwargs):
event_id = None
expire = None
- for o in Order.objects.filter(expires__lt=now(), status=Order.STATUS_PENDING,
- require_approval=False).select_related('event').order_by('event_id'):
+ qs = Order.objects.filter(
+ expires__lt=now(),
+ status=Order.STATUS_PENDING,
+ require_approval=False
+ ).exclude(
+ Exists(
+ OrderFee.objects.filter(order_id=OuterRef('pk'), fee_type=OrderFee.FEE_TYPE_CANCELLATION)
+ )
+ ).select_related('event').order_by('event_id')
+ for o in qs:
if o.event_id != event_id:
expire = o.event.settings.get('payment_term_expire_automatically', as_type=bool)
event_id = o.event_id
diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py
index 366db1f394..a68a67e4ed 100644
--- a/src/pretix/base/settings.py
+++ b/src/pretix/base/settings.py
@@ -1421,6 +1421,45 @@ DEFAULTS = {
label=_("Customers can cancel their unpaid orders"),
)
},
+ 'cancel_allow_user_unpaid_keep': {
+ 'default': '0.00',
+ 'type': Decimal,
+ 'form_class': forms.DecimalField,
+ 'serializer_class': serializers.DecimalField,
+ 'serializer_kwargs': dict(
+ max_digits=10, decimal_places=2
+ ),
+ 'form_kwargs': dict(
+ label=_("Charge a fixed cancellation fee"),
+ help_text=_("Only affects orders pending payments, a cancellation fee for free orders is never charged. "
+ "Note that it will be your responsibility to claim the cancellation fee from the user."),
+ )
+ },
+ 'cancel_allow_user_unpaid_keep_fees': {
+ 'default': 'False',
+ 'type': bool,
+ 'form_class': forms.BooleanField,
+ 'serializer_class': serializers.BooleanField,
+ 'form_kwargs': dict(
+ label=_("Charge payment, shipping and service fees"),
+ help_text=_("Only affects orders pending payments, a cancellation fee for free orders is never charged. "
+ "Note that it will be your responsibility to claim the cancellation fee from the user."),
+ )
+ },
+ 'cancel_allow_user_unpaid_keep_percentage': {
+ 'default': '0.00',
+ 'type': Decimal,
+ 'form_class': forms.DecimalField,
+ 'serializer_class': serializers.DecimalField,
+ 'serializer_kwargs': dict(
+ max_digits=10, decimal_places=2
+ ),
+ 'form_kwargs': dict(
+ label=_("Charge a percentual cancellation fee"),
+ help_text=_("Only affects orders pending payments, a cancellation fee for free orders is never charged. "
+ "Note that it will be your responsibility to claim the cancellation fee from the user."),
+ )
+ },
'cancel_allow_user_until': {
'default': None,
'type': RelativeDateWrapper,
diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py
index 9d02087252..4519078fc0 100644
--- a/src/pretix/control/forms/event.py
+++ b/src/pretix/control/forms/event.py
@@ -666,6 +666,9 @@ class CancelSettingsForm(SettingsForm):
'cancel_allow_user_until',
'cancel_allow_user_paid',
'cancel_allow_user_paid_until',
+ 'cancel_allow_user_unpaid_keep',
+ 'cancel_allow_user_unpaid_keep_fees',
+ 'cancel_allow_user_unpaid_keep_percentage',
'cancel_allow_user_paid_keep',
'cancel_allow_user_paid_keep_fees',
'cancel_allow_user_paid_keep_percentage',
diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py
index c864ab055c..139f3c536b 100644
--- a/src/pretix/control/forms/orders.py
+++ b/src/pretix/control/forms/orders.py
@@ -158,7 +158,7 @@ class CancelForm(ForceQuotaConfirmationForm):
localize=True,
label=_('Keep a cancellation fee of'),
help_text=_('If you keep a fee, all positions within this order will be canceled and the order will be reduced '
- 'to a paid cancellation fee. Payment and shipping fees will be canceled as well, so include them '
+ 'to a cancellation fee. Payment and shipping fees will be canceled as well, so include them '
'in your cancellation fee if you want to keep them. Please always enter a gross value, '
'tax will be calculated automatically.'),
)
@@ -176,23 +176,19 @@ class CancelForm(ForceQuotaConfirmationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- prs = self.instance.payment_refund_sum
- if prs > 0:
- change_decimal_field(self.fields['cancellation_fee'], self.instance.event.currency)
- self.fields['cancellation_fee'].widget.attrs['placeholder'] = floatformat(
- Decimal('0.00'),
- settings.CURRENCY_PLACES.get(self.instance.event.currency, 2)
- )
- self.fields['cancellation_fee'].max_value = prs
- else:
- del self.fields['cancellation_fee']
+ change_decimal_field(self.fields['cancellation_fee'], self.instance.event.currency)
+ self.fields['cancellation_fee'].widget.attrs['placeholder'] = floatformat(
+ Decimal('0.00'),
+ settings.CURRENCY_PLACES.get(self.instance.event.currency, 2)
+ )
+ self.fields['cancellation_fee'].max_value = self.instance.total
if not self.instance.invoices.exists():
del self.fields['cancel_invoice']
def clean_cancellation_fee(self):
val = self.cleaned_data['cancellation_fee'] or Decimal('0.00')
- if val > self.instance.payment_refund_sum:
- raise ValidationError(_('The cancellation fee cannot be higher than the payment credit of this order.'))
+ if val > self.instance.total:
+ raise ValidationError(_('The cancellation fee cannot be higher than the total amount of this order.'))
return val
diff --git a/src/pretix/control/templates/pretixcontrol/event/cancel.html b/src/pretix/control/templates/pretixcontrol/event/cancel.html
index d02b1c0c1c..56815c3ac9 100644
--- a/src/pretix/control/templates/pretixcontrol/event/cancel.html
+++ b/src/pretix/control/templates/pretixcontrol/event/cancel.html
@@ -11,6 +11,9 @@
{% bootstrap_field form.cancel_allow_user layout="control" %}
{% bootstrap_field form.cancel_allow_user_until layout="control" %}
+ {% bootstrap_field form.cancel_allow_user_unpaid_keep layout="control" %}
+ {% bootstrap_field form.cancel_allow_user_unpaid_keep_percentage layout="control" %}
+ {% bootstrap_field form.cancel_allow_user_unpaid_keep_fees layout="control" %}