Added PDF exporter for the order overview

This commit is contained in:
Raphael Michel
2015-09-08 23:36:42 +02:00
parent 6279540372
commit 1b65d3cfaf
12 changed files with 366 additions and 100 deletions

View File

@@ -1668,7 +1668,7 @@ msgstr "Exportieren"
#: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:4 #: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:4
#: pretix/control/templates/pretixcontrol/orders/index.html:20 #: pretix/control/templates/pretixcontrol/orders/index.html:20
msgid "Pending" msgid "Pending"
msgstr "Zahlung ausstehend" msgstr "ausstehend"
#: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:6 #: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:6
#: pretix/control/templates/pretixcontrol/orders/index.html:19 #: pretix/control/templates/pretixcontrol/orders/index.html:19
@@ -1680,7 +1680,7 @@ msgstr "bezahlt"
#: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:8 #: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:8
#: pretix/control/templates/pretixcontrol/orders/index.html:21 #: pretix/control/templates/pretixcontrol/orders/index.html:21
msgid "Pending (expired)" msgid "Pending (expired)"
msgstr "Zahlung ausstehend (abgelaufen)" msgstr "ausstehend (abgelaufen)"
#: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:10 #: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:10
#: pretix/control/templates/pretixcontrol/orders/index.html:22 #: pretix/control/templates/pretixcontrol/orders/index.html:22
@@ -1736,7 +1736,7 @@ msgstr "Zeige %(currency)s"
#: pretix/control/templates/pretixcontrol/orders/overview.html:20 #: pretix/control/templates/pretixcontrol/orders/overview.html:20
msgid "Total orders" msgid "Total orders"
msgstr "Bestellungen gesamt" msgstr "bestellt"
#: pretix/control/templates/pretixcontrol/orders/overview.html:21 #: pretix/control/templates/pretixcontrol/orders/overview.html:21
#: pretix/presale/templates/pretixpresale/event/fragment_order_status.html:4 #: pretix/presale/templates/pretixpresale/event/fragment_order_status.html:4

View File

@@ -1664,7 +1664,7 @@ msgstr "Exportieren"
#: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:4 #: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:4
#: pretix/control/templates/pretixcontrol/orders/index.html:20 #: pretix/control/templates/pretixcontrol/orders/index.html:20
msgid "Pending" msgid "Pending"
msgstr "Zahlung ausstehend" msgstr "ausstehend"
#: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:6 #: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:6
#: pretix/control/templates/pretixcontrol/orders/index.html:19 #: pretix/control/templates/pretixcontrol/orders/index.html:19
@@ -1676,7 +1676,7 @@ msgstr "bezahlt"
#: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:8 #: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:8
#: pretix/control/templates/pretixcontrol/orders/index.html:21 #: pretix/control/templates/pretixcontrol/orders/index.html:21
msgid "Pending (expired)" msgid "Pending (expired)"
msgstr "Zahlung ausstehend (abgelaufen)" msgstr "ausstehend (abgelaufen)"
#: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:10 #: pretix/control/templates/pretixcontrol/orders/fragment_order_status.html:10
#: pretix/control/templates/pretixcontrol/orders/index.html:22 #: pretix/control/templates/pretixcontrol/orders/index.html:22
@@ -1732,7 +1732,7 @@ msgstr "Zeige %(currency)s"
#: pretix/control/templates/pretixcontrol/orders/overview.html:20 #: pretix/control/templates/pretixcontrol/orders/overview.html:20
msgid "Total orders" msgid "Total orders"
msgstr "Bestellungen gesamt" msgstr "bestellt"
#: pretix/control/templates/pretixcontrol/orders/overview.html:21 #: pretix/control/templates/pretixcontrol/orders/overview.html:21
#: pretix/presale/templates/pretixpresale/event/fragment_order_status.html:4 #: pretix/presale/templates/pretixpresale/event/fragment_order_status.html:4

View File

@@ -0,0 +1,99 @@
from django.db.models import Count, Sum
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import ItemCategory, Order, OrderPosition
def tuplesum(tuples):
return tuple(map(sum, zip(*list(tuples))))
def order_overview(event):
items = event.items.all().select_related(
'category', # for re-grouping
).prefetch_related(
'properties', # for .get_all_available_variations()
).order_by('category__position', 'category_id', 'name')
num_total = {
(p['item'], p['variation']): (p['cnt'], p['price'])
for p in
OrderPosition.objects.current.filter(order__event=event).values('item', 'variation').annotate(
cnt=Count('id'), price=Sum('price'))
}
num_cancelled = {
(p['item'], p['variation']): (p['cnt'], p['price'])
for p in (OrderPosition.objects.current
.filter(order__event=event, order__status=Order.STATUS_CANCELLED)
.values('item', 'variation')
.annotate(cnt=Count('id'), price=Sum('price')))
}
num_refunded = {
(p['item'], p['variation']): (p['cnt'], p['price'])
for p in (OrderPosition.objects.current
.filter(order__event=event, order__status=Order.STATUS_REFUNDED)
.values('item', 'variation')
.annotate(cnt=Count('id'), price=Sum('price')))
}
num_pending = {
(p['item'], p['variation']): (p['cnt'], p['price'])
for p in (OrderPosition.objects.current
.filter(order__event=event,
order__status__in=(Order.STATUS_PENDING, Order.STATUS_EXPIRED))
.values('item', 'variation')
.annotate(cnt=Count('id'), price=Sum('price')))
}
num_paid = {
(p['item'], p['variation']): (p['cnt'], p['price'])
for p in (OrderPosition.objects.current
.filter(order__event=event, order__status=Order.STATUS_PAID)
.values('item', 'variation')
.annotate(cnt=Count('id'), price=Sum('price')))
}
for item in items:
item.all_variations = sorted(item.get_all_variations(),
key=lambda vd: vd.ordered_values())
for var in item.all_variations:
variid = var['variation'].identity if 'variation' in var else None
var.num_total = num_total.get((item.identity, variid), (0, 0))
var.num_pending = num_pending.get((item.identity, variid), (0, 0))
var.num_cancelled = num_cancelled.get((item.identity, variid), (0, 0))
var.num_refunded = num_refunded.get((item.identity, variid), (0, 0))
var.num_paid = num_paid.get((item.identity, variid), (0, 0))
item.has_variations = (len(item.all_variations) != 1
or not item.all_variations[0].empty())
item.num_total = tuplesum(var.num_total for var in item.all_variations)
item.num_pending = tuplesum(var.num_pending for var in item.all_variations)
item.num_cancelled = tuplesum(var.num_cancelled for var in item.all_variations)
item.num_refunded = tuplesum(var.num_refunded for var in item.all_variations)
item.num_paid = tuplesum(var.num_paid for var in item.all_variations)
nonecat = ItemCategory(name=_('Uncategorized'))
# Regroup those by category
items_by_category = sorted(
[
# a group is a tuple of a category and a list of items
(cat if cat is not None else nonecat, [i for i in items if i.category == cat])
for cat in set([i.category for i in items])
# insert categories into a set for uniqueness
# a set is unsorted, so sort again by category
],
key=lambda group: (group[0].position, group[0].identity) if group[0] is not None else (0, "")
)
for c in items_by_category:
c[0].num_total = tuplesum(item.num_total for item in c[1])
c[0].num_pending = tuplesum(item.num_pending for item in c[1])
c[0].num_cancelled = tuplesum(item.num_cancelled for item in c[1])
c[0].num_refunded = tuplesum(item.num_refunded for item in c[1])
c[0].num_paid = tuplesum(item.num_paid for item in c[1])
total = {
'num_total': tuplesum(c.num_total for c, i in items_by_category),
'num_pending': tuplesum(c.num_pending for c, i in items_by_category),
'num_cancelled': tuplesum(c.num_cancelled for c, i in items_by_category),
'num_refunded': tuplesum(c.num_refunded for c, i in items_by_category),
'num_paid': tuplesum(c.num_paid for c, i in items_by_category)
}
return items_by_category, total

View File

@@ -1,9 +1,9 @@
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.shortcuts import render from django.shortcuts import render
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import CreateView, ListView, TemplateView from django.views.generic import CreateView, ListView, TemplateView
from django.conf import settings
from pretix.base.models import Event, EventPermission, OrganizerPermission from pretix.base.models import Event, EventPermission, OrganizerPermission
from pretix.control.forms.event import EventCreateForm from pretix.control.forms.event import EventCreateForm

View File

@@ -3,7 +3,7 @@ from itertools import groupby
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import Q, Count, Sum from django.db.models import Q
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.utils.functional import cached_property from django.utils.functional import cached_property
@@ -11,8 +11,9 @@ from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView, ListView, TemplateView, View from django.views.generic import DetailView, ListView, TemplateView, View
from pretix.base.models import Item, ItemCategory, Order, OrderPosition, Quota from pretix.base.models import Item, Order, Quota
from pretix.base.services.orders import mark_order_paid from pretix.base.services.orders import mark_order_paid
from pretix.base.services.stats import order_overview
from pretix.base.signals import ( from pretix.base.signals import (
register_data_exporters, register_payment_providers, register_data_exporters, register_payment_providers,
register_ticket_outputs, register_ticket_outputs,
@@ -260,103 +261,13 @@ class OrderExtend(OrderView):
data=self.request.POST if self.request.method == "POST" else None) data=self.request.POST if self.request.method == "POST" else None)
def tuplesum(tuples):
return tuple(map(sum, zip(*list(tuples))))
class OverView(EventPermissionRequiredMixin, TemplateView): class OverView(EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/orders/overview.html' template_name = 'pretixcontrol/orders/overview.html'
permission = 'can_view_orders' permission = 'can_view_orders'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data() ctx = super().get_context_data()
items = self.request.event.items.all().select_related( ctx['items_by_category'], ctx['total'] = order_overview(self.request.event)
'category', # for re-grouping
).prefetch_related(
'properties', # for .get_all_available_variations()
).order_by('category__position', 'category_id', 'name')
num_total = {
(p['item'], p['variation']): (p['cnt'], p['price'])
for p in
OrderPosition.objects.current.filter(order__event=self.request.event).values('item', 'variation').annotate(
cnt=Count('id'), price=Sum('price'))
}
num_cancelled = {
(p['item'], p['variation']): (p['cnt'], p['price'])
for p in (OrderPosition.objects.current
.filter(order__event=self.request.event, order__status=Order.STATUS_CANCELLED)
.values('item', 'variation')
.annotate(cnt=Count('id'), price=Sum('price')))
}
num_refunded = {
(p['item'], p['variation']): (p['cnt'], p['price'])
for p in (OrderPosition.objects.current
.filter(order__event=self.request.event, order__status=Order.STATUS_REFUNDED)
.values('item', 'variation')
.annotate(cnt=Count('id'), price=Sum('price')))
}
num_pending = {
(p['item'], p['variation']): (p['cnt'], p['price'])
for p in (OrderPosition.objects.current
.filter(order__event=self.request.event,
order__status__in=(Order.STATUS_PENDING, Order.STATUS_EXPIRED))
.values('item', 'variation')
.annotate(cnt=Count('id'), price=Sum('price')))
}
num_paid = {
(p['item'], p['variation']): (p['cnt'], p['price'])
for p in (OrderPosition.objects.current
.filter(order__event=self.request.event, order__status=Order.STATUS_PAID)
.values('item', 'variation')
.annotate(cnt=Count('id'), price=Sum('price')))
}
for item in items:
item.all_variations = sorted(item.get_all_variations(),
key=lambda vd: vd.ordered_values())
for var in item.all_variations:
variid = var['variation'].identity if 'variation' in var else None
var.num_total = num_total.get((item.identity, variid), (0, 0))
var.num_pending = num_pending.get((item.identity, variid), (0, 0))
var.num_cancelled = num_cancelled.get((item.identity, variid), (0, 0))
var.num_refunded = num_refunded.get((item.identity, variid), (0, 0))
var.num_paid = num_paid.get((item.identity, variid), (0, 0))
item.has_variations = (len(item.all_variations) != 1
or not item.all_variations[0].empty())
item.num_total = tuplesum(var.num_total for var in item.all_variations)
item.num_pending = tuplesum(var.num_pending for var in item.all_variations)
item.num_cancelled = tuplesum(var.num_cancelled for var in item.all_variations)
item.num_refunded = tuplesum(var.num_refunded for var in item.all_variations)
item.num_paid = tuplesum(var.num_paid for var in item.all_variations)
nonecat = ItemCategory(name=_('Uncategorized'))
# Regroup those by category
ctx['items_by_category'] = sorted(
[
# a group is a tuple of a category and a list of items
(cat if cat is not None else nonecat, [i for i in items if i.category == cat])
for cat in set([i.category for i in items])
# insert categories into a set for uniqueness
# a set is unsorted, so sort again by category
],
key=lambda group: (group[0].position, group[0].identity) if group[0] is not None else (0, "")
)
for c in ctx['items_by_category']:
c[0].num_total = tuplesum(item.num_total for item in c[1])
c[0].num_pending = tuplesum(item.num_pending for item in c[1])
c[0].num_cancelled = tuplesum(item.num_cancelled for item in c[1])
c[0].num_refunded = tuplesum(item.num_refunded for item in c[1])
c[0].num_paid = tuplesum(item.num_paid for item in c[1])
ctx['total'] = {
'num_total': tuplesum(c.num_total for c, i in ctx['items_by_category']),
'num_pending': tuplesum(c.num_pending for c, i in ctx['items_by_category']),
'num_cancelled': tuplesum(c.num_cancelled for c, i in ctx['items_by_category']),
'num_refunded': tuplesum(c.num_refunded for c, i in ctx['items_by_category']),
'num_paid': tuplesum(c.num_paid for c, i in ctx['items_by_category'])
}
return ctx return ctx

View File

@@ -0,0 +1,32 @@
from django.apps import AppConfig
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from pretix import __version__ as version
from pretix.base.plugins import PluginType
class ReportsApp(AppConfig):
name = 'pretix.plugins.reports'
verbose_name = _("Report exporter")
class PretixPluginMeta:
type = PluginType.PAYMENT
name = _("Report exporter")
author = _("the pretix team")
version = version
description = _("This plugin allows you to generate printable reports about your sales.")
def ready(self):
from . import signals # NOQA
@cached_property
def compatibility_errors(self):
errs = []
try:
import reportlab # NOQA
except ImportError:
errs.append("Python package 'reportlab' is not installed.")
return errs
default_app_config = 'pretix.plugins.reports.ReportsApp'

View File

@@ -0,0 +1,214 @@
import tempfile
from django.conf import settings
from django.contrib.staticfiles import finders
from django.http import HttpResponse
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from pretix.base.exporter import BaseExporter
from pretix.base.services.stats import order_overview
class Report:
name = "report"
def __init__(self, event):
self.event = event
@property
def pagesize(self):
from reportlab.lib import pagesizes
return pagesizes.portrait(pagesizes.A4)
def get_filename(self):
return "%s-%s.pdf" % (self.name, now().strftime("%Y-%m-%d-%H-%M-%S"))
@staticmethod
def register_fonts():
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase import pdfmetrics
pdfmetrics.registerFont(TTFont('OpenSans', finders.find('fonts/OpenSans-Regular.ttf')))
pdfmetrics.registerFont(TTFont('OpenSansIt', finders.find('fonts/OpenSans-Italic.ttf')))
pdfmetrics.registerFont(TTFont('OpenSansBd', finders.find('fonts/OpenSans-Bold.ttf')))
def create(self):
from reportlab.platypus import BaseDocTemplate, PageTemplate
from reportlab.lib.units import mm
with tempfile.NamedTemporaryFile(suffix=".pdf") as f:
Report.register_fonts()
doc = BaseDocTemplate(f.name, pagesize=self.pagesize,
leftMargin=15 * mm,
rightMargin=15 * mm,
topMargin=20 * mm,
bottomMargin=15 * mm)
doc.addPageTemplates([
PageTemplate(id='All', frames=self.get_frames(doc), onPage=self.on_page, pagesize=self.pagesize)
])
doc.build(self.get_story(doc))
f.seek(0)
return f.read()
def get_frames(self, doc):
from reportlab.platypus import Frame
self.frame = Frame(doc.leftMargin, doc.bottomMargin,
doc.width,
doc.height,
leftPadding=0,
rightPadding=0,
topPadding=0,
bottomPadding=0,
id='normal')
return [self.frame]
def get_story(self, doc):
return []
def get_style(self):
from reportlab.lib.styles import getSampleStyleSheet
styles = getSampleStyleSheet()
style = styles["Normal"]
style.fontName = 'OpenSans'
return style
def on_page(self, canvas, doc):
canvas.saveState()
self.page_footer(canvas, doc)
self.page_header(canvas, doc)
canvas.restoreState()
def page_footer(self, canvas, doc):
from reportlab.lib.units import mm
canvas.setFont('OpenSans', 8)
canvas.drawString(15 * mm, 10 * mm, _("Page %d") % (doc.page,))
canvas.drawRightString(self.pagesize[0] - 15 * mm, 10 * mm,
_("Created: %s") % now().strftime("%d.%m.%Y %H:%M:%S"))
def page_header(self, canvas, doc):
from reportlab.lib.units import mm
canvas.setFont('OpenSans', 10)
canvas.drawString(15 * mm, self.pagesize[1] - 15 * mm,
"%s %s" % (self.event.organizer.name, self.event.name))
canvas.drawRightString(self.pagesize[0] - 15 * mm, self.pagesize[1] - 15 * mm,
settings.PRETIX_INSTANCE_NAME)
canvas.setStrokeColorRGB(0, 0, 0)
canvas.line(15 * mm, self.pagesize[1] - 17 * mm,
self.pagesize[0] - 15 * mm, self.pagesize[1] - 17 * mm)
class OverviewReport(Report):
name = "overview"
@property
def pagesize(self):
from reportlab.lib import pagesizes
return pagesizes.landscape(pagesizes.A4)
def get_story(self, doc):
from reportlab.platypus import Paragraph, Spacer, TableStyle, Table
from reportlab.lib.units import mm
headlinestyle = self.get_style()
headlinestyle.fontSize = 15
headlinestyle.fontName = 'OpenSansBd'
colwidths = [a * doc.width for a in (.30, .06, .08, .06, .08, .06, .08, .06, .08, .06, .08)]
tstyledata = [
('SPAN', (1, 0), (2, 0)),
('SPAN', (3, 0), (4, 0)),
('SPAN', (5, 0), (6, 0)),
('SPAN', (7, 0), (8, 0)),
('SPAN', (9, 0), (10, 0)),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
('ALIGN', (1, 1), (-1, -1), 'RIGHT'),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('FONTNAME', (0, 0), (-1, 0), 'OpenSansBd'),
('FONTNAME', (0, -1), (-1, -1), 'OpenSansBd'),
]
story = [
Paragraph(_('Orders by product'), headlinestyle),
Spacer(1, 5 * mm)
]
tdata = [
[
_('Product'), _('Total orders'), '', _('Pending'), '', _('Cancelled'), '', _('Refunded'), '',
_('Paid'), ''
],
[
'',
_('Number'), self.event.currency,
_('Number'), self.event.currency,
_('Number'), self.event.currency,
_('Number'), self.event.currency,
_('Number'), self.event.currency
],
]
items_by_category, total = order_overview(self.event)
for tup in items_by_category:
if tup[0]:
tstyledata.append(('FONTNAME', (0, len(tdata)), (-1, len(tdata)), 'OpenSansBd'))
tdata.append([
tup[0].name,
str(tup[0].num_total[0]), str(tup[0].num_total[1]),
str(tup[0].num_pending[0]), str(tup[0].num_pending[1]),
str(tup[0].num_cancelled[0]), str(tup[0].num_cancelled[1]),
str(tup[0].num_refunded[0]), str(tup[0].num_refunded[1]),
str(tup[0].num_paid[0]), str(tup[0].num_paid[1])
])
for item in tup[1]:
tdata.append([
item.name,
str(item.num_total[0]), str(item.num_total[1]),
str(item.num_pending[0]), str(item.num_pending[1]),
str(item.num_cancelled[0]), str(item.num_cancelled[1]),
str(item.num_refunded[0]), str(item.num_refunded[1]),
str(item.num_paid[0]), str(item.num_paid[1])
])
if item.has_variations:
for var in item.all_variations:
tdata.append([
" " + str(var),
str(var.num_total[0]), str(var.num_total[1]),
str(var.num_pending[0]), str(var.num_pending[1]),
str(var.num_cancelled[0]), str(var.num_cancelled[1]),
str(var.num_refunded[0]), str(var.num_refunded[1]),
str(var.num_paid[0]), str(var.num_paid[1])
])
tdata.append([
_("Total"),
str(total['num_total'][0]), str(total['num_total'][1]),
str(total['num_pending'][0]), str(total['num_pending'][1]),
str(total['num_cancelled'][0]), str(total['num_cancelled'][1]),
str(total['num_refunded'][0]), str(total['num_refunded'][1]),
str(total['num_paid'][0]), str(total['num_paid'][1])
])
table = Table(tdata, colWidths=colwidths, repeatRows=2)
table.setStyle(TableStyle(tstyledata))
story.append(table)
return story
class OverviewReportExporter(BaseExporter):
identifier = 'pdfreport'
verbose_name = _('Order overview (PDF)')
def render(self, request):
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'inline; filename="report-%s.pdf"' % request.event.slug
report = OverviewReport(request.event)
response.write(report.create())
return response

View File

@@ -0,0 +1,9 @@
from django.dispatch import receiver
from pretix.base.signals import register_data_exporters
@receiver(register_data_exporters, dispatch_uid="export_overview_report_pdf")
def register_report_pdf(sender, **kwargs):
from .exporters import OverviewReportExporter
return OverviewReportExporter

View File

@@ -142,6 +142,7 @@ INSTALLED_APPS = (
'pretix.plugins.ticketoutputpdf', 'pretix.plugins.ticketoutputpdf',
'pretix.plugins.sendmail', 'pretix.plugins.sendmail',
'pretix.plugins.statistics', 'pretix.plugins.statistics',
'pretix.plugins.reports',
'pretix.plugins.pretixdroid', 'pretix.plugins.pretixdroid',
'easy_thumbnails', 'easy_thumbnails',
) )

Binary file not shown.

Binary file not shown.

Binary file not shown.