diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 0efc9f8d1..d2eaee486 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -1676,7 +1676,7 @@ class OrderPayment(models.Model): """ Marks the order as failed and sets info to ``info``, but only if the order is in ``created`` or ``pending`` state. This is equivalent to setting ``state`` to ``OrderPayment.PAYMENT_STATE_FAILED`` and logging a failure, - but it adds strong database logging since we do not want to report a failure for an order that has just + but it adds strong database locking since we do not want to report a failure for an order that has just been marked as paid. :param send_mail: Whether an email should be sent to the user about this event (default: ``True``). """ diff --git a/src/pretix/plugins/banktransfer/payment.py b/src/pretix/plugins/banktransfer/payment.py index 0ad0e66a2..e20bb8eff 100644 --- a/src/pretix/plugins/banktransfer/payment.py +++ b/src/pretix/plugins/banktransfer/payment.py @@ -535,9 +535,11 @@ class BankTransfer(BasePaymentProvider): 'eu_barcodes': self.event.currency == 'EUR', '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(), + 'invoice_email_enabled': self.settings.get('invoice_email', as_type=bool), } ctx['any_barcodes'] = ctx['swiss_qrbill'] or ctx['eu_barcodes'] - return template.render(ctx) + return template.render(ctx, request=request) def payment_control_render(self, request: HttpRequest, payment: OrderPayment) -> str: warning = None diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html index 9523482c3..e7ab5660c 100644 --- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html +++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html @@ -7,6 +7,7 @@ {% load money %} {% load unidecode %} {% load rich_text %} +{% load eventurl %} {% if pending_description %} {{ pending_description|rich_text }} @@ -103,3 +104,28 @@ SCT {% if swiss_qrbill %} {% endif %} + +{% if invoice_email_enabled and has_invoices %} +
+ {% csrf_token %} +

+ {% blocktrans trimmed %} + To send the invoice directly to your accounting department, please enter their email address: + {% endblocktrans %} +

+
+
+ + +
+
+ +
+
+
+
+{% endif %} \ No newline at end of file diff --git a/src/pretix/plugins/banktransfer/urls.py b/src/pretix/plugins/banktransfer/urls.py index 968d0a9f1..b1ad0ee01 100644 --- a/src/pretix/plugins/banktransfer/urls.py +++ b/src/pretix/plugins/banktransfer/urls.py @@ -19,13 +19,19 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # -from django.urls import re_path +from django.urls import include, re_path from pretix.api.urls import orga_router from pretix.plugins.banktransfer.api import BankImportJobViewSet from . import views +event_patterns = [ + re_path(r'^banktransfer/', include([ + re_path(r'^(?P[^/][^w]+)/(?P[A-Za-z0-9]+)/mail-invoice/$', views.SendInvoiceMailView.as_view(), name='mail_invoice'), + ])), +] + urlpatterns = [ re_path(r'^control/organizer/(?P[^/]+)/banktransfer/import/', views.OrganizerImportView.as_view(), diff --git a/src/pretix/plugins/banktransfer/views.py b/src/pretix/plugins/banktransfer/views.py index 57c2457de..e4598fa65 100644 --- a/src/pretix/plugins/banktransfer/views.py +++ b/src/pretix/plugins/banktransfer/views.py @@ -44,14 +44,18 @@ from typing import Set from django import forms from django.contrib import messages +from django.core.exceptions import ValidationError +from django.core.validators import validate_email from django.db import transaction from django.db.models import Count, Q, QuerySet -from django.http import FileResponse, JsonResponse +from django.http import FileResponse, Http404, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse +from django.utils.decorators import method_decorator from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import gettext as _ +from django.views.decorators.clickjacking import xframe_options_exempt from django.views.generic import DetailView, FormView, ListView, View from django.views.generic.detail import SingleObjectMixin from localflavor.generic.forms import BICFormField, IBANFormField @@ -75,6 +79,8 @@ from pretix.plugins.banktransfer.refund_export import ( build_sepa_xml, get_refund_export_csv, ) from pretix.plugins.banktransfer.tasks import process_banktransfers +from pretix.presale.views import EventViewMixin +from pretix.presale.views.order import OrderDetailMixin logger = logging.getLogger('pretix.plugins.banktransfer') @@ -886,3 +892,36 @@ class OrganizerSepaXMLExportView(OrganizerPermissionRequiredMixin, OrganizerDeta organizer=self.request.organizer, pk=self.kwargs.get('id') ) + + +@method_decorator(xframe_options_exempt, 'dispatch') +class SendInvoiceMailView(EventViewMixin, OrderDetailMixin, View): + def post(self, request, *args, **kwargs): + if not self.order: + raise Http404(_('Unknown order code or not authorized to access this order.')) + try: + validate_email(request.POST['email']) + except ValidationError: + messages.error(request, _('Please enter a valid email address.')) + return redirect(self.get_order_url()) + + last_payment = self.order.payments.last() + if (not last_payment + or last_payment.provider != BankTransfer.identifier + or last_payment.state != OrderPayment.PAYMENT_STATE_CREATED): + messages.error(request, _('No pending bank transfer payment found. Maybe the order has been paid already?')) + return redirect(self.get_order_url()) + if not last_payment.payment_provider.settings.get('invoice_email', as_type=bool): + messages.error(request, _('Sending invoices via email is disabled by the event organizer.')) + return redirect(self.get_order_url()) + + last_invoice = self.order.invoices.last() + if not last_invoice: + messages.error(request, _('No invoice found, please request an invoice first.')) + return redirect(self.get_order_url()) + + provider = last_payment.payment_provider + provider.send_invoice_to_alternate_email(self.order, last_invoice, request.POST['email']) + + messages.success(request, _('Sending the latest invoice via e-mail to {email}.').format(email=request.POST['email'])) + return redirect(self.get_order_url()) diff --git a/src/tests/plugins/banktransfer/test_presale.py b/src/tests/plugins/banktransfer/test_presale.py new file mode 100644 index 000000000..7cdfc36af --- /dev/null +++ b/src/tests/plugins/banktransfer/test_presale.py @@ -0,0 +1,91 @@ +# +# This file is part of pretix (Community Edition). +# +# Copyright (C) 2014-2020 Raphael Michel and contributors +# Copyright (C) 2020-2021 rami.io 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 +# . +# + +# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of +# the Apache License 2.0 can be obtained at . +# +# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A +# full history of changes and contributors is available at . +# +# This file contains Apache-licensed contributions copyrighted by: Flavia Bastos +# +# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under the License. + +from django.core import mail as djmail +from django_countries.fields import Country +from django_scopes import scopes_disabled +from tests.presale.test_orders import BaseOrdersTest + +from pretix.base.models import InvoiceAddress, OrderPayment +from pretix.base.services.invoices import generate_invoice + + +class BanktransferOrdersTest(BaseOrdersTest): + def test_unknown_order(self): + response = self.client.post( + '/%s/%s/banktransfer/ABCDE/123/mail-invoice/' % (self.orga.slug, self.event.slug) + ) + assert response.status_code == 404 + response = self.client.post( + '/%s/%s/banktransfer/%s/123/mail-invoice/' % (self.orga.slug, self.event.slug, self.not_my_order.code) + ) + assert response.status_code == 404 + + def test_order_with_no_invoice(self): + djmail.outbox = [] + response = self.client.post( + '/%s/%s/banktransfer/%s/%s/mail-invoice/' % ( + self.orga.slug, self.event.slug, self.order.code, self.order.secret), + {'email': 'test@example.org'} + ) + assert response.status_code == 302 + + from django.contrib.messages import get_messages + messages = list(get_messages(response.wsgi_request)) + assert len(messages) == 1 + assert str(messages[0]) == 'No pending bank transfer payment found. Maybe the order has been paid already?' + assert len(djmail.outbox) == 0 + + def test_valid_order(self): + with scopes_disabled(): + self.event.settings.set('payment_banktransfer_invoice_email', True) + self.order.payments.create(provider='banktransfer', state=OrderPayment.PAYMENT_STATE_CREATED, + amount=self.order.total) + InvoiceAddress.objects.create(order=self.order, company="Sample company", country=Country('NZ')) + generate_invoice(self.order) + + djmail.outbox = [] + response = self.client.post( + '/%s/%s/banktransfer/%s/%s/mail-invoice/' % ( + self.orga.slug, self.event.slug, self.order.code, self.order.secret), + {'email': 'test@example.org'} + ) + assert response.status_code == 302 + + from django.contrib.messages import get_messages + messages = list(get_messages(response.wsgi_request)) + assert len(messages) == 1 + assert str(messages[0]) == 'Sending the latest invoice via e-mail to test@example.org.' + + assert len(djmail.outbox) == 1