untested draft

This commit is contained in:
Raphael Michel
2026-04-17 02:09:57 +02:00
parent 4867afc503
commit adf167e611
5 changed files with 386 additions and 58 deletions

View File

@@ -33,16 +33,18 @@
# License for the specific language governing permissions and limitations under the License. # License for the specific language governing permissions and limitations under the License.
import csv import csv
from collections import namedtuple from collections import namedtuple, Counter
from io import StringIO from io import StringIO
from django import forms from django import forms
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import EmailValidator from django.core.validators import EmailValidator
from django.db.models import Max, Sum, Count, Q, F
from django.db.models.functions import Upper from django.db.models.functions import Upper
from django.forms.utils import ErrorDict from django.forms.utils import ErrorDict
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelChoiceField from django_scopes.forms import SafeModelChoiceField
from pretix.base.email import get_available_placeholders from pretix.base.email import get_available_placeholders
@@ -51,7 +53,9 @@ from pretix.base.forms import (
) )
from pretix.base.forms.widgets import format_placeholders_help_text from pretix.base.forms.widgets import format_placeholders_help_text
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import Item, Voucher from pretix.base.models import Item, Voucher, Quota, SubEvent, ItemVariation
from pretix.base.services.locking import lock_objects
from pretix.base.services.quotas import QuotaAvailability
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
from pretix.control.forms.widgets import Select2, Select2ItemVarQuota from pretix.control.forms.widgets import Select2, Select2ItemVarQuota
from pretix.control.signals import voucher_form_validation from pretix.control.signals import voucher_form_validation
@@ -107,14 +111,14 @@ class VoucherForm(I18nModelForm):
pass pass
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if instance.event.has_subevents: if self.event.has_subevents:
self.fields['subevent'].queryset = instance.event.subevents.all() self.fields['subevent'].queryset = self.event.subevents.all()
self.fields['subevent'].widget = Select2( self.fields['subevent'].widget = Select2(
attrs={ attrs={
'data-model-select2': 'event', 'data-model-select2': 'event',
'data-select2-url': reverse('control:event.subevents.select2', kwargs={ 'data-select2-url': reverse('control:event.subevents.select2', kwargs={
'event': instance.event.slug, 'event': self.event.slug,
'organizer': instance.event.organizer.slug, 'organizer': self.event.organizer.slug,
}), }),
} }
) )
@@ -127,15 +131,15 @@ class VoucherForm(I18nModelForm):
if 'itemvar' in initial or (self.data and 'itemvar' in self.data): if 'itemvar' in initial or (self.data and 'itemvar' in self.data):
iv = self.data.get('itemvar') or initial.get('itemvar', '') iv = self.data.get('itemvar') or initial.get('itemvar', '')
if iv.startswith('q-'): if iv.startswith('q-'):
q = self.instance.event.quotas.get(pk=iv[2:]) q = self.event.quotas.get(pk=iv[2:])
choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q))) choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q)))
elif '-' in iv: elif '-' in iv:
itemid, varid = iv.split('-') itemid, varid = iv.split('-')
i = self.instance.event.items.get(pk=itemid) i = self.event.items.get(pk=itemid)
v = i.variations.get(pk=varid) v = i.variations.get(pk=varid)
choices.append(('%d-%d' % (i.pk, v.pk), '%s %s' % (str(i), v.value))) choices.append(('%d-%d' % (i.pk, v.pk), '%s %s' % (str(i), v.value)))
elif iv: elif iv:
i = self.instance.event.items.get(pk=iv) i = self.event.items.get(pk=iv)
if i.variations.exists(): if i.variations.exists():
choices.append((str(i.pk), _('{product} Any variation').format(product=i))) choices.append((str(i.pk), _('{product} Any variation').format(product=i)))
else: else:
@@ -146,8 +150,8 @@ class VoucherForm(I18nModelForm):
attrs={ attrs={
'data-model-select2': 'generic', 'data-model-select2': 'generic',
'data-select2-url': reverse('control:event.vouchers.itemselect2', kwargs={ 'data-select2-url': reverse('control:event.vouchers.itemselect2', kwargs={
'event': instance.event.slug, 'event': self.event.slug,
'organizer': instance.event.organizer.slug, 'organizer': self.event.organizer.slug,
}), }),
'data-placeholder': _('All products') 'data-placeholder': _('All products')
} }
@@ -155,7 +159,7 @@ class VoucherForm(I18nModelForm):
self.fields['itemvar'].required = False self.fields['itemvar'].required = False
self.fields['itemvar'].widget.choices = self.fields['itemvar'].choices self.fields['itemvar'].widget.choices = self.fields['itemvar'].choices
if self.instance.event.seating_plan or self.instance.event.subevents.filter(seating_plan__isnull=False).exists(): if self.event.seating_plan or self.event.subevents.filter(seating_plan__isnull=False).exists():
self.fields['seat'] = forms.CharField( self.fields['seat'] = forms.CharField(
label=_("Specific seat ID"), label=_("Specific seat ID"),
max_length=255, max_length=255,
@@ -182,14 +186,14 @@ class VoucherForm(I18nModelForm):
itemid, varid = None, None itemid, varid = None, None
if itemid: if itemid:
self.instance.item = self.instance.event.items.get(pk=itemid) self.instance.item = self.event.items.get(pk=itemid)
if varid: if varid:
self.instance.variation = self.instance.item.variations.get(pk=varid) self.instance.variation = self.instance.item.variations.get(pk=varid)
else: else:
self.instance.variation = None self.instance.variation = None
self.instance.quota = None self.instance.quota = None
elif quotaid: elif quotaid:
self.instance.quota = self.instance.event.quotas.get(pk=quotaid) self.instance.quota = self.event.quotas.get(pk=quotaid)
self.instance.item = None self.instance.item = None
self.instance.variation = None self.instance.variation = None
else: else:
@@ -210,7 +214,7 @@ class VoucherForm(I18nModelForm):
try: try:
Voucher.clean_item_properties( Voucher.clean_item_properties(
data, self.instance.event, data, self.event,
self.instance.quota, self.instance.item, self.instance.variation, self.instance.quota, self.instance.item, self.instance.variation,
seats_given=data.get('seat') or data.get('seats'), seats_given=data.get('seat') or data.get('seats'),
block_quota=data.get('block_quota') block_quota=data.get('block_quota')
@@ -230,7 +234,7 @@ class VoucherForm(I18nModelForm):
try: try:
Voucher.clean_subevent( Voucher.clean_subevent(
data, self.instance.event data, self.event
) )
except ValidationError as e: except ValidationError as e:
raise ValidationError({"subevent": e.message}) raise ValidationError({"subevent": e.message})
@@ -246,19 +250,19 @@ class VoucherForm(I18nModelForm):
if check_quota: if check_quota:
Voucher.clean_quota_check( Voucher.clean_quota_check(
data, cnt, self.initial_instance_data, data, cnt, self.initial_instance_data,
self.instance.event, self.instance.quota, self.instance.item, self.instance.variation self.event, self.instance.quota, self.instance.item, self.instance.variation
) )
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk) Voucher.clean_voucher_code(data, self.event, self.instance.pk)
if 'seat' in self.fields: if 'seat' in self.fields:
if data.get('seat'): if data.get('seat'):
self.instance.seat = Voucher.clean_seat_id( self.instance.seat = Voucher.clean_seat_id(
data, self.instance.item, self.instance.quota, self.instance.event, self.instance.pk data, self.instance.item, self.instance.quota, self.event, self.instance.pk
) )
self.instance.item = self.instance.seat.product self.instance.item = self.instance.seat.product
else: else:
self.instance.seat = None self.instance.seat = None
voucher_form_validation.send(sender=self.instance.event, form=self, data=data) voucher_form_validation.send(sender=self.event, form=self, data=data)
return data return data
@@ -267,33 +271,253 @@ class VoucherForm(I18nModelForm):
class VoucherBulkEditForm(VoucherForm): class VoucherBulkEditForm(VoucherForm):
# TODO: clean quota changes!
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.mixed_values = kwargs.pop('mixed_values') self.mixed_values = kwargs.pop('mixed_values')
self.queryset = kwargs.pop('queryset') self.queryset = kwargs.pop('queryset')
super().__init__(**kwargs) super().__init__(**kwargs)
del self.fields["code"]
self.fields.pop("seat", None)
def clean(self):
# We skip the parent class because it's not suited for bulk editing and implement custom validation here.
# This does not validate everything we validate in VoucherForm. For example, we skip validation that one does
# not create a voucher for an add-on product to save on complexity. This is a UX validation only anyways, since
# one could first create the voucher and then make the product an add-on product. However, we need to validate
# everything that we don't want violated in the database.
data = super(VoucherForm, self).clean()
if self.prefix + "itemvar" in self.data.getlist('_bulk'):
try:
itemid = quotaid = None
iv = self.data.get('itemvar', '')
if iv.startswith('q-'):
quotaid = iv[2:]
elif '-' in iv:
itemid, varid = iv.split('-')
elif iv:
itemid, varid = iv, None
else:
itemid, varid = None, None
if itemid:
data["item"] = self.event.items.get(pk=itemid)
if varid:
data["variation"] = self.instance.item.variations.get(pk=varid)
else:
data["variation"] = None
data["quota"] = None
elif quotaid:
data["quota"] = self.event.quotas.get(pk=quotaid)
data["item"] = None
data["variation"] = None
else:
data["quota"] = None
data["item"] = None
data["variation"] = None
except ObjectDoesNotExist:
raise ValidationError(_("Invalid product selected."))
if self.prefix + "max_usages" in self.data.getlist('_bulk'):
max_redeemed = self.queryset.aggregate(m=Max("redeemed"))["m"]
if data["max_usages"] > max_redeemed:
raise ValidationError(_(
"You cannot reduce the maximum number of redemptions to %(max_usages)s, because at least one "
"of the selected vouchers has already been redeemed %(max_redeemed)s times."
) % {"max_usages": data["max_usages"], "max_redeemed": max_redeemed})
# Check diff on product and quota usage based on old groups of vouchers
if any(self.prefix + k in self.data.getlist('_bulk') for k in ("max_usages", "itemvar", "block_quota", "valid_until", "subevent")):
quota_diff = Counter()
current_vouchers = self.queryset.order_by().values(
"item", "variation", "quota", "block_quota", "valid_until", "subevent", "redeemed", "max_usages"
).annotate(c=Count("*"))
item_cache = {i.pk: i for i in Item.objects.filter(pk__in=[c["item"] for c in current_vouchers])}
var_cache = {v.pk: v for v in ItemVariation.objects.filter(pk__in=[c["variation"] for c in current_vouchers])}
quota_cache = {q.pk: q for q in Quota.objects.filter(pk__in=[c["quota"] for c in current_vouchers])}
subevent_cache = {s.pk: s for s in SubEvent.objects.filter(pk__in=[c["subevent"] for c in current_vouchers])}
for current in current_vouchers:
was_valid = current["valid_until"] is None or current["valid_until"] >= now()
# Get quotas that are currently used
if current["item"]:
current["item"] = item_cache[current["item"]]
if current["variation"]:
current["variation"] = var_cache[current["variation"]]
if current["quota"]:
current["quota"] = quota_cache[current["quota"]]
if current["subevent"]:
current["subevent"] = subevent_cache[current["subevent"]]
old_quotas = set()
if was_valid and current["block_quota"] and current["max_usages"] > current["redeemed"]:
if current["quota"]:
old_quotas.add(current["quota"])
elif current["variation"]:
old_quotas |= set(current["variation"].quotas.filter(subevent=current["subevent"]))
elif current["item"]:
if current["item"].has_variations:
old_quotas |= set(
Quota.objects.filter(pk__in=Quota.variations.through.objects.filter(
itemvariation__item=current["item"],
quota__subevent=current["subevent"],
).values('quota_id'))
)
else:
old_quotas |= set(current["item"].quotas.filter(subevent=current["subevent"]))
old_amount = max(current["max_usages"] - current["redeemed"], 0)
# Predict state after change
after_change = dict(current)
if self.prefix + "itemvar" in self.data.getlist('_bulk'):
after_change["item"] = data["item"]
after_change["variation"] = data["variation"]
after_change["quota"] = data["quota"]
if self.prefix + "subevent" in self.data.getlist('_bulk'):
after_change["subevent"] = data["subevent"]
if self.prefix + "max_usages" in self.data.getlist('_bulk'):
after_change["max_usages"] = data["max_usages"]
if self.prefix + "block_quota" in self.data.getlist('_bulk'):
after_change["block_quota"] = data["block_quota"]
if self.prefix + "valid_until" in self.data.getlist('_bulk'):
after_change["valid_until"] = data["valid_until"]
if self.prefix + "allow_ignore_quota" in self.data.getlist('_bulk'):
after_change["allow_ignore_quota"] = data["allow_ignore_quota"]
if after_change["quota"] and self.event.has_subevents and not after_change["subevent"]:
raise _("You cannot create a voucher that allows selection of a quota but has no date selected.")
if after_change["quota"] and after_change["subevent"] and after_change["quota"].subevent_id != after_change["subevent"].pk:
raise _("The selected quota does not match the selected subevent.")
if after_change["block_quota"] and self.event.has_subevents and not after_change["subevent"]:
raise ValidationError(
_('If you want this voucher to block quota, you need to select a specific date.'))
if after_change["block_quota"] and not after_change["item"] and not after_change["quota"]:
raise ValidationError(
_('You need to select a specific product or quota if this voucher should reserve '
'tickets.')
)
if after_change["allow_ignore_quota"]:
# todo: is this the most useful way to do this?
continue
will_be_valid = current["valid_until"] is None or current["valid_until"] >= now()
new_quotas = set()
if will_be_valid and after_change["block_quota"] and after_change["max_usages"] > current["redeemed"]:
if after_change["quota"]:
new_quotas.add(after_change["quota"])
elif after_change["variation"]:
new_quotas |= set(after_change["variation"].quotas.filter(subevent=after_change["subevent"]))
elif after_change["item"]:
if after_change["item"].has_variations:
new_quotas |= set(
Quota.objects.filter(pk__in=Quota.variations.through.objects.filter(
itemvariation__item=after_change["item"],
quota__subevent=after_change["subevent"],
).values('quota_id'))
)
else:
new_quotas |= set(after_change["item"].quotas.filter(subevent=after_change["subevent"]))
new_amount = max(current["max_usages"] - current["redeemed"], 0)
if new_quotas != old_quotas or new_amount != old_amount:
for q in old_quotas:
quota_diff[q] -= old_amount
for q in new_quotas:
quota_diff[q] += new_quotas
if any(v > 0 for q, v in quota_diff.items()):
lock_objects([q for q, in quota_diff.items() if q.size is not None and v > 0], shared_lock_objects=[self.event])
qa = QuotaAvailability(count_waitinglist=False)
qa.queue(*(q for q, v in quota_diff.items() if v > 0))
qa.compute()
if any(r[0] != Quota.AVAILABILITY_OK or (r[1] is not None and r[1] < cnt) for r in qa.results.values()):
raise ValidationError(_(
'There is no sufficient quota available to perform this change.'
))
has_seat = self.queryset.filter(seat__isnull=False).exists()
if has_seat:
if self.prefix + "max_usages" in self.data.getlist('_bulk'):
raise ValidationError(_(
'Changing the maximum number of usages in bulk is not supported if any of the selected vouchers '
'is assigned a seat.'
))
if self.prefix + "subevent" in self.data.getlist('_bulk'):
raise ValidationError(pgettext_lazy(
'subevent',
'Changing the date in bulk is not supported if any of the selected vouchers '
'is assigned a seat.'
))
if self.prefix + "itemvar" in self.data.getlist('_bulk') and data["quota"]:
raise ValidationError(_(
'Changing the product to a quota is not supported if any of the selected vouchers '
'is assigned a seat.'
))
if self.prefix + "valid_until" in self.data.getlist('_bulk'):
if data["valid_until"] is None or data["valid_until"] >= now():
currently_not_blocked_seats = self.queryset.filter(
seat__isnull=False,
max_usages__gt=F("redeemed"),
valid_until__lt=now(),
)
if self.event.has_subevents:
conflicts = currently_not_blocked_seats.exclude(
seat_id__in=self.event.free_seats.values(pk)
)
if conflicts:
raise ValidationError(_(
'This change cannot be completed because not all assigned seats of the vouchers are '
'still available'
))
else:
subevents = self.event.subevents.filter(pk__in=currently_not_blocked_seats.values_list("subevent"))
for se in subevents:
conflicts = currently_not_blocked_seats.filter(
subevent=se
).exclude(
seat_id__in=se.free_seats.values(pk)
)
if conflicts:
raise ValidationError(_(
'This change cannot be completed because not all assigned seats of the vouchers are '
'still available'
))
return data
def save(self, commit=True): def save(self, commit=True):
objs = list(self.queryset) objs = list(self.queryset)
fields = set() fields = set()
check_map = {
'price_mode': '__price',
'value': '__price',
}
for k in self.fields: for k in self.fields:
cb_val = self.prefix + k cb_val = self.prefix + check_map.get(k, k)
if cb_val not in self.data.getlist('_bulk'): if cb_val not in self.data.getlist('_bulk'):
continue continue
fields.add(k) if k == 'itemvar':
fields.add("item")
fields.add("variation")
fields.add("quota")
else:
fields.add(k)
for obj in objs: for obj in objs:
if k == 'itemvar': if k == 'itemvar':
selected_items = set(list(self.event.items.filter(id__in=[ obj.item = self.cleaned_data["item"]
i.split('-')[0] for i in self.cleaned_data['itemvars'] obj.variation = self.cleaned_data["variation"]
]))) obj.quota = self.cleaned_data["quota"]
selected_variations = list(ItemVariation.objects.filter(item__event=self.event, id__in=[
i.split('-')[1] for i in self.cleaned_data['itemvars'] if '-' in i
]))
obj.items.set(selected_items)
obj.variations.set(selected_variations)
else: else:
setattr(obj, k, self.cleaned_data[k]) setattr(obj, k, self.cleaned_data[k])
@@ -308,6 +532,9 @@ class VoucherBulkEditForm(VoucherForm):
return return
super().full_clean() super().full_clean()
def _post_clean(self):
pass # skip model-level clean
class VoucherBulkForm(VoucherForm): class VoucherBulkForm(VoucherForm):
codes = forms.CharField( codes = forms.CharField(

View File

@@ -0,0 +1,87 @@
{% extends "pretixcontrol/items/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load eventsignal %}
{% load eventurl %}
{% block title %}{% trans "Change multiple vouchers" %}{% endblock %}
{% block inside %}
<h1>
{% trans "Change multiple vouchers" %}
<small>
{% blocktrans trimmed with number=vouchers.count %}
{{ number }} selected
{% endblocktrans %}
</small>
</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
<legend>{% trans "Voucher details" %}</legend>
{% bootstrap_field form.max_usages layout="bulkedit" %}
{% bootstrap_field form.valid_until layout="bulkedit" %}
{% bootstrap_field form.itemvar layout="bulkedit" %}
<div class="bulk-edit-field-group">
<label class="field-toggle">
<input type="checkbox" name="_bulk" value="{{ form.prefix }}__price" {% if form.prefix|add:"__price" in bulk_selected %}checked{% endif %}>
{% trans "change" context "form_bulk" %}
</label>
<div class="field-content">
<div class="form-group">
<label class="col-md-3 control-label" for="id_tag">{% trans "Price effect" %}</label>
<div class="col-md-5">
{% bootstrap_field form.price_mode show_label=False form_group_class="" %}
</div>
<div class="col-md-4">
{% bootstrap_field form.value show_label=False form_group_class="" %}
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<div class="controls">
<div class="alert alert-info">
{% blocktrans trimmed %}
If you choose "any product" for a specific quota and choose to reserve quota for this
voucher above, the product can still be unavailable to the voucher holder if another quota
associated with the product is sold out!
{% endblocktrans %}
</div>
</div>
</div>
</div>
{% if form.subevent %}
{% bootstrap_field form.subevent layout="bulkedit" %}
{% endif %}
</fieldset>
<fieldset>
<legend>{% trans "Advanced settings" %}</legend>
{% bootstrap_field form.block_quota layout="bulkedit" %}
{% bootstrap_field form.allow_ignore_quota layout="bulkedit" %}
{% bootstrap_field form.min_usages layout="bulkedit" %}
{% bootstrap_field form.budget addon_after=request.event.currency layout="bulkedit" %}
{% bootstrap_field form.tag layout="bulkedit" %}
{% bootstrap_field form.comment layout="bulkedit" %}
{% bootstrap_field form.show_hidden_items layout="bulkedit" %}
{% bootstrap_field form.all_addons_included layout="bulkedit" %}
{% bootstrap_field form.all_bundles_included layout="bulkedit" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
{% if voucher.pk %}
<div class="pull-left">
<a href="{% url "control:event.voucher.delete" organizer=request.organizer.slug event=request.event.slug voucher=voucher.pk %}"
class="btn btn-danger btn-lg">
<span class="fa fa-trash"></span>
{% trans "Delete voucher" %}
</a>
</div>
{% endif %}
</div>
</form>
{% endblock %}

View File

@@ -224,7 +224,7 @@
{% trans "Delete selected" %} {% trans "Delete selected" %}
</button> </button>
<button type="submit" class="btn btn-primary btn-save" name="action" value="edit" <button type="submit" class="btn btn-primary btn-save" name="action" value="edit"
formaction="{% url "control:event.subevents.bulkedit" organizer=request.event.organizer.slug event=request.event.slug %}"> formaction="{% url "control:event.vouchers.bulkedit" organizer=request.event.organizer.slug event=request.event.slug %}">
<i class="fa fa-edit"></i>{% trans "Edit selected" %} <i class="fa fa-edit"></i>{% trans "Edit selected" %}
</button> </button>
</div> </div>

View File

@@ -379,7 +379,7 @@ urlpatterns = [
re_path(r'^vouchers/bulk_add$', vouchers.VoucherBulkCreate.as_view(), name='event.vouchers.bulk'), re_path(r'^vouchers/bulk_add$', vouchers.VoucherBulkCreate.as_view(), name='event.vouchers.bulk'),
re_path(r'^vouchers/bulk_add/mail_preview$', vouchers.VoucherBulkMailPreview.as_view(), name='event.vouchers.bulk.mail_preview'), re_path(r'^vouchers/bulk_add/mail_preview$', vouchers.VoucherBulkMailPreview.as_view(), name='event.vouchers.bulk.mail_preview'),
re_path(r'^vouchers/bulk_action$', vouchers.VoucherBulkAction.as_view(), name='event.vouchers.bulkaction'), re_path(r'^vouchers/bulk_action$', vouchers.VoucherBulkAction.as_view(), name='event.vouchers.bulkaction'),
#re_path(r'^vouchers/bulk_edit$', vouchers.VoucherBulkUpdateView.as_view(), name='event.vouchers.bulkedit'), re_path(r'^vouchers/bulk_edit$', vouchers.VoucherBulkUpdateView.as_view(), name='event.vouchers.bulkedit'),
re_path(r'^vouchers/import/$', modelimport.VoucherImportView.as_view(), name='event.vouchers.import'), re_path(r'^vouchers/import/$', modelimport.VoucherImportView.as_view(), name='event.vouchers.import'),
re_path(r'^vouchers/import/(?P<file>[^/]+)/$', modelimport.VoucherProcessView.as_view(), name='event.vouchers.import.process'), re_path(r'^vouchers/import/(?P<file>[^/]+)/$', modelimport.VoucherProcessView.as_view(), name='event.vouchers.import.process'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(), re_path(r'^orders/(?P<code>[0-9A-Z]+)/transition$', orders.OrderTransition.as_view(),

View File

@@ -42,7 +42,7 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
from django.db import connection, transaction from django.db import connection, transaction
from django.db.models import Exists, OuterRef, Sum, Subquery from django.db.models import Exists, OuterRef, Sum, Subquery, Count
from django.http import ( from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
JsonResponse, JsonResponse,
@@ -70,7 +70,7 @@ from pretix.base.services.vouchers import vouchers_send
from pretix.base.templatetags.rich_text import markdown_compile_email from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.views.tasks import AsyncFormView from pretix.base.views.tasks import AsyncFormView
from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm from pretix.control.forms.filter import VoucherFilterForm, VoucherTagFilterForm
from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm, VoucherBulkEditForm
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import voucher_form_class from pretix.control.signals import voucher_form_class
from pretix.control.views import PaginationMixin from pretix.control.views import PaginationMixin
@@ -311,6 +311,12 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
f.disabled = True f.disabled = True
return form return form
def get_form_kwargs(self):
return {
**super().get_form_kwargs(),
"event": self.request.event,
}
def get_object(self, queryset=None) -> VoucherForm: def get_object(self, queryset=None) -> VoucherForm:
url = resolve(self.request.path_info) url = resolve(self.request.path_info)
try: try:
@@ -669,7 +675,7 @@ class VoucherBulkAction(VoucherQueryMixin, EventPermissionRequiredMixin, View):
class VoucherBulkUpdateView(VoucherQueryMixin, EventPermissionRequiredMixin, FormView): class VoucherBulkUpdateView(VoucherQueryMixin, EventPermissionRequiredMixin, FormView):
template_name = 'pretixcontrol/vouchers/bulk_edit.html' template_name = 'pretixcontrol/vouchers/bulk_edit.html'
permission = 'event.vouchers:write' permission = 'event.vouchers:write'
context_object_name = 'voucher' context_object_name = 'vouchers'
form_class = VoucherBulkEditForm form_class = VoucherBulkEditForm
def get_queryset(self): def get_queryset(self):
@@ -690,28 +696,36 @@ class VoucherBulkUpdateView(VoucherQueryMixin, EventPermissionRequiredMixin, For
mixed_values = set() mixed_values = set()
qs = self.get_queryset().annotate() qs = self.get_queryset().annotate()
fields = { fields = (
'name': 'name', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment', 'max_usages',
'size': 'size', 'min_usages', 'price_mode', 'subevent', 'show_hidden_items', 'all_addons_included', 'all_bundles_included',
'subevent': 'subevent', 'budget',
'close_when_sold_out': 'close_when_sold_out', )
'release_after_exit': 'release_after_exit', for f in fields:
'ignore_for_event_availability': 'ignore_for_event_availability',
}
for k, f in fields.items():
existing_values = list(qs.order_by(f).values(f).annotate(c=Count('*'))) existing_values = list(qs.order_by(f).values(f).annotate(c=Count('*')))
if len(existing_values) == 1: if len(existing_values) == 1:
initial[k] = existing_values[0][f] initial[f] = existing_values[0][f]
elif len(existing_values) > 1: elif len(existing_values) > 1:
mixed_values.add(k) mixed_values.add(f)
initial[k] = None if f == "max_usages":
initial[f] = 1
else:
initial[f] = None
item_values = list(qs.order_by("items_list").values("items_list").annotate(c=Count('*'))) existing_values = list(qs.order_by("item", "variation", "quota").values("item", "variation", "quota").annotate(c=Count('*')))
var_values = list(qs.order_by("vars_list").values("vars_list").annotate(c=Count('*'))) if len(existing_values) == 1:
if len(item_values) > 1 or len(var_values) > 1: i = existing_values[0]
mixed_values.add("itemvars") if i["quota"]:
else: initial["itemvar"] = f'q-{i["quota"]}'
initial["itemvars"] = [iv for iv in (item_values[0]["items_list"] or "").split(",") + (var_values[0]["vars_list"] or "").split(",") if iv] elif i["variation"]:
initial["itemvar"] = f'{i["item"]}-{i["variation"]}'
elif i["item"]:
initial["itemvar"] = f'{i["item"]}'
else:
initial["itemvar"] = None
elif len(existing_values) > 1:
mixed_values.add("itemvar")
initial["itemvar"] = None
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.event kwargs['event'] = self.request.event
@@ -725,7 +739,7 @@ class VoucherBulkUpdateView(VoucherQueryMixin, EventPermissionRequiredMixin, For
return kwargs return kwargs
def get_success_url(self): def get_success_url(self):
return reverse('control:event.items.quotas', kwargs={ return reverse('control:event.vouchers', kwargs={
'organizer': self.request.event.organizer.slug, 'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug, 'event': self.request.event.slug,
}) })
@@ -754,7 +768,7 @@ class VoucherBulkUpdateView(VoucherQueryMixin, EventPermissionRequiredMixin, For
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['quotas'] = self.get_queryset() ctx['vouchers'] = self.get_queryset()
ctx['bulk_selected'] = self.request.POST.getlist("_bulk") ctx['bulk_selected'] = self.request.POST.getlist("_bulk")
return ctx return ctx