From 55752a4319bd43a0cbb25e809c5256e197da93e3 Mon Sep 17 00:00:00 2001 From: Maico Timmerman Date: Fri, 18 Mar 2022 14:05:32 +0100 Subject: [PATCH] Event cancellation: Allow allow creating manual refunds for all orders (#2526) --- src/pretix/base/services/cancelevent.py | 8 ++-- src/pretix/base/services/orders.py | 64 ++++++++++++------------- src/pretix/control/forms/orders.py | 13 ++--- src/tests/base/test_cancelevent.py | 31 ++++++++++++ 4 files changed, 74 insertions(+), 42 deletions(-) diff --git a/src/pretix/base/services/cancelevent.py b/src/pretix/base/services/cancelevent.py index f93605d629..58d2527d3a 100644 --- a/src/pretix/base/services/cancelevent.py +++ b/src/pretix/base/services/cancelevent.py @@ -214,8 +214,8 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, refund_amount = o.payment_refund_sum try: - if auto_refund: - _try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, + if auto_refund or manual_refund: + _try_auto_refund(o.pk, auto_refund=auto_refund, manual_refund=manual_refund, allow_partial=True, source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard, giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions, comment=gettext('Event canceled')) @@ -272,8 +272,8 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, ocm.commit() refund_amount = o.payment_refund_sum - o.total - if auto_refund: - _try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, + if auto_refund or manual_refund: + _try_auto_refund(o.pk, auto_refund=auto_refund, manual_refund=manual_refund, allow_partial=True, source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard, giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions, comment=gettext('Event canceled')) diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index e63763c99a..b5b24bb75d 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -2385,7 +2385,8 @@ def perform_order(self, event: Event, payment_provider: str, positions: List[str _unset = object() -def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=OrderRefund.REFUND_SOURCE_BUYER, +def _try_auto_refund(order, auto_refund=True, manual_refund=False, allow_partial=False, + source=OrderRefund.REFUND_SOURCE_BUYER, refund_as_giftcard=False, giftcard_expires=_unset, giftcard_conditions=None, comment=None): notify_admin = False error = False @@ -2395,9 +2396,9 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord if refund_amount <= Decimal('0.00'): return + can_auto_refund_sum = 0 + if refund_as_giftcard: - proposals = {} - can_auto_refund = True can_auto_refund_sum = refund_amount with transaction.atomic(): giftcard = order.event.organizer.issued_gift_cards.create( @@ -2437,42 +2438,41 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord if r.state != OrderRefund.REFUND_STATE_DONE: notify_admin = True - else: + elif auto_refund: proposals = order.propose_auto_refunds(refund_amount) can_auto_refund_sum = sum(proposals.values()) - can_auto_refund = (allow_partial and can_auto_refund_sum) or can_auto_refund_sum == refund_amount - if can_auto_refund: - for p, value in proposals.items(): - with transaction.atomic(): - r = order.refunds.create( - payment=p, - source=source, - state=OrderRefund.REFUND_STATE_CREATED, - amount=value, - comment=comment, - provider=p.provider - ) - order.log_action('pretix.event.order.refund.created', { - 'local_id': r.local_id, - 'provider': r.provider, - }) - - try: - r.payment_provider.execute_refund(r) - except PaymentException as e: + if (allow_partial and can_auto_refund_sum) or can_auto_refund_sum == refund_amount: + for p, value in proposals.items(): with transaction.atomic(): - r.state = OrderRefund.REFUND_STATE_FAILED - r.save() - order.log_action('pretix.event.order.refund.failed', { + r = order.refunds.create( + payment=p, + source=source, + state=OrderRefund.REFUND_STATE_CREATED, + amount=value, + comment=comment, + provider=p.provider + ) + order.log_action('pretix.event.order.refund.created', { 'local_id': r.local_id, 'provider': r.provider, - 'error': str(e) }) - error = True - notify_admin = True - else: - if r.state not in (OrderRefund.REFUND_STATE_TRANSIT, OrderRefund.REFUND_STATE_DONE): + + try: + r.payment_provider.execute_refund(r) + except PaymentException as e: + with transaction.atomic(): + r.state = OrderRefund.REFUND_STATE_FAILED + r.save() + order.log_action('pretix.event.order.refund.failed', { + 'local_id': r.local_id, + 'provider': r.provider, + 'error': str(e) + }) + error = True notify_admin = True + else: + if r.state not in (OrderRefund.REFUND_STATE_TRANSIT, OrderRefund.REFUND_STATE_DONE): + notify_admin = True if refund_amount - can_auto_refund_sum > Decimal('0.00'): if manual_refund: diff --git a/src/pretix/control/forms/orders.py b/src/pretix/control/forms/orders.py index 701e3f4cb6..e8a7776004 100644 --- a/src/pretix/control/forms/orders.py +++ b/src/pretix/control/forms/orders.py @@ -749,16 +749,17 @@ class EventCancelForm(forms.Form): auto_refund = forms.BooleanField( label=_('Automatically refund money if possible'), initial=True, - required=False + required=False, + help_text=_('Only available for payment method that support automatic refunds.') ) manual_refund = forms.BooleanField( - label=_('Create manual refund if the payment method does not support automatic refunds'), - widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_auto_refund'}), + label=_('Create refund in the manual refund to-do list'), initial=True, required=False, - help_text=_('If checked, all payments with a payment method not supporting automatic refunds will be on your ' - 'manual refund to-do list. Do not check if you want to refund some of the orders by offsetting ' - 'with different orders or issuing gift cards.') + help_text=_('Manual refunds will be created which will be listed in the manual refund to-do list. ' + 'When combined with the automatic refund functionally, only payments with a payment method not ' + 'supporting automatic refunds will be on your manual refund to-do list. Do not check if you want ' + 'to refund some of the orders by offsetting with different orders or issuing gift cards.') ) refund_as_giftcard = forms.BooleanField( label=_('Refund order value to a gift card instead instead of the original payment method'), diff --git a/src/tests/base/test_cancelevent.py b/src/tests/base/test_cancelevent.py index cc1c0de69b..fdb599f781 100644 --- a/src/tests/base/test_cancelevent.py +++ b/src/tests/base/test_cancelevent.py @@ -413,6 +413,37 @@ class EventCancelTests(TestCase): assert r.source == OrderRefund.REFUND_SOURCE_ADMIN assert r.payment == p1 + @classscope(attr='o') + def test_cancel_refund_paid_only_manual(self): + gc = self.o.issued_gift_cards.create(currency="EUR") + self.order.payments.create( + amount=Decimal('20.00'), + state=OrderPayment.PAYMENT_STATE_CONFIRMED, + provider='giftcard', + info='{"gift_card": %d}' % gc.pk + ) + self.order.payments.create( + amount=Decimal('26.00'), + state=OrderPayment.PAYMENT_STATE_CONFIRMED, + provider='manual', + ) + self.order.status = Order.STATUS_PAID + self.order.save() + + cancel_event( + self.event.pk, subevent=None, manual_refund=True, + auto_refund=False, 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 + ) + + assert self.order.refunds.count() == 1 + r = self.order.refunds.get(provider='manual') + assert r.state == OrderRefund.REFUND_STATE_CREATED + assert r.amount == Decimal('46.00') + assert r.source == OrderRefund.REFUND_SOURCE_ADMIN + assert r.payment is None + class SubEventCancelTests(TestCase): def setUp(self):