Compare commits

..

2 Commits

Author SHA1 Message Date
Raphael Michel
4282613fd4 Add a log entry on manual VAT ID validation (Z#23223874) 2026-02-26 17:00:35 +01:00
Richard Schreiber
a25bca7471 Fix static instance name in emails (Z#23224360) (#5914) 2026-02-25 13:19:53 +01:00
31 changed files with 119 additions and 438 deletions

View File

@@ -192,7 +192,7 @@ Cart position endpoints
* ``attendee_email`` (optional)
* ``subevent`` (optional)
* ``expires`` (optional)
* ``includes_tax`` (optional, **DEPRECATED**, do not use, will be removed)
* ``includes_tax`` (optional, **deprecated**, do not use, will be removed)
* ``sales_channel`` (optional)
* ``voucher`` (optional, expect a voucher code)
* ``addons`` (optional, expect a list of nested objects of cart positions)

View File

@@ -1066,7 +1066,6 @@ Creating orders
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID)
* ``add_to_reusable_medium`` (optional, causes the new ticket to be added to the given reusable medium, identified by its ID)
* ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run)
* ``answers``

View File

@@ -21,16 +21,12 @@ id integer Internal ID of
type string Type of medium, e.g. ``"barcode"``, ``"nfc_uid"`` or ``"nfc_mf0aes"``.
organizer string Organizer slug of the organizer who "owns" this medium.
identifier string Unique identifier of the medium. The format depends on the ``type``.
claim_token string Secret token to claim ownership of the medium (or ``null``)
label string Label to identify the medium, usually something human readable (or ``null``)
active boolean Whether this medium may be used.
created datetime Date of creation
updated datetime Date of last modification
expires datetime Expiry date (or ``null``)
customer string Identifier of a customer account this medium belongs to.
linked_orderpositions list of integer Internal IDs of tickets this medium is linked to.
linked_orderposition integer **DEPRECATED.** ID of the ticket the medium is linked to, if it is linked to
only one ticket. ``null``, if the medium is linked to none or multiple tickets.
linked_orderposition integer Internal ID of a ticket this medium is linked to.
linked_giftcard integer Internal ID of a gift card this medium is linked to.
info object Additional data, content depends on the ``type``. Consider
this internal to the system and don't use it for your own data.
@@ -43,14 +39,6 @@ Existing media types are:
- ``nfc_uid``
- ``nfc_mf0aes``
.. versionchanged:: 2025.11
The ``claim_token``, ``label``, ``linked_orderpositions`` attributes have been added, the ``linked_orderposition`` attribute has been
deprecated. Note: To maintain backwards compatibility ``linked_orderposition`` contains the internal ID of the linked order position
if the medium has exactly one order position in ``linked_orderpositions``.
Endpoints
---------
@@ -89,7 +77,6 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -105,13 +92,10 @@ Endpoints
:query string customer: Only show media linked to the given customer.
:query string created_since: Only show media created since a given date.
:query string updated_since: Only show media updated since a given date.
:query integer linked_orderpositions: Only show media linked to the given tickets. Note: you can pass multiple ticket IDs by passing
``linked_orderpositions`` multiple times. Any medium matching any linked orderposition will be returned.
:query integer linked_orderposition: Only show media linked to the given ticket.
:query integer linked_giftcard: Only show media linked to the given gift card.
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderpositions"``,
``"linked_orderposition"`` (**DEPRECATED**), or ``"customer"``, the respective field will be shown
as a nested value instead of just an ID.
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderposition"``,
or ``"customer"``, the respective field will be shown as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that order positions
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
@@ -150,7 +134,6 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -208,7 +191,6 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -216,9 +198,9 @@ Endpoints
}
:param organizer: The ``slug`` field of the organizer to look up a medium for
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderpositions"``, or ``"customer"``, the respective
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderpositions`` each will have an attribute of the
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:statuscode 201: no error
@@ -245,7 +227,6 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -270,7 +251,6 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -278,7 +258,7 @@ Endpoints
}
:param organizer: The ``slug`` field of the organizer to create a medium for
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderpositions"``, or ``"customer"``, the respective
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
@@ -307,7 +287,7 @@ Endpoints
Content-Length: 94
{
"linked_orderpositions": [13, 29]
"linked_orderposition": 13
}
**Example response**:
@@ -328,8 +308,7 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [13, 29],
"linked_orderposition": None,
"linked_orderposition": 13,
"linked_giftcard": None,
"notes": None,
"info": {}
@@ -337,7 +316,7 @@ Endpoints
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the medium to modify
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderpositions"``, or ``"customer"``, the respective
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter

View File

@@ -65,11 +65,9 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
expand_nested = self.context['request'].query_params.getlist('expand')
if 'linked_giftcard' in expand_nested:
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
if 'linked_giftcard.owner_ticket' in expand_nested:
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
else:
self.fields['linked_giftcard'] = serializers.PrimaryKeyRelatedField(
@@ -78,27 +76,16 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
queryset=self.context['organizer'].issued_gift_cards.all()
)
# keep linked_orderposition (singular) for backwards compatibility, will be overwritten in self.validate
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
required=False,
allow_null=True,
queryset=OrderPosition.all.filter(order__event__organizer=self.context['organizer']),
)
if 'linked_orderposition' in expand_nested or 'linked_orderpositions' in expand_nested:
self.fields['linked_orderpositions'] = NestedOrderPositionSerializer(
many=True,
read_only=True
)
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
else:
self.fields['linked_orderpositions'] = serializers.PrimaryKeyRelatedField(
many=True,
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
required=False,
allow_null=True,
queryset=OrderPosition.all.filter(order__event__organizer=self.context['organizer']),
)
if 'customer' in expand_nested:
if 'customer' in self.context['request'].query_params.getlist('expand'):
self.fields['customer'] = CustomerSerializer(read_only=True)
else:
self.fields['customer'] = serializers.SlugRelatedField(
@@ -110,20 +97,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
def validate(self, data):
data = super().validate(data)
if 'linked_orderposition' in data:
linked_orderposition = data['linked_orderposition']
# backwards-compatibility
if 'linked_orderpositions' in data:
raise ValidationError({
'linked_orderposition': _('You cannot use linked_orderposition and linked_orderpositions at the same time.')
})
if self.instance.linked_orderpositions.count() > 1:
raise ValidationError({
'linked_orderposition': _('There are more than one linked_orderposition. You need to use linked_orderpositions.')
})
data['linked_orderpositions'] = [linked_orderposition] if linked_orderposition else []
if 'type' in data and 'identifier' in data:
qs = self.context['organizer'].reusable_media.filter(
identifier=data['identifier'], type=data['type']
@@ -136,14 +109,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
)
return data
def to_representation(self, obj):
r = super(ReusableMediaSerializer, self).to_representation(obj)
ops = r.get('linked_orderpositions')
if len(ops) < 2:
# add linked_orderposition (singular) for backwards compatibility
r['linked_orderposition'] = ops[0] if ops else None
return r
class Meta:
model = ReusableMedium
fields = (
@@ -153,12 +118,10 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
'updated',
'type',
'identifier',
'claim_token',
'label',
'active',
'expires',
'customer',
'linked_orderpositions',
'linked_orderposition',
'linked_giftcard',
'info',
'notes',

View File

@@ -1029,15 +1029,13 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
requested_valid_from = serializers.DateTimeField(required=False, allow_null=True)
use_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
required=False, allow_null=True)
add_to_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
required=False, allow_null=True)
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
'requested_valid_from', 'use_reusable_medium', 'add_to_reusable_medium', 'discount')
'requested_valid_from', 'use_reusable_medium', 'discount')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1049,8 +1047,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
with scopes_disabled():
if 'use_reusable_medium' in self.fields:
self.fields['use_reusable_medium'].queryset = ReusableMedium.objects.all()
if 'add_to_reusable_medium' in self.fields:
self.fields['add_to_reusable_medium'].queryset = ReusableMedium.objects.all()
def validate_secret(self, secret):
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
@@ -1066,9 +1062,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
)
return m
def validate_add_to_reusable_medium(self, m):
return self.validate_use_reusable_medium(m)
def validate_item(self, item):
if item.event != self.context['event']:
raise ValidationError(
@@ -1142,13 +1135,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(
{'discount': ['You can only specify a discount if you do the price computation, but price is not set.']}
)
if 'use_reusable_medium' in data and 'add_to_reusable_medium' in data:
raise ValidationError({
'use_reusable_medium': ['You can only specify either use_reusable_medium or add_to_reusable_medium.'],
'add_to_reusable_medium': ['You can only specify either use_reusable_medium or add_to_reusable_medium.'],
})
return data
@@ -1564,7 +1550,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k not in ('answers', '_quotas', 'use_reusable_medium', 'add_to_reusable_medium')})
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas' and k != 'use_reusable_medium'})
if simulate:
pos.order = order._wrapped
else:
@@ -1638,7 +1624,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
add_to_reusable_medium = pos_data.pop('add_to_reusable_medium', None)
pos = pos_data['__instance']
pos._calculate_tax(invoice_address=ia)
@@ -1680,7 +1665,8 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
answ.options.add(*options)
if use_reusable_medium:
use_reusable_medium.linked_orderpositions.set([pos])
use_reusable_medium.linked_orderposition = pos
use_reusable_medium.save(update_fields=['linked_orderposition'])
use_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.changed',
data={
@@ -1688,15 +1674,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
'linked_orderposition': pos.pk,
}
)
if add_to_reusable_medium:
add_to_reusable_medium.linked_orderpositions.add(pos)
add_to_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.added',
data={
'by_order': order.code,
'linked_orderposition': pos.pk,
}
)
if not simulate:
for cp in delete_cps:

View File

@@ -365,9 +365,10 @@ class TeamInviteSerializer(serializers.ModelSerializer):
def _send_invite(self, instance):
mail(
instance.email,
_('pretix account invitation'),
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.context['organizer'].name,
'team': instance.team.name,

View File

@@ -520,13 +520,11 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
# with respecting the force option), or it's a reusable medium (-> proceed with that)
if not op_candidates:
try:
media = ReusableMedium.objects.active().annotate(
has_linked_orderpositions=Exists(ReusableMedium.linked_orderpositions.through.objects.filter(reusablemedium_id=OuterRef('pk')))
).get(
media = ReusableMedium.objects.select_related('linked_orderposition').active().get(
organizer_id=checkinlists[0].event.organizer_id,
type=source_type,
identifier=raw_barcode,
has_linked_orderpositions=True,
linked_orderposition__isnull=False,
)
raw_barcode_for_checkin = raw_barcode
except ReusableMedium.DoesNotExist:
@@ -629,8 +627,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
}, status=400)
else:
linked_event_ids = media.linked_orderpositions.values_list("order__event_id", flat=True).order_by().distinct()
if not any(event_id in list_by_event for event_id in linked_event_ids):
if media.linked_orderposition.order.event_id not in list_by_event:
# Medium exists but connected ticket is for the wrong event
if not simulate:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
@@ -656,34 +653,21 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'checkin_texts': [],
'list': MiniCheckinListSerializer(checkinlists[0]).data,
}, status=404)
op_candidates = []
for op in media.linked_orderpositions.all().select_related("order"):
op_candidates.append(op)
if list_by_event[op.order.event_id].addon_match:
op_candidates += list(op.addons.all())
op_candidates = [media.linked_orderposition]
if list_by_event[media.linked_orderposition.order.event_id].addon_match:
op_candidates += list(media.linked_orderposition.addons.all())
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
# which add-on has the right product.
if len(op_candidates) > 1:
# only check addons if at most one non-addon-op is in op_candidates
# otherwise it is likely a medium linked to multiple orderpositions, which we need to filter based on validity
if len([op for op in op_candidates if not op.addon_to]) <= 1:
op_candidates_matching_product = [
op for op in op_candidates
if (
(list_by_event[op.order.event_id].addon_match or op.secret == raw_barcode or legacy_url_support) and
(list_by_event[op.order.event_id].all_products or op.item_id in {i.pk for i in list_by_event[op.order.event_id].limit_products.all()})
)
]
else:
op_candidates_matching_product = [
op for op in op_candidates
if (
(not op.valid_from or op.valid_from < now()) and
(not op.valid_until or op.valid_until > now())
)
]
op_candidates_matching_product = [
op for op in op_candidates
if (
(list_by_event[op.order.event_id].addon_match or op.secret == raw_barcode or legacy_url_support) and
(list_by_event[op.order.event_id].all_products or op.item_id in {i.pk for i in list_by_event[op.order.event_id].limit_products.all()})
)
]
if len(op_candidates_matching_product) == 0:
# None of the found add-ons has the correct product, too bad! We could just error out here, but

View File

@@ -53,12 +53,10 @@ with scopes_disabled():
customer = django_filters.CharFilter(field_name='customer__identifier')
updated_since = django_filters.IsoDateTimeFilter(field_name='updated', lookup_expr='gte')
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
# backwards-compatible
linked_orderposition = django_filters.NumberFilter(field_name='linked_orderpositions__id')
class Meta:
model = ReusableMedium
fields = ['identifier', 'type', 'active', 'customer', 'linked_orderpositions', 'linked_giftcard']
fields = ['identifier', 'type', 'active', 'customer', 'linked_orderposition', 'linked_giftcard']
class ReusableMediaViewSet(viewsets.ModelViewSet):
@@ -77,7 +75,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
).order_by().values('card').annotate(s=Sum('value')).values('s')
return self.request.organizer.reusable_media.prefetch_related(
Prefetch(
'linked_orderpositions',
'linked_orderposition',
queryset=OrderPosition.objects.select_related(
'order', 'order__event', 'order__event__organizer', 'seat',
).prefetch_related(
@@ -157,6 +155,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
type=s.validated_data["type"],
identifier=s.validated_data["identifier"],
)
m.linked_orderposition = None # not relevant for cross-organizer
m.customer = None # not relevant for cross-organizer
s = self.get_serializer(m)
return Response({"result": s.data})

View File

@@ -193,7 +193,7 @@ with scopes_disabled():
)
).values('id')
matching_media = ReusableMedium.objects.filter(identifier=u).values_list('linked_orderpositions__order_id', flat=True)
matching_media = ReusableMedium.objects.filter(identifier=u).values_list('linked_orderposition__order_id', flat=True)
mainq = (
code
@@ -1030,7 +1030,7 @@ with scopes_disabled():
search = django_filters.CharFilter(method='search_qs')
def search_qs(self, queryset, name, value):
matching_media = ReusableMedium.objects.filter(identifier=value).values_list('linked_orderpositions', flat=True)
matching_media = ReusableMedium.objects.filter(identifier=value).values_list('linked_orderposition', flat=True)
return queryset.filter(
Q(secret__istartswith=value)
| Q(attendee_name_cached__icontains=value)

View File

@@ -20,13 +20,12 @@
# <https://www.gnu.org/licenses/>.
#
from django.db.models import Prefetch
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _, pgettext, pgettext_lazy
from ..exporter import ListExporter, OrganizerLevelExportMixin
from ..models import OrderPosition, ReusableMedium
from ..models import ReusableMedium
from ..signals import register_multievent_data_exporters
@@ -41,9 +40,7 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
media = ReusableMedium.objects.filter(
organizer=self.organizer,
).select_related(
'customer', 'linked_giftcard',
).prefetch_related(
Prefetch('linked_orderpositions', queryset=OrderPosition.objects.select_related("order"))
'customer', 'linked_orderposition', 'linked_giftcard',
).order_by('created')
headers = [
@@ -61,16 +58,17 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
yield self.ProgressSetTotal(total=media.count())
for medium in media.iterator(chunk_size=1000):
yield [
row = [
medium.type,
medium.identifier,
_('Yes') if medium.active else _('No'),
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
medium.customer.identifier if medium.customer_id else '',
', '.join([f"{op.order.code}-{op.positionid}" for op in medium.linked_orderpositions.all()]),
f"{medium.linked_orderposition.order.code}-{medium.linked_orderposition.positionid}" if medium.linked_orderposition_id else '',
medium.linked_giftcard.secret if medium.linked_giftcard_id else '',
medium.notes,
]
yield row
def get_filename(self):
return f'{self.organizer.slug}_media'

View File

@@ -1,35 +0,0 @@
# Generated by Django 4.2.26 on 2025-11-24 11:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0297_outgoingmail"),
]
operations = [
migrations.AddField(
model_name="reusablemedium",
name="claim_token",
field=models.CharField(max_length=200, null=True),
),
migrations.AddField(
model_name="reusablemedium",
name="label",
field=models.CharField(max_length=200, null=True),
),
# use temporary related_name "linked_mediums" for ManyToManyField, so we can migrate existing data
migrations.AddField(
model_name="reusablemedium",
name="linked_orderpositions",
field=models.ManyToManyField(
related_name="linked_mediums", to="pretixbase.orderposition"
),
),
migrations.RunSQL(
sql="INSERT INTO pretixbase_reusablemedium_linked_orderpositions (reusablemedium_id, orderposition_id) SELECT id, linked_orderposition_id FROM pretixbase_reusablemedium WHERE linked_orderposition_id IS NOT NULL;",
reverse_sql="DELETE FROM pretixbase_reusablemedium_linked_orderpositions;",
),
]

View File

@@ -1,44 +0,0 @@
# Generated by Django 4.2.26 on 2025-11-24 11:32
from django.db import migrations, models
def reverse(apps, schema_editor):
ReusableMedium = apps.get_model('pretixbase', 'ReusableMedium')
qs = ReusableMedium.linked_orderpositions.through.objects
objs = []
# get last added orderposition from linked_orderpositions
for rm_id, op_id in qs.filter(id__in=qs.values("reusablemedium_id").annotate(max_id=models.Max('id')).values('max_id')).values_list("reusablemedium_id", "orderposition_id"):
obj = ReusableMedium(
id=rm_id,
linked_orderposition_id=op_id,
)
objs.append(obj)
ReusableMedium.objects.bulk_update(objs, ['linked_orderposition_id'])
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0298_add_reusablemedium_label"),
]
operations = [
# according to the docs, UPDATE FROM should run similarly on sqlite and postgres, but I could not get it to work
# so roll back the data migration with code before deleting data from through-table in 0297
migrations.RunPython(migrations.RunPython.noop, reverse),
migrations.RemoveField(
model_name="reusablemedium",
name="linked_orderposition",
),
# change related_name for new ManyToManyField to previously used linked_media
migrations.AlterField(
model_name="reusablemedium",
name="linked_orderpositions",
field=models.ManyToManyField(
related_name="linked_media", to="pretixbase.orderposition"
),
),
]

View File

@@ -346,7 +346,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
{
'user': self,
'messages': msg,
'url': build_absolute_uri('control:user.settings')
'url': build_absolute_uri('control:user.settings'),
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
user=self,
@@ -391,6 +392,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
'user': self,
'reason': msg,
'code': code,
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
user=self,
@@ -430,6 +432,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
mail(
self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'url': (build_absolute_uri('control:auth.forgot.recover')
+ '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self)))

View File

@@ -72,16 +72,6 @@ class ReusableMedium(LoggedModel):
max_length=200,
verbose_name=pgettext_lazy('reusable_medium', 'Identifier'),
)
claim_token = models.CharField(
max_length=200,
verbose_name=pgettext_lazy('reusable_medium', 'Claim token'),
null=True, blank=True
)
label = models.CharField(
max_length=200,
verbose_name=pgettext_lazy('reusable_medium', 'Label'),
null=True, blank=True
)
active = models.BooleanField(
verbose_name=_('Active'),
@@ -99,14 +89,12 @@ class ReusableMedium(LoggedModel):
on_delete=models.SET_NULL,
verbose_name=_('Customer account'),
)
linked_orderpositions = models.ManyToManyField(
linked_orderposition = models.ForeignKey(
OrderPosition,
null=True, blank=True,
related_name='linked_media',
verbose_name=_('Linked tickets'),
help_text=_(
'If you link to more than one ticket, make sure there is no overlap in validity. '
'If multiple tickets are valid at once, this will lead to failed check-ins.'
)
on_delete=models.SET_NULL,
verbose_name=_('Linked ticket'),
)
linked_giftcard = models.ForeignKey(
GiftCard,

View File

@@ -3495,8 +3495,8 @@ def signal_listener_issue_media(sender: Event, order: Order, **kwargs):
identifier=mt.generate_identifier(sender.organizer),
active=True,
customer=order.customer,
linked_orderposition=p,
)
rm.linked_orderpositions.add(p)
rm.log_action(
'pretix.reusable_medium.created',
data={

View File

@@ -176,6 +176,7 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
_('Data shredding completed'),
'pretixbase/email/shred_completed.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': user,
'organizer': event.organizer.name,
'event': str(event.name),

View File

@@ -13,5 +13,5 @@ Start time: {{ start_time }} (new data added after this time might not have been
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}

View File

@@ -1744,7 +1744,7 @@ class ReusableMediaFilterForm(FilterForm):
Q(identifier__icontains=query)
| Q(customer__identifier__icontains=query)
| Q(customer__external_identifier__istartswith=query)
| Q(linked_orderpositions__order__code__icontains=query)
| Q(linked_orderposition__order__code__icontains=query)
| Q(linked_giftcard__secret__icontains=query)
)

View File

@@ -83,7 +83,7 @@ from pretix.control.forms import ExtFileField, SplitDateTimeField
from pretix.control.forms.event import (
SafeEventMultipleChoiceField, multimail_validate,
)
from pretix.control.forms.widgets import Select2, Select2Multiple
from pretix.control.forms.widgets import Select2
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -246,12 +246,6 @@ class SafeOrderPositionChoiceField(forms.ModelChoiceField):
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
class SafeOrderPositionMultipleChoiceField(forms.ModelMultipleChoiceField):
def __init__(self, queryset, **kwargs):
queryset = queryset.model.all.none()
super().__init__(queryset, **kwargs)
class EventMetaPropertyForm(I18nModelForm):
class Meta:
model = EventMetaProperty
@@ -851,12 +845,12 @@ class ReusableMediumUpdateForm(forms.ModelForm):
class Meta:
model = ReusableMedium
fields = ['active', 'expires', 'customer', 'linked_giftcard', 'linked_orderpositions', 'notes']
fields = ['active', 'expires', 'customer', 'linked_giftcard', 'linked_orderposition', 'notes']
field_classes = {
'expires': SplitDateTimeField,
'customer': SafeModelChoiceField,
'linked_giftcard': SafeModelChoiceField,
'linked_orderpositions': SafeOrderPositionMultipleChoiceField,
'linked_orderposition': SafeOrderPositionChoiceField,
}
widgets = {
'expires': SplitDateTimePickerWidget,
@@ -866,8 +860,8 @@ class ReusableMediumUpdateForm(forms.ModelForm):
super().__init__(*args, **kwargs)
organizer = self.instance.organizer
self.fields['linked_orderpositions'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
self.fields['linked_orderpositions'].widget = Select2Multiple(
self.fields['linked_orderposition'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
self.fields['linked_orderposition'].widget = Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:organizer.ticket_select2', kwargs={
@@ -875,8 +869,8 @@ class ReusableMediumUpdateForm(forms.ModelForm):
}),
}
)
self.fields['linked_orderpositions'].widget.choices = self.fields['linked_orderpositions'].choices
self.fields['linked_orderpositions'].required = False
self.fields['linked_orderposition'].widget.choices = self.fields['linked_orderposition'].choices
self.fields['linked_orderposition'].required = False
self.fields['linked_giftcard'].queryset = organizer.issued_gift_cards.all()
self.fields['linked_giftcard'].widget = Select2(
@@ -930,12 +924,12 @@ class ReusableMediumCreateForm(ReusableMediumUpdateForm):
class Meta:
model = ReusableMedium
fields = ['active', 'type', 'identifier', 'expires', 'linked_orderpositions', 'linked_giftcard', 'customer', 'notes']
fields = ['active', 'type', 'identifier', 'expires', 'linked_orderposition', 'linked_giftcard', 'customer', 'notes']
field_classes = {
'expires': SplitDateTimeField,
'customer': SafeModelChoiceField,
'linked_giftcard': SafeModelChoiceField,
'linked_orderpositions': SafeOrderPositionMultipleChoiceField,
'linked_orderposition': SafeOrderPositionChoiceField,
}
widgets = {
'expires': SplitDateTimePickerWidget,

View File

@@ -518,6 +518,7 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl
'The order requires approval before it can continue to be processed.'),
'pretix.event.order.approved': _('The order has been approved.'),
'pretix.event.order.denied': _('The order has been denied (comment: "{comment}").'),
'pretix.event.order.vatid.validated': _('The customer VAT ID has been verified.'),
'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" '
'to "{new_email}".'),
'pretix.event.order.contact.confirmed': _(
@@ -741,7 +742,6 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.reusable_medium.created': _('The reusable medium has been created.'),
'pretix.reusable_medium.created.auto': _('The reusable medium has been created automatically.'),
'pretix.reusable_medium.changed': _('The reusable medium has been changed.'),
'pretix.reusable_medium.linked_orderposition.added': _('A new ticket has been added to the medium.'),
'pretix.reusable_medium.linked_orderposition.changed': _('The medium has been connected to a new ticket.'),
'pretix.reusable_medium.linked_giftcard.changed': _('The medium has been connected to a new gift card.'),
'pretix.email.error': _('Sending of an email has failed.'),

View File

@@ -9,5 +9,5 @@ Please do never give this code to another person. Our support team will never as
If this code was not requested by you, please contact us immediately.
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}

View File

@@ -5,5 +5,5 @@ you requested a new password. Please go to the following page to reset your pass
{{ url }}
Best regards,
Your pretix team
{% endblocktrans %}
Your {{ instance }} team
{% endblocktrans %}

View File

@@ -1,6 +1,6 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
you have been invited to a team on pretix, a platform to perform event
you have been invited to a team on {{ instance }}, a platform to perform event
ticket sales.
Organizer: {{ organizer }}
@@ -13,5 +13,5 @@ If you do not want to join, you can safely ignore or delete this email.
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}

View File

@@ -1,6 +1,6 @@
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
this is to inform you that the account information of your pretix account has been
this is to inform you that the account information of your {{ instance }} account has been
changed. In particular, the following changes have been performed:
{{ messages }}
@@ -12,5 +12,5 @@ You can review and change your account settings here:
{{ url }}
Best regards,
Your pretix team
Your {{ instance }} team
{% endblocktrans %}

View File

@@ -54,8 +54,8 @@
<a href="?{% url_replace request 'ordering' 'identifier' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>{% trans "Media type" context "reusable_media" %}
<a href="?{% url_replace request 'ordering' '-type' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'type' %}"><i class="fa fa-caret-up"></i></a></th>
<a href="?{% url_replace request 'ordering' '-email' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'email' %}"><i class="fa fa-caret-up"></i></a></th>
<th>{% trans "Connections" context "reusable_media" %}</th>
<th></th>
</tr>
@@ -82,13 +82,13 @@
</a>
</span>
{% endif %}
{% for op in m.linked_orderpositions.all %}
{% if m.linked_orderposition %}
<span class="helper-display-block">
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=op.order.event.slug organizer=request.organizer.slug code=op.order.code %}">
{{ op.order.code }}</a>-{{ op.positionid }}
<a href="{% url "control:event.order" event=m.linked_orderposition.order.event.slug organizer=request.organizer.slug code=m.linked_orderposition.order.code %}">
{{ m.linked_orderposition.order.code }}</a>-{{ m.linked_orderposition.positionid }}
</span>
{% endfor %}
{% endif %}
{% if m.linked_giftcard %}
<span class="helper-display-block">
<span class="fa fa-credit-card fa-fw"></span>

View File

@@ -28,19 +28,7 @@
<dt>{% trans "Media type" context "reusable_media" %}</dt>
<dd>{{ medium.get_type_display }}</dd>
<dt>{% trans "Identifier" context "reusable_media" %}</dt>
<dd>
<code id="medium_identifier">{{ medium.identifier }}</code>
<button type="button" class="btn btn-default btn-xs btn-clipboard js-only" data-clipboard-target="#medium_identifier">
<i class="fa fa-clipboard" aria-hidden="true"></i>
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
</button>
{% if medium.type == "barcode" %}
<button type="button" class="btn btn-default btn-xs js-only" data-toggle="qrcode" data-qrcode="{{ medium.identifier }}">
<i class="fa fa-qrcode" aria-hidden="true"></i>
<span class="sr-only">{% trans "Create QR code" %}</span>
</button>
{% endif %}
</dd>
<dd><code>{{ medium.identifier }}</code></dd>
<dt>{% trans "Status" %}</dt>
<dd>
{% if not medium.active %}
@@ -61,13 +49,13 @@
</a>
</span>
{% endif %}
{% for op in medium.linked_orderpositions.all %}
{% if medium.linked_orderposition %}
<span class="helper-display-block">
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=op.order.event.slug organizer=request.organizer.slug code=op.order.code %}">
{{ op.order.code }}</a>-{{ op.positionid }}
</span>
{% endfor %}
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=medium.linked_orderposition.order.event.slug organizer=request.organizer.slug code=medium.linked_orderposition.order.code %}">
{{ medium.linked_orderposition.order.code }}</a>-{{ medium.linked_orderposition.positionid }}
</span>
{% endif %}
{% if medium.linked_giftcard %}
<span class="helper-display-block">
<span class="fa fa-credit-card fa-fw"></span>

View File

@@ -1641,9 +1641,17 @@ class OrderCheckVATID(OrderView):
try:
normalized_id = validate_vat_id(ia.vat_id, str(ia.country))
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
with transaction.atomic():
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
self.order.log_action(
'pretix.event.order.vatid.validated',
data={
'vat_id': normalized_id,
},
user=self.request.user,
)
except VATIDFinalError as e:
messages.error(self.request, e.message)
except VATIDTemporaryError:

View File

@@ -1039,9 +1039,10 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
def _send_invite(self, instance):
mail(
instance.email,
_('pretix account invitation'),
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.request.organizer.name,
'team': instance.team.name,
@@ -3338,10 +3339,8 @@ class ReusableMediaListView(OrganizerDetailViewMixin, OrganizerPermissionRequire
def get_queryset(self):
qs = self.request.organizer.reusable_media.select_related(
'customer',
'linked_giftcard',
).prefetch_related(
Prefetch('linked_orderpositions', queryset=OrderPosition.objects.select_related("order", "order__event"))
'customer', 'linked_orderposition', 'linked_orderposition__order', 'linked_orderposition__order__event',
'linked_giftcard'
)
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
@@ -3389,14 +3388,10 @@ class ReusableMediumCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
@transaction.atomic
def form_valid(self, form):
r = super().form_valid(form)
data = {
form.instance.log_action('pretix.reusable_medium.created', user=self.request.user, data={
k: getattr(form.instance, k)
for k in form.changed_data
}
if "linked_orderpositions" in data:
data["linked_orderpositions"] = data["linked_orderpositions"].values_list("pk", flat=True)
form.instance.log_action('pretix.reusable_medium.created', user=self.request.user, data=data)
})
messages.success(self.request, _('Your changes have been saved.'))
return r
@@ -3422,13 +3417,10 @@ class ReusableMediumUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
@transaction.atomic
def form_valid(self, form):
if form.has_changed():
data = {
self.object.log_action('pretix.reusable_medium.changed', user=self.request.user, data={
k: getattr(self.object, k)
for k in form.changed_data
}
if "linked_orderpositions" in data:
data["linked_orderpositions"] = data["linked_orderpositions"].values_list("pk", flat=True)
self.object.log_action('pretix.reusable_medium.changed', user=self.request.user, data=data)
})
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)

View File

@@ -286,12 +286,12 @@ def test_by_secret_special_chars(token_client, organizer, clist, event, order):
@pytest.mark.django_db
def test_by_medium(token_client, organizer, clist, event, order):
with scopes_disabled():
rm = ReusableMedium.objects.create(
ReusableMedium.objects.create(
type="barcode",
identifier="abcdef",
organizer=organizer,
linked_orderposition=order.positions.first(),
)
rm.linked_orderpositions.add(order.positions.first())
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 201
assert resp.data['status'] == 'ok'
@@ -301,48 +301,6 @@ def test_by_medium(token_client, organizer, clist, event, order):
assert ci.raw_source_type == "barcode"
@pytest.mark.django_db
def test_by_medium_multiple_orderpositions(token_client, organizer, clist, event, order):
with scopes_disabled():
rm = ReusableMedium.objects.create(
type="barcode",
identifier="abcdef",
organizer=organizer,
)
rm.linked_orderpositions.add(order.positions.first())
op_item_other = order.positions.all()[1]
rm.linked_orderpositions.add(op_item_other)
# multiple tickets are valid => no check-in
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 400
assert resp.data['status'] == 'error'
assert resp.data['reason'] == 'ambiguous'
with scopes_disabled():
op_item_other.valid_from = datetime.datetime(2020, 1, 1, 12, 0, 0, tzinfo=event.timezone)
op_item_other.valid_until = datetime.datetime(2020, 1, 1, 15, 0, 0, tzinfo=event.timezone)
op_item_other.save()
with freeze_time("2020-01-01 13:45:00"):
# multiple tickets are valid => no check-in
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 400
assert resp.data['status'] == 'error'
assert resp.data['reason'] == 'ambiguous'
with freeze_time("2020-01-01 10:45:00"):
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 201
assert resp.data['status'] == 'ok'
with freeze_time("2020-01-01 15:45:00"):
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 400
assert resp.data['status'] == 'error'
assert resp.data['reason'] == 'already_redeemed'
@pytest.mark.django_db
def test_by_medium_not_connected(token_client, organizer, clist, event, order):
with scopes_disabled():
@@ -360,12 +318,12 @@ def test_by_medium_not_connected(token_client, organizer, clist, event, order):
@pytest.mark.django_db
def test_by_medium_wrong_event(token_client, organizer, clist, event, order2):
with scopes_disabled():
rm = ReusableMedium.objects.create(
ReusableMedium.objects.create(
type="barcode",
identifier="abcdef",
organizer=organizer,
linked_orderposition=order2.positions.first(),
)
rm.linked_orderpositions.add(order2.positions.first())
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 404
assert resp.data['status'] == 'error'
@@ -379,12 +337,12 @@ def test_by_medium_wrong_event(token_client, organizer, clist, event, order2):
@pytest.mark.django_db
def test_by_medium_wrong_type(token_client, organizer, clist, event, order):
with scopes_disabled():
rm = ReusableMedium.objects.create(
ReusableMedium.objects.create(
type="nfc_uid",
identifier="abcdef",
organizer=organizer,
linked_orderposition=order.positions.first(),
)
rm.linked_orderpositions.add(order.positions.first())
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 404
assert resp.data['status'] == 'error'
@@ -397,13 +355,13 @@ def test_by_medium_wrong_type(token_client, organizer, clist, event, order):
@pytest.mark.django_db
def test_by_medium_inactive(token_client, organizer, clist, event, order):
with scopes_disabled():
rm = ReusableMedium.objects.create(
ReusableMedium.objects.create(
type="barcode",
identifier="abcdef",
organizer=organizer,
active=False,
linked_orderposition=order.positions.first(),
)
rm.linked_orderpositions.add(order.positions.first())
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 404
assert resp.data['status'] == 'error'

View File

@@ -3086,78 +3086,9 @@ def test_order_create_use_medium(token_client, organizer, event, item, quota, qu
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
medium.refresh_from_db()
assert o.positions.first() == medium.linked_orderpositions.first()
assert o.positions.first() == medium.linked_orderposition
assert resp.data['positions'][0]['pdf_data']['medium_identifier'] == medium.identifier
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
medium.refresh_from_db()
assert medium.linked_orderpositions.count() == 1
assert o.positions.first() == medium.linked_orderpositions.first()
assert resp.data['positions'][0]['pdf_data']['medium_identifier'] == medium.identifier
@pytest.mark.django_db
def test_order_create_add_to_medium(token_client, organizer, event, item, quota, question, medium):
item.media_type = medium.type
item.media_policy = Item.MEDIA_POLICY_REUSE_OR_NEW
item.save()
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['use_reusable_medium'] = medium.pk
res['positions'][0]['add_to_reusable_medium'] = medium.pk
res['positions'][0]['answers'][0]['question'] = question.pk
# do not use use_reusable_medium and add_to_reusable_medium
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
del res['positions'][0]['use_reusable_medium']
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
medium.refresh_from_db()
assert medium.linked_orderpositions.count() == 1
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
medium.refresh_from_db()
assert medium.linked_orderpositions.count() == 2
res['positions'][0]['use_reusable_medium'] = medium.pk
del res['positions'][0]['add_to_reusable_medium']
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
medium.refresh_from_db()
assert medium.linked_orderpositions.count() == 1
assert o.positions.first() == medium.linked_orderpositions.first()
@pytest.mark.django_db
def test_order_create_use_medium_other_organizer(token_client, organizer, event, item, quota, question, medium2):
@@ -3202,7 +3133,7 @@ def test_order_create_create_medium(token_client, organizer, event, item, quota,
i = resp.data['positions'][0]['pdf_data']['medium_identifier']
assert i
m = organizer.reusable_media.get(identifier=i)
assert m.linked_orderpositions.first() == o.positions.first()
assert m.linked_orderposition == o.positions.first()
assert m.type == "barcode"

View File

@@ -89,13 +89,10 @@ TEST_MEDIUM_RES = {
"organizer": "dummy",
"identifier": "ABCDEFGH",
"type": "barcode",
"claim_token": None,
"label": None,
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": None,
"linked_orderpositions": [],
"linked_giftcard": None,
"notes": None,
"info": {},
@@ -141,7 +138,7 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
personalized=True)
op = o.positions.create(item=ticket, price=Decimal("14"))
medium.linked_orderpositions.add(op)
medium.linked_orderposition = op
medium.linked_giftcard = giftcard
medium.customer = customer
medium.save()
@@ -422,7 +419,7 @@ def test_medium_lookup_cross_organizer(token_client, organizer, organizer2, org2
ticket = org2_event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
personalized=True)
op = o.positions.create(item=ticket, price=Decimal("14"))
medium2.linked_orderpositions.add(op)
medium2.linked_orderposition = op
medium2.linked_giftcard = giftcard2
medium2.save()