Refund process (closes #26)

This commit is contained in:
Raphael Michel
2015-04-06 16:23:51 +02:00
parent f8322da3ad
commit 6d73267912
9 changed files with 170 additions and 15 deletions

View File

@@ -1555,6 +1555,16 @@ class Order(Versionable):
return True
return False # nothing there to modify
def mark_refunded(self):
"""
Mark this order as refunded. This clones the order object, sets the payment status and
returns the cloned order object.
"""
order = self.clone()
order.status = Order.STATUS_REFUNDED
order.save()
return order
def mark_paid(self, provider=None, info=None, date=None, manual=None):
"""
Mark this order as paid. This clones the order object, sets the payment provider,

View File

@@ -1,6 +1,7 @@
from collections import OrderedDict
from decimal import Decimal
from django import forms
from django.contrib import messages
from django.forms import Form
from django.http import HttpRequest
@@ -299,3 +300,38 @@ class BasePaymentProvider:
:param order: The order object
"""
return _('Payment provider: %s' % self.verbose_name)
def order_control_refund_render(self, order: Order) -> str:
"""
Will be called if the event administrator clicks an order's 'refund' button.
This can be used to display information *before* the order is being refunded.
It should return HTML code which should be displayed to the user. It should
contain information about to which extend the money will be refunded
automatically.
:param order: The order object
"""
return '<div class="alert alert-warning">%s</div>' % _('The money can not be automatically refunded, '
'please transfer the money back manually.')
def order_control_refund_perform(self, request: HttpRequest, order: Order) -> "bool|str":
"""
Will be called if the event administrator confirms the refund.
This should transfer the money back (if possible). You can return an URL the
user should be redirected to if you need special behaviour or None to continue
with default behaviour.
On failure, you should use Django's message framework to display an error message
to the user.
The default implementation sets the Orders state to refunded and shows a success
message.
:param request: The HTTP request
:param order: The order object
"""
order.mark_refunded()
messages.success(request, _('The order has been marked as refunded. Please transfer the money '
'back to the buyer manually.'))

View File

@@ -0,0 +1,34 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% block title %}
{% trans "Refund order" %}
{% endblock %}
{% block content %}
<h1>
{% trans "Refund order" %}
</h1>
<p>{% blocktrans trimmed %}
Do you really want to refund this order? You cannot revert this action.
{% endblocktrans %}</p>
{{ payment|safe }}
<form method="post" href="">
{% csrf_token %}
<input type="hidden" name="status" value="r" />
<div class="row checkout-button-row">
<div class="col-md-4">
<a class="btn btn-block btn-default btn-lg"
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
{% trans "No, take me back" %}
</a>
</div>
<div class="col-md-4 col-md-offset-4">
<button class="btn btn-block btn-danger btn-lg" type="submit">
{% trans "Yes, refund order" %}
</button>
</div>
<div class="clearfix"></div>
</div>
</form>
{% endblock %}

View File

@@ -40,19 +40,19 @@ class OrderView(DetailView):
def order(self):
return self.get_object()
class OrderDetail(EventPermissionRequiredMixin, OrderView):
template_name = 'pretixcontrol/order/index.html'
permission = 'can_view_orders'
@cached_property
def payment_provider(self):
responses = register_payment_providers.send(self.request.event)
for receiver, response in responses:
provider = response(self.request.event)
if provider.identifier == self.object.payment_provider:
if provider.identifier == self.order.payment_provider:
return provider
class OrderDetail(EventPermissionRequiredMixin, OrderView):
template_name = 'pretixcontrol/order/index.html'
permission = 'can_view_orders'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['items'] = self.get_items()
@@ -118,6 +118,10 @@ class OrderTransition(EventPermissionRequiredMixin, OrderView):
order.payment_manual = True
order.save()
messages.success(self.request, _('The order has been marked as not paid.'))
elif self.order.status == 'p' and to == 'r':
ret = self.payment_provider.order_control_refund_perform(self.request, self.order)
if ret:
return redirect(ret)
return redirect(reverse(
'control:event.order',
kwargs={
@@ -134,14 +138,9 @@ class OrderTransition(EventPermissionRequiredMixin, OrderView):
'order': self.order,
})
elif self.order.status == 'p' and to == 'r':
messages.error(self.request, _('Refunding orders is not yet implemented.'))
return redirect(reverse(
'control:event.order',
kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'code': self.order.code,
}
))
return render(self.request, 'pretixcontrol/order/refund.html', {
'order': self.order,
'payment': self.payment_provider.order_control_refund_render(self.order),
})
else:
return HttpResponse(status=405)

View File

@@ -200,3 +200,37 @@ class Paypal(BasePaymentProvider):
ctx = {'request': request, 'event': self.event, 'settings': self.settings,
'payment_info': payment_info, 'order': order}
return template.render(ctx)
def order_control_refund_render(self, order) -> str:
return '<div class="alert alert-info">%s</div>' % _('The money will be automatically refunded.')
def order_control_refund_perform(self, request, order) -> "bool|str":
self.init_api()
if order.payment_info:
payment_info = json.loads(order.payment_info)
else:
payment_info = None
if not payment_info:
order.mark_refunded()
messages.warning(request, _('We were unable to transfer the money back automatically. '
'Please get in touch with the customer and transfer it back manually.'))
return
for res in payment_info['transactions'][0]['related_resources']:
for k, v in res.items():
if k == 'sale':
sale = paypalrestsdk.Sale.find(v['id'])
break
refund = sale.refund({})
if not refund.success():
order.mark_refunded()
messages.warning(request, _('We were unable to transfer the money back automatically. '
'Please get in touch with the customer and transfer it back manually.'))
else:
sale = paypalrestsdk.Payment.find(payment_info['id'])
order = order.mark_refunded()
order.payment_info = json.dumps(sale.to_dict())
order.save()

View File

@@ -5,6 +5,10 @@
<p>{% blocktrans trimmed %}
This order has been paid via PayPal.
{% endblocktrans %}</p>
{% elif order.status == "r" %}
<p>{% blocktrans trimmed %}
This order has been planned to be paid via PayPal and has been marked as refunded.
{% endblocktrans %}</p>
{% else %}
<p>{% blocktrans trimmed %}
This order has been planned to be paid via PayPal, but the payment has not yet been completed.

View File

@@ -91,3 +91,33 @@ class Stripe(BasePaymentProvider):
ctx = {'request': request, 'event': self.event, 'settings': self.settings,
'payment_info': payment_info, 'order': order}
return template.render(ctx)
def order_control_refund_render(self, order) -> str:
return '<div class="alert alert-info">%s</div>' % _('The money will be automatically refunded.')
def order_control_refund_perform(self, request, order) -> "bool|str":
self._init_api()
if order.payment_info:
payment_info = json.loads(order.payment_info)
else:
payment_info = None
if not payment_info:
order.mark_refunded()
messages.warning(request, _('We were unable to transfer the money back automatically. '
'Please get in touch with the customer and transfer it back manually.'))
return
try:
ch = stripe.Charge.retrieve(payment_info['id'])
ch.refunds.create()
ch.refresh()
except stripe.error.StripeError:
order.mark_refunded()
messages.warning(request, _('We were unable to transfer the money back automatically. '
'Please get in touch with the customer and transfer it back manually.'))
else:
order = order.mark_refunded()
order.payment_info = str(ch)
order.save()

View File

@@ -5,6 +5,10 @@
<p>{% blocktrans trimmed %}
This order has been paid via Stripe.
{% endblocktrans %}</p>
{% elif order.status == "p" %}
<p>{% blocktrans trimmed %}
This order has been planned to be paid via Stripe and has been marked as refunded.
{% endblocktrans %}</p>
{% else %}
<p>{% blocktrans trimmed %}
This order has been planned to be paid via Stripe, but the payment has not yet been completed.