From a62c7939ae4530eba8c8e68420ddbad3efe5bb1f Mon Sep 17 00:00:00 2001
From: Felix Rindt
Date: Thu, 22 Oct 2020 11:00:36 +0200
Subject: [PATCH] Improvements for bank transfer importing (#1762)
Co-authored-by: Raphael Michel
Co-authored-by: Raphael Michel
---
src/pretix/base/payment.py | 2 +-
src/pretix/plugins/banktransfer/csvimport.py | 11 +-
.../migrations/0006_auto_20200901_1419.py | 30 ++
.../migrations/0007_refundexport.py | 27 ++
src/pretix/plugins/banktransfer/models.py | 40 +++
.../plugins/banktransfer/mt940import.py | 5 +-
src/pretix/plugins/banktransfer/payment.py | 54 +++-
.../plugins/banktransfer/refund_export.py | 68 ++++
src/pretix/plugins/banktransfer/signals.py | 47 ++-
.../static/pretixplugins/banktransfer/ui.js | 1 +
src/pretix/plugins/banktransfer/tasks.py | 35 +-
.../pretixplugins/banktransfer/control.html | 22 +-
.../banktransfer/import_assign.html | 130 +++++---
.../banktransfer/import_form.html | 57 ++--
.../banktransfer/refund_export.html | 113 +++++++
.../banktransfer/sepa_export.html | 36 +++
.../banktransfer/transaction_list.html | 46 ++-
src/pretix/plugins/banktransfer/urls.py | 17 +
src/pretix/plugins/banktransfer/views.py | 301 +++++++++++++++++-
src/requirements/production.txt | 1 +
src/setup.py | 2 +-
src/tests/control/test_permissions.py | 17 +-
src/tests/plugins/banktransfer/test_mt940.py | 37 ++-
.../plugins/banktransfer/test_parsing.py | 21 ++
src/tests/plugins/banktransfer/test_refund.py | 88 +++++
.../banktransfer/test_refund_export.py | 135 ++++++++
26 files changed, 1204 insertions(+), 139 deletions(-)
create mode 100644 src/pretix/plugins/banktransfer/migrations/0006_auto_20200901_1419.py
create mode 100644 src/pretix/plugins/banktransfer/migrations/0007_refundexport.py
create mode 100644 src/pretix/plugins/banktransfer/refund_export.py
create mode 100644 src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html
create mode 100644 src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/sepa_export.html
create mode 100644 src/tests/plugins/banktransfer/test_parsing.py
create mode 100644 src/tests/plugins/banktransfer/test_refund.py
create mode 100644 src/tests/plugins/banktransfer/test_refund_export.py
diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py
index 00deae46e..d50495481 100644
--- a/src/pretix/base/payment.py
+++ b/src/pretix/base/payment.py
@@ -717,7 +717,7 @@ class BasePaymentProvider:
The default implementation returns an empty string.
- :param order: The order object
+ :param refund: The refund object
"""
return ''
diff --git a/src/pretix/plugins/banktransfer/csvimport.py b/src/pretix/plugins/banktransfer/csvimport.py
index d5369f3fb..c874c9084 100644
--- a/src/pretix/plugins/banktransfer/csvimport.py
+++ b/src/pretix/plugins/banktransfer/csvimport.py
@@ -2,6 +2,8 @@ import csv
import io
import re
+from django.utils.text import Truncator
+
class HintMismatchError(Exception):
pass
@@ -28,6 +30,11 @@ def parse(data, hint):
resrow['amount'] = re.sub('[^0-9,+.-]', '', resrow['amount'])
if hint.get('date') is not None:
resrow['date'] = row[int(hint.get('date'))].strip()
+ if hint.get('iban') is not None:
+ resrow['iban'] = Truncator(row[int(hint.get('iban'))].strip()).chars(200)
+ if hint.get('bic') is not None:
+ resrow['bic'] = Truncator(row[int(hint.get('bic'))].strip()).chars(200)
+
if len(resrow['amount']) == 0 or 'amount' not in resrow or resrow.get('date') == '':
# This is probably a headline or something other special.
continue
@@ -88,5 +95,7 @@ def new_hint(data):
'reference': data.getlist('reference') if 'reference' in data else None,
'date': int(data.get('date')) if 'date' in data else None,
'amount': int(data.get('amount')) if 'amount' in data else None,
- 'cols': int(data.get('cols')) if 'cols' in data else None
+ 'cols': int(data.get('cols')) if 'cols' in data else None,
+ 'iban': int(data.get('iban')) if 'iban' in data else None,
+ 'bic': int(data.get('bic')) if 'bic' in data else None,
}
diff --git a/src/pretix/plugins/banktransfer/migrations/0006_auto_20200901_1419.py b/src/pretix/plugins/banktransfer/migrations/0006_auto_20200901_1419.py
new file mode 100644
index 000000000..3dbdf7beb
--- /dev/null
+++ b/src/pretix/plugins/banktransfer/migrations/0006_auto_20200901_1419.py
@@ -0,0 +1,30 @@
+# Generated by Django 3.0.9 on 2020-09-01 14:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('banktransfer', '0005_auto_20181023_2209'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='banktransaction',
+ name='bic',
+ field=models.CharField(default='', max_length=250),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='banktransaction',
+ name='date_parsed',
+ field=models.DateField(null=True),
+ ),
+ migrations.AddField(
+ model_name='banktransaction',
+ name='iban',
+ field=models.CharField(default='', max_length=250),
+ preserve_default=False,
+ ),
+ ]
diff --git a/src/pretix/plugins/banktransfer/migrations/0007_refundexport.py b/src/pretix/plugins/banktransfer/migrations/0007_refundexport.py
new file mode 100644
index 000000000..72d873095
--- /dev/null
+++ b/src/pretix/plugins/banktransfer/migrations/0007_refundexport.py
@@ -0,0 +1,27 @@
+# Generated by Django 3.0.9 on 2020-09-09 15:43
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pretixbase', '0162_remove_seat_name'),
+ ('banktransfer', '0006_auto_20200901_1419'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='RefundExport',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
+ ('datetime', models.DateTimeField(auto_now_add=True)),
+ ('testmode', models.BooleanField(default=False)),
+ ('rows', models.TextField(default='[]')),
+ ('event', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='banktransfer_refund_exports', to='pretixbase.Event')),
+ ('organizer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='banktransfer_refund_exports', to='pretixbase.Organizer')),
+ ('downloaded', models.BooleanField(default=False)),
+ ],
+ ),
+ ]
diff --git a/src/pretix/plugins/banktransfer/models.py b/src/pretix/plugins/banktransfer/models.py
index 23fde5245..4ec31250a 100644
--- a/src/pretix/plugins/banktransfer/models.py
+++ b/src/pretix/plugins/banktransfer/models.py
@@ -1,7 +1,10 @@
import hashlib
+import json
import re
+from decimal import Decimal
from django.db import models
+from django.utils.functional import cached_property
class BankImportJob(models.Model):
@@ -61,6 +64,9 @@ class BankTransaction(models.Model):
reference = models.TextField(blank=True)
amount = models.DecimalField(max_digits=10, decimal_places=2)
date = models.CharField(max_length=50)
+ date_parsed = models.DateField(null=True)
+ iban = models.CharField(max_length=250, blank=True)
+ bic = models.CharField(max_length=250, blank=True)
order = models.ForeignKey('pretixbase.Order', null=True, blank=True, on_delete=models.CASCADE)
comment = models.TextField(blank=True)
@@ -80,3 +86,37 @@ class BankTransaction(models.Model):
class Meta:
unique_together = ('event', 'organizer', 'checksum')
ordering = ('date', 'id')
+
+
+class RefundExport(models.Model):
+ event = models.ForeignKey('pretixbase.Event', related_name='banktransfer_refund_exports', on_delete=models.CASCADE, null=True, blank=True)
+ organizer = models.ForeignKey('pretixbase.Organizer', related_name='banktransfer_refund_exports', on_delete=models.PROTECT, null=True, blank=True)
+ datetime = models.DateTimeField(auto_now_add=True)
+ testmode = models.BooleanField(default=False)
+ rows = models.TextField(default="[]")
+ downloaded = models.BooleanField(default=False)
+
+ @cached_property
+ def entity_slug(self):
+ if self.organizer:
+ return self.organizer.slug
+ else:
+ return self.event.slug
+
+ @cached_property
+ def currency(self):
+ if self.event:
+ return self.event.currency
+ return self.organizer.events.first().currency
+
+ @property
+ def rows_data(self):
+ return json.loads(self.rows)
+
+ @property
+ def sum(self):
+ return sum(Decimal(row["amount"]) for row in self.rows_data)
+
+ @property
+ def cnt(self):
+ return len(self.rows_data)
diff --git a/src/pretix/plugins/banktransfer/mt940import.py b/src/pretix/plugins/banktransfer/mt940import.py
index a60440e9b..0c78e2968 100644
--- a/src/pretix/plugins/banktransfer/mt940import.py
+++ b/src/pretix/plugins/banktransfer/mt940import.py
@@ -158,8 +158,9 @@ def parse(file):
result.append({
'amount': str(round_decimal(t.data['amount'].amount)),
'reference': reference + (' EREF: {}'.format(eref) if eref else ''),
- 'payer': (payer.get('name', '') + ' - ' + payer.get('iban', '')).strip(),
- 'date': t.data['date'].isoformat()
+ 'payer': payer['name'].strip(),
+ 'date': t.data['date'].isoformat(),
+ **{k: payer[k].strip() for k in ("iban", "bic") if payer.get(k)}
})
else:
result.append({
diff --git a/src/pretix/plugins/banktransfer/payment.py b/src/pretix/plugins/banktransfer/payment.py
index 21659f6c6..cdff2a9e2 100644
--- a/src/pretix/plugins/banktransfer/payment.py
+++ b/src/pretix/plugins/banktransfer/payment.py
@@ -11,8 +11,9 @@ from i18nfield.fields import I18nFormField, I18nTextarea
from i18nfield.forms import I18nTextInput
from i18nfield.strings import LazyI18nString
from localflavor.generic.forms import BICFormField, IBANFormField
+from localflavor.generic.validators import BICValidator, IBANValidator
-from pretix.base.models import OrderPayment
+from pretix.base.models import OrderPayment, OrderRefund
from pretix.base.payment import BasePaymentProvider
@@ -144,9 +145,7 @@ class BankTransfer(BasePaymentProvider):
def settings_form_clean(self, cleaned_data):
if cleaned_data.get('payment_banktransfer_bank_details_type') == 'sepa':
- for f in (
- 'bank_details_sepa_name', 'bank_details_sepa_bank', 'bank_details_sepa_bic',
- 'bank_details_sepa_iban'):
+ for f in ('bank_details_sepa_name', 'bank_details_sepa_bank', 'bank_details_sepa_bic', 'bank_details_sepa_iban'):
if not cleaned_data.get('payment_banktransfer_%s' % f):
raise ValidationError(
{'payment_banktransfer_%s' % f: _('Please fill out your bank account details.')})
@@ -213,10 +212,17 @@ class BankTransfer(BasePaymentProvider):
return template.render(ctx)
def payment_control_render(self, request: HttpRequest, payment: OrderPayment) -> str:
+ warning = None
+ if not self.payment_refund_supported(payment):
+ warning = _("Invalid IBAN/BIC")
+ return self._render_control_info(request, payment.order, payment.info_data, warning=warning)
+
+ def _render_control_info(self, request, order, info_data, **extra_context):
template = get_template('pretixplugins/banktransfer/control.html')
ctx = {'request': request, 'event': self.event,
- 'code': self._code(payment.order),
- 'payment_info': payment.info_data, 'order': payment.order}
+ 'code': self._code(order),
+ 'payment_info': info_data, 'order': order,
+ **extra_context}
return template.render(ctx)
def _code(self, order):
@@ -234,3 +240,39 @@ class BankTransfer(BasePaymentProvider):
d['_shredded'] = True
obj.info = json.dumps(d)
obj.save(update_fields=['info'])
+
+ @staticmethod
+ def norm(s):
+ return s.strip().upper().replace(" ", "")
+
+ def payment_refund_supported(self, payment: OrderPayment) -> bool:
+ if not all(payment.info_data.get(key) for key in ("payer", "iban", "bic")):
+ return False
+ try:
+ IBANValidator()(self.norm(payment.info_data['iban']))
+ BICValidator()(self.norm(payment.info_data['bic']))
+ except ValidationError:
+ return False
+ else:
+ return True
+
+ def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
+ return self.payment_refund_supported(payment)
+
+ def execute_refund(self, refund: OrderRefund):
+ """
+ We just keep a created refund object. It will be marked as done using the control view
+ for bank transfer refunds.
+ """
+ if refund.payment is None:
+ raise ValueError(_("Can only create a bank transfer refund from an existing payment."))
+
+ refund.info_data = {
+ 'payer': refund.payment.info_data['payer'],
+ 'iban': self.norm(refund.payment.info_data['iban']),
+ 'bic': self.norm(refund.payment.info_data['bic']),
+ }
+ refund.save(update_fields=["info"])
+
+ def refund_control_render(self, request: HttpRequest, refund: OrderRefund) -> str:
+ return self._render_control_info(request, refund.order, refund.info_data)
diff --git a/src/pretix/plugins/banktransfer/refund_export.py b/src/pretix/plugins/banktransfer/refund_export.py
new file mode 100644
index 000000000..585180c4b
--- /dev/null
+++ b/src/pretix/plugins/banktransfer/refund_export.py
@@ -0,0 +1,68 @@
+import codecs
+import datetime
+import io
+from decimal import Decimal
+
+from defusedcsv import csv
+from django.templatetags.l10n import localize
+from django.utils.translation import gettext_lazy as _
+
+from pretix.plugins.banktransfer.models import RefundExport
+
+
+def _get_filename(refund_export):
+ return 'bank_transfer_refunds-{}_{}-{}'.format(refund_export.entity_slug, refund_export.datetime.strftime("%Y-%m-%d"), refund_export.id)
+
+
+def get_refund_export_csv(refund_export: RefundExport):
+ byte_data = io.BytesIO()
+ StreamWriter = codecs.getwriter('utf-8')
+ output = StreamWriter(byte_data)
+
+ writer = csv.writer(output)
+ writer.writerow([_("Payer"), "IBAN", "BIC", _("Amount"), _("Currency"), _("Code")])
+ for row in refund_export.rows_data:
+ writer.writerow([
+ row['payer'],
+ row['iban'],
+ row['bic'],
+ localize(Decimal(row['amount'])),
+ refund_export.currency,
+ row['id'],
+ ])
+
+ filename = _get_filename(refund_export) + ".csv"
+ byte_data.seek(0)
+ return filename, 'text/csv', byte_data
+
+
+from sepaxml import SepaTransfer
+
+
+def build_sepa_xml(refund_export: RefundExport, account_holder, iban, bic):
+ if refund_export.currency != "EUR":
+ raise ValueError("Cannot create SEPA export for currency other than EUR.")
+
+ config = {
+ "name": account_holder,
+ "IBAN": iban,
+ "BIC": bic,
+ "batch": True,
+ "currency": refund_export.currency,
+ }
+ sepa = SepaTransfer(config, clean=True)
+
+ for row in refund_export.rows_data:
+ payment = {
+ "name": row['payer'],
+ "IBAN": row["iban"],
+ "BIC": row["bic"],
+ "amount": int(Decimal(row['amount']) * 100), # in euro-cents
+ "execution_date": datetime.date.today(),
+ "description": f"{_('Refund')} {refund_export.entity_slug} {row['id']}",
+ }
+ sepa.add_payment(payment)
+
+ data = sepa.export(validate=True)
+ filename = _get_filename(refund_export) + ".xml"
+ return filename, 'application/xml', io.BytesIO(data)
diff --git a/src/pretix/plugins/banktransfer/signals.py b/src/pretix/plugins/banktransfer/signals.py
index 35e422aed..818ac2026 100644
--- a/src/pretix/plugins/banktransfer/signals.py
+++ b/src/pretix/plugins/banktransfer/signals.py
@@ -21,14 +21,31 @@ def control_nav_import(sender, request=None, **kwargs):
return []
return [
{
- 'label': _('Import bank data'),
+ 'label': _("Bank transfer"),
'url': reverse('plugins:banktransfer:import', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
- 'active': (url.namespace == 'plugins:banktransfer' and url.url_name == 'import'),
- 'icon': 'upload',
- }
+ 'icon': 'university',
+ 'children': [
+ {
+ 'label': _('Import bank data'),
+ 'url': reverse('plugins:banktransfer:import', kwargs={
+ 'event': request.event.slug,
+ 'organizer': request.event.organizer.slug,
+ }),
+ 'active': (url.namespace == 'plugins:banktransfer' and url.url_name == 'import'),
+ },
+ {
+ 'label': _('Export refunds'),
+ 'url': reverse('plugins:banktransfer:refunds.list', kwargs={
+ 'event': request.event.slug,
+ 'organizer': request.event.organizer.slug,
+ }),
+ 'active': (url.namespace == 'plugins:banktransfer' and url.url_name.startswith("refunds")),
+ },
+ ]
+ },
]
@@ -41,12 +58,28 @@ def control_nav_orga_import(sender, request=None, **kwargs):
return []
return [
{
- 'label': _('Import bank data'),
+ 'label': _("Bank transfer"),
'url': reverse('plugins:banktransfer:import', kwargs={
'organizer': request.organizer.slug,
}),
- 'active': (url.namespace == 'plugins:banktransfer' and url.url_name == 'import'),
- 'icon': 'upload',
+ 'icon': 'university',
+ 'children': [
+ {
+ 'label': _('Import bank data'),
+ 'url': reverse('plugins:banktransfer:import', kwargs={
+ 'organizer': request.organizer.slug,
+ }),
+ 'active': (url.namespace == 'plugins:banktransfer' and url.url_name == 'import'),
+ 'icon': 'upload',
+ },
+ {
+ 'label': _('Export refunds'),
+ 'url': reverse('plugins:banktransfer:refunds.list', kwargs={
+ 'organizer': request.organizer.slug,
+ }),
+ 'active': (url.namespace == 'plugins:banktransfer' and url.url_name.startswith("refunds")),
+ },
+ ]
}
]
diff --git a/src/pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js b/src/pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
index fd6f925de..91c0ec967 100644
--- a/src/pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
+++ b/src/pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
@@ -104,6 +104,7 @@ var bankimport_transactionlist = {
var text = $box.find("textarea").val();
$box.find("input, textarea, button").prop("disabled", true);
bankimport_transactionlist._action(id, "comment:" + text, function () {
+ $("tr[data-id=" + id + "] button").prop("disabled", false);
});
});
$btn2.click(function () {
diff --git a/src/pretix/plugins/banktransfer/tasks.py b/src/pretix/plugins/banktransfer/tasks.py
index b084ff5e3..4d5fa26e1 100644
--- a/src/pretix/plugins/banktransfer/tasks.py
+++ b/src/pretix/plugins/banktransfer/tasks.py
@@ -2,6 +2,7 @@ import logging
import re
from decimal import Decimal
+import dateutil.parser
from celery.exceptions import MaxRetriesExceededError
from django.conf import settings
from django.db import transaction
@@ -41,9 +42,9 @@ def notify_incomplete_payment(o: Order):
def cancel_old_payments(order):
for p in order.payments.filter(
- state__in=(OrderPayment.PAYMENT_STATE_PENDING,
- OrderPayment.PAYMENT_STATE_CREATED),
- provider='banktransfer',
+ state__in=(OrderPayment.PAYMENT_STATE_PENDING,
+ OrderPayment.PAYMENT_STATE_CREATED),
+ provider='banktransfer',
):
try:
with transaction.atomic():
@@ -64,8 +65,8 @@ def cancel_old_payments(order):
@transaction.atomic
-def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, organizer: Organizer=None,
- slug: str=None):
+def _handle_transaction(trans: BankTransaction, code: str, event: Event = None, organizer: Organizer = None,
+ slug: str = None):
if event:
try:
trans.order = event.orders.get(code=code)
@@ -117,8 +118,10 @@ def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, or
p.info_data = {
'reference': trans.reference,
- 'date': trans.date,
+ 'date': trans.date_parsed.isoformat() if trans.date_parsed else trans.date,
'payer': trans.payer,
+ 'iban': trans.iban,
+ 'bic': trans.bic,
'trans_id': trans.pk
}
@@ -150,7 +153,18 @@ def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, or
trans.save()
-def _get_unknown_transactions(job: BankImportJob, data: list, event: Event=None, organizer: Organizer=None):
+def parse_date(date_str):
+ try:
+ return dateutil.parser.parse(
+ date_str,
+ dayfirst="." in date_str,
+ ).date()
+ except (ValueError, OverflowError):
+ pass
+ return None
+
+
+def _get_unknown_transactions(job: BankImportJob, data: list, event: Event = None, organizer: Organizer = None):
amount_pattern = re.compile("[^0-9.-]")
known_checksums = set(t['checksum'] for t in BankTransaction.objects.filter(
Q(event=event) if event else Q(organizer=organizer)
@@ -176,8 +190,11 @@ def _get_unknown_transactions(job: BankImportJob, data: list, event: Event=None,
trans = BankTransaction(event=event, organizer=organizer, import_job=job,
payer=row.get('payer', ''),
reference=row['reference'],
- amount=amount,
- date=row['date'])
+ amount=amount, date=row['date'],
+ iban=row.get('iban', ''), bic=row.get('bic', ''))
+
+ trans.date_parsed = parse_date(trans.date)
+
trans.checksum = trans.calculate_checksum()
if trans.checksum not in known_checksums:
trans.state = BankTransaction.STATE_UNCHECKED
diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/control.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/control.html
index c08244269..bc79e6393 100644
--- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/control.html
+++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/control.html
@@ -4,10 +4,24 @@
{% trans "Payer" %}
{{ payment_info.payer }}
- {% trans "Payment date" %}
- {{ payment_info.date }}
- {% trans "Reference" %}
- {{ payment_info.reference }}
+ {% if payment_info.iban %}
+ {% trans "Account" %}
+
+ {{ payment_info.iban }} {{ payment_info.bic }}
+ {% if warning %}
+
+ {% endif %}
+
+ {% endif %}
+ {% if payment_info.date %}
+ {% trans "Payment date" %}
+ {{ payment_info.date }}
+ {% endif %}
+ {% if payment_info.reference %}
+ {% trans "Reference" %}
+ {{ payment_info.reference }}
+ {% endif %}
{% else %}
diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_assign.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_assign.html
index b195c92f6..91c8d5853 100644
--- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_assign.html
+++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_assign.html
@@ -13,62 +13,94 @@
-
-
+
+
diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html
index 77715c59e..27e1337b9 100644
--- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html
+++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html
@@ -23,7 +23,8 @@
Currently, this feature supports .csv files and files in the MT940 format.
{% endblocktrans %}
{% if job_running %}
-
+
{% trans "An import is currently being processed, please try again in a few minutes." %}
@@ -31,7 +32,8 @@
{% endif %}
- {% if transactions_unhandled|length > 0 or request.GET.search %}
+ {% if transactions_unhandled|length > 0 or filter_form.is_valid %}
{% trans "Unresolved transactions" %}
@@ -55,25 +57,42 @@
unmatched transactions imported directly for this event.
{% endblocktrans %}
{% trans "Go to organizer-level import" %}
+ class="btn btn-default btn-xs">{% trans "Go to organizer-level import" %}
{% endif %}
-
-
- {% if not request.GET.search %}
-
+ {% csrf_token %}
+
+ {% trans "Create new export file" %}
+
+
+
+ {% blocktrans %}
+ Beware that refunds will be marked as done once an export is created.
+ Make sure to download the export and execute the refunds.
+ {% endblocktrans %}
+
+
+ {% endif %}
+
{% trans "Exported files" %}
+
+
+
+
+ {% trans "Export date" %}
+ {% trans "Number of orders" %}
+ {% trans "Total amount" %}
+
+
+
+
+ {% for export in exports %}
+
+
+ {{ export.datetime|date:"SHORT_DATETIME_FORMAT" }}
+ {% if export.testmode %}
+ {% trans "TEST MODE" %}
+ {% endif %}
+
+ {{ export.cnt }}
+
+ {{ export.sum|default_if_none:0|money:export.currency }}
+
+
+ {% if not export.downloaded %}
+ {% trans "not downloaded" %}
+ {% endif %}
+ {% if export.event %}
+
+ {% trans "Download CSV" %}
+
+ {% if export.currency == "EUR" %}
+
+ {% trans "SEPA XML" %}
+
+ {% endif %}
+ {% else %}
+
+ {% trans "Download CSV" %}
+
+ {% if export.currency == "EUR" %}
+
+ {% trans "SEPA XML" %}
+
+ {% endif %}
+ {% endif %}
+
+
+ {% empty %}
+
+
+ {% trans "No exports have been created yet." %}
+
+
+ {% endfor %}
+
+
+
+{% endblock %}
diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/sepa_export.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/sepa_export.html
new file mode 100644
index 000000000..812f850db
--- /dev/null
+++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/sepa_export.html
@@ -0,0 +1,36 @@
+{% extends basetpl %}
+{% load bootstrap3 %}
+{% load i18n %}
+{% load rich_text %}
+{% load money %}
+{% load static %}
+{% block title %}{% trans "Export bank transfer refunds" %}{% endblock %}
+{% block content %}
+
+
{% trans "Export SEPA xml" %}
+
+
+ {% blocktrans with cnt=export.cnt sum=export.sum|money:export.currency date=export.datetime|date %}
+ You are trying to download a refund export from {{ date }} with {{ cnt }} order{{ cnt|pluralize }} and a
+ total of {{ sum }}.
+ {% endblocktrans %}
+
+
+
+ {% blocktrans %}
+ Please state from which bank account the refunds should be transferred from.
+ {% endblocktrans %}
+
+
+
+ {% csrf_token %}
+
+
+ {% bootstrap_form form layout="control" %}
+
+
+ {% trans "Download" %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/transaction_list.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/transaction_list.html
index 2e0ebeece..919284e84 100644
--- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/transaction_list.html
+++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/transaction_list.html
@@ -22,11 +22,11 @@
{% if trans.order and trans.state == 'invalid' %}
+ data-toggle="tooltip" title="{% trans "Accept anyway" %}" data-placement="right">
+ data-toggle="tooltip" title="{% trans "Discard" %}">
@@ -34,22 +34,23 @@
+ value="assign" data-toggle="tooltip" title="{% trans "Assign to order" %}"
+ data-placement="right">
+ data-toggle="tooltip" title="{% trans "Discard" %}">
{% elif trans.state == 'error' %}
+ data-toggle="tooltip" title="{% trans "Retry" %}" data-placement="right">
+ data-toggle="tooltip" title="{% trans "Discard" %}">
@@ -57,23 +58,44 @@
+ value="assign" data-toggle="tooltip" title="{% trans "Assign to order" %}"
+ data-placement="right">
+ data-toggle="tooltip" title="{% trans "Retry" %}" data-placement="right">
+ data-toggle="tooltip" title="{% trans "Discard" %}">
{% endif %}
-
{{ trans.date }}
- {{ trans.payer }}
+ {% if trans.date_parsed is not None %}
+ {{ trans.date_parsed|date:"SHORT_DATE_FORMAT" }}
+ {% else %}
+ {{ trans.date }}
+ {% endif %}
+
+
+
+ {% if trans.payer %}
+ {{ trans.payer }}
+
+ {% endif %}
+ {% if trans.iban or trans.bic %}
+ {% if trans.iban %}
+ {{ trans.iban }}
+ {% endif %}
+ {% if trans.bic %}
+ {{ trans.bic }}
+ {% endif %}
+
+ {% endif %}
+
{{ trans.reference }}