From 243e4ac4c8a20430066b8aa1dc082987faae5ad5 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 4 Apr 2019 09:48:59 +0200 Subject: [PATCH] Allow not to ask for invoice addresses on free orders --- src/pretix/base/settings.py | 4 +++ src/pretix/base/views/mixins.py | 25 ++++++++++---- src/pretix/control/forms/event.py | 4 +++ src/pretix/control/forms/item.py | 1 + .../pretixcontrol/event/invoicing.html | 1 + .../pretixcontrol/order/change_questions.html | 2 +- src/pretix/control/views/orders.py | 3 +- src/pretix/presale/checkoutflow.py | 34 +++++++++++++++---- .../pretixpresale/event/checkout_confirm.html | 6 ++-- .../event/checkout_questions.html | 4 +-- .../templates/pretixpresale/event/order.html | 10 +++--- .../pretixpresale/event/order_modify.html | 6 ++-- src/pretix/presale/views/__init__.py | 28 +++++++++++++++ src/pretix/presale/views/order.py | 6 +++- src/tests/presale/test_checkout.py | 21 ++++++++++++ 15 files changed, 125 insertions(+), 30 deletions(-) diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 276c1667dd..9074037bc4 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -49,6 +49,10 @@ DEFAULTS = { 'default': 'True', 'type': bool, }, + 'invoice_address_not_asked_free': { + 'default': 'False', + 'type': bool, + }, 'invoice_name_required': { 'default': 'False', 'type': bool, diff --git a/src/pretix/base/views/mixins.py b/src/pretix/base/views/mixins.py index 0ace5d315f..645c24b306 100644 --- a/src/pretix/base/views/mixins.py +++ b/src/pretix/base/views/mixins.py @@ -1,5 +1,6 @@ import json from collections import OrderedDict +from decimal import Decimal from django import forms from django.core.files.uploadedfile import UploadedFile @@ -186,25 +187,35 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin): except InvoiceAddress.DoesNotExist: return InvoiceAddress(order=self.order) + @cached_property + def address_asked(self): + return self.request.event.settings.invoice_address_asked and ( + self.order.total != Decimal('0.00') or not self.request.event.settings.invoice_address_not_asked_free + ) + @cached_property def invoice_form(self): - if not self.request.event.settings.invoice_address_asked and self.request.event.settings.invoice_name_required: + if not self.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, all_optional=self.all_optional ) - return self.invoice_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, - all_optional=self.all_optional, - ) + if self.address_asked: + return self.invoice_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, + all_optional=self.all_optional, + ) + else: + return forms.Form(data=self.request.POST if self.request.method == "POST" else None) def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['order'] = self.order ctx['formgroups'] = self.formdict.items() ctx['invoice_form'] = self.invoice_form + ctx['invoice_address_asked'] = self.address_asked return ctx diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 6169368dcf..f0e1ad5550 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -621,6 +621,10 @@ class InvoiceSettingsForm(SettingsForm): widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}), required=False ) + invoice_address_not_asked_free = forms.BooleanField( + label=_('Do not ask for invoice address if an order is free'), + required=False + ) invoice_include_free = forms.BooleanField( label=_("Show free products on invoices"), help_text=_("Note that invoices will never be generated for orders that contain only free " diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index 7cc2843fc9..4e22d3318c 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -43,6 +43,7 @@ class QuestionForm(I18nModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['items'].queryset = self.instance.event.items.all() + self.fields['items'].required = True self.fields['dependency_question'].queryset = self.instance.event.questions.filter( type__in=(Question.TYPE_BOOLEAN, Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE) ) diff --git a/src/pretix/control/templates/pretixcontrol/event/invoicing.html b/src/pretix/control/templates/pretixcontrol/event/invoicing.html index 63aa7e7da5..a1f0902f3a 100644 --- a/src/pretix/control/templates/pretixcontrol/event/invoicing.html +++ b/src/pretix/control/templates/pretixcontrol/event/invoicing.html @@ -24,6 +24,7 @@ {% bootstrap_field form.invoice_address_company_required layout="control" %} {% bootstrap_field form.invoice_address_vatid layout="control" %} {% bootstrap_field form.invoice_address_beneficiary layout="control" %} + {% bootstrap_field form.invoice_address_not_asked_free layout="control" %}
{% trans "Your invoice details" %} diff --git a/src/pretix/control/templates/pretixcontrol/order/change_questions.html b/src/pretix/control/templates/pretixcontrol/order/change_questions.html index 2a3c696e5c..4322b416da 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 or order.invoice_address or request.event.settings.invoice_name_required %} + {% if invoice_address_asked or order.invoice_address or request.event.settings.invoice_name_required %}

diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 6d0be55975..5800f5ad84 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -1305,7 +1305,8 @@ class OrderModifyInformation(OrderQuestionsViewMixin, OrderView): messages.error(self.request, _("We had difficulties processing your input. Please review the errors below.")) return self.get(request, *args, **kwargs) - self.invoice_form.save() + if hasattr(self.invoice_form, 'save'): + self.invoice_form.save() self.order.log_action('pretix.event.order.modified', { 'invoice_data': self.invoice_form.cleaned_data, 'data': [{ diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index 347d0db3e7..2ed6f20e40 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -28,7 +28,9 @@ from pretix.presale.signals import ( checkout_all_optional, checkout_confirm_messages, checkout_flow_steps, contact_form_fields, order_meta_from_request, question_form_fields, ) -from pretix.presale.views import CartMixin, get_cart, get_cart_total +from pretix.presale.views import ( + CartMixin, get_cart, get_cart_is_free, get_cart_total, +) from pretix.presale.views.cart import ( cart_session, create_empty_cart_id, get_or_create_cart_id, ) @@ -351,7 +353,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): 'city': self.cart_session.get('widget_data', {}).get('invoice-address-city', ''), 'country': self.cart_session.get('widget_data', {}).get('invoice-address-country', ''), } - if not self.request.event.settings.invoice_address_asked and self.request.event.settings.invoice_name_required: + if not self.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, @@ -365,10 +367,17 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): instance=self.invoice_address, validate_vat_id=self.eu_reverse_charge_relevant, all_optional=self.all_optional) + @cached_property + def address_asked(self): + return ( + self.request.event.settings.invoice_address_asked + and (not self.request.event.settings.invoice_address_not_asked_free or not get_cart_is_free(self.request)) + ) + 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 or self.request.event.settings.invoice_name_required: + if self.address_asked or self.request.event.settings.invoice_name_required: failed = failed or not self.invoice_form.is_valid() if failed: messages.error(request, @@ -376,7 +385,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 or self.request.event.settings.invoice_name_required: + if self.address_asked or self.request.event.settings.invoice_name_required: addr = self.invoice_form.save() self.cart_session['invoice_address'] = addr.pk @@ -404,9 +413,11 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): return False if not self.all_optional: - if request.event.settings.invoice_address_required and (not self.invoice_address or not self.invoice_address.street): - messages.warning(request, _('Please enter your invoicing address.')) - return False + + if self.address_asked: + if request.event.settings.invoice_address_required and (not self.invoice_address or not self.invoice_address.street): + messages.warning(request, _('Please enter your invoicing address.')) + return False 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.')) @@ -471,6 +482,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): ctx['reverse_charge_relevant'] = self.eu_reverse_charge_relevant ctx['cart'] = self.get_cart() ctx['cart_session'] = self.cart_session + ctx['invoice_address_asked'] = self.address_asked return ctx @@ -591,6 +603,13 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): def is_completed(self, request, warn=False): pass + @cached_property + def address_asked(self): + return ( + self.request.event.settings.invoice_address_asked + and (not self.request.event.settings.invoice_address_not_asked_free or not get_cart_is_free(self.request)) + ) + def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['cart'] = self.get_cart(answers=True) @@ -601,6 +620,7 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep): ctx['addr'] = self.invoice_address ctx['confirm_messages'] = self.confirm_messages ctx['cart_session'] = self.cart_session + ctx['invoice_address_asked'] = self.address_asked email = self.cart_session.get('contact_form_data', {}).get('email') if email != settings.PRETIX_EMAIL_NONE_VALUE: diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html b/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html index 69b214276f..63e739d862 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_confirm.html @@ -72,7 +72,7 @@ {% endif %} {% eventsignal event "pretix.presale.signals.checkout_confirm_page_content" request=request %}
- {% if request.event.settings.invoice_address_asked %} + {% if invoice_address_asked %}
@@ -111,7 +111,7 @@
{% endif %} -
+
@@ -125,7 +125,7 @@

- {% if not event.settings.invoice_address_asked and event.settings.invoice_name_required %} + {% if not asked and event.settings.invoice_name_required %}
{% trans "Name" %}
{{ addr.name }}
diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html b/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html index d60674608f..78ad4ec0df 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html @@ -22,13 +22,13 @@
{% bootstrap_form contact_form layout="horizontal" %} - {% if not event.settings.invoice_address_asked and event.settings.invoice_name_required %} + {% if not invoice_address_asked and event.settings.invoice_name_required %} {% bootstrap_form invoice_form layout="horizontal" %} {% endif %}
- {% if event.settings.invoice_address_asked %} + {% if invoice_address_asked %}

diff --git a/src/pretix/presale/templates/pretixpresale/event/order.html b/src/pretix/presale/templates/pretixpresale/event/order.html index 5f0290cb13..e6e1181c08 100644 --- a/src/pretix/presale/templates/pretixpresale/event/order.html +++ b/src/pretix/presale/templates/pretixpresale/event/order.html @@ -159,7 +159,7 @@ {% eventsignal event "pretix.presale.signals.order_info" order=order %}
{% if invoices %} -
+

@@ -180,7 +180,7 @@

{% elif can_generate_invoice %} -
+

@@ -199,7 +199,7 @@

{% endif %} - {% if request.event.settings.invoice_address_asked or request.event.settings.invoice_name_required %} + {% if invoice_address_asked or request.event.settings.invoice_name_required %}
@@ -221,13 +221,13 @@
- {% if request.event.settings.invoice_address_asked %} + {% if invoice_address_asked %}
{% trans "Company" %}
{{ order.invoice_address.company }}
{% endif %}
{% trans "Name" %}
{{ order.invoice_address.name }}
- {% if request.event.settings.invoice_address_asked %} + {% if invoice_address_asked %}
{% trans "Address" %}
{{ order.invoice_address.street|linebreaksbr }}
{% trans "ZIP code and city" %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/order_modify.html b/src/pretix/presale/templates/pretixpresale/event/order_modify.html index de1f718b06..7c7f483693 100644 --- a/src/pretix/presale/templates/pretixpresale/event/order_modify.html +++ b/src/pretix/presale/templates/pretixpresale/event/order_modify.html @@ -12,8 +12,8 @@ {% csrf_token %}
- {% if event.settings.invoice_address_asked or event.settings.invoice_name_required %} - {% if event.settings.invoice_address_asked %} + {% if invoice_address_asked or event.settings.invoice_name_required %} + {% if invoice_address_asked %}
{% blocktrans trimmed %} Modifying your invoice address will not automatically generate a new invoice. @@ -25,7 +25,7 @@

- {% if request.event.settings.invoice_address_asked %} + {% if invoice_address_asked %} {% trans "Invoice information" %}{% if not event.settings.invoice_address_required %} {% trans "(optional)" %} {% endif %} diff --git a/src/pretix/presale/views/__init__.py b/src/pretix/presale/views/__init__.py index ab7efeeb86..ac85c2c116 100644 --- a/src/pretix/presale/views/__init__.py +++ b/src/pretix/presale/views/__init__.py @@ -205,6 +205,34 @@ def get_cart_total(request): return request._cart_total_cache +def get_cart_invoice_address(request): + from pretix.presale.views.cart import cart_session + + if not hasattr(request, '_checkout_flow_invoice_address'): + cs = cart_session(request) + iapk = cs.get('invoice_address') + if not iapk: + request._checkout_flow_invoice_address = InvoiceAddress() + else: + try: + request._checkout_flow_invoice_address = InvoiceAddress.objects.get(pk=iapk, order__isnull=True) + except InvoiceAddress.DoesNotExist: + request._checkout_flow_invoice_address = InvoiceAddress() + return request._checkout_flow_invoice_address + + +def get_cart_is_free(request): + from pretix.presale.views.cart import cart_session + + if not hasattr(request, '_cart_free_cache'): + cs = cart_session(request) + ia = get_cart_invoice_address(request) + total = get_cart_total(request) + fees = get_fees(request.event, request, total, ia, cs.get('payment')) + request._cart_free_cache = total + sum(f.value for f in fees) == Decimal('0.00') + return request._cart_free_cache + + class EventViewMixin: def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index 12aba9eb94..3ff3e74e19 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -122,6 +122,9 @@ class OrderDetails(EventViewMixin, OrderDetailMixin, CartMixin, TemplateView): 'secret': self.order.secret } ) + ctx['invoice_address_asked'] = self.request.event.settings.invoice_address_asked and ( + self.order.total != Decimal('0.00') or not self.request.event.settings.invoice_address_not_asked_free + ) if self.order.status == Order.STATUS_PENDING: ctx['pending_sum'] = self.order.pending_sum @@ -526,7 +529,8 @@ class OrderModify(EventViewMixin, OrderDetailMixin, OrderQuestionsViewMixin, Tem messages.error(self.request, _("We had difficulties processing your input. Please review the errors below.")) return self.get(request, *args, **kwargs) - self.invoice_form.save() + if hasattr(self.invoice_form, 'save'): + self.invoice_form.save() self.order.log_action('pretix.event.order.modified', { 'invoice_data': self.invoice_form.cleaned_data, 'data': [{ diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index 19e92692d6..f6afcea89a 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -504,6 +504,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase): def test_invoice_address_required(self): self.event.settings.invoice_address_asked = True self.event.settings.invoice_address_required = True + self.event.settings.invoice_address_not_asked_free = True self.event.settings.set('name_scheme', 'title_given_middle_family') CartPosition.objects.create( @@ -552,6 +553,26 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase): } assert ia.name_cached == 'Mr John Kennedy' + def test_invoice_address_hidden_for_free(self): + self.event.settings.invoice_address_asked = True + self.event.settings.invoice_address_required = True + self.event.settings.invoice_address_not_asked_free = True + self.event.settings.set('name_scheme', 'title_given_middle_family') + + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=0, 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"]')), 0) + + response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), { + 'email': 'admin@localhost' + }, follow=True) + self.assertRedirects(response, '/%s/%s/checkout/confirm/' % (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