diff --git a/src/pretix/base/services/cancelevent.py b/src/pretix/base/services/cancelevent.py index 71c35d515..ef2dcc87c 100644 --- a/src/pretix/base/services/cancelevent.py +++ b/src/pretix/base/services/cancelevent.py @@ -83,8 +83,8 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s @app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,)) def cancel_event(self, event: Event, subevent: int, auto_refund: bool, - keep_fee_fixed: str, keep_fee_percentage: str, keep_fees: list=None, manual_refund: bool=False, - send: bool=False, send_subject: dict=None, send_message: dict=None, + keep_fee_fixed: str, keep_fee_per_ticket: str, keep_fee_percentage: str, keep_fees: list=None, + manual_refund: bool=False, send: bool=False, send_subject: dict=None, send_message: dict=None, send_waitinglist: bool=False, send_waitinglist_subject: dict={}, send_waitinglist_message: dict={}, user: int=None, refund_as_giftcard: bool=False, giftcard_expires=None, giftcard_conditions=None, subevents_from: str=None, subevents_to: str=None): @@ -182,6 +182,10 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, fee += Decimal(keep_fee_percentage) / Decimal('100.00') * (o.total - fee_sum) if keep_fee_fixed: fee += Decimal(keep_fee_fixed) + if keep_fee_per_ticket: + for p in o.positions.all(): + if p.addon_to_id is None: + fee += min(p.price, Decimal(keep_fee_per_ticket)) fee = round_decimal(min(fee, o.payment_refund_sum), event.currency) _cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects) @@ -213,6 +217,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, with transaction.atomic(): o = event.orders.select_for_update().get(pk=o) total = Decimal('0.00') + fee = Decimal('0.00') positions = [] ocm = OrderChangeManager(o, user=user, notify=False) @@ -222,7 +227,10 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, ocm.cancel(p) positions.append(p) - fee = Decimal('0.00') + if keep_fee_per_ticket: + if p.addon_to_id is None: + fee += min(p.price, Decimal(keep_fee_per_ticket)) + if keep_fee_fixed: fee += Decimal(keep_fee_fixed) if keep_fee_percentage: diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index 30e2aa4db..d00450e24 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -641,6 +641,12 @@ class EventCancelForm(forms.Form): max_digits=10, decimal_places=2, required=False ) + keep_fee_per_ticket = forms.DecimalField( + label=_("Keep a fixed cancellation fee per ticket"), + help_text=_("Free tickets and add-on products are not counted"), + max_digits=10, decimal_places=2, + required=False + ) keep_fee_percentage = forms.DecimalField( label=_("Keep a percentual cancellation fee"), max_digits=10, decimal_places=2, diff --git a/src/pretix/control/templates/pretixcontrol/orders/cancel.html b/src/pretix/control/templates/pretixcontrol/orders/cancel.html index 51df2e6c0..47c91ecb9 100644 --- a/src/pretix/control/templates/pretixcontrol/orders/cancel.html +++ b/src/pretix/control/templates/pretixcontrol/orders/cancel.html @@ -41,6 +41,7 @@ {% bootstrap_field form.gift_card_expires layout="control" %} {% bootstrap_field form.gift_card_conditions layout="control" %} {% bootstrap_field form.keep_fee_fixed layout="control" %} + {% bootstrap_field form.keep_fee_per_ticket layout="control" %} {% bootstrap_field form.keep_fee_percentage layout="control" %} {% bootstrap_field form.keep_fees layout="control" %} diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 8ee20c797..067ab8673 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -2082,6 +2082,7 @@ class EventCancel(EventPermissionRequiredMixin, AsyncAction, FormView): giftcard_expires=form.cleaned_data.get('gift_card_expires'), giftcard_conditions=form.cleaned_data.get('gift_card_conditions'), keep_fee_fixed=form.cleaned_data.get('keep_fee_fixed'), + keep_fee_per_ticket=form.cleaned_data.get('keep_fee_per_ticket'), keep_fee_percentage=form.cleaned_data.get('keep_fee_percentage'), keep_fees=form.cleaned_data.get('keep_fees'), send=form.cleaned_data.get('send'), diff --git a/src/tests/base/test_cancelevent.py b/src/tests/base/test_cancelevent.py index d89222264..76c0042c3 100644 --- a/src/tests/base/test_cancelevent.py +++ b/src/tests/base/test_cancelevent.py @@ -54,7 +54,7 @@ class EventCancelTests(TestCase): self.order.save() cancel_event( self.event.pk, subevent=None, - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", user=None ) @@ -69,7 +69,7 @@ class EventCancelTests(TestCase): self.op1.save() cancel_event( self.event.pk, subevent=None, - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -91,7 +91,7 @@ class EventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -119,7 +119,7 @@ class EventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, - auto_refund=False, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=False, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -128,6 +128,84 @@ class EventCancelTests(TestCase): assert self.order.status == Order.STATUS_CANCELED assert not self.order.refunds.exists() + @classscope(attr='o') + def test_cancel_refund_paid_with_per_ticket_fees(self): + gc = self.o.issued_gift_cards.create(currency="EUR") + self.order.payments.create( + amount=Decimal('46.00'), + state=OrderPayment.PAYMENT_STATE_CONFIRMED, + provider='giftcard', + info='{"gift_card": %d}' % gc.pk + ) + self.order.status = Order.STATUS_PAID + self.order.save() + + cancel_event( + self.event.pk, subevent=None, + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00", + send=False, send_subject="Event canceled", send_message="Event canceled :-(", + user=None + ) + + r = self.order.refunds.get() + assert r.state == OrderRefund.REFUND_STATE_DONE + assert r.amount == Decimal('42.00') + assert r.source == OrderRefund.REFUND_SOURCE_ADMIN + + @classscope(attr='o') + def test_cancel_refund_paid_with_per_ticket_fees_ignore_free(self): + self.op1.price = Decimal('46.00') + self.op1.save() + self.op2.price = Decimal('0.00') + self.op2.save() + gc = self.o.issued_gift_cards.create(currency="EUR") + self.order.payments.create( + amount=Decimal('46.00'), + state=OrderPayment.PAYMENT_STATE_CONFIRMED, + provider='giftcard', + info='{"gift_card": %d}' % gc.pk + ) + self.order.status = Order.STATUS_PAID + self.order.save() + + cancel_event( + self.event.pk, subevent=None, + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00", + send=False, send_subject="Event canceled", send_message="Event canceled :-(", + user=None + ) + + r = self.order.refunds.get() + assert r.state == OrderRefund.REFUND_STATE_DONE + assert r.amount == Decimal('44.00') + assert r.source == OrderRefund.REFUND_SOURCE_ADMIN + + @classscope(attr='o') + def test_cancel_refund_paid_with_per_ticket_fees_ignore_addon(self): + self.op2.addon_to = self.op1 + self.op2.save() + gc = self.o.issued_gift_cards.create(currency="EUR") + self.order.payments.create( + amount=Decimal('46.00'), + state=OrderPayment.PAYMENT_STATE_CONFIRMED, + provider='giftcard', + info='{"gift_card": %d}' % gc.pk + ) + self.order.status = Order.STATUS_PAID + self.order.save() + + cancel_event( + self.event.pk, subevent=None, + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00", + send=False, send_subject="Event canceled", send_message="Event canceled :-(", + user=None + ) + + r = self.order.refunds.get() + assert r.state == OrderRefund.REFUND_STATE_DONE + assert r.amount == Decimal('44.00') + assert r.source == OrderRefund.REFUND_SOURCE_ADMIN + @classscope(attr='o') def test_cancel_refund_paid_with_fees(self): gc = self.o.issued_gift_cards.create(currency="EUR") @@ -142,7 +220,7 @@ class EventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, - auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00", + auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -170,7 +248,7 @@ class EventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, - auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00", + auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -201,7 +279,7 @@ class EventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT], + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT], keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) r = self.order.refunds.get() @@ -237,7 +315,7 @@ class EventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT], + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT], keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -266,7 +344,7 @@ class EventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, manual_refund=True, - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -302,7 +380,7 @@ class EventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, manual_refund=False, - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -351,7 +429,7 @@ class SubEventCancelTests(TestCase): self.op2.save() cancel_event( self.event.pk, subevent=self.se1.pk, - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -366,7 +444,7 @@ class SubEventCancelTests(TestCase): self.op2.save() cancel_event( self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from - timedelta(days=2), - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -374,7 +452,7 @@ class SubEventCancelTests(TestCase): assert self.order.status == Order.STATUS_PENDING cancel_event( self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from + timedelta(days=2), - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -387,7 +465,7 @@ class SubEventCancelTests(TestCase): self.op2.save() cancel_event( self.event.pk, subevent=self.se1.pk, - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -398,7 +476,7 @@ class SubEventCancelTests(TestCase): def test_cancel_all_subevents(self): cancel_event( self.event.pk, subevent=None, - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -418,7 +496,7 @@ class SubEventCancelTests(TestCase): self.order.save() cancel_event( self.event.pk, subevent=self.se1.pk, - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", user=None ) @@ -430,7 +508,7 @@ class SubEventCancelTests(TestCase): def test_cancel_mixed_order_range(self): cancel_event( self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from - timedelta(days=2), - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", user=None ) @@ -439,7 +517,7 @@ class SubEventCancelTests(TestCase): assert self.order.positions.count() == 2 cancel_event( self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from + timedelta(days=2), - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", user=None ) @@ -467,7 +545,7 @@ class SubEventCancelTests(TestCase): cancel_event( self.event.pk, subevent=self.se1.pk, - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -484,6 +562,31 @@ class SubEventCancelTests(TestCase): f = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_CANCELLATION) assert f.value == Decimal('1.80') + @classscope(attr='o') + def test_cancel_partially_keep_fees_per_ticket(self): + gc = self.o.issued_gift_cards.create(currency="EUR") + self.order.payments.create( + amount=Decimal('46.00'), + state=OrderPayment.PAYMENT_STATE_CONFIRMED, + provider='giftcard', + info='{"gift_card": %d}' % gc.pk + ) + self.order.status = Order.STATUS_PAID + self.order.save() + + cancel_event( + self.event.pk, subevent=self.se1.pk, + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00", + send=False, send_subject="Event canceled", send_message="Event canceled :-(", + user=None + ) + r = self.order.refunds.get() + assert r.state == OrderRefund.REFUND_STATE_DONE + assert r.amount == Decimal('21.00') + assert r.source == OrderRefund.REFUND_SOURCE_ADMIN + f = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_CANCELLATION) + assert f.value == Decimal('2.00') + @classscope(attr='o') def test_cancel_send_mail_waitinglist(self): v = Voucher.objects.create(event=self.event, block_quota=True, redeemed=1) @@ -495,7 +598,7 @@ class SubEventCancelTests(TestCase): ) cancel_event( self.event.pk, subevent=None, - auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", send_waitinglist=True, send_waitinglist_message="Event canceled", send_waitinglist_subject=":(", user=None