diff --git a/src/pretix/helpers/payment.py b/src/pretix/helpers/payment.py new file mode 100644 index 0000000000..8d19d56fd3 --- /dev/null +++ b/src/pretix/helpers/payment.py @@ -0,0 +1,210 @@ +# +# This file is part of pretix (Community Edition). +# +# Copyright (C) 2014-2020 Raphael Michel and contributors +# Copyright (C) 2020-today pretix GmbH and contributors +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General +# Public License as published by the Free Software Foundation in version 3 of the License. +# +# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are +# applicable granting you additional permissions and placing additional restrictions on your usage of this software. +# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive +# this file, see . +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along with this program. If not, see +# . +# +from urllib.parse import quote, urlencode + +import text_unidecode +from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ + + +def dotdecimal(value): + return str(value).replace(",", ".") + + +def commadecimal(value): + return str(value).replace(".", ",") + + +def generate_payment_qr_codes( + event, + code, + amount, + bank_details_sepa_bic, + bank_details_sepa_name, + bank_details_sepa_iban, +): + out = [] + for method in [ + swiss_qrbill, + czech_spayd, + euro_epc_qr, + euro_bezahlcode, + ]: + data = method( + event, + code, + amount, + bank_details_sepa_bic, + bank_details_sepa_name, + bank_details_sepa_iban, + ) + if data: + out.append(data) + + return out + + +def euro_epc_qr( + event, + code, + amount, + bank_details_sepa_bic, + bank_details_sepa_name, + bank_details_sepa_iban, +): + if event.currency != 'EUR' or not bank_details_sepa_iban: + return + + return { + "id": "girocode", + "label": "EPC-QR", + "qr_data": "\n".join(text_unidecode.unidecode(str(d or '')) for d in [ + "BCD", # Service Tag: ‘BCD’ + "002", # Version: V2 + "2", # Character set: ISO 8859-1 + "SCT", # Identification code: ‘SCT‘ + bank_details_sepa_bic, # AT-23 BIC of the Beneficiary Bank + bank_details_sepa_name, # AT-21 Name of the Beneficiary + bank_details_sepa_iban, # AT-20 Account number of the Beneficiary + f"{event.currency}{dotdecimal(amount)}", # AT-04 Amount of the Credit Transfer in Euro + "", # AT-44 Purpose of the Credit Transfer + "", # AT-05 Remittance Information (Structured) + code, # AT-05 Remittance Information (Unstructured) + "", # Beneficiary to originator information + "", + ]), + } + + +def euro_bezahlcode( + event, + code, + amount, + bank_details_sepa_bic, + bank_details_sepa_name, + bank_details_sepa_iban, +): + if not bank_details_sepa_iban or bank_details_sepa_iban[:2] != 'DE': + return + if event.currency != 'EUR': + return + + qr_data = "bank://singlepaymentsepa?" + urlencode({ + "name": str(bank_details_sepa_name), + "iban": str(bank_details_sepa_iban), + "bic": str(bank_details_sepa_bic), + "amount": commadecimal(amount), + "reason": str(code), + "currency": str(event.currency), + }, quote_via=quote) + return { + "id": "bezahlcode", + "label": "BezahlCode", + "qr_data": mark_safe(qr_data), + "link": qr_data, + "link_aria_label": _("Open BezahlCode in your banking app to start the payment process."), + } + + +def swiss_qrbill( + event, + code, + amount, + bank_details_sepa_bic, + bank_details_sepa_name, + bank_details_sepa_iban, +): + if not bank_details_sepa_iban or not bank_details_sepa_iban[:2] in ('CH', 'LI'): + return + if event.currency not in ('EUR', 'CHF'): + return + if not event.settings.invoice_address_from or not event.settings.invoice_address_from_country: + return + + data_fields = [ + 'SPC', + '0200', + '1', + bank_details_sepa_iban, + 'K', + bank_details_sepa_name[:70], + event.settings.invoice_address_from.replace('\n', ', ')[:70], + (event.settings.invoice_address_from_zipcode + ' ' + event.settings.invoice_address_from_city)[:70], + '', + '', + str(event.settings.invoice_address_from_country), + '', # rfu + '', # rfu + '', # rfu + '', # rfu + '', # rfu + '', # rfu + '', # rfu + str(amount), + event.currency, + '', # debtor address + '', # debtor address + '', # debtor address + '', # debtor address + '', # debtor address + '', # debtor address + '', # debtor address + 'NON', + '', # structured reference + code, + 'EPD', + ] + + data_fields = [text_unidecode.unidecode(d or '') for d in data_fields] + qr_data = '\r\n'.join(data_fields) + return { + "id": "qrbill", + "label": "QR-bill", + "html_prefix": mark_safe( + '' + '' + '' + ), + "qr_data": qr_data, + "css_class": "banktransfer-swiss-cross-overlay", + } + + +def czech_spayd( + event, + code, + amount, + bank_details_sepa_bic, + bank_details_sepa_name, + bank_details_sepa_iban, +): + if not bank_details_sepa_iban or not bank_details_sepa_iban[:2] in ('CZ', 'SK'): + return + if event.currency not in ('EUR', 'CZK'): + return + + qr_data = f"SPD*1.0*ACC:{bank_details_sepa_iban}*AM:{dotdecimal(amount)}*CC:{event.currency}*MSG:{code}" + return { + "id": "spayd", + "label": "SPAYD", + "qr_data": qr_data, + } diff --git a/src/pretix/plugins/banktransfer/payment.py b/src/pretix/plugins/banktransfer/payment.py index 380c31a12d..c53aaa339a 100644 --- a/src/pretix/plugins/banktransfer/payment.py +++ b/src/pretix/plugins/banktransfer/payment.py @@ -46,12 +46,12 @@ from i18nfield.forms import I18nTextInput from i18nfield.strings import LazyI18nString from localflavor.generic.forms import BICFormField, IBANFormField from localflavor.generic.validators import IBANValidator -from text_unidecode import unidecode from pretix.base.forms import I18nMarkdownTextarea from pretix.base.models import InvoiceAddress, Order, OrderPayment, OrderRefund from pretix.base.payment import BasePaymentProvider from pretix.base.templatetags.money import money_filter +from pretix.helpers.payment import generate_payment_qr_codes from pretix.plugins.banktransfer.templatetags.ibanformat import ibanformat from pretix.presale.views.cart import cart_session @@ -313,51 +313,6 @@ class BankTransfer(BasePaymentProvider): t += str(self.settings.get('bank_details', as_type=LazyI18nString)) return t - def swiss_qrbill(self, payment): - if not self.settings.get('bank_details_sepa_iban') or not self.settings.get('bank_details_sepa_iban')[:2] in ('CH', 'LI'): - return - if self.event.currency not in ('EUR', 'CHF'): - return - if not self.event.settings.invoice_address_from or not self.event.settings.invoice_address_from_country: - return - - data_fields = [ - 'SPC', - '0200', - '1', - self.settings.get('bank_details_sepa_iban'), - 'K', - self.settings.get('bank_details_sepa_name')[:70], - self.event.settings.invoice_address_from.replace('\n', ', ')[:70], - (self.event.settings.invoice_address_from_zipcode + ' ' + self.event.settings.invoice_address_from_city)[:70], - '', - '', - str(self.event.settings.invoice_address_from_country), - '', # rfu - '', # rfu - '', # rfu - '', # rfu - '', # rfu - '', # rfu - '', # rfu - str(payment.amount), - self.event.currency, - '', # debtor address - '', # debtor address - '', # debtor address - '', # debtor address - '', # debtor address - '', # debtor address - '', # debtor address - 'NON', - '', # structured reference - self._code(payment.order), - 'EPD', - ] - - data_fields = [unidecode(d or '') for d in data_fields] - return '\r\n'.join(data_fields) - def payment_pending_render(self, request: HttpRequest, payment: OrderPayment): template = get_template('pretixplugins/banktransfer/pending.html') ctx = { @@ -367,13 +322,18 @@ class BankTransfer(BasePaymentProvider): 'amount': payment.amount, 'payment_info': payment.info_data, 'settings': self.settings, - 'swiss_qrbill': self.swiss_qrbill(payment), - 'eu_barcodes': self.event.currency == 'EUR', + 'payment_qr_codes': generate_payment_qr_codes( + event=self.event, + code=self._code(payment.order), + amount=payment.amount, + bank_details_sepa_bic=self.settings.get('bank_details_sepa_bic'), + bank_details_sepa_name=self.settings.get('bank_details_sepa_name'), + bank_details_sepa_iban=self.settings.get('bank_details_sepa_iban'), + ) if self.settings.bank_details_type == "sepa" else None, 'pending_description': self.settings.get('pending_description', as_type=LazyI18nString), 'details': self.settings.get('bank_details', as_type=LazyI18nString), 'has_invoices': payment.order.invoices.exists(), } - ctx['any_barcodes'] = ctx['swiss_qrbill'] or ctx['eu_barcodes'] return template.render(ctx, request=request) def payment_control_render(self, request: HttpRequest, payment: OrderPayment) -> str: diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html index 028d90bac2..5d25bad1b1 100644 --- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html +++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html @@ -1,7 +1,6 @@ {% load i18n %} {% load l10n %} {% load commadecimal %} -{% load static %} {% load dotdecimal %} {% load ibanformat %} {% load money %} @@ -17,7 +16,7 @@ {% endblocktrans %}

-
+
{% trans "Reference code (important):" %}
{{ code }}
{% trans "Amount:" %}
{{ amount|money:event.currency }}
@@ -36,94 +35,7 @@ {% trans "We will send you an email as soon as we received your payment." %}

- {% if settings.bank_details_type == "sepa" and any_barcodes %} - + {% if payment_qr_codes %} + {% include "pretixpresale/event/payment_qr_codes.html" %} {% endif %} -
-{% if swiss_qrbill %} - -{% endif %} +
\ No newline at end of file diff --git a/src/pretix/presale/templates/pretixpresale/event/payment_qr_codes.html b/src/pretix/presale/templates/pretixpresale/event/payment_qr_codes.html new file mode 100644 index 0000000000..9c1500f082 --- /dev/null +++ b/src/pretix/presale/templates/pretixpresale/event/payment_qr_codes.html @@ -0,0 +1,44 @@ +{% load i18n %} +{% load static %} + +{% if payment_qr_codes %} + + {% for code_info in payment_qr_codes %} + {% if code_info.id == "qrbill" %} + + {% endif %} + {% endfor %} +{% endif %} \ No newline at end of file diff --git a/src/tests/helpers/test_payment.py b/src/tests/helpers/test_payment.py new file mode 100644 index 0000000000..82ab754d69 --- /dev/null +++ b/src/tests/helpers/test_payment.py @@ -0,0 +1,141 @@ +# +# This file is part of pretix (Community Edition). +# +# Copyright (C) 2014-2020 Raphael Michel and contributors +# Copyright (C) 2020-today pretix GmbH and contributors +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General +# Public License as published by the Free Software Foundation in version 3 of the License. +# +# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are +# applicable granting you additional permissions and placing additional restrictions on your usage of this software. +# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive +# this file, see . +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along with this program. If not, see +# . +# +from datetime import timedelta +from decimal import Decimal + +import pytest +from django.utils.timezone import now + +from pretix.base.models import Event, Organizer +from pretix.helpers.payment import generate_payment_qr_codes + + +@pytest.fixture +def env(): + o = Organizer.objects.create(name='Verein für Testzwecke e.V.', slug='testverein') + event = Event.objects.create( + organizer=o, name='Testveranstaltung', slug='testveranst', + date_from=now() + timedelta(days=10), + live=True, is_public=False, currency='EUR', + ) + event.settings.invoice_address_from = 'Verein für Testzwecke e.V.' + event.settings.invoice_address_from_zipcode = '1234' + event.settings.invoice_address_from_city = 'Testhausen' + event.settings.invoice_address_from_country = 'CH' + + return o, event + + +@pytest.mark.django_db +def test_payment_qr_codes_euro(env): + o, event = env + codes = generate_payment_qr_codes( + event=event, + code='TESTVERANST-12345', + amount=Decimal('123.00'), + bank_details_sepa_bic='BYLADEM1MIL', + bank_details_sepa_iban='DE37796500000069799047', + bank_details_sepa_name='Verein für Testzwecke e.V.', + ) + assert len(codes) == 2 + assert codes[0]['label'] == 'EPC-QR' + assert codes[0]['qr_data'] == '''BCD +002 +2 +SCT +BYLADEM1MIL +Verein fur Testzwecke e.V. +DE37796500000069799047 +EUR123.00 + + +TESTVERANST-12345 + +''' + + assert codes[1]['label'] == 'BezahlCode' + assert codes[1]['qr_data'] == ('bank://singlepaymentsepa?name=Verein%20f%C3%BCr%20Testzwecke%20e.V.&iban=DE37796500000069799047' + '&bic=BYLADEM1MIL&amount=123%2C00&reason=TESTVERANST-12345¤cy=EUR') + + +@pytest.mark.django_db +def test_payment_qr_codes_swiss(env): + o, event = env + codes = generate_payment_qr_codes( + event=event, + code='TESTVERANST-12345', + amount=Decimal('123.00'), + bank_details_sepa_bic='TESTCHXXXXX', + bank_details_sepa_iban='CH6389144757654882127', + bank_details_sepa_name='Verein für Testzwecke e.V.', + ) + assert codes[0]['label'] == 'QR-bill' + assert codes[0]['qr_data'] == "\r\n".join([ + "SPC", + "0200", + "1", + "CH6389144757654882127", + "K", + "Verein fur Testzwecke e.V.", + "Verein fur Testzwecke e.V.", + "1234 Testhausen", + "", + "", + "CH", + "", + "", + "", + "", + "", + "", + "", + "123.00", + "EUR", + "", + "", + "", + "", + "", + "", + "", + "NON", + "", + "TESTVERANST-12345", + "EPD", + ]) + + +@pytest.mark.django_db +def test_payment_qr_codes_spayd(env): + o, event = env + codes = generate_payment_qr_codes( + event=event, + code='TESTVERANST-12345', + amount=Decimal('123.00'), + bank_details_sepa_bic='TESTCZXXXXX', + bank_details_sepa_iban='CZ7450513769129174398769', + bank_details_sepa_name='Verein für Testzwecke e.V.', + ) + assert len(codes) == 2 + assert codes[0]['label'] == 'SPAYD' + assert codes[0]['qr_data'] == 'SPD*1.0*ACC:CZ7450513769129174398769*AM:123.00*CC:EUR*MSG:TESTVERANST-12345' + assert codes[1]['label'] == 'EPC-QR'