Add sales channels (#1103)

- [x] Data model
- [x] Enforce constraint
- [x] Filter order list
- [x] Set channel on created order
- [x] Products API
- [x] Order API
- [x] Tests
- [x] Filter reports
- [x] Resellers
- [ ] deploy plugins
  - [ ] posbackend
  - [ ] resellers
  - [ ] reports
- [x] Ticketlayouts
- [x] Support in pretixPOS
This commit is contained in:
Raphael Michel
2018-11-23 15:35:09 +01:00
committed by GitHub
parent 0f76779fb1
commit b4290384e1
39 changed files with 472 additions and 57 deletions

View File

@@ -74,7 +74,7 @@ class ItemSerializer(I18nAwareModelSerializer):
class Meta:
model = Item
fields = ('id', 'category', 'name', 'internal_name', 'active', 'description',
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
'position', 'picture', 'available_from', 'available_until',
'require_voucher', 'hide_without_voucher', 'allow_cancel',

View File

@@ -11,6 +11,7 @@ from rest_framework.relations import SlugRelatedField
from rest_framework.reverse import reverse
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.channels import get_all_sales_channels
from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
Question, QuestionAnswer,
@@ -232,7 +233,7 @@ class OrderSerializer(I18nAwareModelSerializer):
model = Order
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval')
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -412,7 +413,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
class Meta:
model = Order
fields = ('code', 'status', 'email', 'locale', 'payment_provider', 'fees', 'comment',
fields = ('code', 'status', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'consume_carts')
def validate_payment_provider(self, pp):
@@ -420,6 +421,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError('The given payment provider is not known.')
return pp
def validate_sales_channel(self, channel):
if channel not in get_all_sales_channels():
raise ValidationError('Unknown sales channel.')
return channel
def validate_code(self, code):
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
raise ValidationError(

View File

@@ -0,0 +1,66 @@
import logging
from collections import OrderedDict
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from pretix.base.signals import register_sales_channels
logger = logging.getLogger(__name__)
_ALL_CHANNELS = None
class SalesChannel:
def __repr__(self):
return '<SalesChannel: {}>'.format(self.identifier)
@property
def identifier(self) -> str:
"""
The internal identifier of this sales channel.
"""
raise NotImplementedError() # NOQA
@property
def verbose_name(self) -> str:
"""
A human-readable name of this sales channel.
"""
raise NotImplementedError() # NOQA
@property
def icon(self) -> str:
"""
The name of a Font Awesome icon to represent this channel
"""
return "circle"
def get_all_sales_channels():
global _ALL_CHANNELS
if _ALL_CHANNELS:
return _ALL_CHANNELS
types = OrderedDict()
for recv, ret in register_sales_channels.send(None):
if isinstance(ret, (list, tuple)):
for r in ret:
types[r.identifier] = r
else:
types[ret.identifier] = ret
_ALL_CHANNELS = types
return types
class WebshopSalesChannel(SalesChannel):
identifier = "web"
verbose_name = _('Online shop')
icon = "globe"
@receiver(register_sales_channels, dispatch_uid="base_register_default_sales_channels")
def base_sales_channels(sender, **kwargs):
return (
WebshopSalesChannel(),
)

View File

@@ -0,0 +1,29 @@
# Generated by Django 2.1.1 on 2018-11-21 12:24
import django.db.models.deletion
import jsonfallback.fields
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0102_auto_20181017_0024'),
]
operations = [
migrations.AddField(
model_name='item',
name='sales_channels',
field=pretix.base.models.fields.MultiStringField(default=['web'], verbose_name='Sales channels'),
preserve_default=False,
),
migrations.AddField(
model_name='order',
name='sales_channel',
field=models.CharField(default='web', max_length=190),
preserve_default=False,
),
]

View File

@@ -0,0 +1,94 @@
from django.core import exceptions
from django.db.models import TextField, lookups as builtin_lookups
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
DELIMITER = "\x1F"
class MultiStringField(TextField):
default_error_messages = {
'delimiter_found': _('No value can contain the delimiter character.')
}
def __init__(self, verbose_name=None, name=None, **kwargs):
super().__init__(verbose_name, name, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
return name, path, args, kwargs
def to_python(self, value):
if isinstance(value, (list, tuple)):
return value
elif value:
return [v for v in value.split(DELIMITER) if v]
else:
return []
def get_prep_value(self, value):
if isinstance(value, (list, tuple)):
return DELIMITER + DELIMITER.join(value) + DELIMITER
elif value is None:
return ""
raise TypeError("Invalid data type passed.")
def get_prep_lookup(self, lookup_type, value): # NOQA
raise TypeError('Lookups on multi strings are currently not supported.')
def from_db_value(self, value, expression, connection, context):
if value:
return [v for v in value.split(DELIMITER) if v]
else:
return []
def validate(self, value, model_instance):
super().validate(value, model_instance)
for l in value:
if DELIMITER in l:
raise exceptions.ValidationError(
self.error_messages['delimiter_found'],
code='delimiter_found',
)
def get_lookup(self, lookup_name):
if lookup_name == 'contains':
return MultiStringContains
elif lookup_name == 'icontains':
return MultiStringIContains
raise NotImplementedError(
"Lookup '{}' doesn't work with MultiStringField".format(lookup_name),
)
class MultiStringContains(builtin_lookups.Contains):
def process_rhs(self, qn, connection):
sql, params = super().process_rhs(qn, connection)
params[0] = "%" + DELIMITER + params[0][1:-1] + DELIMITER + "%"
return sql, params
class MultiStringIContains(builtin_lookups.IContains):
def process_rhs(self, qn, connection):
sql, params = super().process_rhs(qn, connection)
params[0] = "%" + DELIMITER + params[0][1:-1] + DELIMITER + "%"
return sql, params
class MultiStringSerializer(serializers.Field):
def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
super().__init__(**kwargs)
def to_representation(self, value):
return value
def to_internal_value(self, data):
if isinstance(data, list):
return data
else:
raise ValidationError('Invalid data type.')
serializers.ModelSerializer.serializer_field_mapping[MultiStringField] = MultiStringSerializer

View File

@@ -17,6 +17,7 @@ from django.utils.timezone import is_naive, make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models import fields
from pretix.base.models.base import LoggedModel
from pretix.base.models.tax import TaxedPrice
@@ -195,6 +196,8 @@ 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
"""
event = models.ForeignKey(
@@ -329,6 +332,10 @@ 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']
)
# !!! Attention: If you add new fields here, also add them to the copying code in
# pretix/control/forms/item.py if applicable.

View File

@@ -94,6 +94,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
"""
STATUS_PENDING = "n"
@@ -174,6 +176,7 @@ class Order(LockModel, LoggedModel):
require_approval = models.BooleanField(
default=False
)
sales_channel = models.CharField(max_length=190, default="web")
class Meta:
verbose_name = _("Order")

View File

@@ -102,7 +102,8 @@ class CartManager:
AddOperation: 30
}
def __init__(self, event: Event, cart_id: str, invoice_address: InvoiceAddress=None, widget_data=None):
def __init__(self, event: Event, cart_id: str, invoice_address: InvoiceAddress=None, widget_data=None,
sales_channel='web'):
self.event = event
self.cart_id = cart_id
self.now_dt = now()
@@ -115,6 +116,7 @@ class CartManager:
self._expiry = None
self.invoice_address = invoice_address
self._widget_data = widget_data or {}
self._sales_channel = sales_channel
@property
def positions(self):
@@ -192,6 +194,9 @@ class CartManager:
if not op.item.is_available() or (op.variation and not op.variation.active):
raise CartError(error_messages['unavailable'])
if self._sales_channel not in op.item.sales_channels:
raise CartError(error_messages['unavailable'])
if op.voucher and not op.voucher.applies_to(op.item, op.variation):
raise CartError(error_messages['voucher_invalid_item'])
@@ -735,7 +740,7 @@ def get_fees(event, request, total, invoice_address, provider):
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None, widget_data=None) -> None:
invoice_address: int=None, widget_data=None, sales_channel='web') -> None:
"""
Adds a list of items to a user's cart.
:param event: The event ID in question
@@ -755,7 +760,8 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
try:
try:
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, widget_data=widget_data)
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, widget_data=widget_data,
sales_channel=sales_channel)
cm.add_new_items(items)
cm.commit()
except LockTimeoutException:
@@ -807,7 +813,7 @@ def clear_cart(self, event: int, cart_id: str=None, locale='en') -> None:
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None) -> None:
invoice_address: int=None, sales_channel='web') -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
@@ -825,7 +831,7 @@ def set_cart_addons(self, event: int, addons: List[dict], cart_id: str=None, loc
pass
try:
try:
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia)
cm = CartManager(event=event, cart_id=cart_id, invoice_address=ia, sales_channel=sales_channel)
cm.set_addons(addons)
cm.commit()
except LockTimeoutException:

View File

@@ -498,7 +498,7 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
payment_provider: BasePaymentProvider, locale: str=None, address: InvoiceAddress=None,
meta_info: dict=None):
meta_info: dict=None, sales_channel: str='web'):
fees, pf = _get_fees(positions, payment_provider, address, meta_info, event)
total = sum([c.price for c in positions]) + sum([c.value for c in fees])
@@ -511,7 +511,8 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
locale=locale,
total=total,
meta_info=json.dumps(meta_info or {}),
require_approval=any(p.item.require_approval for p in positions)
require_approval=any(p.item.require_approval for p in positions),
sales_channel=sales_channel
)
order.set_expires(now_dt, event.subevents.filter(id__in=[p.subevent_id for p in positions]))
order.save()
@@ -550,7 +551,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
def _perform_order(event: str, payment_provider: str, position_ids: List[str],
email: str, locale: str, address: int, meta_info: dict=None):
email: str, locale: str, address: int, meta_info: dict=None, sales_channel: str='web'):
event = Event.objects.get(id=event)
if payment_provider:
@@ -579,7 +580,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
raise OrderError(error_messages['internal'])
_check_positions(event, now_dt, positions, address=addr)
order = _create_order(event, email, positions, now_dt, pprov,
locale=locale, address=addr, meta_info=meta_info)
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel)
invoice = order.invoices.last() # Might be generated by plugin already
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
@@ -1318,11 +1319,13 @@ class OrderChangeManager:
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
def perform_order(self, event: str, payment_provider: str, positions: List[str],
email: str=None, locale: str=None, address: int=None, meta_info: dict=None):
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
sales_channel: str='web'):
with language(locale):
try:
try:
return _perform_order(event, payment_provider, positions, email, locale, address, meta_info)
return _perform_order(event, payment_provider, positions, email, locale, address, meta_info,
sales_channel)
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):

View File

@@ -178,6 +178,15 @@ however for this signal, the ``sender`` **may also be None** to allow creating t
notification settings!
"""
register_sales_channels = django.dispatch.Signal(
providing_args=[]
)
"""
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
instances.
"""
register_data_exporters = EventPluginSignal(
providing_args=[]
)

View File

@@ -8,6 +8,7 @@ from django.utils.translation import (
)
from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.channels import get_all_sales_channels
from pretix.base.forms import I18nFormSet, I18nModelForm
from pretix.base.models import (
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
@@ -226,6 +227,10 @@ class ItemCreateForm(I18nModelForm):
self.instance.checkin_attention = self.cleaned_data['copy_from'].checkin_attention
self.instance.free_price = self.cleaned_data['copy_from'].free_price
self.instance.original_price = self.cleaned_data['copy_from'].original_price
self.instance.sales_channels = self.cleaned_data['copy_from'].sales_channels
else:
# Add to all sales channels by default
self.instance.sales_channels = [k for k in get_all_sales_channels().keys()]
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
instance = super().save(*args, **kwargs)
@@ -302,6 +307,13 @@ class ItemUpdateForm(I18nModelForm):
'over 65. This ticket includes access to all parts of the event, except the VIP '
'area.'
)
self.fields['sales_channels'] = forms.MultipleChoiceField(
label=_('Sales channels'),
choices=(
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
),
widget=forms.CheckboxSelectMultiple
)
change_decimal_field(self.fields['default_price'], self.event.currency)
class Meta:
@@ -312,6 +324,7 @@ class ItemUpdateForm(I18nModelForm):
'name',
'internal_name',
'active',
'sales_channels',
'admission',
'description',
'picture',

View File

@@ -26,6 +26,7 @@
</fieldset>
<fieldset>
<legend>{% trans "Availability" %}</legend>
{% bootstrap_field form.sales_channels layout="control" %}
{% bootstrap_field form.available_from layout="control" %}
{% bootstrap_field form.available_until layout="control" %}
{% bootstrap_field form.max_per_order layout="control" %}

View File

@@ -114,6 +114,10 @@
<dd>{{ order.code }}</dd>
<dt>{% trans "Order date" %}</dt>
<dd>{{ order.datetime }}</dd>
{% if sales_channel %}
<dt>{% trans "Sales channel" %}</dt>
<dd>{{ sales_channel.verbose_name }}</dd>
{% endif %}
<dt>{% trans "Order locale" %}</dt>
<dd>
{{ display_locale }}

View File

@@ -834,7 +834,10 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, UpdateVie
def plugin_forms(self):
forms = []
for rec, resp in item_forms.send(sender=self.request.event, item=self.item, request=self.request):
forms.append(resp)
if isinstance(resp, (list, tuple)):
forms.extend(resp)
else:
forms.append(resp)
return forms
def get_success_url(self) -> str:

View File

@@ -26,6 +26,7 @@ from django.views.generic import (
)
from i18nfield.strings import LazyI18nString
from pretix.base.channels import get_all_sales_channels
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedFile, CachedTicket, Invoice, InvoiceAddress,
@@ -160,6 +161,7 @@ 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)
return ctx
def get_items(self):

View File

@@ -10,7 +10,7 @@ class ItemAssignmentSerializer(I18nAwareModelSerializer):
class Meta:
model = TicketLayoutItem
fields = ('item',)
fields = ('item', 'sales_channel')
class TicketLayoutSerializer(I18nAwareModelSerializer):

View File

@@ -85,7 +85,13 @@ class AllTicketsPDF(BaseExporter):
with language(op.order.locale):
buffer = BytesIO()
p = o._create_canvas(buffer)
layout = o.layout_map.get(op.item_id, o.default_layout)
layout = o.layout_map.get(
(op.item_id, op.order.sales_channel),
o.layout_map.get(
(op.item_id, 'web'),
o.default_layout
)
)
o._draw_page(layout, p, op, op.order)
p.save()
outbuffer = o._render_with_background(layout, buffer)

View File

@@ -19,13 +19,21 @@ class TicketLayoutItemForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
event = kwargs.pop('event')
self.sales_channel = kwargs.pop('sales_channel')
super().__init__(*args, **kwargs)
self.fields['layout'].label = _('PDF ticket layout')
self.fields['layout'].empty_label = _('(Event default)')
if self.sales_channel.identifier != 'web':
self.fields['layout'].label = _('PDF ticket layout for {channel}').format(
channel=self.sales_channel.verbose_name
)
self.fields['layout'].empty_label = _('(Same as above)')
else:
self.fields['layout'].label = _('PDF ticket layout')
self.fields['layout'].empty_label = _('(Event default)')
self.fields['layout'].queryset = event.ticket_layouts.all()
self.fields['layout'].required = False
def save(self, commit=True):
self.instance.sales_channel = self.sales_channel.identifier
if self.cleaned_data['layout'] is None:
if self.instance.pk:
self.instance.delete()

View File

@@ -0,0 +1,29 @@
# Generated by Django 2.1.1 on 2018-11-23 10:59
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0103_auto_20181121_1224'),
('ticketoutputpdf', '0006_auto_20181017_0024'),
]
operations = [
migrations.AddField(
model_name='ticketlayoutitem',
name='sales_channel',
field=models.CharField(default='web', max_length=190),
),
migrations.AlterField(
model_name='ticketlayoutitem',
name='item',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ticketlayout_assignments', to='pretixbase.Item'),
),
migrations.AlterUniqueTogether(
name='ticketlayoutitem',
unique_together={('item', 'layout', 'sales_channel')},
),
]

View File

@@ -66,6 +66,10 @@ class TicketLayout(LoggedModel):
class TicketLayoutItem(models.Model):
item = models.OneToOneField('pretixbase.Item', null=True, blank=True, related_name='ticketlayout_assignment',
on_delete=models.CASCADE)
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')
class Meta:
unique_together = (('item', 'layout', 'sales_channel'),)

View File

@@ -7,6 +7,7 @@ from django.urls import reverse
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _
from pretix.base.channels import get_all_sales_channels
from pretix.base.models import QuestionAnswer
from pretix.base.signals import ( # NOQA: legacy import
event_copy_data, item_copy_data, layout_text_variables, logentry_display,
@@ -61,25 +62,26 @@ def variables_from_questions(sender, *args, **kwargs):
@receiver(item_forms, dispatch_uid="pretix_ticketoutputpdf_item_forms")
def control_item_forms(sender, request, item, **kwargs):
try:
inst = TicketLayoutItem.objects.get(item=item)
except TicketLayoutItem.DoesNotExist:
inst = TicketLayoutItem(item=item)
return TicketLayoutItemForm(
instance=inst,
event=sender,
data=(request.POST if request.method == "POST" else None),
prefix="ticketlayoutitem"
)
forms = []
for k, v in sorted(list(get_all_sales_channels().items()), key=lambda a: (int(a[0] != 'web'), a[0])):
try:
inst = TicketLayoutItem.objects.get(item=item, sales_channel=k)
except TicketLayoutItem.DoesNotExist:
inst = TicketLayoutItem(item=item)
forms.append(TicketLayoutItemForm(
instance=inst,
event=sender,
sales_channel=v,
data=(request.POST if request.method == "POST" else None),
prefix="ticketlayoutitem_{}".format(k)
))
return forms
@receiver(item_copy_data, dispatch_uid="pretix_ticketoutputpdf_item_copy")
def copy_item(sender, source, target, **kwargs):
try:
inst = TicketLayoutItem.objects.get(item=source)
TicketLayoutItem.objects.create(item=target, layout=inst.layout)
except TicketLayoutItem.DoesNotExist:
pass
for tli in TicketLayoutItem.objects.filter(item=source):
TicketLayoutItem.objects.create(item=target, layout=tli.layout, sales_channel=tli.sales_channel)
@receiver(signal=event_copy_data, dispatch_uid="pretix_ticketoutputpdf_copy_data")
@@ -110,7 +112,8 @@ 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):
TicketLayoutItem.objects.create(item=item_map.get(bi.item_id), layout=layout_map.get(bi.layout_id))
TicketLayoutItem.objects.create(item=item_map.get(bi.item_id), layout=layout_map.get(bi.layout_id),
sales_channel=bi.sales_channel)
return layout_map

View File

@@ -37,7 +37,7 @@ class PdfTicketOutput(BaseTicketOutput):
@cached_property
def layout_map(self):
return {
bi.item_id: bi.layout
(bi.item_id, bi.sales_channel): bi.layout
for bi in TicketLayoutItem.objects.select_related('layout').filter(item__event=self.event)
}
@@ -68,7 +68,13 @@ class PdfTicketOutput(BaseTicketOutput):
buffer = BytesIO()
p = self._create_canvas(buffer)
layout = self.layout_map.get(op.item_id, self.default_layout)
layout = self.layout_map.get(
(op.item_id, order.sales_channel),
self.layout_map.get(
(op.item_id, 'web'),
self.default_layout
)
)
self._draw_page(layout, p, op, order)
p.save()
outbuffer = self._render_with_background(layout, buffer)
@@ -84,7 +90,13 @@ class PdfTicketOutput(BaseTicketOutput):
buffer = BytesIO()
p = self._create_canvas(buffer)
order = op.order
layout = self.layout_map.get(op.item_id, self.default_layout)
layout = self.layout_map.get(
(op.item_id, order.sales_channel),
self.layout_map.get(
(op.item_id, 'web'),
self.default_layout
)
)
with language(order.locale):
self._draw_page(layout, p, op, order)
p.save()

View File

@@ -236,7 +236,8 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
data=(self.request.POST if self.request.method == 'POST' else None),
quota_cache=quota_cache,
item_cache=item_cache,
subevent=cartpos.subevent
subevent=cartpos.subevent,
sales_channel=self.request.sales_channel
)
}
@@ -294,7 +295,8 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
return self.get(request, *args, **kwargs)
return self.do(self.request.event.id, data, get_or_create_cart_id(self.request),
invoice_address=self.invoice_address.pk, locale=get_language())
invoice_address=self.invoice_address.pk, locale=get_language(),
sales_channel=request.sales_channel)
class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
@@ -613,7 +615,8 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
return self.do(self.request.event.id, self.payment_provider.identifier if self.payment_provider else None,
[p.id for p in self.positions], self.cart_session.get('email'),
translation.get_language(), self.invoice_address.pk, meta_info)
translation.get_language(), self.invoice_address.pk, meta_info,
request.sales_channel)
def get_success_message(self, value):
create_empty_cart_id(self.request)

View File

@@ -138,7 +138,6 @@ class AddOnsForm(forms.Form):
if override_price:
price = override_price
print(price, repr(price), type(price), repr(item.default_price))
if self.price_included:
price = TAXED_ZERO
else:
@@ -191,6 +190,7 @@ class AddOnsForm(forms.Form):
quota_cache = kwargs.pop('quota_cache')
item_cache = kwargs.pop('item_cache')
self.price_included = kwargs.pop('price_included')
self.sales_channel = kwargs.pop('sales_channel')
super().__init__(*args, **kwargs)
@@ -209,6 +209,7 @@ class AddOnsForm(forms.Form):
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(hide_without_voucher=False)
& Q(sales_channels__contains=self.sales_channel)
).select_related('tax_rule').prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',

View File

@@ -92,6 +92,9 @@ def _detect_event(request, require_live=True, require_plugin=None):
if require_plugin not in request.event.get_plugins() and not is_core:
raise Http404(_('This feature is not enabled.'))
if not hasattr(request, 'sales_channel'):
# The environ lookup is only relevant during unit testing
request.sales_channel = request.environ.get('PRETIX_SALES_CHANNEL', 'web')
for receiver, response in process_request.send(request.event, request=request):
if response:
return response

View File

@@ -382,7 +382,7 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
items = self._items_from_post_data()
if items:
return self.do(self.request.event.id, items, cart_id, translation.get_language(),
self.invoice_address.pk, widget_data)
self.invoice_address.pk, widget_data, self.request.sales_channel)
else:
if 'ajax' in self.request.GET or 'ajax' in self.request.POST:
return JsonResponse({
@@ -405,7 +405,7 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, TemplateView):
# Fetch all items
items, display_add_to_cart = get_grouped_items(self.request.event, self.subevent,
voucher=self.voucher)
voucher=self.voucher, channel=self.request.sales_channel)
# Calculate how many options the user still has. If there is only one option, we can
# check the box right away ;)

View File

@@ -47,12 +47,13 @@ def item_group_by_category(items):
)
def get_grouped_items(event, subevent=None, voucher=None):
def get_grouped_items(event, subevent=None, voucher=None, channel='web'):
items = event.items.all().filter(
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(Q(category__isnull=True) | Q(category__is_addon=False))
& Q(sales_channels__contains=channel)
)
vouchq = Q(hide_without_voucher=False)
@@ -249,7 +250,8 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
context = super().get_context_data(**kwargs)
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)
items, display_add_to_cart = get_grouped_items(self.request.event, self.subevent,
channel=self.request.sales_channel)
context['itemnum'] = len(items)
# Regroup those by category

View File

@@ -155,7 +155,7 @@ class WidgetAPIProductList(View):
def _get_items(self):
items, display_add_to_cart = get_grouped_items(
self.request.event, subevent=self.subevent, voucher=self.voucher
self.request.event, subevent=self.subevent, voucher=self.voucher, channel='web'
)
grps = []
for cat, g in item_group_by_category(items):