Pretix is an open source ticket sales platform with a self-hosted ‘Community’ edition. Pretix is highly versatile, and features a plugin system making it highly customisable, and the API is fairly well-documented. However, complete examples of how exactly to use the API to accomplish certain customisations are limited. This post outlines how to use custom plugins to effect certain customisations to Pretix. The details are correct as for Pretix 3.10.

Plugin creation generally

The process for creating a plugin is detailed in the Pretix documentation here.

In short, install cookiecutter and run:

cookiecutter https://github.com/pretix/pretix-plugin-cookiecutter

Follow the prompts to create an empty project.

The default __init__.py created by cookiecutter contains, among other things:

class PluginApp(PluginConfig):
	# ...
	def ready(self):
		from . import signals  # NOQA

Therefore, we can use signals.py to house code we want to run on plugin initialisation. (Alternatively, we could, and from a code cleanliness perspective probably should, split out into a different file and import that as well.)

To ensure our plugin can be found by Pretix, we also need to activate the Pretix virtual environment, then run:

python setup.py develop

Default settings

The setting of default settings is outlined in the Pretix documentation here. For example, within signals.py, we can override the default settings applied to events:

from pretix.base.settings import settings_hierarkey

# setting name, default value, value type
settings_hierarkey.add_default('invoice_generate', 'paid', str)
settings_hierarkey.add_default('invoice_email_attachment', 'True', bool)
settings_hierarkey.add_default('ticket_download', 'True', bool)
settings_hierarkey.add_default('attendee_names_required', 'True', bool)
settings_hierarkey.add_default('invoice_name_required', 'True', bool)
settings_hierarkey.add_default('invoice_address_asked', 'False', bool)

settings_hierarkey.add_default('invoice_address_from_name', 'Example Corp', str)
settings_hierarkey.add_default('invoice_address_from', '1 Example St', str)
settings_hierarkey.add_default('invoice_address_from_city', 'Melbourne, 3001', str)
settings_hierarkey.add_default('invoice_address_from_country', 'AU', str)
settings_hierarkey.add_default('invoice_address_from_tax_id', '12 345 678 900', str)

The internal names for these settings and valid values can generally be found within the pretix/base/settings.py file of the Pretix source code.

Default email text

Similarly, default email text is also stored within settings, so we can override this too. For example:

from pretix.base.settings import settings_hierarkey
from i18nfield.strings import LazyI18nString

settings_hierarkey.add_default('mail_text_resend_link', """You are receiving this message because you requested that we resend you the link to your order for {event}.

You can change your order details and view the status of your order at {url}.

Regards,    
Example Corp""", LazyI18nString)

As above, the internal names for these strings can be found within pretix/base/settings.py file.

Suppressing certain emails

For our use case, the only valid payment method is credit card, which is effectively instantaneous. It was therefore unnecessary for us to send an order confirmation email, when a payment confirmation email will be sent almost immediately after. Using a plugin, we can suppress this message:

import pretix.base.services.orders

__order_placed_email = pretix.base.services.orders._order_placed_email

def _order_placed_email(event, order, pprov, email_template, log_entry, invoice, payment):
	if log_entry == 'pretix.event.order.email.order_placed':
		return
	return __order_placed_email(event, order, pprov, email_template, log_entry, invoice, payment)

pretix.base.services.orders._order_placed_email = _order_placed_email

At the time of writing, this code and the code responsible for calling it with the appropriate log_entry were located in pretix/api/views/order.py.

Default tax rates

For our use case, we required a 10% GST charge on all tickets. We can set a 10% charge from the event creation wizard, but the label defaults to ‘VAT’, which is incorrect for our requirements. It is somewhat tedious and error prone to set this manually, so we use a plugin to do it automatically:

from pretix.base.models.event import Event
from i18nfield.strings import LazyI18nString

_set_defaults = Event.set_defaults

def set_defaults(self, *args, **kwargs):
	result = _set_defaults(self, *args, **kwargs)
	
	if not self.settings.tax_rate_default:
		self.settings.tax_rate_default = self.tax_rules.create(
			name=LazyI18nString('GST'),
			rate=10
		)
	
	return result

Event.set_defaults = set_defaults

Compare pretix/control/views/main.py.

Default header image

Similarly, we can modify the above patched code to automatically allow an event to inherit its header image from the organisation:

from pretix.base.models.event import Event

_set_defaults = Event.set_defaults

def set_defaults(self, *args, **kwargs):
	result = _set_defaults(self, *args, **kwargs)
	
	f = self.settings.get('logo_image', default=None)
	if not f:
		f = self.organizer.settings.get('organizer_logo_image', default=None)
		if f:
			self.settings.set('logo_image', f)
	
	return result

Event.set_defaults = set_defaults

Custom contact information fields

Pretix allows us to request email address and name on a per-order basis. Using ‘Questions’, we can also request additional information on a per-item basis. But we cannot natively request additional contact information on a per-order basis. Thankfully, Pretix contains signals that allow us to add additional fields to the order.

First, in signals.py we add, for example:

from django import forms
from django.dispatch import receiver
from django.template.loader import get_template

import json

import pretix.control.signals
import pretix.presale.signals

@receiver(pretix.presale.signals.contact_form_fields, dispatch_uid="custom_questions")
def add_custom_questions(sender, **kwargs):
	return {
		'phone_number': forms.CharField(
			label='Phone number',
			required=True,
		),
		'member': forms.ChoiceField(
			label='Membership',
			required=True,
			choices=(('Y', 'I am a member'), ('N', 'I am not a member')),
		),
	}

@receiver(pretix.presale.signals.order_info, dispatch_uid="custom_presale_order_info")
def add_custom_presale_order_info(sender, order=None, **kwargs):
	if not order:
		return
	template = get_template('PLUGIN_MODULE/presale_order_info.html') # replace PLUGIN_MODULE with the module name of the plugin
	ctx = json.loads(order.meta_info).get('contact_form_data', {})
	return template.render(ctx)

@receiver(pretix.control.signals.order_info, dispatch_uid="custom_control_order_info")
def add_custom_control_order_info(sender, order=None, **kwargs):
	if not order:
		return
	template = get_template('PLUGIN_MODULE/control_order_info.html') # replace PLUGIN_MODULE with the module name of the plugin
	ctx = json.loads(order.meta_info).get('contact_form_data', {})
	return template.render(ctx)

We then create each of templates/PLUGIN_MODULE/presale_order_info.html and templates/PLUGIN_MODULE/control_order_info.html with the following content:

<div class="panel panel-default">
	<div class="panel-heading">
		<h3 class="panel-title">
			Contact information
		</h3>
	</div>
	<div class="panel-body">
		<dl class="dl-horizontal">
			<dt>E-mail</dt>
			<dd>{{ email }}</dd>
			<dt>Phone number</dt>
			<dd>{{ phone_number }}</dd>
			<dt>Membership</dt>
			<dd>{{ member }}</dd>
		</dl>
	</div>
</div>

Exporting this data is somewhat more onerous, but still possible. We can create and register a variant of OrderListExporter along the lines of:

import json

# ...
class OrderListExporter(MultiSheetListExporter):
	# ...
	def iterate_list(self, form_data: dict):
		# ...
		# Before "yield headers", add:
		contact_keys = set()
		for order in qs:
			contact_form_data = json.loads(order.meta_info).get('contact_form_data', {})
			for k in contact_form_data.keys():
				contact_keys.add(k)
		contact_keys = sorted(list(contact_keys))
		headers.extend(contact_keys)
		
		yield headers
		
		# ...
		for order in qs.order_by('datetime'):
			# ...
			# Before "yield row", add:
			contact_form_data = json.loads(order.meta_info).get('contact_form_data', {})
			for key in contact_keys:
				row.append(contact_form_data.get(key, ''))
			
			yield row