diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index 181839c1ac..10d0f44a47 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -177,6 +177,13 @@ price money (string) Price of this p attendee_name string Specified attendee name for this position (or ``null``) attendee_name_parts object of strings Decomposition of attendee name (i.e. given name, family name) attendee_email string Specified attendee email address for this position (or ``null``) +company string Attendee company name (or ``null``) +street string Attendee street (or ``null``) +zipcode string Attendee ZIP code (or ``null``) +city string Attendee city (or ``null``) +country string Attendee country code (or ``null``) +state string Attendee state (ISO 3166-2 code). Only supported in + AU, BR, CA, CN, MY, MX, and US, otherwise ``null``. voucher integer Internal ID of the voucher used for this position (or ``null``) tax_rate decimal (string) VAT rate applied for this position tax_value money (string) VAT included in this position @@ -240,6 +247,10 @@ pdf_data object Data object req The attribute ``canceled`` has been added. +.. versionchanged:: 3.8 + + The attributes ``company``, ``street``, ``zipcode``, ``city``, ``country``, and ``state`` have been added. + .. _order-payment-resource: Order payment resource @@ -384,6 +395,12 @@ List of all orders "full_name": "Peter", }, "attendee_email": null, + "company": "Sample company", + "street": "Test street 12", + "zipcode": "12345", + "city": "Testington", + "country": "DE", + "state": null, "voucher": null, "tax_rate": "0.00", "tax_value": "0.00", @@ -540,6 +557,12 @@ Fetching individual orders "full_name": "Peter", }, "attendee_email": null, + "company": "Sample company", + "street": "Test street 12", + "zipcode": "12345", + "city": "Testington", + "country": "DE", + "state": null, "voucher": null, "tax_rate": "0.00", "tax_rule": null, @@ -820,9 +843,9 @@ Creating orders * ``consume_carts`` (optional) – A list of cart IDs. All cart positions with these IDs will be deleted if the order creation is successful. Any quotas or seats that become free by this operation will be credited to your order creation. - * ``email`` + * ``email`` (optional) * ``locale`` - * ``sales_channel`` + * ``sales_channel`` (optional) * ``payment_provider`` (optional) – The identifier of the payment provider set for this order. This needs to be an existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"`` for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is @@ -855,15 +878,21 @@ Creating orders * ``positionid`` (optional, see below) * ``item`` - * ``variation`` + * ``variation`` (optional) * ``price`` (optional, if set to ``null`` or missing the price will be computed from the given product) * ``seat`` (The ``seat_guid`` attribute of a seat. Required when the specified ``item`` requires a seat, otherwise must be ``null``.) - * ``attendee_name`` **or** ``attendee_name_parts`` + * ``attendee_name`` **or** ``attendee_name_parts`` (optional) * ``voucher`` (optional, the ``code`` attribute of a valid voucher) - * ``attendee_email`` + * ``attendee_email`` (optional) + * ``company`` (optional) + * ``street`` (optional) + * ``zipcode`` (optional) + * ``city`` (optional) + * ``country`` (optional) + * ``state`` (optional) * ``secret`` (optional) * ``addon_to`` (optional, see below) - * ``subevent`` + * ``subevent`` (optional) * ``answers`` * ``question`` diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index 3ca522ae89..42b3285334 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -558,6 +558,10 @@ class EventSettingsSerializer(serializers.Serializer): 'attendee_names_required', 'attendee_emails_asked', 'attendee_emails_required', + 'attendee_addresses_asked', + 'attendee_addresses_required', + 'attendee_company_asked', + 'attendee_company_required', 'confirm_text', 'order_email_asked_twice', 'payment_term_days', diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index cdd0e9e857..2fee373264 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -39,7 +39,7 @@ class CompatibleCountryField(serializers.Field): def to_representation(self, instance: InvoiceAddress): if instance.country: return str(instance.country) - else: + elif hasattr(instance, 'country_old'): return instance.country_old @@ -211,10 +211,12 @@ class OrderPositionSerializer(I18nAwareModelSerializer): order = serializers.SlugRelatedField(slug_field='code', read_only=True) pdf_data = PdfDataSerializer(source='*') seat = InlineSeatSerializer(read_only=True) + country = CompatibleCountryField(source='*') class Meta: model = OrderPosition fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', + 'company', 'street', 'zipcode', 'city', 'country', 'state', 'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled') @@ -516,12 +518,22 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer): max_digits=10) voucher = serializers.SlugRelatedField(slug_field='code', queryset=Voucher.objects.none(), required=False, allow_null=True) + country = CompatibleCountryField(source='*') class Meta: model = OrderPosition fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email', + 'company', 'street', 'zipcode', 'city', 'country', 'state', 'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher') + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for k, v in self.fields.items(): + if k in ('company', 'street', 'zipcode', 'city', 'country', 'state'): + v.required = False + v.allow_blank = True + v.allow_null = True + def validate_secret(self, secret): if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists(): raise ValidationError( @@ -576,6 +588,24 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer): ) if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'): data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme + + if data.get('country'): + if not pycountry.countries.get(alpha_2=data.get('country')): + raise ValidationError( + {'country': ['Invalid country code.']} + ) + + if data.get('state'): + cc = str(data.get('country') or self.instance.country or '') + if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS: + raise ValidationError( + {'state': ['States are not supported in country "{}".'.format(cc)]} + ) + if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')): + raise ValidationError( + {'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]} + ) + return data diff --git a/src/pretix/base/exporters/orderlist.py b/src/pretix/base/exporters/orderlist.py index 111a55a0bd..891794fb7d 100644 --- a/src/pretix/base/exporters/orderlist.py +++ b/src/pretix/base/exporters/orderlist.py @@ -305,6 +305,12 @@ class OrderListExporter(MultiSheetListExporter): headers.append(_('Attendee name') + ': ' + str(label)) headers += [ _('Attendee email'), + _('Company'), + _('Address'), + _('ZIP code'), + _('City'), + _('Country'), + pgettext('address', 'State'), _('Voucher'), _('Pseudonymization ID'), ] @@ -364,6 +370,12 @@ class OrderListExporter(MultiSheetListExporter): ) row += [ op.attendee_email, + op.company or '', + op.street or '', + op.zipcode or '', + op.city or '', + op.country if op.country else '', + op.state or '', op.voucher.code if op.voucher else '', op.pseudonymization_id, ] diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py index 4e3280fff8..3645c5381a 100644 --- a/src/pretix/base/forms/questions.py +++ b/src/pretix/base/forms/questions.py @@ -245,7 +245,7 @@ class BaseQuestionsForm(forms.Form): if item.admission and event.settings.attendee_names_asked: self.fields['attendee_name_parts'] = NamePartsFormField( max_length=255, - required=event.settings.attendee_names_required, + 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'), @@ -253,7 +253,7 @@ class BaseQuestionsForm(forms.Form): ) if item.admission and event.settings.attendee_emails_asked: self.fields['attendee_email'] = forms.EmailField( - required=event.settings.attendee_emails_required, + 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( @@ -262,6 +262,73 @@ class BaseQuestionsForm(forms.Form): } ) ) + if item.admission and event.settings.attendee_company_asked: + self.fields['company'] = forms.CharField( + required=event.settings.attendee_company_required and not self.all_optional, + label=_('Company'), + initial=(cartpos.company if cartpos else orderpos.company), + ) + + if item.admission and event.settings.attendee_addresses_asked: + self.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), + ) + self.fields['zipcode'] = forms.CharField( + required=event.settings.attendee_addresses_required and not self.all_optional, + label=_('ZIP code'), + initial=(cartpos.zipcode if cartpos else orderpos.zipcode), + widget=forms.TextInput(attrs={ + 'autocomplete': 'postal-code', + }), + ) + self.fields['city'] = forms.CharField( + required=event.settings.attendee_addresses_required and not self.all_optional, + label=_('City'), + 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) + self.fields['country'] = CountryField().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'] + + 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 for q in questions: # Do we already have an answer? Provide it as the initial value @@ -423,6 +490,10 @@ class BaseQuestionsForm(forms.Form): 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): diff --git a/src/pretix/base/migrations/0150_auto_20200401_1123.py b/src/pretix/base/migrations/0150_auto_20200401_1123.py new file mode 100644 index 0000000000..5a0a6855b4 --- /dev/null +++ b/src/pretix/base/migrations/0150_auto_20200401_1123.py @@ -0,0 +1,74 @@ +# Generated by Django 3.0.4 on 2020-04-01 11:24 + +import django_countries.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0149_order_cancellation_date'), + ] + + operations = [ + migrations.AddField( + model_name='cartposition', + name='city', + field=models.CharField(max_length=255, null=True), + ), + migrations.AddField( + model_name='cartposition', + name='company', + field=models.CharField(max_length=255, null=True), + ), + migrations.AddField( + model_name='cartposition', + name='country', + field=django_countries.fields.CountryField(max_length=2, null=True), + ), + migrations.AddField( + model_name='cartposition', + name='state', + field=models.CharField(max_length=255, null=True), + ), + migrations.AddField( + model_name='cartposition', + name='street', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='cartposition', + name='zipcode', + field=models.CharField(max_length=30, null=True), + ), + migrations.AddField( + model_name='orderposition', + name='city', + field=models.CharField(max_length=255, null=True), + ), + migrations.AddField( + model_name='orderposition', + name='company', + field=models.CharField(max_length=255, null=True), + ), + migrations.AddField( + model_name='orderposition', + name='country', + field=django_countries.fields.CountryField(max_length=2, null=True), + ), + migrations.AddField( + model_name='orderposition', + name='state', + field=models.CharField(max_length=255, null=True), + ), + migrations.AddField( + model_name='orderposition', + name='street', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='orderposition', + name='zipcode', + field=models.CharField(max_length=30, null=True), + ), + ] diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 2c6fc368d8..f854d3e79b 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -1065,6 +1065,13 @@ class AbstractPosition(models.Model): 'Seat', null=True, blank=True, on_delete=models.PROTECT ) + company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'), null=True) + street = models.TextField(verbose_name=_('Address'), blank=True, null=True) + zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=True, null=True) + city = models.CharField(max_length=255, verbose_name=_('City'), blank=True, null=True) + country = CountryField(verbose_name=_('Country'), blank=True, blank_label=_('Select country'), null=True) + state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True, null=True) + class Meta: abstract = True diff --git a/src/pretix/base/orderimport.py b/src/pretix/base/orderimport.py index f9b0dd7829..e9030d5659 100644 --- a/src/pretix/base/orderimport.py +++ b/src/pretix/base/orderimport.py @@ -398,6 +398,99 @@ class AttendeeEmail(ImportColumn): position.attendee_email = value +class AttendeeCompany(ImportColumn): + identifier = 'attendee_company' + + @property + def verbose_name(self): + return _('Attendee address') + ': ' + _('Company') + + def assign(self, value, order, position, invoice_address, **kwargs): + position.company = value or '' + + +class AttendeeStreet(ImportColumn): + identifier = 'attendee_street' + + @property + def verbose_name(self): + return _('Attendee address') + ': ' + _('Address') + + def assign(self, value, order, position, invoice_address, **kwargs): + position.address = value or '' + + +class AttendeeZip(ImportColumn): + identifier = 'attendee_zipcode' + + @property + def verbose_name(self): + return _('Attendee address') + ': ' + _('ZIP code') + + def assign(self, value, order, position, invoice_address, **kwargs): + position.zipcode = value or '' + + +class AttendeeCity(ImportColumn): + identifier = 'attendee_city' + + @property + def verbose_name(self): + return _('Attendee address') + ': ' + _('City') + + def assign(self, value, order, position, invoice_address, **kwargs): + position.city = value or '' + + +class AttendeeCountry(ImportColumn): + identifier = 'attendee_country' + default_value = None + + @property + def initial(self): + return 'static:' + str(guess_country(self.event)) + + @property + def verbose_name(self): + return _('Attendee address') + ': ' + _('Country') + + def static_choices(self): + return list(countries) + + def clean(self, value, previous_values): + if value and not Country(value).numeric: + raise ValidationError(_("Please enter a valid country code.")) + return value + + def assign(self, value, order, position, invoice_address, **kwargs): + position.country = value or '' + + +class AttendeeState(ImportColumn): + identifier = 'attendee_state' + + @property + def verbose_name(self): + return _('Attendee address') + ': ' + _('State') + + def clean(self, value, previous_values): + if value: + if previous_values.get('attendee_country') not in COUNTRIES_WITH_STATE_IN_ADDRESS: + raise ValidationError(_("States are not supported for this country.")) + + types, form = COUNTRIES_WITH_STATE_IN_ADDRESS[previous_values.get('attendee_country')] + match = [ + s for s in pycountry.subdivisions.get(country_code=previous_values.get('attendee_country')) + if s.type in types and (s.code[3:] == value or s.name == value) + ] + if len(match) == 0: + raise ValidationError(_("Please enter a valid state.")) + return match[0].code[3:] + + def assign(self, value, order, position, invoice_address, **kwargs): + position.state = value or '' + + class Price(ImportColumn): identifier = 'price' verbose_name = gettext_lazy('Price') @@ -596,6 +689,12 @@ def get_all_columns(event): default.append(AttendeeNamePart(event, n, l)) default += [ AttendeeEmail(event), + AttendeeCompany(event), + AttendeeStreet(event), + AttendeeZip(event), + AttendeeCity(event), + AttendeeCountry(event), + AttendeeState(event), Price(event), Secret(event), Locale(event), diff --git a/src/pretix/base/pdf.py b/src/pretix/base/pdf.py index f634d9a66a..d7c93e1817 100644 --- a/src/pretix/base/pdf.py +++ b/src/pretix/base/pdf.py @@ -205,6 +205,11 @@ DEFAULT_VARIABLES = OrderedDict(( "editor_sample": _("Sample city"), "evaluate": lambda op, order, ev: order.invoice_address.city if getattr(order, 'invoice_address', None) else '' }), + ("attendee_company", { + "label": _("Attendee company"), + "editor_sample": _("Sample company"), + "evaluate": lambda op, order, ev: op.company or (op.addon_to.company if op.addon_to else '') + }), ("addons", { "label": _("List of Add-Ons"), "editor_sample": _("Addon 1\nAddon 2"), diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 26449ff77f..d60d3ae2f1 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -105,6 +105,44 @@ DEFAULTS = { widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}), ) }, + 'attendee_company_asked': { + 'default': 'False', + 'type': bool, + 'form_class': forms.BooleanField, + 'serializer_class': serializers.BooleanField, + 'form_kwargs': dict( + label=_("Ask for company per ticket"), + ) + }, + 'attendee_company_required': { + 'default': 'False', + 'type': bool, + 'form_class': forms.BooleanField, + 'serializer_class': serializers.BooleanField, + 'form_kwargs': dict( + label=_("Require company per ticket"), + widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_company_asked'}), + ) + }, + 'attendee_addresses_asked': { + 'default': 'False', + 'type': bool, + 'form_class': forms.BooleanField, + 'serializer_class': serializers.BooleanField, + 'form_kwargs': dict( + label=_("Ask for postal addresses per ticket"), + ) + }, + 'attendee_addresses_required': { + 'default': 'False', + 'type': bool, + 'form_class': forms.BooleanField, + 'serializer_class': serializers.BooleanField, + 'form_kwargs': dict( + label=_("Require postal addresses per ticket"), + widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_addresses_asked'}), + ) + }, 'order_email_asked_twice': { 'default': 'False', 'type': bool, diff --git a/src/pretix/base/shredder.py b/src/pretix/base/shredder.py index 3584d7f38d..f233eb97ec 100644 --- a/src/pretix/base/shredder.py +++ b/src/pretix/base/shredder.py @@ -194,15 +194,23 @@ class WaitingListShredder(BaseDataShredder): le.save(update_fields=['data', 'shredded']) -class AttendeeNameShredder(BaseDataShredder): - verbose_name = _('Attendee names') - identifier = 'attendee_names' - description = _('This will remove all attendee names from order positions, as well as logged changes to them.') +class AttendeeInfoShredder(BaseDataShredder): + verbose_name = _('Attendee info') + identifier = 'attendee_info' + description = _('This will remove all attendee names and postal addresses from order positions, as well as logged ' + 'changes to them.') def generate_files(self) -> List[Tuple[str, str, str]]: - yield 'attendee-names.json', 'application/json', json.dumps({ - '{}-{}'.format(op.order.code, op.positionid): op.attendee_name - for op in OrderPosition.all.filter( + yield 'attendee-info.json', 'application/json', json.dumps({ + '{}-{}'.format(op.order.code, op.positionid): { + 'name': op.attendee_name, + 'company': op.company, + 'street': op.street, + 'zipcode': op.zipcode, + 'city': op.city, + 'country': str(op.country) if op.country else None, + 'state': op.state + } for op in OrderPosition.all.filter( order__event=self.event ).filter( Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False)) @@ -214,8 +222,10 @@ class AttendeeNameShredder(BaseDataShredder): OrderPosition.all.filter( order__event=self.event ).filter( - Q(Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False)) - ).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True}) + Q(attendee_name_cached__isnull=False) | Q(attendee_name_parts__isnull=False) | + Q(company__isnull=False) | Q(street__isnull=False) | Q(zipcode__isnull=False) | Q(city__isnull=False) + ).update(attendee_name_cached=None, attendee_name_parts={'_shredded': True}, company=None, street=None, + zipcode=None, city=None) for le in self.event.logentry_set.filter(action_type="pretix.event.order.modified").exclude(data=""): d = le.parsed_data @@ -227,6 +237,14 @@ class AttendeeNameShredder(BaseDataShredder): d['data'][i]['attendee_name_parts'] = { '_legacy': '█' } + if 'company' in row: + d['data'][i]['company'] = '█' + if 'street' in row: + d['data'][i]['street'] = '█' + if 'zipcode' in row: + d['data'][i]['zipcode'] = '█' + if 'city' in row: + d['data'][i]['city'] = '█' le.data = json.dumps(d) le.shredded = True le.save(update_fields=['data', 'shredded']) @@ -357,7 +375,7 @@ class PaymentInfoShredder(BaseDataShredder): def register_payment_provider(sender, **kwargs): return [ EmailAddressShredder, - AttendeeNameShredder, + AttendeeInfoShredder, InvoiceAddressShredder, QuestionAnswerShredder, InvoiceShredder, diff --git a/src/pretix/base/views/mixins.py b/src/pretix/base/views/mixins.py index f3767d04df..ec88d171e1 100644 --- a/src/pretix/base/views/mixins.py +++ b/src/pretix/base/views/mixins.py @@ -85,10 +85,20 @@ class BaseQuestionsViewMixin: for k, v in form.cleaned_data.items(): if k == 'attendee_name_parts': form.pos.attendee_name_parts = v if v else None - form.pos.save() elif k == 'attendee_email': form.pos.attendee_email = v if v != '' else None - form.pos.save() + elif k == 'company': + form.pos.company = v if v != '' else None + elif k == 'street': + form.pos.street = v if v != '' else None + elif k == 'zipcode': + form.pos.zipcode = v if v != '' else None + elif k == 'city': + form.pos.city = v if v != '' else None + elif k == 'country': + form.pos.country = v if v != '' else None + elif k == 'state': + form.pos.state = v if v != '' else None elif k.startswith('question_'): field = form.fields[k] if hasattr(field, 'answer'): @@ -119,7 +129,7 @@ class BaseQuestionsViewMixin: meta_info['question_form_data'][k] = v form.pos.meta_info = json.dumps(meta_info) - form.pos.save(update_fields=['meta_info']) + form.pos.save() return not failed def _save_to_answer(self, field, answer, value): diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 3c7981b739..fae0768967 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -515,6 +515,10 @@ class EventSettingsForm(SettingsForm): 'attendee_names_required', 'attendee_emails_asked', 'attendee_emails_required', + 'attendee_company_asked', + 'attendee_company_required', + 'attendee_addresses_asked', + 'attendee_addresses_required', 'confirm_text', 'order_email_asked_twice', 'last_order_modification_date', diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html index dd0d15bdcb..d572a8670b 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings.html @@ -89,6 +89,10 @@ {% bootstrap_field sform.order_email_asked_twice layout="control" %} {% bootstrap_field sform.attendee_emails_asked layout="control" %} {% bootstrap_field sform.attendee_emails_required layout="control" %} + {% bootstrap_field sform.attendee_company_asked layout="control" %} + {% bootstrap_field sform.attendee_company_required layout="control" %} + {% bootstrap_field sform.attendee_addresses_asked layout="control" %} + {% bootstrap_field sform.attendee_addresses_required layout="control" %}