diff --git a/.gitattributes b/.gitattributes
index db2e5ec1b2..7048a9c0fc 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,17 +1,17 @@
-src/static/fontawesome/* linguist-vendored
-src/static/lightbox/* linguist-vendored
-src/static/typeahead/* linguist-vendored
-src/static/moment/* linguist-vendored
-src/static/datetimepicker/* linguist-vendored
-src/static/colorpicker/* linguist-vendored
-src/static/fileupload/* linguist-vendored
-src/static/vuejs/* linguist-vendored
-src/static/select2/* linguist-vendored
-src/static/charts/* linguist-vendored
-src/static/rrule/* linguist-vendored
-src/static/iframeresizer/* linguist-vendored
-src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.* linguist-vendored
-src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.* linguist-vendored
+src/pretix/static/fontawesome/* linguist-vendored
+src/pretix/static/lightbox/* linguist-vendored
+src/pretix/static/typeahead/* linguist-vendored
+src/pretix/static/moment/* linguist-vendored
+src/pretix/static/datetimepicker/* linguist-vendored
+src/pretix/static/colorpicker/* linguist-vendored
+src/pretix/static/fileupload/* linguist-vendored
+src/pretix/static/vuejs/* linguist-vendored
+src/pretix/static/select2/* linguist-vendored
+src/pretix/static/charts/* linguist-vendored
+src/pretix/static/rrule/* linguist-vendored
+src/pretix/static/iframeresizer/* linguist-vendored
+src/pretix/static/pdfjs/* linguist-vendored
+src/pretix/static/fabric/* linguist-vendored
# Denote all files that are truly binary and should not be modified.
*.eot binary
diff --git a/doc/development/api/general.rst b/doc/development/api/general.rst
index bf40e7fea0..abf2a9b227 100644
--- a/doc/development/api/general.rst
+++ b/doc/development/api/general.rst
@@ -68,5 +68,5 @@ Dashboards
Ticket designs
""""""""""""""
-.. automodule:: pretix.plugins.ticketoutputpdf.signals
+.. automodule:: pretix.base.signals
:members: layout_text_variables
diff --git a/src/pretix/base/pdf.py b/src/pretix/base/pdf.py
new file mode 100644
index 0000000000..07674a76a2
--- /dev/null
+++ b/src/pretix/base/pdf.py
@@ -0,0 +1,143 @@
+import copy
+from collections import OrderedDict
+
+from django.utils.formats import date_format
+from django.utils.translation import ugettext_lazy as _
+from pytz import timezone
+
+from pretix.base.signals import layout_text_variables
+from pretix.base.templatetags.money import money_filter
+
+DEFAULT_VARIABLES = OrderedDict((
+ ("secret", {
+ "label": _("Ticket code (barcode content)"),
+ "editor_sample": "tdmruoekvkpbv1o2mv8xccvqcikvr58u",
+ "evaluate": lambda orderposition, order, event: orderposition.secret
+ }),
+ ("order", {
+ "label": _("Order code"),
+ "editor_sample": "A1B2C",
+ "evaluate": lambda orderposition, order, event: orderposition.order.code
+ }),
+ ("item", {
+ "label": _("Product name"),
+ "editor_sample": _("Sample product"),
+ "evaluate": lambda orderposition, order, event: str(orderposition.item)
+ }),
+ ("variation", {
+ "label": _("Variation name"),
+ "editor_sample": _("Sample variation"),
+ "evaluate": lambda op, order, event: str(op.variation) if op.variation else ''
+ }),
+ ("item_description", {
+ "label": _("Product description"),
+ "editor_sample": _("Sample product description"),
+ "evaluate": lambda orderposition, order, event: str(orderposition.item.description)
+ }),
+ ("itemvar", {
+ "label": _("Product name and variation"),
+ "editor_sample": _("Sample product – sample variation"),
+ "evaluate": lambda orderposition, order, event: (
+ '{} - {}'.format(orderposition.item, orderposition.variation)
+ if orderposition.variation else str(orderposition.item)
+ )
+ }),
+ ("item_category", {
+ "label": _("Product category"),
+ "editor_sample": _("Ticket category"),
+ "evaluate": lambda orderposition, order, event: (
+ str(orderposition.item.category.name) if orderposition.item.category else ""
+ )
+ }),
+ ("price", {
+ "label": _("Price"),
+ "editor_sample": _("123.45 EUR"),
+ "evaluate": lambda op, order, event: money_filter(op.price, event.currency)
+ }),
+ ("attendee_name", {
+ "label": _("Attendee name"),
+ "editor_sample": _("John Doe"),
+ "evaluate": lambda op, order, ev: op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '')
+ }),
+ ("event_name", {
+ "label": _("Event name"),
+ "editor_sample": _("Sample event name"),
+ "evaluate": lambda op, order, ev: str(ev.name)
+ }),
+ ("event_date", {
+ "label": _("Event date"),
+ "editor_sample": _("May 31st, 2017"),
+ "evaluate": lambda op, order, ev: ev.get_date_from_display(show_times=False)
+ }),
+ ("event_date_range", {
+ "label": _("Event date range"),
+ "editor_sample": _("May 31st – June 4th, 2017"),
+ "evaluate": lambda op, order, ev: ev.get_date_range_display()
+ }),
+ ("event_begin", {
+ "label": _("Event begin date and time"),
+ "editor_sample": _("2017-05-31 20:00"),
+ "evaluate": lambda op, order, ev: ev.get_date_from_display(show_times=True)
+ }),
+ ("event_begin_time", {
+ "label": _("Event begin time"),
+ "editor_sample": _("20:00"),
+ "evaluate": lambda op, order, ev: ev.get_time_from_display()
+ }),
+ ("event_admission", {
+ "label": _("Event admission date and time"),
+ "editor_sample": _("2017-05-31 19:00"),
+ "evaluate": lambda op, order, ev: date_format(
+ ev.date_admission.astimezone(timezone(ev.settings.timezone)),
+ "SHORT_DATETIME_FORMAT"
+ ) if ev.date_admission else ""
+ }),
+ ("event_admission_time", {
+ "label": _("Event admission time"),
+ "editor_sample": _("19:00"),
+ "evaluate": lambda op, order, ev: date_format(
+ ev.date_admission.astimezone(timezone(ev.settings.timezone)),
+ "TIME_FORMAT"
+ ) if ev.date_admission else ""
+ }),
+ ("event_location", {
+ "label": _("Event location"),
+ "editor_sample": _("Random City"),
+ "evaluate": lambda op, order, ev: str(ev.location).replace("\n", "
\n")
+ }),
+ ("invoice_name", {
+ "label": _("Invoice address: name"),
+ "editor_sample": _("John Doe"),
+ "evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address') else ''
+ }),
+ ("invoice_company", {
+ "label": _("Invoice address: company"),
+ "editor_sample": _("Sample company"),
+ "evaluate": lambda op, order, ev: order.invoice_address.company if getattr(order, 'invoice_address') else ''
+ }),
+ ("addons", {
+ "label": _("List of Add-Ons"),
+ "editor_sample": _("Addon 1\nAddon 2"),
+ "evaluate": lambda op, order, ev: "
".join([
+ '{} - {}'.format(p.item, p.variation) if p.variation else str(p.item)
+ for p in op.addons.select_related('item', 'variation')
+ ])
+ }),
+ ("organizer", {
+ "label": _("Organizer name"),
+ "editor_sample": _("Event organizer company"),
+ "evaluate": lambda op, order, ev: str(order.event.organizer.name)
+ }),
+ ("organizer_info_text", {
+ "label": _("Organizer info text"),
+ "editor_sample": _("Event organizer info text"),
+ "evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text)
+ }),
+))
+
+
+def get_variables(event):
+ v = copy.copy(DEFAULT_VARIABLES)
+ for recv, res in layout_text_variables.send(sender=event):
+ v.update(res)
+ return v
diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py
index bfc51d898a..67aea57afd 100644
--- a/src/pretix/base/signals.py
+++ b/src/pretix/base/signals.py
@@ -338,3 +338,22 @@ The ``message`` argument will contain an ``EmailMultiAlternatives`` object.
If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise
it will be ``None``.
"""
+
+
+layout_text_variables = EventPluginSignal()
+"""
+This signal is sent out to collect variables that can be used to display text in ticket-related PDF layouts.
+Receivers are expected to return a dictionary with globally unique identifiers as keys and more
+dictionaries as values that contain keys like in the following example::
+
+ return {
+ "product": {
+ "label": _("Product name"),
+ "editor_sample": _("Sample product"),
+ "evaluate": lambda orderposition, order, event: str(orderposition.item)
+ }
+ }
+
+The evaluate member will be called with the order position, order and event as arguments. The event might
+also be a subevent, if applicable.
+"""
diff --git a/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/index.html b/src/pretix/control/templates/pretixcontrol/pdf/index.html
similarity index 97%
rename from src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/index.html
rename to src/pretix/control/templates/pretixcontrol/pdf/index.html
index 435aea0632..cb0a802554 100644
--- a/src/pretix/plugins/ticketoutputpdf/templates/pretixplugins/ticketoutputpdf/index.html
+++ b/src/pretix/control/templates/pretixcontrol/pdf/index.html
@@ -1,9 +1,22 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load staticfiles %}
+{% load compress %}
{% block title %}{% trans "PDF Ticket Editor" %}{% endblock %}
+{% block custom_header %}
+ {{ block.super }}
+ {% compress css %}
+
+ {% endcompress %}
+
+{% endblock %}
{% block content %}
-