This commit is contained in:
Raphael Michel
2022-04-27 14:43:16 +02:00
committed by GitHub
parent 7730cc6170
commit 6fee0ac0a9
56 changed files with 4159 additions and 480 deletions

View File

@@ -0,0 +1,109 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from decimal import Decimal
from django import forms
from django.utils.translation import gettext_lazy as _
from pretix.base.channels import get_all_sales_channels
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
class DiscountForm(I18nModelForm):
class Meta:
model = Discount
localized_fields = '__all__'
fields = [
'active',
'internal_name',
'sales_channels',
'available_from',
'available_until',
'subevent_mode',
'condition_all_products',
'condition_limit_products',
'condition_min_count',
'condition_min_value',
'condition_apply_to_addons',
'condition_ignore_voucher_discounted',
'benefit_discount_matching_percent',
'benefit_only_apply_to_cheapest_n_matches',
]
field_classes = {
'available_from': SplitDateTimeField,
'available_until': SplitDateTimeField,
'condition_limit_products': ItemMultipleChoiceField,
}
widgets = {
'subevent_mode': forms.RadioSelect,
'available_from': SplitDateTimePickerWidget(),
'available_until': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_available_from_0'}),
'condition_limit_products': forms.CheckboxSelectMultiple(attrs={
'data-inverse-dependency': '<[name$=all_products]',
'class': 'scrolling-multiple-choice',
}),
'benefit_only_apply_to_cheapest_n_matches': forms.NumberInput(
attrs={
'data-display-dependency': '#id_condition_min_count',
}
)
}
def __init__(self, *args, **kwargs):
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['condition_limit_products'].queryset = self.event.items.all()
self.fields['condition_min_count'].required = False
self.fields['condition_min_count'].widget.is_required = False
self.fields['condition_min_value'].required = False
self.fields['condition_min_value'].widget.is_required = False
if not self.event.has_subevents:
del self.fields['subevent_mode']
def clean(self):
d = super().clean()
if d.get('condition_min_value') and d.get('benefit_only_apply_to_cheapest_n_matches'):
# field is hidden by JS
d['benefit_only_apply_to_cheapest_n_matches'] = None
if d.get('subevent_mode') == Discount.SUBEVENT_MODE_DISTINCT and d.get('condition_min_value'):
# field is hidden by JS
d['condition_min_value'] = Decimal('0.00')
if d.get('condition_min_count') is None:
d['condition_min_count'] = 0
if d.get('condition_min_value') is None:
d['condition_min_value'] = Decimal('0.00')
return d

View File

@@ -449,6 +449,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.event.question.added': _('The question has been added.'),
'pretix.event.question.deleted': _('The question has been deleted.'),
'pretix.event.question.changed': _('The question has been changed.'),
'pretix.event.discount.added': _('The discount has been added.'),
'pretix.event.discount.deleted': _('The discount has been deleted.'),
'pretix.event.discount.changed': _('The discount has been changed.'),
'pretix.event.taxrule.added': _('The tax rule has been added.'),
'pretix.event.taxrule.deleted': _('The tax rule has been deleted.'),
'pretix.event.taxrule.changed': _('The tax rule has been changed.'),

View File

@@ -186,6 +186,14 @@ def get_event_navigation(request: HttpRequest):
}),
'active': 'event.items.questions' in url.url_name,
},
{
'label': _('Discounts'),
'url': reverse('control:event.items.discounts', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.items.discounts' in url.url_name,
},
]
})

View File

@@ -0,0 +1,74 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Automatic discount" %}{% endblock %}
{% block inside %}
<h1>{% trans "Automatic discount" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
<div class="row">
<div class="col-xs-12{% if discount %} col-lg-10{% endif %}">
<fieldset>
<legend>{% trans "General information" %}</legend>
{% bootstrap_field form.active layout="control" %}
{% 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" %}
</fieldset>
<fieldset>
<legend>{% trans "Condition" context "discount" %}</legend>
{% bootstrap_field form.condition_all_products layout="control" %}
{% bootstrap_field form.condition_limit_products layout="control" %}
{% bootstrap_field form.condition_apply_to_addons layout="control" %}
{% bootstrap_field form.condition_ignore_voucher_discounted layout="control" %}
{% if form.subevent_mode %}
{% bootstrap_field form.subevent_mode layout="control" %}
{% endif %}
<div class="form-group form-alternatives">
<label class="col-md-3 control-label">
{% trans "Minimum cart content" %}<br>
<span class="optional">{% trans "Optional" %}</span>
</label>
<div class="col-md-4">
{% bootstrap_field form.condition_min_count form_group_class="" %}
</div>
<div class="col-md-1 text-center condition-or" data-display-dependency="#id_subevent_mode_2" data-inverse>
<div class="hr">
<div class="sep">
<div class="sepText">{% trans "OR" %}</div>
</div>
</div>
</div>
<div class="col-md-4" data-display-dependency="#id_subevent_mode_2" data-inverse>
{% bootstrap_field form.condition_min_value form_group_class="" %}
</div>
</div>
</fieldset>
<fieldset>
<legend>{% trans "Benefit" context "discount" %}</legend>
{% bootstrap_field form.benefit_discount_matching_percent layout="control" addon_after="%" %}
{% bootstrap_field form.benefit_only_apply_to_cheapest_n_matches layout="control" %}
</fieldset>
</div>
{% if discount %}
<div class="col-xs-12 col-lg-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Discount history" %}
</h3>
</div>
{% include "pretixcontrol/includes/logs.html" with obj=discount %}
</div>
</div>
{% endif %}
</div>
<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,41 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Delete discount" %}{% endblock %}
{% block inside %}
<h1>{% trans "Delete discount" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if not possible and not item.active %}
<p>{% blocktrans %}You cannot delete the discount <strong>{{ discount }}</strong> because it already has
been used as part of an order.{% endblocktrans %}</p>
<div class="form-group submit-group">
<a href="{% url "control:event.discounts" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<div class="clearfix"></div>
</div>
{% else %}
{% if possible %}
<p>{% blocktrans trimmed with name=discount.internal_name %}
Are you sure you want to delete the discount <strong>{{ name }}</strong>?
{% endblocktrans %}</p>
{% else %}
<p>{% blocktrans trimmed with name=discount.internal_name %}
You cannot delete the discount <strong>{{ name }}</strong> because it already has been used as part
of an order, but you can deactivate it.
{% endblocktrans %}</p>
{% endif %}
<div class="form-group submit-group">
<a href="{% url "control:event.items.discounts" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% if possible %}{% trans "Delete" %}{% else %}{% trans "Deactivate" %}{% endif %}
</button>
</div>
{% endif %}
</form>
{% endblock %}

View File

@@ -0,0 +1,147 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% block title %}{% trans "Automatic discounts" %}{% endblock %}
{% block inside %}
<h1>{% trans "Automatic discounts" %}</h1>
<p>
{% blocktrans trimmed %}
With automatic discounts, you can automatically apply a discount to purchases from your customers based
on certain conditions. For example, you can create group discounts like "get 20% off if you buy 3 or more
tickets" or "buy 2 tickets, get 1 free".
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
Automatic discounts are available to all customers as long as they are active. If you want to offer special
prices only to specific customers, you can use vouchers instead. If you want to offer discounts across
multiple purchases ("buy a package of 10 you can turn into individual tickest later"), you can use
customer accounts and memberships instead.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
Discounts are only automatically applied during an initial purchase. They are not applied if an existing
order is changed through any of the available options.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
Every product in the cart can only be affected by one discount. If you have overlapping discounts, the
first one in the order of the list below will apply.
{% endblocktrans %}
</p>
{% if discounts|length == 0 %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
You haven't created any discounts yet.
{% endblocktrans %}
</p>
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new discount" %}</a>
</div>
{% else %}
<p>
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new discount" %}
</a>
</p>
<form method="post">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>{% trans "Internal name" %}</th>
<th></th>
<th></th>
<th>{% trans "Products" %}</th>
<th class="action-col-2"></th>
<th class="action-col-2"></th>
</tr>
</thead>
<tbody data-dnd-url="{% url "control:event.items.discounts.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
{% for d in discounts %}
<tr data-dnd-id="{{ d.id }}">
<td>
{% if d.active %}
<strong>
{% else %}
<del>
{% endif %}
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}">
{{ d.internal_name }}</a>
{% if d.active %}
</strong>
{% else %}
</del>
{% 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>
{% else %}
{% endif %}
{% endfor %}
</td>
<td>
{% if d.available_from or d.available_until %}
{% if not d.is_available_by_time %}
<span class="label label-danger" data-toggle="tooltip"
title="{% trans "Currently unavailable since a limited timeframe for this product has been set" %}">
<span class="fa fa-clock-o fa-fw" data-toggle="tooltip">
</span>
</span>
{% else %}
<span class="fa fa-clock-o fa-fw text-muted" data-toggle="tooltip"
title="{% trans "Only available in a limited timeframe" %}">
</span>
{% endif %}
{% endif %}
</td>
<td>
{% if d.condition_all_products %}
<em>{% trans "All" %}</em>
{% else %}
<ul>
{% for item in d.condition_limit_products.all %}
<li>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</td>
<td>
<button formaction="{% url "control:event.items.discounts.up" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
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:event.items.discounts.down" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
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:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ d.id }}"
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
<a href="{% url "control:event.items.discounts.delete" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</form>
{% include "pretixcontrol/pagination.html" %}
{% endif %}
{% endblock %}

View File

@@ -387,7 +387,7 @@
{% if line.voucher %}
<br/><span class="fa fa-tags fa-fw"></span> {% trans "Voucher code used:" %}
<a
{% if line.price_before_voucher|default_if_none:"NONE" != "NONE" %}data-toggle="tooltip" title="{% blocktrans trimmed with price=line.price_before_voucher|money:request.event.currency %}Original price: {{ price }}{% endblocktrans %}"{% endif %}
{% if line.voucher.budget and line.voucher_budget_use|default_if_none:"NONE" != "NONE" %}data-toggle="tooltip" title="{% blocktrans trimmed with amount=line.voucher_budget_use|money:request.event.currency %}Used {{ amount }} discount from budget{% endblocktrans %}"{% endif %}
href="{% url "control:event.voucher" event=request.event.slug organizer=request.event.organizer.slug voucher=line.voucher.pk %}">
{{ line.voucher.code }}
</a>
@@ -406,6 +406,15 @@
{{ line.used_membership }}
</a>
{% endif %}
{% if line.discount %}
<br />
<span class="text-success discounted" data-toggle="tooltip" title="{% trans "The price of this product was reduced because of an automatic discount or this product was part of the discount calculation for a different product in this order." %}">
<span class="fa fa-percent fa-fw" aria-hidden="true"></span>
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=line.discount.id %}">
{{ line.discount.internal_name }}
</a>
</span>
{% endif %}
{% if not line.canceled %}
<div class="position-buttons">
{% if line.generate_ticket %}

View File

@@ -37,9 +37,9 @@ from django.conf.urls import include, re_path
from django.views.generic.base import RedirectView
from pretix.control.views import (
auth, checkin, dashboards, event, geo, global_settings, item, main, oauth,
orderimport, orders, organizer, pdf, search, shredder, subevents,
typeahead, user, users, vouchers, waitinglist,
auth, checkin, dashboards, discounts, event, geo, global_settings, item,
main, oauth, orderimport, orders, organizer, pdf, search, shredder,
subevents, typeahead, user, users, vouchers, waitinglist,
)
urlpatterns = [
@@ -279,6 +279,16 @@ urlpatterns = [
re_path(r'^quotas/(?P<quota>\d+)/delete$', item.QuotaDelete.as_view(),
name='event.items.quotas.delete'),
re_path(r'^quotas/add$', item.QuotaCreate.as_view(), name='event.items.quotas.add'),
re_path(r'^discounts/$', discounts.DiscountList.as_view(), name='event.items.discounts'),
re_path(r'^discounts/(?P<discount>\d+)/delete$', discounts.DiscountDelete.as_view(),
name='event.items.discounts.delete'),
re_path(r'^discounts/(?P<discount>\d+)/up$', discounts.discount_move_up, name='event.items.discounts.up'),
re_path(r'^discounts/(?P<discount>\d+)/down$', discounts.discount_move_down,
name='event.items.discounts.down'),
re_path(r'^discounts/reorder$', discounts.reorder_discounts, name='event.items.discounts.reorder'),
re_path(r'^discounts/(?P<discount>\d+)/$', discounts.DiscountUpdate.as_view(),
name='event.items.discounts.edit'),
re_path(r'^discounts/add$', discounts.DiscountCreate.as_view(), name='event.items.discounts.add'),
re_path(r'^vouchers/$', vouchers.VoucherList.as_view(), name='event.vouchers'),
re_path(r'^vouchers/tags/$', vouchers.VoucherTags.as_view(), name='event.vouchers.tags'),
re_path(r'^vouchers/rng$', vouchers.VoucherRNG.as_view(), name='event.vouchers.rng'),

View File

@@ -0,0 +1,269 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import json
from json.decoder import JSONDecodeError
from django.contrib import messages
from django.db import transaction
from django.db.models import Max
from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
)
from django.shortcuts import redirect
from django.urls import resolve, reverse
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_http_methods
from django.views.generic import ListView
from django.views.generic.edit import DeleteView
from pretix.base.models import CartPosition, Discount
from pretix.control.forms.discounts import DiscountForm
from pretix.control.permissions import (
EventPermissionRequiredMixin, event_permission_required,
)
from pretix.helpers.models import modelcopy
from ...base.channels import get_all_sales_channels
from . import CreateView, PaginationMixin, UpdateView
class DiscountDelete(EventPermissionRequiredMixin, DeleteView):
model = Discount
template_name = 'pretixcontrol/items/discount_delete.html'
permission = 'can_change_items'
context_object_name = 'discount'
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['possible'] = self.object.allow_delete()
return context
def get_object(self, queryset=None) -> Discount:
try:
return self.request.event.discounts.get(
id=self.kwargs['discount']
)
except Discount.DoesNotExist:
raise Http404(_("The requested discount does not exist."))
@transaction.atomic
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
success_url = self.get_success_url()
if self.object.allow_delete():
CartPosition.objects.filter(discount=self.object).update(discount=None)
self.object.log_action('pretix.event.discount.deleted', user=self.request.user)
self.object.delete()
messages.success(request, _('The selected discount has been deleted.'))
else:
o = self.get_object()
o.active = False
o.save()
o.log_action('pretix.event.discount.changed', user=self.request.user, data={
'active': False
})
messages.success(request, _('The selected discount has been deactivated.'))
return HttpResponseRedirect(success_url)
def get_success_url(self) -> str:
return reverse('control:event.items.discounts', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
class DiscountUpdate(EventPermissionRequiredMixin, UpdateView):
model = Discount
form_class = DiscountForm
template_name = 'pretixcontrol/items/discount.html'
permission = 'can_change_items'
context_object_name = 'discount'
def get_object(self, queryset=None) -> Discount:
url = resolve(self.request.path_info)
try:
return self.request.event.discounts.get(
id=url.kwargs['discount']
)
except Discount.DoesNotExist:
raise Http404(_("The requested discount does not exist."))
@transaction.atomic
def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.'))
if form.has_changed():
self.object.log_action(
'pretix.event.discount.changed', user=self.request.user, data={
k: form.cleaned_data.get(k) for k in form.changed_data
}
)
return super().form_valid(form)
def get_success_url(self) -> str:
return reverse('control:event.items.discounts', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.event
return kwargs
def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
class DiscountCreate(EventPermissionRequiredMixin, CreateView):
model = Discount
form_class = DiscountForm
template_name = 'pretixcontrol/items/discount.html'
permission = 'can_change_items'
context_object_name = 'discount'
def get_success_url(self) -> str:
return reverse('control:event.items.discounts', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
})
@cached_property
def copy_from(self):
if self.request.GET.get("copy_from") and not getattr(self, 'object', None):
try:
return self.request.event.discounts.get(pk=self.request.GET.get("copy_from"))
except Discount.DoesNotExist:
pass
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if self.copy_from:
i = modelcopy(self.copy_from)
i.pk = None
kwargs['instance'] = i
else:
kwargs['instance'] = Discount(event=self.request.event)
kwargs['event'] = self.request.event
return kwargs
@transaction.atomic
def form_valid(self, form):
form.instance.event = self.request.event
form.instance.position = (self.request.event.discounts.aggregate(m=Max('position'))['m'] or 0) + 1
messages.success(self.request, _('The new discount has been created.'))
ret = super().form_valid(form)
form.instance.log_action('pretix.event.discount.added', data=dict(form.cleaned_data), user=self.request.user)
return ret
def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
class DiscountList(PaginationMixin, ListView):
model = Discount
context_object_name = 'discounts'
template_name = 'pretixcontrol/items/discounts.html'
def get_queryset(self):
return self.request.event.discounts.prefetch_related('condition_limit_products')
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['sales_channels'] = get_all_sales_channels()
return ctx
def discount_move(request, discount, up=True):
"""
This is a helper function to avoid duplicating code in discount_move_up and
discount_move_down. It takes a discount and a direction and then tries to bring
all discounts for this event in a new order.
"""
try:
discount = request.event.discounts.get(
id=discount
)
except Discount.DoesNotExist:
raise Http404(_("The requested discount does not exist."))
discounts = list(request.event.discounts.order_by("position"))
index = discounts.index(discount)
if index != 0 and up:
discounts[index - 1], discounts[index] = discounts[index], discounts[index - 1]
elif index != len(discounts) - 1 and not up:
discounts[index + 1], discounts[index] = discounts[index], discounts[index + 1]
for i, d in enumerate(discounts):
if d.position != i:
d.position = i
d.save()
messages.success(request, _('The order of discounts has been updated.'))
@event_permission_required("can_change_items")
@require_http_methods(["POST"])
def discount_move_up(request, organizer, event, discount):
discount_move(request, discount, up=True)
return redirect('control:event.items.discounts',
organizer=request.event.organizer.slug,
event=request.event.slug)
@event_permission_required("can_change_items")
@require_http_methods(["POST"])
def discount_move_down(request, organizer, event, discount):
discount_move(request, discount, up=False)
return redirect('control:event.items.discounts',
organizer=request.event.organizer.slug,
event=request.event.slug)
@transaction.atomic
@event_permission_required("can_change_items")
@require_http_methods(["POST"])
def reorder_discounts(request, organizer, event):
try:
ids = json.loads(request.body.decode('utf-8'))['ids']
except (JSONDecodeError, KeyError, ValueError):
return HttpResponseBadRequest("expected JSON: {ids:[]}")
input_discounts = list(request.event.discounts.filter(id__in=[i for i in ids if i.isdigit()]))
if len(input_discounts) != len(ids):
raise Http404(_("Some of the provided object ids are invalid."))
if len(input_discounts) != request.event.discounts.count():
raise Http404(_("Not all discounts have been selected."))
for c in input_discounts:
pos = ids.index(str(c.pk))
if pos != c.position: # Save unneccessary UPDATE queries
c.position = pos
c.save(update_fields=['position'])
return HttpResponse()

View File

@@ -169,7 +169,7 @@ def reorder_items(request, organizer, event):
input_items = list(request.event.items.filter(id__in=[i for i in ids if i.isdigit()]))
if len(input_items) != len(ids):
raise Http404(_("Some of the provided item ids are invalid."))
raise Http404(_("Some of the provided object ids are invalid."))
item_categories = {i.category_id for i in input_items}
if len(item_categories) > 1:
@@ -178,7 +178,7 @@ def reorder_items(request, organizer, event):
# get first and only category
item_category = next(iter(item_categories))
if len(input_items) != request.event.items.filter(category=item_category).count():
raise Http404(_("Not all items have been selected."))
raise Http404(_("Not all objects have been selected."))
for i in input_items:
pos = ids.index(str(i.pk))
@@ -372,10 +372,10 @@ def reorder_categories(request, organizer, event):
input_categories = list(request.event.categories.filter(id__in=[i for i in ids if i.isdigit()]))
if len(input_categories) != len(ids):
raise Http404(_("Some of the provided category ids are invalid."))
raise Http404(_("Some of the provided object ids are invalid."))
if len(input_categories) != request.event.categories.count():
raise Http404(_("Not all categories have been selected."))
raise Http404(_("Not all objects have been selected."))
for c in input_categories:
pos = ids.index(str(c.pk))
@@ -501,10 +501,10 @@ def reorder_questions(request, organizer, event):
input_questions = list(request.event.questions.filter(id__in=custom_question_ids))
if len(input_questions) != len(custom_question_ids):
raise Http404(_("Some of the provided question ids are invalid."))
raise Http404(_("Some of the provided object ids are invalid."))
if len(input_questions) != request.event.questions.count():
raise Http404(_("Not all questions have been selected."))
raise Http404(_("Not all objects have been selected."))
for q in input_questions:
pos = ids.index(str(q.pk))

View File

@@ -360,7 +360,8 @@ class OrderDetail(OrderView):
cartpos = queryset.order_by(
'item', 'variation'
).select_related(
'item', 'variation', 'addon_to', 'tax_rule', 'used_membership', 'used_membership__membership_type'
'item', 'variation', 'addon_to', 'tax_rule', 'used_membership', 'used_membership__membership_type',
'discount',
).prefetch_related(
'item__questions', 'issued_gift_cards',
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options').select_related('question')),