diff --git a/src/pretix/base/services/cancelevent.py b/src/pretix/base/services/cancelevent.py index b493a6f2df..19a5377ee7 100644 --- a/src/pretix/base/services/cancelevent.py +++ b/src/pretix/base/services/cancelevent.py @@ -2,9 +2,7 @@ import logging from decimal import Decimal from django.db import transaction -from django.db.models import ( - Count, Exists, IntegerField, OuterRef, Subquery, Sum, -) +from django.db.models import Count, Exists, IntegerField, OuterRef, Subquery from i18nfield.strings import LazyI18nString from pretix.base.decimal import round_decimal @@ -85,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: bool, - send: bool, send_subject: dict, send_message: dict, + keep_fee_percentage: str, keep_fees: list=None, + 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): send_subject = LazyI18nString(send_subject) @@ -151,20 +149,21 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_ for o in orders_to_cancel.only('id', 'total'): try: fee = Decimal('0.00') + fee_sum = Decimal('0.00') + keep_fee_objects = [] if keep_fees: - fee += o.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 + for f in o.fees.all(): + if f.fee_type in keep_fees: + fee += f.value + keep_fee_objects.append(f) + fee_sum += f.value if keep_fee_percentage: - fee += Decimal(keep_fee_percentage) / Decimal('100.00') * (o.total - fee) + fee += Decimal(keep_fee_percentage) / Decimal('100.00') * (o.total - fee_sum) if keep_fee_fixed: fee += Decimal(keep_fee_fixed) fee = round_decimal(min(fee, o.payment_refund_sum), event.currency) - _cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee) + _cancel_order(o.pk, user, send_mail=False, cancellation_fee=fee, keep_fees=keep_fee_objects) refund_amount = o.payment_refund_sum if auto_refund: diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index c95fec8edb..bd55f90c52 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -319,7 +319,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None): def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None, - cancellation_fee=None): + cancellation_fee=None, keep_fees=None): """ Mark this order as canceled :param order: The order to change @@ -367,23 +367,28 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1)) position.canceled = True position.save(update_fields=['canceled']) + new_fee = cancellation_fee for fee in order.fees.all(): - fee.canceled = True - fee.save(update_fields=['canceled']) + if keep_fees and fee in keep_fees: + new_fee -= fee.value + else: + fee.canceled = True + fee.save(update_fields=['canceled']) - f = OrderFee( - fee_type=OrderFee.FEE_TYPE_CANCELLATION, - value=cancellation_fee, - tax_rule=order.event.settings.tax_rate_default, - order=order, - ) - f._calculate_tax() - f.save() + if new_fee: + f = OrderFee( + fee_type=OrderFee.FEE_TYPE_CANCELLATION, + value=new_fee, + tax_rule=order.event.settings.tax_rate_default, + order=order, + ) + 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 - order.total = f.value + order.total = cancellation_fee order.save(update_fields=['status', 'total']) if i: diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index 96866f2cb9..dec99146e8 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -16,7 +16,9 @@ from i18nfield.strings import LazyI18nString from pretix.base.email import get_available_placeholders from pretix.base.forms import I18nModelForm, PlaceholderValidator from pretix.base.forms.widgets import DatePickerWidget -from pretix.base.models import InvoiceAddress, ItemAddOn, Order, OrderPosition +from pretix.base.models import ( + InvoiceAddress, ItemAddOn, Order, OrderFee, OrderPosition, +) from pretix.base.models.event import SubEvent from pretix.base.services.pricing import get_price from pretix.control.forms.widgets import Select2 @@ -542,8 +544,13 @@ class EventCancelForm(forms.Form): max_digits=10, decimal_places=2, required=False ) - keep_fees = forms.BooleanField( - label=_("Keep payment, shipping and service fees"), + keep_fees = forms.MultipleChoiceField( + label=_("Keep fees"), + widget=forms.CheckboxSelectMultiple, + choices=[(k, v) for k, v in OrderFee.FEE_TYPES if k != OrderFee.FEE_TYPE_GIFTCARD], + help_text=_('The selected types of fees will not be refunded but instead added to the cancellation fee. Fees ' + 'are never refunded in when an order in an event series is only partially canceled since it ' + 'consists of tickets for multiple dates.'), required=False, ) send = forms.BooleanField( @@ -600,6 +607,7 @@ class EventCancelForm(forms.Form): 'Your {event} team' )) ) + self._set_field_placeholders('send_subject', ['event_or_subevent', 'refund_amount', 'position_or_address', 'order', 'event']) self._set_field_placeholders('send_message', ['event_or_subevent', 'refund_amount', 'position_or_address', diff --git a/src/tests/base/test_cancelevent.py b/src/tests/base/test_cancelevent.py index 3ac91ac695..1688b49679 100644 --- a/src/tests/base/test_cancelevent.py +++ b/src/tests/base/test_cancelevent.py @@ -55,7 +55,7 @@ class EventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", - keep_fees=True, send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", + send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", user=None ) assert len(djmail.outbox) == 1 @@ -70,7 +70,7 @@ class EventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", - keep_fees=True, send=True, send_subject="Event canceled", send_message="Event canceled :-(", + send=True, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) assert len(djmail.outbox) == 2 @@ -92,7 +92,7 @@ class EventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", - keep_fees=True, send=True, send_subject="Event canceled", send_message="Event canceled :-(", + send=True, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -120,7 +120,7 @@ class EventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, auto_refund=False, keep_fee_fixed="0.00", keep_fee_percentage="0.00", - keep_fees=True, send=True, send_subject="Event canceled", send_message="Event canceled :-(", + send=True, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -143,7 +143,7 @@ class EventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00", - keep_fees=True, send=False, send_subject="Event canceled", send_message="Event canceled :-(", + send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -171,7 +171,7 @@ class EventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00", - keep_fees=True, send=False, send_subject="Event canceled", send_message="Event canceled :-(", + send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) @@ -201,9 +201,8 @@ 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=True, send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT], + send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) r = self.order.refunds.get() assert r.state == OrderRefund.REFUND_STATE_DONE @@ -214,6 +213,40 @@ class EventCancelTests(TestCase): assert not self.order.all_logentries().filter(action_type='pretix.event.order.refund.requested').exists() assert gc.value == Decimal('36.90') + @classscope(attr='o') + def test_cancel_keep_some_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.op1.price -= Decimal('5.00') + self.op1.save() + self.order.fees.create( + fee_type=OrderFee.FEE_TYPE_PAYMENT, + value=Decimal('2.50'), + ) + self.order.fees.create( + fee_type=OrderFee.FEE_TYPE_SHIPPING, + value=Decimal('2.50'), + ) + 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="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT], + send=False, send_subject="Event canceled", send_message="Event canceled :-(", + user=None + ) + r = self.order.refunds.get() + assert r.amount == Decimal('39.40') + assert self.order.all_fees.get(fee_type=OrderFee.FEE_TYPE_SHIPPING).canceled + assert not self.order.all_fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT).canceled + assert self.order.all_fees.get(fee_type=OrderFee.FEE_TYPE_CANCELLATION).value == Decimal('4.10') + class SubEventCancelTests(TestCase): def setUp(self): @@ -252,7 +285,7 @@ class SubEventCancelTests(TestCase): cancel_event( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", - keep_fees=True, send=True, send_subject="Event canceled", send_message="Event canceled :-(", + send=True, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) assert len(djmail.outbox) == 2 @@ -267,7 +300,7 @@ class SubEventCancelTests(TestCase): cancel_event( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", - keep_fees=True, send=True, send_subject="Event canceled", send_message="Event canceled :-(", + send=True, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) self.order.refresh_from_db() @@ -287,7 +320,7 @@ class SubEventCancelTests(TestCase): cancel_event( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", - keep_fees=True, send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", + send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", user=None ) self.order.refresh_from_db() @@ -315,7 +348,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", - keep_fees=True, send=False, send_subject="Event canceled", send_message="Event canceled :-(", + send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None ) r = self.order.refunds.get() @@ -343,7 +376,7 @@ class SubEventCancelTests(TestCase): cancel_event( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", - keep_fees=True, send=False, send_subject="Event canceled", send_message="Event canceled :-(", + send=False, send_subject="Event canceled", send_message="Event canceled :-(", send_waitinglist=True, send_waitinglist_message="Event canceled", send_waitinglist_subject=":(", user=None )