import copy import json import logging from decimal import Decimal from urllib.error import HTTPError import dateutil.parser import pycountry import pytz import vat_moss.errors import vat_moss.id from babel import Locale from django import forms from django.contrib import messages from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db.models import QuerySet from django.forms import Select from django.utils import translation from django.utils.formats import date_format from django.utils.html import escape from django.utils.safestring import mark_safe from django.utils.timezone import get_current_timezone from django.utils.translation import gettext_lazy as _, pgettext_lazy from django_countries import countries from django_countries.fields import Country, CountryField from phonenumber_field.formfields import PhoneNumberField from phonenumber_field.phonenumber import PhoneNumber from phonenumber_field.widgets import PhoneNumberPrefixWidget from phonenumbers import NumberParseException, national_significant_number from phonenumbers.data import _COUNTRY_CODE_TO_REGION_CODE from pretix.base.forms.widgets import ( BusinessBooleanRadio, DatePickerWidget, SplitDateTimePickerWidget, TimePickerWidget, UploadedFileWidget, ) from pretix.base.i18n import ( get_babel_locale, get_language_without_region, language, ) from pretix.base.models import InvoiceAddress, Question, QuestionOption from pretix.base.models.tax import ( EU_COUNTRIES, cc_to_vat_prefix, is_eu_country, ) from pretix.base.settings import ( COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SALUTATIONS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, ) from pretix.base.templatetags.rich_text import rich_text from pretix.control.forms import ExtFileField, SplitDateTimeField from pretix.helpers.countries import CachedCountries from pretix.helpers.escapejson import escapejson_attr from pretix.helpers.i18n import get_format_without_seconds from pretix.presale.signals import question_form_fields logger = logging.getLogger(__name__) REQUIRED_NAME_PARTS = ['salutation', 'given_name', 'family_name', 'full_name'] class NamePartsWidget(forms.MultiWidget): widget = forms.TextInput autofill_map = { 'given_name': 'given-name', 'family_name': 'family-name', 'middle_name': 'additional-name', 'title': 'honorific-prefix', 'full_name': 'name', 'calling_name': 'nickname', } def __init__(self, scheme: dict, field: forms.Field, attrs=None, titles: list=None): widgets = [] self.scheme = scheme self.field = field self.titles = titles for fname, label, size in self.scheme['fields']: a = copy.copy(attrs) or {} a['data-fname'] = fname if fname == 'title' and self.titles: widgets.append(Select(attrs=a, choices=[('', '')] + [(d, d) for d in self.titles[1]])) elif fname == 'salutation': widgets.append(Select(attrs=a, choices=[('', '---')] + [(s, s) for s in PERSON_NAME_SALUTATIONS])) else: widgets.append(self.widget(attrs=a)) super().__init__(widgets, attrs) def decompress(self, value): if value is None: return None data = [] for i, field in enumerate(self.scheme['fields']): fname, label, size = field data.append(value.get(fname, "")) if '_legacy' in value and not data[-1]: data[-1] = value.get('_legacy', '') return data def render(self, name: str, value, attrs=None, renderer=None) -> str: if not isinstance(value, list): value = self.decompress(value) output = [] final_attrs = self.build_attrs(attrs or dict()) if 'required' in final_attrs: del final_attrs['required'] id_ = final_attrs.get('id', None) for i, widget in enumerate(self.widgets): try: widget_value = value[i] except (IndexError, TypeError): widget_value = None if id_: these_attrs = dict( final_attrs, id='%s_%s' % (id_, i), title=self.scheme['fields'][i][1], placeholder=self.scheme['fields'][i][1], ) if self.scheme['fields'][i][0] in REQUIRED_NAME_PARTS: if self.field.required: these_attrs['required'] = 'required' these_attrs.pop('data-no-required-attr', None) these_attrs['autocomplete'] = (self.attrs.get('autocomplete', '') + ' ' + self.autofill_map.get(self.scheme['fields'][i][0], 'off')).strip() these_attrs['data-size'] = self.scheme['fields'][i][2] else: these_attrs = final_attrs output.append(widget.render(name + '_%s' % i, widget_value, these_attrs, renderer=renderer)) return mark_safe(self.format_output(output)) def format_output(self, rendered_widgets) -> str: return '
%s
' % ''.join(rendered_widgets) class NamePartsFormField(forms.MultiValueField): widget = NamePartsWidget def compress(self, data_list) -> dict: data = {} data['_scheme'] = self.scheme_name for i, value in enumerate(data_list): data[self.scheme['fields'][i][0]] = value or '' return data def __init__(self, *args, **kwargs): fields = [] defaults = { 'widget': self.widget, 'max_length': kwargs.pop('max_length', None), } self.scheme_name = kwargs.pop('scheme') self.titles = kwargs.pop('titles') self.scheme = PERSON_NAME_SCHEMES.get(self.scheme_name) if self.titles: self.scheme_titles = PERSON_NAME_TITLE_GROUPS.get(self.titles) else: self.scheme_titles = None self.one_required = kwargs.get('required', True) require_all_fields = kwargs.pop('require_all_fields', False) kwargs['required'] = False kwargs['widget'] = (kwargs.get('widget') or self.widget)( scheme=self.scheme, titles=self.scheme_titles, field=self, **kwargs.pop('widget_kwargs', {}) ) defaults.update(**kwargs) for fname, label, size in self.scheme['fields']: defaults['label'] = label if fname == 'title' and self.scheme_titles: d = dict(defaults) d.pop('max_length', None) field = forms.ChoiceField( **d, choices=[('', '')] + [(d, d) for d in self.scheme_titles[1]] ) elif fname == 'salutation': d = dict(defaults) d.pop('max_length', None) field = forms.ChoiceField( **d, choices=[('', '---')] + [(s, s) for s in PERSON_NAME_SALUTATIONS] ) else: field = forms.CharField(**defaults) field.part_name = fname fields.append(field) super().__init__( fields=fields, require_all_fields=False, *args, **kwargs ) self.require_all_fields = require_all_fields self.required = self.one_required def clean(self, value) -> dict: value = super().clean(value) if self.one_required and (not value or not any(v for v in value.values())): raise forms.ValidationError(self.error_messages['required'], code='required') if self.one_required: for k, v in value.items(): if k in REQUIRED_NAME_PARTS and not v: raise forms.ValidationError(self.error_messages['required'], code='required') if self.require_all_fields and not all(v for v in value): raise forms.ValidationError(self.error_messages['incomplete'], code='required') if sum(len(v) for v in value if v) > 250: raise forms.ValidationError(_('Please enter a shorter name.'), code='max_length') return value class WrappedPhonePrefixSelect(Select): initial = None def __init__(self, initial=None): choices = [("", "---------")] language = get_babel_locale() # changed from default implementation that used the django locale locale = Locale(translation.to_locale(language)) for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items(): prefix = "+%d" % prefix if initial and initial in values: self.initial = prefix for country_code in values: country_name = locale.territories.get(country_code) if country_name: choices.append((prefix, "{} {}".format(country_name, prefix))) super().__init__(choices=sorted(choices, key=lambda item: item[1])) def render(self, name, value, *args, **kwargs): return super().render(name, value or self.initial, *args, **kwargs) def get_context(self, name, value, attrs): if value and self.choices[1][0] != value: matching_choices = len([1 for p, c in self.choices if p == value]) if matching_choices > 1: # Some countries share a phone pretix, for example +1 is used all over the Americas. # This causes a UX problem: If the default value or the existing data is +12125552368, # the widget will just show the first without an explicit country. self.choices.insert(1, (value, value)) context = super().get_context(name, value, attrs) return context class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget): def __init__(self, attrs=None, initial=None): widgets = (WrappedPhonePrefixSelect(initial), forms.TextInput()) super(PhoneNumberPrefixWidget, self).__init__(widgets, attrs) def render(self, name, value, attrs=None, renderer=None): output = super().render(name, value, attrs, renderer) return mark_safe(self.format_output(output)) def format_output(self, rendered_widgets) -> str: return '
%s
' % ''.join(rendered_widgets) def decompress(self, value): """ If an incomplete phone number (e.g. without country prefix) is currently entered, the default implementation just discards the value and shows nothing at all. Let's rather show something invalid, so the user is prompted to fix it, instead of silently deleting data. """ if value: if type(value) == PhoneNumber: if value.country_code and value.national_number: return [ "+%d" % value.country_code, national_significant_number(value), ] return [ None, str(value) ] elif "." in value: return value.split(".") else: return [None, value] return [None, ""] def value_from_datadict(self, data, files, name): # In contrast to defualt implementation, do not silently fail if a number without # country prefix is entered values = super(PhoneNumberPrefixWidget, self).value_from_datadict(data, files, name) if values[1]: return "%s.%s" % tuple(values) return "" def guess_country(event): # Try to guess the initial country from either the country of the merchant # or the locale. This will hopefully save at least some users some scrolling :) locale = get_language_without_region() country = event.settings.region or event.settings.invoice_address_from_country if not country: valid_countries = countries.countries if '-' in locale: parts = locale.split('-') # TODO: does this actually work? if parts[1].upper() in valid_countries: country = Country(parts[1].upper()) elif parts[0].upper() in valid_countries: country = Country(parts[0].upper()) else: if locale.upper() in valid_countries: country = Country(locale.upper()) return country class QuestionCheckboxSelectMultiple(forms.CheckboxSelectMultiple): option_template_name = 'pretixbase/forms/widgets/checkbox_option_with_links.html' class MinDateValidator(MinValueValidator): def __call__(self, value): try: return super().__call__(value) except ValidationError as e: e.params['limit_value'] = date_format(e.params['limit_value'], 'SHORT_DATE_FORMAT') raise e class MinDateTimeValidator(MinValueValidator): def __call__(self, value): try: return super().__call__(value) except ValidationError as e: e.params['limit_value'] = date_format(e.params['limit_value'].astimezone(get_current_timezone()), 'SHORT_DATETIME_FORMAT') raise e class MaxDateValidator(MaxValueValidator): def __call__(self, value): try: return super().__call__(value) except ValidationError as e: e.params['limit_value'] = date_format(e.params['limit_value'], 'SHORT_DATE_FORMAT') raise e class MaxDateTimeValidator(MaxValueValidator): def __call__(self, value): try: return super().__call__(value) except ValidationError as e: e.params['limit_value'] = date_format(e.params['limit_value'].astimezone(get_current_timezone()), 'SHORT_DATETIME_FORMAT') raise e class BaseQuestionsForm(forms.Form): """ This form class is responsible for asking order-related questions. This includes the attendee name for admission tickets, if the corresponding setting is enabled, as well as additional questions defined by the organizer. """ def __init__(self, *args, **kwargs): """ Takes two additional keyword arguments: :param cartpos: The cart position the form should be for :param event: The event this belongs to """ cartpos = self.cartpos = kwargs.pop('cartpos', None) orderpos = self.orderpos = kwargs.pop('orderpos', None) pos = cartpos or orderpos item = pos.item questions = pos.item.questions_to_ask event = kwargs.pop('event') self.all_optional = kwargs.pop('all_optional', False) super().__init__(*args, **kwargs) add_fields = {} if item.admission and event.settings.attendee_names_asked: add_fields['attendee_name_parts'] = NamePartsFormField( max_length=255, required=event.settings.attendee_names_required and not self.all_optional, scheme=event.settings.name_scheme, titles=event.settings.name_scheme_titles, label=_('Attendee name'), initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts), ) if item.admission and event.settings.attendee_emails_asked: add_fields['attendee_email'] = forms.EmailField( required=event.settings.attendee_emails_required and not self.all_optional, label=_('Attendee email'), initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email), widget=forms.EmailInput( attrs={ 'autocomplete': 'email' } ) ) if item.admission and event.settings.attendee_company_asked: add_fields['company'] = forms.CharField( required=event.settings.attendee_company_required and not self.all_optional, label=_('Company'), max_length=255, initial=(cartpos.company if cartpos else orderpos.company), ) if item.admission and event.settings.attendee_addresses_asked: add_fields['street'] = forms.CharField( required=event.settings.attendee_addresses_required and not self.all_optional, label=_('Address'), widget=forms.Textarea(attrs={ 'rows': 2, 'placeholder': _('Street and Number'), 'autocomplete': 'street-address' }), initial=(cartpos.street if cartpos else orderpos.street), ) add_fields['zipcode'] = forms.CharField( required=event.settings.attendee_addresses_required and not self.all_optional, max_length=30, label=_('ZIP code'), initial=(cartpos.zipcode if cartpos else orderpos.zipcode), widget=forms.TextInput(attrs={ 'autocomplete': 'postal-code', }), ) add_fields['city'] = forms.CharField( required=event.settings.attendee_addresses_required and not self.all_optional, label=_('City'), max_length=255, initial=(cartpos.city if cartpos else orderpos.city), widget=forms.TextInput(attrs={ 'autocomplete': 'address-level2', }), ) country = (cartpos.country if cartpos else orderpos.country) or guess_country(event) add_fields['country'] = CountryField( countries=CachedCountries ).formfield( required=event.settings.attendee_addresses_required and not self.all_optional, label=_('Country'), initial=country, widget=forms.Select(attrs={ 'autocomplete': 'country', }), ) c = [('', pgettext_lazy('address', 'Select state'))] fprefix = str(self.prefix) + '-' if self.prefix is not None and self.prefix != '-' else '' cc = None if fprefix + 'country' in self.data: cc = str(self.data[fprefix + 'country']) elif country: cc = str(country) if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS: types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc] statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types] c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1]) elif fprefix + 'state' in self.data: self.data = self.data.copy() del self.data[fprefix + 'state'] add_fields['state'] = forms.ChoiceField( label=pgettext_lazy('address', 'State'), required=False, choices=c, widget=forms.Select(attrs={ 'autocomplete': 'address-level1', }), ) add_fields['state'].widget.is_required = True field_positions = list( [ (n, event.settings.system_question_order.get(n if n != 'state' else 'country', 0)) for n in add_fields.keys() ] ) for q in questions: # Do we already have an answer? Provide it as the initial value answers = [a for a in pos.answerlist if a.question_id == q.id] if answers: initial = answers[0] else: initial = None tz = pytz.timezone(event.settings.timezone) help_text = rich_text(q.help_text) label = escape(q.question) # django-bootstrap3 calls mark_safe required = q.required and not self.all_optional if q.type == Question.TYPE_BOOLEAN: if q.required: # For some reason, django-bootstrap3 does not set the required attribute # itself. widget = forms.CheckboxInput(attrs={'required': 'required'}) else: widget = forms.CheckboxInput() if initial: initialbool = (initial.answer == "True") else: initialbool = False field = forms.BooleanField( label=label, required=required, help_text=help_text, initial=initialbool, widget=widget, ) elif q.type == Question.TYPE_NUMBER: field = forms.DecimalField( label=label, required=required, min_value=q.valid_number_min or Decimal('0.00'), max_value=q.valid_number_max, help_text=q.help_text, initial=initial.answer if initial else None, ) elif q.type == Question.TYPE_STRING: field = forms.CharField( label=label, required=required, help_text=help_text, initial=initial.answer if initial else None, ) elif q.type == Question.TYPE_TEXT: field = forms.CharField( label=label, required=required, help_text=help_text, widget=forms.Textarea, initial=initial.answer if initial else None, ) elif q.type == Question.TYPE_COUNTRYCODE: field = CountryField( countries=CachedCountries, blank=True, null=True, blank_label=' ', ).formfield( label=label, required=required, help_text=help_text, widget=forms.Select, empty_label=' ', initial=initial.answer if initial else (guess_country(event) if required else None), ) elif q.type == Question.TYPE_CHOICE: field = forms.ModelChoiceField( queryset=q.options, label=label, required=required, help_text=help_text, widget=forms.Select, to_field_name='identifier', empty_label='', initial=initial.options.first() if initial else None, ) elif q.type == Question.TYPE_CHOICE_MULTIPLE: field = forms.ModelMultipleChoiceField( queryset=q.options, label=label, required=required, help_text=help_text, to_field_name='identifier', widget=QuestionCheckboxSelectMultiple, initial=initial.options.all() if initial else None, ) elif q.type == Question.TYPE_FILE: field = ExtFileField( label=label, required=required, help_text=help_text, initial=initial.file if initial else None, widget=UploadedFileWidget(position=pos, event=event, answer=initial), ext_whitelist=( ".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg", ".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages", ".bmp", ".tif", ".tiff" ), max_size=10 * 1024 * 1024, ) elif q.type == Question.TYPE_DATE: attrs = {} if q.valid_date_min: attrs['data-min'] = q.valid_date_min.isoformat() if q.valid_date_max: attrs['data-max'] = q.valid_date_max.isoformat() field = forms.DateField( label=label, required=required, help_text=help_text, initial=dateutil.parser.parse(initial.answer).date() if initial and initial.answer else None, widget=DatePickerWidget(attrs), ) if q.valid_date_min: field.validators.append(MinDateValidator(q.valid_date_min)) if q.valid_date_max: field.validators.append(MaxDateValidator(q.valid_date_max)) elif q.type == Question.TYPE_TIME: field = forms.TimeField( label=label, required=required, help_text=help_text, initial=dateutil.parser.parse(initial.answer).time() if initial and initial.answer else None, widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')), ) elif q.type == Question.TYPE_DATETIME: field = SplitDateTimeField( label=label, required=required, help_text=help_text, initial=dateutil.parser.parse(initial.answer).astimezone(tz) if initial and initial.answer else None, widget=SplitDateTimePickerWidget( time_format=get_format_without_seconds('TIME_INPUT_FORMATS'), min_date=q.valid_datetime_min, max_date=q.valid_datetime_max ), ) if q.valid_datetime_min: field.validators.append(MinDateTimeValidator(q.valid_datetime_min)) if q.valid_datetime_max: field.validators.append(MaxDateTimeValidator(q.valid_datetime_max)) elif q.type == Question.TYPE_PHONENUMBER: with language(get_babel_locale()): default_country = guess_country(event) default_prefix = None for prefix, values in _COUNTRY_CODE_TO_REGION_CODE.items(): if str(default_country) in values: default_prefix = prefix try: initial = PhoneNumber().from_string(initial.answer) if initial else "+{}.".format(default_prefix) except NumberParseException: initial = None field = PhoneNumberField( label=label, required=required, help_text=help_text, # We now exploit an implementation detail in PhoneNumberPrefixWidget to allow us to pass just # a country code but no number as an initial value. It's a bit hacky, but should be stable for # the future. initial=initial, widget=WrappedPhoneNumberPrefixWidget() ) field.question = q if answers: # Cache the answer object for later use field.answer = answers[0] if q.dependency_question_id: field.widget.attrs['data-question-dependency'] = q.dependency_question_id field.widget.attrs['data-question-dependency-values'] = escapejson_attr(json.dumps(q.dependency_values)) if q.type != 'M': field.widget.attrs['required'] = q.required and not self.all_optional field._required = q.required and not self.all_optional field.required = False add_fields['question_%s' % q.id] = field field_positions.append(('question_%s' % q.id, q.position)) field_positions.sort(key=lambda e: e[1]) for fname, p in field_positions: self.fields[fname] = add_fields[fname] responses = question_form_fields.send(sender=event, position=pos) data = pos.meta_info_data for r, response in sorted(responses, key=lambda r: str(r[0])): for key, value in response.items(): # We need to be this explicit, since OrderedDict.update does not retain ordering self.fields[key] = value value.initial = data.get('question_form_data', {}).get(key) for k, v in self.fields.items(): if v.widget.attrs.get('autocomplete') or k == 'attendee_name_parts': v.widget.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + v.widget.attrs.get('autocomplete', '') def clean(self): d = super().clean() if d.get('city') and d.get('country') and str(d['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS: if not d.get('state'): self.add_error('state', _('This field is required.')) question_cache = {f.question.pk: f.question for f in self.fields.values() if getattr(f, 'question', None)} def question_is_visible(parentid, qvals): if parentid not in question_cache: return False parentq = question_cache[parentid] if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values): return False if 'question_%d' % parentid not in d: return False dval = d.get('question_%d' % parentid) return ( ('True' in qvals and dval) or ('False' in qvals and not dval) or (isinstance(dval, QuestionOption) and dval.identifier in qvals) or (isinstance(dval, (list, QuerySet)) and any(qval in [o.identifier for o in dval] for qval in qvals)) ) def question_is_required(q): return ( q.required and (not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_values)) ) if not self.all_optional: for q in question_cache.values(): answer = d.get('question_%d' % q.pk) if question_is_required(q) and not answer and answer != 0: raise ValidationError({'question_%d' % q.pk: [_('This field is required')]}) return d class BaseInvoiceAddressForm(forms.ModelForm): vat_warning = False class Meta: model = InvoiceAddress fields = ('is_business', 'company', 'name_parts', 'street', 'zipcode', 'city', 'country', 'state', 'vat_id', 'internal_reference', 'beneficiary', 'custom_field') widgets = { 'is_business': BusinessBooleanRadio, 'street': forms.Textarea(attrs={ 'rows': 2, 'placeholder': _('Street and Number'), 'autocomplete': 'street-address' }), 'beneficiary': forms.Textarea(attrs={'rows': 3}), 'country': forms.Select(attrs={ 'autocomplete': 'country', }), 'zipcode': forms.TextInput(attrs={ 'autocomplete': 'postal-code', }), 'city': forms.TextInput(attrs={ 'autocomplete': 'address-level2', }), 'company': forms.TextInput(attrs={ 'data-display-dependency': '#id_is_business_1', 'autocomplete': 'organization', }), 'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1', 'data-countries-in-eu': ','.join(EU_COUNTRIES)}), 'internal_reference': forms.TextInput, } labels = { 'is_business': '' } def __init__(self, *args, **kwargs): self.event = event = kwargs.pop('event') self.request = kwargs.pop('request', None) self.validate_vat_id = kwargs.pop('validate_vat_id') self.all_optional = kwargs.pop('all_optional', False) kwargs.setdefault('initial', {}) if not kwargs.get('instance') or not kwargs['instance'].country: kwargs['initial']['country'] = guess_country(self.event) super().__init__(*args, **kwargs) if not event.settings.invoice_address_vatid: del self.fields['vat_id'] self.fields['country'].choices = CachedCountries() c = [('', pgettext_lazy('address', 'Select state'))] fprefix = self.prefix + '-' if self.prefix else '' cc = None if fprefix + 'country' in self.data: cc = str(self.data[fprefix + 'country']) elif 'country' in self.initial: cc = str(self.initial['country']) elif self.instance and self.instance.country: cc = str(self.instance.country) if cc and cc in COUNTRIES_WITH_STATE_IN_ADDRESS: types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[cc] statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types] c += sorted([(s.code[3:], s.name) for s in statelist], key=lambda s: s[1]) elif fprefix + 'state' in self.data: self.data = self.data.copy() del self.data[fprefix + 'state'] self.fields['state'] = forms.ChoiceField( label=pgettext_lazy('address', 'State'), required=False, choices=c, widget=forms.Select(attrs={ 'autocomplete': 'address-level1', }), ) self.fields['state'].widget.is_required = True # Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected. if cc and not is_eu_country(cc) and fprefix + 'vat_id' in self.data: self.data = self.data.copy() del self.data[fprefix + 'vat_id'] if not event.settings.invoice_address_required or self.all_optional: for k, f in self.fields.items(): f.required = False f.widget.is_required = False if 'required' in f.widget.attrs: del f.widget.attrs['required'] elif event.settings.invoice_address_company_required and not self.all_optional: self.initial['is_business'] = True self.fields['is_business'].widget = BusinessBooleanRadio(require_business=True) self.fields['company'].required = True self.fields['company'].widget.is_required = True self.fields['company'].widget.attrs['required'] = 'required' del self.fields['company'].widget.attrs['data-display-dependency'] self.fields['name_parts'] = NamePartsFormField( max_length=255, required=event.settings.invoice_name_required and not self.all_optional, scheme=event.settings.name_scheme, titles=event.settings.name_scheme_titles, label=_('Name'), initial=(self.instance.name_parts if self.instance else self.instance.name_parts), ) if event.settings.invoice_address_required and not event.settings.invoice_address_company_required and not self.all_optional: if not event.settings.invoice_name_required: self.fields['name_parts'].widget.attrs['data-required-if'] = '#id_is_business_0' self.fields['name_parts'].widget.attrs['data-no-required-attr'] = '1' self.fields['company'].widget.attrs['data-required-if'] = '#id_is_business_1' if not event.settings.invoice_address_beneficiary: del self.fields['beneficiary'] if event.settings.invoice_address_custom_field: self.fields['custom_field'].label = event.settings.invoice_address_custom_field else: del self.fields['custom_field'] for k, v in self.fields.items(): if v.widget.attrs.get('autocomplete') or k == 'name_parts': v.widget.attrs['autocomplete'] = 'section-invoice billing ' + v.widget.attrs.get('autocomplete', '') def clean(self): data = self.cleaned_data if not data.get('is_business'): data['company'] = '' data['vat_id'] = '' if data.get('is_business') and not is_eu_country(data.get('country')): data['vat_id'] = '' if self.event.settings.invoice_address_required: if data.get('is_business') and not data.get('company'): raise ValidationError(_('You need to provide a company name.')) if not data.get('is_business') and not data.get('name_parts'): raise ValidationError(_('You need to provide your name.')) if 'vat_id' in self.changed_data or not data.get('vat_id'): self.instance.vat_id_validated = False if data.get('city') and data.get('country') and str(data['country']) in COUNTRIES_WITH_STATE_IN_ADDRESS: if not data.get('state'): self.add_error('state', _('This field is required.')) self.instance.name_parts = data.get('name_parts') if all( not v for k, v in data.items() if k not in ('is_business', 'country', 'name_parts') ) and len(data.get('name_parts', {})) == 1: # Do not save the country if it is the only field set -- we don't know the user even checked it! self.cleaned_data['country'] = '' if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data: pass elif self.validate_vat_id and data.get('is_business') and is_eu_country(data.get('country')) and data.get('vat_id'): if data.get('vat_id')[:2] != cc_to_vat_prefix(str(data.get('country'))): raise ValidationError(_('Your VAT ID does not match the selected country.')) try: result = vat_moss.id.validate(data.get('vat_id')) if result: country_code, normalized_id, company_name = result self.instance.vat_id_validated = True self.instance.vat_id = normalized_id except (vat_moss.errors.InvalidError, ValueError): raise ValidationError(_('This VAT ID is not valid. Please re-check your input.')) except vat_moss.errors.WebServiceUnavailableError: logger.exception('VAT ID checking failed for country {}'.format(data.get('country'))) self.instance.vat_id_validated = False if self.request and self.vat_warning: messages.warning(self.request, _('Your VAT ID could not be checked, as the VAT checking service of ' 'your country is currently not available. We will therefore ' 'need to charge VAT on your invoice. You can get the tax amount ' 'back via the VAT reimbursement process.')) except (vat_moss.errors.WebServiceError, HTTPError): logger.exception('VAT ID checking failed for country {}'.format(data.get('country'))) self.instance.vat_id_validated = False if self.request and self.vat_warning: messages.warning(self.request, _('Your VAT ID could not be checked, as the VAT checking service of ' 'your country returned an incorrect result. We will therefore ' 'need to charge VAT on your invoice. Please contact support to ' '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_parts': del self.fields[f]