mirror of
https://github.com/pretix/pretix.git
synced 2026-05-04 15:04:03 +00:00
116
src/pretix/base/media.py
Normal file
116
src/pretix/base/media.py
Normal file
@@ -0,0 +1,116 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from django.db import transaction
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class BaseMediaType:
|
||||
medium_created_by_server = False
|
||||
supports_orderposition = False
|
||||
supports_giftcard = False
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def generate_identifier(self, organizer):
|
||||
if self.medium_created_by_server:
|
||||
raise NotImplementedError()
|
||||
else:
|
||||
raise ValueError("Media type does not allow to generate identifier")
|
||||
|
||||
def is_active(self, organizer):
|
||||
return organizer.settings.get(f'reusable_media_type_{self.identifier}', as_type=bool, default=False)
|
||||
|
||||
def handle_unknown(self, organizer, identifier, user, auth):
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return str(self.verbose_name)
|
||||
|
||||
|
||||
class BarcodePlainMediaType(BaseMediaType):
|
||||
identifier = 'barcode'
|
||||
verbose_name = _('Barcode / QR-Code')
|
||||
medium_created_by_server = True
|
||||
supports_giftcard = False
|
||||
supports_orderposition = True
|
||||
|
||||
def generate_identifier(self, organizer):
|
||||
return get_random_string(
|
||||
length=organizer.settings.reusable_media_type_barcode_identifier_length,
|
||||
# Exclude o,0,1,i to avoid confusion with bad fonts/printers
|
||||
# We use upper case to make collisions with ticket secrets less likely
|
||||
allowed_chars='ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||
)
|
||||
|
||||
|
||||
class NfcUidMediaType(BaseMediaType):
|
||||
identifier = 'nfc_uid'
|
||||
verbose_name = _('NFC UID-based')
|
||||
medium_created_by_server = False
|
||||
supports_giftcard = True
|
||||
supports_orderposition = False
|
||||
|
||||
def handle_unknown(self, organizer, identifier, user, auth):
|
||||
from pretix.base.models import GiftCard, ReusableMedium
|
||||
|
||||
if organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard', as_type=bool):
|
||||
if identifier.startswith("08"):
|
||||
# Don't create gift cards for NFC UIDs that start with 08, which represents NFC cards that issue random
|
||||
# UIDs on every read, so they won't be useful.
|
||||
return
|
||||
with transaction.atomic():
|
||||
gc = GiftCard.objects.create(
|
||||
issuer=organizer,
|
||||
expires=organizer.default_gift_card_expiry,
|
||||
currency=organizer.settings.get(f'reusable_media_type_{self.identifier}_autocreate_giftcard_currency'),
|
||||
)
|
||||
m = ReusableMedium.objects.create(
|
||||
type=self.identifier,
|
||||
identifier=identifier,
|
||||
organizer=organizer,
|
||||
active=True,
|
||||
linked_giftcard=gc
|
||||
)
|
||||
m.log_action(
|
||||
'pretix.reusable_medium.created.auto',
|
||||
user=user, auth=auth,
|
||||
)
|
||||
gc.log_action(
|
||||
'pretix.giftcards.created',
|
||||
user=user, auth=auth,
|
||||
)
|
||||
return m
|
||||
|
||||
|
||||
MEDIA_TYPES = {
|
||||
m.identifier: m for m in [
|
||||
BarcodePlainMediaType(),
|
||||
NfcUidMediaType(),
|
||||
]
|
||||
}
|
||||
77
src/pretix/base/migrations/0236_reusable_media.py
Normal file
77
src/pretix/base/migrations/0236_reusable_media.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# Generated by Django 3.2.18 on 2023-02-20 12:46
|
||||
|
||||
import django.core.serializers.json
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.base
|
||||
|
||||
|
||||
def set_can_manage_reusable_media(apps, schema_editor):
|
||||
Team = apps.get_model('pretixbase', 'Team')
|
||||
Team.objects.filter(can_change_organizer_settings=True).update(can_manage_reusable_media=True)
|
||||
Team.objects.filter(can_change_orders=True, all_events=True).update(can_manage_reusable_media=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0235_auto_20230316_2023'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='media_policy',
|
||||
field=models.CharField(max_length=16, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='media_type',
|
||||
field=models.CharField(max_length=100, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='team',
|
||||
name='can_manage_reusable_media',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReusableMedium',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('type', models.CharField(max_length=100)),
|
||||
('identifier', models.CharField(max_length=200)),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('expires', models.DateTimeField(blank=True, null=True)),
|
||||
('info', models.JSONField(default=dict)),
|
||||
('notes', models.TextField(null=True, blank=True)),
|
||||
('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='reusable_media', to='pretixbase.customer')),
|
||||
('linked_giftcard',
|
||||
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='linked_media',
|
||||
to='pretixbase.giftcard')),
|
||||
('linked_orderposition',
|
||||
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='linked_media',
|
||||
to='pretixbase.orderposition')),
|
||||
('organizer',
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reusable_media',
|
||||
to='pretixbase.organizer')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('identifier', 'type', 'organizer'),
|
||||
'unique_together': {('identifier', 'type', 'organizer')},
|
||||
'index_together': {('identifier', 'type', 'organizer'), ('updated', 'id')},
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='raw_source_type',
|
||||
field=models.CharField(max_length=100, null=True),
|
||||
),
|
||||
migrations.RunPython(
|
||||
set_can_manage_reusable_media,
|
||||
migrations.RunPython.noop,
|
||||
),
|
||||
]
|
||||
@@ -40,6 +40,7 @@ from .items import (
|
||||
SubEventItem, SubEventItemVariation, itempicture_upload_to,
|
||||
)
|
||||
from .log import LogEntry
|
||||
from .media import ReusableMedium
|
||||
from .memberships import Membership, MembershipType
|
||||
from .notifications import NotificationSetting
|
||||
from .orders import (
|
||||
|
||||
@@ -44,6 +44,7 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import LoggedModel
|
||||
from pretix.base.models.fields import MultiStringField
|
||||
from pretix.helpers import PostgresWindowFrame
|
||||
@@ -377,6 +378,11 @@ class Checkin(models.Model):
|
||||
# For "raw" scans where we do not know which position they belong to (e.g. scan of signed
|
||||
# barcode that is not in database).
|
||||
raw_barcode = models.TextField(null=True, blank=True)
|
||||
raw_source_type = models.CharField(
|
||||
max_length=100,
|
||||
null=True, blank=True,
|
||||
choices=[(k, v) for k, v in MEDIA_TYPES.items()],
|
||||
)
|
||||
raw_item = models.ForeignKey(
|
||||
'pretixbase.Item',
|
||||
related_name='checkins',
|
||||
|
||||
@@ -142,6 +142,7 @@ class Customer(LoggedModel):
|
||||
self.save()
|
||||
self.all_logentries().update(data={}, shredded=True)
|
||||
self.orders.all().update(customer=None)
|
||||
self.reusable_media.all().update(customer=None)
|
||||
self.memberships.all().update(attendee_name_parts=None)
|
||||
self.attendee_profiles.all().delete()
|
||||
self.invoice_addresses.all().delete()
|
||||
|
||||
@@ -180,7 +180,8 @@ class Device(LoggedModel):
|
||||
'can_view_orders',
|
||||
'can_change_orders',
|
||||
'can_view_vouchers',
|
||||
'can_manage_gift_cards'
|
||||
'can_manage_gift_cards',
|
||||
'can_manage_reusable_media',
|
||||
}
|
||||
|
||||
def get_event_permission_set(self, organizer, event) -> set:
|
||||
|
||||
@@ -64,6 +64,7 @@ from pretix.base.models.fields import MultiStringField
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
|
||||
from ...helpers.images import ImageSizeValidator
|
||||
from ..media import MEDIA_TYPES
|
||||
from .event import Event, SubEvent
|
||||
|
||||
|
||||
@@ -368,6 +369,16 @@ class Item(LoggedModel):
|
||||
(VALIDITY_MODE_DYNAMIC, _('Dynamic validity')),
|
||||
)
|
||||
|
||||
MEDIA_POLICY_REUSE = 'reuse'
|
||||
MEDIA_POLICY_NEW = 'new'
|
||||
MEDIA_POLICY_REUSE_OR_NEW = 'reuse_or_new'
|
||||
MEDIA_POLICIES = (
|
||||
(None, _("Don't use re-usable media, use regular one-off tickets")),
|
||||
(MEDIA_POLICY_REUSE, _('Require an existing medium to be re-used')),
|
||||
(MEDIA_POLICY_NEW, _('Require a previously unknown medium to be newly added')),
|
||||
(MEDIA_POLICY_REUSE_OR_NEW, _('Require either an existing or a new medium to be used')),
|
||||
)
|
||||
|
||||
objects = ItemQuerySetManager()
|
||||
|
||||
event = models.ForeignKey(
|
||||
@@ -630,6 +641,29 @@ class Item(LoggedModel):
|
||||
help_text=_('The selected start date may only be this many days in the future.')
|
||||
)
|
||||
|
||||
media_policy = models.CharField(
|
||||
choices=MEDIA_POLICIES,
|
||||
null=True, blank=True, max_length=16,
|
||||
verbose_name=_('Reusable media policy'),
|
||||
help_text=_(
|
||||
'If this product should be stored on a re-usable physical medium, you can attach a physical media policy. '
|
||||
'This is not required for regular tickets, which just use a one-time barcode, but only for products like '
|
||||
'renewable season tickets or re-chargable gift card wristbands. '
|
||||
'This is an advanced feature that also requires specific configuration of ticketing and printing settings.'
|
||||
)
|
||||
)
|
||||
media_type = models.CharField(
|
||||
max_length=100,
|
||||
null=True, blank=True,
|
||||
choices=[(None, _("Don't use re-usable media, use regular one-off tickets"))] + [(k, v) for k, v in MEDIA_TYPES.items()],
|
||||
verbose_name=_('Reusable media type'),
|
||||
help_text=_(
|
||||
'Select the type of physical medium that should be used for this product. Note that not all media types '
|
||||
'support all types of products, and not all media types are supported across all sales channels or '
|
||||
'check-in processes.'
|
||||
)
|
||||
)
|
||||
|
||||
# !!! Attention: If you add new fields here, also add them to the copying code in
|
||||
# pretix/control/forms/item.py if applicable.
|
||||
|
||||
@@ -801,6 +835,24 @@ class Item(LoggedModel):
|
||||
def has_variations(self):
|
||||
return self.variations.exists()
|
||||
|
||||
@staticmethod
|
||||
def clean_media_settings(event, media_policy, media_type, issue_giftcard):
|
||||
if media_policy:
|
||||
if not media_type:
|
||||
raise ValidationError(_('If you select a reusable media policy, you also need to select a reusable '
|
||||
'media type.'))
|
||||
mt = MEDIA_TYPES[media_type]
|
||||
if not mt.is_active(event.organizer):
|
||||
raise ValidationError(_('The selected media type is not enabled in your organizer settings.'))
|
||||
if not mt.supports_orderposition and not issue_giftcard:
|
||||
raise ValidationError(_('The selected media type does not support usage for tickets currently.'))
|
||||
if not mt.supports_giftcard and issue_giftcard:
|
||||
raise ValidationError(_('The selected media type does not support usage for gift cards currently.'))
|
||||
if issue_giftcard:
|
||||
raise ValidationError(_('You currently cannot create gift cards with a reusable media policy. Instead, '
|
||||
'gift cards for some reusable media types can be created or re-charged directly '
|
||||
'at the POS.'))
|
||||
|
||||
@staticmethod
|
||||
def clean_per_order(min_per_order, max_per_order):
|
||||
if min_per_order is not None and max_per_order is not None:
|
||||
|
||||
125
src/pretix/base/models/media.py
Normal file
125
src/pretix/base/models/media.py
Normal file
@@ -0,0 +1,125 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import LoggedModel
|
||||
from pretix.base.models.customers import Customer
|
||||
from pretix.base.models.giftcards import GiftCard
|
||||
from pretix.base.models.orders import OrderPosition
|
||||
from pretix.base.models.organizer import Organizer
|
||||
|
||||
|
||||
class ReusableMediumQuerySet(models.QuerySet):
|
||||
|
||||
def active(self):
|
||||
return self.filter(
|
||||
Q(expires__isnull=True) | Q(expires__gte=now()),
|
||||
active=True,
|
||||
)
|
||||
|
||||
|
||||
class ReusableMediumQuerySetManager(ScopedManager(organizer='organizer').__class__):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._queryset_class = ReusableMediumQuerySet
|
||||
|
||||
def active(self):
|
||||
return self.get_queryset().active()
|
||||
|
||||
|
||||
class ReusableMedium(LoggedModel):
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
updated = models.DateTimeField(auto_now=True)
|
||||
|
||||
organizer = models.ForeignKey(
|
||||
Organizer,
|
||||
related_name='reusable_media',
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
|
||||
type = models.CharField(
|
||||
verbose_name=pgettext_lazy('reusable_medium', 'Media type'),
|
||||
choices=((k, v) for k, v in MEDIA_TYPES.items()),
|
||||
max_length=100,
|
||||
)
|
||||
identifier = models.CharField(
|
||||
max_length=200,
|
||||
verbose_name=pgettext_lazy('reusable_medium', 'Identifier'),
|
||||
)
|
||||
|
||||
active = models.BooleanField(
|
||||
verbose_name=_('Active'),
|
||||
default=True
|
||||
)
|
||||
expires = models.DateTimeField(
|
||||
verbose_name=_('Expiration date'),
|
||||
null=True, blank=True
|
||||
)
|
||||
|
||||
customer = models.ForeignKey(
|
||||
Customer,
|
||||
null=True, blank=True,
|
||||
related_name='reusable_media',
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_('Customer account'),
|
||||
)
|
||||
linked_orderposition = models.ForeignKey(
|
||||
OrderPosition,
|
||||
null=True, blank=True,
|
||||
related_name='linked_media',
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_('Linked ticket'),
|
||||
)
|
||||
linked_giftcard = models.ForeignKey(
|
||||
GiftCard,
|
||||
null=True, blank=True,
|
||||
related_name='linked_media',
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_('Linked gift card'),
|
||||
)
|
||||
|
||||
info = models.JSONField(
|
||||
default=dict
|
||||
)
|
||||
notes = models.TextField(verbose_name=_('Notes'), null=True, blank=True)
|
||||
|
||||
objects = ReusableMediumQuerySetManager()
|
||||
|
||||
@cached_property
|
||||
def media_type(self):
|
||||
return MEDIA_TYPES[self.type]
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
return self.expires and self.expires > now()
|
||||
|
||||
class Meta:
|
||||
unique_together = (("identifier", "type", "organizer"),)
|
||||
index_together = (("identifier", "type", "organizer"), ("updated", "id"))
|
||||
ordering = "identifier", "type", "organizer"
|
||||
@@ -236,6 +236,8 @@ class Team(LoggedModel):
|
||||
:type can_change_teams: bool
|
||||
:param can_manage_customers: If ``True``, the members can view and change organizer-level customer accounts.
|
||||
:type can_manage_customers: bool
|
||||
:param can_manage_reusable_media: If ``True``, the members can view and change organizer-level reusable media.
|
||||
:type can_manage_reusable_media: bool
|
||||
:param can_change_organizer_settings: If ``True``, the members can change the settings of this organizer account.
|
||||
:type can_change_organizer_settings: bool
|
||||
:param can_change_event_settings: If ``True``, the members can change the settings of the associated events.
|
||||
@@ -277,6 +279,10 @@ class Team(LoggedModel):
|
||||
default=False,
|
||||
verbose_name=_("Can manage customer accounts")
|
||||
)
|
||||
can_manage_reusable_media = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can manage reusable media")
|
||||
)
|
||||
can_manage_gift_cards = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can manage gift cards")
|
||||
|
||||
@@ -455,6 +455,11 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
"TIME_FORMAT"
|
||||
) if op.valid_until else ""
|
||||
}),
|
||||
("medium_identifier", {
|
||||
"label": _("Reusable Medium ID"),
|
||||
"editor_sample": _("ABC1234DEF4567"),
|
||||
"evaluate": lambda op, order, ev: op.linked_media.all()[0].identifier if op.linked_media.all() else "",
|
||||
}),
|
||||
("seat", {
|
||||
"label": _("Seat: Full name"),
|
||||
"editor_sample": _("Ground floor, Row 3, Seat 4"),
|
||||
@@ -761,6 +766,9 @@ class Renderer:
|
||||
else:
|
||||
content = self._get_text_content(op, order, o)
|
||||
|
||||
if len(content) == 0:
|
||||
return
|
||||
|
||||
level = 'H'
|
||||
if len(content) > 32:
|
||||
level = 'M'
|
||||
|
||||
@@ -52,6 +52,7 @@ from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, InvoiceAddress, Item, ItemVariation, Seat,
|
||||
SeatCategoryMapping, Voucher,
|
||||
@@ -199,6 +200,8 @@ error_messages = {
|
||||
'seat_multiple': gettext_lazy('You can not select the same seat multiple times.'),
|
||||
'gift_card': gettext_lazy("You entered a gift card instead of a voucher. Gift cards can be entered later on when you're asked for your payment details."),
|
||||
'country_blocked': gettext_lazy('One of the selected products is not available in the selected country.'),
|
||||
'media_usage_not_implemented': gettext_lazy('The configuration of this product requires mapping to a physical '
|
||||
'medium, which is currently not available online.'),
|
||||
}
|
||||
|
||||
|
||||
@@ -394,6 +397,13 @@ class CartManager:
|
||||
if not op.item.is_available() or (op.variation and not op.variation.is_available()):
|
||||
raise CartError(error_messages['unavailable'])
|
||||
|
||||
if op.item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
|
||||
mt = MEDIA_TYPES[op.item.media_type]
|
||||
if not mt.medium_created_by_server:
|
||||
raise CartError(error_messages['media_usage_not_implemented'])
|
||||
elif op.item.media_policy == Item.MEDIA_POLICY_REUSE:
|
||||
raise CartError(error_messages['media_usage_not_implemented'])
|
||||
|
||||
if self._sales_channel not in op.item.sales_channels or (op.variation and self._sales_channel not in op.variation.sales_channels):
|
||||
raise CartError(error_messages['unavailable'])
|
||||
|
||||
|
||||
@@ -693,7 +693,7 @@ def _save_answers(op, answers, given_answers):
|
||||
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
|
||||
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
||||
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
|
||||
raw_barcode=None, from_revoked_secret=False):
|
||||
raw_barcode=None, raw_source_type=None, from_revoked_secret=False):
|
||||
"""
|
||||
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
|
||||
not valid at this time.
|
||||
@@ -870,6 +870,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
forced=force and (not entry_allowed or from_revoked_secret or force_used),
|
||||
force_sent=force,
|
||||
raw_barcode=raw_barcode,
|
||||
raw_source_type=raw_source_type,
|
||||
)
|
||||
op.order.log_action('pretix.event.checkin', data={
|
||||
'position': op.id,
|
||||
|
||||
@@ -62,6 +62,7 @@ from pretix.api.models import OAuthApplication
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import get_language_without_region, language
|
||||
from pretix.base.media import MEDIA_TYPES
|
||||
from pretix.base.models import (
|
||||
CartPosition, Device, Event, GiftCard, Item, ItemVariation, Membership,
|
||||
Order, OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User,
|
||||
@@ -2911,3 +2912,32 @@ def signal_listener_issue_memberships(sender: Event, order: Order, **kwargs):
|
||||
for p in order.positions.all():
|
||||
if p.item.grant_membership_type_id:
|
||||
create_membership(order.customer, p)
|
||||
|
||||
|
||||
@receiver(order_placed, dispatch_uid="pretixbase_order_placed_media")
|
||||
@receiver(order_changed, dispatch_uid="pretixbase_order_changed_media")
|
||||
@transaction.atomic()
|
||||
def signal_listener_issue_media(sender: Event, order: Order, **kwargs):
|
||||
from pretix.base.models import ReusableMedium
|
||||
|
||||
for p in order.positions.all():
|
||||
if p.item.media_policy in (Item.MEDIA_POLICY_NEW, Item.MEDIA_POLICY_REUSE_OR_NEW):
|
||||
mt = MEDIA_TYPES[p.item.media_type]
|
||||
if mt.medium_created_by_server and not p.linked_media.exists():
|
||||
rm = ReusableMedium.objects.create(
|
||||
organizer=sender.organizer,
|
||||
type=p.item.media_type,
|
||||
identifier=mt.generate_identifier(sender.organizer),
|
||||
active=True,
|
||||
customer=order.customer,
|
||||
linked_orderposition=p,
|
||||
)
|
||||
rm.log_action(
|
||||
'pretix.reusable_medium.created',
|
||||
data={
|
||||
'by_order': order.code,
|
||||
'linked_orderposition': p.pk,
|
||||
'active': True,
|
||||
'customer': order.customer_id,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -169,6 +169,84 @@ DEFAULTS = {
|
||||
"was not logged in during the purchase.")
|
||||
)
|
||||
},
|
||||
'reusable_media_active': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Activate re-usable media"),
|
||||
help_text=_("The re-usable media feature allows you to connect tickets and gift cards with physical media "
|
||||
"such as wristbands or chip cards that may be re-used for different tickets or gift cards "
|
||||
"later.")
|
||||
)
|
||||
},
|
||||
'reusable_media_type_barcode': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Active"),
|
||||
)
|
||||
},
|
||||
'reusable_media_type_barcode_identifier_length': {
|
||||
'default': 24,
|
||||
'type': int,
|
||||
'form_class': forms.IntegerField,
|
||||
'serializer_class': serializers.IntegerField,
|
||||
'serializer_kwargs': dict(
|
||||
validators=[
|
||||
MinValueValidator(12),
|
||||
MaxValueValidator(64),
|
||||
]
|
||||
),
|
||||
'form_kwargs': dict(
|
||||
label=_('Length of barcodes'),
|
||||
validators=[
|
||||
MinValueValidator(12),
|
||||
MaxValueValidator(64),
|
||||
],
|
||||
required=True,
|
||||
widget=forms.NumberInput(
|
||||
attrs={
|
||||
'min': '12',
|
||||
'max': '64',
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
'reusable_media_type_nfc_uid': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Active"),
|
||||
)
|
||||
},
|
||||
'reusable_media_type_nfc_uid_autocreate_giftcard': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_("Automatically create a new gift card if a previously unknown chip is seen"),
|
||||
)
|
||||
},
|
||||
'reusable_media_type_nfc_uid_autocreate_giftcard_currency': {
|
||||
'default': 'EUR',
|
||||
'type': str,
|
||||
'form_class': forms.ChoiceField,
|
||||
'serializer_class': serializers.ChoiceField,
|
||||
'serializer_kwargs': dict(
|
||||
choices=[(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES],
|
||||
),
|
||||
'form_kwargs': dict(
|
||||
choices=[(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES],
|
||||
label=_("Gift card currency"),
|
||||
)
|
||||
},
|
||||
'max_items_per_order': {
|
||||
'default': '10',
|
||||
'type': int,
|
||||
@@ -3416,7 +3494,12 @@ def validate_organizer_settings(organizer, settings_dict):
|
||||
# organizer-settings either.
|
||||
#
|
||||
# N.B.: When actually fleshing out this stub, adding it to the OrganizerUpdateForm should be considered.
|
||||
pass
|
||||
"""
|
||||
if settings_dict.get('reusable_media_type_ntag_pretix1') and settings_dict.get('reusable_media_type_nfc_uid'):
|
||||
raise ValidationError({
|
||||
'reusable_media_type_nfc_uid': _('This needs to be disabled if other NFC-based types are active.')
|
||||
})
|
||||
"""
|
||||
|
||||
|
||||
def global_settings_object(holder):
|
||||
|
||||
Reference in New Issue
Block a user