mirror of
https://github.com/pretix/pretix.git
synced 2025-12-22 16:52:27 +00:00
* Upgrade Django to 3.0 and other dependencies to recent versions * Fix otp version contsraint * Remove six dependency * Resolve some warnings * Fix failing tests * Update django-countries * Resolve all RemovedInDjango31Warnings in test suite * Run isort * Fix import * Update PostgreSQL version on travis
637 lines
28 KiB
Python
637 lines
28 KiB
Python
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 localedata
|
|
from django import forms
|
|
from django.contrib import messages
|
|
from django.core.exceptions import ValidationError
|
|
from django.db.models import QuerySet
|
|
from django.forms import Select
|
|
from django.utils.html import escape
|
|
from django.utils.safestring import mark_safe
|
|
from django.utils.translation import (
|
|
get_language, 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
|
|
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 language
|
|
from pretix.base.models import InvoiceAddress, Question, QuestionOption
|
|
from pretix.base.models.tax import EU_COUNTRIES, cc_to_vat_prefix
|
|
from pretix.base.settings import (
|
|
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SCHEMES,
|
|
PERSON_NAME_TITLE_GROUPS,
|
|
)
|
|
from pretix.base.templatetags.rich_text import rich_text
|
|
from pretix.control.forms import SplitDateTimeField
|
|
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 = ['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]]))
|
|
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 '<div class="nameparts-form-group">%s</div>' % ''.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]]
|
|
)
|
|
field.part_name = fname
|
|
fields.append(field)
|
|
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')
|
|
return value
|
|
|
|
|
|
class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
|
|
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 '<div class="nameparts-form-group">%s</div>' % ''.join(rendered_widgets)
|
|
|
|
|
|
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()
|
|
country = event.settings.invoice_address_from_country
|
|
if not country:
|
|
valid_countries = countries.countries
|
|
if '-' in locale:
|
|
parts = locale.split('-')
|
|
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 in valid_countries:
|
|
country = Country(locale.upper())
|
|
return country
|
|
|
|
|
|
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)
|
|
|
|
if item.admission and event.settings.attendee_names_asked:
|
|
self.fields['attendee_name_parts'] = NamePartsFormField(
|
|
max_length=255,
|
|
required=event.settings.attendee_names_required,
|
|
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:
|
|
self.fields['attendee_email'] = forms.EmailField(
|
|
required=event.settings.attendee_emails_required,
|
|
label=_('Attendee email'),
|
|
initial=(cartpos.attendee_email if cartpos else orderpos.attendee_email),
|
|
widget=forms.EmailInput(
|
|
attrs={
|
|
'autocomplete': 'email'
|
|
}
|
|
)
|
|
)
|
|
|
|
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,
|
|
help_text=q.help_text,
|
|
initial=initial.answer if initial else None,
|
|
min_value=Decimal('0.00'),
|
|
)
|
|
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().formfield(
|
|
label=label, required=required,
|
|
help_text=help_text,
|
|
widget=forms.Select,
|
|
empty_label='',
|
|
initial=initial.answer if initial 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=forms.CheckboxSelectMultiple,
|
|
initial=initial.options.all() if initial else None,
|
|
)
|
|
elif q.type == Question.TYPE_FILE:
|
|
field = forms.FileField(
|
|
label=label, required=required,
|
|
help_text=help_text,
|
|
initial=initial.file if initial else None,
|
|
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
|
|
)
|
|
elif q.type == Question.TYPE_DATE:
|
|
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(),
|
|
)
|
|
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')),
|
|
)
|
|
elif q.type == Question.TYPE_PHONENUMBER:
|
|
babel_locale = 'en'
|
|
# Babel, and therefore django-phonenumberfield, do not support our custom locales such das de_Informal
|
|
if localedata.exists(get_language()):
|
|
babel_locale = get_language()
|
|
elif localedata.exists(get_language()[:2]):
|
|
babel_locale = get_language()[:2]
|
|
with language(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
|
|
|
|
self.fields['question_%s' % q.id] = field
|
|
|
|
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()
|
|
|
|
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():
|
|
if question_is_required(q) and not d.get('question_%d' % q.pk):
|
|
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'}),
|
|
'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']
|
|
|
|
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
|
|
|
|
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']
|
|
if 'vat_id' in self.fields:
|
|
del self.fields['vat_id'].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'] = ''
|
|
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 data.get('country') in EU_COUNTRIES 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]
|