New implementation of sales channels (#4111)

Co-authored-by: Martin Gross <gross@rami.io>
This commit is contained in:
Raphael Michel
2024-06-30 19:24:30 +02:00
committed by GitHub
parent 95511b0330
commit 4fb5c6bef0
174 changed files with 2902 additions and 616 deletions

View File

@@ -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(),
}

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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:

View File

@@ -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(),

View File

@@ -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

View File

@@ -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.'),

View File

@@ -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={

View File

@@ -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 %}
<td>
{% for channel in cl.auto_checkin_sales_channels %}
<span class="fa fa-{{ channel.icon }} text-muted"
data-toggle="tooltip" title="{% trans channel.verbose_name %}"></span>
{% for channel in cl.auto_checkin_sales_channels.all %}
{% if "." in channel.icon %}
<img src="{% static channel.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ channel.label }}">
{% else %}
<span class="fa fa-{{ channel.icon }} text-muted"
data-toggle="tooltip" title="{{ channel.label }}"></span>
{% endif %}
{% endfor %}
</td>
<td>

View File

@@ -1,5 +1,6 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load static %}
{% load bootstrap3 %}
{% block inside %}
<h1>{% trans "Payment settings" %}</h1>
@@ -30,8 +31,13 @@
</td>
<td class="iconcol">
{% for channel in provider.sales_channels %}
<span class="fa fa-{{ channel.icon }} text-muted"
data-toggle="tooltip" title="{% trans channel.verbose_name %}"></span>
{% if "." in channel.icon %}
<img src="{% static channel.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ channel.label }}">
{% else %}
<span class="fa fa-{{ channel.icon }} text-muted"
data-toggle="tooltip" title="{{ channel.label }}"></span>
{% endif %}
{% endfor %}
</td>
<td class="text-right flip">

View File

@@ -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 %}
<div class="form-group metadata-group">

View File

@@ -1,6 +1,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
{% load static %}
{% load getitem %}
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}" id="item_variations">
{{ formset.management_form }}
@@ -40,9 +41,14 @@
title="{% trans "Require a valid membership" %}"></span>
</div>
<div class="col-md-2 col-xs-6">
{% for k, c in sales_channels.items %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted variation-channel-{{ k }} variation-icon-hidden"
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
{% for c in sales_channels %}
{% if "." in c.icon %}
<img src="{% static c.icon %}" class="fa-like-image variation-channel-{{ c.id }} variation-icon-hidden"
data-toggle="tooltip" title="{{ c.label }}">
{% else %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted variation-channel-{{ c.id }} variation-icon-hidden"
data-toggle="tooltip" title="{{ c.label }}"></span>
{% endif %}
{% endfor %}
</div>
<div class="col-md-1 col-xs-6 text-right flip variation-price">
@@ -97,7 +103,8 @@
{% endif %}
{% bootstrap_field form.available_from visibility_field=form.available_from_mode layout="control_with_visibility" %}
{% bootstrap_field form.available_until visibility_field=form.available_until_mode layout="control_with_visibility" %}
{% bootstrap_field form.sales_channels layout="control" %}
{% bootstrap_field form.all_sales_channels layout="control" %}
{% bootstrap_field form.limit_sales_channels layout="control" %}
{% bootstrap_field form.hide_without_voucher layout="control" %}
{% bootstrap_field form.require_approval layout="control" %}
{% if form.require_membership %}
@@ -148,9 +155,14 @@
title="{% trans "Require a valid membership" %}"></span>
</div>
<div class="col-md-2 col-xs-6">
{% for k, c in sales_channels.items %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted variation-channel-{{ k }} variation-icon-hidden"
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
{% for c in sales_channels %}
{% if "." in c.icon %}
<img src="{% static c.icon %}" class="fa-like-image variation-channel-{{ c.id }} variation-icon-hidden"
data-toggle="tooltip" title="{{ c.label }}">
{% else %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted variation-channel-{{ c.id }} variation-icon-hidden"
data-toggle="tooltip" title="{{ c.label }}"></span>
{% endif %}
{% endfor %}
</div>
<div class="col-md-1 col-xs-6 text-right flip variation-price">
@@ -197,7 +209,8 @@
{% bootstrap_field formset.empty_form.available_from visibility_field=formset.empty_form.available_from_mode layout="control_with_visibility" %}
{% bootstrap_field formset.empty_form.available_until visibility_field=formset.empty_form.available_until_mode layout="control_with_visibility" %}
{% bootstrap_field formset.empty_form.available_until layout="control" %}
{% bootstrap_field formset.empty_form.sales_channels layout="control" %}
{% bootstrap_field formset.empty_form.all_sales_channels layout="control" %}
{% bootstrap_field formset.empty_form.limit_sales_channels layout="control" %}
{% bootstrap_field formset.empty_form.hide_without_voucher layout="control" %}
{% bootstrap_field formset.empty_form.require_approval layout="control" %}
{% if formset.empty_form.require_membership %}

View File

@@ -152,9 +152,8 @@
</fieldset>
<fieldset>
<legend>{% trans "Availability" %}</legend>
{% bootstrap_field form.sales_channels layout="control" horizontal_field_class="col-md-7" %}
{% bootstrap_field form.all_sales_channels layout="control" horizontal_field_class="col-md-7" %}
{% bootstrap_field form.limit_sales_channels layout="control" horizontal_field_class="col-md-7" %}
{% bootstrap_field form.available_from visibility_field=form.available_from_mode layout="control_with_visibility" %}
{% bootstrap_field form.available_until visibility_field=form.available_until_mode layout="control_with_visibility" %}
{% bootstrap_field form.max_per_order layout="control" horizontal_field_class="col-md-7" %}

View File

@@ -15,7 +15,8 @@
{% bootstrap_field form.internal_name layout="control" %}
{% bootstrap_field form.available_from layout="control" %}
{% bootstrap_field form.available_until 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" %}
</fieldset>
<fieldset>
<legend>{% trans "Condition" context "discount" %}</legend>

View File

@@ -1,5 +1,6 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Automatic discounts" %}{% endblock %}
{% block inside %}
<h1>{% trans "Automatic discounts" %}</h1>
@@ -78,10 +79,15 @@
{% endif %}
</td>
<td>
{% for k, c in sales_channels.items %}
{% if k in d.sales_channels %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
{% for c in sales_channels %}
{% if d.all_sales_channels or c in d.limit_sales_channels.all %}
{% if "." in c.icon %}
<img src="{% static c.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ c.label }}">
{% else %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
data-toggle="tooltip" title="{{ c.label }}"></span>
{% endif %}
{% else %}
{% endif %}
{% endfor %}

View File

@@ -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 @@
<br>
<small class="text-muted">
#{{ i.pk }}
{% for k, c in sales_channels.items %}
{% if k in i.sales_channels %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
{% for c in sales_channels %}
{% if i.all_sales_channels or c in i.limit_sales_channels.all %}
{% if "." in c.icon %}
<img src="{% static c.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ c.label }}">
{% else %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
data-toggle="tooltip" title="{{ c.label }}"></span>
{% endif %}
{% else %}
{% endif %}
{% endfor %}

View File

@@ -185,9 +185,9 @@
<dt>{% trans "Cancellation date" %}</dt>
<dd>{{ order.cancellation_date|date:"SHORT_DATETIME_FORMAT" }}</dd>
{% endif %}
{% if sales_channel %}
{% if order.sales_channel %}
<dt>{% trans "Sales channel" %}</dt>
<dd>{{ sales_channel.verbose_name }}</dd>
<dd>{{ order.sales_channel.label }}</dd>
{% endif %}
<dt>{% trans "Order locale" %}</dt>
<dd>

View File

@@ -4,6 +4,7 @@
{% load urlreplace %}
{% load money %}
{% load bootstrap3 %}
{% load static %}
{% block title %}{% trans "Orders" %}{% endblock %}
{% block content %}
<h1>{% trans "Orders" %}</h1>
@@ -201,8 +202,13 @@
{% endif %}
</td>
<td>
<span class="fa fa-fw fa-{{ o.sales_channel_obj.icon }} text-muted"
data-toggle="tooltip" title="{% trans o.sales_channel_obj.verbose_name %}"></span>
{% if "." in o.sales_channel.icon %}
<img src="{% static o.sales_channel.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ o.sales_channel.label }}">
{% else %}
<span class="fa fa-fw fa-{{ o.sales_channel.icon }} text-muted"
data-toggle="tooltip" title="{{ o.sales_channel.label }}"></span>
{% endif %}
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
</td>
<td class="text-right flip">

View File

@@ -0,0 +1,27 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load formset_tags %}
{% load bootstrap3 %}
{% block inner %}
<h1>{% trans "Add sales channel" %}</h1>
<form class="form-horizontal" action="" method="post">
{% csrf_token %}
{% bootstrap_form_errors form layout="control" %}
{% bootstrap_field form.label layout="control" %}
<div class="form-group">
<label class="col-md-3 control-label" for="id_identifier">{% trans "Channel type" %}</label>
<div class="col-md-9">
<input type="text" value="{{ type.verbose_name }}" class="form-control" disabled>
</div>
</div>
{% bootstrap_field form.identifier addon_before=identifier_prefix layout="control" %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load static %}
{% load bootstrap3 %}
{% block inner %}
<h1>{% trans "Add sales channel" %}</h1>
<div class="list-group large-link-group">
{% for t in types %}
<a class="list-group-item" href="?type={{ t.identifier }}">
<h4>
{% if "." in t.icon %}
<img class="fa-like-image" src="{% static t.icon %}" alt="">
{% else %}
<span class="fa fa-fw fa-{{ t.icon }} text-muted"></span>
{% endif %}
{{ t.verbose_name }}
</h4>
{% if t.description %}
<p>
{{ t.description }}
</p>
{% endif %}
</a>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,33 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<h1>{% trans "Delete sales channel:" %} {{ channel.label }}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if is_allowed %}
<p>
{% blocktrans trimmed %}
Are you sure you want to delete this sales channel?
{% endblocktrans %}
{% else %}
<div class="alert alert-danger">
{% blocktrans trimmed %}
This sales channel cannot be deleted since it has already been used to sell orders or because it is
a core element of the system.
{% endblocktrans %}
</div>
{% endif %}
<div class="form-group submit-group">
<a href="{% url "control:organizer.channels" organizer=request.organizer.slug %}"
class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
{% if is_allowed %}
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
{% endif %}
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load formset_tags %}
{% load bootstrap3 %}
{% block inner %}
<h1>{% trans "Sales channel:" %} {{ channel.label }}</h1>
<form class="form-horizontal" action="" method="post">
{% csrf_token %}
{% bootstrap_form_errors form layout="control" %}
{% bootstrap_field form.label layout="control" %}
<div class="form-group">
<label class="col-md-3 control-label" for="id_identifier">{% trans "Channel type" %}</label>
<div class="col-md-9">
<input type="text" value="{{ type.verbose_name }}" class="form-control" disabled>
</div>
</div>
{% bootstrap_field form.identifier layout="control" %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,64 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load static %}
{% block inner %}
<h1>{% trans "Sales channels" %}</h1>
<p>
{% 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 %}
</p>
<a href="{% url "control:organizer.channel.add" organizer=request.organizer.slug %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Add a new channel" %}
</a>
<form method="post">
{% csrf_token %}
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Channel" %}</th>
<th>{% trans "Identifier" %}</th>
<th>{% trans "Channel type" %}</th>
<th class="action-col-2"></th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody data-dnd-url="{% url "control:organizer.channels.reorder" organizer=request.organizer.slug %}">
{% for c in channels %}
<tr data-dnd-id="{{ c.pk }}">
<td><strong>
<a href="{% url "control:organizer.channel.edit" organizer=request.organizer.slug channel=c.identifier %}">
{{ c.label }}
</a>
</strong></td>
<td>
<code>{{ c.identifier }}</code>
</td>
<td>
{% if "." in c.type_instance.icon %}
<img class="fa-like-image" src="{% static c.icon %}" alt="">
{% else %}
<span class="fa fa-fw fa-{{ c.type_instance.icon }} text-muted"></span>
{% endif %}
{{ c.type_instance.verbose_name }}
</td>
<td>
<button formaction="{% url "control:organizer.channel.up" organizer=request.organizer.slug channel=c.identifier %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:organizer.channel.down" organizer=request.organizer.slug channel=c.identifier %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span>
</td>
<td class="text-right flip">
<a href="{% url "control:organizer.channel.edit" organizer=request.organizer.slug channel=c.identifier %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:organizer.channel.delete" organizer=request.organizer.slug channel=c.identifier %}"
class="btn btn-danger btn-sm {% if c.type_instance.default_created %}disabled{% endif %}"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
{% endblock %}

View File

@@ -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 }}
</td>
<td>
<span class="fa fa-{{ o.sales_channel_obj.icon }} text-muted"
data-toggle="tooltip" title="{% trans o.sales_channel_obj.verbose_name %}"></span>
{% if "." in o.sales_channel.icon %}
<img src="{% static o.sales_channel.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ o.sales_channel.label }}">
{% else %}
<span class="fa fa-{{ o.sales_channel.icon }} text-muted"
data-toggle="tooltip" title="{{ o.sales_channel.label }}"></span>
{% endif %}
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if o.customer_id != customer.pk %}
<span class="fa fa-link text-muted"

View File

@@ -136,6 +136,19 @@ urlpatterns = [
name='organizer.property.down'),
re_path(r'^organizer/(?P<organizer>[^/]+)/property/reorder$', organizer.reorder_meta_properties,
name='organizer.properties.reorder'),
re_path(r'^organizer/(?P<organizer>[^/]+)/channels$', organizer.ChannelListView.as_view(), name='organizer.channels'),
re_path(r'^organizer/(?P<organizer>[^/]+)/channel/add$', organizer.ChannelCreateView.as_view(),
name='organizer.channel.add'),
re_path(r'^organizer/(?P<organizer>[^/]+)/channel/(?P<channel>[^/]+)/edit$', organizer.ChannelUpdateView.as_view(),
name='organizer.channel.edit'),
re_path(r'^organizer/(?P<organizer>[^/]+)/channel/(?P<channel>[^/]+)/delete$', organizer.ChannelDeleteView.as_view(),
name='organizer.channel.delete'),
re_path(r'^organizer/(?P<organizer>[^/]+)/channel/(?P<channel>[^/]+)/up$', organizer.channel_move_up,
name='organizer.channel.up'),
re_path(r'^organizer/(?P<organizer>[^/]+)/channel/(?P<channel>[^/]+)/down$', organizer.channel_move_down,
name='organizer.channel.down'),
re_path(r'^organizer/(?P<organizer>[^/]+)/channel/reorder$', organizer.reorder_channels,
name='organizer.channels.reorder'),
re_path(r'^organizer/(?P<organizer>[^/]+)/membershiptypes$', organizer.MembershipTypeListView.as_view(), name='organizer.membershiptypes'),
re_path(r'^organizer/(?P<organizer>[^/]+)/membershiptype/add$', organizer.MembershipTypeCreateView.as_view(),
name='organizer.membershiptype.add'),

View File

@@ -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(

View File

@@ -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

View File

@@ -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']:

View File

@@ -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(

View File

@@ -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

View File

@@ -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()

View File

@@ -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'))