diff --git a/doc/api/resources/checkinlists.rst b/doc/api/resources/checkinlists.rst
index 0c044b6393..d3ccff25bd 100644
--- a/doc/api/resources/checkinlists.rst
+++ b/doc/api/resources/checkinlists.rst
@@ -375,6 +375,7 @@ Order position endpoints
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
+ "pseudonymization_id": "MQLJvANO3B",
"checkins": [
{
"list": 1,
@@ -467,6 +468,7 @@ Order position endpoints
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
+ "pseudonymization_id": "MQLJvANO3B",
"checkins": [
{
"list": 1,
diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst
index c5a6443a73..967ed7a2b5 100644
--- a/doc/api/resources/orders.rst
+++ b/doc/api/resources/orders.rst
@@ -130,6 +130,7 @@ tax_rule integer The ID of the u
secret string Secret code printed on the tickets for validation
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
+pseudonymization_id string A random ID, e.g. for use in lead scanning apps
checkins list of objects List of check-ins with this ticket
├ list integer Internal ID of the check-in list
└ datetime datetime Time of check-in
@@ -156,6 +157,10 @@ answers list of objects Answers to user
The attributes ``answers.question_identifier`` and ``answers.option_identifiers`` have been added.
+.. versionchanged:: 1.16
+
+ The attribute ``pseudonymization_id`` has been added.
+
Order endpoints
---------------
@@ -235,6 +240,7 @@ Order endpoints
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
+ "pseudonymization_id": "MQLJvANO3B",
"checkins": [
{
"list": 44,
@@ -349,6 +355,7 @@ Order endpoints
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
+ "pseudonymization_id": "MQLJvANO3B",
"checkins": [
{
"list": 44,
@@ -847,6 +854,7 @@ Order position endpoints
"tax_rule": null,
"tax_value": "0.00",
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
+ "pseudonymization_id": "MQLJvANO3B",
"addon_to": null,
"subevent": null,
"checkins": [
@@ -939,6 +947,7 @@ Order position endpoints
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
"addon_to": null,
"subevent": null,
+ "pseudonymization_id": "MQLJvANO3B",
"checkins": [
{
"list": 44,
diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py
index 0169a73089..393670f10e 100644
--- a/src/pretix/api/serializers/order.py
+++ b/src/pretix/api/serializers/order.py
@@ -128,7 +128,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
model = OrderPosition
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads',
- 'answers', 'tax_rule')
+ 'answers', 'tax_rule', 'pseudonymization_id')
class OrderFeeSerializer(I18nAwareModelSerializer):
diff --git a/src/pretix/base/migrations/0093_auto_20180528_1432.py b/src/pretix/base/migrations/0093_auto_20180528_1432.py
new file mode 100644
index 0000000000..3f0c9c84fa
--- /dev/null
+++ b/src/pretix/base/migrations/0093_auto_20180528_1432.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.13 on 2018-05-28 14:32
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+from django.utils.crypto import get_random_string
+
+
+def set_pids(apps, schema_editor):
+ OrderPosition = apps.get_model('pretixbase', 'OrderPosition') # noqa
+ taken = set()
+ charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
+ for op in OrderPosition.objects.iterator():
+ while True:
+ code = get_random_string(length=10, allowed_chars=charset)
+ if code not in taken:
+ op.pseudonymization_id = code
+ taken.add(code)
+ break
+ op.save(update_fields=['pseudonymization_id'])
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pretixbase', '0092_auto_20180511_1224'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='orderposition',
+ name='pseudonymization_id',
+ field=models.CharField(db_index=True, max_length=16, null=True, unique=True),
+ ),
+ migrations.RunPython(
+ set_pids,
+ migrations.RunPython.noop,
+ ),
+ migrations.AlterField(
+ model_name='orderposition',
+ name='pseudonymization_id',
+ field=models.CharField(db_index=True, default='', max_length=16, unique=True),
+ preserve_default=False,
+ ),
+ ]
diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py
index 6ed7943524..a5b609c446 100644
--- a/src/pretix/base/models/orders.py
+++ b/src/pretix/base/models/orders.py
@@ -835,6 +835,11 @@ class OrderPosition(AbstractPosition):
verbose_name=_('Tax value')
)
secret = models.CharField(max_length=64, default=generate_position_secret, db_index=True)
+ pseudonymization_id = models.CharField(
+ max_length=16,
+ unique=True,
+ db_index=True
+ )
class Meta:
verbose_name = _("Order position")
@@ -916,8 +921,24 @@ class OrderPosition(AbstractPosition):
if self.pk is None:
while OrderPosition.objects.filter(secret=self.secret).exists():
self.secret = generate_position_secret()
+
+ if not self.pseudonymization_id:
+ self.assign_pseudonymization_id()
+
return super().save(*args, **kwargs)
+ def assign_pseudonymization_id(self):
+ # This omits some character pairs completely because they are hard to read even on screens (1/I and O/0)
+ # and includes only one of two characters for some pairs because they are sometimes hard to distinguish in
+ # handwriting (2/Z, 4/A, 5/S, 6/G). This allows for better detection e.g. in incoming wire transfers that
+ # might include OCR'd handwritten text
+ charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
+ while True:
+ code = get_random_string(length=10, allowed_chars=charset)
+ if not OrderPosition.objects.filter(pseudonymization_id=code).exists():
+ self.pseudonymization_id = code
+ return
+
class CartPosition(AbstractPosition):
"""
diff --git a/src/pretix/base/pdf.py b/src/pretix/base/pdf.py
index b070228993..6bfd1a1142 100644
--- a/src/pretix/base/pdf.py
+++ b/src/pretix/base/pdf.py
@@ -211,8 +211,14 @@ class Renderer:
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
def _draw_barcodearea(self, canvas: Canvas, op: OrderPosition, o: dict):
+ content = o.get('content', 'secret')
+ if content == 'secret':
+ content = op.secret
+ elif content == 'pseudonymization_id':
+ content = op.pseudonymization_id
+
reqs = float(o['size']) * mm
- qrw = QrCodeWidget(op.secret, barLevel='H', barHeight=reqs, barWidth=reqs)
+ qrw = QrCodeWidget(content, barLevel='H', barHeight=reqs, barWidth=reqs)
d = Drawing(reqs, reqs)
d.add(qrw)
qr_x = float(o['left']) * mm
diff --git a/src/pretix/control/templates/pretixcontrol/pdf/index.html b/src/pretix/control/templates/pretixcontrol/pdf/index.html
index bea593bf6e..17d7eec75d 100644
--- a/src/pretix/control/templates/pretixcontrol/pdf/index.html
+++ b/src/pretix/control/templates/pretixcontrol/pdf/index.html
@@ -317,9 +317,15 @@
{% trans "Text" %}
-