diff --git a/src/pretix/base/exporters/__init__.py b/src/pretix/base/exporters/__init__.py
index 43cf23a37..b0d1a778c 100644
--- a/src/pretix/base/exporters/__init__.py
+++ b/src/pretix/base/exporters/__init__.py
@@ -20,6 +20,7 @@
# .
#
from .answers import * # noqa
+from .customers import * # noqa
from .dekodi import * # noqa
from .events import * # noqa
from .invoices import * # noqa
diff --git a/src/pretix/base/exporters/customers.py b/src/pretix/base/exporters/customers.py
new file mode 100644
index 000000000..3d3e39bdc
--- /dev/null
+++ b/src/pretix/base/exporters/customers.py
@@ -0,0 +1,113 @@
+#
+# This file is part of pretix (Community Edition).
+#
+# Copyright (C) 2014-2020 Raphael Michel and contributors
+# Copyright (C) 2020-2021 rami.io GmbH and contributors
+#
+# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
+# Public License as published by the Free Software Foundation in version 3 of the License.
+#
+# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
+# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
+# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
+# this file, see .
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
+# .
+#
+
+# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
+# the Apache License 2.0 can be obtained at .
+#
+# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
+# full history of changes and contributors is available at .
+#
+# This file contains Apache-licensed contributions copyrighted by: Benjamin Hättasch, Tobias Kunze
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under the License.
+
+from collections import OrderedDict
+
+from django.dispatch import receiver
+from django.utils.timezone import get_current_timezone
+from django.utils.translation import gettext as _, gettext_lazy
+
+from pretix.base.settings import PERSON_NAME_SCHEMES
+
+from ..exporter import ListExporter, OrganizerLevelExportMixin
+from ..signals import register_multievent_data_exporters
+
+
+class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
+ identifier = 'customerlist'
+ verbose_name = gettext_lazy('Customer accounts')
+ organizer_required_permission = 'can_manage_customers'
+
+ @property
+ def additional_form_fields(self):
+ return OrderedDict(
+ []
+ )
+
+ def iterate_list(self, form_data):
+ qs = self.organizer.customers.prefetch_related('provider')
+
+ headers = [
+ _('Customer ID'),
+ _('SSO provider'),
+ _('External identifier'),
+ _('E-mail'),
+ _('Phone number'),
+ _('Full name'),
+ ]
+ name_scheme = PERSON_NAME_SCHEMES[self.organizer.settings.name_scheme]
+ if name_scheme and len(name_scheme['fields']) > 1:
+ for k, label, w in name_scheme['fields']:
+ headers.append(_('Name') + ': ' + str(label))
+
+ headers += [
+ _('Account active'),
+ _('Verified email address'),
+ _('Last login'),
+ _('Registration date'),
+ _('Language'),
+ _('Notes'),
+ ]
+ yield headers
+
+ tz = get_current_timezone()
+ for obj in qs:
+ row = [
+ obj.identifier,
+ obj.provider.name if obj.provider else None,
+ obj.external_identifier,
+ obj.email or '',
+ obj.phone or '',
+ obj.name,
+ ]
+ if name_scheme and len(name_scheme['fields']) > 1:
+ for k, label, w in name_scheme['fields']:
+ row.append(obj.name_parts.get(k, ''))
+ row += [
+ _('Yes') if obj.is_active else _('No'),
+ _('Yes') if obj.is_verified else _('No'),
+ obj.last_login.astimezone(tz).date().strftime('%Y-%m-%d') if obj.last_login else '',
+ obj.date_joined.astimezone(tz).date().strftime('%Y-%m-%d') if obj.date_joined else '',
+ obj.get_locale_display(),
+ obj.notes or '',
+ ]
+ yield row
+
+ def get_filename(self):
+ return '{}_customers'.format(self.organizer.slug)
+
+
+@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_customerlist")
+def register_multievent_i_customerlist_exporter(sender, **kwargs):
+ return CustomerListExporter
diff --git a/src/pretix/base/models/customers.py b/src/pretix/base/models/customers.py
index 4df92010a..fc8dbaf1b 100644
--- a/src/pretix/base/models/customers.py
+++ b/src/pretix/base/models/customers.py
@@ -78,6 +78,7 @@ class Customer(LoggedModel):
organizer = models.ForeignKey(Organizer, related_name='customers', on_delete=models.CASCADE)
provider = models.ForeignKey(CustomerSSOProvider, related_name='customers', on_delete=models.PROTECT, null=True, blank=True)
identifier = models.CharField(
+ verbose_name=_('Customer ID'),
max_length=190,
db_index=True,
help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do '
diff --git a/src/pretix/base/services/export.py b/src/pretix/base/services/export.py
index 9adac010e..763de8ca6 100644
--- a/src/pretix/base/services/export.py
+++ b/src/pretix/base/services/export.py
@@ -102,9 +102,9 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
timezone = e.settings.timezone
region = e.settings.region
else:
- locale = settings.LANGUAGE_CODE
- timezone = settings.TIME_ZONE
- region = None
+ locale = organizer.settings.locale or settings.LANGUAGE_CODE
+ timezone = organizer.settings.timezone or settings.TIME_ZONE
+ region = organizer.settings.region
with language(locale, region), override(timezone):
if form_data.get('events') is not None:
if isinstance(form_data['events'][0], str):
@@ -123,9 +123,7 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
if (
isinstance(ex, OrganizerLevelExportMixin) and
not staff_session and
- not (device or token or user).has_organizer_permission(self.request.organizer,
- ex.organizer_required_permission,
- self.request)
+ not (device or token or user).has_organizer_permission(organizer, ex.organizer_required_permission)
):
raise ExportError(
gettext('You do not have sufficient permission to perform this export.')