Cancelling events: Allow to create manual and partial refunds

This commit is contained in:
Raphael Michel
2020-03-16 16:00:44 +01:00
parent d61e8a9204
commit b664cc712a
5 changed files with 106 additions and 14 deletions

View File

@@ -9,8 +9,8 @@ from pretix.base.decimal import round_decimal
from pretix.base.email import get_email_context
from pretix.base.i18n import language
from pretix.base.models import (
Event, InvoiceAddress, Order, OrderFee, OrderPosition, SubEvent, User,
WaitingListEntry,
Event, InvoiceAddress, Order, OrderFee, OrderPosition, OrderRefund,
SubEvent, User, WaitingListEntry,
)
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException, TolerantDict, mail
@@ -83,7 +83,7 @@ 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,
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):
@@ -167,7 +167,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
refund_amount = o.payment_refund_sum
if auto_refund:
_try_auto_refund(o.pk)
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, source=OrderRefund.REFUND_SOURCE_ADMIN)
if send:
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, o.positions.all())
@@ -211,7 +211,7 @@ def cancel_event(self, event: Event, subevent: int, auto_refund: bool, keep_fee_
refund_amount = o.payment_refund_sum - o.total
if auto_refund:
_try_auto_refund(o.pk)
_try_auto_refund(o.pk, manual_refund=manual_refund, allow_partial=True, source=OrderRefund.REFUND_SOURCE_ADMIN)
if send:
_send_mail(o, send_subject, send_message, subevent, refund_amount, user, positions)

View File

@@ -1888,7 +1888,7 @@ def perform_order(self, event: Event, payment_provider: str, positions: List[str
raise OrderError(str(error_messages['busy']))
def _try_auto_refund(order):
def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=OrderRefund.REFUND_SOURCE_BUYER):
notify_admin = False
error = False
if isinstance(order, int):
@@ -1898,13 +1898,14 @@ def _try_auto_refund(order):
return
proposals = order.propose_auto_refunds(refund_amount)
can_auto_refund = sum(proposals.values()) == 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=OrderRefund.REFUND_SOURCE_BUYER,
source=source,
state=OrderRefund.REFUND_STATE_CREATED,
amount=value,
provider=p.provider
@@ -1930,8 +1931,22 @@ def _try_auto_refund(order):
else:
if r.state != OrderRefund.REFUND_STATE_DONE:
notify_admin = True
elif refund_amount != Decimal('0.00'):
notify_admin = True
if refund_amount - can_auto_refund_sum > Decimal('0.00'):
if manual_refund:
with transaction.atomic():
r = order.refunds.create(
source=source,
state=OrderRefund.REFUND_STATE_CREATED,
amount=refund_amount - can_auto_refund_sum,
provider='manual'
)
order.log_action('pretix.event.order.refund.created', {
'local_id': r.local_id,
'provider': r.provider,
})
else:
notify_admin = True
if notify_admin:
order.log_action('pretix.event.order.refund.requested')

View File

@@ -534,6 +534,15 @@ class EventCancelForm(forms.Form):
initial=True,
required=False
)
manual_refund = forms.BooleanField(
label=_('Create manual refund if the payment method odes not support automatic refunds'),
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_auto_refund'}),
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.')
)
keep_fee_fixed = forms.DecimalField(
label=_("Keep a fixed cancellation fee"),
max_digits=10, decimal_places=2,

View File

@@ -33,6 +33,7 @@
<fieldset>
<legend>{% trans "Refund options" %}</legend>
{% bootstrap_field form.auto_refund layout="control" %}
{% bootstrap_field form.manual_refund layout="control" %}
{% bootstrap_field form.keep_fee_fixed layout="control" %}
{% bootstrap_field form.keep_fee_percentage layout="control" %}
{% bootstrap_field form.keep_fees layout="control" %}

View File

@@ -99,7 +99,7 @@ class EventCancelTests(TestCase):
r = self.order.refunds.get()
assert r.state == OrderRefund.REFUND_STATE_DONE
assert r.amount == Decimal('46.00')
assert r.source == OrderRefund.REFUND_SOURCE_BUYER
assert r.source == OrderRefund.REFUND_SOURCE_ADMIN
assert r.payment == p1
assert self.order.all_logentries().filter(action_type='pretix.event.order.refund.created').exists()
assert not self.order.all_logentries().filter(action_type='pretix.event.order.refund.requested').exists()
@@ -150,7 +150,7 @@ class EventCancelTests(TestCase):
r = self.order.refunds.get()
assert r.state == OrderRefund.REFUND_STATE_DONE
assert r.amount == Decimal('31.40')
assert r.source == OrderRefund.REFUND_SOURCE_BUYER
assert r.source == OrderRefund.REFUND_SOURCE_ADMIN
assert r.payment == p1
assert self.order.all_logentries().filter(action_type='pretix.event.order.refund.created').exists()
assert not self.order.all_logentries().filter(action_type='pretix.event.order.refund.requested').exists()
@@ -207,7 +207,7 @@ class EventCancelTests(TestCase):
r = self.order.refunds.get()
assert r.state == OrderRefund.REFUND_STATE_DONE
assert r.amount == Decimal('36.90')
assert r.source == OrderRefund.REFUND_SOURCE_BUYER
assert r.source == OrderRefund.REFUND_SOURCE_ADMIN
assert r.payment == p1
assert self.order.all_logentries().filter(action_type='pretix.event.order.refund.created').exists()
assert not self.order.all_logentries().filter(action_type='pretix.event.order.refund.requested').exists()
@@ -247,6 +247,73 @@ class EventCancelTests(TestCase):
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')
@classscope(attr='o')
def test_cancel_refund_paid_partial_to_manual(self):
gc = self.o.issued_gift_cards.create(currency="EUR")
p1 = 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=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
user=None
)
assert self.order.refunds.count() == 2
r = self.order.refunds.get(provider='giftcard')
assert r.state == OrderRefund.REFUND_STATE_DONE
assert r.amount == Decimal('20.00')
assert r.source == OrderRefund.REFUND_SOURCE_ADMIN
assert r.payment == p1
r = self.order.refunds.get(provider='manual')
assert r.state == OrderRefund.REFUND_STATE_CREATED
assert r.amount == Decimal('26.00')
assert r.source == OrderRefund.REFUND_SOURCE_ADMIN
assert r.payment is None
@classscope(attr='o')
def test_cancel_refund_paid_partial_no_manual(self):
gc = self.o.issued_gift_cards.create(currency="EUR")
p1 = 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=False,
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00",
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
user=None
)
assert self.order.refunds.count() == 1
r = self.order.refunds.get(provider='giftcard')
assert r.state == OrderRefund.REFUND_STATE_DONE
assert r.amount == Decimal('20.00')
assert r.source == OrderRefund.REFUND_SOURCE_ADMIN
assert r.payment == p1
class SubEventCancelTests(TestCase):
def setUp(self):
@@ -354,7 +421,7 @@ class SubEventCancelTests(TestCase):
r = self.order.refunds.get()
assert r.state == OrderRefund.REFUND_STATE_DONE
assert r.amount == Decimal('16.20')
assert r.source == OrderRefund.REFUND_SOURCE_BUYER
assert r.source == OrderRefund.REFUND_SOURCE_ADMIN
assert r.payment == p1
assert self.order.all_logentries().filter(action_type='pretix.event.order.refund.created').exists()
assert not self.order.all_logentries().filter(action_type='pretix.event.order.refund.requested').exists()