mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Organizer/MultiEvent-Exports (#1684)
Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user