Event cancellation: Allow allow creating manual refunds for all orders (#2526)

This commit is contained in:
Maico Timmerman
2022-03-18 14:05:32 +01:00
committed by GitHub
parent 5bde98e349
commit 55752a4319
4 changed files with 74 additions and 42 deletions

View File

@@ -214,8 +214,8 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
refund_amount = o.payment_refund_sum refund_amount = o.payment_refund_sum
try: try:
if auto_refund: if auto_refund or manual_refund:
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, _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, source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard,
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions, giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions,
comment=gettext('Event canceled')) comment=gettext('Event canceled'))
@@ -272,8 +272,8 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool,
ocm.commit() ocm.commit()
refund_amount = o.payment_refund_sum - o.total refund_amount = o.payment_refund_sum - o.total
if auto_refund: if auto_refund or manual_refund:
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, _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, source=OrderRefund.REFUND_SOURCE_ADMIN, refund_as_giftcard=refund_as_giftcard,
giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions, giftcard_expires=giftcard_expires, giftcard_conditions=giftcard_conditions,
comment=gettext('Event canceled')) comment=gettext('Event canceled'))

View File

@@ -2385,7 +2385,8 @@ def perform_order(self, event: Event, payment_provider: str, positions: List[str
_unset = object() _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): refund_as_giftcard=False, giftcard_expires=_unset, giftcard_conditions=None, comment=None):
notify_admin = False notify_admin = False
error = 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'): if refund_amount <= Decimal('0.00'):
return return
can_auto_refund_sum = 0
if refund_as_giftcard: if refund_as_giftcard:
proposals = {}
can_auto_refund = True
can_auto_refund_sum = refund_amount can_auto_refund_sum = refund_amount
with transaction.atomic(): with transaction.atomic():
giftcard = order.event.organizer.issued_gift_cards.create( 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: if r.state != OrderRefund.REFUND_STATE_DONE:
notify_admin = True notify_admin = True
else: elif auto_refund:
proposals = order.propose_auto_refunds(refund_amount) proposals = order.propose_auto_refunds(refund_amount)
can_auto_refund_sum = sum(proposals.values()) 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 (allow_partial and can_auto_refund_sum) or can_auto_refund_sum == refund_amount:
if can_auto_refund: for p, value in proposals.items():
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:
with transaction.atomic(): with transaction.atomic():
r.state = OrderRefund.REFUND_STATE_FAILED r = order.refunds.create(
r.save() payment=p,
order.log_action('pretix.event.order.refund.failed', { 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, 'local_id': r.local_id,
'provider': r.provider, 'provider': r.provider,
'error': str(e)
}) })
error = True
notify_admin = True try:
else: r.payment_provider.execute_refund(r)
if r.state not in (OrderRefund.REFUND_STATE_TRANSIT, OrderRefund.REFUND_STATE_DONE): 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 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 refund_amount - can_auto_refund_sum > Decimal('0.00'):
if manual_refund: if manual_refund:

View File

@@ -749,16 +749,17 @@ class EventCancelForm(forms.Form):
auto_refund = forms.BooleanField( auto_refund = forms.BooleanField(
label=_('Automatically refund money if possible'), label=_('Automatically refund money if possible'),
initial=True, initial=True,
required=False required=False,
help_text=_('Only available for payment method that support automatic refunds.')
) )
manual_refund = forms.BooleanField( manual_refund = forms.BooleanField(
label=_('Create manual refund if the payment method does not support automatic refunds'), label=_('Create refund in the manual refund to-do list'),
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_auto_refund'}),
initial=True, initial=True,
required=False, required=False,
help_text=_('If checked, all payments with a payment method not supporting automatic refunds will be on your ' help_text=_('Manual refunds will be created which will be listed in the manual refund to-do list. '
'manual refund to-do list. Do not check if you want to refund some of the orders by offsetting ' 'When combined with the automatic refund functionally, only payments with a payment method not '
'with different orders or issuing gift cards.') '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( refund_as_giftcard = forms.BooleanField(
label=_('Refund order value to a gift card instead instead of the original payment method'), label=_('Refund order value to a gift card instead instead of the original payment method'),

View File

@@ -413,6 +413,37 @@ class EventCancelTests(TestCase):
assert r.source == OrderRefund.REFUND_SOURCE_ADMIN assert r.source == OrderRefund.REFUND_SOURCE_ADMIN
assert r.payment == p1 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): class SubEventCancelTests(TestCase):
def setUp(self): def setUp(self):