Organizer/MultiEvent-Exports (#1684)

Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
Martin Gross
2020-06-16 11:06:40 +02:00
committed by GitHub
parent e895c13b28
commit 0b20d3f6f8
14 changed files with 384 additions and 78 deletions

View File

@@ -6,11 +6,14 @@ from typing import Tuple
from defusedcsv import csv
from django import forms
from django.db.models import QuerySet
from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _
from openpyxl import Workbook
from openpyxl.cell.cell import KNOWN_TYPES
from pretix.base.models import Event
class BaseExporter:
"""
@@ -19,6 +22,12 @@ class BaseExporter:
def __init__(self, event):
self.event = event
self.is_multievent = isinstance(event, QuerySet)
if isinstance(event, QuerySet):
self.events = event
self.event = None
else:
self.events = Event.objects.filter(pk=event.pk)
def __str__(self):
return self.identifier

View File

@@ -9,11 +9,14 @@ from django.db.models import Exists, OuterRef, Q
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from pretix.base.models import OrderPayment
from pretix.base.models import Invoice, OrderPayment
from ...control.forms.filter import get_all_payment_providers
from ..exporter import BaseExporter
from ..services.invoices import invoice_pdf_task
from ..signals import register_data_exporters
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
)
class InvoiceExporter(BaseExporter):
@@ -21,7 +24,7 @@ class InvoiceExporter(BaseExporter):
verbose_name = _('All invoices')
def render(self, form_data: dict, output_file=None):
qs = self.event.invoices.filter(shredded=False)
qs = Invoice.objects.filter(event__in=self.events, shredded=False)
if form_data.get('payment_provider'):
qs = qs.annotate(
@@ -68,11 +71,16 @@ class InvoiceExporter(BaseExporter):
if not any:
return None
if self.is_multievent:
filename = '{}_invoices.zip'.format(self.events.first().organizer.slug)
else:
filename = '{}_invoices'.format(self.event.slug)
if output_file:
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', None
return filename, 'application/zip', None
else:
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
return '{}_invoices.zip'.format(self.event.slug), 'application/zip', zipf.read()
return filename, 'application/zip', zipf.read()
@property
def export_form_fields(self):
@@ -99,7 +107,7 @@ class InvoiceExporter(BaseExporter):
label=_('Payment provider'),
choices=[
('', _('All payment providers')),
] + [
] + get_all_payment_providers() if self.is_multievent else [
(k, v.verbose_name) for k, v in self.event.get_payment_providers().items()
],
required=False,
@@ -115,3 +123,8 @@ class InvoiceExporter(BaseExporter):
@receiver(register_data_exporters, dispatch_uid="exporter_invoices")
def register_invoice_export(sender, **kwargs):
return InvoiceExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_invoices")
def register_multievent_invoice_export(sender, **kwargs):
return InvoiceExporter

View File

@@ -8,7 +8,9 @@ from pretix.base.models import OrderPosition
from ..exporter import BaseExporter
from ..models import Order
from ..signals import register_data_exporters
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
)
class MailExporter(BaseExporter):
@@ -16,14 +18,18 @@ class MailExporter(BaseExporter):
verbose_name = _('Email addresses (text file)')
def render(self, form_data: dict):
qs = self.event.orders.filter(status__in=form_data['status'])
qs = Order.objects.filter(event__in=self.events, status__in=form_data['status']).prefetch_related('event')
addrs = qs.values('email')
pos = OrderPosition.objects.filter(
order__event=self.event, order__status__in=form_data['status']
order__event__in=self.events, order__status__in=form_data['status']
).values('attendee_email')
data = "\r\n".join(set(a['email'] for a in addrs if a['email'])
| set(a['attendee_email'] for a in pos if a['attendee_email']))
return '{}_pretixemails.txt'.format(self.event.slug), 'text/plain', data.encode("utf-8")
if self.is_multievent:
return '{}_pretixemails.txt'.format(self.events.first().organizer.slug), 'text/plain', data.encode("utf-8")
else:
return '{}_pretixemails.txt'.format(self.event.slug), 'text/plain', data.encode("utf-8")
@property
def export_form_fields(self):
@@ -44,3 +50,8 @@ class MailExporter(BaseExporter):
@receiver(register_data_exporters, dispatch_uid="exporter_mail")
def register_mail_export(sender, **kwargs):
return MailExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_mail")
def register_multievent_mail_export(sender, **kwargs):
return MailExporter

View File

@@ -11,14 +11,18 @@ from django.utils.formats import date_format
from django.utils.translation import gettext as _, gettext_lazy, pgettext
from pretix.base.models import (
GiftCard, InvoiceAddress, InvoiceLine, Order, OrderPosition, Question,
GiftCard, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
Question,
)
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.settings import PERSON_NAME_SCHEMES
from ...control.forms.filter import get_all_payment_providers
from ..exporter import ListExporter, MultiSheetListExporter
from ..signals import register_data_exporters
from ..signals import (
register_data_exporters, register_multievent_data_exporters,
)
class OrderListExporter(MultiSheetListExporter):
@@ -50,13 +54,13 @@ class OrderListExporter(MultiSheetListExporter):
tax_rates = set(
a for a
in OrderFee.objects.filter(
order__event=self.event
order__event__in=self.events
).values_list('tax_rate', flat=True).distinct().order_by()
)
tax_rates |= set(
a for a
in OrderPosition.objects.filter(
order__event=self.event
order__event__in=self.events
).values_list('tax_rate', flat=True).distinct().order_by()
)
tax_rates = sorted(tax_rates)
@@ -71,8 +75,6 @@ class OrderListExporter(MultiSheetListExporter):
return self.iterate_fees(form_data)
def iterate_orders(self, form_data: dict):
tz = pytz.timezone(self.event.settings.timezone)
p_date = OrderPayment.objects.filter(
order=OuterRef('pk'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
@@ -86,20 +88,20 @@ class OrderListExporter(MultiSheetListExporter):
s = OrderPosition.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(k=Count('id')).values('k')
qs = self.event.orders.annotate(
qs = Order.objects.filter(event__in=self.events).annotate(
payment_date=Subquery(p_date, output_field=DateTimeField()),
pcnt=Subquery(s, output_field=IntegerField())
).select_related('invoice_address').prefetch_related('invoices')
).select_related('invoice_address').prefetch_related('invoices').prefetch_related('event')
if form_data['paid_only']:
qs = qs.filter(status=Order.STATUS_PAID)
tax_rates = self._get_all_tax_rates(qs)
headers = [
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
_('Event slug'), _('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
_('Company'), _('Name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
if len(name_scheme['fields']) > 1:
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(label)
headers += [
@@ -140,7 +142,10 @@ class OrderListExporter(MultiSheetListExporter):
}
for order in qs.order_by('datetime'):
tz = pytz.timezone(order.event.settings.timezone)
row = [
order.event.slug,
order.code,
order.total,
order.get_status_display(),
@@ -152,7 +157,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.company,
order.invoice_address.name,
]
if len(name_scheme['fields']) > 1:
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
@@ -167,7 +172,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0))
row += [
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
@@ -197,15 +202,14 @@ class OrderListExporter(MultiSheetListExporter):
yield row
def iterate_fees(self, form_data: dict):
tz = pytz.timezone(self.event.settings.timezone)
qs = OrderFee.objects.filter(
order__event=self.event,
order__event__in=self.events,
).select_related('order', 'order__invoice_address', 'tax_rule')
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID)
headers = [
_('Event slug'),
_('Order code'),
_('Status'),
_('Email'),
@@ -219,8 +223,8 @@ class OrderListExporter(MultiSheetListExporter):
_('Company'),
_('Invoice address name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
if len(name_scheme['fields']) > 1:
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
@@ -231,7 +235,9 @@ class OrderListExporter(MultiSheetListExporter):
for op in qs.order_by('order__datetime'):
order = op.order
tz = pytz.timezone(order.event.settings.timezone)
row = [
order.event.slug,
order.code,
order.get_status_display(),
order.email,
@@ -248,7 +254,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.company,
order.invoice_address.name,
]
if len(name_scheme['fields']) > 1:
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
@@ -263,14 +269,12 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0))
yield row
def iterate_positions(self, form_data: dict):
tz = pytz.timezone(self.event.settings.timezone)
qs = OrderPosition.objects.filter(
order__event=self.event,
order__event__in=self.events,
).select_related(
'order', 'order__invoice_address', 'item', 'variation',
'voucher', 'tax_rule'
@@ -281,13 +285,14 @@ class OrderListExporter(MultiSheetListExporter):
qs = qs.filter(order__status=Order.STATUS_PAID)
headers = [
_('Event slug'),
_('Order code'),
_('Position ID'),
_('Status'),
_('Email'),
_('Order date'),
]
if self.event.has_subevents:
if self.events.filter(has_subevents=True).exists():
headers.append(pgettext('subevent', 'Date'))
headers.append(_('Start date'))
headers.append(_('End date'))
@@ -300,8 +305,8 @@ class OrderListExporter(MultiSheetListExporter):
_('Tax value'),
_('Attendee name'),
]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
if len(name_scheme['fields']) > 1:
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Attendee name') + ': ' + str(label))
headers += [
@@ -315,7 +320,8 @@ class OrderListExporter(MultiSheetListExporter):
_('Voucher'),
_('Pseudonymization ID'),
]
questions = list(self.event.questions.all())
questions = list(Question.objects.filter(event__in=self.events))
options = {}
for q in questions:
if q.type == Question.TYPE_CHOICE_MULTIPLE:
@@ -329,7 +335,7 @@ class OrderListExporter(MultiSheetListExporter):
_('Company'),
_('Invoice address name'),
]
if len(name_scheme['fields']) > 1:
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
headers.append(_('Invoice address name') + ': ' + str(label))
headers += [
@@ -343,18 +349,20 @@ class OrderListExporter(MultiSheetListExporter):
for op in qs.order_by('order__datetime', 'positionid'):
order = op.order
tz = pytz.timezone(order.event.settings.timezone)
row = [
order.event.slug,
order.code,
op.positionid,
order.get_status_display(),
order.email,
order.datetime.astimezone(tz).strftime('%Y-%m-%d'),
]
if self.event.has_subevents:
if order.event.has_subevents:
row.append(op.subevent.name)
row.append(op.subevent.date_from.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
row.append(op.subevent.date_from.astimezone(order.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
if op.subevent.date_to:
row.append(op.subevent.date_to.astimezone(self.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
row.append(op.subevent.date_to.astimezone(order.event.timezone).strftime('%Y-%m-%d %H:%M:%S'))
else:
row.append('')
row += [
@@ -366,7 +374,7 @@ class OrderListExporter(MultiSheetListExporter):
op.tax_value,
op.attendee_name,
]
if len(name_scheme['fields']) > 1:
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
op.attendee_name_parts.get(k, '')
@@ -404,7 +412,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.company,
order.invoice_address.name,
]
if len(name_scheme['fields']) > 1:
if name_scheme and len(name_scheme['fields']) > 1:
for k, label, w in name_scheme['fields']:
row.append(
order.invoice_address.name_parts.get(k, '')
@@ -419,7 +427,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
row += [''] * (8 + (len(name_scheme['fields']) if len(name_scheme['fields']) > 1 else 0))
row += [''] * (8 + (len(name_scheme['fields']) if name_scheme and len(name_scheme['fields']) > 1 else 0))
row += [
order.sales_channel,
order.locale
@@ -427,7 +435,10 @@ class OrderListExporter(MultiSheetListExporter):
yield row
def get_filename(self):
return '{}_orders'.format(self.event.slug)
if self.is_multievent:
return '{}_orders'.format(self.events.first().organizer.slug)
else:
return '{}_orders'.format(self.event.slug)
class PaymentListExporter(ListExporter):
@@ -459,31 +470,27 @@ class PaymentListExporter(ListExporter):
)
def iterate_list(self, form_data):
tz = pytz.timezone(self.event.settings.timezone)
provider_names = {
k: v.verbose_name
for k, v in self.event.get_payment_providers().items()
}
provider_names = dict(get_all_payment_providers())
payments = OrderPayment.objects.filter(
order__event=self.event,
order__event__in=self.events,
state__in=form_data.get('payment_states', [])
).order_by('created')
refunds = OrderRefund.objects.filter(
order__event=self.event,
order__event__in=self.events,
state__in=form_data.get('refund_states', [])
).order_by('created')
objs = sorted(list(payments) + list(refunds), key=lambda o: o.created)
headers = [
_('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Status code'), _('Amount'), _('Payment method')
]
yield headers
for obj in objs:
tz = pytz.timezone(obj.order.event.settings.timezone)
if isinstance(obj, OrderPayment) and obj.payment_date:
d2 = obj.payment_date.astimezone(tz).date().strftime('%Y-%m-%d')
elif isinstance(obj, OrderRefund) and obj.execution_date:
@@ -491,6 +498,7 @@ class PaymentListExporter(ListExporter):
else:
d2 = ''
row = [
obj.order.event.slug,
obj.order.code,
obj.full_id,
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
@@ -503,7 +511,10 @@ class PaymentListExporter(ListExporter):
yield row
def get_filename(self):
return '{}_payments'.format(self.event.slug)
if self.is_multievent:
return '{}_payments'.format(self.events.first().organizer.slug)
else:
return '{}_payments'.format(self.event.slug)
class QuotaListExporter(ListExporter):
@@ -585,7 +596,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
_('Total value (without taxes)'),
_('Payment matching IDs'),
]
qs = self.event.invoices.order_by('full_invoice_no').select_related(
qs = Invoice.objects.filter(event__in=self.events).order_by('full_invoice_no').select_related(
'order', 'refers'
).prefetch_related('order__payments').annotate(
total_gross=Subquery(
@@ -682,7 +693,7 @@ class InvoiceDataExporter(MultiSheetListExporter):
_('Invoice recipient:') + ' ' + _('Internal reference'),
]
qs = InvoiceLine.objects.filter(
invoice__event=self.event
invoice__event__in=self.events
).order_by('invoice__full_invoice_no', 'position').select_related(
'invoice', 'invoice__order', 'invoice__refers'
)
@@ -723,7 +734,10 @@ class InvoiceDataExporter(MultiSheetListExporter):
]
def get_filename(self):
return '{}_invoices'.format(self.event.slug)
if self.is_multievent:
return '{}_invoices'.format(self.events.first().organizer.slug)
else:
return '{}_invoices'.format(self.event.slug)
class GiftcardRedemptionListExporter(ListExporter):
@@ -731,27 +745,27 @@ class GiftcardRedemptionListExporter(ListExporter):
verbose_name = gettext_lazy('Gift card redemptions')
def iterate_list(self, form_data):
tz = pytz.timezone(self.event.settings.timezone)
payments = OrderPayment.objects.filter(
order__event=self.event,
order__event__in=self.events,
provider='giftcard'
).order_by('created')
refunds = OrderRefund.objects.filter(
order__event=self.event,
order__event__in=self.events,
provider='giftcard'
).order_by('created')
objs = sorted(list(payments) + list(refunds), key=lambda o: (o.order.code, o.created))
headers = [
_('Order'), _('Payment ID'), _('Date'), _('Gift card code'), _('Amount'), _('Issuer')
_('Event slug'), _('Order'), _('Payment ID'), _('Date'), _('Gift card code'), _('Amount'), _('Issuer')
]
yield headers
for obj in objs:
tz = pytz.timezone(obj.order.event.settings.timezone)
gc = GiftCard.objects.get(pk=obj.info_data.get('gift_card'))
row = [
obj.order.event.slug,
obj.order.code,
obj.full_id,
obj.created.astimezone(tz).date().strftime('%Y-%m-%d'),
@@ -762,7 +776,10 @@ class GiftcardRedemptionListExporter(ListExporter):
yield row
def get_filename(self):
return '{}_giftcardredemptions'.format(self.event.slug)
if self.is_multievent:
return '{}_giftcardredemptions'.format(self.events.first().organizer.slug)
else:
return '{}_giftcardredemptions'.format(self.event.slug)
@receiver(register_data_exporters, dispatch_uid="exporter_orderlist")
@@ -770,11 +787,21 @@ def register_orderlist_exporter(sender, **kwargs):
return OrderListExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_orderlist")
def register_multievent_orderlist_exporter(sender, **kwargs):
return OrderListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_paymentlist")
def register_paymentlist_exporter(sender, **kwargs):
return PaymentListExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_paymentlist")
def register_multievent_paymentlist_exporter(sender, **kwargs):
return PaymentListExporter
@receiver(register_data_exporters, dispatch_uid="exporter_quotalist")
def register_quotalist_exporter(sender, **kwargs):
return QuotaListExporter
@@ -785,6 +812,16 @@ def register_invoicedata_exporter(sender, **kwargs):
return InvoiceDataExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_invoicedata")
def register_multievent_invoicedatae_xporter(sender, **kwargs):
return InvoiceDataExporter
@receiver(register_data_exporters, dispatch_uid="exporter_giftcardredemptionlist")
def register_giftcardredemptionlist_exporter(sender, **kwargs):
return GiftcardRedemptionListExporter
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_giftcardredemptionlist")
def register_multievent_i_giftcardredemptionlist_exporter(sender, **kwargs):
return GiftcardRedemptionListExporter

View File

@@ -5,9 +5,15 @@ from django.utils.timezone import override
from django.utils.translation import gettext
from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import CachedFile, Event, cachedfile_name
from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.signals import register_data_exporters
from pretix.base.models import (
CachedFile, Event, Organizer, User, cachedfile_name,
)
from pretix.base.services.tasks import (
ProfiledEventTask, ProfiledOrganizerUserTask,
)
from pretix.base.signals import (
register_data_exporters, register_multievent_data_exporters,
)
from pretix.celery_app import app
@@ -32,3 +38,26 @@ def export(event: Event, fileid: str, provider: str, form_data: Dict[str, Any])
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
file.save()
return file.pk
@app.task(base=ProfiledOrganizerUserTask, throws=(ExportError,))
def multiexport(organizer: Organizer, user: User, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
file = CachedFile.objects.get(id=fileid)
with language(user.locale), override(user.timezone):
allowed_events = user.get_events_with_permission('can_view_orders')
events = allowed_events.filter(pk__in=form_data.get('events'))
responses = register_multievent_data_exporters.send(organizer)
for receiver, response in responses:
ex = response(events)
if ex.identifier == provider:
d = ex.render(form_data)
if d is None:
raise ExportError(
gettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
file.save()
return file.pk

View File

@@ -19,7 +19,7 @@ from django_scopes import scope, scopes_disabled
from pretix.base.metrics import (
pretix_task_duration_seconds, pretix_task_runs_total,
)
from pretix.base.models import Event
from pretix.base.models import Event, Organizer, User
from pretix.celery_app import app
@@ -88,10 +88,30 @@ class EventTask(app.Task):
return ret
class OrganizerUserTask(app.Task):
def __call__(self, *args, **kwargs):
organizer_id = kwargs['organizer']
with scopes_disabled():
organizer = Organizer.objects.get(pk=organizer_id)
kwargs['organizer'] = organizer
user_id = kwargs['user']
user = User.objects.get(pk=user_id)
kwargs['user'] = user
with scope(organizer=organizer):
ret = super().__call__(*args, **kwargs)
return ret
class ProfiledEventTask(ProfiledTask, EventTask):
pass
class ProfiledOrganizerUserTask(ProfiledTask, OrganizerUserTask):
pass
class TransactionAwareTask(ProfiledTask):
"""
Task class which is aware of django db transactions and only executes tasks

View File

@@ -268,6 +268,16 @@ subclass of pretix.base.exporter.BaseExporter
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
register_multievent_data_exporters = django.dispatch.Signal(
providing_args=["event"]
)
"""
This signal is sent out to get all known data exporters, which support exporting data for
multiple events. Receivers should return a subclass of pretix.base.exporter.BaseExporter
The ``sender`` keyword argument will contain an organizer.
"""
validate_order = EventPluginSignal(
providing_args=["payment_provider", "positions", "email", "locale", "invoice_address",
"meta_info"]

View File

@@ -411,6 +411,14 @@ def get_organizer_navigation(request):
'active': url.url_name == 'organizer',
'icon': 'calendar',
},
{
'label': _('Export'),
'url': reverse('control:organizer.export', kwargs={
'organizer': request.organizer.slug,
}),
'active': 'organizer.export' in url.url_name,
'icon': 'download',
},
]
if 'can_change_organizer_settings' in request.orgapermset:
nav.append({

View File

@@ -0,0 +1,37 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load order_overview %}
{% block title %}{% trans "Data export" %}{% endblock %}
{% block content %}
<h1>
{% trans "Data export" %}
{% if "identifier" in request.GET %}
<a href="?" class="btn btn-default">{% trans "Show all" %}</a>
{% endif %}
</h1>
{% for e in exporters %}
<details class="panel panel-default" {% if "identifier" in request.GET %}open{% endif %}>
<summary class="panel-heading">
<h3 class="panel-title">
{{ e.verbose_name }}
<i class="fa fa-angle-down collapse-indicator"></i>
</h3>
</summary>
<div id="{{ e.identifier }}">
<div class="panel-body">
<form action="{% url "control:organizer.export.do" organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long>
{% csrf_token %}
<input type="hidden" name="exporter" value="{{ e.identifier }}" />
{% bootstrap_form e.form layout='control' %}
<button class="btn btn-primary pull-right flip" type="submit">
<span class="icon icon-upload"></span> {% trans "Start export" %}
</button>
</form>
</div>
</div>
</details>
{% endfor %}
{% endblock %}

View File

@@ -106,6 +106,8 @@ urlpatterns = [
url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/delete$', organizer.TeamDeleteView.as_view(),
name='organizer.team.delete'),
url(r'^organizer/(?P<organizer>[^/]+)/slugrng', main.SlugRNG.as_view(), name='events.add.slugrng'),
url(r'^organizer/(?P<organizer>[^/]+)/export/$', organizer.ExportView.as_view(), name='organizer.export'),
url(r'^organizer/(?P<organizer>[^/]+)/export/do$', organizer.ExportDoView.as_view(), name='organizer.export.do'),
url(r'^nav/typeahead/$', typeahead.nav_context_list, name='nav.typeahead'),
url(r'^events/$', main.EventList.as_view(), name='events'),
url(r'^events/add$', main.EventWizard.as_view(), name='events.add'),

View File

@@ -1,4 +1,5 @@
import json
from datetime import timedelta
from decimal import Decimal
from django import forms
@@ -14,25 +15,32 @@ from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.generic import (
CreateView, DeleteView, DetailView, FormView, ListView, UpdateView,
CreateView, DeleteView, DetailView, FormView, ListView, TemplateView,
UpdateView,
)
from pretix.api.models import WebHook
from pretix.base.auth import get_auth_backends
from pretix.base.models import (
Device, GiftCard, OrderPayment, Organizer, Team, TeamInvite, User,
CachedFile, Device, GiftCard, OrderPayment, Organizer, Team, TeamInvite,
User,
)
from pretix.base.models.event import Event, EventMetaProperty, EventMetaValue
from pretix.base.models.giftcards import gen_giftcard_secret
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.payment import PaymentException
from pretix.base.services.export import multiexport
from pretix.base.services.mail import SendMailException, mail
from pretix.base.signals import register_multievent_data_exporters
from pretix.base.views.tasks import AsyncAction
from pretix.control.forms.filter import (
EventFilterForm, GiftCardFilterForm, OrganizerFilterForm,
)
from pretix.control.forms.orders import ExporterForm
from pretix.control.forms.organizer import (
DeviceForm, EventMetaPropertyForm, GiftCardCreateForm, GiftCardUpdateForm,
OrganizerDeleteForm, OrganizerForm, OrganizerSettingsForm,
@@ -1127,3 +1135,97 @@ class GiftCardUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
'giftcard': self.object.pk
}
))
class ExportMixin:
@cached_property
def exporters(self):
exporters = []
events = self.request.user.get_events_with_permission('can_view_orders')
responses = register_multievent_data_exporters.send(self.request.organizer)
for ex in sorted([response(events) for r, response in responses], key=lambda ex: str(ex.verbose_name)):
if self.request.GET.get("identifier") and ex.identifier != self.request.GET.get("identifier"):
continue
# Use form parse cycle to generate useful defaults
test_form = ExporterForm(data=self.request.GET, prefix=ex.identifier)
test_form.fields = ex.export_form_fields
test_form.is_valid()
initial = {
k: v for k, v in test_form.cleaned_data.items() if ex.identifier + "-" + k in self.request.GET
}
ex.form = ExporterForm(
data=(self.request.POST if self.request.method == 'POST' else None),
prefix=ex.identifier,
initial=initial
)
ex.form.fields = ex.export_form_fields
ex.form.fields.update([
('events',
forms.ModelMultipleChoiceField(
queryset=events,
initial=events,
widget=forms.CheckboxSelectMultiple(
attrs={'class': 'scrolling-multiple-choice'}
),
label=_('Events'),
required=True
)),
])
exporters.append(ex)
return exporters
class ExportDoView(OrganizerPermissionRequiredMixin, ExportMixin, AsyncAction, View):
known_errortypes = ['ExportError']
task = multiexport
def get_success_message(self, value):
return None
def get_success_url(self, value):
return reverse('cachedfile.download', kwargs={'id': str(value)})
def get_error_url(self):
return reverse('control:organizer.export', kwargs={
'organizer': self.request.organizer.slug
})
@cached_property
def exporter(self):
for ex in self.exporters:
if ex.identifier == self.request.POST.get("exporter"):
return ex
def post(self, request, *args, **kwargs):
if not self.exporter:
messages.error(self.request, _('The selected exporter was not found.'))
return redirect('control:organizer.export', kwargs={
'organizer': self.request.organizer.slug
})
if not self.exporter.form.is_valid():
messages.error(self.request, _('There was a problem processing your input. See below for error details.'))
return self.get(request, *args, **kwargs)
cf = CachedFile()
cf.date = now()
cf.expires = now() + timedelta(days=3)
cf.save()
return self.do(
organizer=self.request.organizer.id,
user=self.request.user.id,
fileid=str(cf.id),
provider=self.exporter.identifier,
form_data=self.exporter.form.cleaned_data
)
class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, TemplateView):
template_name = 'pretixcontrol/organizers/export.html'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['exporters'] = self.exporters
return ctx

View File

@@ -11,7 +11,7 @@ from PyPDF2.merger import PdfFileMerger
from pretix.base.exporter import BaseExporter
from pretix.base.i18n import language
from pretix.base.models import Order, OrderPosition
from pretix.base.models import Event, Order, OrderPosition
from pretix.base.settings import PERSON_NAME_SCHEMES
from .ticketoutput import PdfTicketOutput
@@ -24,7 +24,7 @@ class AllTicketsPDF(BaseExporter):
@property
def export_form_fields(self):
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme]
name_scheme = PERSON_NAME_SCHEMES[self.event.settings.name_scheme] if not self.is_multievent else None
d = OrderedDict(
[
('include_pending',
@@ -41,7 +41,7 @@ class AllTicketsPDF(BaseExporter):
] + ([
('name:{}'.format(k), _('Attendee name: {part}').format(part=label))
for k, label, w in name_scheme['fields']
] if settings.JSON_FIELD_AVAILABLE and len(name_scheme['fields']) > 1 else []),
] if settings.JSON_FIELD_AVAILABLE and name_scheme and len(name_scheme['fields']) > 1 else []),
)),
]
)
@@ -49,10 +49,8 @@ class AllTicketsPDF(BaseExporter):
def render(self, form_data):
merger = PdfFileMerger()
o = PdfTicketOutput(self.event)
qs = OrderPosition.objects.filter(
order__event=self.event
order__event__in=self.events
).prefetch_related(
'answers', 'answers__question'
).select_related('order', 'item', 'variation', 'addon_to')
@@ -76,10 +74,14 @@ class AllTicketsPDF(BaseExporter):
'resolved_name_part'
)
o = PdfTicketOutput(Event.objects.none())
for op in qs:
if not op.generate_ticket:
continue
if op.order.event != o.event:
o = PdfTicketOutput(op.event)
with language(op.order.locale):
layout = o.layout_map.get(
(op.item_id, op.order.sales_channel),
@@ -95,4 +97,8 @@ class AllTicketsPDF(BaseExporter):
merger.write(outbuffer)
merger.close()
outbuffer.seek(0)
return '{}_tickets.pdf'.format(self.event.slug), 'application/pdf', outbuffer.read()
if self.is_multievent:
return '{}_tickets.pdf'.format(self.events.first().organizer.slug), 'application/pdf', outbuffer.read()
else:
return '{}_tickets.pdf'.format(self.event.slug), 'application/pdf', outbuffer.read()

View File

@@ -10,7 +10,7 @@ from pretix.base.channels import get_all_sales_channels
from pretix.base.signals import ( # NOQA: legacy import
EventPluginSignal, event_copy_data, item_copy_data, layout_text_variables,
logentry_display, logentry_object_link, register_data_exporters,
register_ticket_outputs,
register_multievent_data_exporters, register_ticket_outputs,
)
from pretix.control.signals import item_forms
from pretix.plugins.ticketoutputpdf.forms import TicketLayoutItemForm
@@ -34,6 +34,12 @@ def register_data(sender, **kwargs):
return AllTicketsPDF
@receiver(register_multievent_data_exporters, dispatch_uid="dataexport_multievent_pdf")
def register_multievent_data(sender, **kwargs):
from .exporters import AllTicketsPDF
return AllTicketsPDF
@receiver(item_forms, dispatch_uid="pretix_ticketoutputpdf_item_forms")
def control_item_forms(sender, request, item, **kwargs):
forms = []