mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
New implementation of sales channels (#4111)
Co-authored-by: Martin Gross <gross@rami.io>
This commit is contained in:
@@ -51,7 +51,8 @@ from .orders import (
|
||||
generate_secret,
|
||||
)
|
||||
from .organizer import (
|
||||
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,
|
||||
Organizer, Organizer_SettingsStore, SalesChannel, Team, TeamAPIToken,
|
||||
TeamInvite,
|
||||
)
|
||||
from .seating import Seat, SeatCategoryMapping, SeatingPlan
|
||||
from .tax import TaxRule
|
||||
|
||||
@@ -46,7 +46,6 @@ 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
|
||||
|
||||
|
||||
@@ -100,13 +99,13 @@ class CheckinList(LoggedModel):
|
||||
verbose_name=_('Automatically check out everyone at'),
|
||||
null=True, blank=True
|
||||
)
|
||||
auto_checkin_sales_channels = MultiStringField(
|
||||
default=[],
|
||||
blank=True,
|
||||
auto_checkin_sales_channels = models.ManyToManyField(
|
||||
"SalesChannel",
|
||||
verbose_name=_('Sales channels to automatically check in'),
|
||||
help_text=_('All items on this check-in list will be automatically marked as checked-in when purchased through '
|
||||
'any of the selected sales channels. This option can be useful when tickets sold at the box office '
|
||||
'are not checked again before entry and should be considered validated directly upon purchase.')
|
||||
'are not checked again before entry and should be considered validated directly upon purchase.'),
|
||||
blank=True,
|
||||
)
|
||||
rules = models.JSONField(default=dict, blank=True)
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes import ScopedManager
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import fields
|
||||
from pretix.base.models.base import LoggedModel
|
||||
|
||||
|
||||
@@ -65,10 +64,14 @@ class Discount(LoggedModel):
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
sales_channels = fields.MultiStringField(
|
||||
verbose_name=_('Sales channels'),
|
||||
default=['web'],
|
||||
blank=False,
|
||||
all_sales_channels = models.BooleanField(
|
||||
verbose_name=_("All supported sales channels"),
|
||||
default=True,
|
||||
)
|
||||
limit_sales_channels = models.ManyToManyField(
|
||||
"SalesChannel",
|
||||
verbose_name=_("Sales channels"),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
available_from = models.DateTimeField(
|
||||
|
||||
@@ -36,6 +36,7 @@ import logging
|
||||
import os
|
||||
import string
|
||||
import uuid
|
||||
import warnings
|
||||
from collections import Counter, OrderedDict, defaultdict
|
||||
from datetime import datetime, time, timedelta
|
||||
from operator import attrgetter
|
||||
@@ -66,7 +67,6 @@ from django_scopes import ScopedManager, scopes_disabled
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.fields import MultiStringField
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.base.validators import EventSlugBanlistValidator
|
||||
@@ -307,6 +307,7 @@ class EventMixin:
|
||||
def annotated(cls, qs, channel='web', voucher=None):
|
||||
from pretix.base.models import Item, ItemVariation, Quota
|
||||
|
||||
assert isinstance(channel, str)
|
||||
sq_active_item = Item.objects.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).filter(
|
||||
Q(variations__isnull=True)
|
||||
& Q(quotas__pk=OuterRef('pk'))
|
||||
@@ -316,14 +317,14 @@ class EventMixin:
|
||||
|
||||
q_variation = (
|
||||
Q(active=True)
|
||||
& Q(sales_channels__contains=channel)
|
||||
& Q(Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=channel))
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()))
|
||||
& Q(item__active=True)
|
||||
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=time_machine_now()))
|
||||
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=time_machine_now()))
|
||||
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
|
||||
& Q(item__sales_channels__contains=channel)
|
||||
& Q(Q(item__all_sales_channels=True) | Q(item__limit_sales_channels__identifier=channel))
|
||||
& Q(item__require_bundling=False)
|
||||
& Q(quotas__pk=OuterRef('pk'))
|
||||
)
|
||||
@@ -467,6 +468,7 @@ class EventMixin:
|
||||
return best_state_found, num_tickets_found, num_tickets_possible
|
||||
|
||||
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
|
||||
assert isinstance(sales_channel, str)
|
||||
qs_annotated = self._seats(ignore_voucher=ignore_voucher)
|
||||
|
||||
qs = qs_annotated.filter(has_order=False, has_cart=False, has_voucher=False)
|
||||
@@ -495,10 +497,13 @@ class EventMixin:
|
||||
return qs.filter(q)
|
||||
|
||||
|
||||
def default_sales_channels():
|
||||
from ..channels import get_all_sales_channels
|
||||
def default_sales_channels(): # kept for legacy migration
|
||||
from ..channels import get_all_sales_channel_types
|
||||
|
||||
return list(get_all_sales_channels().keys())
|
||||
if "PYTEST_CURRENT_TEST" not in os.environ:
|
||||
warnings.warn('Method should not be used in new code.', DeprecationWarning)
|
||||
|
||||
return list(get_all_sales_channel_types().keys())
|
||||
|
||||
|
||||
@settings_hierarkey.add(parent_field='organizer', cache_namespace='event')
|
||||
@@ -535,8 +540,10 @@ class Event(EventMixin, LoggedModel):
|
||||
:type plugins: str
|
||||
:param has_subevents: Enable event series functionality
|
||||
:type has_subevents: bool
|
||||
:param sales_channels: A list of sales channel identifiers, that this event is available for sale on
|
||||
:type sales_channels: list
|
||||
:param all_sales_channels: A flag indicating that this event is available on all channels and limit_sales_channels will be ignored.
|
||||
:type all_sales_channels: bool
|
||||
:param limit_sales_channels: A list of sales channel identifiers, that this event is available for sale on
|
||||
:type limit_sales_channels: list
|
||||
"""
|
||||
|
||||
settings_namespace = 'event'
|
||||
@@ -628,10 +635,14 @@ class Event(EventMixin, LoggedModel):
|
||||
auto_now=True, db_index=True
|
||||
)
|
||||
|
||||
sales_channels = MultiStringField(
|
||||
verbose_name=_('Restrict to specific sales channels'),
|
||||
help_text=_('Only sell tickets for this event on the following sales channels.'),
|
||||
default=default_sales_channels,
|
||||
all_sales_channels = models.BooleanField(
|
||||
verbose_name=_("Sell on all sales channels"),
|
||||
default=True,
|
||||
)
|
||||
limit_sales_channels = models.ManyToManyField(
|
||||
"SalesChannel",
|
||||
verbose_name=_("Restrict to specific sales channels"),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='organizer')
|
||||
@@ -805,10 +816,17 @@ class Event(EventMixin, LoggedModel):
|
||||
if other.date_admission:
|
||||
self.date_admission = self.date_from + (other.date_admission - other.date_from)
|
||||
self.testmode = other.testmode
|
||||
self.sales_channels = other.sales_channels
|
||||
self.all_sales_channels = other.all_sales_channels
|
||||
self.save()
|
||||
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
||||
|
||||
if not self.all_sales_channels:
|
||||
self.limit_sales_channels.set(
|
||||
self.organizer.sales_channels.filter(
|
||||
identifier__in=other.limit_sales_channels.values_list("identifier", flat=True)
|
||||
)
|
||||
)
|
||||
|
||||
if not skip_meta_data:
|
||||
for emv in EventMetaValue.objects.filter(event=other):
|
||||
emv.pk = None
|
||||
@@ -846,12 +864,17 @@ class Event(EventMixin, LoggedModel):
|
||||
|
||||
item_map = {}
|
||||
variation_map = {}
|
||||
for i in Item.objects.filter(event=other).prefetch_related('variations'):
|
||||
for i in Item.objects.filter(event=other).prefetch_related(
|
||||
'variations', 'limit_sales_channels', 'require_membership_types',
|
||||
'variations__limit_sales_channels', 'variations__require_membership_types',
|
||||
):
|
||||
vars = list(i.variations.all())
|
||||
require_membership_types = list(i.require_membership_types.all())
|
||||
limit_sales_channels = list(i.limit_sales_channels.all())
|
||||
item_map[i.pk] = i
|
||||
i.pk = None
|
||||
i.event = self
|
||||
i._prefetched_objects_cache = {}
|
||||
if i.picture:
|
||||
i.picture.save(os.path.basename(i.picture.name), i.picture)
|
||||
if i.category_id:
|
||||
@@ -868,12 +891,23 @@ class Event(EventMixin, LoggedModel):
|
||||
if require_membership_types and other.organizer_id == self.organizer_id:
|
||||
i.require_membership_types.set(require_membership_types)
|
||||
|
||||
if not i.all_sales_channels:
|
||||
i.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
||||
|
||||
for v in vars:
|
||||
require_membership_types = list(v.require_membership_types.all())
|
||||
limit_sales_channels = list(v.limit_sales_channels.all())
|
||||
variation_map[v.pk] = v
|
||||
v.pk = None
|
||||
v.item = i
|
||||
v._prefetched_objects_cache = {}
|
||||
v.save(force_insert=True)
|
||||
|
||||
if require_membership_types and other.organizer_id == self.organizer_id:
|
||||
v.require_membership_types.set(require_membership_types)
|
||||
if not v.all_sales_channels:
|
||||
v.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
||||
|
||||
for i in self.items.filter(hidden_if_item_available__isnull=False):
|
||||
i.hidden_if_item_available = item_map[i.hidden_if_item_available_id]
|
||||
i.save()
|
||||
@@ -911,6 +945,7 @@ class Event(EventMixin, LoggedModel):
|
||||
vars = list(q.variations.all())
|
||||
oldid = q.pk
|
||||
q.pk = None
|
||||
q._prefetched_objects_cache = {}
|
||||
q.event = self
|
||||
q.closed = False
|
||||
q.save(force_insert=True)
|
||||
@@ -922,11 +957,15 @@ class Event(EventMixin, LoggedModel):
|
||||
q.variations.add(variation_map[v.pk])
|
||||
self.items.filter(hidden_if_available_id=oldid).update(hidden_if_available=q)
|
||||
|
||||
for d in Discount.objects.filter(event=other).prefetch_related('condition_limit_products'):
|
||||
for d in Discount.objects.filter(event=other).prefetch_related(
|
||||
'condition_limit_products', 'benefit_limit_products', 'limit_sales_channels'
|
||||
):
|
||||
c_items = list(d.condition_limit_products.all())
|
||||
b_items = list(d.benefit_limit_products.all())
|
||||
limit_sales_channels = list(v.limit_sales_channels.all())
|
||||
d.pk = None
|
||||
d.event = self
|
||||
d._prefetched_objects_cache = {}
|
||||
d.save(force_insert=True)
|
||||
d.log_action('pretix.object.cloned')
|
||||
for i in c_items:
|
||||
@@ -936,12 +975,16 @@ class Event(EventMixin, LoggedModel):
|
||||
if i.pk in item_map:
|
||||
d.benefit_limit_products.add(item_map[i.pk])
|
||||
|
||||
if not d.all_sales_channels:
|
||||
d.limit_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in limit_sales_channels]))
|
||||
|
||||
question_map = {}
|
||||
for q in Question.objects.filter(event=other).prefetch_related('items', 'options'):
|
||||
items = list(q.items.all())
|
||||
opts = list(q.options.all())
|
||||
question_map[q.pk] = q
|
||||
q.pk = None
|
||||
q._prefetched_objects_cache = {}
|
||||
q.event = self
|
||||
q.save(force_insert=True)
|
||||
q.log_action('pretix.object.cloned')
|
||||
@@ -972,10 +1015,14 @@ class Event(EventMixin, LoggedModel):
|
||||
_walk_rules(i)
|
||||
|
||||
checkin_list_map = {}
|
||||
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related('limit_products'):
|
||||
for cl in other.checkin_lists.filter(subevent__isnull=True).prefetch_related(
|
||||
'limit_products', 'auto_checkin_sales_channels'
|
||||
):
|
||||
items = list(cl.limit_products.all())
|
||||
auto_checkin_sales_channels = list(cl.auto_checkin_sales_channels.all())
|
||||
checkin_list_map[cl.pk] = cl
|
||||
cl.pk = None
|
||||
cl._prefetched_objects_cache = {}
|
||||
cl.event = self
|
||||
rules = cl.rules
|
||||
_walk_rules(rules)
|
||||
@@ -984,6 +1031,8 @@ class Event(EventMixin, LoggedModel):
|
||||
cl.log_action('pretix.object.cloned')
|
||||
for i in items:
|
||||
cl.limit_products.add(item_map[i.pk])
|
||||
if auto_checkin_sales_channels:
|
||||
cl.auto_checkin_sales_channels.set(self.organizer.sales_channels.filter(identifier__in=[s.identifier for s in auto_checkin_sales_channels]))
|
||||
|
||||
if other.seating_plan:
|
||||
if other.seating_plan.organizer_id == self.organizer_id:
|
||||
|
||||
@@ -34,8 +34,10 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import calendar
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
import warnings
|
||||
from collections import Counter, OrderedDict
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from decimal import Decimal, DecimalException
|
||||
@@ -61,7 +63,6 @@ from django_countries.fields import Country
|
||||
from django_scopes import ScopedManager
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
from pretix.base.models import fields
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.fields import MultiStringField
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
@@ -270,13 +271,15 @@ class SubEventItemVariation(models.Model):
|
||||
|
||||
|
||||
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
||||
assert isinstance(channel, str)
|
||||
q = (
|
||||
# IMPORTANT: If this is updated, also update the ItemVariation query
|
||||
# in models/event.py: EventMixin.annotated()
|
||||
Q(active=True)
|
||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info'))
|
||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
|
||||
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
|
||||
& Q(Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=channel))
|
||||
& Q(require_bundling=False)
|
||||
)
|
||||
if not allow_addons:
|
||||
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
|
||||
@@ -353,8 +356,10 @@ class Item(LoggedModel):
|
||||
:type original_price: decimal.Decimal
|
||||
:param require_approval: If set to ``True``, orders containing this product can only be processed and paid after approved by an administrator
|
||||
:type require_approval: bool
|
||||
:param sales_channels: Sales channels this item is available on.
|
||||
:type sales_channels: bool
|
||||
:param all_sales_channels: A flag indicating that this item is available on all channels and limit_sales_channels will be ignored.
|
||||
:type all_sales_channels: bool
|
||||
:param limit_sales_channels: A list of sales channel identifiers, that this item is available for sale on.
|
||||
:type limit_sales_channels: list
|
||||
:param issue_giftcard: If ``True``, buying this product will give you a gift card with the value of the product's price
|
||||
:type issue_giftcard: bool
|
||||
:param validity_mode: Instruction how to set ``valid_from``/``valid_until`` on tickets, ``null`` is default event validity.
|
||||
@@ -609,9 +614,14 @@ class Item(LoggedModel):
|
||||
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
|
||||
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
|
||||
)
|
||||
sales_channels = fields.MultiStringField(
|
||||
verbose_name=_('Sales channels'),
|
||||
default=['web'],
|
||||
all_sales_channels = models.BooleanField(
|
||||
verbose_name=_("Sell on all sales channels"),
|
||||
default=True,
|
||||
)
|
||||
limit_sales_channels = models.ManyToManyField(
|
||||
"SalesChannel",
|
||||
verbose_name=_("Restrict to specific sales channels"),
|
||||
help_text=_('Only sell tickets for this product on the selected sales channels.'),
|
||||
blank=True,
|
||||
)
|
||||
issue_giftcard = models.BooleanField(
|
||||
@@ -1033,9 +1043,13 @@ class Item(LoggedModel):
|
||||
return None, None
|
||||
|
||||
|
||||
def _all_sales_channels_identifiers():
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
return list(get_all_sales_channels().keys())
|
||||
def _all_sales_channels_identifiers(): # kept for legacy migrations
|
||||
from pretix.base.channels import get_all_sales_channel_types
|
||||
|
||||
if "PYTEST_CURRENT_TEST" not in os.environ:
|
||||
warnings.warn('Method should not be used in new code.', DeprecationWarning)
|
||||
|
||||
return list(get_all_sales_channel_types().keys())
|
||||
|
||||
|
||||
class ItemVariation(models.Model):
|
||||
@@ -1058,6 +1072,10 @@ class ItemVariation(models.Model):
|
||||
:param require_approval: If set to ``True``, orders containing this variation can only be processed and paid after
|
||||
approval by an administrator
|
||||
:type require_approval: bool
|
||||
:param all_sales_channels: A flag indicating that this variation is available on all channels and limit_sales_channels will be ignored.
|
||||
:type all_sales_channels: bool
|
||||
:param limit_sales_channels: A list of sales channel identifiers, that this variation is available for sale on.
|
||||
:type limit_sales_channels: list
|
||||
|
||||
"""
|
||||
item = models.ForeignKey(
|
||||
@@ -1143,9 +1161,13 @@ class ItemVariation(models.Model):
|
||||
default=Item.UNAVAIL_MODE_HIDDEN,
|
||||
max_length=16,
|
||||
)
|
||||
sales_channels = fields.MultiStringField(
|
||||
verbose_name=_('Sales channels'),
|
||||
default=_all_sales_channels_identifiers,
|
||||
all_sales_channels = models.BooleanField(
|
||||
verbose_name=_("Sell on all sales channels the product is sold on"),
|
||||
default=True,
|
||||
)
|
||||
limit_sales_channels = models.ManyToManyField(
|
||||
"SalesChannel",
|
||||
verbose_name=_("Restrict to specific sales channels"),
|
||||
help_text=_('The sales channel selection for the product as a whole takes precedence, so if a sales channel is '
|
||||
'selected here but not on product level, the variation will not be available.'),
|
||||
blank=True,
|
||||
|
||||
@@ -187,8 +187,8 @@ class Order(LockModel, LoggedModel):
|
||||
:type require_approval: bool
|
||||
:param meta_info: Additional meta information on the order, JSON-encoded.
|
||||
:type meta_info: str
|
||||
:param sales_channel: Identifier of the sales channel this order was created through.
|
||||
:type sales_channel: str
|
||||
:param sales_channel: Foreign key to the sales channel this order was created through.
|
||||
:type sales_channel: SalesChannel
|
||||
"""
|
||||
|
||||
STATUS_PENDING = "n"
|
||||
@@ -305,7 +305,10 @@ class Order(LockModel, LoggedModel):
|
||||
require_approval = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
sales_channel = models.CharField(max_length=190, default="web")
|
||||
sales_channel = models.ForeignKey(
|
||||
"SalesChannel",
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
email_known_to_work = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('E-mail address verified')
|
||||
@@ -1932,7 +1935,7 @@ class OrderPayment(models.Model):
|
||||
trigger_pdf=not send_mail or not self.order.event.settings.invoice_email_attachment
|
||||
)
|
||||
|
||||
if send_mail and self.order.sales_channel in self.order.event.settings.mail_sales_channel_placed_paid:
|
||||
if send_mail and self.order.sales_channel.identifier in self.order.event.settings.mail_sales_channel_placed_paid:
|
||||
self._send_paid_mail(invoice, user, mail_text)
|
||||
if self.order.event.settings.mail_send_order_paid_attendee:
|
||||
for p in self.order.positions.all():
|
||||
|
||||
@@ -46,7 +46,9 @@ from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scope
|
||||
from i18nfield.fields import I18nCharField
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.validators import OrganizerSlugBanlistValidator
|
||||
@@ -104,6 +106,8 @@ class Organizer(LoggedModel):
|
||||
if is_new:
|
||||
kwargs.pop('update_fields', None) # does not make sense here
|
||||
self.set_defaults()
|
||||
with scope(organizer=self):
|
||||
self.create_default_sales_channels()
|
||||
else:
|
||||
self.get_cache().clear()
|
||||
return obj
|
||||
@@ -212,6 +216,24 @@ class Organizer(LoggedModel):
|
||||
else:
|
||||
return get_connection(fail_silently=False)
|
||||
|
||||
def create_default_sales_channels(self):
|
||||
from pretix.base.channels import get_all_sales_channel_types
|
||||
|
||||
i = 0
|
||||
for channel in get_all_sales_channel_types().values():
|
||||
if not channel.default_created:
|
||||
continue
|
||||
|
||||
self.sales_channels.get_or_create(
|
||||
identifier=channel.identifier,
|
||||
defaults={
|
||||
'label': LazyI18nString.from_gettext(channel.verbose_name),
|
||||
'type': channel.identifier,
|
||||
},
|
||||
position=i
|
||||
)
|
||||
i += 1
|
||||
|
||||
|
||||
def generate_invite_token():
|
||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
@@ -504,3 +526,58 @@ class OrganizerFooterLink(models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
self.organizer.cache.clear()
|
||||
|
||||
|
||||
class SalesChannel(LoggedModel):
|
||||
organizer = models.ForeignKey('Organizer', on_delete=models.CASCADE, related_name='sales_channels')
|
||||
label = I18nCharField(
|
||||
max_length=200,
|
||||
verbose_name=_("Name"),
|
||||
)
|
||||
identifier = models.CharField(
|
||||
verbose_name=_("Identifier"),
|
||||
max_length=200,
|
||||
validators=[
|
||||
RegexValidator(
|
||||
regex=r"^[a-zA-Z0-9.\-_]+$",
|
||||
message=_("The identifier may only contain letters, numbers, dots, dashes, and underscores."),
|
||||
),
|
||||
],
|
||||
)
|
||||
type = models.CharField(
|
||||
verbose_name=_("Type"),
|
||||
max_length=200,
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
configuration = models.JSONField(default=dict)
|
||||
|
||||
objects = ScopedManager(organizer="organizer")
|
||||
|
||||
class Meta:
|
||||
ordering = ("position", "type", "identifier", "id")
|
||||
unique_together = ("organizer", "identifier")
|
||||
|
||||
def __str__(self):
|
||||
return str(self.label)
|
||||
|
||||
@cached_property
|
||||
def type_instance(self):
|
||||
from ..channels import get_all_sales_channel_types
|
||||
|
||||
types = get_all_sales_channel_types()
|
||||
return types[self.type]
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
return self.type_instance.icon
|
||||
|
||||
def allow_delete(self):
|
||||
from . import Order
|
||||
|
||||
if self.type_instance.default_created:
|
||||
return False
|
||||
|
||||
return not Order.objects.filter(sales_channel=self).exists()
|
||||
|
||||
@@ -243,10 +243,14 @@ class Seat(models.Model):
|
||||
qs_annotated = qs_annotated.annotate(has_closeby_taken=Exists(sq_closeby))
|
||||
return qs_annotated
|
||||
|
||||
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None, sales_channel='web',
|
||||
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None,
|
||||
sales_channel='web',
|
||||
ignore_distancing=False, distance_ignore_cart_id=None):
|
||||
from .orders import Order
|
||||
from .organizer import SalesChannel
|
||||
|
||||
if isinstance(sales_channel, SalesChannel):
|
||||
sales_channel = sales_channel.identifier
|
||||
if self.blocked and sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel:
|
||||
return False
|
||||
opqs = self.orderposition_set.filter(
|
||||
|
||||
Reference in New Issue
Block a user