From cf3c4d26cb7dce649c33e3b122dc6a9563976533 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Fri, 8 Jan 2021 23:21:26 +0100 Subject: [PATCH] Bank transfer: Allow to refund payments without BIC --- src/pretix/plugins/banktransfer/payment.py | 7 ++-- .../plugins/banktransfer/refund_export.py | 21 +++++++++- src/pretix/plugins/banktransfer/views.py | 2 +- src/requirements/production.txt | 2 +- src/setup.py | 2 +- src/tests/plugins/banktransfer/test_refund.py | 12 ++++++ .../banktransfer/test_refund_export.py | 40 ++++++++++++++++++- 7 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/pretix/plugins/banktransfer/payment.py b/src/pretix/plugins/banktransfer/payment.py index fdb033c4c5..eaa26aa991 100644 --- a/src/pretix/plugins/banktransfer/payment.py +++ b/src/pretix/plugins/banktransfer/payment.py @@ -11,7 +11,7 @@ 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 localflavor.generic.validators import IBANValidator from pretix.base.models import OrderPayment, OrderRefund from pretix.base.payment import BasePaymentProvider @@ -269,11 +269,10 @@ class BankTransfer(BasePaymentProvider): 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")): + if not all(payment.info_data.get(key) for key in ("payer", "iban")): return False try: IBANValidator()(self.norm(payment.info_data['iban'])) - BICValidator()(self.norm(payment.info_data['bic'])) except ValidationError: return False else: @@ -305,7 +304,7 @@ class BankTransfer(BasePaymentProvider): 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']), + 'bic': self.norm(refund.payment.info_data['bic']) if refund.payment.info_data.get('bic') else None, } refund.save(update_fields=["info"]) diff --git a/src/pretix/plugins/banktransfer/refund_export.py b/src/pretix/plugins/banktransfer/refund_export.py index 585180c4ba..04419616a9 100644 --- a/src/pretix/plugins/banktransfer/refund_export.py +++ b/src/pretix/plugins/banktransfer/refund_export.py @@ -4,8 +4,10 @@ import io from decimal import Decimal from defusedcsv import csv +from django.core.exceptions import ValidationError from django.templatetags.l10n import localize from django.utils.translation import gettext_lazy as _ +from localflavor.generic.validators import BICValidator from pretix.plugins.banktransfer.models import RefundExport @@ -22,10 +24,18 @@ def get_refund_export_csv(refund_export: RefundExport): writer = csv.writer(output) writer.writerow([_("Payer"), "IBAN", "BIC", _("Amount"), _("Currency"), _("Code")]) for row in refund_export.rows_data: + bic = '' + if row.get('bic'): + try: + BICValidator()(row['bic']) + except ValidationError: + pass + else: + bic = row['bic'] writer.writerow([ row['payer'], row['iban'], - row['bic'], + bic, localize(Decimal(row['amount'])), refund_export.currency, row['id'], @@ -56,11 +66,18 @@ def build_sepa_xml(refund_export: RefundExport, account_holder, iban, bic): 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']}", } + if row.get('bic'): + try: + BICValidator()(row['bic']) + except ValidationError: + pass + else: + payment['BIC'] = row['bic'] + sepa.add_payment(payment) data = sepa.export(validate=True) diff --git a/src/pretix/plugins/banktransfer/views.py b/src/pretix/plugins/banktransfer/views.py index bacfb54687..5e2a4b7629 100644 --- a/src/pretix/plugins/banktransfer/views.py +++ b/src/pretix/plugins/banktransfer/views.py @@ -649,7 +649,7 @@ class RefundExportListView(ListView): transaction_rows.append({ "amount": refund.amount, "id": refund.full_id, - **{key: data[key] for key in ("payer", "iban", "bic")} + **{key: data.get(key) for key in ("payer", "iban", "bic")} }) refund.done(user=self.request.user) diff --git a/src/requirements/production.txt b/src/requirements/production.txt index 2fe5ccbb90..928c8f784e 100644 --- a/src/requirements/production.txt +++ b/src/requirements/production.txt @@ -70,4 +70,4 @@ tlds>=2020041600 text-unidecode==1.* protobuf==3.13.* cryptography -sepaxml==2.3.* +sepaxml==2.4.* diff --git a/src/setup.py b/src/setup.py index 67a9848b39..abd0f02ff9 100644 --- a/src/setup.py +++ b/src/setup.py @@ -156,7 +156,7 @@ setup( 'text-unidecode==1.*', 'protobuf==3.13.*', 'cryptography', - 'sepaxml==2.3.*', + 'sepaxml==2.4.*', ], extras_require={ 'dev': [ diff --git a/src/tests/plugins/banktransfer/test_refund.py b/src/tests/plugins/banktransfer/test_refund.py index 7ef3ba2a1e..7050f04a73 100644 --- a/src/tests/plugins/banktransfer/test_refund.py +++ b/src/tests/plugins/banktransfer/test_refund.py @@ -86,3 +86,15 @@ def test_cannot_perform_refund_with_invalid_iban(client, env): assert r.status_code == 200 # no successfull POST with scope(organizer=event.organizer): assert not OrderRefund.objects.exists() + + +@pytest.mark.django_db +def test_can_perform_refund_without_valid_bic(client, env): + event, user, payment = env + payment.info_data = { + 'payer': "Abc Def", + 'iban': "DE27520521540534534466", + 'bic': "TROLOLOL", + } + payment.save() + assert payment.payment_provider.payment_refund_supported(payment) diff --git a/src/tests/plugins/banktransfer/test_refund_export.py b/src/tests/plugins/banktransfer/test_refund_export.py index f4691a885e..ca0b522efb 100644 --- a/src/tests/plugins/banktransfer/test_refund_export.py +++ b/src/tests/plugins/banktransfer/test_refund_export.py @@ -62,7 +62,9 @@ def test_export_refunds_as_sepa_xml(client, env, url_prefix): "iban": "DE71720690050653667120", "bic": "GENODEF1AIL", }) - assert "DE27520521540534534466" in "".join(str(part) for part in r.streaming_content) + r = "".join(str(part) for part in r.streaming_content) + assert "DE27520521540534534466" in r + assert "HELADEF" in r @pytest.mark.django_db @@ -78,7 +80,41 @@ def test_export_refunds(client, env, url_prefix): assert b"Download CSV" in r.content r = client.get(f'{url_prefix}banktransfer/export/{refund.id}/') assert r.status_code == 200 - assert "DE27520521540534534466" in "".join(str(part) for part in r.streaming_content) + r = "".join(str(part) for part in r.streaming_content) + assert "DE27520521540534534466" in r + assert "HELADEF" in r + + +@pytest.mark.django_db +@pytest.mark.parametrize("url_prefix", url_prefixes) +def test_export_refunds_omit_invalid_bic(client, env, url_prefix): + d = env[2].info_data + d['bic'] = 'TROLOLO' + env[2].info = json.dumps(d) + env[2].save() + client.login(email='dummy@dummy.dummy', password='dummy') + r = client.get(f'{url_prefix}banktransfer/refunds/') + assert r.status_code == 200 + r = client.post(f'{url_prefix}banktransfer/refunds/', {"unite_transactions": True}, follow=True) + assert r.status_code == 200 + refund = RefundExport.objects.last() + assert refund is not None + assert b"Download CSV" in r.content + r = client.get(f'{url_prefix}banktransfer/export/{refund.id}/') + assert r.status_code == 200 + r = "".join(str(part) for part in r.streaming_content) + assert "DE27520521540534534466" in r + assert "TROLOLO" not in r + assert "HELADEF" not in r + r = client.post(f'{url_prefix}banktransfer/sepa-export/{refund.id}/', { + "account_holder": "Fission Festival", + "iban": "DE71720690050653667120", + "bic": "GENODEF1AIL", + }) + assert r.status_code == 200 + r = "".join(str(part) for part in r.streaming_content) + assert "DE27520521540534534466" in r + assert "TROLOLO" not in r def test_unite_transaction_rows():