forked from CGM_Public/pretix_original
Discounts (#2510)
This commit is contained in:
109
src/pretix/control/forms/discounts.py
Normal file
109
src/pretix/control/forms/discounts.py
Normal 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
|
||||
@@ -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.'),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
147
src/pretix/control/templates/pretixcontrol/items/discounts.html
Normal file
147
src/pretix/control/templates/pretixcontrol/items/discounts.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
269
src/pretix/control/views/discounts.py
Normal file
269
src/pretix/control/views/discounts.py
Normal 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()
|
||||
@@ -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))
|
||||
|
||||
@@ -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')),
|
||||
|
||||
Reference in New Issue
Block a user