forked from CGM_Public/pretix_original
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:
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
66
src/pretix/base/channels.py
Normal file
66
src/pretix/base/channels.py
Normal 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(),
|
||||
)
|
||||
29
src/pretix/base/migrations/0103_auto_20181121_1224.py
Normal file
29
src/pretix/base/migrations/0103_auto_20181121_1224.py
Normal 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,
|
||||
),
|
||||
]
|
||||
94
src/pretix/base/models/fields.py
Normal file
94
src/pretix/base/models/fields.py
Normal 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
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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=[]
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -10,7 +10,7 @@ class ItemAssignmentSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = TicketLayoutItem
|
||||
fields = ('item',)
|
||||
fields = ('item', 'sales_channel')
|
||||
|
||||
|
||||
class TicketLayoutSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')},
|
||||
),
|
||||
]
|
||||
@@ -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'),)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ;)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user