mirror of
https://github.com/pretix/pretix.git
synced 2026-05-03 14:54:04 +00:00
Allow to create refunds without a payment (#1914)
Co-authored-by: Martin Gross <gross@rami.io>
This commit is contained in:
@@ -9,7 +9,7 @@ import pytz
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.db import transaction
|
||||
from django.dispatch import receiver
|
||||
from django.forms import Form
|
||||
@@ -773,6 +773,32 @@ class BasePaymentProvider:
|
||||
"""
|
||||
raise PaymentException(_('Automatic refunds are not supported by this payment provider.'))
|
||||
|
||||
def new_refund_control_form_render(self, request: HttpRequest, order: Order) -> str:
|
||||
"""
|
||||
Render a form that will be shown to backend users when trying to create a new refund.
|
||||
|
||||
Usually, refunds are created from an existing payment object, e.g. if there is a credit card
|
||||
payment and the credit card provider returns ``True`` from ``payment_refund_supported``, the system
|
||||
will automatically create an ``OrderRefund`` and call ``execute_refund`` on that payment. This method
|
||||
can and should not be used in that situation! Instead, by implementing this method you can add a refund
|
||||
flow for this payment provider that starts without an existing payment. For example, even though an order
|
||||
was paid by credit card, it could easily be refunded by SEPA bank transfer. In that case, the SEPA bank
|
||||
transfer provider would implement this method and return a form that asks for the IBAN.
|
||||
|
||||
This method should return HTML or ``None``. All form fields should have a globally unique name.
|
||||
"""
|
||||
return
|
||||
|
||||
def new_refund_control_form_process(self, request: HttpRequest, amount: Decimal, order: Order) -> OrderRefund:
|
||||
"""
|
||||
Process a backend user's request to initiate a new refund with an amount of ``amount`` for ``order``.
|
||||
|
||||
This method should parse the input provided to the form created and either raise ``ValidationError``
|
||||
or return an ``OrderRefund`` object in ``created`` state that has not yet been saved to the database.
|
||||
The system will then call ``execute_refund`` on that object.
|
||||
"""
|
||||
raise ValidationError('Not implemented')
|
||||
|
||||
def shred_payment_info(self, obj: Union[OrderPayment, OrderRefund]):
|
||||
"""
|
||||
When personal data is removed from an event, this method is called to scrub payment-related data
|
||||
|
||||
@@ -68,6 +68,28 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for prov, form in new_refunds %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>
|
||||
{{ prov.verbose_name }}
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
{% trans "Automatically refund" context "amount_label" %}
|
||||
<div class="input-group">
|
||||
<input type="text" name="newrefund-{{ prov }}"
|
||||
placeholder="{{ 0|floatformat:2 }}"
|
||||
title="" class="form-control">
|
||||
<span class="input-group-addon">
|
||||
{{ request.event.currency }}
|
||||
</span>
|
||||
</div><br>
|
||||
{{ form|safe }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
|
||||
@@ -46,8 +46,8 @@ from pretix.control.permissions import (
|
||||
from pretix.control.signals import item_forms, item_formsets
|
||||
from pretix.helpers.models import modelcopy
|
||||
|
||||
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
|
||||
from ...base.channels import get_all_sales_channels
|
||||
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
|
||||
|
||||
|
||||
class ItemList(ListView):
|
||||
|
||||
@@ -10,6 +10,7 @@ from urllib.parse import quote, urlencode
|
||||
import vat_moss.id
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
@@ -866,6 +867,29 @@ class OrderRefundView(OrderView):
|
||||
})
|
||||
))
|
||||
|
||||
for identifier, prov in self.request.event.get_payment_providers().items():
|
||||
prof_value = self.request.POST.get(f'newrefund-{identifier}', '0') or '0'
|
||||
prof_value = formats.sanitize_separators(prof_value)
|
||||
try:
|
||||
prof_value = Decimal(prof_value)
|
||||
except (DecimalException, TypeError):
|
||||
messages.error(self.request, _('You entered an invalid number.'))
|
||||
is_valid = False
|
||||
continue
|
||||
if prof_value > Decimal('0.00'):
|
||||
try:
|
||||
refund = prov.new_refund_control_form_process(self.request, prof_value, self.order)
|
||||
except ValidationError as e:
|
||||
for err in e:
|
||||
messages.error(self.request, err)
|
||||
is_valid = False
|
||||
continue
|
||||
if refund:
|
||||
refund_selected += refund.amount
|
||||
refund.comment = comment
|
||||
refund.source = OrderRefund.REFUND_SOURCE_ADMIN
|
||||
refunds.append(refund)
|
||||
|
||||
for p in payments:
|
||||
value = self.request.POST.get('refund-{}'.format(p.pk), '0') or '0'
|
||||
value = formats.sanitize_separators(value)
|
||||
@@ -907,7 +931,7 @@ class OrderRefundView(OrderView):
|
||||
'local_id': r.local_id,
|
||||
'provider': r.provider,
|
||||
}, user=self.request.user)
|
||||
if r.payment or r.provider == "offsetting" or r.provider == "giftcard":
|
||||
if r.provider != "manual":
|
||||
try:
|
||||
r.payment_provider.execute_refund(r)
|
||||
except PaymentException as e:
|
||||
@@ -969,8 +993,17 @@ class OrderRefundView(OrderView):
|
||||
messages.error(self.request, _('The refunds you selected do not match the selected total refund '
|
||||
'amount.'))
|
||||
|
||||
new_refunds = []
|
||||
for identifier, prov in self.request.event.get_payment_providers().items():
|
||||
form = prov.new_refund_control_form_render(self.request, self.order)
|
||||
if form:
|
||||
new_refunds.append(
|
||||
(prov, form)
|
||||
)
|
||||
|
||||
return render(self.request, 'pretixcontrol/order/refund_choose.html', {
|
||||
'payments': payments,
|
||||
'new_refunds': new_refunds,
|
||||
'remainder': to_refund,
|
||||
'order': self.order,
|
||||
'comment': comment,
|
||||
@@ -2078,7 +2111,7 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, View)
|
||||
messages.error(
|
||||
self.request,
|
||||
str(_('There was a problem processing your input:')) + ' ' + ', '.join(
|
||||
', '.join(l) for l in self.exporter.form.errors.values()
|
||||
', '.join(line) for line in self.exporter.form.errors.values()
|
||||
)
|
||||
)
|
||||
return redirect(reverse('control:event.orders.export', kwargs={
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import textwrap
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -13,7 +14,7 @@ from i18nfield.strings import LazyI18nString
|
||||
from localflavor.generic.forms import BICFormField, IBANFormField
|
||||
from localflavor.generic.validators import IBANValidator
|
||||
|
||||
from pretix.base.models import OrderPayment, OrderRefund
|
||||
from pretix.base.models import Order, OrderPayment, OrderRefund
|
||||
from pretix.base.payment import BasePaymentProvider
|
||||
|
||||
|
||||
@@ -298,6 +299,9 @@ class BankTransfer(BasePaymentProvider):
|
||||
We just keep a created refund object. It will be marked as done using the control view
|
||||
for bank transfer refunds.
|
||||
"""
|
||||
if refund.info_data.get('iban'):
|
||||
return # we're already done here
|
||||
|
||||
if refund.payment is None:
|
||||
raise ValueError(_("Can only create a bank transfer refund from an existing payment."))
|
||||
|
||||
@@ -310,3 +314,59 @@ class BankTransfer(BasePaymentProvider):
|
||||
|
||||
def refund_control_render(self, request: HttpRequest, refund: OrderRefund) -> str:
|
||||
return self._render_control_info(request, refund.order, refund.info_data)
|
||||
|
||||
class NewRefundForm(forms.Form):
|
||||
payer = forms.CharField(
|
||||
label=_('Account holder'),
|
||||
)
|
||||
iban = IBANFormField(
|
||||
label=_('IBAN'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for n, f in self.fields.items():
|
||||
f.required = False
|
||||
f.widget.is_required = False
|
||||
|
||||
def clean_payer(self):
|
||||
val = self.cleaned_data.get('payer')
|
||||
if not val:
|
||||
raise ValidationError(_("This field is required."))
|
||||
return val
|
||||
|
||||
def clean_iban(self):
|
||||
val = self.cleaned_data.get('iban')
|
||||
if not val:
|
||||
raise ValidationError(_("This field is required."))
|
||||
return val
|
||||
|
||||
def new_refund_control_form_render(self, request: HttpRequest, order: Order) -> str:
|
||||
f = self.NewRefundForm(
|
||||
prefix="refund-banktransfer",
|
||||
data=request.POST if request.method == "POST" and request.POST.get("refund-banktransfer-iban") else None,
|
||||
)
|
||||
template = get_template('pretixplugins/banktransfer/new_refund_control_form.html')
|
||||
ctx = {
|
||||
'form': f,
|
||||
}
|
||||
return template.render(ctx)
|
||||
|
||||
def new_refund_control_form_process(self, request: HttpRequest, amount: Decimal, order: Order) -> OrderRefund:
|
||||
f = self.NewRefundForm(
|
||||
prefix="refund-banktransfer",
|
||||
data=request.POST
|
||||
)
|
||||
if not f.is_valid():
|
||||
raise ValidationError(_('Your input was invalid, please see below for details.'))
|
||||
return OrderRefund(
|
||||
order=order,
|
||||
payment=None,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
amount=amount,
|
||||
provider=self.identifier,
|
||||
info=json.dumps({
|
||||
'payer': f.cleaned_data['payer'],
|
||||
'iban': self.norm(f.cleaned_data['iban']),
|
||||
})
|
||||
)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{% load i18n %}
|
||||
{% load ibanformat %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% trans "to" %}
|
||||
{% bootstrap_field form.payer layout="inline" %}
|
||||
{% bootstrap_field form.iban layout="inline" %}
|
||||
{% bootstrap_form_errors form error_types="all" %}
|
||||
Reference in New Issue
Block a user