'.format(self.identifier)
@property
def identifier(self) -> str:
"""
- The internal identifier of this sales channel.
+ The internal identifier of this sales channel type.
"""
raise NotImplementedError() # NOQA
@property
def verbose_name(self) -> str:
"""
- A human-readable name of this sales channel.
+ A human-readable name of this sales channel type.
"""
raise NotImplementedError() # NOQA
+ @property
+ def description(self) -> str:
+ """
+ A human-readable description of this sales channel type.
+ """
+ return ""
+
@property
def icon(self) -> str:
"""
- The name of a Font Awesome icon to represent this channel
+ This can be:
+
+ - The name of a Font Awesome icon to represent this channel type.
+ - The name of a SVG icon file that is resolvable through the static file system. We recommend to design for a size of 18x14 pixels.
"""
return "circle"
+ @property
+ def default_created(self) -> bool:
+ """
+ Indication, if a sales channel of this type should automatically be created for every organizer
+ """
+ return True
+
+ @property
+ def multiple_allowed(self) -> bool:
+ """
+ Indication, if multiple sales channels of this type may exist in the same organizer
+ """
+ return False
+
@property
def testmode_supported(self) -> bool:
"""
- Indication, if a saleschannels supports test mode orders
+ Indication, if a sales channel of this type supports test mode orders
"""
return True
@property
def payment_restrictions_supported(self) -> bool:
"""
- If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel.
+ If this property is ``True``, organizers can restrict the usage of payment providers to this sales channel type.
- Example: pretixPOS provides its own sales channel, ignores the configured payment providers completely and
- handles payments locally. Therefor, this property should be set to ``False`` for the pretixPOS sales channel as
+ Example: pretixPOS provides its own sales channel type, ignores the configured payment providers completely and
+ handles payments locally. Therefore, this property should be set to ``False`` for the pretixPOS sales channel as
the event organizer cannot restrict the usage of any payment provider through the backend.
"""
return True
@@ -77,8 +104,8 @@ class SalesChannel:
@property
def unlimited_items_per_order(self) -> bool:
"""
- If this property is ``True``, purchases made using this sales channel are not limited to the maximum amount of
- items defined in the event settings.
+ If this property is ``True``, purchases made using sales channels of this type are not limited to the maximum
+ amount of items defined in the event settings.
"""
return False
@@ -96,34 +123,67 @@ class SalesChannel:
"""
return True
+ @property
+ def required_event_plugin(self) -> str:
+ """
+ Name of an event plugin that is required for this sales channel to be useful. Defaults to ``None``.
+ """
+ return
-def get_all_sales_channels():
- global _ALL_CHANNELS
- if _ALL_CHANNELS:
- return _ALL_CHANNELS
+def get_all_sales_channel_types():
+ from pretix.base.signals import register_sales_channel_types
+ global _ALL_CHANNEL_TYPES
+
+ if _ALL_CHANNEL_TYPES:
+ return _ALL_CHANNEL_TYPES
channels = []
- for recv, ret in register_sales_channels.send(None):
+ for recv, ret in register_sales_channel_types.send(None):
+ if isinstance(ret, (list, tuple)):
+ channels += ret
+ else:
+ channels.append(ret)
+ for recv, ret in register_sales_channels.send(None): # todo: remove me
if isinstance(ret, (list, tuple)):
channels += ret
else:
channels.append(ret)
channels.sort(key=lambda c: c.identifier)
- _ALL_CHANNELS = OrderedDict([(c.identifier, c) for c in channels])
- if 'web' in _ALL_CHANNELS:
- _ALL_CHANNELS.move_to_end('web', last=False)
- return _ALL_CHANNELS
+ _ALL_CHANNEL_TYPES = OrderedDict([(c.identifier, c) for c in channels])
+ if 'web' in _ALL_CHANNEL_TYPES:
+ _ALL_CHANNEL_TYPES.move_to_end('web', last=False)
+ return _ALL_CHANNEL_TYPES
-class WebshopSalesChannel(SalesChannel):
+def get_all_sales_channels():
+ # TODO: remove me
+ warnings.warn('Using get_all_sales_channels() is no longer appropriate, use get_al_sales_channel_types() instead.',
+ DeprecationWarning, stacklevel=2)
+ return get_all_sales_channel_types()
+
+
+class WebshopSalesChannelType(SalesChannelType):
identifier = "web"
verbose_name = _('Online shop')
icon = "globe"
-@receiver(register_sales_channels, dispatch_uid="base_register_default_sales_channels")
+class ApiSalesChannelType(SalesChannelType):
+ identifier = "api"
+ verbose_name = _('API')
+ description = _('API sales channels come with no built-in functionality, but may be used for custom integrations.')
+ icon = "exchange"
+ default_created = False
+ multiple_allowed = True
+
+
+SalesChannel = SalesChannelType # TODO: remove me
+
+
+@receiver(register_sales_channel_types, dispatch_uid="base_register_default_sales_channel_types")
def base_sales_channels(sender, **kwargs):
return (
- WebshopSalesChannel(),
+ WebshopSalesChannelType(),
+ ApiSalesChannelType(),
)
diff --git a/src/pretix/base/exporters/items.py b/src/pretix/base/exporters/items.py
index cbf891714b..fdff516e43 100644
--- a/src/pretix/base/exporters/items.py
+++ b/src/pretix/base/exporters/items.py
@@ -27,7 +27,6 @@ from openpyxl.styles import Alignment
from openpyxl.utils import get_column_letter
from ...helpers.safe_openpyxl import SafeCell
-from ..channels import get_all_sales_channels
from ..exporter import ListExporter
from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue
from ..signals import register_data_exporters
@@ -53,7 +52,7 @@ class ItemDataExporter(ListExporter):
def iterate_list(self, form_data):
locales = self.event.settings.locales
- scs = get_all_sales_channels()
+ scs = self.organizer.sales_channels.all()
header = [
_("Product ID"),
_("Variation ID"),
@@ -141,9 +140,15 @@ class ItemDataExporter(ListExporter):
row.append(i.name.localize(l))
for l in locales:
row.append(v.value.localize(l))
+
+ sales_channels = list(scs)
+ if not i.all_sales_channels:
+ sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in i.limit_sales_channels.all())]
+ if not v.all_sales_channels:
+ sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in v.limit_sales_channels.all())]
row += [
_("Yes") if i.active and v.active else "",
- ", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels and s in v.sales_channels]),
+ ", ".join([str(sn.label) for sn in sales_channels]),
v.default_price or i.default_price,
_("Yes") if i.free_price else "",
str(i.tax_rule) if i.tax_rule else "",
@@ -186,9 +191,12 @@ class ItemDataExporter(ListExporter):
row.append(i.name.localize(l))
for l in locales:
row.append("")
+ sales_channels = list(scs)
+ if not i.all_sales_channels:
+ sales_channels = [s for s in sales_channels if s.identifier in (p.identifier for p in i.limit_sales_channels.all())]
row += [
_("Yes") if i.active else "",
- ", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels]),
+ ", ".join([str(sn.label) for sn in sales_channels]),
i.default_price,
_("Yes") if i.free_price else "",
str(i.tax_rule) if i.tax_rule else "",
diff --git a/src/pretix/base/migrations/0159_mails_by_sales_channel.py b/src/pretix/base/migrations/0159_mails_by_sales_channel.py
index 2c18e7c1b9..37949c6902 100644
--- a/src/pretix/base/migrations/0159_mails_by_sales_channel.py
+++ b/src/pretix/base/migrations/0159_mails_by_sales_channel.py
@@ -2,7 +2,7 @@
from django.db import migrations
-from pretix.base.channels import get_all_sales_channels
+from pretix.base.channels import get_all_sales_channel_types
def set_sales_channels(apps, schema_editor):
@@ -11,7 +11,7 @@ def set_sales_channels(apps, schema_editor):
# Therefore, for existing events, we enable all sales channels
Event_SettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
Event = apps.get_model('pretixbase', 'Event')
- all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channels()) + "]"
+ all_sales_channels = "[" + ", ".join('"' + sc + '"' for sc in get_all_sales_channel_types()) + "]"
batch_size = 1000
Event_SettingsStore.objects.bulk_create([
Event_SettingsStore(
diff --git a/src/pretix/base/migrations/0172_event_sales_channels.py b/src/pretix/base/migrations/0172_event_sales_channels.py
index f04b3abf35..a55ff7c9aa 100644
--- a/src/pretix/base/migrations/0172_event_sales_channels.py
+++ b/src/pretix/base/migrations/0172_event_sales_channels.py
@@ -3,7 +3,7 @@
from django.db import migrations
import pretix.base.models.fields
-from pretix.base.channels import get_all_sales_channels
+from pretix.base.channels import get_all_sales_channel_types
class Migration(migrations.Migration):
@@ -15,6 +15,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='event',
name='sales_channels',
- field=pretix.base.models.fields.MultiStringField(default=list(get_all_sales_channels().keys())),
+ field=pretix.base.models.fields.MultiStringField(default=list(get_all_sales_channel_types().keys())),
),
]
diff --git a/src/pretix/base/migrations/0265_saleschannel_and_more.py b/src/pretix/base/migrations/0265_saleschannel_and_more.py
new file mode 100644
index 0000000000..05519dca1b
--- /dev/null
+++ b/src/pretix/base/migrations/0265_saleschannel_and_more.py
@@ -0,0 +1,110 @@
+# Generated by Django 4.2.8 on 2024-03-24 17:43
+
+import django.db.models.deletion
+import i18nfield.fields
+from django.db import migrations, models
+
+import pretix.base.models.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("pretixbase", "0264_order_internal_secret"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="SalesChannel",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True, primary_key=True, serialize=False
+ ),
+ ),
+ ("label", i18nfield.fields.I18nCharField(max_length=200)),
+ ("identifier", models.CharField(max_length=200)),
+ ("type", models.CharField(max_length=200)),
+ ("position", models.PositiveIntegerField(default=0)),
+ ("configuration", models.JSONField(default=dict)),
+ ],
+ ),
+ migrations.RenameField(
+ model_name="checkinlist",
+ old_name="auto_checkin_sales_channels",
+ new_name="auto_checkin_sales_channel_types",
+ ),
+ migrations.AddField(
+ model_name="discount",
+ name="all_sales_channels",
+ field=models.BooleanField(default=True),
+ ),
+ migrations.AddField(
+ model_name="event",
+ name="all_sales_channels",
+ field=models.BooleanField(default=True),
+ ),
+ migrations.AddField(
+ model_name="item",
+ name="all_sales_channels",
+ field=models.BooleanField(default=True),
+ ),
+ migrations.AddField(
+ model_name="itemvariation",
+ name="all_sales_channels",
+ field=models.BooleanField(default=True),
+ ),
+ migrations.RenameField(
+ model_name="order",
+ old_name="sales_channel",
+ new_name="sales_channel_type",
+ ),
+ migrations.AddField(
+ model_name="saleschannel",
+ name="organizer",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="sales_channels",
+ to="pretixbase.organizer",
+ ),
+ ),
+ migrations.AddField(
+ model_name="discount",
+ name="limit_sales_channels",
+ field=models.ManyToManyField(to="pretixbase.saleschannel"),
+ ),
+ migrations.AddField(
+ model_name="event",
+ name="limit_sales_channels",
+ field=models.ManyToManyField(to="pretixbase.saleschannel"),
+ ),
+ migrations.AddField(
+ model_name="item",
+ name="limit_sales_channels",
+ field=models.ManyToManyField(to="pretixbase.saleschannel"),
+ ),
+ migrations.AddField(
+ model_name="itemvariation",
+ name="limit_sales_channels",
+ field=models.ManyToManyField(to="pretixbase.saleschannel"),
+ ),
+ migrations.AddField(
+ model_name="checkinlist",
+ name="auto_checkin_sales_channels",
+ field=models.ManyToManyField(to="pretixbase.saleschannel"),
+ ),
+ migrations.AddField(
+ model_name="order",
+ name="sales_channel",
+ field=models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ to="pretixbase.saleschannel",
+ ),
+ ),
+ migrations.AlterUniqueTogether(
+ name="saleschannel",
+ unique_together={("organizer", "identifier")},
+ ),
+ ]
diff --git a/src/pretix/base/migrations/0266_saleschannel_migrate_data.py b/src/pretix/base/migrations/0266_saleschannel_migrate_data.py
new file mode 100644
index 0000000000..214f576cf2
--- /dev/null
+++ b/src/pretix/base/migrations/0266_saleschannel_migrate_data.py
@@ -0,0 +1,84 @@
+# Generated by Django 4.2.8 on 2024-03-24 17:55
+
+from django.db import migrations
+from i18nfield.strings import LazyI18nString
+
+from pretix.base.channels import get_all_sales_channel_types
+
+
+def create_sales_channels(apps, schema_editor):
+ channel_types = get_all_sales_channel_types()
+ type_to_channel = dict()
+ full_discount_set = set()
+ full_set = set()
+
+ Organizer = apps.get_model("pretixbase", "Organizer")
+ for o in Organizer.objects.all().iterator():
+ for i, t in enumerate(channel_types.values()):
+ if not t.default_created:
+ continue
+ type_to_channel[t.identifier, o.pk] = o.sales_channels.get_or_create(
+ type=t.identifier,
+ defaults=dict(
+ position=i,
+ identifier=t.identifier,
+ label=LazyI18nString.from_gettext(t.verbose_name),
+ ),
+ )[0]
+ full_set.add(t.identifier)
+ if t.discounts_supported:
+ full_discount_set.add(t.identifier)
+
+ Event = apps.get_model("pretixbase", "Event")
+ for d in Event.objects.all().iterator():
+ if set(d.sales_channels) != full_set:
+ d.all_sales_channels = False
+ d.save()
+ for s in d.sales_channels:
+ d.limit_sales_channels.add(type_to_channel[s, d.organizer_id])
+
+ Item = apps.get_model("pretixbase", "Item")
+ for d in Item.objects.select_related("event").iterator():
+ if set(d.sales_channels) != full_set:
+ d.all_sales_channels = False
+ d.save()
+ for s in d.sales_channels:
+ d.limit_sales_channels.add(type_to_channel[s, d.event.organizer_id])
+
+ ItemVariation = apps.get_model("pretixbase", "ItemVariation")
+ for d in ItemVariation.objects.select_related("item__event").iterator():
+ if set(d.sales_channels) != full_set:
+ d.all_sales_channels = False
+ d.save()
+ for s in d.sales_channels:
+ d.limit_sales_channels.add(type_to_channel[s, d.item.event.organizer_id])
+
+ Discount = apps.get_model("pretixbase", "Discount")
+ for d in Discount.objects.select_related("event").iterator():
+ if set(d.sales_channels) != full_discount_set:
+ d.all_sales_channels = False
+ d.save()
+ for s in d.sales_channels:
+ d.limit_sales_channels.add(type_to_channel[s, d.event.organizer_id])
+
+ CheckinList = apps.get_model("pretixbase", "CheckinList")
+ for c in CheckinList.objects.select_related("event").iterator():
+ for s in c.auto_checkin_sales_channel_types:
+ c.auto_checkin_sales_channels.add(type_to_channel[s, c.event.organizer_id])
+
+ Order = apps.get_model("pretixbase", "Order")
+ for (k, orgid), v in type_to_channel.items():
+ Order.objects.filter(sales_channel_type=k, event__organizer_id=orgid, sales_channel__isnull=True).update(
+ sales_channel=v
+ )
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("pretixbase", "0265_saleschannel_and_more"),
+ ]
+
+ operations = [
+ migrations.RunPython(create_sales_channels, migrations.RunPython.noop),
+ ]
diff --git a/src/pretix/base/migrations/0267_remove_old_sales_channels.py b/src/pretix/base/migrations/0267_remove_old_sales_channels.py
new file mode 100644
index 0000000000..545ce366cf
--- /dev/null
+++ b/src/pretix/base/migrations/0267_remove_old_sales_channels.py
@@ -0,0 +1,46 @@
+# Generated by Django 4.2.8 on 2024-03-25 13:34
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("pretixbase", "0266_saleschannel_migrate_data"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="checkinlist",
+ name="auto_checkin_sales_channel_types",
+ ),
+ migrations.RemoveField(
+ model_name="discount",
+ name="sales_channels",
+ ),
+ migrations.RemoveField(
+ model_name="event",
+ name="sales_channels",
+ ),
+ migrations.RemoveField(
+ model_name="item",
+ name="sales_channels",
+ ),
+ migrations.RemoveField(
+ model_name="itemvariation",
+ name="sales_channels",
+ ),
+ migrations.RemoveField(
+ model_name="order",
+ name="sales_channel_type",
+ ),
+ migrations.AlterField(
+ model_name="order",
+ name="sales_channel",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ to="pretixbase.saleschannel",
+ ),
+ ),
+ ]
diff --git a/src/pretix/base/modelimport_orders.py b/src/pretix/base/modelimport_orders.py
index 53584d3f28..0bda26fd4d 100644
--- a/src/pretix/base/modelimport_orders.py
+++ b/src/pretix/base/modelimport_orders.py
@@ -38,7 +38,6 @@ from i18nfield.strings import LazyI18nString
from phonenumber_field.phonenumber import to_python
from phonenumbers import SUPPORTED_REGIONS
-from pretix.base.channels import get_all_sales_channels
from pretix.base.forms.questions import guess_country
from pretix.base.modelimport import (
DatetimeColumnMixin, DecimalColumnMixin, ImportColumn, SubeventColumnMixin,
@@ -538,18 +537,28 @@ class Expires(DatetimeColumnMixin, ImportColumn):
class Saleschannel(ImportColumn):
identifier = 'sales_channel'
verbose_name = gettext_lazy('Sales channel')
+ default_value = None
+ initial = 'static:web'
+
+ @cached_property
+ def channels(self):
+ return list(self.event.organizer.sales_channels.all())
def static_choices(self):
return [
- (sc.identifier, sc.verbose_name) for sc in get_all_sales_channels().values()
+ (c.identifier, str(c.label)) for c in self.channels
]
def clean(self, value, previous_values):
- if not value:
- value = 'web'
- if value not in get_all_sales_channels():
+ matches = [
+ p for p in self.channels
+ if p.identifier == value or any((v and v == value) for v in i18n_flat(p.label))
+ ]
+ if len(matches) == 0:
raise ValidationError(_("Please enter a valid sales channel."))
- return value
+ if len(matches) > 1:
+ raise ValidationError(_("Please enter a valid sales channel."))
+ return matches[0]
def assign(self, value, order, position, invoice_address, **kwargs):
order.sales_channel = value
diff --git a/src/pretix/base/models/__init__.py b/src/pretix/base/models/__init__.py
index ea5b8d0252..d3e68f6bde 100644
--- a/src/pretix/base/models/__init__.py
+++ b/src/pretix/base/models/__init__.py
@@ -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
diff --git a/src/pretix/base/models/checkin.py b/src/pretix/base/models/checkin.py
index 0590d6b102..f69da42754 100644
--- a/src/pretix/base/models/checkin.py
+++ b/src/pretix/base/models/checkin.py
@@ -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)
diff --git a/src/pretix/base/models/discount.py b/src/pretix/base/models/discount.py
index 0a2a646af9..d547de5c72 100644
--- a/src/pretix/base/models/discount.py
+++ b/src/pretix/base/models/discount.py
@@ -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(
diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py
index 4493e2b575..0b070e7a6f 100644
--- a/src/pretix/base/models/event.py
+++ b/src/pretix/base/models/event.py
@@ -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:
diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py
index 7007f62c65..8c3fd3f408 100644
--- a/src/pretix/base/models/items.py
+++ b/src/pretix/base/models/items.py
@@ -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,
diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py
index edd82514f0..ad2bbf27bd 100644
--- a/src/pretix/base/models/orders.py
+++ b/src/pretix/base/models/orders.py
@@ -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():
diff --git a/src/pretix/base/models/organizer.py b/src/pretix/base/models/organizer.py
index e45855bbe6..c9e0ee3ba1 100644
--- a/src/pretix/base/models/organizer.py
+++ b/src/pretix/base/models/organizer.py
@@ -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()
diff --git a/src/pretix/base/models/seating.py b/src/pretix/base/models/seating.py
index 7d311f66cc..c55d635508 100644
--- a/src/pretix/base/models/seating.py
+++ b/src/pretix/base/models/seating.py
@@ -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(
diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py
index 955f57e824..2f4148879a 100644
--- a/src/pretix/base/payment.py
+++ b/src/pretix/base/payment.py
@@ -56,7 +56,6 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
-from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import I18nMarkdownTextarea, PlaceholderValidator
from pretix.base.models import (
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
@@ -417,8 +416,8 @@ class BasePaymentProvider:
forms.MultipleChoiceField(
label=_('Restrict to specific sales channels'),
choices=(
- (c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
- if c.payment_restrictions_supported
+ (c.identifier, c.label) for c in self.event.organizer.sales_channels.all()
+ if c.type_instance.payment_restrictions_supported
),
initial=['web'],
widget=forms.CheckboxSelectMultiple,
@@ -853,7 +852,7 @@ class BasePaymentProvider:
if str(ia.country) != '' and str(ia.country) not in restricted_countries:
return False
- if order.sales_channel not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
+ if order.sales_channel.identifier not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
return False
return self._is_available_by_time(order=order)
diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py
index a3e7b7f09e..f989d77e11 100644
--- a/src/pretix/base/services/cart.py
+++ b/src/pretix/base/services/cart.py
@@ -52,12 +52,11 @@ from django.utils.translation import (
)
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,
+ CartPosition, Event, InvoiceAddress, Item, ItemVariation, SalesChannel,
+ Seat, SeatCategoryMapping, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import OrderFee
@@ -275,8 +274,8 @@ class CartManager:
AddOperation: 30
}
- def __init__(self, event: Event, cart_id: str, invoice_address: InvoiceAddress=None, widget_data=None,
- sales_channel='web'):
+ def __init__(self, event: Event, cart_id: str, sales_channel: SalesChannel,
+ invoice_address: InvoiceAddress=None, widget_data=None):
self.event = event
self.cart_id = cart_id
self.real_now_dt = now()
@@ -384,7 +383,7 @@ class CartManager:
})
def _check_max_cart_size(self):
- if not get_all_sales_channels()[self._sales_channel].unlimited_items_per_order:
+ if not self._sales_channel.type_instance.unlimited_items_per_order:
cartsize = self.positions.filter(addon_to__isnull=True).count()
cartsize += sum([op.count for op in self._operations if isinstance(op, self.AddOperation) and not op.addon_to])
cartsize -= len([1 for op in self._operations if isinstance(op, self.RemoveOperation) if
@@ -422,8 +421,13 @@ class CartManager:
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'])
+ if not op.item.all_sales_channels:
+ if self._sales_channel.identifier not in (s.identifier for s in op.item.limit_sales_channels.all()):
+ raise CartError(error_messages['unavailable'])
+
+ if op.variation and not op.variation.all_sales_channels:
+ if self._sales_channel.identifier not in (s.identifier for s in op.variation.limit_sales_channels.all()):
+ raise CartError(error_messages['unavailable'])
if op.subevent and op.item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():
raise CartError(error_messages['not_for_sale'])
@@ -457,7 +461,14 @@ class CartManager:
raise CartError(error_messages['ended'])
seated = self._is_seated(op.item, op.subevent)
- if seated and (not op.seat or (op.seat.blocked and self._sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel)):
+ if (
+ seated and (
+ not op.seat or (
+ op.seat.blocked and
+ self._sales_channel.identifier not in self.event.settings.seating_allow_blocked_seats_for_channel
+ )
+ )
+ ):
raise CartError(error_messages['seat_invalid'])
elif op.seat and not seated:
raise CartError(error_messages['seat_forbidden'])
@@ -1371,7 +1382,7 @@ class CartManager:
discount_results = apply_discounts(
self.event,
- self._sales_channel,
+ self._sales_channel.identifier,
[
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
for cp in positions
@@ -1505,6 +1516,11 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
except InvoiceAddress.DoesNotExist:
pass
+ try:
+ sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
+ except SalesChannel.DoesNotExist:
+ raise CartError("Invalid sales channel.")
+
try:
try:
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, widget_data=widget_data,
@@ -1526,6 +1542,10 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
+ try:
+ sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
+ except SalesChannel.DoesNotExist:
+ raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1546,6 +1566,10 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
+ try:
+ sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
+ except SalesChannel.DoesNotExist:
+ raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1565,6 +1589,10 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
:param session: Session ID of a guest
"""
with language(locale), time_machine_now_assigned(override_now_dt):
+ try:
+ sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
+ except SalesChannel.DoesNotExist:
+ raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
@@ -1593,6 +1621,10 @@ def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, l
ia = InvoiceAddress.objects.get(pk=invoice_address)
except InvoiceAddress.DoesNotExist:
pass
+ try:
+ sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
+ except SalesChannel.DoesNotExist:
+ raise CartError("Invalid sales channel.")
try:
try:
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
diff --git a/src/pretix/base/services/checkin.py b/src/pretix/base/services/checkin.py
index 661160922a..b8d94c7df9 100644
--- a/src/pretix/base/services/checkin.py
+++ b/src/pretix/base/services/checkin.py
@@ -1159,7 +1159,7 @@ def order_placed(sender, **kwargs):
order = kwargs['order']
event = sender
- cls = list(event.checkin_lists.filter(auto_checkin_sales_channels__contains=order.sales_channel).prefetch_related(
+ cls = list(event.checkin_lists.filter(auto_checkin_sales_channels=order.sales_channel).prefetch_related(
'limit_products'))
if not cls:
return
diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py
index 25a90e0340..e9a0c7022a 100644
--- a/src/pretix/base/services/invoices.py
+++ b/src/pretix/base/services/invoices.py
@@ -420,7 +420,7 @@ def invoice_pdf_task(invoice: int):
def invoice_qualified(order: Order):
if order.total == Decimal('0.00') or order.require_approval or \
- order.sales_channel not in order.event.settings.get('invoice_generate_sales_channels'):
+ order.sales_channel.identifier not in order.event.settings.get('invoice_generate_sales_channels'):
return False
return True
@@ -443,8 +443,11 @@ def build_preview_invoice_pdf(event):
locale = event.settings.locale
with rolledback_transaction(), language(locale, event.settings.region):
- order = event.orders.create(status=Order.STATUS_PENDING, datetime=timezone.now(),
- expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count())
+ order = event.orders.create(
+ status=Order.STATUS_PENDING, datetime=timezone.now(),
+ expires=timezone.now(), code="PREVIEW", total=100 * event.tax_rules.count(),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
+ )
invoice = Invoice(
order=order, event=event, invoice_no="PREVIEW",
date=timezone.now().date(), locale=locale, organizer=event.organizer
diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py
index 8697a98303..7a9f189d22 100644
--- a/src/pretix/base/services/orders.py
+++ b/src/pretix/base/services/orders.py
@@ -62,7 +62,6 @@ from django.utils.translation import gettext as _, gettext_lazy, ngettext_lazy
from django_scopes import scopes_disabled
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
@@ -76,7 +75,7 @@ from pretix.base.models.orders import (
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
generate_secret,
)
-from pretix.base.models.organizer import TeamAPIToken
+from pretix.base.models.organizer import SalesChannel, TeamAPIToken
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.payment import GiftCardPayment, PaymentException
from pretix.base.reldate import RelativeDateWrapper
@@ -650,8 +649,7 @@ def _check_date(event: Event, now_dt: datetime):
def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: datetime, positions: List[CartPosition],
- address: InvoiceAddress = None,
- sales_channel='web', customer=None):
+ sales_channel: SalesChannel, address: InvoiceAddress=None, customer=None):
err = None
_check_date(event, time_machine_now_dt)
@@ -775,7 +773,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
if cp.seat:
# Unlike quotas (which we blindly trust as long as the position is not expired), we check seats every
# time, since we absolutely can not overbook a seat.
- if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel):
+ if not cp.seat.is_available(ignore_cart=cp, ignore_voucher_id=cp.voucher_id, sales_channel=sales_channel.identifier):
err = err or error_messages['seat_unavailable']
delete(cp)
continue
@@ -873,7 +871,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
sorted_positions = [cp for cp in sorted_positions if cp.pk and cp.pk not in deleted_positions]
discount_results = apply_discounts(
event,
- sales_channel,
+ sales_channel.identifier,
[
(cp.item_id, cp.subevent_id, cp.line_price_gross, bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
for cp in sorted_positions
@@ -959,12 +957,11 @@ def _get_fees(positions: List[CartPosition], payment_requests: List[dict], addre
return fees
-def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
- payment_requests: List[dict], locale: str=None, address: InvoiceAddress=None,
- meta_info: dict=None, sales_channel: str='web', shown_total=None,
+def _create_order(event: Event, *, email: str, positions: List[CartPosition], now_dt: datetime,
+ payment_requests: List[dict], sales_channel: SalesChannel, locale: str=None,
+ address: InvoiceAddress=None, meta_info: dict=None, shown_total=None,
customer=None, valid_if_pending=False):
payments = []
- sales_channel = get_all_sales_channels()[sales_channel]
try:
validate_memberships_in_order(customer, positions, event, lock=True, testmode=event.testmode)
@@ -986,10 +983,10 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
datetime=now_dt,
locale=get_language_without_region(locale),
total=total,
- testmode=True if sales_channel.testmode_supported and event.testmode else False,
+ testmode=True if sales_channel.type_instance.testmode_supported and event.testmode else False,
meta_info=json.dumps(meta_info or {}),
require_approval=require_approval,
- sales_channel=sales_channel.identifier,
+ sales_channel=sales_channel,
customer=customer,
valid_if_pending=valid_if_pending,
)
@@ -1108,6 +1105,11 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
if customer:
customer = event.organizer.customers.get(pk=customer)
+ try:
+ sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
+ except SalesChannel.DoesNotExist:
+ raise OrderError("Invalid sales channel.")
+
if email == settings.PRETIX_EMAIL_NONE_VALUE:
email = None
@@ -1186,9 +1188,20 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
if 'sleep-after-quota-check' in debugflags_var.get():
sleep(2)
- order, payment_objs = _create_order(event, email, positions, real_now_dt, payment_requests,
- locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
- shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending)
+ order, payment_objs = _create_order(
+ event,
+ email=email,
+ positions=positions,
+ now_dt=real_now_dt,
+ payment_requests=payment_requests,
+ locale=locale,
+ address=addr,
+ meta_info=meta_info,
+ sales_channel=sales_channel,
+ shown_total=shown_total,
+ customer=customer,
+ valid_if_pending=valid_if_pending
+ )
try:
for p in payment_objs:
@@ -1272,7 +1285,7 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
email_attendees_template = event.settings.mail_text_order_placed_attendee
subject_attendees_template = event.settings.mail_subject_order_placed_attendee
- if sales_channel in event.settings.mail_sales_channel_placed_paid:
+ if sales_channel.identifier in event.settings.mail_sales_channel_placed_paid:
_order_placed_email(event, order, email_template, subject_template, log_entry, invoice, payment_objs,
is_free=free_order_flow)
if email_attendees:
@@ -1424,7 +1437,7 @@ def send_download_reminders(sender, **kwargs):
if days is None:
continue
- if o.sales_channel not in event.settings.mail_sales_channel_download_reminder:
+ if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
continue
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
@@ -1926,9 +1939,13 @@ class OrderChangeManager:
if not item.is_available() or (variation and not variation.is_available()):
raise OrderError(error_messages['unavailable'])
- if self.order.sales_channel not in item.sales_channels or (
- variation and self.order.sales_channel not in variation.sales_channels):
- raise OrderError(error_messages['unavailable'])
+ if not item.all_sales_channels:
+ if self.order.sales_channel.identifier not in (s.identifier for s in item.limit_sales_channels.all()):
+ raise OrderError(error_messages['unavailable'])
+
+ if variation and not variation.all_sales_channels:
+ if self.order.sales_channel.identifier not in (s.identifier for s in variation.limit_sales_channels.all()):
+ raise OrderError(error_messages['unavailable'])
if subevent and item.pk in subevent.item_overrides and not subevent.item_overrides[item.pk].is_available():
raise OrderError(error_messages['not_for_sale'])
@@ -2027,7 +2044,10 @@ class OrderChangeManager:
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
or (a.variation and self.order.sales_channel not in a.variation.sales_channels)
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
- or self.order.sales_channel not in item.sales_channels
+ or (
+ not item.all_sales_channels and
+ not item.limit_sales_channels.contains(self.order.sales_channel)
+ )
)
if is_unavailable:
continue
diff --git a/src/pretix/base/services/pricing.py b/src/pretix/base/services/pricing.py
index 9c4a12ffb5..acfdc44b0d 100644
--- a/src/pretix/base/services/pricing.py
+++ b/src/pretix/base/services/pricing.py
@@ -28,7 +28,8 @@ from django.db.models import Q
from pretix.base.decimal import round_decimal
from pretix.base.models import (
- AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation, Voucher,
+ AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation,
+ SalesChannel, Voucher,
)
from pretix.base.models.event import Event, SubEvent
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
@@ -164,12 +165,14 @@ def apply_discounts(event: Event, sales_channel: str,
:param positions: Tuple of the form ``(item_id, subevent_id, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
:return: A list of ``(new_gross_price, discount)`` tuples in the same order as the input
"""
+ if isinstance(sales_channel, SalesChannel):
+ sales_channel = sales_channel.identifier
new_prices = {}
discount_qs = event.discounts.filter(
Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()),
Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()),
- sales_channels__contains=sales_channel,
+ Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=sales_channel),
active=True,
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
for discount in discount_qs:
diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py
index dc8467d2c4..4f6cb19025 100644
--- a/src/pretix/base/signals.py
+++ b/src/pretix/base/signals.py
@@ -294,13 +294,16 @@ This signal is sent out when a notification is sent.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
-register_sales_channels = django.dispatch.Signal()
+register_sales_channel_types = django.dispatch.Signal()
"""
This signal is sent out to get all known sales channels types. Receivers should return an
-instance of a subclass of ``pretix.base.channels.SalesChannel`` or a list of such
+instance of a subclass of ``pretix.base.channels.SalesChannelType`` or a list of such
instances.
"""
+
+register_sales_channels = DeprecatedSignal() # TODO: remove me
+
register_data_exporters = EventPluginSignal()
"""
This signal is sent out to get all known data exporters. Receivers should return a
diff --git a/src/pretix/base/templates/pretixbase/forms/widgets/checkbox_sales_channel_option.html b/src/pretix/base/templates/pretixbase/forms/widgets/checkbox_sales_channel_option.html
new file mode 100644
index 0000000000..9492b2376d
--- /dev/null
+++ b/src/pretix/base/templates/pretixbase/forms/widgets/checkbox_sales_channel_option.html
@@ -0,0 +1,19 @@
+{% load rich_text %}
+{% load static %}
+{% load i18n %}
+{% if widget.wrap_label %}{% endif %}
+{% include "django/forms/widgets/input.html" %}
+{% if widget.wrap_label %}
+{% if "." in widget.value.instance.type_instance.icon %}
+
+{% else %}
+
+{% endif %}
+{% if widget.plugin_missing %}
+
+{% endif %}
+{{ widget.label }}{% if widget.plugin_missing %}
+
+{% endif %}
+
+{% endif %}
diff --git a/src/pretix/control/forms/__init__.py b/src/pretix/control/forms/__init__.py
index f406774ec4..42a0fdf6d2 100644
--- a/src/pretix/control/forms/__init__.py
+++ b/src/pretix/control/forms/__init__.py
@@ -438,3 +438,20 @@ class ButtonGroupRadioSelect(forms.RadioSelect):
attrs['icon'] = self.option_icons[value]
opt = super().create_option(name, value, label, selected, index, subindex, attrs)
return opt
+
+
+class SalesChannelCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
+ option_template_name = 'pretixbase/forms/widgets/checkbox_sales_channel_option.html'
+
+ def __init__(self, event, attrs=None, choices=()):
+ self.event = event
+ super().__init__(attrs, choices)
+
+ def create_option(
+ self, name, value, label, selected, index, subindex=None, attrs=None
+ ):
+ plugin = value.instance.type_instance.required_event_plugin
+ return {
+ **super().create_option(name, value, label, selected, index, subindex, attrs),
+ "plugin_missing": plugin and plugin not in self.event.get_plugins(),
+ }
diff --git a/src/pretix/control/forms/checkin.py b/src/pretix/control/forms/checkin.py
index 0012cfcf8d..52a52217fd 100644
--- a/src/pretix/control/forms/checkin.py
+++ b/src/pretix/control/forms/checkin.py
@@ -30,11 +30,12 @@ from django_scopes.forms import (
SafeModelChoiceField, SafeModelMultipleChoiceField,
)
-from pretix.base.channels import get_all_sales_channels
from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import Gate
from pretix.base.models.checkin import Checkin, CheckinList
-from pretix.control.forms import ItemMultipleChoiceField
+from pretix.control.forms import (
+ ItemMultipleChoiceField, SalesChannelCheckboxSelectMultiple,
+)
from pretix.control.forms.widgets import Select2
@@ -66,14 +67,9 @@ class CheckinListForm(forms.ModelForm):
kwargs.pop('locales', None)
super().__init__(**kwargs)
self.fields['limit_products'].queryset = self.event.items.all()
- self.fields['auto_checkin_sales_channels'] = forms.MultipleChoiceField(
- label=self.fields['auto_checkin_sales_channels'].label,
- help_text=self.fields['auto_checkin_sales_channels'].help_text,
- required=self.fields['auto_checkin_sales_channels'].required,
- choices=(
- (c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
- ),
- widget=forms.CheckboxSelectMultiple
+ self.fields['auto_checkin_sales_channels'].queryset = self.event.organizer.sales_channels.all()
+ self.fields['auto_checkin_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(
+ self.event, choices=self.fields['auto_checkin_sales_channels'].widget.choices
)
if not self.event.organizer.gates.exists():
@@ -123,13 +119,13 @@ class CheckinListForm(forms.ModelForm):
'gates': forms.CheckboxSelectMultiple(attrs={
'class': 'scrolling-multiple-choice'
}),
- 'auto_checkin_sales_channels': forms.CheckboxSelectMultiple(),
'exit_all_at': NextTimeInput(attrs={'class': 'timepickerfield'}),
}
field_classes = {
'limit_products': ItemMultipleChoiceField,
'gates': SafeModelMultipleChoiceField,
'subevent': SafeModelChoiceField,
+ 'auto_checkin_sales_channels': SafeModelMultipleChoiceField,
'exit_all_at': NextTimeField,
}
diff --git a/src/pretix/control/forms/discounts.py b/src/pretix/control/forms/discounts.py
index d61dd3aef5..aac9e08270 100644
--- a/src/pretix/control/forms/discounts.py
+++ b/src/pretix/control/forms/discounts.py
@@ -22,13 +22,16 @@
from decimal import Decimal
from django import forms
-from django.utils.translation import gettext_lazy as _
+from django_scopes.forms import SafeModelMultipleChoiceField
-from pretix.base.channels import get_all_sales_channels
+from pretix.base.channels import get_all_sales_channel_types
from pretix.base.forms import I18nModelForm
from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import Discount
-from pretix.control.forms import ItemMultipleChoiceField, SplitDateTimeField
+from pretix.control.forms import (
+ ItemMultipleChoiceField, SalesChannelCheckboxSelectMultiple,
+ SplitDateTimeField,
+)
class DiscountForm(I18nModelForm):
@@ -38,7 +41,8 @@ class DiscountForm(I18nModelForm):
fields = [
'active',
'internal_name',
- 'sales_channels',
+ 'all_sales_channels',
+ 'limit_sales_channels',
'available_from',
'available_until',
'subevent_mode',
@@ -60,6 +64,7 @@ class DiscountForm(I18nModelForm):
'available_until': SplitDateTimeField,
'condition_limit_products': ItemMultipleChoiceField,
'benefit_limit_products': ItemMultipleChoiceField,
+ 'limit_sales_channels': SafeModelMultipleChoiceField,
}
widgets = {
'subevent_mode': forms.RadioSelect,
@@ -83,15 +88,12 @@ class DiscountForm(I18nModelForm):
self.event = kwargs['event']
super().__init__(*args, **kwargs)
- self.fields['sales_channels'] = forms.MultipleChoiceField(
- label=_('Sales channels'),
- required=True,
- choices=(
- (c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
- if c.discounts_supported
- ),
- widget=forms.CheckboxSelectMultiple,
+ self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.filter(
+ type__in=[k for k, v in get_all_sales_channel_types().items() if v.discounts_supported]
)
+ self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
+ 'data-inverse-dependency': '<[name$=all_sales_channels]',
+ }, choices=self.fields['limit_sales_channels'].widget.choices)
self.fields['condition_limit_products'].queryset = self.event.items.all()
self.fields['benefit_limit_products'].queryset = self.event.items.all()
self.fields['condition_min_count'].required = False
diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py
index a0ca8d3040..584660b527 100644
--- a/src/pretix/control/forms/event.py
+++ b/src/pretix/control/forms/event.py
@@ -44,9 +44,7 @@ from django.conf import settings
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from django.core.validators import MaxValueValidator
from django.db.models import Prefetch, Q, prefetch_related_objects
-from django.forms import (
- CheckboxSelectMultiple, formset_factory, inlineformset_factory,
-)
+from django.forms import formset_factory, inlineformset_factory
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.html import escape, format_html
@@ -54,12 +52,12 @@ from django.utils.safestring import mark_safe
from django.utils.timezone import get_current_timezone_name
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
from django_countries.fields import LazyTypedChoiceField
+from django_scopes.forms import SafeModelMultipleChoiceField
from i18nfield.forms import (
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
)
from pytz import common_timezones
-from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import (
I18nMarkdownTextarea, I18nModelForm, PlaceholderValidator, SettingsForm,
)
@@ -73,8 +71,8 @@ from pretix.base.settings import (
)
from pretix.base.validators import multimail_validate
from pretix.control.forms import (
- MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
- SplitDateTimePickerWidget,
+ MultipleLanguagesWidget, SalesChannelCheckboxSelectMultiple, SlugWidget,
+ SplitDateTimeField, SplitDateTimePickerWidget,
)
from pretix.control.forms.widgets import Select2
from pretix.helpers.countries import CachedCountries
@@ -378,16 +376,10 @@ class EventUpdateForm(I18nModelForm):
required=False,
help_text=_('You need to configure the custom domain in the webserver beforehand.')
)
- self.fields['sales_channels'] = forms.MultipleChoiceField(
- label=self.fields['sales_channels'].label,
- help_text=self.fields['sales_channels'].help_text,
- required=self.fields['sales_channels'].required,
- initial=self.fields['sales_channels'].initial,
- choices=(
- (c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
- ),
- widget=forms.CheckboxSelectMultiple
- )
+ self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
+ self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
+ 'data-inverse-dependency': '<[name$=all_sales_channels]',
+ }, choices=self.fields['limit_sales_channels'].widget.choices)
def clean_domain(self):
d = self.cleaned_data['domain']
@@ -444,7 +436,8 @@ class EventUpdateForm(I18nModelForm):
'location',
'geo_lat',
'geo_lon',
- 'sales_channels'
+ 'all_sales_channels',
+ 'limit_sales_channels',
]
field_classes = {
'date_from': SplitDateTimeField,
@@ -452,6 +445,7 @@ class EventUpdateForm(I18nModelForm):
'date_admission': SplitDateTimeField,
'presale_start': SplitDateTimeField,
'presale_end': SplitDateTimeField,
+ 'limit_sales_channels': SafeModelMultipleChoiceField,
}
widgets = {
'date_from': SplitDateTimePickerWidget(),
@@ -459,7 +453,6 @@ class EventUpdateForm(I18nModelForm):
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}),
'presale_start': SplitDateTimePickerWidget(),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
- 'sales_channels': CheckboxSelectMultiple(),
}
@@ -915,7 +908,7 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
locale_names = dict(settings.LANGUAGES)
self.fields['invoice_language'].choices = [('__user__', _('The user\'s language'))] + [(a, locale_names[a]) for a in event.settings.locales]
self.fields['invoice_generate_sales_channels'].choices = (
- (c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
+ (c.identifier, c.label) for c in event.organizer.sales_channels.all()
)
self.fields['invoice_numbers_counter_length'].validators.append(MaxValueValidator(15))
@@ -961,7 +954,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
]
mail_sales_channel_placed_paid = forms.MultipleChoiceField(
- choices=lambda: [(ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items()],
+ choices=[],
label=_('Sales channels for checkout emails'),
help_text=_('The order placed and paid emails will only be send to orders from these sales channels. '
'The online shop must be enabled.'),
@@ -972,7 +965,7 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
)
mail_sales_channel_download_reminder = forms.MultipleChoiceField(
- choices=lambda: [(ident, sc.verbose_name) for ident, sc in get_all_sales_channels().items()],
+ choices=[],
label=_('Sales channels'),
help_text=_('This email will only be send to orders from these sales channels. The online shop must be enabled.'),
widget=forms.CheckboxSelectMultiple(
@@ -1367,6 +1360,12 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
self.fields['mail_html_renderer'].choices = [
(r.identifier, r.verbose_name) for r in event.get_html_mail_renderers().values()
]
+ self.fields['mail_sales_channel_placed_paid'].choices = (
+ (c.identifier, c.label) for c in event.organizer.sales_channels.all()
+ )
+ self.fields['mail_sales_channel_download_reminder'].choices = (
+ (c.identifier, c.label) for c in event.organizer.sales_channels.all()
+ )
prefetch_related_objects([self.event.organizer], Prefetch('meta_properties'))
self.event.meta_values_cached = self.event.meta_values.select_related('property').all()
diff --git a/src/pretix/control/forms/filter.py b/src/pretix/control/forms/filter.py
index 9d6771fb00..4f26abc8ba 100644
--- a/src/pretix/control/forms/filter.py
+++ b/src/pretix/control/forms/filter.py
@@ -50,15 +50,14 @@ from django.utils.timezone import get_current_timezone, make_aware, now
from django.utils.translation import gettext, gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelChoiceField
-from pretix.base.channels import get_all_sales_channels
from pretix.base.forms.widgets import (
DatePickerWidget, SplitDateTimePickerWidget, TimePickerWidget,
)
from pretix.base.models import (
Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue,
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
- OrderRefund, Organizer, Question, QuestionAnswer, Quota, SubEvent,
- SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
+ OrderRefund, Organizer, Question, QuestionAnswer, Quota, SalesChannel,
+ SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
)
from pretix.base.signals import register_payment_providers
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
@@ -579,9 +578,11 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
required=False,
label=_('Maximal sum of payments and refunds'),
)
- sales_channel = forms.ChoiceField(
+ sales_channel = SafeModelChoiceField(
label=_('Sales channel'),
required=False,
+ queryset=SalesChannel.objects.none(),
+ to_field_name="identifier",
)
has_checkin = forms.NullBooleanField(
required=False,
@@ -604,9 +605,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
del self.fields['subevents_from']
del self.fields['subevents_to']
- self.fields['sales_channel'].choices = [('', '')] + [
- (k, v.verbose_name) for k, v in get_all_sales_channels().items()
- ]
+ self.fields['sales_channel'].queryset = self.event.organizer.sales_channels.all()
locale_names = dict(settings.LANGUAGES)
self.fields['locale'].choices = [('', '')] + [(a, locale_names[a]) for a in self.event.settings.locales]
@@ -719,7 +718,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
if fdata.get('comment'):
qs = qs.filter(comment__icontains=fdata.get('comment'))
if fdata.get('sales_channel'):
- qs = qs.filter(sales_channel=fdata.get('sales_channel'))
+ qs = qs.filter(sales_channel__identifier=fdata.get('sales_channel').identifier)
if fdata.get('total'):
qs = qs.filter(total=fdata.get('total'))
if fdata.get('email_known_to_work') is not None:
diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py
index 228c222344..6b0669e7ae 100644
--- a/src/pretix/control/forms/item.py
+++ b/src/pretix/control/forms/item.py
@@ -55,7 +55,6 @@ from django_scopes.forms import (
)
from i18nfield.forms import I18nFormField, I18nTextarea
-from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import I18nFormSet, I18nMarkdownTextarea, I18nModelForm
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import (
@@ -64,7 +63,8 @@ from pretix.base.models import (
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
from pretix.base.signals import item_copy_data
from pretix.control.forms import (
- ButtonGroupRadioSelect, ItemMultipleChoiceField, SizeValidationMixin,
+ ButtonGroupRadioSelect, ItemMultipleChoiceField,
+ SalesChannelCheckboxSelectMultiple, SizeValidationMixin,
SplitDateTimeField, SplitDateTimePickerWidget,
)
from pretix.control.forms.widgets import Select2, Select2ItemVarMulti
@@ -413,7 +413,7 @@ class ItemCreateForm(I18nModelForm):
'checkin_text',
'free_price',
'original_price',
- 'sales_channels',
+ 'all_sales_channels',
'issue_giftcard',
'require_approval',
'allow_waitinglist',
@@ -443,9 +443,6 @@ class ItemCreateForm(I18nModelForm):
if src.picture:
self.instance.picture.save(os.path.basename(src.picture.name), src.picture)
- else:
- # Add to all sales channels by default
- self.instance.sales_channels = list(get_all_sales_channels().keys())
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
if not self.instance.admission:
@@ -474,6 +471,8 @@ class ItemCreateForm(I18nModelForm):
})
if self.cleaned_data.get('copy_from'):
+ if not self.instance.all_sales_channels:
+ self.instance.limit_sales_channels.set(self.cleaned_data['copy_from'].limit_sales_channels.all())
self.instance.require_membership_types.set(
self.cleaned_data['copy_from'].require_membership_types.all()
)
@@ -574,14 +573,10 @@ class ItemUpdateForm(I18nModelForm):
if self.event.tax_rules.exists():
self.fields['tax_rule'].required = True
self.fields['description'].widget.attrs['rows'] = '4'
- self.fields['sales_channels'] = forms.MultipleChoiceField(
- label=_('Sales channels'),
- required=False,
- choices=(
- (c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
- ),
- widget=forms.CheckboxSelectMultiple
- )
+ self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
+ self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
+ 'data-inverse-dependency': '<[name$=all_sales_channels]',
+ }, choices=self.fields['limit_sales_channels'].widget.choices)
change_decimal_field(self.fields['default_price'], self.event.currency)
self.fields['available_from_mode'].widget = ButtonGroupRadioSelect(
@@ -772,7 +767,8 @@ class ItemUpdateForm(I18nModelForm):
'name',
'internal_name',
'active',
- 'sales_channels',
+ 'all_sales_channels',
+ 'limit_sales_channels',
'admission',
'personalized',
'description',
@@ -829,6 +825,7 @@ class ItemUpdateForm(I18nModelForm):
'hidden_if_item_available': SafeModelChoiceField,
'grant_membership_type': SafeModelChoiceField,
'require_membership_types': SafeModelMultipleChoiceField,
+ 'limit_sales_channels': SafeModelMultipleChoiceField,
}
widgets = {
'available_from': SplitDateTimePickerWidget(),
@@ -900,18 +897,10 @@ class ItemVariationForm(I18nModelForm):
qs = kwargs.pop('membership_types')
super().__init__(*args, **kwargs)
change_decimal_field(self.fields['default_price'], self.event.currency)
- self.fields['sales_channels'] = forms.MultipleChoiceField(
- label=_('Sales channels'),
- required=False,
- choices=(
- (c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
- ),
- 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.'),
- widget=forms.CheckboxSelectMultiple
- )
- if not self.instance.pk:
- self.initial.setdefault('sales_channels', list(get_all_sales_channels().keys()))
+ self.fields['limit_sales_channels'].queryset = self.event.organizer.sales_channels.all()
+ self.fields['limit_sales_channels'].widget = SalesChannelCheckboxSelectMultiple(self.event, attrs={
+ 'data-inverse-dependency': '<[name$=all_sales_channels]',
+ }, choices=self.fields['limit_sales_channels'].widget.choices)
self.fields['description'].widget.attrs['rows'] = 3
if qs:
@@ -983,12 +972,14 @@ class ItemVariationForm(I18nModelForm):
'available_from_mode',
'available_until',
'available_until_mode',
- 'sales_channels',
+ 'all_sales_channels',
+ 'limit_sales_channels',
'hide_without_voucher',
]
field_classes = {
'available_from': SplitDateTimeField,
'available_until': SplitDateTimeField,
+ 'limit_sales_channels': SafeModelMultipleChoiceField,
}
widgets = {
'available_from': SplitDateTimePickerWidget(),
diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py
index d22d59c2f6..3f5c667964 100644
--- a/src/pretix/control/forms/organizer.py
+++ b/src/pretix/control/forms/organizer.py
@@ -50,6 +50,7 @@ from django_scopes.forms import SafeModelChoiceField
from i18nfield.forms import (
I18nForm, I18nFormField, I18nFormSetMixin, I18nTextInput,
)
+from i18nfield.strings import LazyI18nString
from phonenumber_field.formfields import PhoneNumberField
from pytz import common_timezones
@@ -68,7 +69,8 @@ from pretix.base.forms.widgets import (
)
from pretix.base.models import (
Customer, Device, EventMetaProperty, Gate, GiftCard, GiftCardAcceptance,
- Membership, MembershipType, OrderPosition, Organizer, ReusableMedium, Team,
+ Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
+ SalesChannel, Team,
)
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
from pretix.base.models.organizer import OrganizerFooterLink
@@ -1090,3 +1092,40 @@ class GiftCardAcceptanceInviteForm(forms.Form):
if self.organizer.gift_card_acceptor_acceptance.filter(acceptor=acceptor).exists():
raise ValidationError(_('The selected organizer has already been invited.'))
return acceptor
+
+
+class SalesChannelForm(I18nModelForm):
+ class Meta:
+ model = SalesChannel
+ fields = ['label', 'identifier']
+ widgets = {
+ 'default': forms.TextInput(),
+ }
+
+ def __init__(self, *args, **kwargs):
+ self.type = kwargs.pop("type")
+ super().__init__(*args, **kwargs)
+
+ if not self.type.multiple_allowed or (self.instance and self.instance.pk):
+ self.fields["identifier"].initial = self.type.identifier
+ self.fields["identifier"].disabled = True
+ self.fields["label"].initial = LazyI18nString.from_gettext(self.type.verbose_name)
+
+ def clean(self):
+ d = super().clean()
+
+ if self.instance.pk:
+ d["identifier"] = self.instance.identifier
+ elif self.type.multiple_allowed:
+ d["identifier"] = self.type.identifier + "." + d["identifier"]
+ else:
+ d["identifier"] = self.type.identifier
+
+ if not self.instance.pk:
+ # self.event is actually the organizer, sorry I18nModelForm!
+ if self.event.sales_channels.filter(identifier=d["identifier"]).exists():
+ raise ValidationError(
+ _("A sales channel with the same identifier already exists.")
+ )
+
+ return d
diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py
index 8c2a49b598..44da167da7 100644
--- a/src/pretix/control/logdisplay.py
+++ b/src/pretix/control/logdisplay.py
@@ -357,6 +357,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.membershiptype.created': _('The membership type has been created.'),
'pretix.membershiptype.changed': _('The membership type has been changed.'),
'pretix.membershiptype.deleted': _('The membership type has been deleted.'),
+ 'pretix.saleschannel.created': _('The sales channel has been created.'),
+ 'pretix.saleschannel.changed': _('The sales channel has been changed.'),
+ 'pretix.saleschannel.deleted': _('The sales channel has been deleted.'),
'pretix.customer.created': _('The account has been created.'),
'pretix.customer.changed': _('The account has been changed.'),
'pretix.customer.membership.created': _('A membership for this account has been added.'),
diff --git a/src/pretix/control/navigation.py b/src/pretix/control/navigation.py
index 46b8cc5a32..4fb90f823d 100644
--- a/src/pretix/control/navigation.py
+++ b/src/pretix/control/navigation.py
@@ -502,6 +502,13 @@ def get_organizer_navigation(request):
}),
'active': url.url_name == 'organizer.settings.mail',
},
+ {
+ 'label': _('Sales channels'),
+ 'url': reverse('control:organizer.channels', kwargs={
+ 'organizer': request.organizer.slug
+ }),
+ 'active': url.url_name.startswith('organizer.channel'),
+ },
{
'label': _('Webhooks'),
'url': reverse('control:organizer.webhooks', kwargs={
diff --git a/src/pretix/control/templates/pretixcontrol/checkin/lists.html b/src/pretix/control/templates/pretixcontrol/checkin/lists.html
index 5a0246dce3..00d3de598a 100644
--- a/src/pretix/control/templates/pretixcontrol/checkin/lists.html
+++ b/src/pretix/control/templates/pretixcontrol/checkin/lists.html
@@ -1,6 +1,7 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
+{% load static %}
{% load urlreplace %}
{% block title %}{% trans "Check-in lists" %}{% endblock %}
{% block inside %}
@@ -137,9 +138,14 @@
{% endif %}
{% endif %}
- {% for channel in cl.auto_checkin_sales_channels %}
-
+ {% for channel in cl.auto_checkin_sales_channels.all %}
+ {% if "." in channel.icon %}
+
+ {% else %}
+
+ {% endif %}
{% endfor %}
diff --git a/src/pretix/control/templates/pretixcontrol/event/payment.html b/src/pretix/control/templates/pretixcontrol/event/payment.html
index cb47b67347..1583ff9d6c 100644
--- a/src/pretix/control/templates/pretixcontrol/event/payment.html
+++ b/src/pretix/control/templates/pretixcontrol/event/payment.html
@@ -1,5 +1,6 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
+{% load static %}
{% load bootstrap3 %}
{% block inside %}
{% trans "Payment settings" %}
@@ -30,8 +31,13 @@
{% for channel in provider.sales_channels %}
-
+ {% if "." in channel.icon %}
+
+ {% else %}
+
+ {% endif %}
{% endfor %}
diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html
index db89dc9967..befac052fb 100644
--- a/src/pretix/control/templates/pretixcontrol/event/settings.html
+++ b/src/pretix/control/templates/pretixcontrol/event/settings.html
@@ -32,7 +32,8 @@
{% bootstrap_field sform.contact_mail layout="control" %}
{% bootstrap_field sform.imprint_url layout="control" %}
{% bootstrap_field form.is_public layout="control" %}
- {% bootstrap_field form.sales_channels layout="control" %}
+ {% bootstrap_field form.all_sales_channels layout="control" %}
+ {% bootstrap_field form.limit_sales_channels layout="control" %}
{% if meta_forms %}
- {% for k, c in sales_channels.items %}
- {% if k in d.sales_channels %}
-
+ {% for c in sales_channels %}
+ {% if d.all_sales_channels or c in d.limit_sales_channels.all %}
+ {% if "." in c.icon %}
+
+ {% else %}
+
+ {% endif %}
{% else %}
{% endif %}
{% endfor %}
diff --git a/src/pretix/control/templates/pretixcontrol/items/index.html b/src/pretix/control/templates/pretixcontrol/items/index.html
index 402018e4e1..fa309a0d81 100644
--- a/src/pretix/control/templates/pretixcontrol/items/index.html
+++ b/src/pretix/control/templates/pretixcontrol/items/index.html
@@ -1,6 +1,7 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load money %}
+{% load static %}
{% block title %}{% trans "Products" %}{% endblock %}
{% block inside %}
{% blocktrans asvar s_taxes %}taxes{% endblocktrans %}
@@ -66,10 +67,15 @@
#{{ i.pk }}
- {% for k, c in sales_channels.items %}
- {% if k in i.sales_channels %}
-
+ {% for c in sales_channels %}
+ {% if i.all_sales_channels or c in i.limit_sales_channels.all %}
+ {% if "." in c.icon %}
+
+ {% else %}
+
+ {% endif %}
{% else %}
{% endif %}
{% endfor %}
diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html
index 61a5252228..889ffc8b0b 100644
--- a/src/pretix/control/templates/pretixcontrol/order/index.html
+++ b/src/pretix/control/templates/pretixcontrol/order/index.html
@@ -185,9 +185,9 @@
{% trans "Cancellation date" %}
{{ order.cancellation_date|date:"SHORT_DATETIME_FORMAT" }}
{% endif %}
- {% if sales_channel %}
+ {% if order.sales_channel %}
{% trans "Sales channel" %}
- {{ sales_channel.verbose_name }}
+ {{ order.sales_channel.label }}
{% endif %}
{% trans "Order locale" %}
diff --git a/src/pretix/control/templates/pretixcontrol/orders/index.html b/src/pretix/control/templates/pretixcontrol/orders/index.html
index 00494672cf..cc1319f0a9 100644
--- a/src/pretix/control/templates/pretixcontrol/orders/index.html
+++ b/src/pretix/control/templates/pretixcontrol/orders/index.html
@@ -4,6 +4,7 @@
{% load urlreplace %}
{% load money %}
{% load bootstrap3 %}
+{% load static %}
{% block title %}{% trans "Orders" %}{% endblock %}
{% block content %}
{% trans "Orders" %}
@@ -201,8 +202,13 @@
{% endif %}
-
+ {% if "." in o.sales_channel.icon %}
+
+ {% else %}
+
+ {% endif %}
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/channel_add.html b/src/pretix/control/templates/pretixcontrol/organizers/channel_add.html
new file mode 100644
index 0000000000..b78dd88bb8
--- /dev/null
+++ b/src/pretix/control/templates/pretixcontrol/organizers/channel_add.html
@@ -0,0 +1,27 @@
+{% extends "pretixcontrol/organizers/base.html" %}
+{% load i18n %}
+{% load formset_tags %}
+{% load bootstrap3 %}
+{% block inner %}
+ {% trans "Add sales channel" %}
+
+{% endblock %}
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/channel_add_choice.html b/src/pretix/control/templates/pretixcontrol/organizers/channel_add_choice.html
new file mode 100644
index 0000000000..ed2076c7a1
--- /dev/null
+++ b/src/pretix/control/templates/pretixcontrol/organizers/channel_add_choice.html
@@ -0,0 +1,26 @@
+{% extends "pretixcontrol/organizers/base.html" %}
+{% load i18n %}
+{% load static %}
+{% load bootstrap3 %}
+{% block inner %}
+ {% trans "Add sales channel" %}
+
+{% endblock %}
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/channel_delete.html b/src/pretix/control/templates/pretixcontrol/organizers/channel_delete.html
new file mode 100644
index 0000000000..2a32bcf566
--- /dev/null
+++ b/src/pretix/control/templates/pretixcontrol/organizers/channel_delete.html
@@ -0,0 +1,33 @@
+{% extends "pretixcontrol/organizers/base.html" %}
+{% load i18n %}
+{% load bootstrap3 %}
+{% block inner %}
+ {% trans "Delete sales channel:" %} {{ channel.label }}
+
+{% endblock %}
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/channel_edit.html b/src/pretix/control/templates/pretixcontrol/organizers/channel_edit.html
new file mode 100644
index 0000000000..975373a603
--- /dev/null
+++ b/src/pretix/control/templates/pretixcontrol/organizers/channel_edit.html
@@ -0,0 +1,27 @@
+{% extends "pretixcontrol/organizers/base.html" %}
+{% load i18n %}
+{% load formset_tags %}
+{% load bootstrap3 %}
+{% block inner %}
+ {% trans "Sales channel:" %} {{ channel.label }}
+
+{% endblock %}
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/channels.html b/src/pretix/control/templates/pretixcontrol/organizers/channels.html
new file mode 100644
index 0000000000..9ef398a1e7
--- /dev/null
+++ b/src/pretix/control/templates/pretixcontrol/organizers/channels.html
@@ -0,0 +1,64 @@
+{% extends "pretixcontrol/organizers/base.html" %}
+{% load i18n %}
+{% load bootstrap3 %}
+{% load static %}
+{% block inner %}
+ {% trans "Sales channels" %}
+
+ {% blocktrans trimmed %}
+ On this page, you can manage the different channels your tickets can be sold through. This is useful
+ to unlock new revenue streams or to separate revenue between different sources for reporting purchases.
+ {% endblocktrans %}
+
+
+
+ {% trans "Add a new channel" %}
+
+
+{% endblock %}
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/customer.html b/src/pretix/control/templates/pretixcontrol/organizers/customer.html
index b39bbd5f7a..8d79ebdffd 100644
--- a/src/pretix/control/templates/pretixcontrol/organizers/customer.html
+++ b/src/pretix/control/templates/pretixcontrol/organizers/customer.html
@@ -2,6 +2,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% load money %}
+{% load static %}
{% block title %}
{% blocktrans trimmed with id=customer.identifier %}
Customer #{{ id }}
@@ -225,8 +226,13 @@
{{ o.event }}
-
+ {% if "." in o.sales_channel.icon %}
+
+ {% else %}
+
+ {% endif %}
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if o.customer_id != customer.pk %}
[^/]+)/property/reorder$', organizer.reorder_meta_properties,
name='organizer.properties.reorder'),
+ re_path(r'^organizer/(?P[^/]+)/channels$', organizer.ChannelListView.as_view(), name='organizer.channels'),
+ re_path(r'^organizer/(?P[^/]+)/channel/add$', organizer.ChannelCreateView.as_view(),
+ name='organizer.channel.add'),
+ re_path(r'^organizer/(?P[^/]+)/channel/(?P[^/]+)/edit$', organizer.ChannelUpdateView.as_view(),
+ name='organizer.channel.edit'),
+ re_path(r'^organizer/(?P[^/]+)/channel/(?P[^/]+)/delete$', organizer.ChannelDeleteView.as_view(),
+ name='organizer.channel.delete'),
+ re_path(r'^organizer/(?P[^/]+)/channel/(?P[^/]+)/up$', organizer.channel_move_up,
+ name='organizer.channel.up'),
+ re_path(r'^organizer/(?P[^/]+)/channel/(?P[^/]+)/down$', organizer.channel_move_down,
+ name='organizer.channel.down'),
+ re_path(r'^organizer/(?P[^/]+)/channel/reorder$', organizer.reorder_channels,
+ name='organizer.channels.reorder'),
re_path(r'^organizer/(?P[^/]+)/membershiptypes$', organizer.MembershipTypeListView.as_view(), name='organizer.membershiptypes'),
re_path(r'^organizer/(?P[^/]+)/membershiptype/add$', organizer.MembershipTypeCreateView.as_view(),
name='organizer.membershiptype.add'),
diff --git a/src/pretix/control/views/checkin.py b/src/pretix/control/views/checkin.py
index 54b1c57fd0..042322f89c 100644
--- a/src/pretix/control/views/checkin.py
+++ b/src/pretix/control/views/checkin.py
@@ -49,7 +49,6 @@ from django.views.generic import FormView, ListView, TemplateView
from i18nfield.strings import LazyI18nString
from pretix.api.views.checkin import _redeem_process
-from pretix.base.channels import get_all_sales_channels
from pretix.base.models import Checkin, Order, OrderPosition
from pretix.base.models.checkin import CheckinList
from pretix.base.services.checkin import (
@@ -296,7 +295,9 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
ordering = ('subevent__date_from', 'name', 'pk')
def get_queryset(self):
- qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related("limit_products")
+ qs = self.request.event.checkin_lists.select_related('subevent').prefetch_related(
+ "limit_products", "auto_checkin_sales_channels"
+ )
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
@@ -305,12 +306,10 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
clists = list(ctx['checkinlists'])
- sales_channels = get_all_sales_channels()
for cl in clists:
if cl.subevent:
cl.subevent.event = self.request.event # re-use same event object to make sure settings are cached
- cl.auto_checkin_sales_channels = [sales_channels[channel] for channel in cl.auto_checkin_sales_channels]
ctx['checkinlists'] = clists
ctx['can_change_organizer_settings'] = self.request.user.has_organizer_permission(
diff --git a/src/pretix/control/views/discounts.py b/src/pretix/control/views/discounts.py
index cb028d6f69..cc95ad7209 100644
--- a/src/pretix/control/views/discounts.py
+++ b/src/pretix/control/views/discounts.py
@@ -43,7 +43,6 @@ from pretix.control.permissions import (
)
from pretix.helpers.models import modelcopy
-from ...base.channels import get_all_sales_channels
from ...helpers.compat import CompatDeleteView
from . import CreateView, PaginationMixin, UpdateView
@@ -190,11 +189,14 @@ class DiscountList(PaginationMixin, ListView):
template_name = 'pretixcontrol/items/discounts.html'
def get_queryset(self):
- return self.request.event.discounts.prefetch_related('condition_limit_products')
+ return self.request.event.discounts.prefetch_related('condition_limit_products', 'limit_sales_channels')
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
- ctx['sales_channels'] = get_all_sales_channels()
+ ctx['sales_channels'] = [
+ c for c in self.request.organizer.sales_channels.all()
+ if c.type_instance.discounts_supported
+ ]
return ctx
diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py
index 8558eed2a0..831caf6ecd 100644
--- a/src/pretix/control/views/event.py
+++ b/src/pretix/control/views/event.py
@@ -71,7 +71,6 @@ from django.views.generic.detail import SingleObjectMixin
from i18nfield.strings import LazyI18nString
from i18nfield.utils import I18nJSONEncoder
-from pretix.base.channels import get_all_sales_channels
from pretix.base.email import get_available_placeholders
from pretix.base.forms import PlaceholderValidator
from pretix.base.models import Event, LogEntry, Order, TaxRule, Voucher
@@ -559,7 +558,7 @@ class PaymentSettings(EventSettingsViewMixin, EventSettingsFormView):
key=lambda s: s.verbose_name
)
- sales_channels = get_all_sales_channels()
+ sales_channels = {s.identifier: s for s in self.request.organizer.sales_channels.all()}
for p in context['providers']:
p.show_enabled = p.is_enabled
p.sales_channels = [sales_channels[channel] for channel in p.settings.get('_restrict_to_sales_channels', as_type=list, default=['web'])]
@@ -1468,7 +1467,7 @@ class QuickSetupView(FormView):
admission=True,
personalized=True,
position=i,
- sales_channels=list(get_all_sales_channels().keys())
+ all_sales_channels=True,
)
item.log_action('pretix.event.item.added', user=self.request.user, data=dict(f.cleaned_data))
if f.cleaned_data['quota'] or not form.cleaned_data['total_quota']:
diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py
index f64a4f13fc..c6810ce580 100644
--- a/src/pretix/control/views/item.py
+++ b/src/pretix/control/views/item.py
@@ -84,7 +84,6 @@ from pretix.control.permissions import (
from pretix.control.signals import item_forms, item_formsets
from pretix.helpers.models import modelcopy
-from ...base.channels import get_all_sales_channels
from ...helpers.compat import CompatDeleteView
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
@@ -106,14 +105,14 @@ class ItemList(ListView):
event=self.request.event
).annotate(
var_count=Count('variations')
- ).prefetch_related("category").order_by(
+ ).prefetch_related("category", "limit_sales_channels").order_by(
F('category__position').asc(nulls_first=True),
'category', 'position'
)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
- ctx['sales_channels'] = get_all_sales_channels()
+ ctx['sales_channels'] = self.request.organizer.sales_channels.all()
items_by_category = {cat: list(items) for cat, items in groupby(ctx['items'], lambda item: item.category)}
ctx['cat_list'] = [(cat, items_by_category.get(cat, [])) for cat in [None, *self.request.event.categories.all()]]
return ctx
@@ -1503,7 +1502,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
"Your participants won't be able to buy the bundle unless you remove this "
"item from it."))
- ctx['sales_channels'] = get_all_sales_channels()
+ ctx['sales_channels'] = self.request.organizer.sales_channels.all()
return ctx
@cached_property
@@ -1515,7 +1514,9 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
can_order=True, can_delete=True, extra=0
)(
self.request.POST if self.request.method == "POST" else None,
- queryset=ItemVariation.objects.filter(item=self.get_object()).prefetch_related('meta_values', 'require_membership_types'),
+ queryset=ItemVariation.objects.filter(item=self.get_object()).prefetch_related(
+ 'meta_values', 'limit_sales_channels', 'require_membership_types'
+ ),
event=self.request.event, prefix="variations"
)),
('addons', inlineformset_factory(
diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py
index 175a1b188c..f5d13b13f2 100644
--- a/src/pretix/control/views/orders.py
+++ b/src/pretix/control/views/orders.py
@@ -71,7 +71,6 @@ from django.views.generic import (
)
from i18nfield.strings import LazyI18nString
-from pretix.base.channels import get_all_sales_channels
from pretix.base.decimal import round_decimal
from pretix.base.email import get_email_context
from pretix.base.exporter import MultiSheetListExporter
@@ -375,7 +374,7 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
def get_queryset(self):
qs = Order.objects.filter(
event=self.request.event
- ).select_related('invoice_address')
+ ).select_related('invoice_address').prefetch_related("sales_channel")
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
@@ -420,7 +419,6 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
)
}
- scs = get_all_sales_channels()
for o in ctx['orders']:
if o.pk not in annotated:
continue
@@ -433,7 +431,6 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request']
o.computed_payment_refund_sum = annotated.get(o.pk)['computed_payment_refund_sum']
o.icnt = annotated.get(o.pk)['icnt']
- o.sales_channel_obj = scs[o.sales_channel]
if ctx['page_obj'].paginator.count < 1000:
# Performance safeguard: Only count positions if the data set is small
@@ -520,7 +517,6 @@ class OrderDetail(OrderView):
ctx['display_locale'] = dict(settings.LANGUAGES)[self.object.locale or self.request.event.settings.locale]
ctx['overpaid'] = self.order.pending_sum * -1
- ctx['sales_channel'] = get_all_sales_channels().get(self.order.sales_channel)
ctx['download_buttons'] = self.download_buttons
ctx['payment_refund_sum'] = self.order.payment_refund_sum
ctx['pending_sum'] = self.order.pending_sum
diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py
index 852721336a..708b3bc3fb 100644
--- a/src/pretix/control/views/organizer.py
+++ b/src/pretix/control/views/organizer.py
@@ -56,7 +56,7 @@ from django.forms import DecimalField
from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, JsonResponse,
)
-from django.shortcuts import get_object_or_404, redirect
+from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.functional import cached_property
@@ -71,7 +71,7 @@ from django.views.generic import (
from pretix.api.models import ApiCall, WebHook
from pretix.api.webhooks import manually_retry_all_calls
from pretix.base.auth import get_auth_backends
-from pretix.base.channels import get_all_sales_channels
+from pretix.base.channels import get_all_sales_channel_types
from pretix.base.exporter import (
MultiSheetListExporter, OrganizerLevelExportMixin,
)
@@ -87,7 +87,7 @@ from pretix.base.models.giftcards import (
GiftCardAcceptance, GiftCardTransaction, gen_giftcard_secret,
)
from pretix.base.models.orders import CancellationRequest
-from pretix.base.models.organizer import TeamAPIToken
+from pretix.base.models.organizer import SalesChannel, TeamAPIToken
from pretix.base.payment import PaymentException
from pretix.base.services.export import multiexport, scheduled_organizer_export
from pretix.base.services.mail import SendMailException, mail
@@ -107,8 +107,8 @@ from pretix.control.forms.organizer import (
MailSettingsForm, MembershipTypeForm, MembershipUpdateForm,
OrganizerDeleteForm, OrganizerFooterLinkFormset, OrganizerForm,
OrganizerSettingsForm, OrganizerUpdateForm, ReusableMediumCreateForm,
- ReusableMediumUpdateForm, SSOClientForm, SSOProviderForm, TeamForm,
- WebHookForm,
+ ReusableMediumUpdateForm, SalesChannelForm, SSOClientForm, SSOProviderForm,
+ TeamForm, WebHookForm,
)
from pretix.control.forms.rrule import RRuleForm
from pretix.control.logdisplay import OVERVIEW_BANLIST
@@ -2206,7 +2206,7 @@ def meta_property_move_down(request, organizer, property):
@transaction.atomic
-@organizer_permission_required("can_change_items")
+@organizer_permission_required("can_change_organizer_settings")
@require_http_methods(["POST"])
def reorder_meta_properties(request, organizer):
try:
@@ -2643,7 +2643,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
q |= Q(email__iexact=self.customer.email)
qs = Order.objects.filter(
q
- ).select_related('event').order_by('-datetime', 'pk')
+ ).select_related('event').prefetch_related('sales_channel').order_by('-datetime', 'pk')
return qs
@cached_property
@@ -2720,7 +2720,6 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
)
}
- scs = get_all_sales_channels()
for o in ctx['orders']:
if o.pk not in annotated:
continue
@@ -2733,7 +2732,6 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
o.has_cancellation_request = annotated.get(o.pk)['has_cancellation_request']
o.computed_payment_refund_sum = annotated.get(o.pk)['computed_payment_refund_sum']
o.icnt = annotated.get(o.pk)['icnt']
- o.sales_channel_obj = scs[o.sales_channel]
ctx["lifetime_spending"] = (
self.get_queryset()
@@ -3040,3 +3038,242 @@ class ReusableMediumUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
'organizer': self.request.organizer.slug,
'pk': self.object.pk,
})
+
+
+class ChannelListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
+ model = SalesChannel
+ template_name = 'pretixcontrol/organizers/channels.html'
+ permission = 'can_change_organizer_settings'
+ context_object_name = 'channels'
+
+ def get_queryset(self):
+ return self.request.organizer.sales_channels.all()
+
+
+class ChannelEditorMixin:
+ form_class = SalesChannelForm
+
+ def get_form_kwargs(self):
+ return {
+ **super().get_form_kwargs(),
+ 'event': self.request.organizer,
+ }
+
+
+class ChannelCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ChannelEditorMixin, CreateView):
+ model = SalesChannel
+ permission = 'can_change_organizer_settings'
+ template_name = 'pretixcontrol/organizers/channel_add.html'
+
+ def get_object(self, queryset=None):
+ return SalesChannel()
+
+ @property
+ def allowed_types(self):
+ existing_types = set(self.request.organizer.sales_channels.values_list("type", flat=True))
+ return {
+ k: t for k, t in get_all_sales_channel_types().items()
+ if t.multiple_allowed or t.identifier not in existing_types
+ }
+
+ @cached_property
+ def selected_type(self):
+ try:
+ return self.allowed_types[self.request.GET.get("type")]
+ except KeyError:
+ return None
+
+ def post(self, request, *args, **kwargs):
+ if not self.selected_type:
+ return render(request, "pretixcontrol/organizers/channel_add_choice.html", {
+ "types": self.allowed_types.values()
+ })
+ return super().post(request, *args, **kwargs)
+
+ def get(self, request, *args, **kwargs):
+ if not self.selected_type:
+ return render(request, "pretixcontrol/organizers/channel_add_choice.html", {
+ "types": self.allowed_types.values()
+ })
+ return super().get(request, *args, **kwargs)
+
+ def get_success_url(self):
+ return reverse('control:organizer.channels', kwargs={
+ 'organizer': self.request.organizer.slug,
+ })
+
+ def get_context_data(self, **kwargs):
+ ctx = super().get_context_data(**kwargs)
+ ctx["type"] = self.selected_type
+ if self.selected_type.multiple_allowed:
+ ctx["identifier_prefix"] = self.selected_type.identifier + "."
+ return ctx
+
+ def get_form_kwargs(self):
+ return {
+ **super().get_form_kwargs(),
+ "type": self.selected_type,
+ }
+
+ def form_valid(self, form):
+ messages.success(self.request, _('The sales channel has been created.'))
+ form.instance.organizer = self.request.organizer
+ form.instance.type = self.selected_type.identifier
+ form.instance.position = (self.request.organizer.sales_channels.aggregate(m=Max("position"))["m"] or 0) + 1
+ ret = super().form_valid(form)
+ form.instance.log_action('pretix.saleschannel.created', user=self.request.user, data={
+ k: getattr(self.object, k) for k in form.changed_data
+ })
+ return ret
+
+ def form_invalid(self, form):
+ messages.error(self.request, _('Your changes could not be saved.'))
+ return super().form_invalid(form)
+
+
+class ChannelUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ChannelEditorMixin, UpdateView):
+ model = SalesChannel
+ permission = 'can_change_organizer_settings'
+ context_object_name = 'channel'
+ template_name = 'pretixcontrol/organizers/channel_edit.html'
+
+ def get_object(self, queryset=None):
+ return get_object_or_404(SalesChannel, organizer=self.request.organizer, identifier=self.kwargs.get('channel'))
+
+ def get_success_url(self):
+ return reverse('control:organizer.channels', kwargs={
+ 'organizer': self.request.organizer.slug,
+ })
+
+ @cached_property
+ def type(self):
+ return get_all_sales_channel_types()[self.object.type]
+
+ def get_context_data(self, **kwargs):
+ ctx = super().get_context_data(**kwargs)
+ ctx["type"] = self.type
+ return ctx
+
+ def get_form_kwargs(self):
+ return {
+ **super().get_form_kwargs(),
+ "type": self.type,
+ }
+
+ def form_valid(self, form):
+ if form.has_changed() or self.formset.has_changed():
+ self.object.log_action('pretix.saleschannel.changed', user=self.request.user, data={
+ k: getattr(self.object, k)
+ for k in form.changed_data
+ })
+ messages.success(self.request, _('Your changes have been saved.'))
+ return super().form_valid(form)
+
+ def form_invalid(self, form):
+ messages.error(self.request, _('Your changes could not be saved.'))
+ return super().form_invalid(form)
+
+
+class ChannelDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
+ model = SalesChannel
+ template_name = 'pretixcontrol/organizers/channel_delete.html'
+ permission = 'can_change_organizer_settings'
+ context_object_name = 'channel'
+
+ def get_object(self, queryset=None):
+ return get_object_or_404(SalesChannel, organizer=self.request.organizer, identifier=self.kwargs.get('channel'))
+
+ def get_success_url(self):
+ return reverse('control:organizer.channels', kwargs={
+ 'organizer': self.request.organizer.slug,
+ })
+
+ def get_context_data(self, **kwargs):
+ ctx = super().get_context_data()
+ ctx["is_allowed"] = self.get_object().allow_delete
+ return ctx
+
+ @transaction.atomic
+ def delete(self, request, *args, **kwargs):
+ self.object = self.get_object()
+ success_url = self.get_success_url()
+ if not self.object.allow_delete():
+ messages.error(self.request, _('This channel can not be deleted.'))
+ return redirect(success_url)
+ try:
+ self.object.log_action('pretix.saleschannel.deleted', user=self.request.user)
+ self.object.delete()
+ messages.success(request, _('The selected sales channel has been deleted.'))
+ except ProtectedError:
+ messages.error(self.request, _('The channel could not be deleted as some constraints (e.g. data created by '
+ 'plug-ins) did not allow it.'))
+ return redirect(success_url)
+
+
+def channel_move(request, channel, up=True):
+ channel = get_object_or_404(request.organizer.sales_channels, identifier=channel)
+ channels = list(request.organizer.sales_channels.order_by("position"))
+
+ index = channels.index(channel)
+ if index != 0 and up:
+ channels[index - 1], channels[index] = channels[index], channels[index - 1]
+ elif index != len(channels) - 1 and not up:
+ channels[index + 1], channels[index] = channels[index], channels[index + 1]
+
+ for i, prop in enumerate(channels):
+ if prop.position != i:
+ prop.position = i
+ prop.save()
+ prop.log_action(
+ 'pretix.saleschannel.reordered', user=request.user, data={
+ 'position': i,
+ }
+ )
+ messages.success(request, _('The order of sales channels has been updated.'))
+
+
+@organizer_permission_required("can_change_organizer_settings")
+@require_http_methods(["POST"])
+def channel_move_up(request, organizer, channel):
+ channel_move(request, channel, up=True)
+ return redirect('control:organizer.channels',
+ organizer=request.organizer.slug)
+
+
+@organizer_permission_required("can_change_organizer_settings")
+@require_http_methods(["POST"])
+def channel_move_down(request, organizer, channel):
+ channel_move(request, channel, up=False)
+ return redirect('control:organizer.channels',
+ organizer=request.organizer.slug)
+
+
+@transaction.atomic
+@organizer_permission_required("can_change_organizer_settings")
+@require_http_methods(["POST"])
+def reorder_channels(request, organizer):
+ try:
+ ids = json.loads(request.body.decode('utf-8'))['ids']
+ except (JSONDecodeError, KeyError, ValueError):
+ return HttpResponseBadRequest("expected JSON: {ids:[]}")
+
+ input_channels = list(request.organizer.sales_channels.filter(id__in=[i for i in ids if i.isdigit()]))
+
+ if len(input_channels) != len(ids):
+ raise Http404(_("Some of the provided object ids are invalid."))
+
+ if len(input_channels) != request.organizer.sales_channels.count():
+ raise Http404(_("Not all objects have been selected."))
+
+ for c in input_channels:
+ pos = ids.index(str(c.pk))
+ if pos != c.position: # Save unneccessary UPDATE queries
+ c.position = pos
+ c.save(update_fields=['position'])
+ c.log_action(
+ 'pretix.saleschannel.reordered', user=request.user, data={
+ 'position': pos,
+ }
+ )
+
+ return HttpResponse()
diff --git a/src/pretix/control/views/pdf.py b/src/pretix/control/views/pdf.py
index 9407cb949d..ce7de7ce77 100644
--- a/src/pretix/control/views/pdf.py
+++ b/src/pretix/control/views/pdf.py
@@ -98,6 +98,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
from pretix.base.models import Order
order = self.request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
email='sample@pretix.eu',
+ sales_channel=self.request.event.organizer.sales_channels.get(identifier="web"),
locale=self.request.event.settings.locale,
expires=now(), code="PREVIEW1234", total=Decimal('119.00'))
diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py
index 0889d53378..95b3929dd0 100644
--- a/src/pretix/plugins/stripe/payment.py
+++ b/src/pretix/plugins/stripe/payment.py
@@ -1162,7 +1162,7 @@ class StripeCC(StripeMethod):
request.sales_channel.identifier == 'resellers'
if payment:
- return moto and payment.order.sales_channel == 'resellers'
+ return moto and payment.order.sales_channel.identifier == 'resellers'
return moto
diff --git a/src/pretix/plugins/ticketoutputpdf/api.py b/src/pretix/plugins/ticketoutputpdf/api.py
index 3f2ddba236..efcbba4380 100644
--- a/src/pretix/plugins/ticketoutputpdf/api.py
+++ b/src/pretix/plugins/ticketoutputpdf/api.py
@@ -21,31 +21,50 @@
#
from django.conf import settings
from django.db import transaction
-from rest_framework import viewsets
+from django.db.models import QuerySet
+from django.utils.functional import lazy
+from rest_framework import serializers, viewsets
from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
from ...api.serializers.fields import UploadedFileField
+from ...base.models import SalesChannel
from ...base.pdf import PdfLayoutValidator
from ...multidomain.utils import static_absolute
from .models import TicketLayout, TicketLayoutItem
class ItemAssignmentSerializer(I18nAwareModelSerializer):
+ sales_channel = serializers.SlugRelatedField(
+ slug_field='identifier',
+ queryset=SalesChannel.objects.none(),
+ )
class Meta:
model = TicketLayoutItem
fields = ('id', 'layout', 'item', 'sales_channel')
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields["sales_channel"].queryset = self.context["event"].organizer.sales_channels.all()
+
class NestedItemAssignmentSerializer(I18nAwareModelSerializer):
+ sales_channel = serializers.SlugRelatedField(
+ slug_field='identifier',
+ queryset=SalesChannel.objects.none(),
+ )
class Meta:
model = TicketLayoutItem
fields = ('item', 'sales_channel')
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields["sales_channel"].queryset = lazy(lambda: self.context["event"].organizer.sales_channels.all(), QuerySet)
+
class TicketLayoutSerializer(I18nAwareModelSerializer):
layout = CompatibleJSONField(
@@ -131,3 +150,9 @@ class TicketLayoutItemViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
return TicketLayoutItem.objects.filter(item__event=self.request.event)
+
+ def get_serializer_context(self):
+ return {
+ **super().get_serializer_context(),
+ 'event': self.request.event,
+ }
diff --git a/src/pretix/plugins/ticketoutputpdf/exporters.py b/src/pretix/plugins/ticketoutputpdf/exporters.py
index d993ff0974..5663ee3adc 100644
--- a/src/pretix/plugins/ticketoutputpdf/exporters.py
+++ b/src/pretix/plugins/ticketoutputpdf/exporters.py
@@ -223,7 +223,7 @@ class AllTicketsPDF(BaseExporter):
with language(op.order.locale, o.event.settings.region):
layout = o.layout_map.get(
- (op.item_id, op.order.sales_channel),
+ (op.item_id, op.order.sales_channel_id),
o.layout_map.get(
(op.item_id, 'web'),
o.default_layout
diff --git a/src/pretix/plugins/ticketoutputpdf/forms.py b/src/pretix/plugins/ticketoutputpdf/forms.py
index 15f4244133..57e8999c9c 100644
--- a/src/pretix/plugins/ticketoutputpdf/forms.py
+++ b/src/pretix/plugins/ticketoutputpdf/forms.py
@@ -47,7 +47,7 @@ class TicketLayoutItemForm(forms.ModelForm):
super().__init__(*args, **kwargs)
if self.sales_channel.identifier != 'web':
self.fields['layout'].label = _('PDF ticket layout for {channel}').format(
- channel=self.sales_channel.verbose_name
+ channel=self.sales_channel.label
)
self.fields['layout'].empty_label = _('(Same as PDF ticket layout)')
else:
@@ -57,7 +57,7 @@ class TicketLayoutItemForm(forms.ModelForm):
self.fields['layout'].required = False
def save(self, commit=True):
- self.instance.sales_channel = self.sales_channel.identifier
+ self.instance.sales_channel = self.sales_channel
if self.cleaned_data['layout'] is None:
if self.instance.pk:
self.instance.delete()
diff --git a/src/pretix/plugins/ticketoutputpdf/migrations/0009_sales_channels_new_fields.py b/src/pretix/plugins/ticketoutputpdf/migrations/0009_sales_channels_new_fields.py
new file mode 100644
index 0000000000..e42e63dd66
--- /dev/null
+++ b/src/pretix/plugins/ticketoutputpdf/migrations/0009_sales_channels_new_fields.py
@@ -0,0 +1,37 @@
+# Generated by Django 4.2.10 on 2024-05-14 10:19
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("pretixbase", "0267_remove_old_sales_channels"),
+ ("ticketoutputpdf", "0008_bigint"),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="ticketlayoutitem",
+ old_name="sales_channel",
+ new_name="sales_channel_type",
+ ),
+ migrations.AlterUniqueTogether(
+ name="ticketlayoutitem",
+ unique_together=set(),
+ ),
+ migrations.AddField(
+ model_name="ticketlayoutitem",
+ name="sales_channel",
+ field=models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="pretixbase.saleschannel",
+ ),
+ ),
+ migrations.AlterUniqueTogether(
+ name="ticketlayoutitem",
+ unique_together={("item", "layout", "sales_channel")},
+ ),
+ ]
diff --git a/src/pretix/plugins/ticketoutputpdf/migrations/0010_sales_channels_migrate_data.py b/src/pretix/plugins/ticketoutputpdf/migrations/0010_sales_channels_migrate_data.py
new file mode 100644
index 0000000000..345c540783
--- /dev/null
+++ b/src/pretix/plugins/ticketoutputpdf/migrations/0010_sales_channels_migrate_data.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.2.10 on 2024-05-14 10:30
+
+from django.db import migrations
+
+
+def use_sales_channels(apps, schema_editor):
+ SalesChannel = apps.get_model("pretixbase", "SalesChannel")
+ TicketLayoutItem = apps.get_model("ticketoutputpdf", "TicketLayoutItem")
+ for sc in SalesChannel.objects.all():
+ TicketLayoutItem.objects.filter(item__event__organizer_id=sc.organizer_id, sales_channel_type=sc.identifier).update(
+ sales_channel=sc
+ )
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("ticketoutputpdf", "0009_sales_channels_new_fields"),
+ ]
+
+ operations = [
+ migrations.RunPython(use_sales_channels, migrations.RunPython.noop),
+ ]
diff --git a/src/pretix/plugins/ticketoutputpdf/migrations/0011_sales_channels_remove_old_fields.py b/src/pretix/plugins/ticketoutputpdf/migrations/0011_sales_channels_remove_old_fields.py
new file mode 100644
index 0000000000..8c4386970d
--- /dev/null
+++ b/src/pretix/plugins/ticketoutputpdf/migrations/0011_sales_channels_remove_old_fields.py
@@ -0,0 +1,27 @@
+# Generated by Django 4.2.10 on 2024-05-14 10:31
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("pretixbase", "0267_remove_old_sales_channels"),
+ ("ticketoutputpdf", "0010_sales_channels_migrate_data"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="ticketlayoutitem",
+ name="sales_channel_type",
+ ),
+ migrations.AlterField(
+ model_name="ticketlayoutitem",
+ name="sales_channel",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="pretixbase.saleschannel",
+ ),
+ ),
+ ]
diff --git a/src/pretix/plugins/ticketoutputpdf/models.py b/src/pretix/plugins/ticketoutputpdf/models.py
index 1f074eda3d..8619b627ed 100644
--- a/src/pretix/plugins/ticketoutputpdf/models.py
+++ b/src/pretix/plugins/ticketoutputpdf/models.py
@@ -254,7 +254,10 @@ class TicketLayoutItem(models.Model):
item = models.ForeignKey('pretixbase.Item', null=True, blank=True, related_name='ticketlayout_assignments',
on_delete=models.CASCADE)
layout = models.ForeignKey('TicketLayout', on_delete=models.CASCADE, related_name='item_assignments')
- sales_channel = models.CharField(max_length=190, default='web')
+ sales_channel = models.ForeignKey(
+ "pretixbase.SalesChannel",
+ on_delete=models.CASCADE,
+ )
class Meta:
unique_together = (('item', 'layout', 'sales_channel'),)
diff --git a/src/pretix/plugins/ticketoutputpdf/signals.py b/src/pretix/plugins/ticketoutputpdf/signals.py
index 4cbb779fc3..eb1d377a9d 100644
--- a/src/pretix/plugins/ticketoutputpdf/signals.py
+++ b/src/pretix/plugins/ticketoutputpdf/signals.py
@@ -28,8 +28,7 @@ from django.urls import reverse
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _
-from pretix.base.channels import get_all_sales_channels
-from pretix.base.models import Event
+from pretix.base.models import Event, SalesChannel
from pretix.base.signals import ( # NOQA: legacy import
EventPluginSignal, event_copy_data, item_copy_data, layout_text_variables,
logentry_display, logentry_object_link, register_data_exporters,
@@ -67,25 +66,25 @@ def register_multievent_data(sender, **kwargs):
def control_item_forms(sender, request, item, **kwargs):
forms = []
queryset = sender.ticket_layouts.all()
- for k, v in sorted(list(get_all_sales_channels().items()), key=lambda a: (int(a[0] != 'web'), a[0])):
+ for sc in sorted(list(sender.organizer.sales_channels.all()), key=lambda a: (int(a.identifier != 'web'), a.identifier)):
try:
- inst = TicketLayoutItem.objects.get(item=item, sales_channel=k)
+ inst = TicketLayoutItem.objects.get(item=item, sales_channel=sc)
except TicketLayoutItem.DoesNotExist:
inst = TicketLayoutItem(item=item)
forms.append(TicketLayoutItemForm(
instance=inst,
event=sender,
- sales_channel=v,
+ sales_channel=sc,
queryset=queryset,
data=(request.POST if request.method == "POST" else None),
- prefix="ticketlayoutitem_{}".format(k)
+ prefix="ticketlayoutitem_{}".format(sc.identifier)
))
return forms
@receiver(item_copy_data, dispatch_uid="pretix_ticketoutputpdf_item_copy")
def copy_item(sender, source, target, **kwargs):
- for tli in TicketLayoutItem.objects.filter(item=source):
+ for tli in TicketLayoutItem.objects.filter(item=source).select_related("sales_channel"):
TicketLayoutItem.objects.create(item=target, layout=tli.layout, sales_channel=tli.sales_channel)
@@ -122,9 +121,16 @@ def pdf_event_copy_data_receiver(sender, other, item_map, question_map, **kwargs
layout_map[oldid] = bl
- for bi in TicketLayoutItem.objects.filter(item__event=other):
+ for bi in TicketLayoutItem.objects.filter(item__event=other).select_related("sales_channel"):
+ if sender.organizer != other.organizer:
+ try:
+ sc = sender.organizer.sales_channels.get(identifier=bi.sales_channel.identifier)
+ except SalesChannel.DoesNotExist:
+ continue
+ else:
+ sc = bi.sales_channel
TicketLayoutItem.objects.create(item=item_map.get(bi.item_id), layout=layout_map.get(bi.layout_id),
- sales_channel=bi.sales_channel)
+ sales_channel=sc)
return layout_map
@@ -167,7 +173,7 @@ def _ticket_layouts_for_item(request, item):
request._ticket_layouts_for_item = {}
if item.pk not in request._ticket_layouts_for_item:
request._ticket_layouts_for_item[item.pk] = {
- tli.sales_channel: tli.layout
+ tli.sales_channel.identifier: tli.layout
for tli in item.ticketlayout_assignments.select_related('layout')
}
if request._ticket_layouts_for_item[item.pk] and 'web' not in request._ticket_layouts_for_item[item.pk]:
@@ -184,10 +190,10 @@ def control_order_position_info(sender: Event, position, request, order, **kwarg
layouts = []
seen = set()
lm = _ticket_layouts_for_item(request, position.item)
- if order.sales_channel in lm:
- seen.add(lm[order.sales_channel])
+ if order.sales_channel.identifier in lm:
+ seen.add(lm[order.sales_channel.identifier])
for k, l in lm.items():
- if k == order.sales_channel or l in seen:
+ if k == order.sales_channel.identifier or l in seen:
continue
layouts.append({
'label': str(l.name),
diff --git a/src/pretix/plugins/ticketoutputpdf/ticketoutput.py b/src/pretix/plugins/ticketoutputpdf/ticketoutput.py
index e8c797a14c..2179cec609 100644
--- a/src/pretix/plugins/ticketoutputpdf/ticketoutput.py
+++ b/src/pretix/plugins/ticketoutputpdf/ticketoutput.py
@@ -75,8 +75,8 @@ class PdfTicketOutput(BaseTicketOutput):
def layout_map(self):
if not hasattr(self.event, '_ticketoutputpdf_cache_layoutmap'):
self.event._ticketoutputpdf_cache_layoutmap = {
- (bi.item_id, bi.sales_channel): bi.layout
- for bi in TicketLayoutItem.objects.select_related('layout').filter(item__event=self.event)
+ (bi.item_id, bi.sales_channel.identifier): bi.layout
+ for bi in TicketLayoutItem.objects.select_related('layout', 'sales_channel').filter(item__event=self.event)
}
return self.event._ticketoutputpdf_cache_layoutmap
@@ -118,7 +118,7 @@ class PdfTicketOutput(BaseTicketOutput):
for op in self.get_tickets_to_print(order):
layout = override_layout.send_chained(
order.event, 'layout', orderposition=op, layout=self.layout_map.get(
- (op.item_id, self.override_channel or order.sales_channel),
+ (op.item_id, self.override_channel or order.sales_channel.identifier),
self.layout_map.get(
(op.item_id, 'web'),
self.default_layout
@@ -139,7 +139,7 @@ class PdfTicketOutput(BaseTicketOutput):
layout = override_layout.send_chained(
order.event, 'layout', orderposition=op, layout=self.layout_map.get(
- (op.item_id, self.override_channel or order.sales_channel),
+ (op.item_id, self.override_channel or order.sales_channel.identifier),
self.layout_map.get(
(op.item_id, 'web'),
self.default_layout
diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py
index 49298d2ce4..12b238036d 100644
--- a/src/pretix/presale/checkoutflow.py
+++ b/src/pretix/presale/checkoutflow.py
@@ -249,7 +249,7 @@ class CustomerStep(CartMixin, TemplateFlowStep):
icon = 'user'
def is_applicable(self, request):
- return request.organizer.settings.customer_accounts and request.sales_channel.customer_accounts_supported
+ return request.organizer.settings.customer_accounts and request.sales_channel.type_instance.customer_accounts_supported
@cached_property
def login_form(self):
@@ -539,7 +539,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
self.request.event,
subevent=cartpos.subevent,
voucher=None,
- channel=self.request.sales_channel.identifier,
+ channel=self.request.sales_channel,
base_qs=iao.addon_category.items,
allow_addons=True,
quota_cache=quota_cache,
@@ -935,7 +935,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
event=self.request.event,
cart_id=get_or_create_cart_id(request),
invoice_address=addr,
- sales_channel=request.sales_channel.identifier,
+ sales_channel=request.sales_channel,
)
diff = cm.recompute_final_prices_and_taxes()
except TaxRule.SaleNotAllowed:
diff --git a/src/pretix/presale/forms/waitinglist.py b/src/pretix/presale/forms/waitinglist.py
index 1343c1fbcb..785bcdb34d 100644
--- a/src/pretix/presale/forms/waitinglist.py
+++ b/src/pretix/presale/forms/waitinglist.py
@@ -51,7 +51,10 @@ class WaitingListForm(forms.ModelForm):
('', '')
]
items, display_add_to_cart = get_grouped_items(
- self.event, self.instance.subevent, require_seat=None,
+ self.event,
+ subevent=self.instance.subevent,
+ require_seat=None,
+ channel=self.event.organizer.sales_channels.get(identifier="web"),
memberships=(
customer.usable_memberships(
for_event=self.instance.subevent or self.event,
diff --git a/src/pretix/presale/middleware.py b/src/pretix/presale/middleware.py
index 81d4e137f1..8832d45509 100644
--- a/src/pretix/presale/middleware.py
+++ b/src/pretix/presale/middleware.py
@@ -36,7 +36,6 @@ from django.template.response import TemplateResponse
from django.urls import resolve
from django_scopes import scope
-from pretix.base.channels import WebshopSalesChannel
from pretix.base.timemachine import time_machine_now_assigned_from_request
from pretix.presale.signals import process_response
@@ -57,10 +56,6 @@ class EventMiddleware:
url = resolve(request.path_info)
request._namespace = url.namespace
- if not hasattr(request, 'sales_channel'):
- # The environ lookup is only relevant during unit testing
- request.sales_channel = request.environ.get('PRETIX_SALES_CHANNEL', WebshopSalesChannel())
-
if url.namespace != 'presale':
return self.get_response(request)
@@ -71,6 +66,12 @@ class EventMiddleware:
with scope(organizer=getattr(request, 'organizer', None)), \
time_machine_now_assigned_from_request(request):
+ if not hasattr(request, 'sales_channel') and hasattr(request, 'organizer'):
+ # The environ lookup is only relevant during unit testing
+ request.sales_channel = request.organizer.sales_channels.get(
+ identifier=request.environ.get('PRETIX_SALES_CHANNEL', 'web')
+ )
+
response = self.get_response(request)
if hasattr(request, '_namespace') and request._namespace == 'presale' and hasattr(request, 'event'):
diff --git a/src/pretix/presale/templates/pretixpresale/event/base.html b/src/pretix/presale/templates/pretixpresale/event/base.html
index 1baeda2350..b925deb129 100644
--- a/src/pretix/presale/templates/pretixpresale/event/base.html
+++ b/src/pretix/presale/templates/pretixpresale/event/base.html
@@ -120,7 +120,7 @@
{% if request.event.testmode %}
- {% if request.sales_channel.testmode_supported %}
+ {% if request.sales_channel.type_instance.testmode_supported %}
{% trans "Warning" context "alert-messages" %}:
@@ -194,7 +194,7 @@
{% endif %}
{% if request.event.testmode %}
- {% if request.sales_channel.testmode_supported %}
+ {% if request.sales_channel.type_instance.testmode_supported %}
{% trans "Warning" context "alert-messages" %}:
diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html b/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html
index f0f05523f1..b17a711ff8 100644
--- a/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html
+++ b/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html
@@ -89,7 +89,7 @@
{{ p.provider.test_mode_message }}
- {% if not request.sales_channel.testmode_supported %}
+ {% if not request.sales_channel.type_instance.testmode_supported %}
{% trans "This sales channel does not provide support for test mode." %}
diff --git a/src/pretix/presale/utils.py b/src/pretix/presale/utils.py
index 297d8cb55c..b933cc39c5 100644
--- a/src/pretix/presale/utils.py
+++ b/src/pretix/presale/utils.py
@@ -415,6 +415,12 @@ def _event_view(function=None, require_live=True, require_plugin=None):
else:
with scope(organizer=getattr(request, 'organizer', None)), \
time_machine_now_assigned_from_request(request):
+ if not hasattr(request, 'sales_channel') and hasattr(request, 'organizer'):
+ # The environ lookup is only relevant during unit testing
+ request.sales_channel = request.organizer.sales_channels.get(
+ identifier=request.environ.get('PRETIX_SALES_CHANNEL', 'web')
+ )
+
response = func(request=request, *args, **kwargs)
if getattr(request, 'event', None):
for receiver, r in process_response.send(request.event, request=request, response=response):
diff --git a/src/pretix/presale/views/cart.py b/src/pretix/presale/views/cart.py
index 92c1880562..1d73d22fa6 100644
--- a/src/pretix/presale/views/cart.py
+++ b/src/pretix/presale/views/cart.py
@@ -515,7 +515,7 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
return u
def post(self, request, *args, **kwargs):
- if request.sales_channel.identifier not in request.event.sales_channels:
+ if not request.event.all_sales_channels and request.sales_channel.identifier not in (s.identifier for s in request.event.limit_sales_channels.all()):
raise Http404(_('Tickets for this event cannot be purchased on this sales channel.'))
cart_id = get_or_create_cart_id(self.request)
@@ -564,9 +564,9 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
# Fetch all items
items, display_add_to_cart = get_grouped_items(
self.request.event,
- self.subevent,
+ subevent=self.subevent,
voucher=self.voucher,
- channel=self.request.sales_channel.identifier,
+ channel=self.request.sales_channel,
memberships=(
self.request.customer.usable_memberships(
for_event=self.subevent or self.request.event,
diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py
index c1b56c5255..1106dc16c4 100644
--- a/src/pretix/presale/views/event.py
+++ b/src/pretix/presale/views/event.py
@@ -63,10 +63,9 @@ from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView
-from pretix.base.channels import get_all_sales_channels
from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import (
- ItemVariation, Quota, SeatCategoryMapping, Voucher,
+ ItemVariation, Quota, SalesChannel, SeatCategoryMapping, Voucher,
)
from pretix.base.models.event import Event, SubEvent
from pretix.base.models.items import (
@@ -110,7 +109,7 @@ def item_group_by_category(items):
)
-def get_grouped_items(event, subevent=None, voucher=None, channel='web', require_seat=0, base_qs=None, allow_addons=False,
+def get_grouped_items(event, *, channel: SalesChannel, subevent=None, voucher=None, require_seat=0, base_qs=None, allow_addons=False,
quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
ignore_hide_sold_out_for_item_ids=None):
base_qs_set = base_qs is not None
@@ -152,8 +151,8 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
),
).filter(
variation_q,
+ Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=channel.identifier),
active=True,
- sales_channels__contains=channel,
quotas__isnull=False,
subevent_disabled=False
).prefetch_related(
@@ -192,7 +191,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
)
)
- items = base_qs.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher, allow_addons=allow_addons).select_related(
+ items = base_qs.using(settings.DATABASE_REPLICA).filter_available(channel=channel.identifier, voucher=voucher, allow_addons=allow_addons).select_related(
'category', 'tax_rule', # for re-grouping
'hidden_if_available',
).prefetch_related(
@@ -244,7 +243,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
items = items.filter(category_id__in=[a for a in filter_categories if a.isdigit()])
display_add_to_cart = False
- quota_cache_key = f'item_quota_cache:{subevent.id if subevent else 0}:{channel}:{bool(require_seat)}'
+ quota_cache_key = f'item_quota_cache:{subevent.id if subevent else 0}:{channel.identifier}:{bool(require_seat)}'
quota_cache = quota_cache or event.cache.get(quota_cache_key) or {}
quota_cache_existed = bool(quota_cache)
@@ -284,7 +283,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
item.available_variations = [v for v in item.available_variations
if v.pk == voucher.variation_id]
- if get_all_sales_channels()[channel].unlimited_items_per_order:
+ if channel.type_instance.unlimited_items_per_order:
max_per_order = sys.maxsize
else:
max_per_order = item.max_per_order or int(event.settings.max_items_per_order)
@@ -527,7 +526,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
r._csp_ignore = True
return r
- if request.sales_channel.identifier not in request.event.sales_channels:
+ if not request.event.all_sales_channels and request.sales_channel.identifier not in (s.identifier for s in request.event.limit_sales_channels.all()):
raise Http404(_('Tickets for this event cannot be purchased on this sales channel.'))
if request.event.has_subevents:
@@ -565,11 +564,12 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
if not self.request.event.has_subevents or self.subevent:
# Fetch all items
items, display_add_to_cart = get_grouped_items(
- self.request.event, self.subevent,
+ self.request.event,
+ subevent=self.subevent,
filter_items=self.request.GET.getlist('item'),
filter_categories=self.request.GET.getlist('category'),
require_seat=None,
- channel=self.request.sales_channel.identifier,
+ channel=self.request.sales_channel,
memberships=(
self.request.customer.usable_memberships(
for_event=self.subevent or self.request.event,
diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py
index d6f35f80ff..2541d4947a 100644
--- a/src/pretix/presale/views/order.py
+++ b/src/pretix/presale/views/order.py
@@ -705,7 +705,7 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
def can_generate_invoice(event, order, ignore_payments=False):
v = (
- order.sales_channel in event.settings.get('invoice_generate_sales_channels')
+ order.sales_channel.identifier in event.settings.get('invoice_generate_sales_channels')
and (
event.settings.get('invoice_generate') in ('user', 'True')
or (
diff --git a/src/pretix/presale/views/organizer.py b/src/pretix/presale/views/organizer.py
index f389dff3fd..2b0ce53876 100644
--- a/src/pretix/presale/views/organizer.py
+++ b/src/pretix/presale/views/organizer.py
@@ -185,7 +185,7 @@ class EventListMixin:
def _get_event_list_queryset(self):
query = Q(is_public=True) & Q(live=True)
qs = self.request.organizer.events.using(settings.DATABASE_REPLICA).filter(query)
- qs = qs.filter(sales_channels__contains=self.request.sales_channel.identifier)
+ qs = qs.filter(Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=self.request.sales_channel.identifier))
qs = qs.annotate(
min_from=Min('subevents__date_from'),
min_to=Min('subevents__date_to'),
@@ -724,13 +724,13 @@ class CalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
ctx['has_before'], ctx['has_after'] = has_before_after(
self.request.organizer.events.filter(
- sales_channels__contains=self.request.sales_channel.identifier
+ Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=self.request.sales_channel.identifier),
),
SubEvent.objects.filter(
+ Q(event__all_sales_channels=True) | Q(event__limit_sales_channels__identifier=self.request.sales_channel.identifier),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
- event__sales_channels__contains=self.request.sales_channel.identifier
),
before,
after,
@@ -749,13 +749,14 @@ class CalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
add_events_for_days(self.request, Event.annotated(self.request.organizer.events, 'web').using(
settings.DATABASE_REPLICA
).filter(
- sales_channels__contains=self.request.sales_channel.identifier
+ Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=self.request.sales_channel.identifier),
), before, after, ebd, timezones)
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
+ Q(event__all_sales_channels=True) |
+ Q(event__limit_sales_channels__identifier=self.request.sales_channel.identifier),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
- event__sales_channels__contains=self.request.sales_channel.identifier
).prefetch_related(
Prefetch(
'event',
@@ -806,13 +807,14 @@ class WeekCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
ctx['has_before'], ctx['has_after'] = has_before_after(
self.request.organizer.events.filter(
- sales_channels__contains=self.request.sales_channel.identifier
+ Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=self.request.sales_channel.identifier),
),
SubEvent.objects.filter(
+ Q(event__all_sales_channels=True) |
+ Q(event__limit_sales_channels__identifier=self.request.sales_channel.identifier),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
- event__sales_channels__contains=self.request.sales_channel.identifier
),
before,
after,
@@ -843,13 +845,14 @@ class WeekCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
add_events_for_days(self.request, Event.annotated(self.request.organizer.events, 'web').using(
settings.DATABASE_REPLICA
).filter(
- sales_channels__contains=self.request.sales_channel.identifier
+ Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=self.request.sales_channel.identifier),
), before, after, ebd, timezones)
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
+ Q(event__all_sales_channels=True) |
+ Q(event__limit_sales_channels__identifier=self.request.sales_channel.identifier),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
- event__sales_channels__contains=self.request.sales_channel.identifier
).prefetch_related(
Prefetch(
'event',
@@ -943,13 +946,14 @@ class DayCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
ctx['has_before'], ctx['has_after'] = has_before_after(
self.request.organizer.events.filter(
- sales_channels__contains=self.request.sales_channel.identifier
+ Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=self.request.sales_channel.identifier),
),
SubEvent.objects.filter(
+ Q(event__all_sales_channels=True) |
+ Q(event__limit_sales_channels__identifier=self.request.sales_channel.identifier),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
- event__sales_channels__contains=self.request.sales_channel.identifier
),
before,
after,
@@ -1193,13 +1197,14 @@ class DayCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
add_events_for_days(self.request, Event.annotated(self.request.organizer.events, 'web').using(
settings.DATABASE_REPLICA
).filter(
- sales_channels__contains=self.request.sales_channel.identifier
+ Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=self.request.sales_channel.identifier),
), before, after, ebd, timezones)
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
+ Q(event__all_sales_channels=True) |
+ Q(event__limit_sales_channels__identifier=self.request.sales_channel.identifier),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
- event__sales_channels__contains=self.request.sales_channel.identifier
).prefetch_related(
Prefetch(
'event',
@@ -1224,10 +1229,10 @@ class OrganizerIcalDownload(OrganizerViewMixin, View):
filter_qs_by_attr(
self.request.organizer.events.filter(
Q(date_from__gt=cutoff) | Q(date_to__gt=cutoff),
+ Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=self.request.sales_channel.identifier),
is_public=True,
live=True,
has_subevents=False,
- sales_channels__contains=self.request.sales_channel.identifier,
),
request
).order_by(
@@ -1244,12 +1249,13 @@ class OrganizerIcalDownload(OrganizerViewMixin, View):
filter_qs_by_attr(
SubEvent.objects.filter(
Q(date_from__gt=cutoff) | Q(date_to__gt=cutoff),
+ Q(event__all_sales_channels=True) |
+ Q(event__limit_sales_channels__identifier=self.request.sales_channel.identifier),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
is_public=True,
active=True,
- event__sales_channels__contains=self.request.sales_channel.identifier
),
request
).prefetch_related(
diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py
index cfd3b842e5..94af5e79e1 100644
--- a/src/pretix/presale/views/widget.py
+++ b/src/pretix/presale/views/widget.py
@@ -274,7 +274,7 @@ class WidgetAPIProductList(EventListMixin, View):
self.request.event,
subevent=self.subevent,
voucher=self.voucher,
- channel=self.request.sales_channel.identifier,
+ channel=self.request.sales_channel,
base_qs=qs,
require_seat=None,
memberships=(
@@ -372,7 +372,7 @@ class WidgetAPIProductList(EventListMixin, View):
'error': gettext('This ticket shop is currently disabled.')
})
- if request.sales_channel.identifier not in request.event.sales_channels:
+ if not request.event.all_sales_channels and request.sales_channel.identifier not in (s.identifier for s in request.event.limit_sales_channels.all()):
return self.response({
'error': gettext('Tickets for this event cannot be purchased on this sales channel.')
})
@@ -546,7 +546,8 @@ class WidgetAPIProductList(EventListMixin, View):
add_subevents_for_days(
filter_qs_by_attr(
self.request.event.subevents_annotated('web').filter(
- event__sales_channels__contains=self.request.sales_channel.identifier
+ Q(event__all_sales_channels=True) |
+ Q(event__limit_sales_channels__identifier=self.request.sales_channel.identifier),
), self.request
),
limit_before, after, ebd, set(), self.request.event,
@@ -558,16 +559,17 @@ class WidgetAPIProductList(EventListMixin, View):
self.request,
filter_qs_by_attr(
Event.annotated(self.request.organizer.events, 'web').filter(
- sales_channels__contains=self.request.sales_channel.identifier
+ Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=self.request.sales_channel.identifier),
), self.request
),
limit_before, after, ebd, timezones
)
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
+ Q(event__all_sales_channels=True) |
+ Q(event__limit_sales_channels__identifier=self.request.sales_channel.identifier),
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
- event__sales_channels__contains=self.request.sales_channel.identifier
).prefetch_related(
'event___settings_objects', 'event__organizer___settings_objects'
)), self.request), limit_before, after, ebd, timezones)
diff --git a/src/pretix/static/pretixcontrol/js/ui/variations.js b/src/pretix/static/pretixcontrol/js/ui/variations.js
index c00994fb89..f15520c2a3 100644
--- a/src/pretix/static/pretixcontrol/js/ui/variations.js
+++ b/src/pretix/static/pretixcontrol/js/ui/variations.js
@@ -45,9 +45,26 @@ $(function () {
$el.find(".variation-error").toggleClass("hidden", !(
$el.find(".alert-danger, .has-error").length
));
- $el.find("input[name$=-sales_channels]").each(function () {
+ $el.find("input[name$=-limit_sales_channels]").each(function () {
+ console.log(
+ $(this).val(),
+ $el.find(".variation-channel-" + $(this).val()),
+ (
+ $(this).closest("[data-formset-form]").find("input[name$=-all_sales_channels]").prop("checked") ||
+ $(this).prop("checked")
+ ), (
+ $("input[name=all_sales_channels]").prop("checked") ||
+ $("input[name=limit_sales_channels][value=" + $(this).val() + "]").prop("checked")
+ )
+ )
$el.find(".variation-channel-" + $(this).val()).toggleClass("variation-icon-hidden", !(
- $(this).prop("checked") && $("input[name=sales_channels][value=" + $(this).val() + "]").prop("checked")
+ (
+ $(this).closest("[data-formset-form]").find("input[name$=-all_sales_channels]").prop("checked") ||
+ $(this).prop("checked")
+ ) && (
+ $("input[name=all_sales_channels]").prop("checked") ||
+ $("input[name=limit_sales_channels][value=" + $(this).val() + "]").prop("checked")
+ )
));
})
}
@@ -57,7 +74,7 @@ $(function () {
update_variation_summary($el);
$(this).on("change dp.change", "input", function () {update_variation_summary($el)});
});
- $("input[name=sales_channels]").on("change", function() {
+ $("input[name=limit_sales_channels] input[name=all_sales_channels]").on("change", function() {
$("#item_variations [data-formset-form]").each(function () {
update_variation_summary($(this));
});
diff --git a/src/pretix/static/pretixcontrol/scss/main.scss b/src/pretix/static/pretixcontrol/scss/main.scss
index a8404a628a..c5edae22cd 100644
--- a/src/pretix/static/pretixcontrol/scss/main.scss
+++ b/src/pretix/static/pretixcontrol/scss/main.scss
@@ -925,6 +925,11 @@ tbody th {
margin-left: -1px;
}
+.fa-like-image {
+ width: 1.28571em;
+ height: auto;
+}
+
@import "../../pretixbase/scss/_rtl.scss";
@import "../../bootstrap/scss/_rtl.scss";
@import "_rtl.scss";
diff --git a/src/tests/api/test_checkin.py b/src/tests/api/test_checkin.py
index 7965e0808b..f3f1f18634 100644
--- a/src/tests/api/test_checkin.py
+++ b/src/tests/api/test_checkin.py
@@ -65,7 +65,8 @@ def order(event, item, other_item, taxrule):
status=Order.STATUS_PAID, secret="k24fiuwvu8kxz3y1",
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc),
- total=46, locale='en'
+ total=46, locale='en',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
InvoiceAddress.objects.create(order=o, company="Sample company", country=Country('NZ'))
op1 = OrderPosition.objects.create(
@@ -250,7 +251,7 @@ def test_list_list(token_client, organizer, event, clist, item, subevent, django
res["limit_products"] = [item.pk]
res["auto_checkin_sales_channels"] = []
- with django_assert_num_queries(11):
+ with django_assert_num_queries(12):
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
assert [res] == resp.data['results']
@@ -335,7 +336,7 @@ def test_list_create(token_client, organizer, event, item, item_on_wrong_event):
assert cl.name == "VIP"
assert cl.limit_products.count() == 1
assert not cl.all_products
- assert "web" in cl.auto_checkin_sales_channels
+ assert cl.auto_checkin_sales_channels.filter(identifier="web").exists()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug),
@@ -381,7 +382,7 @@ def test_list_create_with_subevent(token_client, organizer, event, event3, item,
assert resp.status_code == 201
with scopes_disabled():
cl = CheckinList.objects.get(pk=resp.data['id'])
- assert "web" in cl.auto_checkin_sales_channels
+ assert cl.auto_checkin_sales_channels.filter(identifier="web").exists()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/checkinlists/'.format(organizer.slug, event.slug),
@@ -448,7 +449,7 @@ def test_list_update(token_client, organizer, event, clist):
assert resp.status_code == 200
with scopes_disabled():
cl = CheckinList.objects.get(pk=resp.data['id'])
- assert "web" in cl.auto_checkin_sales_channels
+ assert cl.auto_checkin_sales_channels.filter(identifier="web").exists()
@pytest.mark.django_db
diff --git a/src/tests/api/test_checkinrpc.py b/src/tests/api/test_checkinrpc.py
index f1ea62c37c..b23795aa67 100644
--- a/src/tests/api/test_checkinrpc.py
+++ b/src/tests/api/test_checkinrpc.py
@@ -67,6 +67,7 @@ def order(event, item, other_item, taxrule):
status=Order.STATUS_PAID, secret="k24fiuwvu8kxz3y1",
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=46, locale='en'
)
InvoiceAddress.objects.create(order=o, company="Sample company", country=Country('NZ'))
@@ -114,6 +115,7 @@ def order2(event2, item_on_event2):
status=Order.STATUS_PAID, secret="ylptCPNOxTyA",
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc),
+ sales_channel=event2.organizer.sales_channels.get(identifier="web"),
total=46, locale='en'
)
InvoiceAddress.objects.create(order=o, company="Sample company", country=Country('NZ'))
diff --git a/src/tests/api/test_discounts.py b/src/tests/api/test_discounts.py
index 355ae2e6a5..e5347a33ac 100644
--- a/src/tests/api/test_discounts.py
+++ b/src/tests/api/test_discounts.py
@@ -41,7 +41,9 @@ TEST_DISCOUNT_RES = {
"active": True,
"internal_name": "3 for 2",
"position": 1,
- "sales_channels": ["web"],
+ "all_sales_channels": True,
+ "limit_sales_channels": [],
+ "sales_channels": ["bar", "baz", "web"],
"available_from": None,
"available_until": None,
"subevent_mode": "mixed",
diff --git a/src/tests/api/test_events.py b/src/tests/api/test_events.py
index 35255c0291..82b8ab3b4e 100644
--- a/src/tests/api/test_events.py
+++ b/src/tests/api/test_events.py
@@ -70,6 +70,7 @@ def order(event, item, taxrule):
status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
datetime=datetime(2017, 12, 1, 10, 0, 0, tzinfo=timezone.utc),
expires=datetime(2017, 12, 10, 10, 0, 0, tzinfo=timezone.utc),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=23, locale='en'
)
o.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('19.00'),
@@ -121,7 +122,9 @@ TEST_EVENT_RES = {
'item_meta_properties': {
'day': 'Monday',
},
- 'sales_channels': ['web', 'bar', 'baz'],
+ 'sales_channels': ['bar', 'baz', 'web'],
+ 'all_sales_channels': True,
+ 'limit_sales_channels': [],
'public_url': 'http://example.com/dummy/dummy/'
}
diff --git a/src/tests/api/test_giftcards.py b/src/tests/api/test_giftcards.py
index 58c389bc15..2df4c922f1 100644
--- a/src/tests/api/test_giftcards.py
+++ b/src/tests/api/test_giftcards.py
@@ -103,6 +103,7 @@ def test_giftcard_detail_expand(token_client, organizer, event, giftcard):
o = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
@@ -196,6 +197,7 @@ def test_giftcard_patch_owner_by_id(token_client, organizer, event, giftcard):
o = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
@@ -220,6 +222,7 @@ def test_giftcard_patch_owner_by_secret(token_client, organizer, event, giftcard
o = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
diff --git a/src/tests/api/test_idempotency.py b/src/tests/api/test_idempotency.py
index e5602599e5..884df388c0 100644
--- a/src/tests/api/test_idempotency.py
+++ b/src/tests/api/test_idempotency.py
@@ -160,6 +160,7 @@ def order(event):
status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=23, locale='en'
)
diff --git a/src/tests/api/test_invoices.py b/src/tests/api/test_invoices.py
index 0cf7f8d81e..4be83047ac 100644
--- a/src/tests/api/test_invoices.py
+++ b/src/tests/api/test_invoices.py
@@ -85,6 +85,7 @@ def order(event, item, taxrule, question):
status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=23, locale='en'
)
p1 = o.payments.create(
@@ -148,6 +149,7 @@ def order2(event2, item2):
status=Order.STATUS_PENDING, secret="asd436cvbfd1",
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc),
+ sales_channel=event2.organizer.sales_channels.get(identifier="web"),
total=23, locale='en'
)
o.payments.create(
diff --git a/src/tests/api/test_items.py b/src/tests/api/test_items.py
index 88340751d5..a0a606a851 100644
--- a/src/tests/api/test_items.py
+++ b/src/tests/api/test_items.py
@@ -44,7 +44,6 @@ from django_countries.fields import Country
from django_scopes import scopes_disabled
from tests.const import SAMPLE_PNG
-from pretix.base.channels import get_all_sales_channels
from pretix.base.models import (
CartPosition, InvoiceAddress, Item, ItemAddOn, ItemBundle, ItemCategory,
ItemVariation, Order, OrderPosition, Question, QuestionOption, Quota,
@@ -81,6 +80,7 @@ def order(event, item, taxrule):
status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
datetime=datetime(2017, 12, 1, 10, 0, 0, tzinfo=timezone.utc),
expires=datetime(2017, 12, 10, 10, 0, 0, tzinfo=timezone.utc),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=23, locale='en'
)
o.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('19.00'),
@@ -254,7 +254,9 @@ TEST_ITEM_RES = {
"name": {"en": "Budget Ticket"},
"internal_name": None,
"default_price": "23.00",
- "sales_channels": ["web"],
+ "sales_channels": ["bar", "baz", "web"],
+ "all_sales_channels": True,
+ "limit_sales_channels": [],
"category": None,
"active": True,
"description": None,
@@ -383,32 +385,34 @@ def test_item_detail(token_client, organizer, event, team, item):
def test_item_detail_variations(token_client, organizer, event, team, item):
with scopes_disabled():
var = item.variations.create(value="Children")
- res = dict(TEST_ITEM_RES)
- res["id"] = item.pk
- res["variations"] = [{
- "id": var.pk,
- "value": {"en": "Children"},
- "default_price": None,
- "free_price_suggestion": None,
- "price": "23.00",
- "active": True,
- "description": None,
- "position": 0,
- "checkin_attention": False,
- "checkin_text": None,
- "require_approval": False,
- "require_membership": False,
- "require_membership_hidden": False,
- "require_membership_types": [],
- "sales_channels": list(get_all_sales_channels().keys()),
- "available_from": None,
- "available_until": None,
- "available_from_mode": "hide",
- "available_until_mode": "hide",
- "hide_without_voucher": False,
- "original_price": None,
- "meta_data": {}
- }]
+ res = dict(TEST_ITEM_RES)
+ res["id"] = item.pk
+ res["variations"] = [{
+ "id": var.pk,
+ "value": {"en": "Children"},
+ "default_price": None,
+ "free_price_suggestion": None,
+ "price": "23.00",
+ "active": True,
+ "description": None,
+ "position": 0,
+ "checkin_attention": False,
+ "checkin_text": None,
+ "require_approval": False,
+ "require_membership": False,
+ "require_membership_hidden": False,
+ "require_membership_types": [],
+ "sales_channels": sorted(organizer.sales_channels.values_list("identifier", flat=True)),
+ "all_sales_channels": True,
+ "limit_sales_channels": [],
+ "available_from": None,
+ "available_until": None,
+ "available_from_mode": "hide",
+ "available_until_mode": "hide",
+ "hide_without_voucher": False,
+ "original_price": None,
+ "meta_data": {}
+ }]
res["has_variations"] = True
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug,
item.pk))
@@ -466,7 +470,7 @@ def test_item_create(token_client, organizer, event, item, category, taxrule, me
"en": "Ticket"
},
"active": True,
- "sales_channels": ["web", "pretixpos"],
+ "sales_channels": ["web", "bar"],
"description": None,
"default_price": "23.00",
"free_price": False,
@@ -496,7 +500,8 @@ def test_item_create(token_client, organizer, event, item, category, taxrule, me
assert resp.status_code == 201
with scopes_disabled():
i = Item.objects.get(pk=resp.data['id'])
- assert i.sales_channels == ["web", "pretixpos"]
+ assert not i.all_sales_channels
+ assert sorted(i.limit_sales_channels.values_list("identifier", flat=True)) == ["bar", "web"]
assert i.meta_data == {'day': 'Wednesday'}
assert i.require_membership_types.count() == 1
assert i.personalized is True # auto-set for backwards-compatibility
@@ -596,7 +601,8 @@ def test_item_create_with_variation(token_client, organizer, event, item, catego
assert new_item.variations.first().value.localize('de') == "Kommentar"
assert new_item.variations.first().value.localize('en') == "Comment"
assert new_item.variations.first().require_approval is True
- assert set(new_item.variations.first().sales_channels) == set(get_all_sales_channels().keys())
+ assert new_item.variations.first().all_sales_channels is True
+ assert not new_item.variations.first().limit_sales_channels.exists()
assert new_item.variations.first().meta_data == {"day": "Wednesday"}
@@ -1192,7 +1198,7 @@ def test_item_file_upload(token_client, organizer, event, item):
"en": "Ticket"
},
"active": True,
- "sales_channels": ["web", "pretixpos"],
+ "sales_channels": ["web"],
"picture": file_id_png,
"description": None,
"default_price": "23.00",
@@ -1331,7 +1337,8 @@ TEST_VARIATIONS_RES = {
"require_membership": False,
"require_membership_hidden": False,
"require_membership_types": [],
- "sales_channels": list(get_all_sales_channels().keys()),
+ "all_sales_channels": True,
+ "limit_sales_channels": [],
"available_from": None,
"available_until": None,
"available_from_mode": "hide",
@@ -1357,6 +1364,8 @@ TEST_VARIATIONS_UPDATE = {
"require_membership_hidden": False,
"require_membership_types": [],
"sales_channels": ["web"],
+ "all_sales_channels": False,
+ "limit_sales_channels": ["web"],
"available_from": None,
"available_until": None,
"available_from_mode": "hide",
@@ -1372,6 +1381,8 @@ TEST_VARIATIONS_UPDATE = {
def test_variations_list(token_client, organizer, event, item, variation):
res = dict(TEST_VARIATIONS_RES)
res["id"] = variation.pk
+ with scopes_disabled():
+ res["sales_channels"] = sorted(organizer.sales_channels.values_list("identifier", flat=True))
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/variations/'.format(organizer.slug, event.slug, item.pk))
assert resp.status_code == 200
assert res['value'] == resp.data['results'][0]['value']
@@ -1383,6 +1394,8 @@ def test_variations_list(token_client, organizer, event, item, variation):
def test_variations_detail(token_client, organizer, event, item, variation):
res = dict(TEST_VARIATIONS_RES)
res["id"] = variation.pk
+ with scopes_disabled():
+ res["sales_channels"] = sorted(organizer.sales_channels.values_list("identifier", flat=True))
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/variations/{}/'.format(organizer.slug, event.slug, item.pk, variation.pk))
assert resp.status_code == 200
assert res == resp.data
@@ -1413,7 +1426,9 @@ def test_variations_create(token_client, organizer, event, item, variation):
var = ItemVariation.objects.get(pk=resp.data['id'])
assert var.position == 1
assert var.price == 23.0
- assert set(var.sales_channels) == set(get_all_sales_channels().keys())
+ assert var.all_sales_channels
+ with scopes_disabled():
+ assert not var.limit_sales_channels.exists()
assert var.meta_data == {"day": "Wednesday"}
diff --git a/src/tests/api/test_order_change.py b/src/tests/api/test_order_change.py
index 2f85d91b19..964b18ef3e 100644
--- a/src/tests/api/test_order_change.py
+++ b/src/tests/api/test_order_change.py
@@ -100,7 +100,8 @@ def order(event, item, taxrule, question):
status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc),
- total=23, locale='en'
+ total=23, locale='en',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
p1 = o.payments.create(
provider='stripe',
diff --git a/src/tests/api/test_order_create.py b/src/tests/api/test_order_create.py
index 3e53f7a19a..daab41cd54 100644
--- a/src/tests/api/test_order_create.py
+++ b/src/tests/api/test_order_create.py
@@ -112,6 +112,7 @@ def order(event, item, taxrule, question):
status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=23, locale='en'
)
p1 = o.payments.create(
@@ -166,7 +167,8 @@ def order(event, item, taxrule, question):
@pytest.fixture
def clist_autocheckin(event):
- c = event.checkin_lists.create(name="Default", all_products=True, auto_checkin_sales_channels=['web'])
+ c = event.checkin_lists.create(name="Default", all_products=True)
+ c.auto_checkin_sales_channels.add(event.organizer.sales_channels.get(identifier="web"))
return c
@@ -245,7 +247,7 @@ def test_order_create(token_client, organizer, event, item, quota, question):
assert o.locale == "en"
assert o.total == Decimal('23.25')
assert o.status == Order.STATUS_PENDING
- assert o.sales_channel == "web"
+ assert o.sales_channel.identifier == "web"
assert o.valid_if_pending
assert o.expires > now()
assert not o.testmode
@@ -550,11 +552,10 @@ def test_order_create_autocheckin(token_client, organizer, event, item, quota, q
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
- assert "web" in clist_autocheckin.auto_checkin_sales_channels
+ assert clist_autocheckin.auto_checkin_sales_channels.contains(organizer.sales_channels.get(identifier="web"))
assert o.positions.first().checkins.first().auto_checked_in
- clist_autocheckin.auto_checkin_sales_channels = []
- clist_autocheckin.save()
+ clist_autocheckin.auto_checkin_sales_channels.clear()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
@@ -564,7 +565,7 @@ def test_order_create_autocheckin(token_client, organizer, event, item, quota, q
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
- assert clist_autocheckin.auto_checkin_sales_channels == []
+ assert clist_autocheckin.auto_checkin_sales_channels.count() == 0
assert o.positions.first().checkins.count() == 0
@@ -625,7 +626,7 @@ def test_order_create_sales_channel_optional(token_client, organizer, event, ite
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
- assert o.sales_channel == "web"
+ assert o.sales_channel.identifier == "web"
@pytest.mark.django_db
@@ -640,7 +641,7 @@ def test_order_create_sales_channel_invalid(token_client, organizer, event, item
), format='json', data=res
)
assert resp.status_code == 400
- assert resp.data == {'sales_channel': ['Unknown sales channel.']}
+ assert resp.data == {'sales_channel': ['Object with identifier=foo does not exist.']}
@pytest.mark.django_db
diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py
index 78d9d23180..0ed8ee28f6 100644
--- a/src/tests/api/test_orders.py
+++ b/src/tests/api/test_orders.py
@@ -88,6 +88,7 @@ def order(event, item, taxrule, question):
status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=23, locale='en'
)
p1 = o.payments.create(
@@ -151,6 +152,7 @@ def order2(event2, item2):
status=Order.STATUS_PENDING, secret="asd436cvbfd1",
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc),
+ sales_channel=event2.organizer.sales_channels.get(identifier="web"),
total=23, locale='en'
)
o.payments.create(
@@ -173,7 +175,8 @@ def order2(event2, item2):
@pytest.fixture
def clist_autocheckin(event):
- c = event.checkin_lists.create(name="Default", all_products=True, auto_checkin_sales_channels=['web'])
+ c = event.checkin_lists.create(name="Default", all_products=True)
+ c.auto_checkin_sales_channels.add(event.organizer.sales_channels.get(identifier="web"))
return c
diff --git a/src/tests/api/test_permissions.py b/src/tests/api/test_permissions.py
index 3b5cf49449..a96054c206 100644
--- a/src/tests/api/test_permissions.py
+++ b/src/tests/api/test_permissions.py
@@ -215,6 +215,12 @@ org_permission_sub_urls = [
('patch', 'can_manage_customers', 'memberships/1/', 404),
('put', 'can_manage_customers', 'memberships/1/', 404),
('delete', 'can_manage_customers', 'memberships/1/', 404),
+ ('get', 'can_change_organizer_settings', 'saleschannels/', 200),
+ ('post', 'can_change_organizer_settings', 'saleschannels/', 400),
+ ('get', 'can_change_organizer_settings', 'saleschannels/web/', 200),
+ ('patch', 'can_change_organizer_settings', 'saleschannels/web/', 200),
+ ('put', 'can_change_organizer_settings', 'saleschannels/api.1/', 404),
+ ('delete', 'can_change_organizer_settings', 'saleschannels/api.1/', 404),
('get', 'can_change_organizer_settings', 'membershiptypes/', 200),
('post', 'can_change_organizer_settings', 'membershiptypes/', 400),
('get', 'can_change_organizer_settings', 'membershiptypes/1/', 404),
diff --git a/src/tests/api/test_reusable_media.py b/src/tests/api/test_reusable_media.py
index 242dddaf92..0d0f564f97 100644
--- a/src/tests/api/test_reusable_media.py
+++ b/src/tests/api/test_reusable_media.py
@@ -132,6 +132,7 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome
o = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
@@ -409,6 +410,7 @@ def test_medium_lookup_cross_organizer(token_client, organizer, organizer2, org2
o = Order.objects.create(
code='FOO', event=org2_event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=org2_event.organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
ticket = org2_event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
diff --git a/src/tests/api/test_saleschannels.py b/src/tests/api/test_saleschannels.py
new file mode 100644
index 0000000000..09a60004aa
--- /dev/null
+++ b/src/tests/api/test_saleschannels.py
@@ -0,0 +1,176 @@
+#
+# 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 .
+#
+# 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
+# .
+#
+import pytest
+from django_scopes import scopes_disabled
+
+
+@pytest.mark.django_db
+def test_channel_list(token_client, organizer):
+ resp = token_client.get('/api/v1/organizers/{}/saleschannels/'.format(organizer.slug))
+ assert resp.status_code == 200
+ assert resp.data['results'][0]["label"]["en"] == "Online shop"
+ assert resp.data['results'][0]["position"] == 0
+ assert resp.data['results'][0]["identifier"] == "web"
+ assert resp.data['results'][0]["type"] == "web"
+
+
+@pytest.mark.django_db
+def test_channel_detail(token_client, organizer):
+ resp = token_client.get('/api/v1/organizers/{}/saleschannels/web/'.format(organizer.slug))
+ assert resp.status_code == 200
+ assert resp.data["label"]["en"] == "Online shop"
+ assert resp.data["position"] == 0
+ assert resp.data["identifier"] == "web"
+ assert resp.data["type"] == "web"
+
+
+@pytest.mark.django_db
+def test_channel_create(token_client, organizer):
+ resp = token_client.post(
+ '/api/v1/organizers/{}/saleschannels/'.format(organizer.slug),
+ format='json',
+ data={
+ "label": {
+ "en": "API 1"
+ },
+ "type": "api",
+ "identifier": "api.1",
+ "position": 5,
+ }
+ )
+ assert resp.status_code == 201
+ with scopes_disabled():
+ sc = organizer.sales_channels.get(identifier="api.1")
+ assert sc.type == "api"
+ assert sc.identifier == "api.1"
+ assert sc.position == 5
+
+
+@pytest.mark.django_db
+def test_channel_create_invalid(token_client, organizer):
+ resp = token_client.post(
+ '/api/v1/organizers/{}/saleschannels/'.format(organizer.slug),
+ format='json',
+ data={
+ "label": {
+ "en": "API 1"
+ },
+ "type": "foo",
+ "identifier": "api.1",
+ "position": 5,
+ }
+ )
+ assert resp.status_code == 400
+ assert resp.data == {"type": ["You can currently only create channels of type 'api' through the API."]}
+
+ resp = token_client.post(
+ '/api/v1/organizers/{}/saleschannels/'.format(organizer.slug),
+ format='json',
+ data={
+ "label": {
+ "en": "API 1"
+ },
+ "type": "api",
+ "identifier": "foo.1",
+ "position": 5,
+ }
+ )
+ assert resp.status_code == 400
+ assert resp.data == {"identifier": ["Your identifier needs to start with 'api.'."]}
+
+ resp = token_client.post(
+ '/api/v1/organizers/{}/saleschannels/'.format(organizer.slug),
+ format='json',
+ data={
+ "label": {
+ "en": "API 1"
+ },
+ "type": "api",
+ "identifier": "api.Ungültig",
+ "position": 5,
+ }
+ )
+ assert resp.status_code == 400
+ assert resp.data == {"identifier": ["The identifier may only contain letters, numbers, dots, dashes, and underscores."]}
+
+
+@pytest.mark.django_db
+def test_channel_patch(token_client, organizer):
+ resp = token_client.patch(
+ '/api/v1/organizers/{}/saleschannels/web/'.format(organizer.slug),
+ format='json',
+ data={
+ 'label': {
+ "en": "World Wide Web"
+ },
+ 'position': 9000,
+ }
+ )
+ assert resp.status_code == 200
+ with scopes_disabled():
+ assert str(organizer.sales_channels.get(identifier="web").label) == "World Wide Web"
+ assert organizer.sales_channels.get(identifier="web").position == 9000
+
+
+@pytest.mark.django_db
+def test_channel_patch_invalid(token_client, organizer):
+ resp = token_client.patch(
+ '/api/v1/organizers/{}/saleschannels/web/'.format(organizer.slug),
+ format='json',
+ data={
+ 'identifier': 'foobar',
+ }
+ )
+ assert resp.status_code == 400
+ assert resp.data == {"identifier": ["You cannot change the identifier of a sales channel."]}
+
+ resp = token_client.patch(
+ '/api/v1/organizers/{}/saleschannels/web/'.format(organizer.slug),
+ format='json',
+ data={
+ 'type': 'foobar',
+ }
+ )
+ assert resp.status_code == 400
+ assert resp.data == {"type": ["You cannot change the type of a sales channel."]}
+
+
+@pytest.mark.django_db
+def test_channel_delete(token_client, organizer):
+ with scopes_disabled():
+ organizer.sales_channels.create(identifier="api.1", type="api", label="api")
+ resp = token_client.delete(
+ '/api/v1/organizers/{}/saleschannels/api.1/'.format(organizer.slug),
+ )
+ assert resp.status_code == 204
+ with scopes_disabled():
+ assert not organizer.sales_channels.filter(identifier="api.1").exists()
+
+
+@pytest.mark.django_db
+def test_channel_delete_invalid(token_client, organizer):
+ resp = token_client.delete(
+ '/api/v1/organizers/{}/saleschannels/web/'.format(organizer.slug),
+ )
+ assert resp.status_code == 403
+ with scopes_disabled():
+ assert organizer.sales_channels.filter(identifier="web").exists()
diff --git a/src/tests/api/test_shredders.py b/src/tests/api/test_shredders.py
index ca518c95f7..81d456726d 100644
--- a/src/tests/api/test_shredders.py
+++ b/src/tests/api/test_shredders.py
@@ -57,6 +57,7 @@ def order(event, item):
status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
datetime=datetime(2017, 12, 1, 10, 0, 0, tzinfo=timezone.utc),
expires=datetime(2017, 12, 10, 10, 0, 0, tzinfo=timezone.utc),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=23, locale='en'
)
OrderPosition.objects.create(
diff --git a/src/tests/api/test_subevents.py b/src/tests/api/test_subevents.py
index a5244d7a4e..4bc26bc506 100644
--- a/src/tests/api/test_subevents.py
+++ b/src/tests/api/test_subevents.py
@@ -60,6 +60,7 @@ def order(event, item, taxrule):
status=Order.STATUS_PENDING, secret="k24fiuwvu8kxz3y1",
datetime=datetime(2017, 12, 1, 10, 0, 0, tzinfo=timezone.utc),
expires=datetime(2017, 12, 10, 10, 0, 0, tzinfo=timezone.utc),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=23, locale='en'
)
o.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('19.00'),
diff --git a/src/tests/base/test_cancelevent.py b/src/tests/base/test_cancelevent.py
index 6637052c72..d4ef4eea2f 100644
--- a/src/tests/base/test_cancelevent.py
+++ b/src/tests/base/test_cancelevent.py
@@ -47,6 +47,7 @@ class EventCancelTests(TestCase):
code='FOO', event=self.event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
total=Decimal('46.00'),
)
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket',
@@ -495,6 +496,7 @@ class SubEventCancelTests(TestCase):
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal('46.00'),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
)
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket',
default_price=Decimal('23.00'), admission=True)
diff --git a/src/tests/base/test_checkin.py b/src/tests/base/test_checkin.py
index 221dfb43a9..7ce184933b 100644
--- a/src/tests/base/test_checkin.py
+++ b/src/tests/base/test_checkin.py
@@ -68,6 +68,7 @@ def position(event, item):
datetime=now() - timedelta(days=4),
expires=now() - timedelta(hours=4) + timedelta(days=10),
total=Decimal('23.00'),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
return OrderPosition.objects.create(
order=order, item=item, variation=None,
diff --git a/src/tests/base/test_event_clone.py b/src/tests/base/test_event_clone.py
index e09a02c9d1..4099342afc 100644
--- a/src/tests/base/test_event_clone.py
+++ b/src/tests/base/test_event_clone.py
@@ -49,6 +49,7 @@ def test_full_clone_same_organizer():
organizer = Organizer.objects.create(name='Dummy', slug='dummy')
membership_type = organizer.membership_types.create(name="Membership")
plan = SeatingPlan.objects.create(name="Plan", organizer=organizer, layout="{}")
+ sc = organizer.sales_channels.get(identifier="web")
event = Event.objects.create(
organizer=organizer, name='Dummy', slug='dummy',
@@ -57,7 +58,9 @@ def test_full_clone_same_organizer():
date_to=now() + timedelta(hours=1),
testmode=True,
seating_plan=plan,
+ all_sales_channels=False,
)
+ event.limit_sales_channels.add(sc)
item_meta = event.item_meta_properties.create(name="Bla")
tax_rule = event.tax_rules.create(name="VAT", rate=19)
@@ -67,13 +70,16 @@ def test_full_clone_same_organizer():
q2 = event.quotas.create(name="Quota 2", size=0, closed=True)
item1 = event.items.create(category=category, tax_rule=tax_rule, name="Ticket", default_price=23,
- grant_membership_type=membership_type, hidden_if_available=q2)
+ grant_membership_type=membership_type, hidden_if_available=q2,
+ all_sales_channels=False)
+ item1.limit_sales_channels.add(sc)
# todo: test that item pictures are copied, not linked
ItemMetaValue.objects.create(item=item1, property=item_meta, value="Foo")
assert item1.meta_data
item2 = event.items.create(category=category, tax_rule=tax_rule, name="T-shirt", default_price=15,
hidden_if_item_available=item1)
- item2v = item2.variations.create(value="red", default_price=15)
+ item2v = item2.variations.create(value="red", default_price=15, all_sales_channels=False)
+ item2v.limit_sales_channels.add(sc)
item2v.meta_values.create(property=item_meta, value="Bar")
item2.require_membership_types.add(membership_type)
ItemAddOn.objects.create(base_item=item1, addon_category=category)
@@ -117,6 +123,7 @@ def test_full_clone_same_organizer():
],
})
clist.limit_products.add(item1)
+ clist.auto_checkin_sales_channels.add(sc)
copied_event = Event.objects.create(
organizer=organizer, name='Dummy2', slug='dummy2',
@@ -129,6 +136,7 @@ def test_full_clone_same_organizer():
# Verify event properties
assert abs(copied_event.date_admission - (copied_event.date_from - timedelta(hours=1))) < timedelta(minutes=1)
assert copied_event.testmode
+ assert copied_event.limit_sales_channels.get() == sc
# Verify that we actually *copied*, not just moved objects over
assert event.tax_rules.count() == copied_event.tax_rules.count() == 1
@@ -139,6 +147,7 @@ def test_full_clone_same_organizer():
assert event.questions.count() == copied_event.questions.count() == 2
assert event.seat_category_mappings.count() == copied_event.seat_category_mappings.count() == 1
assert event.seats.count() == copied_event.seats.count() == 1
+ assert event.limit_sales_channels.get() == sc
# Verify relationship integrity
copied_q1 = copied_event.quotas.get(name=q1.name)
@@ -148,11 +157,14 @@ def test_full_clone_same_organizer():
copied_item2 = copied_event.items.get(name=item2.name)
assert copied_item1.tax_rule == copied_event.tax_rules.get()
assert copied_item1.category == copied_event.categories.get()
+ assert copied_item1.limit_sales_channels.get() == sc
assert copied_item1.meta_data == item1.meta_data
assert copied_item2.variations.get().meta_data == item2v.meta_data
assert copied_item1.hidden_if_available == copied_q2
assert copied_item1.grant_membership_type == membership_type
assert copied_item2.variations.count() == 1
+ assert copied_item2.variations.get().limit_sales_channels.get() == sc
+ assert copied_item2.require_membership_types.count() == 1
assert copied_item2.require_membership_types.get() == membership_type
assert copied_item1.addons.get().addon_category == copied_event.categories.get()
assert copied_item1.bundles.get().bundled_item == copied_item2
@@ -194,6 +206,7 @@ def test_full_clone_same_organizer():
],
}
assert copied_clist.limit_products.get() == copied_item1
+ assert copied_clist.auto_checkin_sales_channels.get() == sc
# todo: test that the plugin hook is called
# todo: test custom style
@@ -208,6 +221,8 @@ def test_full_clone_cross_organizer_differences():
organizer2 = Organizer.objects.create(name='Dummy2', slug='dummy2')
membership_type = organizer.membership_types.create(name="Membership")
plan = SeatingPlan.objects.create(name="Plan", organizer=organizer, layout="{}")
+ sc = organizer.sales_channels.get(identifier="web")
+ sc2 = organizer2.sales_channels.get(identifier="web")
event = Event.objects.create(
organizer=organizer, name='Dummy', slug='dummy',
@@ -216,13 +231,20 @@ def test_full_clone_cross_organizer_differences():
date_to=now() + timedelta(hours=1),
testmode=True,
seating_plan=plan,
+ all_sales_channels=False,
)
+ event.limit_sales_channels.add(sc)
item1 = event.items.create(name="Ticket", default_price=23,
- grant_membership_type=membership_type)
+ grant_membership_type=membership_type,
+ all_sales_channels=False)
+ item1.limit_sales_channels.add(sc)
item2 = event.items.create(name="T-shirt", default_price=15)
item2.require_membership_types.add(membership_type)
+ clist = event.checkin_lists.create(name="Default")
+ clist.auto_checkin_sales_channels.add(sc)
+
copied_event = Event.objects.create(
organizer=organizer2, name='Dummy2', slug='dummy2',
date_from=datetime.datetime(2022, 4, 15, 9, 0, 0, tzinfo=datetime.timezone.utc),
@@ -234,9 +256,14 @@ def test_full_clone_cross_organizer_differences():
assert organizer2.seating_plans.count() == 1
assert organizer2.seating_plans.get().layout == plan.layout
assert copied_event.seating_plan.organizer == organizer2
+ assert copied_event.limit_sales_channels.get() == sc2
assert event.seating_plan.organizer == organizer
copied_item1 = copied_event.items.get(name=item1.name)
copied_item2 = copied_event.items.get(name=item2.name)
assert copied_item1.grant_membership_type is None
assert copied_item2.require_membership_types.count() == 0
+ assert copied_item1.limit_sales_channels.get() == sc2
+
+ copied_clist = copied_event.checkin_lists.get()
+ assert copied_clist.auto_checkin_sales_channels.get() == sc2
diff --git a/src/tests/base/test_invoices.py b/src/tests/base/test_invoices.py
index 86f0016551..981f24cf73 100644
--- a/src/tests/base/test_invoices.py
+++ b/src/tests/base/test_invoices.py
@@ -66,7 +66,8 @@ def env():
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
- total=0, locale='en'
+ total=0, locale='en',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
tr = event.tax_rules.create(rate=Decimal('19.00'))
o.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('19.00'),
@@ -442,7 +443,8 @@ def test_invoice_numbers(env):
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=0,
- locale='en'
+ locale='en',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
order2.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('0.00'),
tax_value=Decimal('0.00'))
@@ -451,7 +453,8 @@ def test_invoice_numbers(env):
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=0, testmode=True,
- locale='en'
+ locale='en',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
inv1 = generate_invoice(order)
inv2 = generate_invoice(order)
@@ -519,7 +522,8 @@ def test_invoice_number_prefixes(env):
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=0,
- locale='en'
+ locale='en',
+ sales_channel=event2.organizer.sales_channels.get(identifier="web"),
)
order2.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('0.00'),
tax_value=Decimal('0.00'))
diff --git a/src/tests/base/test_memberships.py b/src/tests/base/test_memberships.py
index 0d50c0567a..efe5213190 100644
--- a/src/tests/base/test_memberships.py
+++ b/src/tests/base/test_memberships.py
@@ -366,6 +366,7 @@ def test_validate_membership_max_usages(event, customer, membership, requiring_t
datetime=now() - timedelta(days=3),
expires=now() + timedelta(days=11),
total=Decimal("23"),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
OrderPosition.objects.create(
order=o1,
@@ -446,6 +447,7 @@ def test_validate_membership_parallel(event, customer, membership, subevent, req
datetime=now() - timedelta(days=3),
expires=now() + timedelta(days=11),
total=Decimal("23"),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
OrderPosition.objects.create(
order=o1,
@@ -549,6 +551,7 @@ def test_validate_membership_parallel_validity_dynamic(event, customer, membersh
datetime=now() - timedelta(days=3),
expires=now() + timedelta(days=11),
total=Decimal("23"),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
OrderPosition.objects.create(
order=o1,
@@ -664,6 +667,7 @@ def test_validate_membership_parallel_validity_fixed(event, customer, membership
datetime=now() - timedelta(days=3),
expires=now() + timedelta(days=11),
total=Decimal("23"),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
OrderPosition.objects.create(
order=o1,
@@ -743,6 +747,7 @@ def test_use_membership(event, customer, membership, requiring_ticket):
)
order = _create_order(event, email='dummy@example.org', positions=[cp1],
now_dt=now(),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
payment_requests=[{
"id": "test0",
"provider": "banktransfer",
@@ -790,6 +795,7 @@ def test_grant_when_paid_and_changed(event, customer, granting_ticket):
q.items.add(granting_ticket)
order = _create_order(event, email='dummy@example.org', positions=[cp1],
now_dt=now(),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
payment_requests=[{
"id": "test0",
"provider": "banktransfer",
diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py
index 4e20ddb3c2..5827e897dd 100644
--- a/src/tests/base/test_models.py
+++ b/src/tests/base/test_models.py
@@ -118,6 +118,7 @@ class QuotaTestCase(BaseQuotaTestCase):
self.quota.items.add(self.item1)
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
total=4)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
@@ -131,6 +132,7 @@ class QuotaTestCase(BaseQuotaTestCase):
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
total=4)
OrderPosition.objects.create(order=order, item=self.item2, variation=self.var1, price=2)
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_GONE, 0))
@@ -140,12 +142,14 @@ class QuotaTestCase(BaseQuotaTestCase):
self.quota.items.add(self.item1)
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
total=4)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
order = Order.objects.create(event=self.event, status=Order.STATUS_PENDING,
expires=now() + timedelta(days=3),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
total=4)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_ORDERED, 0))
@@ -168,6 +172,7 @@ class QuotaTestCase(BaseQuotaTestCase):
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
total=4)
OrderPosition.objects.create(order=order, item=self.item2, variation=self.var1, price=2)
@@ -180,6 +185,7 @@ class QuotaTestCase(BaseQuotaTestCase):
self.quota.save()
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
total=4)
op = OrderPosition.objects.create(order=order, item=self.item1, price=2)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 2))
@@ -194,12 +200,14 @@ class QuotaTestCase(BaseQuotaTestCase):
self.quota.save()
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
total=4)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 2))
order = Order.objects.create(event=self.event, status=Order.STATUS_PENDING,
expires=now() + timedelta(days=3),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
total=4)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
@@ -246,6 +254,7 @@ class QuotaTestCase(BaseQuotaTestCase):
self.quota.items.add(self.item1)
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
total=2)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
@@ -589,6 +598,7 @@ class QuotaTestCase(BaseQuotaTestCase):
# Create orders
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
total=6)
OrderPosition.objects.create(order=order, item=self.item1, price=2)
OrderPosition.objects.create(order=order, item=self.item1, price=2, blocked=["foo"])
@@ -610,12 +620,14 @@ class QuotaTestCase(BaseQuotaTestCase):
# Create orders
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
expires=now() + timedelta(days=3),
total=6)
OrderPosition.objects.create(order=order, item=self.item1, subevent=se1, price=2)
OrderPosition.objects.create(order=order, item=self.item1, subevent=se1, price=2)
OrderPosition.objects.create(order=order, item=self.item1, subevent=se2, price=2)
order = Order.objects.create(event=self.event, status=Order.STATUS_PENDING,
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
expires=now() + timedelta(days=3),
total=8)
OrderPosition.objects.create(order=order, item=self.item1, subevent=se1, price=2)
@@ -686,6 +698,7 @@ class CheckinQuotaTestCase(BaseQuotaTestCase):
self.quota.items.add(self.item1)
self.cl = self.event.checkin_lists.create(name="Test", allow_entry_after_exit=False)
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
expires=now() + timedelta(days=3),
total=4)
self.op = OrderPosition.objects.create(order=order, item=self.item1, price=2)
@@ -1071,14 +1084,18 @@ class VoucherTestCase(BaseQuotaTestCase):
order = Order.objects.create(
status=Order.STATUS_PENDING, event=self.event,
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
datetime=now() - timedelta(days=5), expires=now() + timedelta(days=5), total=46,
)
- OrderPosition.objects.create(order=order, item=self.item1, voucher=v, price=Decimal('20.00'),
- voucher_budget_use=Decimal('3.00'))
+ OrderPosition.objects.create(
+ order=order, item=self.item1, voucher=v, price=Decimal('20.00'),
+ voucher_budget_use=Decimal('3.00')
+ )
assert v.budget_used() == Decimal('3.00')
order = Order.objects.create(
status=Order.STATUS_PAID, event=self.event,
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
datetime=now() - timedelta(days=5), expires=now() + timedelta(days=5), total=46,
)
OrderPosition.objects.create(order=order, item=self.item1, voucher=v, price=Decimal('20.00'),
@@ -1095,6 +1112,7 @@ class OrderTestCase(BaseQuotaTestCase):
status=Order.STATUS_PENDING, event=self.event,
datetime=now() - timedelta(days=5),
expires=now() + timedelta(days=5), total=46,
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
)
self.quota.items.add(self.item1)
self.op1 = OrderPosition.objects.create(order=self.order, item=self.item1,
@@ -2079,7 +2097,9 @@ class ItemTest(TestCase):
i = Item.objects.create(
event=self.event, name="Ticket", default_price=23,
active=True, available_until=now() + timedelta(days=1),
+ all_sales_channels=False,
)
+ i.limit_sales_channels.add(self.o.sales_channels.get(identifier="web"))
assert Item.objects.filter_available().exists()
assert not Item.objects.filter_available(channel='foo').exists()
@@ -2300,10 +2320,12 @@ class EventTest(TestCase):
def test_active_quotas_annotation(self):
event = Event.objects.create(
organizer=self.organizer, name='Download', slug='download',
- date_from=now()
+ date_from=now(),
)
q = Quota.objects.create(event=event, name='Quota', size=2)
- item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True)
+ item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True,
+ all_sales_channels=False)
+ item.limit_sales_channels.add(self.organizer.sales_channels.get(identifier="web"))
item2 = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=False)
q.items.add(item)
q.items.add(item2)
@@ -2407,8 +2429,9 @@ class EventTest(TestCase):
)
q = Quota.objects.create(event=event, name='Quota', size=2)
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True)
- v = item.variations.create(value="foo")
+ v = item.variations.create(value="foo", all_sales_channels=False)
item.variations.create(value="bar")
+ v.limit_sales_channels.add(self.organizer.sales_channels.get(identifier="web"))
q.items.add(item)
q.variations.add(v)
assert Event.annotated(Event.objects).first().active_quotas == [q]
@@ -2475,7 +2498,9 @@ class SubEventTest(TestCase):
def test_active_quotas_annotation(self):
q = Quota.objects.create(event=self.event, name='Quota', size=2,
subevent=self.se)
- item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True)
+ item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True,
+ all_sales_channels=False)
+ item.limit_sales_channels.add(self.organizer.sales_channels.get(identifier="web"))
q.items.add(item)
assert SubEvent.annotated(SubEvent.objects).first().active_quotas == [q]
assert SubEvent.annotated(SubEvent.objects, 'foo').first().active_quotas == []
@@ -2497,6 +2522,7 @@ class SubEventTest(TestCase):
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True)
o = Order.objects.create(
code='FOO', event=self.event, email='dummy@dummy.test',
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal("30"), locale='en'
@@ -2594,6 +2620,7 @@ class CheckinListTestCase(TestCase):
self.cl_tickets.limit_products.add(self.item1)
o = Order.objects.create(
code='FOO1', event=self.event, email='dummy@dummy.test',
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal("30"), locale='en'
@@ -2621,6 +2648,7 @@ class CheckinListTestCase(TestCase):
o = Order.objects.create(
code='FOO2', event=self.event, email='dummy@dummy.test',
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal("30"), locale='en'
@@ -2687,6 +2715,7 @@ class SeatingTestCase(TestCase):
def test_blocked_in_proximity(self):
o = Order.objects.create(
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
locale='en', status=Order.STATUS_PENDING, datetime=now(),
expires=now() + timedelta(days=10),
)
@@ -2709,6 +2738,7 @@ class SeatingTestCase(TestCase):
def test_order_pending(self):
o = Order.objects.create(
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
locale='en', status=Order.STATUS_PENDING, datetime=now(),
expires=now() + timedelta(days=10),
)
@@ -2723,6 +2753,7 @@ class SeatingTestCase(TestCase):
def test_order_paid(self):
o = Order.objects.create(
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
locale='en', status=Order.STATUS_PAID, datetime=now(),
expires=now() + timedelta(days=10),
)
@@ -2737,6 +2768,7 @@ class SeatingTestCase(TestCase):
def test_order_expired(self):
o = Order.objects.create(
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
locale='en', status=Order.STATUS_EXPIRED, datetime=now(),
expires=now() + timedelta(days=10),
)
@@ -2772,6 +2804,7 @@ class SeatingTestCase(TestCase):
self.seat_a1.save()
o = Order.objects.create(
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
locale='en', status=Order.STATUS_PAID, datetime=now(),
expires=now() + timedelta(days=10),
)
@@ -2789,6 +2822,7 @@ class SeatingTestCase(TestCase):
self.seat_a1.save()
o = Order.objects.create(
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
locale='en', status=Order.STATUS_CANCELED, datetime=now(),
expires=now() + timedelta(days=10),
)
@@ -2982,9 +3016,15 @@ def test_subevent_date_updates_order_date():
se1 = event.subevents.create(date_from=now(), name="SE 1")
se2 = event.subevents.create(date_from=now(), name="SE 2")
- order1 = Order.objects.create(event=event, status=Order.STATUS_PAID, expires=now() + timedelta(days=3), total=6)
+ order1 = Order.objects.create(
+ event=event, status=Order.STATUS_PAID, expires=now() + timedelta(days=3), total=6,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
+ )
OrderPosition.objects.create(order=order1, item=item1, subevent=se1, price=2)
- order2 = Order.objects.create(event=event, status=Order.STATUS_PAID, expires=now() + timedelta(days=3), total=6)
+ order2 = Order.objects.create(
+ event=event, status=Order.STATUS_PAID, expires=now() + timedelta(days=3), total=6,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
+ )
OrderPosition.objects.create(order=order2, item=item1, subevent=se2, price=2)
o1lm = order1.last_modified
diff --git a/src/tests/base/test_notifications.py b/src/tests/base/test_notifications.py
index b54ff3071c..79727d8679 100644
--- a/src/tests/base/test_notifications.py
+++ b/src/tests/base/test_notifications.py
@@ -50,6 +50,7 @@ def order(event):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=Decimal('46.00'),
)
tr19 = event.tax_rules.create(rate=Decimal('19.00'))
diff --git a/src/tests/base/test_orders.py b/src/tests/base/test_orders.py
index e28495cd06..ad474182bd 100644
--- a/src/tests/base/test_orders.py
+++ b/src/tests/base/test_orders.py
@@ -73,7 +73,8 @@ def event():
@pytest.fixture
def clist_autocheckin(event):
- c = event.checkin_lists.create(name="Default", all_products=True, auto_checkin_sales_channels=['web'])
+ c = event.checkin_lists.create(name="Default", all_products=True)
+ c.auto_checkin_sales_channels.add(event.organizer.sales_channels.get(identifier="web"))
return c
@@ -83,6 +84,7 @@ def test_expiry_days(event):
event.settings.set('payment_term_days', 5)
event.settings.set('payment_term_weekdays', False)
order = _create_order(event, email='dummy@example.org', positions=[],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=today,
payment_requests=[{
"id": "test0",
@@ -104,6 +106,7 @@ def test_expiry_weekdays(event):
event.settings.set('payment_term_weekdays', True)
order = _create_order(event, email='dummy@example.org', positions=[],
now_dt=today,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
payment_requests=[{
"id": "test0",
"provider": "free",
@@ -120,6 +123,7 @@ def test_expiry_weekdays(event):
today = make_aware(datetime(2016, 9, 19, 15, 0, 0, 0))
order = _create_order(event, email='dummy@example.org', positions=[],
now_dt=today,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
payment_requests=[{
"id": "test0",
"provider": "free",
@@ -143,6 +147,7 @@ def test_expiry_minutes(event):
event.settings.set('payment_term_weekdays', False)
order = _create_order(event, email='dummy@example.org', positions=[],
now_dt=today,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
payment_requests=[{
"id": "test0",
"provider": "free",
@@ -164,6 +169,7 @@ def test_expiry_last(event):
event.settings.set('payment_term_weekdays', False)
event.settings.set('payment_term_last', now() + timedelta(days=3))
order = _create_order(event, email='dummy@example.org', positions=[],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=today,
payment_requests=[{
"id": "test0",
@@ -178,6 +184,7 @@ def test_expiry_last(event):
assert (order.expires - today).days == 3
event.settings.set('payment_term_last', now() + timedelta(days=7))
order = _create_order(event, email='dummy@example.org', positions=[],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=today,
payment_requests=[{
"id": "test0",
@@ -204,6 +211,7 @@ def test_expiry_last_relative(event):
))
order = _create_order(event, email='dummy@example.org', positions=[],
now_dt=today,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
payment_requests=[{
"id": "test0",
"provider": "free",
@@ -244,6 +252,7 @@ def test_expiry_last_relative_subevents(event):
))
order = _create_order(event, email='dummy@example.org', positions=[cp1, cp2],
now_dt=today,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
payment_requests=[{
"id": "test0",
"provider": "free",
@@ -264,6 +273,7 @@ def test_expiry_dst(event):
utc = ZoneInfo('UTC')
today = datetime(2016, 10, 29, 12, 0, 0, tzinfo=tz).astimezone(utc)
order = _create_order(event, email='dummy@example.org', positions=[],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=today,
payment_requests=[{
"id": "test0",
@@ -286,12 +296,14 @@ def test_expiring(event):
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() + timedelta(days=10),
total=0,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
o2 = Order.objects.create(
code='FO2', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() - timedelta(days=10),
total=12,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
ticket = Item.objects.create(event=event, name='Early-bird ticket',
default_price=Decimal('23.00'), admission=True)
@@ -325,6 +337,7 @@ def test_expiring_paid_invoice(event):
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() - timedelta(days=10),
total=12,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
ticket = Item.objects.create(event=event, name='Early-bird ticket',
default_price=Decimal('12.00'), admission=True)
@@ -358,6 +371,7 @@ def test_expire_twice(event):
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() - timedelta(days=10),
total=12,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
generate_invoice(o2)
expire_orders(None)
@@ -380,6 +394,7 @@ def test_expire_skipped_if_canceled_with_fee(event):
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() - timedelta(days=10),
total=12,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
o2.fees.create(fee_type=OrderFee.FEE_TYPE_CANCELLATION, value=12)
generate_invoice(o2)
@@ -396,12 +411,14 @@ def test_expiring_auto_disabled(event):
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=0,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
o2 = Order.objects.create(
code='FO2', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() - timedelta(days=10),
total=0,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
expire_orders(None)
o1 = Order.objects.get(id=o1.id)
@@ -422,6 +439,7 @@ def test_expiring_auto_delayed(event):
datetime=datetime(2023, 6, 22, 12, 13, 14, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin")),
expires=datetime(2023, 6, 30, 23, 59, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin")),
total=0,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
o2 = Order.objects.create(
code='FO2', event=event, email='dummy@dummy.test',
@@ -429,6 +447,7 @@ def test_expiring_auto_delayed(event):
datetime=datetime(2023, 6, 22, 12, 13, 14, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin")),
expires=datetime(2023, 6, 28, 23, 59, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin")),
total=0,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
assert o1.payment_term_expire_date == o1.expires + timedelta(days=2) # limited by term_last
assert o2.payment_term_expire_date == o2.expires + timedelta(days=3)
@@ -472,6 +491,7 @@ def test_expiring_auto_delayed_weekdays(event):
datetime=datetime(2023, 6, 22, 12, 13, 14, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin")),
expires=datetime(2023, 6, 30, 23, 59, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin")),
total=0,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
assert o1.payment_term_expire_date == o1.expires + timedelta(days=3)
@@ -482,13 +502,15 @@ def test_do_not_expire_if_approval_pending(event):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() - timedelta(days=10),
- total=0, require_approval=True
+ total=0, require_approval=True,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
o2 = Order.objects.create(
code='FO2', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() - timedelta(days=10),
total=0,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
expire_orders(None)
o1 = Order.objects.get(id=o1.id)
@@ -505,7 +527,8 @@ def test_approve(event):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() - timedelta(days=10),
- total=10, require_approval=True, locale='en'
+ total=10, require_approval=True, locale='en',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
ticket = Item.objects.create(event=event, name='Early-bird ticket',
default_price=Decimal('23.00'), admission=True)
@@ -536,7 +559,8 @@ def test_approve_send_to_attendees(event):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() - timedelta(days=10),
- total=10, require_approval=True, locale='en'
+ total=10, require_approval=True, locale='en',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
ticket = Item.objects.create(event=event, name='Early-bird ticket',
default_price=Decimal('23.00'), admission=True)
@@ -564,7 +588,8 @@ def test_approve_free(event):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() - timedelta(days=10),
- total=0, require_approval=True
+ total=0, require_approval=True,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
approve_order(o1)
o1.refresh_from_db()
@@ -586,7 +611,8 @@ def test_approve_free_send_to_attendees(event):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() - timedelta(days=10),
- total=0, require_approval=True
+ total=0, require_approval=True,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
ticket = Item.objects.create(event=event, name='Free ticket',
default_price=Decimal('0.00'), admission=True)
@@ -617,7 +643,8 @@ def test_approve_free_after_last_payment_date(event):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() - timedelta(days=10),
- total=0, require_approval=True
+ total=0, require_approval=True,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
approve_order(o1)
o1.refresh_from_db()
@@ -636,7 +663,8 @@ def test_deny(event):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() - timedelta(days=10),
- total=10, require_approval=True, locale='en'
+ total=10, require_approval=True, locale='en',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
generate_invoice(o1)
deny_order(o1)
@@ -665,6 +693,7 @@ class PaymentReminderTests(TestCase):
datetime=now() - timedelta(hours=4),
expires=now().replace(hour=12, minute=0, second=0) + timedelta(days=10),
total=Decimal('46.00'),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
)
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket',
default_price=Decimal('23.00'), admission=True)
@@ -788,6 +817,7 @@ class PaymentFailedTests(TestCase):
datetime=now() - timedelta(hours=4),
expires=now() - timedelta(hours=4) + timedelta(days=10),
total=Decimal('46.00'),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
)
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket',
default_price=Decimal('23.00'), admission=True)
@@ -829,6 +859,7 @@ class DownloadReminderTests(TestCase):
datetime=now() - timedelta(days=4),
expires=now() - timedelta(hours=4) + timedelta(days=10),
total=Decimal('46.00'),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
)
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket',
default_price=Decimal('23.00'), admission=True)
@@ -1021,6 +1052,7 @@ class OrderCancelTests(TestCase):
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal('46.00'),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
)
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket',
default_price=Decimal('23.00'), admission=True)
@@ -1227,6 +1259,7 @@ class OrderChangeManagerTests(TestCase):
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal('46.00'),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
)
self.order.payments.create(
provider='banktransfer', state=OrderPayment.PAYMENT_STATE_CREATED, amount=self.order.total
@@ -3405,6 +3438,7 @@ def test_autocheckin(clist_autocheckin, event):
)
order = _create_order(event, email='dummy@example.org', positions=[cp1],
now_dt=today,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
payment_requests=[{
"id": "test0",
"provider": "free",
@@ -3415,17 +3449,17 @@ def test_autocheckin(clist_autocheckin, event):
"pprov": FreeOrderProvider(event),
}],
locale='de')[0]
- assert "web" in clist_autocheckin.auto_checkin_sales_channels
+ assert clist_autocheckin.auto_checkin_sales_channels.contains(event.organizer.sales_channels.get(identifier="web"))
assert order.positions.first().checkins.first().auto_checked_in
- clist_autocheckin.auto_checkin_sales_channels = []
- clist_autocheckin.save()
+ clist_autocheckin.auto_checkin_sales_channels.clear()
cp1 = CartPosition.objects.create(
item=ticket, price=23, expires=now() + timedelta(days=1), event=event, cart_id="123"
)
order = _create_order(event, email='dummy@example.org', positions=[cp1],
now_dt=today,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
payment_requests=[{
"id": "test0",
"provider": "free",
@@ -3436,7 +3470,7 @@ def test_autocheckin(clist_autocheckin, event):
"pprov": FreeOrderProvider(event),
}],
locale='de')[0]
- assert clist_autocheckin.auto_checkin_sales_channels == []
+ assert clist_autocheckin.auto_checkin_sales_channels.count() == 0
assert order.positions.first().checkins.count() == 0
@@ -3451,6 +3485,7 @@ def test_saleschannel_testmode_restriction(event):
)
order = _create_order(event, email='dummy@example.org', positions=[cp1],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=today,
payment_requests=[{
"id": "test0",
@@ -3461,7 +3496,7 @@ def test_saleschannel_testmode_restriction(event):
"info_data": {},
"pprov": FreeOrderProvider(event),
}],
- locale='de', sales_channel='web')[0]
+ locale='de')[0]
assert not order.testmode
cp1 = CartPosition.objects.create(
@@ -3469,6 +3504,7 @@ def test_saleschannel_testmode_restriction(event):
)
order = _create_order(event, email='dummy@example.org', positions=[cp1],
now_dt=today,
+ sales_channel=event.organizer.sales_channels.get(identifier=FoobazSalesChannel.identifier),
payment_requests=[{
"id": "test0",
"provider": "free",
@@ -3478,7 +3514,7 @@ def test_saleschannel_testmode_restriction(event):
"info_data": {},
"pprov": FreeOrderProvider(event),
}],
- locale='de', sales_channel=FoobazSalesChannel.identifier)[0]
+ locale='de')[0]
assert not order.testmode
cp1 = CartPosition.objects.create(
@@ -3487,6 +3523,7 @@ def test_saleschannel_testmode_restriction(event):
event.testmode = True
order = _create_order(event, email='dummy@example.org', positions=[cp1],
now_dt=today,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
payment_requests=[{
"id": "test0",
"provider": "free",
@@ -3496,7 +3533,7 @@ def test_saleschannel_testmode_restriction(event):
"info_data": {},
"pprov": FreeOrderProvider(event),
}],
- locale='de', sales_channel='web')[0]
+ locale='de')[0]
assert order.testmode
cp1 = CartPosition.objects.create(
@@ -3504,6 +3541,7 @@ def test_saleschannel_testmode_restriction(event):
)
order = _create_order(event, email='dummy@example.org', positions=[cp1],
now_dt=today,
+ sales_channel=event.organizer.sales_channels.get(identifier=FoobazSalesChannel.identifier),
payment_requests=[{
"id": "test0",
"provider": "free",
@@ -3513,7 +3551,7 @@ def test_saleschannel_testmode_restriction(event):
"info_data": {},
"pprov": FreeOrderProvider(event),
}],
- locale='de', sales_channel=FoobazSalesChannel.identifier)[0]
+ locale='de')[0]
assert not order.testmode
@@ -3530,6 +3568,7 @@ def test_giftcard_multiple(event):
gc2.transactions.create(value=12, acceptor=event.organizer)
order = _create_order(
event, email='dummy@example.org', positions=[cp1],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=now(),
payment_requests=[
{
@@ -3580,6 +3619,7 @@ def test_giftcard_partial(event):
gc1.transactions.create(value=12, acceptor=event.organizer)
order = _create_order(
event, email='dummy@example.org', positions=[cp1],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=now(),
payment_requests=[
{
@@ -3627,6 +3667,7 @@ def test_giftcard_payment_fee(event):
gc1.transactions.create(value=12, acceptor=event.organizer)
order = _create_order(
event, email='dummy@example.org', positions=[cp1],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=now(),
payment_requests=[
{
@@ -3673,6 +3714,7 @@ def test_giftcard_invalid_currency(event):
gc1.transactions.create(value=12, acceptor=event.organizer)
_create_order(
event, email='dummy@example.org', positions=[cp1],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=now(),
payment_requests=[
{
@@ -3715,6 +3757,7 @@ def test_giftcard_invalid_organizer(event):
gc1.transactions.create(value=12, acceptor=event.organizer)
_create_order(
event, email='dummy@example.org', positions=[cp1],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=now(),
payment_requests=[
{
@@ -3756,6 +3799,7 @@ def test_giftcard_test_mode_invalid(event):
gc1.transactions.create(value=12, acceptor=event.organizer)
_create_order(
event, email='dummy@example.org', positions=[cp1],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=now(),
payment_requests=[
{
@@ -3799,6 +3843,7 @@ def test_giftcard_test_mode_event(event):
gc1.transactions.create(value=12, acceptor=event.organizer)
_create_order(
event, email='dummy@example.org', positions=[cp1],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=now(),
payment_requests=[
{
@@ -3840,6 +3885,7 @@ def test_giftcard_swap(event):
gc1.transactions.create(value=12, acceptor=event.organizer)
_create_order(
event, email='dummy@example.org', positions=[cp1],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=now(),
payment_requests=[
{
@@ -3881,6 +3927,7 @@ def test_issue_when_paid_and_changed(event):
q.items.add(ticket)
order = _create_order(
event, email='dummy@example.org', positions=[cp1],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=now(),
payment_requests=[
{
@@ -3930,6 +3977,7 @@ class OrderReactivateTest(TestCase):
datetime=now(), expires=now() + timedelta(days=1),
cancellation_date=now(),
total=Decimal('46.00'),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
)
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket',
default_price=Decimal('23.00'), admission=True)
@@ -4066,6 +4114,7 @@ def test_autocreate_medium(event):
q.items.add(ticket)
order = _create_order(
event, email='dummy@example.org', positions=[cp1],
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
now_dt=now(),
payment_requests=[
{
diff --git a/src/tests/base/test_payment.py b/src/tests/base/test_payment.py
index e2ecaf2c7a..0bc7ab2200 100644
--- a/src/tests/base/test_payment.py
+++ b/src/tests/base/test_payment.py
@@ -220,6 +220,7 @@ def test_availability_date_order_relative_subevents(event):
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
total=Decimal('46.00'),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
OrderPosition.objects.create(
order=order, item=ticket, variation=None, subevent=se1,
diff --git a/src/tests/base/test_pricing_discount.py b/src/tests/base/test_pricing_discount.py
index ca8fecde7f..a7aa5e5d60 100644
--- a/src/tests/base/test_pricing_discount.py
+++ b/src/tests/base/test_pricing_discount.py
@@ -998,17 +998,20 @@ def test_limit_products_subevents_distinct(event, item, item2):
@pytest.mark.django_db
@scopes_disabled()
def test_sales_channels(event, item):
- d1 = Discount(event=event, condition_min_count=2, benefit_discount_matching_percent=20, sales_channels=['resellers'])
+ d1 = Discount(event=event, condition_min_count=2, benefit_discount_matching_percent=20, all_sales_channels=False)
d1.save()
- d2 = Discount(event=event, condition_min_count=2, benefit_discount_matching_percent=50, sales_channels=['web', 'resellers'])
+ d1.limit_sales_channels.add(event.organizer.sales_channels.get(identifier="bar"))
+ d2 = Discount(event=event, condition_min_count=2, benefit_discount_matching_percent=50, all_sales_channels=False)
d2.save()
+ d2.limit_sales_channels.add(event.organizer.sales_channels.get(identifier="web"))
+ d2.limit_sales_channels.add(event.organizer.sales_channels.get(identifier="bar"))
positions = (
(item.pk, None, Decimal('100.00'), False, False, Decimal('0.00')),
(item.pk, None, Decimal('100.00'), False, False, Decimal('0.00')),
)
- assert sorted([p for p, d in apply_discounts(event, 'resellers', positions)]) == [Decimal('80.00'), Decimal('80.00')]
+ assert sorted([p for p, d in apply_discounts(event, 'bar', positions)]) == [Decimal('80.00'), Decimal('80.00')]
assert sorted([p for p, d in apply_discounts(event, 'web', positions)]) == [Decimal('50.00'), Decimal('50.00')]
diff --git a/src/tests/base/test_shredders.py b/src/tests/base/test_shredders.py
index b2f3a10f0c..d8c9cb22d9 100644
--- a/src/tests/base/test_shredders.py
+++ b/src/tests/base/test_shredders.py
@@ -69,6 +69,7 @@ def order(event, item):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
event.settings.set('attendee_names_asked', True)
diff --git a/src/tests/base/test_webhooks.py b/src/tests/base/test_webhooks.py
index ee8796d437..5affcecf88 100644
--- a/src/tests/base/test_webhooks.py
+++ b/src/tests/base/test_webhooks.py
@@ -65,6 +65,7 @@ def order(event):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=Decimal('46.00'),
)
tr19 = event.tax_rules.create(rate=Decimal('19.00'))
diff --git a/src/tests/concurrency_tests/test_order_paid_locking.py b/src/tests/concurrency_tests/test_order_paid_locking.py
index 18c5b41a5f..c4853d4a01 100644
--- a/src/tests/concurrency_tests/test_order_paid_locking.py
+++ b/src/tests/concurrency_tests/test_order_paid_locking.py
@@ -43,6 +43,7 @@ def order1_expired(event, organizer, item):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_EXPIRED, locale='en',
datetime=now(), expires=now() - timedelta(days=10),
+ sales_channel=organizer.sales_channels.get(identifier="web"),
total=Decimal('0.00'),
)
OrderPosition.objects.create(
@@ -62,6 +63,7 @@ def order2_expired(event, organizer, item, customer):
code='BAR', event=event, email='dummy@dummy.test',
status=Order.STATUS_EXPIRED, locale='en',
datetime=now(), expires=now() - timedelta(days=10),
+ sales_channel=organizer.sales_channels.get(identifier="web"),
total=Decimal('0.00'),
)
OrderPosition.objects.create(
diff --git a/src/tests/control/test_checkins.py b/src/tests/control/test_checkins.py
index 61151341c0..d58aa910e7 100644
--- a/src/tests/control/test_checkins.py
+++ b/src/tests/control/test_checkins.py
@@ -74,7 +74,8 @@ def dashboard_env():
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
- total=33, locale='en'
+ total=33, locale='en',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
OrderPosition.objects.create(
order=order_paid,
@@ -108,7 +109,8 @@ def test_dashboard_pending_not_count(dashboard_env):
code='BAR', event=dashboard_env[0], email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
- total=23, locale='en'
+ total=23, locale='en',
+ sales_channel=dashboard_env[0].organizer.sales_channels.get(identifier="web"),
)
OrderPosition.objects.create(
order=order_pending,
@@ -161,25 +163,29 @@ def checkin_list_env():
code='PENDING', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
- total=23, locale='en'
+ total=23, locale='en',
+ sales_channel=orga.sales_channels.get(identifier="web"),
)
order_a1 = Order.objects.create(
code='A1', event=event, email='a1dummy@dummy.test',
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
- total=33, locale='en'
+ total=33, locale='en',
+ sales_channel=orga.sales_channels.get(identifier="web"),
)
order_a2 = Order.objects.create(
code='A2', event=event, email='a2dummy@dummy.test',
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
- total=23, locale='en'
+ total=23, locale='en',
+ sales_channel=orga.sales_channels.get(identifier="web"),
)
order_a3 = Order.objects.create(
code='A3', event=event, email='a3dummy@dummy.test',
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
- total=23, locale='en'
+ total=23, locale='en',
+ sales_channel=orga.sales_channels.get(identifier="web"),
)
# order position
@@ -381,19 +387,22 @@ def checkin_list_with_addon_env():
code='PENDING', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
- total=23, locale='en'
+ total=23, locale='en',
+ sales_channel=orga.sales_channels.get(identifier="web"),
)
order_a1 = Order.objects.create(
code='A1', event=event, email='a1dummy@dummy.test',
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
- total=33, locale='en'
+ total=33, locale='en',
+ sales_channel=orga.sales_channels.get(identifier="web"),
)
order_a2 = Order.objects.create(
code='A2', event=event, email='a2dummy@dummy.test',
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
- total=23, locale='en'
+ total=23, locale='en',
+ sales_channel=orga.sales_channels.get(identifier="web"),
)
# order position
diff --git a/src/tests/control/test_customer.py b/src/tests/control/test_customer.py
index 52b9febedd..1d660986b7 100644
--- a/src/tests/control/test_customer.py
+++ b/src/tests/control/test_customer.py
@@ -68,6 +68,7 @@ def order(event, customer):
email='admin@localhost',
datetime=now() - timedelta(days=3),
expires=now() + timedelta(days=11),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
total=Decimal("23"),
)
OrderPosition.objects.create(
diff --git a/src/tests/control/test_events.py b/src/tests/control/test_events.py
index dac406cde4..3e90fe1aca 100644
--- a/src/tests/control/test_events.py
+++ b/src/tests/control/test_events.py
@@ -322,13 +322,15 @@ class EventsTest(SoupTest):
code='FOO', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
- total=14, locale='en', testmode=True
+ total=14, locale='en', testmode=True,
+ sales_channel=self.event1.organizer.sales_channels.get(identifier="web"),
)
o2 = Order.objects.create(
code='FOO2', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
- total=14, locale='en'
+ total=14, locale='en',
+ sales_channel=self.event1.organizer.sales_channels.get(identifier="web"),
)
self.event1.testmode = True
self.event1.save()
@@ -346,13 +348,15 @@ class EventsTest(SoupTest):
code='FOO', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
- total=14, locale='en', testmode=True
+ total=14, locale='en', testmode=True,
+ sales_channel=self.event1.organizer.sales_channels.get(identifier="web"),
)
o2 = Order.objects.create(
code='FOO2', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
- total=14, locale='en'
+ total=14, locale='en',
+ sales_channel=self.event1.organizer.sales_channels.get(identifier="web"),
)
self.event1.testmode = True
self.event1.save()
@@ -1267,12 +1271,14 @@ class EventDeletionTest(SoupTest):
assert self.orga1.events.exists()
def test_delete_orders(self):
- Order.objects.create(
- code='FOO', event=self.event1, email='dummy@dummy.test',
- status=Order.STATUS_PENDING,
- datetime=now(), expires=now(),
- total=14, locale='en'
- )
+ with scopes_disabled():
+ Order.objects.create(
+ code='FOO', event=self.event1, email='dummy@dummy.test',
+ status=Order.STATUS_PENDING,
+ datetime=now(), expires=now(),
+ total=14, locale='en',
+ sales_channel=self.event1.organizer.sales_channels.get(identifier="web"),
+ )
self.post_doc('/control/event/ccc/30c3/delete/', {
'user_pw': 'dummy',
'slug': '30c3'
diff --git a/src/tests/control/test_giftcards.py b/src/tests/control/test_giftcards.py
index e598f118aa..8488d80d57 100644
--- a/src/tests/control/test_giftcards.py
+++ b/src/tests/control/test_giftcards.py
@@ -133,7 +133,8 @@ def test_card_detail_view_transact_revert_refund(organizer, admin_user, gift_car
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_CANCELED,
datetime=now(), expires=now() + timedelta(days=10),
- total=14, locale='en'
+ total=14, locale='en',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
o.payments.create(
amount=o.total, provider='banktransfer', state=OrderPayment.PAYMENT_STATE_CONFIRMED
diff --git a/src/tests/control/test_items.py b/src/tests/control/test_items.py
index b790a2866c..56c7bfac4b 100644
--- a/src/tests/control/test_items.py
+++ b/src/tests/control/test_items.py
@@ -225,6 +225,7 @@ class QuestionsTest(ItemFormTest):
o = Order.objects.create(code='FOO', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING, datetime=now(),
expires=now() + datetime.timedelta(days=10),
+ sales_channel=self.event1.organizer.sales_channels.get(identifier="web"),
total=14, locale='en')
op = OrderPosition.objects.create(order=o, item=item1, variation=None, price=Decimal("14"),
attendee_name_parts={'full_name': "Peter"})
@@ -634,6 +635,7 @@ class ItemsTest(ItemFormTest):
code='FOO', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
+ sales_channel=self.event1.organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
OrderPosition.objects.create(
@@ -679,7 +681,7 @@ class ItemsTest(ItemFormTest):
assert i_new.require_voucher == i_old.require_voucher
assert i_new.hide_without_voucher == i_old.hide_without_voucher
assert i_new.allow_cancel == i_old.allow_cancel
- assert i_new.sales_channels == i_old.sales_channels
+ assert set(i_new.limit_sales_channels.all()) == set(i_old.limit_sales_channels.all())
assert i_new.meta_data == i_old.meta_data == {"Foo": "Bar"}
assert set(i_new.questions.all()) == set(i_old.questions.all())
assert set([str(v.value) for v in i_new.variations.all()]) == set([str(v.value) for v in i_old.variations.all()])
diff --git a/src/tests/control/test_orders.py b/src/tests/control/test_orders.py
index 25c15c6c7e..59ca91f3d4 100644
--- a/src/tests/control/test_orders.py
+++ b/src/tests/control/test_orders.py
@@ -74,7 +74,8 @@ def env():
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
- total=14, locale='en'
+ total=14, locale='en',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
o.payments.create(
amount=o.total, provider='banktransfer', state=OrderPayment.PAYMENT_STATE_PENDING
@@ -873,6 +874,7 @@ def test_order_extend_expired_seat_taken(client, env):
code='BAR', event=env[0], email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=env[0].organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
OrderPosition.objects.create(
@@ -1103,6 +1105,7 @@ def test_order_mark_paid_expired_seat_taken(client, env):
code='BAR', event=env[0], email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=env[0].organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
OrderPosition.objects.create(
@@ -1263,6 +1266,7 @@ class OrderChangeTests(SoupTest):
code='FOO', event=self.event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=self.event.organizer.sales_channels.get(identifier="web"),
total=Decimal('46.00'),
)
self.tr7 = self.event.tax_rules.create(rate=Decimal('7.00'))
@@ -2216,6 +2220,7 @@ def test_refund_paid_order_offsetting_to_wrong_currency(client, env):
code='BAZ', event=event2, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=event2.organizer.sales_channels.get(identifier="web"),
total=5, locale='en'
)
o.positions.create(price=5, item=ticket2)
@@ -2244,6 +2249,7 @@ def test_refund_paid_order_offsetting(client, env):
code='BAZ', event=env[0], email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=env[0].organizer.sales_channels.get(identifier="web"),
total=5, locale='en'
)
@@ -2284,6 +2290,7 @@ def test_refund_prevent_duplicate_submit(client, env):
code='BAZ', event=env[0], email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=env[0].organizer.sales_channels.get(identifier="web"),
total=5, locale='en'
)
env[2].refunds.create(provider="manual", amount=Decimal("2.00"), state=OrderRefund.REFUND_STATE_CREATED)
diff --git a/src/tests/control/test_orders_bulk.py b/src/tests/control/test_orders_bulk.py
index eb64aad78b..6bbd135054 100644
--- a/src/tests/control/test_orders_bulk.py
+++ b/src/tests/control/test_orders_bulk.py
@@ -73,6 +73,7 @@ def order1(env):
code='FOO', event=env[0], email='foo@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=env[0].organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
o.payments.create(
@@ -94,6 +95,7 @@ def order2(env):
code='BAR', event=env[0], email='bar@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=env[0].organizer.sales_channels.get(identifier="web"),
total=14, locale='en'
)
o.payments.create(
diff --git a/src/tests/control/test_organizer.py b/src/tests/control/test_organizer.py
index f9f36800b4..4604ebee44 100644
--- a/src/tests/control/test_organizer.py
+++ b/src/tests/control/test_organizer.py
@@ -323,3 +323,60 @@ class OrganizerTest(SoupTest):
p = self.orga1.sso_providers.get()
assert p.configuration['scope'] == 'openid email'
assert p.configuration['provider_config'] == conf
+
+ def test_sales_channel_add_edit_remove(self):
+ doc = self.post_doc(
+ '/control/organizer/%s/channel/add?type=api' % self.orga1.slug,
+ {
+ 'label_0': 'API 1',
+ 'identifier': 'custom',
+ },
+ follow=True
+ )
+ assert not doc.select('.has-error, .alert-danger')
+ with scopes_disabled():
+ assert str(self.orga1.sales_channels.get(identifier="api.custom").label) == "API 1"
+
+ doc = self.post_doc(
+ '/control/organizer/%s/channel/api.custom/edit' % self.orga1.slug,
+ {
+ 'label_0': 'API 2',
+ },
+ follow=True
+ )
+ assert not doc.select('.has-error, .alert-danger')
+ with scopes_disabled():
+ assert str(self.orga1.sales_channels.get(identifier="api.custom").label) == "API 2"
+
+ doc = self.post_doc(
+ '/control/organizer/%s/channel/api.custom/delete' % self.orga1.slug,
+ {},
+ follow=True
+ )
+ assert not doc.select('.has-error, .alert-danger')
+ with scopes_disabled():
+ assert not self.orga1.sales_channels.filter(identifier="api.custom").exists()
+
+ def test_sales_channel_add_invalid_type(self):
+ doc = self.post_doc(
+ '/control/organizer/%s/channel/add?type=web' % self.orga1.slug,
+ {
+ 'label_0': 'API 1',
+ 'identifier': 'custom',
+ },
+ follow=True
+ )
+ assert doc.select('.large-link-group')
+
+ def test_sales_channel_delete_invalid(self):
+ doc = self.post_doc(
+ '/control/organizer/%s/channel/web/delete' % self.orga1.slug,
+ {
+ 'label_0': 'API 1',
+ 'identifier': 'custom',
+ },
+ follow=True
+ )
+ assert doc.select('.alert-danger')
+ with scopes_disabled():
+ assert self.orga1.sales_channels.filter(identifier="web").exists()
diff --git a/src/tests/control/test_permissions.py b/src/tests/control/test_permissions.py
index 913b63c543..cc7b373df9 100644
--- a/src/tests/control/test_permissions.py
+++ b/src/tests/control/test_permissions.py
@@ -55,6 +55,7 @@ def env():
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=0,
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
Team.objects.create(pk=1, organizer=o)
return event, user, o
@@ -536,6 +537,10 @@ organizer_permission_urls = [
("can_change_organizer_settings", "organizer/dummy/property/add", 200),
("can_change_organizer_settings", "organizer/dummy/property/1/edit", 404),
("can_change_organizer_settings", "organizer/dummy/property/1/delete", 404),
+ ("can_change_organizer_settings", "organizer/dummy/channels", 200),
+ ("can_change_organizer_settings", "organizer/dummy/channel/add", 200),
+ ("can_change_organizer_settings", "organizer/dummy/channel/web/edit", 200),
+ ("can_change_organizer_settings", "organizer/dummy/channel/web/delete", 200),
("can_change_organizer_settings", "organizer/dummy/membershiptypes", 200),
("can_change_organizer_settings", "organizer/dummy/membershiptype/add", 200),
("can_change_organizer_settings", "organizer/dummy/membershiptype/1/edit", 404),
diff --git a/src/tests/control/test_reusable_media.py b/src/tests/control/test_reusable_media.py
index 1810188130..9a4d9a12b4 100644
--- a/src/tests/control/test_reusable_media.py
+++ b/src/tests/control/test_reusable_media.py
@@ -112,7 +112,8 @@ def test_typeahead(organizer, admin_user, client, gift_card):
o = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
- total=14, locale='en'
+ total=14, locale='en',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
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"))
diff --git a/src/tests/control/test_search.py b/src/tests/control/test_search.py
index 4eef41de8a..9bcc33a99c 100644
--- a/src/tests/control/test_search.py
+++ b/src/tests/control/test_search.py
@@ -52,7 +52,8 @@ class OrderSearchTest(SoupTest):
code='FO1A', event=self.event1, email='dummy1@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
- total=14, locale='en'
+ total=14, locale='en',
+ sales_channel=self.event1.organizer.sales_channels.get(identifier="web"),
)
InvoiceAddress.objects.create(order=o1, company="Test Ltd.", name_parts={'full_name': "Peter Miller", "_scheme": "full"})
ticket1 = Item.objects.create(event=self.event1, name='Early-bird ticket',
@@ -71,7 +72,8 @@ class OrderSearchTest(SoupTest):
code='FO2', event=self.event2, email='dummy2@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
- total=14, locale='en'
+ total=14, locale='en',
+ sales_channel=self.event2.organizer.sales_channels.get(identifier="web"),
)
ticket2 = Item.objects.create(event=self.event1, name='Early-bird ticket',
category=None, default_price=23,
@@ -192,7 +194,8 @@ class PaymentSearchTest(SoupTest):
code='FO1A', event=self.event1, email='dummy1@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
- total=14, locale='en'
+ total=14, locale='en',
+ sales_channel=self.event1.organizer.sales_channels.get(identifier="web"),
)
InvoiceAddress.objects.create(order=o1, company="Test Ltd.", name_parts={'full_name': "Peter Miller", "_scheme": "full"})
ticket1 = Item.objects.create(event=self.event1, name='Early-bird ticket',
@@ -246,7 +249,8 @@ class PaymentSearchTest(SoupTest):
code='FO2', event=self.event2, email='dummy2@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
- total=15, locale='en'
+ total=15, locale='en',
+ sales_channel=self.event2.organizer.sales_channels.get(identifier="web"),
)
ticket2 = Item.objects.create(event=self.event1, name='Early-bird ticket',
category=None, default_price=23,
diff --git a/src/tests/control/test_shredders.py b/src/tests/control/test_shredders.py
index b41b6114d3..2c03eb9e2c 100644
--- a/src/tests/control/test_shredders.py
+++ b/src/tests/control/test_shredders.py
@@ -66,7 +66,8 @@ class EventShredderTest(SoupTest):
code='FOO', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now(),
- total=14, locale='en'
+ total=14, locale='en',
+ sales_channel=self.orga1.sales_channels.get(identifier="web"),
)
self.client.login(email='dummy@dummy.dummy', password='dummy')
diff --git a/src/tests/control/test_subevents.py b/src/tests/control/test_subevents.py
index 06598d156b..55d41f38b0 100644
--- a/src/tests/control/test_subevents.py
+++ b/src/tests/control/test_subevents.py
@@ -176,7 +176,8 @@ class SubEventsTest(SoupTest):
code='FOO', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
- total=14, locale='en'
+ total=14, locale='en',
+ sales_channel=self.orga1.sales_channels.get(identifier="web"),
)
OrderPosition.objects.create(
order=o,
@@ -755,7 +756,8 @@ class SubEventsTest(SoupTest):
code='FOO', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
- total=14, locale='en'
+ total=14, locale='en',
+ sales_channel=self.orga1.sales_channels.get(identifier="web"),
)
OrderPosition.objects.create(
order=o,
diff --git a/src/tests/control/test_taxrates.py b/src/tests/control/test_taxrates.py
index 8c0805ad2a..49a1905015 100644
--- a/src/tests/control/test_taxrates.py
+++ b/src/tests/control/test_taxrates.py
@@ -116,6 +116,7 @@ class TaxRateFormTest(SoupTest):
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
total=14, locale='en',
+ sales_channel=self.orga1.sales_channels.get(identifier="web"),
)
o.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('19.00'),
tax_value=Decimal('0.05'), tax_rule=tr)
@@ -135,7 +136,8 @@ class TaxRateFormTest(SoupTest):
code='FOO', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
- total=12, locale='en'
+ total=12, locale='en',
+ sales_channel=self.orga1.sales_channels.get(identifier="web"),
)
o.positions.create(
item=i, price=12, tax_rule=tr, tax_rate=19, tax_value=12 - 12 / 1.19
diff --git a/src/tests/control/test_views.py b/src/tests/control/test_views.py
index bfa33f5832..6f06dd104f 100644
--- a/src/tests/control/test_views.py
+++ b/src/tests/control/test_views.py
@@ -70,6 +70,7 @@ def order(item):
o = Order.objects.create(event=item.event, status=Order.STATUS_PENDING,
expires=now() + datetime.timedelta(hours=1),
total=13, code='DUMMY', email='dummy@dummy.test',
+ sales_channel=item.event.organizer.sales_channels.get(identifier="web"),
datetime=now())
OrderPosition.objects.create(order=o, item=item, price=13)
p1 = o.payments.create(
diff --git a/src/tests/control/test_vouchers.py b/src/tests/control/test_vouchers.py
index 8e385d0e94..81682f2f28 100644
--- a/src/tests/control/test_vouchers.py
+++ b/src/tests/control/test_vouchers.py
@@ -744,6 +744,7 @@ class VoucherFormTest(SoupTestMixin, TransactionTestCase):
code='DEDUP', event=self.event, email='dummy@dummy.test',
status=Order.STATUS_PAID,
datetime=now(), expires=now() + datetime.timedelta(days=10),
+ sales_channel=self.orga.sales_channels.get(identifier="web"),
total=0, locale='en'
)
diff --git a/src/tests/plugins/badges/test_pdf.py b/src/tests/plugins/badges/test_pdf.py
index 589f217ebe..57dd2e8630 100644
--- a/src/tests/plugins/badges/test_pdf.py
+++ b/src/tests/plugins/badges/test_pdf.py
@@ -61,6 +61,7 @@ def env():
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal('13.37'),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
shirt = Item.objects.create(event=event, name='T-Shirt', default_price=12)
shirt_red = ItemVariation.objects.create(item=shirt, default_price=14, value="Red")
diff --git a/src/tests/plugins/banktransfer/test_actions.py b/src/tests/plugins/banktransfer/test_actions.py
index 4827348182..78d87d2d18 100644
--- a/src/tests/plugins/banktransfer/test_actions.py
+++ b/src/tests/plugins/banktransfer/test_actions.py
@@ -49,12 +49,14 @@ def env():
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=23,
+ sales_channel=o.sales_channels.get(identifier="web"),
)
o2 = Order.objects.create(
code='6789Z', event=event,
status=Order.STATUS_CANCELED,
datetime=now(), expires=now() + timedelta(days=10),
total=23,
+ sales_channel=o.sales_channels.get(identifier="web"),
)
quota = Quota.objects.create(name="Test", size=2, event=event)
item1 = Item.objects.create(event=event, name="Ticket", default_price=23)
diff --git a/src/tests/plugins/banktransfer/test_api.py b/src/tests/plugins/banktransfer/test_api.py
index 1fe8e6ae1d..8dfde6b0c9 100644
--- a/src/tests/plugins/banktransfer/test_api.py
+++ b/src/tests/plugins/banktransfer/test_api.py
@@ -49,13 +49,15 @@ def env():
code='1Z3AS', event=event,
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
- total=23
+ total=23,
+ sales_channel=o.sales_channels.get(identifier="web"),
)
o2 = Order.objects.create(
code='6789Z', event=event,
status=Order.STATUS_CANCELED,
datetime=now(), expires=now() + timedelta(days=10),
- total=23
+ total=23,
+ sales_channel=o.sales_channels.get(identifier="web"),
)
quota = Quota.objects.create(name="Test", size=2, event=event)
item1 = Item.objects.create(event=event, name="Ticket", default_price=23)
diff --git a/src/tests/plugins/banktransfer/test_import.py b/src/tests/plugins/banktransfer/test_import.py
index 551e1be349..31a502e274 100644
--- a/src/tests/plugins/banktransfer/test_import.py
+++ b/src/tests/plugins/banktransfer/test_import.py
@@ -68,19 +68,22 @@ def env():
code='1Z3AS', event=event, email='admin@localhost',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
- total=23
+ total=23,
+ sales_channel=o.sales_channels.get(identifier="web"),
)
o2 = Order.objects.create(
code='6789Z', event=event,
status=Order.STATUS_CANCELED,
datetime=now(), expires=now() + timedelta(days=10),
- total=23
+ total=23,
+ sales_channel=o.sales_channels.get(identifier="web"),
)
Order.objects.create(
code='GS89Z', event=event,
status=Order.STATUS_CANCELED,
datetime=now(), expires=now() + timedelta(days=10),
- total=23
+ total=23,
+ sales_channel=o.sales_channels.get(identifier="web"),
)
quota = Quota.objects.create(name="Test", size=2, event=event)
item1 = Item.objects.create(event=event, name="Ticket", default_price=23)
@@ -442,12 +445,14 @@ def test_keep_unmatched(env, orga_job):
@pytest.mark.django_db
def test_split_payment_success(env, orga_job):
- o4 = Order.objects.create(
- code='99999', event=env[0],
- status=Order.STATUS_PENDING,
- datetime=now(), expires=now() + timedelta(days=10),
- total=12
- )
+ with scopes_disabled():
+ o4 = Order.objects.create(
+ code='99999', event=env[0],
+ status=Order.STATUS_PENDING,
+ datetime=now(), expires=now() + timedelta(days=10),
+ total=12,
+ sales_channel=env[0].organizer.sales_channels.get(identifier="web"),
+ )
process_banktransfers(orga_job, [{
'payer': 'Karla Kundin',
'reference': 'Bestellungen DUMMY-1Z3AS DUMMY-99999',
@@ -468,12 +473,14 @@ def test_split_payment_success(env, orga_job):
@pytest.mark.django_db
def test_split_payment_mismatch(env, orga_job):
- o4 = Order.objects.create(
- code='99999', event=env[0],
- status=Order.STATUS_PENDING,
- datetime=now(), expires=now() + timedelta(days=10),
- total=12
- )
+ with scopes_disabled():
+ o4 = Order.objects.create(
+ code='99999', event=env[0],
+ status=Order.STATUS_PENDING,
+ datetime=now(), expires=now() + timedelta(days=10),
+ total=12,
+ sales_channel=env[0].organizer.sales_channels.get(identifier="web"),
+ )
process_banktransfers(orga_job, [{
'payer': 'Karla Kundin',
'reference': 'Bestellungen DUMMY-1Z3AS DUMMY-99999',
diff --git a/src/tests/plugins/banktransfer/test_refund.py b/src/tests/plugins/banktransfer/test_refund.py
index a718cc72a8..b895f9dc1c 100644
--- a/src/tests/plugins/banktransfer/test_refund.py
+++ b/src/tests/plugins/banktransfer/test_refund.py
@@ -47,6 +47,7 @@ def env():
code='1Z3AS', event=event, email='admin@localhost',
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
+ sales_channel=o.sales_channels.get(identifier="web"),
total=23
)
payment = OrderPayment.objects.create(
diff --git a/src/tests/plugins/banktransfer/test_refund_export.py b/src/tests/plugins/banktransfer/test_refund_export.py
index 5895ecea60..2cca48ec45 100644
--- a/src/tests/plugins/banktransfer/test_refund_export.py
+++ b/src/tests/plugins/banktransfer/test_refund_export.py
@@ -48,7 +48,8 @@ def env():
code='1Z3AS', event=event, email='admin@localhost',
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
- total=23
+ total=23,
+ sales_channel=o.sales_channels.get(identifier="web"),
)
refund = OrderRefund.objects.create(
order=order,
@@ -74,7 +75,8 @@ def refund_huf(env):
code='HUFFY', event=event, email='admin@localhost',
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
- total=42
+ total=42,
+ sales_channel=env[0].organizer.sales_channels.get(identifier="web"),
)
refund = OrderRefund.objects.create(
order=order,
diff --git a/src/tests/plugins/paypal/test_webhook.py b/src/tests/plugins/paypal/test_webhook.py
index 98558067c3..31c60cb9cd 100644
--- a/src/tests/plugins/paypal/test_webhook.py
+++ b/src/tests/plugins/paypal/test_webhook.py
@@ -49,6 +49,7 @@ def env():
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal('13.37'),
+ sales_channel=o.sales_channels.get(identifier="web"),
)
o1.payments.create(
amount=o1.total,
diff --git a/src/tests/plugins/paypal2/test_webhook.py b/src/tests/plugins/paypal2/test_webhook.py
index 41e78c9baf..390b1c7dd4 100644
--- a/src/tests/plugins/paypal2/test_webhook.py
+++ b/src/tests/plugins/paypal2/test_webhook.py
@@ -50,6 +50,7 @@ def env():
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal('43.59'),
+ sales_channel=o.sales_channels.get(identifier="web"),
)
o1.payments.create(
amount=o1.total,
diff --git a/src/tests/plugins/sendmail/conftest.py b/src/tests/plugins/sendmail/conftest.py
index edd246a11b..6c4d7ef328 100644
--- a/src/tests/plugins/sendmail/conftest.py
+++ b/src/tests/plugins/sendmail/conftest.py
@@ -57,11 +57,12 @@ def checkin_list(event):
@pytest.fixture
-def order(item):
+def order(event, item):
"""Returns an order instance"""
o = Order.objects.create(event=item.event, status=Order.STATUS_PENDING,
expires=now() + datetime.timedelta(hours=1),
total=13, code='DUMMY', email='dummy@dummy.test',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
datetime=now(), locale='en')
return o
diff --git a/src/tests/plugins/sendmail/test_rules.py b/src/tests/plugins/sendmail/test_rules.py
index 45815e263e..bb2900b0aa 100644
--- a/src/tests/plugins/sendmail/test_rules.py
+++ b/src/tests/plugins/sendmail/test_rules.py
@@ -284,12 +284,14 @@ def test_sendmail_rule_all_subevents(event_series, subevent1, subevent2, item):
o1 = Order.objects.create(event=item.event, status=Order.STATUS_PAID,
expires=now() + datetime.timedelta(hours=1),
total=13, code='DUMMY1', email='dummy1@dummy.test',
+ sales_channel=event_series.organizer.sales_channels.get(identifier="web"),
datetime=now(), locale='en')
o1.all_positions.create(item=item, price=13, subevent=subevent1)
o1.all_positions.create(item=item, price=13, subevent=subevent2)
o2 = Order.objects.create(event=item.event, status=Order.STATUS_PAID,
expires=now() + datetime.timedelta(hours=1),
total=13, code='DUMMY2', email='dummy2@dummy.test',
+ sales_channel=event_series.organizer.sales_channels.get(identifier="web"),
datetime=now(), locale='en')
o2.all_positions.create(item=item, price=23, subevent=subevent1)
o2.all_positions.create(item=item, price=23, subevent=subevent2)
diff --git a/src/tests/plugins/sendmail/test_sendmail.py b/src/tests/plugins/sendmail/test_sendmail.py
index ff7ef79c98..fd302582e8 100644
--- a/src/tests/plugins/sendmail/test_sendmail.py
+++ b/src/tests/plugins/sendmail/test_sendmail.py
@@ -173,10 +173,11 @@ def test_sendmail_multi_locales(logged_in_client, sendmail_url, event, item):
event.settings.set('locales', ['en', 'de'])
with scopes_disabled():
- o = Order.objects.create(event=item.event, status=Order.STATUS_PAID,
+ o = Order.objects.create(event=event, status=Order.STATUS_PAID,
expires=now() + datetime.timedelta(hours=1),
total=13, code='DUMMY', email='dummy@dummy.test',
datetime=now(),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
locale='de')
OrderPosition.objects.create(order=o, item=item, price=13)
diff --git a/src/tests/plugins/stripe/test_provider.py b/src/tests/plugins/stripe/test_provider.py
index 5fe0eea5ab..9234709f61 100644
--- a/src/tests/plugins/stripe/test_provider.py
+++ b/src/tests/plugins/stripe/test_provider.py
@@ -60,7 +60,8 @@ def env():
code='FOOBAR', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
- total=Decimal('13.37')
+ total=Decimal('13.37'),
+ sales_channel=o.sales_channels.get(identifier="web"),
)
yield event, o1
diff --git a/src/tests/plugins/stripe/test_webhook.py b/src/tests/plugins/stripe/test_webhook.py
index 3721d0fd8b..40b98480d4 100644
--- a/src/tests/plugins/stripe/test_webhook.py
+++ b/src/tests/plugins/stripe/test_webhook.py
@@ -49,6 +49,7 @@ def env():
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal('13.37'),
+ sales_channel=o.sales_channels.get(identifier="web"),
)
return event, o1
diff --git a/src/tests/plugins/test_checkinlist.py b/src/tests/plugins/test_checkinlist.py
index af98b7f3a0..5e84a617fd 100644
--- a/src/tests/plugins/test_checkinlist.py
+++ b/src/tests/plugins/test_checkinlist.py
@@ -51,7 +51,8 @@ def event():
code='FOO', event=event, email='dummy@dummy.test', phone="+498912345678",
status=Order.STATUS_PAID,
datetime=datetime.datetime(2019, 2, 22, 14, 0, 0, tzinfo=datetime.timezone.utc), expires=now() + datetime.timedelta(days=10),
- total=33, locale='en'
+ total=33, locale='en',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
item_ticket = Item.objects.create(event=event, name="Ticket", default_price=23, admission=True)
OrderPosition.objects.create(
@@ -150,7 +151,8 @@ def test_csv_order_by_inherited_name_parts(event): # noqa
code='BAR', event=event, email='dummy@dummy.test', phone='+498912345678',
status=Order.STATUS_PAID,
datetime=datetime.datetime(2019, 2, 22, 14, 0, 0, tzinfo=datetime.timezone.utc), expires=now() + datetime.timedelta(days=10),
- total=33, locale='en'
+ total=33, locale='en',
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
OrderPosition.objects.create(
order=order2,
diff --git a/src/tests/plugins/ticketoutputpdf/test_api.py b/src/tests/plugins/ticketoutputpdf/test_api.py
index f22f84229d..187678ae51 100644
--- a/src/tests/plugins/ticketoutputpdf/test_api.py
+++ b/src/tests/plugins/ticketoutputpdf/test_api.py
@@ -43,7 +43,7 @@ def env():
t.limit_events.add(event)
item1 = Item.objects.create(event=event, name="Ticket", default_price=23)
tl = event.ticket_layouts.create(name="Foo", default=True, layout='[{"a": 2}]')
- TicketLayoutItem.objects.create(layout=tl, item=item1)
+ TicketLayoutItem.objects.create(layout=tl, item=item1, sales_channel=o.sales_channels.get(identifier="web"))
return event, tl, item1
diff --git a/src/tests/plugins/ticketoutputpdf/test_control.py b/src/tests/plugins/ticketoutputpdf/test_control.py
index 0ee80939b7..c217be8902 100644
--- a/src/tests/plugins/ticketoutputpdf/test_control.py
+++ b/src/tests/plugins/ticketoutputpdf/test_control.py
@@ -134,7 +134,7 @@ class TicketLayoutFormTest(SoupTest):
def test_item_copy(self):
with scopes_disabled():
bl2 = self.event1.ticket_layouts.create(name="Layout 2")
- TicketLayoutItem.objects.create(item=self.item1, layout=bl2)
+ TicketLayoutItem.objects.create(item=self.item1, layout=bl2, sales_channel=self.orga1.sales_channels.get(identifier="web"))
self.client.post('/control/event/%s/%s/items/add' % (self.orga1.slug, self.event1.slug), {
'name_0': 'Intermediate',
'default_price': '23.00',
@@ -150,7 +150,7 @@ class TicketLayoutFormTest(SoupTest):
def test_copy_event(self):
with scopes_disabled():
bl2 = self.event1.ticket_layouts.create(name="Layout 2")
- TicketLayoutItem.objects.create(item=self.item1, layout=bl2)
+ TicketLayoutItem.objects.create(item=self.item1, layout=bl2, sales_channel=self.orga1.sales_channels.get(identifier="web"))
self.post_doc('/control/events/add', {
'event_wizard-current_step': 'foundation',
'event_wizard-prefix': 'event_wizard',
diff --git a/src/tests/plugins/ticketoutputpdf/test_ticketoutputpdf.py b/src/tests/plugins/ticketoutputpdf/test_ticketoutputpdf.py
index 98ca320f44..d4e9a6bc3f 100644
--- a/src/tests/plugins/ticketoutputpdf/test_ticketoutputpdf.py
+++ b/src/tests/plugins/ticketoutputpdf/test_ticketoutputpdf.py
@@ -46,6 +46,7 @@ def env0():
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal('13.37'),
+ sales_channel=o.sales_channels.get(identifier="web"),
)
shirt = Item.objects.create(event=event, name='T-Shirt', default_price=12)
shirt_red = ItemVariation.objects.create(item=shirt, default_price=14, value="Red")
diff --git a/src/tests/presale/test_bundle_prices.py b/src/tests/presale/test_bundle_prices.py
index 0145492352..ca56062e75 100644
--- a/src/tests/presale/test_bundle_prices.py
+++ b/src/tests/presale/test_bundle_prices.py
@@ -47,7 +47,6 @@ class BundlePricesTest(TestCase):
date_from=datetime.datetime(now().year + 1, 12, 26, tzinfo=datetime.timezone.utc),
live=True,
plugins="pretix.plugins.banktransfer",
- sales_channels=['web', 'bar']
)
self.tr19 = self.event.tax_rules.create(rate=Decimal('19.00'))
self.tr7 = self.event.tax_rules.create(rate=Decimal('7.00'))
diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py
index 94839a8100..1ed5f41d27 100644
--- a/src/tests/presale/test_cart.py
+++ b/src/tests/presale/test_cart.py
@@ -71,7 +71,6 @@ class CartTestMixin:
date_from=datetime.datetime(now().year + 1, 12, 26, tzinfo=datetime.timezone.utc),
live=True,
plugins="pretix.plugins.banktransfer",
- sales_channels=['web', 'bar']
)
self.tr19 = self.event.tax_rules.create(rate=Decimal('19.00'))
self.category = ItemCategory.objects.create(event=self.event, name="Everything", position=0)
@@ -914,8 +913,10 @@ class CartTest(CartTestMixin, TestCase):
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists())
def test_wrong_sales_channel(self):
- self.ticket.sales_channels = ['bar']
- self.ticket.save()
+ with scopes_disabled():
+ self.ticket.all_sales_channels = False
+ self.ticket.limit_sales_channels.add(self.orga.sales_channels.get(identifier="bar"))
+ self.ticket.save()
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1',
}, follow=True)
@@ -923,17 +924,22 @@ class CartTest(CartTestMixin, TestCase):
self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 0)
def test_variation_wrong_sales_channel(self):
- self.shirt_blue.sales_channels = ['bar']
- self.shirt_blue.save()
+ with scopes_disabled():
+ self.shirt_blue.all_sales_channels = False
+ self.shirt_blue.limit_sales_channels.add(self.orga.sales_channels.get(identifier="bar"))
+ self.shirt_blue.save()
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'variation_%d_%d' % (self.shirt.id, self.shirt_blue.id): '1',
}, follow=True)
with scopes_disabled():
self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 0)
- self.shirt_blue.sales_channels = ['bar', 'web']
- self.shirt_blue.save()
- self.shirt.sales_channels = ['bar']
- self.shirt.save()
+ self.shirt_blue.all_sales_channels = False
+ self.shirt_blue.limit_sales_channels.add(self.orga.sales_channels.get(identifier="bar"))
+ self.shirt_blue.limit_sales_channels.add(self.orga.sales_channels.get(identifier="web"))
+ self.shirt_blue.save()
+ self.shirt.all_sales_channels = False
+ self.shirt.limit_sales_channels.add(self.orga.sales_channels.get(identifier="bar"))
+ self.shirt.save()
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'variation_%d_%d' % (self.shirt.id, self.shirt_blue.id): '1',
}, follow=True)
@@ -941,11 +947,13 @@ class CartTest(CartTestMixin, TestCase):
self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 0)
def test_other_sales_channel(self):
- self.ticket.sales_channels = ['bar']
- self.ticket.save()
+ with scopes_disabled():
+ self.ticket.all_sales_channels = False
+ self.ticket.limit_sales_channels.add(self.orga.sales_channels.get(identifier="bar"))
+ self.ticket.save()
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1',
- }, follow=True, PRETIX_SALES_CHANNEL=FoobarSalesChannel)
+ }, follow=True, PRETIX_SALES_CHANNEL=FoobarSalesChannel.identifier)
with scopes_disabled():
self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 1)
@@ -1020,13 +1028,13 @@ class CartTest(CartTestMixin, TestCase):
self.event.settings.max_items_per_order = 5
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '5',
- }, follow=True, PRETIX_SALES_CHANNEL=FoobarSalesChannel)
+ }, follow=True, PRETIX_SALES_CHANNEL=FoobarSalesChannel.identifier)
self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),
target_status_code=200)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertNotIn('more than', doc.select('.alert-danger')[0].text)
with scopes_disabled():
- self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 1)
+ self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 5)
def test_max_per_item_variations_failed(self):
self.shirt.max_per_order = 1
@@ -2392,7 +2400,8 @@ class CartAddonTest(CartTestMixin, TestCase):
self.workshopquota.variations.add(self.workshop3a)
self.workshopquota.variations.add(self.workshop3b)
self.addon1 = ItemAddOn.objects.create(base_item=self.ticket, addon_category=self.workshopcat)
- self.cm = CartManager(event=self.event, cart_id=self.session_key)
+ self.cm = CartManager(event=self.event, cart_id=self.session_key,
+ sales_channel=self.orga.sales_channels.get(identifier="web"))
@classscope(attr='orga')
def test_cart_set_simple_addon_included(self):
@@ -2715,7 +2724,7 @@ class CartAddonTest(CartTestMixin, TestCase):
self.addon1.multi_allowed = True
self.addon1.save()
- self.cm = CartManager(event=self.event, cart_id=self.session_key)
+ self.cm = CartManager(event=self.event, cart_id=self.session_key, sales_channel=self.orga.sales_channels.get(identifier="web"))
self.cm.set_addons([
{
'addon_to': cp1.pk,
@@ -2729,7 +2738,7 @@ class CartAddonTest(CartTestMixin, TestCase):
assert cp1.addons.count() == 3
assert all(a.price == Decimal('24.00') for a in cp1.addons.all())
- self.cm = CartManager(event=self.event, cart_id=self.session_key)
+ self.cm = CartManager(event=self.event, cart_id=self.session_key, sales_channel=self.orga.sales_channels.get(identifier="web"))
self.cm.set_addons([
{
'addon_to': cp1.pk,
@@ -2763,7 +2772,7 @@ class CartAddonTest(CartTestMixin, TestCase):
self.cm.commit()
assert cp1.addons.count() == 3
- self.cm = CartManager(event=self.event, cart_id=self.session_key)
+ self.cm = CartManager(event=self.event, cart_id=self.session_key, sales_channel=self.orga.sales_channels.get(identifier="web"))
self.cm.set_addons([
{
'addon_to': cp1.pk,
@@ -2775,7 +2784,7 @@ class CartAddonTest(CartTestMixin, TestCase):
self.cm.commit()
assert cp1.addons.count() == 4
- self.cm = CartManager(event=self.event, cart_id=self.session_key)
+ self.cm = CartManager(event=self.event, cart_id=self.session_key, sales_channel=self.orga.sales_channels.get(identifier="web"))
self.cm.set_addons([
{
'addon_to': cp1.pk,
@@ -3117,7 +3126,7 @@ class CartBundleTest(CartTestMixin, TestCase):
designated_price=1.5,
count=1
)
- self.cm = CartManager(event=self.event, cart_id=self.session_key)
+ self.cm = CartManager(event=self.event, cart_id=self.session_key, sales_channel=self.orga.sales_channels.get(identifier="web"))
@classscope(attr='orga')
def test_simple_bundle(self):
@@ -3537,7 +3546,7 @@ class CartBundleTest(CartTestMixin, TestCase):
b = cp.addons.first()
assert b.item == self.trans
- self.cm = CartManager(event=self.event, cart_id=self.session_key)
+ self.cm = CartManager(event=self.event, cart_id=self.session_key, sales_channel=self.orga.sales_channels.get(identifier="web"))
self.cm.set_addons([
{
'addon_to': cp.pk,
@@ -4093,7 +4102,7 @@ class CartSeatingTest(CartTestMixin, TestCase):
self.seat_a1 = self.event.seats.create(seat_number="A1", product=self.ticket, seat_guid="A1")
self.seat_a2 = self.event.seats.create(seat_number="A2", product=self.ticket, seat_guid="A2")
self.seat_a3 = self.event.seats.create(seat_number="A3", product=self.ticket, seat_guid="A3")
- self.cm = CartManager(event=self.event, cart_id=self.session_key)
+ self.cm = CartManager(event=self.event, cart_id=self.session_key, sales_channel=self.orga.sales_channels.get(identifier="web"))
def test_add_with_seat_without_variation(self):
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py
index 09cd935e9e..ba7ea8ebca 100644
--- a/src/tests/presale/test_checkout.py
+++ b/src/tests/presale/test_checkout.py
@@ -3760,6 +3760,7 @@ class QuestionsTestCase(BaseCheckoutTestCase, TestCase):
o3 = QuestionAnswer.objects.get(question=q3)
order = Order.objects.create(event=self.event, status=Order.STATUS_PAID,
expires=now() + timedelta(days=3),
+ sales_channel=self.orga.sales_channels.get(identifier="web"),
total=4)
op = OrderPosition.objects.create(order=order, item=self.ticket, price=42)
o1.cartposition, o2.cartposition, o3.cartposition = None, None, None
@@ -4454,7 +4455,7 @@ class CheckoutBundleTest(BaseCheckoutTestCase, TestCase):
)
CartPosition.objects.filter(addon_to__isnull=False).delete()
CartPosition.objects.all().delete()
- cm = CartManager(event=self.event, cart_id="temp")
+ cm = CartManager(event=self.event, cart_id="temp", sales_channel=self.orga.sales_channels.get(identifier="web"))
cm.add_new_items([{
'item': self.ticket.pk,
'variation': None,
@@ -4473,7 +4474,7 @@ class CheckoutBundleTest(BaseCheckoutTestCase, TestCase):
cp.cart_id = self.session_key
cp.addon_to = map[cp.addon_to_id]
cp.save()
- cm = CartManager(event=self.event, cart_id=self.session_key)
+ cm = CartManager(event=self.event, cart_id=self.session_key, sales_channel=self.orga.sales_channels.get(identifier="web"))
cm.commit() # execute discounts on resorted cart
self._set_payment()
diff --git a/src/tests/presale/test_customer.py b/src/tests/presale/test_customer.py
index d9ccfade84..085488d110 100644
--- a/src/tests/presale/test_customer.py
+++ b/src/tests/presale/test_customer.py
@@ -466,6 +466,7 @@ def test_org_order_list(env, client):
datetime=now() - datetime.timedelta(days=3),
expires=now() + datetime.timedelta(days=11),
total=Decimal("23"),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
OrderPosition.objects.create(
order=o1,
@@ -481,6 +482,7 @@ def test_org_order_list(env, client):
datetime=now() - datetime.timedelta(days=3),
expires=now() + datetime.timedelta(days=11),
total=Decimal("23"),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
OrderPosition.objects.create(
order=o2,
@@ -497,6 +499,7 @@ def test_org_order_list(env, client):
datetime=now() - datetime.timedelta(days=3),
expires=now() + datetime.timedelta(days=11),
total=Decimal("23"),
+ sales_channel=event.organizer.sales_channels.get(identifier="web"),
)
OrderPosition.objects.create(
order=o3,
diff --git a/src/tests/presale/test_event.py b/src/tests/presale/test_event.py
index 0d81201b9e..4f20b04f6f 100644
--- a/src/tests/presale/test_event.py
+++ b/src/tests/presale/test_event.py
@@ -65,7 +65,7 @@ class EventTestMixin:
self.event = Event.objects.create(
organizer=self.orga, name='30C3', slug='30c3',
date_from=datetime.datetime(now().year + 1, 12, 26, 14, 0, tzinfo=datetime.timezone.utc),
- live=True, sales_channels=['web', 'bar']
+ live=True,
)
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
t = Team.objects.create(organizer=self.orga, can_change_event_settings=True)
@@ -153,11 +153,15 @@ class ItemDisplayTest(EventTestMixin, SoupTest):
with scopes_disabled():
q = Quota.objects.create(event=self.event, name='Quota', size=2)
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True,
- sales_channels=['bar'])
+ all_sales_channels=False)
+ item.limit_sales_channels.add(self.orga.sales_channels.get(identifier="bar"))
q.items.add(item)
html = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug)).rendered_content
self.assertNotIn("Early-bird", html)
- html = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug), PRETIX_SALES_CHANNEL=FoobarSalesChannel).rendered_content
+ html = self.client.get(
+ '/%s/%s/' % (self.orga.slug, self.event.slug),
+ PRETIX_SALES_CHANNEL=FoobarSalesChannel.identifier
+ ).rendered_content
self.assertIn("Early-bird", html)
def test_timely_available(self):
@@ -550,7 +554,8 @@ class ItemDisplayTest(EventTestMixin, SoupTest):
q = Quota.objects.create(event=self.event, name='Quota', size=None)
item = Item.objects.create(event=self.event, name='Early-bird ticket', category=c, default_price=0)
var1 = ItemVariation.objects.create(item=item, value='Red')
- var2 = ItemVariation.objects.create(item=item, value='Blue', sales_channels=['foobar'])
+ var2 = ItemVariation.objects.create(item=item, value='Blue', all_sales_channels=False)
+ var2.limit_sales_channels.add(self.orga.sales_channels.get(identifier='bar'))
q.items.add(item)
q.variations.add(var1)
q.variations.add(var2)
@@ -1278,7 +1283,7 @@ class DeadlineTest(EventTestMixin, TestCase):
def test_saleschannel_disabled(self):
self.event.presale_start = None
self.event.presale_end = None
- self.event.sales_channels = []
+ self.event.all_sales_channels = False
self.event.save()
response = self.client.get(
'/%s/%s/' % (self.orga.slug, self.event.slug)
@@ -1325,6 +1330,7 @@ class TestResendLink(EventTestMixin, SoupTest):
Order.objects.create(
code='DUMMY1', status=Order.STATUS_PENDING, event=self.event,
email='dummy@dummy.dummy', datetime=now(), expires=now(),
+ sales_channel=self.orga.sales_channels.get(identifier="web"),
total=0,
)
mail.outbox = []
@@ -1339,6 +1345,7 @@ class TestResendLink(EventTestMixin, SoupTest):
Order.objects.create(
code='DUMMY1', status=Order.STATUS_PENDING, event=self.event,
email='dummy@dummy.dummy', datetime=now(), expires=now(),
+ sales_channel=self.orga.sales_channels.get(identifier="web"),
total=0,
)
mail.outbox = []
@@ -1354,11 +1361,13 @@ class TestResendLink(EventTestMixin, SoupTest):
Order.objects.create(
code='DUMMY1', status=Order.STATUS_PENDING, event=self.event,
email='dummy@dummy.dummy', datetime=now(), expires=now(),
+ sales_channel=self.orga.sales_channels.get(identifier="web"),
total=0,
)
Order.objects.create(
code='DUMMY2', status=Order.STATUS_PENDING, event=self.event,
email='dummy@dummy.dummy', datetime=now(), expires=now(),
+ sales_channel=self.orga.sales_channels.get(identifier="web"),
total=0,
)
mail.outbox = []
diff --git a/src/tests/presale/test_order_change.py b/src/tests/presale/test_order_change.py
index c6ac22d8d2..da2aa1ce0b 100644
--- a/src/tests/presale/test_order_change.py
+++ b/src/tests/presale/test_order_change.py
@@ -92,6 +92,7 @@ class BaseOrdersTest(TestCase):
datetime=now() - datetime.timedelta(days=3),
expires=now() + datetime.timedelta(days=11),
total=Decimal("23"),
+ sales_channel=self.orga.sales_channels.get(identifier="web"),
locale='en'
)
self.ticket_pos = OrderPosition.objects.create(
@@ -1349,12 +1350,12 @@ class OrderChangeAddonsTest(BaseOrdersTest):
self._assert_ws2a_not_allowed()
def test_forbidden_sales_channel(self):
- self.workshop2.sales_channels = ['pretixpos']
+ self.workshop2.all_sales_channels = False
self.workshop2.save()
self._assert_ws2a_not_allowed()
def test_forbidden_var_sales_channel(self):
- self.workshop2a.sales_channels = ['pretixpos']
+ self.workshop2a.all_sales_channels = False
self.workshop2a.save()
self._assert_ws2a_not_allowed()
diff --git a/src/tests/presale/test_orders.py b/src/tests/presale/test_orders.py
index 198f77a223..f3c608447c 100644
--- a/src/tests/presale/test_orders.py
+++ b/src/tests/presale/test_orders.py
@@ -91,6 +91,7 @@ class BaseOrdersTest(TestCase):
datetime=now() - datetime.timedelta(days=3),
expires=now() + datetime.timedelta(days=11),
total=Decimal("23"),
+ sales_channel=self.orga.sales_channels.get(identifier="web"),
locale='en'
)
self.ticket_pos = OrderPosition.objects.create(
@@ -114,7 +115,8 @@ class BaseOrdersTest(TestCase):
email='user@localhost',
datetime=now() - datetime.timedelta(days=3),
expires=now() + datetime.timedelta(days=11),
- total=Decimal("23")
+ total=Decimal("23"),
+ sales_channel=self.orga.sales_channels.get(identifier="web"),
)
diff --git a/src/tests/presale/test_widget.py b/src/tests/presale/test_widget.py
index 372336457f..8baf7fc1a4 100644
--- a/src/tests/presale/test_widget.py
+++ b/src/tests/presale/test_widget.py
@@ -46,7 +46,8 @@ class WidgetCartTest(CartTestMixin, TestCase):
datetime=now() - datetime.timedelta(days=3),
expires=now() + datetime.timedelta(days=11),
total=Decimal("23"),
- locale='en'
+ locale='en',
+ sales_channel=self.orga.sales_channels.get(identifier="web"),
)
self.ticket_pos = OrderPosition.objects.create(
order=self.order,
@@ -146,7 +147,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[1].text)
def test_saleschannel_disabled(self):
- self.event.sales_channels = []
+ self.event.all_sales_channels = False
self.event.save()
response = self.client.get('/%s/%s/widget/product_list' % (self.orga.slug, self.event.slug))
data = json.loads(response.content.decode())
diff --git a/src/tests/testdummy/signals.py b/src/tests/testdummy/signals.py
index aad1618a59..ccc163dfa0 100644
--- a/src/tests/testdummy/signals.py
+++ b/src/tests/testdummy/signals.py
@@ -21,9 +21,9 @@
#
from django.dispatch import receiver
-from pretix.base.channels import SalesChannel
+from pretix.base.channels import SalesChannelType
from pretix.base.signals import (
- register_payment_providers, register_sales_channels,
+ register_payment_providers, register_sales_channel_types,
register_ticket_outputs,
)
@@ -43,14 +43,14 @@ def register_payment_provider(sender, **kwargs):
return [DummyPaymentProvider, DummyFullRefundablePaymentProvider, DummyPartialRefundablePaymentProvider]
-class FoobazSalesChannel(SalesChannel):
+class FoobazSalesChannel(SalesChannelType):
identifier = "baz"
verbose_name = "Foobar"
icon = "home"
testmode_supported = False
-class FoobarSalesChannel(SalesChannel):
+class FoobarSalesChannel(SalesChannelType):
identifier = "bar"
verbose_name = "Foobar"
icon = "home"
@@ -58,6 +58,6 @@ class FoobarSalesChannel(SalesChannel):
unlimited_items_per_order = True
-@receiver(register_sales_channels, dispatch_uid="sc_dummy")
+@receiver(register_sales_channel_types, dispatch_uid="sc_dummy")
def register_sc(sender, **kwargs):
return [FoobarSalesChannel, FoobazSalesChannel]