diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 8a0b6aa1d6..b977f08259 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -169,6 +169,22 @@ class BasePaymentProvider: label=_('Enable payment method'), required=False, )), + ('_availability_date', + RelativeDateField( + label=_('Available until'), + help_text=_('Users will not be able to choose this payment provider after the given date.'), + required=False, + )), + ('_invoice_text', + I18nFormField( + label=_('Text on invoices'), + help_text=_('Will be printed just below the payment figures and above the closing text on invoices. ' + 'This will only be used if the invoice is generated before the order is paid. If the ' + 'invoice is generated later, it will show a text stating that it has already been paid.'), + required=False, + widget=I18nTextarea, + widget_kwargs={'attrs': {'rows': '2'}} + )), ('_fee_abs', forms.DecimalField( label=_('Additional fee'), @@ -187,12 +203,6 @@ class BasePaymentProvider: localize=True, required=False, )), - ('_availability_date', - RelativeDateField( - label=_('Available until'), - help_text=_('Users will not be able to choose this payment provider after the given date.'), - required=False, - )), ('_fee_reverse_calc', forms.BooleanField( label=_('Calculate the fee from the total value including the fee.'), @@ -202,16 +212,6 @@ class BasePaymentProvider: 'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'), required=False )), - ('_invoice_text', - I18nFormField( - label=_('Text on invoices'), - help_text=_('Will be printed just below the payment figures and above the closing text on invoices. ' - 'This will only be used if the invoice is generated before the order is paid. If the ' - 'invoice is generated later, it will show a text stating that it has already been paid.'), - required=False, - widget=I18nTextarea, - widget_kwargs={'attrs': {'rows': '2'}} - )), ]) def settings_content_render(self, request: HttpRequest) -> str: @@ -220,7 +220,7 @@ class BasePaymentProvider: page, this method is called. It may return HTML containing additional information that is displayed below the form fields configured in ``settings_form_fields``. """ - pass + return "" def render_invoice_text(self, order: Order) -> str: """ diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 059e554115..e7f2076875 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -411,7 +411,10 @@ class EventSettingsForm(SettingsForm): class PaymentSettingsForm(SettingsForm): payment_term_days = forms.IntegerField( label=_('Payment term in days'), - help_text=_("The number of days after placing an order the user has to pay to preserve their reservation."), + help_text=_("The number of days after placing an order the user has to pay to preserve their reservation. If " + "you use slow payment methods like bank transfer, we recommend 14 days. If you only use real-time " + "payment methods, we recommend still setting two or three days to allow people to retry failed " + "payments."), ) payment_term_last = RelativeDateField( label=_('Last date of payments'), diff --git a/src/pretix/control/templates/pretixcontrol/event/payment.html b/src/pretix/control/templates/pretixcontrol/event/payment.html index fd1e2f0ae2..c2c56d6ec9 100644 --- a/src/pretix/control/templates/pretixcontrol/event/payment.html +++ b/src/pretix/control/templates/pretixcontrol/event/payment.html @@ -5,49 +5,53 @@
{% csrf_token %}
- {% trans "Payment settings" %} - {% bootstrap_field sform.payment_term_days layout="control" %} - {% bootstrap_field sform.payment_term_last layout="control" %} - {% bootstrap_field sform.payment_term_weekdays layout="control" %} - {% bootstrap_field sform.payment_term_expire_automatically layout="control" %} - {% bootstrap_field sform.payment_term_accept_late layout="control" %} - {% bootstrap_field sform.tax_rate_default layout="control" %} + {% trans "Payment providers" %} + + + {% for provider in providers %} + + + + + + {% empty %} + + + + {% endfor %} + +
+ {{ provider.verbose_name }} + + {% if provider.is_enabled %} + + + {% trans "Enabled" %} + + {% else %} + + + {% trans "Disabled" %} + + {% endif %} + + + + {% trans "Settings" %} + +
+ {% trans "There are no payment providers available. Please go to the plugin settings and activate one or more payment plugins." %} +
- {% trans "Payment providers" %} -
- - {% trans "Warning:" %} - {% blocktrans trimmed %} - Please note that EU Directive 2015/2366 bans surcharging payment fees for most common payment - methods within the European Union. Depending on the payment method, this might affect - selling to consumers only or to business customers as well. Depending on your country, this - legislation might already be in effect or become relevant from January 2018 at the latest. This - is not legal advice. If in doubt, consult a lawyer or refrain from charging payment fees. - {% endblocktrans %} -
- {% for provider in providers %} -
- -
-
- {% bootstrap_form provider.form layout='control' %} - {% with c=provider.settings_content %} - {% if c %}{{ c|safe }}{% endif %} - {% endwith %} -
-
-
- {% empty %} - {% trans "There are no payment providers available. Please go to the plugin settings and activate one or more payment plugins." %} - {% endfor %} + {% trans "General payment settings" %} + {% bootstrap_field form.payment_term_days layout="control" %} + {% bootstrap_field form.payment_term_last layout="control" %} + {% bootstrap_field form.payment_term_weekdays layout="control" %} + {% bootstrap_field form.payment_term_expire_automatically layout="control" %} + {% bootstrap_field form.payment_term_accept_late layout="control" %} + {% bootstrap_field form.tax_rate_default layout="control" %}
+
+
+{% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 05407d9287..4f961ad87b 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -76,6 +76,8 @@ urlpatterns = [ url(r'^settings/$', event.EventUpdate.as_view(), name='event.settings'), url(r'^settings/plugins$', event.EventPlugins.as_view(), name='event.settings.plugins'), url(r'^settings/permissions$', event.EventPermissions.as_view(), name='event.settings.permissions'), + url(r'^settings/payment/(?P[^/]+)$', event.PaymentProviderSettings.as_view(), + name='event.settings.payment.provider'), url(r'^settings/payment$', event.PaymentSettings.as_view(), name='event.settings.payment'), url(r'^settings/tickets$', event.TicketSettings.as_view(), name='event.settings.tickets'), url(r'^settings/tickets/preview/(?P[^/]+)$', event.TicketSettingsPreview.as_view(), diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index c6f16713ca..f41cb9d841 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -237,83 +237,11 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat }) -class PaymentSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin): +class PaymentProviderSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin): model = Event context_object_name = 'event' permission = 'can_change_event_settings' - template_name = 'pretixcontrol/event/payment.html' - - def get_object(self, queryset=None) -> Event: - return self.request.event - - @cached_property - def provider_forms(self) -> list: - providers = [] - for provider in self.request.event.get_payment_providers().values(): - provider.form = ProviderForm( - obj=self.request.event, - settingspref=provider.settings.get_prefix(), - data=(self.request.POST if self.request.method == 'POST' else None) - ) - provider.form.fields = OrderedDict( - [ - ('%s%s' % (provider.settings.get_prefix(), k), v) - for k, v in provider.settings_form_fields.items() - ] - ) - provider.settings_content = provider.settings_content_render(self.request) - provider.form.prepare_fields() - if provider.settings_content or provider.form.fields: - # Exclude providers which do not provide any settings - providers.append(provider) - return providers - - def get_context_data(self, *args, **kwargs) -> dict: - context = super().get_context_data(*args, **kwargs) - context['sform'] = self.sform - return context - - @cached_property - def sform(self): - return PaymentSettingsForm( - obj=self.object, - prefix='settings', - data=self.request.POST if self.request.method == 'POST' else None - ) - - def get(self, request, *args, **kwargs): - self.object = self.get_object() - context = self.get_context_data(object=self.object) - context['providers'] = self.provider_forms - return self.render_to_response(context) - - @transaction.atomic - def post(self, request, *args, **kwargs): - self.object = self.get_object() - success = self.sform.is_valid() - if success: - self.sform.save() - if self.sform.has_changed(): - self.request.event.log_action('pretix.event.settings', user=self.request.user, data={ - k: self.request.event.settings.get(k) for k in self.sform.changed_data - }) - for provider in self.provider_forms: - if provider.form.is_valid(): - if provider.form.has_changed(): - self.request.event.log_action( - 'pretix.event.payment.provider.' + provider.identifier, user=self.request.user, data={ - k: provider.form.cleaned_data.get(k) for k in provider.form.changed_data - } - ) - provider.form.save() - else: - success = False - if success: - messages.success(self.request, _('Your changes have been saved.')) - return redirect(self.get_success_url()) - else: - messages.error(self.request, _('We could not save your changes. See below for details.')) - return self.get(request) + template_name = 'pretixcontrol/event/payment_provider.html' def get_success_url(self) -> str: return reverse('control:event.settings.payment', kwargs={ @@ -321,6 +249,63 @@ class PaymentSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, Temp 'event': self.get_object().slug, }) + @cached_property + def object(self): + return self.request.event + + def get_object(self, queryset=None): + return self.object + + @cached_property + def provider(self): + provider = self.request.event.get_payment_providers()[self.kwargs['provider']] + if not provider: + raise Http404() + return provider + + @cached_property + def form(self): + form = ProviderForm( + obj=self.request.event, + settingspref=self.provider.settings.get_prefix(), + data=(self.request.POST if self.request.method == 'POST' else None) + ) + form.fields = OrderedDict( + [ + ('%s%s' % (self.provider.settings.get_prefix(), k), v) + for k, v in self.provider.settings_form_fields.items() + ] + ) + form.prepare_fields() + return form + + @cached_property + def settings_content(self): + return self.provider.settings_content_render(self.request) + + def get_context_data(self, *args, **kwargs) -> dict: + context = super().get_context_data(*args, **kwargs) + context['form'] = self.form + context['provider'] = self.provider + context['settings_content'] = self.settings_content + return context + + @transaction.atomic + def post(self, request, *args, **kwargs): + if self.form.is_valid(): + if self.form.has_changed(): + self.request.event.log_action( + 'pretix.event.payment.provider.' + self.provider.identifier, user=self.request.user, data={ + k: self.form.cleaned_data.get(k) for k in self.form.changed_data + } + ) + self.form.save() + messages.success(self.request, _('Your changes have been saved.')) + return redirect(self.get_success_url()) + else: + messages.error(self.request, _('We could not save your changes. See below for details.')) + return self.get(request) + class EventSettingsFormView(EventPermissionRequiredMixin, FormView): model = Event @@ -368,6 +353,27 @@ class EventSettingsFormView(EventPermissionRequiredMixin, FormView): return self.get(request) +class PaymentSettings(EventSettingsViewMixin, EventSettingsFormView): + template_name = 'pretixcontrol/event/payment.html' + form_class = PaymentSettingsForm + permission = 'can_change_event_settings' + + def get_success_url(self) -> str: + return reverse('control:event.settings.payment', kwargs={ + 'organizer': self.request.organizer.slug, + 'event': self.request.event.slug, + }) + + def get_context_data(self, *args, **kwargs) -> dict: + context = super().get_context_data(*args, **kwargs) + context['providers'] = sorted( + [p for p in self.request.event.get_payment_providers().values() + if not p.is_implicit and (p.settings_form_fields or p.settings_content_render(self.request))], + key=lambda s: s.verbose_name + ) + return context + + class InvoiceSettings(EventSettingsViewMixin, EventSettingsFormView): model = Event form_class = InvoiceSettingsForm diff --git a/src/pretix/plugins/banktransfer/payment.py b/src/pretix/plugins/banktransfer/payment.py index a5f39409fe..500465d3a5 100644 --- a/src/pretix/plugins/banktransfer/payment.py +++ b/src/pretix/plugins/banktransfer/payment.py @@ -32,9 +32,12 @@ class BankTransfer(BasePaymentProvider): ) }} ) - return OrderedDict( + d = OrderedDict( list(super().settings_form_fields.items()) + [('bank_details', form_field)] ) + d.move_to_end('bank_details', last=False) + d.move_to_end('_enabled', last=False) + return d def payment_form_render(self, request) -> str: template = get_template('pretixplugins/banktransfer/checkout_payment_form.html') diff --git a/src/pretix/plugins/paypal/payment.py b/src/pretix/plugins/paypal/payment.py index f10150c2a7..2df32474d1 100644 --- a/src/pretix/plugins/paypal/payment.py +++ b/src/pretix/plugins/paypal/payment.py @@ -41,8 +41,8 @@ class Paypal(BasePaymentProvider): @property def settings_form_fields(self): - return OrderedDict( - list(super().settings_form_fields.items()) + [ + d = OrderedDict( + [ ('endpoint', forms.ChoiceField( label=_('Endpoint'), @@ -68,8 +68,10 @@ class Paypal(BasePaymentProvider): max_length=80, min_length=80, )) - ] + ] + list(super().settings_form_fields.items()) ) + d.move_to_end('_enabled', False) + return d def settings_content_render(self, request): return "
%s
%s
" % ( diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py index 6db96e3290..bd84e3a344 100644 --- a/src/pretix/plugins/stripe/payment.py +++ b/src/pretix/plugins/stripe/payment.py @@ -74,24 +74,24 @@ class StripeSettingsHolder(BasePaymentProvider): @property def settings_form_fields(self): - return OrderedDict( - list(super().settings_form_fields.items()) + [ - ('secret_key', + d = OrderedDict( + [ + ('publishable_key', forms.CharField( - label=_('Secret key'), + label=_('Publishable key'), help_text=_('{text}').format( text=_('Click here for a tutorial on how to obtain the required keys'), docs_url='https://docs.pretix.eu/en/latest/user/payments/stripe.html' ), validators=( - StripeKeyValidator('sk_'), + StripeKeyValidator('pk_'), ), )), - ('publishable_key', + ('secret_key', forms.CharField( - label=_('Publishable key'), + label=_('Secret key'), validators=( - StripeKeyValidator('pk_'), + StripeKeyValidator('sk_'), ), )), ('ui', @@ -144,9 +144,12 @@ class StripeSettingsHolder(BasePaymentProvider): 'payments are not immediately confirmed but might take some time.'), required=False, )), - ] + ] + list(super().settings_form_fields.items()) ) + d.move_to_end('_enabled', last=False) + return d + class StripeMethod(BasePaymentProvider): identifier = '' diff --git a/src/pretix/static/pretixcontrol/scss/main.scss b/src/pretix/static/pretixcontrol/scss/main.scss index a867465e27..eebe950bf0 100644 --- a/src/pretix/static/pretixcontrol/scss/main.scss +++ b/src/pretix/static/pretixcontrol/scss/main.scss @@ -521,3 +521,7 @@ ul.pagination { .pagination-container { margin-bottom: 20px; } + +.table-payment-providers > tbody > tr > td { + vertical-align: middle; +} diff --git a/src/tests/control/test_events.py b/src/tests/control/test_events.py index 2e8aea8676..0f38dd8910 100644 --- a/src/tests/control/test_events.py +++ b/src/tests/control/test_events.py @@ -135,52 +135,38 @@ class EventsTest(SoupTest): doc = self.get_doc('/control/event/%s/%s/live/' % (self.orga1.slug, self.event1.slug)) assert len(doc.select(".btn-primary")) == 0 - def test_payment_settings(self): - tr19 = self.event1.tax_rules.create(rate=Decimal('19.00')) - self.get_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug)) - self.post_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug), { + def test_payment_settings_provider(self): + self.get_doc('/control/event/%s/%s/settings/payment/banktransfer' % (self.orga1.slug, self.event1.slug)) + self.post_doc('/control/event/%s/%s/settings/payment/banktransfer' % (self.orga1.slug, self.event1.slug), { 'payment_banktransfer__enabled': 'true', 'payment_banktransfer__fee_abs': '12.23', 'payment_banktransfer_bank_details_0': 'Test', - 'settings-payment_term_days': '2', - 'settings-tax_rate_default': tr19.pk, }) self.event1.settings.flush() assert self.event1.settings.get('payment_banktransfer__enabled', as_type=bool) assert self.event1.settings.get('payment_banktransfer__fee_abs', as_type=Decimal) == Decimal('12.23') - def test_payment_settings_dont_require_fields_of_inactive_providers(self): + def test_payment_settings(self): tr19 = self.event1.tax_rules.create(rate=Decimal('19.00')) - doc = self.post_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug), { - 'settings-tax_rate_default': tr19.pk, - 'settings-payment_term_days': '2' - }, follow=True) - assert doc.select('.alert-success') - - def test_payment_settings_require_fields_of_active_providers(self): - tr19 = self.event1.tax_rules.create(rate=Decimal('19.00')) - doc = self.post_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug), { - 'payment_banktransfer__enabled': 'true', - 'payment_banktransfer__fee_abs': '12.23', - 'settings-payment_term_days': '2', - 'settings-tax_rate_default': tr19.pk, + self.get_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug)) + self.post_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug), { + 'payment_term_days': '2', + 'tax_rate_default': tr19.pk, }) - assert doc.select('.alert-danger') + self.event1.settings.flush() + assert self.event1.settings.get('payment_term_days', as_type=int) == 2 def test_payment_settings_last_date_payment_after_presale_end(self): tr19 = self.event1.tax_rules.create(rate=Decimal('19.00')) self.event1.presale_end = datetime.datetime.now() self.event1.save(update_fields=['presale_end']) doc = self.post_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug), { - 'payment_banktransfer__enabled': 'true', - 'payment_banktransfer__fee_abs': '12.23', - 'payment_banktransfer_bank_details_0': 'Test', - 'settings-payment_term_days': '2', - 'settings-payment_term_last_0': 'absolute', - 'settings-payment_term_last_1': (self.event1.presale_end - datetime.timedelta(1)).strftime('%Y-%m-%d'), - 'settings-payment_term_last_2': '0', - 'settings-payment_term_last_3': 'date_from', - 'settings-tax_rate_default': tr19.pk, + 'payment_term_days': '2', + 'payment_term_last_0': 'absolute', + 'payment_term_last_1': (self.event1.presale_end - datetime.timedelta(1)).strftime('%Y-%m-%d'), + 'payment_term_last_2': '0', + 'payment_term_last_3': 'date_from', + 'tax_rate_default': tr19.pk, }) assert doc.select('.alert-danger') self.event1.presale_end = None @@ -191,15 +177,12 @@ class EventsTest(SoupTest): self.event1.presale_end = self.event1.date_from - datetime.timedelta(days=5) self.event1.save(update_fields=['presale_end']) doc = self.post_doc('/control/event/%s/%s/settings/payment' % (self.orga1.slug, self.event1.slug), { - 'payment_banktransfer__enabled': 'true', - 'payment_banktransfer__fee_abs': '12.23', - 'payment_banktransfer_bank_details_0': 'Test', - 'settings-payment_term_days': '2', - 'settings-payment_term_last_0': 'relative', - 'settings-payment_term_last_1': '', - 'settings-payment_term_last_2': '10', - 'settings-payment_term_last_3': 'date_from', - 'settings-tax_rate_default': tr19.pk, + 'payment_term_days': '2', + 'payment_term_last_0': 'relative', + 'payment_term_last_1': '', + 'payment_term_last_2': '10', + 'payment_term_last_3': 'date_from', + 'tax_rate_default': tr19.pk, }) assert doc.select('.alert-danger') self.event1.presale_end = None diff --git a/src/tests/plugins/paypal/test_settings.py b/src/tests/plugins/paypal/test_settings.py index 1f28c6d3c6..087d989635 100644 --- a/src/tests/plugins/paypal/test_settings.py +++ b/src/tests/plugins/paypal/test_settings.py @@ -27,6 +27,7 @@ def env(client): @pytest.mark.django_db def test_settings(env): client, event = env - response = client.get('/control/event/%s/%s/settings/payment' % (event.organizer.slug, event.slug), follow=True) + response = client.get('/control/event/%s/%s/settings/payment/paypal' % (event.organizer.slug, event.slug), + follow=True) assert response.status_code == 200 assert 'paypal__enabled' in response.rendered_content diff --git a/src/tests/plugins/stripe/test_settings.py b/src/tests/plugins/stripe/test_settings.py index dc84048fba..bc9d981b1d 100644 --- a/src/tests/plugins/stripe/test_settings.py +++ b/src/tests/plugins/stripe/test_settings.py @@ -43,7 +43,7 @@ def env(client): t.members.add(user) t.limit_events.add(event) client.force_login(user) - url = '/control/event/%s/%s/settings/payment' % (event.organizer.slug, event.slug) + url = '/control/event/%s/%s/settings/payment/stripe_settings' % (event.organizer.slug, event.slug) return client, event, url @@ -57,8 +57,8 @@ def test_settings(env): def _stripe_key_test(env, field, value, is_valid): client, event, url = env - data = {'payment_stripe_' + field: value} - response = client.post(url, data) + data = {'payment_stripe_' + field: value, 'payment_stripe__enabled': 'on'} + response = client.post(url, data, follow=True) if not is_valid: assert 'does not look valid' in response.rendered_content