diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py index 391992e208..f2321ae019 100644 --- a/src/pretix/base/forms/questions.py +++ b/src/pretix/base/forms/questions.py @@ -242,3 +242,12 @@ class BaseInvoiceAddressForm(forms.ModelForm): 'resolve this manually.')) else: self.instance.vat_id_validated = False + + +class BaseInvoiceNameForm(BaseInvoiceAddressForm): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for f in list(self.fields.keys()): + if f != 'name': + del self.fields[f] diff --git a/src/pretix/base/views/mixins.py b/src/pretix/base/views/mixins.py index 5760686a03..1092a28103 100644 --- a/src/pretix/base/views/mixins.py +++ b/src/pretix/base/views/mixins.py @@ -7,7 +7,7 @@ from django.db.models import Prefetch from django.utils.functional import cached_property from pretix.base.forms.questions import ( - BaseInvoiceAddressForm, BaseQuestionsForm, + BaseInvoiceAddressForm, BaseInvoiceNameForm, BaseQuestionsForm, ) from pretix.base.models import ( CartPosition, InvoiceAddress, OrderPosition, Question, QuestionAnswer, @@ -144,6 +144,7 @@ class BaseQuestionsViewMixin: class OrderQuestionsViewMixin(BaseQuestionsViewMixin): invoice_form_class = BaseInvoiceAddressForm + invoice_name_form_class = BaseInvoiceNameForm only_user_visible = True @cached_property @@ -184,6 +185,12 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin): @cached_property def invoice_form(self): + if not self.request.event.settings.invoice_address_asked and self.request.event.settings.invoice_name_required: + return self.invoice_name_form_class( + data=self.request.POST if self.request.method == "POST" else None, + event=self.request.event, + instance=self.invoice_address, validate_vat_id=False + ) return self.invoice_form_class( data=self.request.POST if self.request.method == "POST" else None, event=self.request.event, diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 88b18c58f0..a6517a1db9 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -520,8 +520,7 @@ class InvoiceSettingsForm(SettingsForm): label=_("Require customer name"), required=False, widget=forms.CheckboxInput( - attrs={'data-checkbox-dependency': '#id_invoice_address_asked', - 'data-inverse-dependency': '#id_invoice_address_required'} + attrs={'data-inverse-dependency': '#id_invoice_address_required'} ), ) invoice_address_vatid = forms.BooleanField( diff --git a/src/pretix/control/templates/pretixcontrol/order/change_questions.html b/src/pretix/control/templates/pretixcontrol/order/change_questions.html index 9c1f587f26..2a3c696e5c 100644 --- a/src/pretix/control/templates/pretixcontrol/order/change_questions.html +++ b/src/pretix/control/templates/pretixcontrol/order/change_questions.html @@ -18,7 +18,7 @@
{% csrf_token %}
- {% if request.event.settings.invoice_address_asked %} + {% if request.event.settings.invoice_address_asked or order.invoice_address or request.event.settings.invoice_name_required %}

diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index bd862f6354..04e45a3f23 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -349,7 +349,7 @@

{% eventsignal event "pretix.control.signals.order_info" order=order request=request %}
-
+

@@ -371,7 +371,7 @@

- {% if request.event.settings.invoice_address_asked %} + {% if request.event.settings.invoice_address_asked or order.invoice_address %}
diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index 5eb9de2273..aef32bfd7a 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -17,7 +17,7 @@ from pretix.base.services.cart import ( from pretix.base.services.orders import perform_order from pretix.multidomain.urlreverse import eventreverse from pretix.presale.forms.checkout import ( - AddOnsForm, ContactForm, InvoiceAddressForm, + AddOnsForm, ContactForm, InvoiceAddressForm, InvoiceNameForm, ) from pretix.presale.signals import ( checkout_confirm_messages, checkout_flow_steps, contact_form_fields, @@ -322,6 +322,12 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): @cached_property def invoice_form(self): + if not self.request.event.settings.invoice_address_asked and self.request.event.settings.invoice_name_required: + return InvoiceNameForm(data=self.request.POST if self.request.method == "POST" else None, + event=self.request.event, + request=self.request, + instance=self.invoice_address, + validate_vat_id=False) return InvoiceAddressForm(data=self.request.POST if self.request.method == "POST" else None, event=self.request.event, request=self.request, @@ -331,7 +337,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): def post(self, request): self.request = request failed = not self.save() or not self.contact_form.is_valid() - if request.event.settings.invoice_address_asked: + if request.event.settings.invoice_address_asked or self.request.event.settings.invoice_name_required: failed = failed or not self.invoice_form.is_valid() if failed: messages.error(request, @@ -339,7 +345,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): return self.render() self.cart_session['email'] = self.contact_form.cleaned_data['email'] self.cart_session['contact_form_data'] = self.contact_form.cleaned_data - if request.event.settings.invoice_address_asked: + if request.event.settings.invoice_address_asked or self.request.event.settings.invoice_name_required: addr = self.invoice_form.save() self.cart_session['invoice_address'] = addr.pk @@ -369,8 +375,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): messages.warning(request, _('Please enter your invoicing address.')) return False - if request.event.settings.invoice_address_asked and request.event.settings.invoice_name_required and ( - not self.invoice_address or not self.invoice_address.name): + if request.event.settings.invoice_name_required and (not self.invoice_address or not self.invoice_address.name): messages.warning(request, _('Please enter your name.')) return False diff --git a/src/pretix/presale/forms/checkout.py b/src/pretix/presale/forms/checkout.py index 882c769c0d..230de222b6 100644 --- a/src/pretix/presale/forms/checkout.py +++ b/src/pretix/presale/forms/checkout.py @@ -61,6 +61,15 @@ class InvoiceAddressForm(BaseInvoiceAddressForm): vat_warning = True +class InvoiceNameForm(InvoiceAddressForm): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for f in list(self.fields.keys()): + if f != 'name': + del self.fields[f] + + class QuestionsForm(BaseQuestionsForm): """ This form class is responsible for asking order-related questions. This includes diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html b/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html index 578903e885..d6f42c9435 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html @@ -120,6 +120,12 @@
+ {% if not event.settings.invoice_address_asked and event.settings.invoice_name_required %} +
+
{% trans "Name" %}
+
{{ addr.name }}
+
+ {% endif %} {% for l, v in contact_info %}
{{ l }}
diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html b/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html index f9f123647e..8676da65d1 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html @@ -21,6 +21,9 @@
{% bootstrap_form contact_form layout="horizontal" %} + {% if not event.settings.invoice_address_asked and event.settings.invoice_name_required %} + {% bootstrap_form invoice_form layout="horizontal" %} + {% endif %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/order.html b/src/pretix/presale/templates/pretixpresale/event/order.html index 63c44ace2d..3ab1180438 100644 --- a/src/pretix/presale/templates/pretixpresale/event/order.html +++ b/src/pretix/presale/templates/pretixpresale/event/order.html @@ -120,7 +120,7 @@ {% eventsignal event "pretix.presale.signals.order_info" order=order %}
{% if invoices %} -
+

@@ -141,7 +141,7 @@

{% elif can_generate_invoice %} -
+

@@ -160,7 +160,7 @@

{% endif %} - {% if request.event.settings.invoice_address_asked %} + {% if request.event.settings.invoice_address_asked or request.event.settings.invoice_name_required %}
@@ -173,27 +173,35 @@
{% endif %}

- {% trans "Invoice information" %} + {% if request.event.settings.invoice_address_asked %} + {% trans "Invoice information" %} + {% else %} + {% trans "Contact information" %} + {% endif %}

-
{% trans "Company" %}
-
{{ order.invoice_address.company }}
+ {% if request.event.settings.invoice_address_asked %} +
{% trans "Company" %}
+
{{ order.invoice_address.company }}
+ {% endif %}
{% trans "Name" %}
{{ order.invoice_address.name }}
-
{% trans "Address" %}
-
{{ order.invoice_address.street|linebreaksbr }}
-
{% trans "ZIP code and city" %}
-
{{ order.invoice_address.zipcode }} {{ order.invoice_address.city }}
-
{% trans "Country" %}
-
{{ order.invoice_address.country.name|default:order.invoice_address.country_old }}
- {% if request.event.settings.invoice_address_vatid %} -
{% trans "VAT ID" %}
-
{{ order.invoice_address.vat_id }}
+ {% if request.event.settings.invoice_address_asked %} +
{% trans "Address" %}
+
{{ order.invoice_address.street|linebreaksbr }}
+
{% trans "ZIP code and city" %}
+
{{ order.invoice_address.zipcode }} {{ order.invoice_address.city }}
+
{% trans "Country" %}
+
{{ order.invoice_address.country.name|default:order.invoice_address.country_old }}
+ {% if request.event.settings.invoice_address_vatid %} +
{% trans "VAT ID" %}
+
{{ order.invoice_address.vat_id }}
+ {% endif %} +
{% trans "Internal Reference" %}
+
{{ order.invoice_address.internal_reference }}
{% endif %} -
{% trans "Internal Reference" %}
-
{{ order.invoice_address.internal_reference }}
diff --git a/src/pretix/presale/templates/pretixpresale/event/order_modify.html b/src/pretix/presale/templates/pretixpresale/event/order_modify.html index ec38b7e848..e30f65bf17 100644 --- a/src/pretix/presale/templates/pretixpresale/event/order_modify.html +++ b/src/pretix/presale/templates/pretixpresale/event/order_modify.html @@ -11,19 +11,27 @@ {% csrf_token %}
- {% if event.settings.invoice_address_asked %} -
- {% blocktrans trimmed %} - Modifying your invoice address will not automatically generate a new invoice. - Please contact us if you need a new invoice. - {% endblocktrans %} -
+ {% if event.settings.invoice_address_asked or event.settings.invoice_name_required %} + {% if event.settings.invoice_address_asked %} +
+ {% blocktrans trimmed %} + Modifying your invoice address will not automatically generate a new invoice. + Please contact us if you need a new invoice. + {% endblocktrans %} +
+ {% endif %}

- {% trans "Invoice information" %}{% if not event.settings.invoice_address_required %} - {% trans "(optional)" %} - {% endif %} + + {% if request.event.settings.invoice_address_asked %} + {% trans "Invoice information" %}{% if not event.settings.invoice_address_required %} + {% trans "(optional)" %} + {% endif %} + {% else %} + {% trans "Contact information" %} + {% endif %} +

diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index caf6c63f33..b3c7321d02 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -582,6 +582,95 @@ class CheckoutTestCase(TestCase): cr1 = CartPosition.objects.get(id=cr1.id) self.assertIsNone(cr1.attendee_name) + def test_invoice_address_required(self): + self.event.settings.invoice_address_asked = True + self.event.settings.invoice_address_required = True + + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertEqual(len(doc.select('input[name=city]')), 1) + + # Not all required fields filled out, expect failure + response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { + 'is_business': 'business', + 'city': 'Here', + 'country': 'DE', + 'vat_id': 'DE123456', + 'email': 'admin@localhost' + }, follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertGreaterEqual(len(doc.select('.has-error')), 1) + + # Corrected request + response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { + 'is_business': 'business', + 'company': 'Foo', + 'name': 'Bar', + 'street': 'Baz', + 'zipcode': '12345', + 'city': 'Here', + 'country': 'DE', + 'vat_id': 'DE123456', + 'email': 'admin@localhost' + }, follow=True) + self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), + target_status_code=200) + + def test_invoice_address_optional(self): + self.event.settings.invoice_address_asked = True + self.event.settings.invoice_address_required = False + + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertEqual(len(doc.select('input[name=city]')), 1) + + # Not all required fields filled out, expect failure + response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { + 'is_business': 'business', + 'city': 'Here', + 'country': 'DE', + 'vat_id': 'DE123456', + 'email': 'admin@localhost' + }, follow=True) + self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), + target_status_code=200) + + def test_invoice_name_required(self): + self.event.settings.invoice_address_asked = False + self.event.settings.invoice_name_required = True + + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertEqual(len(doc.select('input[name=name]')), 1) + self.assertEqual(len(doc.select('input[name=street]')), 0) + + # Not all required fields filled out, expect failure + response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { + 'email': 'admin@localhost' + }, follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertGreaterEqual(len(doc.select('.has-error')), 1) + + # Corrected request + response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { + 'name': 'Raphael', + 'email': 'admin@localhost' + }, follow=True) + self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), + target_status_code=200) + def test_payment(self): # TODO: Test for correct payment method fees self.event.settings.set('payment_stripe__enabled', True)