Fix #1567 -- Per-subevent availability of items (#2040)

This commit is contained in:
Raphael Michel
2021-04-29 15:34:58 +02:00
committed by GitHub
parent 6447201f9f
commit 4acf660906
18 changed files with 513 additions and 48 deletions

View File

@@ -423,13 +423,13 @@ class CloneEventSerializer(EventSerializer):
class SubEventItemSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEventItem
fields = ('item', 'price', 'disabled')
fields = ('item', 'price', 'disabled', 'available_from', 'available_until')
class SubEventItemVariationSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEventItemVariation
fields = ('variation', 'price', 'disabled')
fields = ('variation', 'price', 'disabled', 'available_from', 'available_until')
class SubEventSerializer(I18nAwareModelSerializer):

View File

@@ -0,0 +1,33 @@
# Generated by Django 3.0.13 on 2021-04-23 08:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0182_question_valid_file_portrait'),
]
operations = [
migrations.AddField(
model_name='subeventitem',
name='available_from',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='subeventitem',
name='available_until',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='subeventitemvariation',
name='available_from',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='subeventitemvariation',
name='available_until',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -50,6 +50,7 @@ from django.core.validators import (
)
from django.db import models
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery, Value
from django.db.models.functions import Coalesce
from django.template.defaultfilters import date as _date
from django.urls import reverse
from django.utils.crypto import get_random_string
@@ -280,6 +281,14 @@ class EventMixin:
vars_reserved = set()
items_gone = set()
vars_gone = set()
items_disabled = set()
vars_disabled = set()
if hasattr(self, 'disabled_items'): # SubEventItem
items_disabled = set(self.disabled_items.split(","))
if hasattr(self, 'disabled_vars'): # SubEventItemVariation
vars_disabled = set(self.disabled_vars.split(","))
r = getattr(self, '_quota_cache', {})
for q in self.active_quotas:
@@ -300,8 +309,19 @@ class EventMixin:
items_gone.update(q.active_items.split(","))
if q.active_variations:
vars_gone.update(q.active_variations.split(","))
if not self.active_quotas:
items_available -= items_disabled
items_reserved -= items_disabled
items_gone -= items_disabled
vars_available -= vars_disabled
vars_reserved -= vars_disabled
vars_gone -= vars_gone
if not self.active_quotas or (
not items_available and not items_reserved and not items_gone and not vars_gone and not vars_available and not vars_reserved
):
return None
if items_available - items_reserved - items_gone or vars_available - vars_reserved - vars_gone:
return Quota.AVAILABILITY_OK
if items_reserved - items_gone or vars_reserved - vars_gone:
@@ -1227,6 +1247,36 @@ class SubEvent(EventMixin, LoggedModel):
distance_only_within_row=self.settings.seating_distance_within_row)
return qs_annotated
@classmethod
def annotated(cls, qs, channel='web'):
from .items import SubEventItem, SubEventItemVariation
qs = super().annotated(qs, channel)
qs = qs.annotate(
disabled_items=Coalesce(
Subquery(
SubEventItem.objects.filter(
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
subevent=OuterRef('pk'),
).order_by().values('subevent').annotate(items=GroupConcat('item_id', delimiter=',')).values('items'),
output_field=models.TextField(),
),
Value('')
),
disabled_vars=Coalesce(
Subquery(
SubEventItemVariation.objects.filter(
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
subevent=OuterRef('pk'),
).order_by().values('subevent').annotate(items=GroupConcat('variation_id', delimiter=',')).values('items'),
output_field=models.TextField(),
),
Value('')
)
)
return qs
@cached_property
def settings(self):
return self.event.settings

View File

@@ -151,11 +151,29 @@ class SubEventItem(models.Model):
:type item: Item
:param price: The modified price (or ``None`` for the original price)
:type price: Decimal
:param disabled: Disable the product for this subevent
:type disabled: bool
:param available_until: The date until when the product is on sale
:type available_until: datetime
:param available_from: The date this product goes on sale
:type available_from: datetime
:param available_until: The date until when the product is on sale
:type available_until: datetime
"""
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
item = models.ForeignKey('Item', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
disabled = models.BooleanField(default=False, verbose_name=_('Disable product for this date'))
available_from = models.DateTimeField(
verbose_name=_("Available from"),
null=True, blank=True,
help_text=_('This product will not be sold before the given date.')
)
available_until = models.DateTimeField(
verbose_name=_("Available until"),
null=True, blank=True,
help_text=_('This product will not be sold after the given date.')
)
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
@@ -167,6 +185,16 @@ class SubEventItem(models.Model):
if self.subevent:
self.subevent.event.cache.clear()
def is_available(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or now()
if self.disabled:
return False
if self.available_from and self.available_from > now_dt:
return False
if self.available_until and self.available_until < now_dt:
return False
return True
class SubEventItemVariation(models.Model):
"""
@@ -179,11 +207,29 @@ class SubEventItemVariation(models.Model):
:type variation: ItemVariation
:param price: The modified price (or ``None`` for the original price)
:type price: Decimal
:param disabled: Disable the product for this subevent
:type disabled: bool
:param available_until: The date until when the product is on sale
:type available_until: datetime
:param available_from: The date this product goes on sale
:type available_from: datetime
:param available_until: The date until when the product is on sale
:type available_until: datetime
"""
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
variation = models.ForeignKey('ItemVariation', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
disabled = models.BooleanField(default=False)
disabled = models.BooleanField(default=False, verbose_name=_('Disable product for this date'))
available_from = models.DateTimeField(
verbose_name=_("Available from"),
null=True, blank=True,
help_text=_('This product will not be sold before the given date.')
)
available_until = models.DateTimeField(
verbose_name=_("Available until"),
null=True, blank=True,
help_text=_('This product will not be sold after the given date.')
)
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
@@ -195,6 +241,16 @@ class SubEventItemVariation(models.Model):
if self.subevent:
self.subevent.event.cache.clear()
def is_available(self, now_dt: datetime=None) -> bool:
now_dt = now_dt or now()
if self.disabled:
return False
if self.available_from and self.available_from > now_dt:
return False
if self.available_until and self.available_until < now_dt:
return False
return True
def filter_available(qs, channel='web', voucher=None, allow_addons=False):
q = (

View File

@@ -292,10 +292,11 @@ class CartManager:
if self._sales_channel not in op.item.sales_channels:
raise CartError(error_messages['unavailable'])
if op.subevent and op.item.pk in op.subevent.item_overrides and op.subevent.item_overrides[op.item.pk].disabled:
if op.subevent and op.item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():
raise CartError(error_messages['not_for_sale'])
if op.subevent and op.variation and op.variation.pk in op.subevent.var_overrides and op.subevent.var_overrides[op.variation.pk].disabled:
if op.subevent and op.variation and op.variation.pk in op.subevent.var_overrides and \
not op.subevent.var_overrides[op.variation.pk].is_available():
raise CartError(error_messages['not_for_sale'])
if op.item.has_variations and not op.variation:

View File

@@ -696,12 +696,13 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
delete(cp)
continue
if cp.subevent and cp.item.pk in cp.subevent.item_overrides and cp.subevent.item_overrides[cp.item.pk].disabled:
if cp.subevent and cp.item.pk in cp.subevent.item_overrides and not cp.subevent.item_overrides[cp.item.pk].is_available(now_dt):
err = err or error_messages['unavailable']
delete(cp)
continue
if cp.subevent and cp.variation and cp.variation.pk in cp.subevent.var_overrides and cp.subevent.var_overrides[cp.variation.pk].disabled:
if cp.subevent and cp.variation and cp.variation.pk in cp.subevent.var_overrides and \
not cp.subevent.var_overrides[cp.variation.pk].is_available(now_dt):
err = err or error_messages['unavailable']
delete(cp)
continue

View File

@@ -166,6 +166,56 @@ def timeline_for_event(event, subevent=None):
})
))
if subevent:
for sei in subevent.item_overrides.values():
if sei.available_from:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=sei.available_from,
description=pgettext_lazy('timeline', 'Product "{name}" becomes available').format(name=str(sei.item)),
edit_url=reverse('control:event.subevent', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'subevent': subevent.pk,
})
))
if sei.available_until:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=sei.available_until,
description=pgettext_lazy('timeline', 'Product "{name}" becomes unavailable').format(name=str(sei.item)),
edit_url=reverse('control:event.subevent', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'subevent': subevent.pk,
})
))
for sei in subevent.var_overrides.values():
if sei.available_from:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=sei.available_from,
description=pgettext_lazy('timeline', 'Product "{name}" becomes available').format(
name=str(sei.variation.item) + ' ' + str(sei.variation)),
edit_url=reverse('control:event.subevent', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'subevent': subevent.pk,
})
))
if sei.available_until:
tl.append(TimelineEvent(
event=event, subevent=subevent,
datetime=sei.available_until,
description=pgettext_lazy('timeline', 'Product "{name}" becomes unavailable').format(
name=str(sei.variation.item) + ' ' + str(sei.variation)),
edit_url=reverse('control:event.subevent', kwargs={
'event': event.slug,
'organizer': event.organizer.slug,
'subevent': subevent.pk,
})
))
for p in event.items.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)):
if p.available_from:
tl.append(TimelineEvent(

View File

@@ -35,8 +35,8 @@ from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm
from pretix.base.forms.widgets import DatePickerWidget, TimePickerWidget
from pretix.base.models.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import SubEventItem
from pretix.base.reldate import RelativeDateTimeField
from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.reldate import RelativeDateTimeField, RelativeDateWrapper
from pretix.base.templatetags.money import money_filter
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
from pretix.helpers.money import change_decimal_field
@@ -263,10 +263,16 @@ class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
class Meta:
model = SubEventItem
fields = ['price', 'disabled']
fields = ['price', 'disabled', 'available_from', 'available_until']
widgets = {
'available_from': SplitDateTimePickerWidget(),
'available_until': SplitDateTimePickerWidget(),
'price': forms.TextInput
}
field_classes = {
'available_from': SplitDateTimeField,
'available_until': SplitDateTimeField,
}
class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
@@ -276,11 +282,61 @@ class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelFor
self.fields['price'].label = '{} {}'.format(str(self.item), self.variation.value)
class Meta:
model = SubEventItem
fields = ['price', 'disabled']
model = SubEventItemVariation
fields = ['price', 'disabled', 'available_from', 'available_until']
widgets = {
'available_from': SplitDateTimePickerWidget(),
'available_until': SplitDateTimePickerWidget(),
'price': forms.TextInput
}
field_classes = {
'available_from': SplitDateTimeField,
'available_until': SplitDateTimeField,
}
class BulkSubEventItemForm(SubEventItemForm):
rel_available_from = RelativeDateTimeField(
label=_('Available from'),
required=False,
limit_choices=('date_from', 'date_to'),
)
rel_available_until = RelativeDateTimeField(
label=_('Available_until'),
required=False,
limit_choices=('date_from', 'date_to'),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
del self.fields['available_from']
del self.fields['available_until']
if self.instance and self.instance.available_from and 'rel_available_from' not in self.initial:
self.initial['rel_available_from'] = RelativeDateWrapper(self.instance.available_from)
if self.instance and self.instance.available_until and 'rel_available_until' not in self.initial:
self.initial['rel_available_until'] = RelativeDateWrapper(self.instance.available_until)
class BulkSubEventItemVariationForm(SubEventItemVariationForm):
rel_available_from = RelativeDateTimeField(
label=_('Available from'),
required=False,
limit_choices=('date_from', 'date_to'),
)
rel_available_until = RelativeDateTimeField(
label=_('Available_until'),
required=False,
limit_choices=('date_from', 'date_to'),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
del self.fields['available_from']
del self.fields['available_until']
if self.instance and self.instance.available_from and 'rel_available_from' not in self.initial:
self.initial['rel_available_from'] = RelativeDateWrapper(self.instance.available_from)
if self.instance and self.instance.available_until and 'rel_available_until' not in self.initial:
self.initial['rel_available_until'] = RelativeDateWrapper(self.instance.available_until)
class QuotaFormSet(I18nInlineFormSet):

View File

@@ -480,21 +480,34 @@
</p>
</fieldset>
<fieldset>
<legend>{% trans "Item prices" %}</legend>
<legend>{% trans "Product settings" %}</legend>
<p class="text-muted">
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
</p>
{% for f in itemvar_forms %}
<div class="form-group">
<div class="form-group subevent-itemvar-group">
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
{% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %}
<br>
<span class="optional">{% trans "Optional" %}</span>
</label>
<div class="col-md-6">
<div class="col-md-4">
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
</div>
<div class="col-md-3">
<div class="col-md-4">
<br>
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
</div>
</div>
<div class="form-group subevent-itemvar-group">
<div class="col-md-4 col-md-offset-3">
<label for="{{ f.rel_available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label><br>
{% bootstrap_field f.rel_available_from form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
<label for="{{ f.rel_available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label><br>
{% bootstrap_field f.rel_available_until form_group_class="" layout="inline" %}
</div>
</div>
{% endfor %}
</fieldset>
<fieldset>

View File

@@ -151,17 +151,29 @@
<fieldset>
<legend>{% trans "Item prices" %}</legend>
{% for f in itemvar_forms %}
<div class="form-group">
<div class="form-group subevent-itemvar-group">
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
{% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %}
</label>
<div class="col-md-6">
<div class="col-md-4">
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="bulkedit_inline" %}
</div>
<div class="col-md-3">
<div class="col-md-5">
<label class="text-muted">&nbsp;</label><br>
{% bootstrap_field f.disabled layout="bulkedit_inline" form_group_class="" %}
</div>
</div>
<div class="form-group subevent-itemvar-group">
<div class="col-md-4 col-md-offset-3">
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label><br>
{% bootstrap_field f.available_from form_group_class="" layout="bulkedit_inline" %}
</div>
<div class="col-md-5">
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label><br>
{% bootstrap_field f.available_until form_group_class="" layout="bulkedit_inline" %}
</div>
</div>
{% endfor %}
</fieldset>
<fieldset>

View File

@@ -122,21 +122,34 @@
</p>
</fieldset>
<fieldset>
<legend>{% trans "Item prices" %}</legend>
<legend>{% trans "Product settings" %}</legend>
<p class="text-muted">
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
</p>
{% for f in itemvar_forms %}
<div class="form-group">
<div class="form-group subevent-itemvar-group">
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
{% if f.variation %}{{ f.item }} {{ f.variation }}{% else %}{{ f.item }}{% endif %}
<br>
<span class="optional">{% trans "Optional" %}</span>
</label>
<div class="col-md-6">
<div class="col-md-4">
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
</div>
<div class="col-md-3">
<div class="col-md-4">
<br>
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
</div>
</div>
<div class="form-group subevent-itemvar-group">
<div class="col-md-4 col-md-offset-3">
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label><br>
{% bootstrap_field f.available_from form_group_class="" layout="inline" %}
</div>
<div class="col-md-4">
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label><br>
{% bootstrap_field f.available_until form_group_class="" layout="inline" %}
</div>
</div>
{% endfor %}
</fieldset>
<fieldset>

View File

@@ -68,9 +68,10 @@ from pretix.control.forms.checkin import SimpleCheckinListForm
from pretix.control.forms.filter import SubEventFilterForm
from pretix.control.forms.item import QuotaForm
from pretix.control.forms.subevents import (
CheckinListFormSet, QuotaFormSet, RRuleFormSet, SubEventBulkEditForm,
SubEventBulkForm, SubEventForm, SubEventItemForm,
SubEventItemVariationForm, SubEventMetaValueForm, TimeFormSet,
BulkSubEventItemForm, BulkSubEventItemVariationForm, CheckinListFormSet,
QuotaFormSet, RRuleFormSet, SubEventBulkEditForm, SubEventBulkForm,
SubEventForm, SubEventItemForm, SubEventItemVariationForm,
SubEventMetaValueForm, TimeFormSet,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import subevent_forms
@@ -193,6 +194,8 @@ class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
class SubEventEditorMixin(MetaDataEditorMixin):
meta_form = SubEventMetaValueForm
meta_model = SubEventMetaValue
itemformclass = SubEventItemForm
itemvarformclass = SubEventItemVariationForm
@cached_property
def plugin_forms(self):
@@ -391,11 +394,17 @@ class SubEventEditorMixin(MetaDataEditorMixin):
if self.copy_from:
se_item_instances = {
sei.item_id: SubEventItem(item=sei.item, price=sei.price, disabled=sei.disabled)
sei.item_id: SubEventItem(
item=sei.item, price=sei.price, disabled=sei.disabled,
available_from=sei.available_from, available_until=sei.available_until
)
for sei in SubEventItem.objects.filter(subevent=self.copy_from).select_related('item')
}
se_var_instances = {
sei.variation_id: SubEventItemVariation(variation=sei.variation, price=sei.price, disabled=sei.disabled)
sei.variation_id: SubEventItemVariation(
variation=sei.variation, price=sei.price, disabled=sei.disabled,
available_from=sei.available_from, available_until=sei.available_until
)
for sei in SubEventItemVariation.objects.filter(subevent=self.copy_from).select_related('variation')
}
@@ -404,7 +413,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
if i.has_variations:
for v in i.variations.all():
inst = se_var_instances.get(v.pk) or SubEventItemVariation(subevent=self.object, variation=v)
formlist.append(SubEventItemVariationForm(
formlist.append(self.itemvarformclass(
prefix='itemvar-{}'.format(v.pk),
item=i, variation=v,
instance=inst,
@@ -412,7 +421,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
))
else:
inst = se_item_instances.get(i.pk) or SubEventItem(subevent=self.object, item=i)
formlist.append(SubEventItemForm(
formlist.append(self.itemformclass(
prefix='item-{}'.format(i.pk),
item=i,
instance=inst,
@@ -641,6 +650,8 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea
permission = 'can_change_settings'
context_object_name = 'subevent'
form_class = SubEventBulkForm
itemformclass = BulkSubEventItemForm
itemvarformclass = BulkSubEventItemVariationForm
def is_valid(self, form):
return self.rrule_formset.is_valid() and self.time_formset.is_valid() and super().is_valid(form)
@@ -846,6 +857,18 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea
i = copy.copy(f.instance)
i.pk = None
i.subevent = se
i.available_from = (
f.cleaned_data['rel_available_from'].datetime(se)
if f.cleaned_data.get('rel_available_from')
else None
)
i.available_until = (
f.cleaned_data['rel_available_until'].datetime(se)
if f.cleaned_data.get('rel_available_until')
else None
)
if isinstance(i, SubEventItem):
to_save_items.append(i)
else:
@@ -962,16 +985,19 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
def cached_num(self):
return self.get_queryset().count()
itemformclass = SubEventItemForm
itemvarformclass = SubEventItemVariationForm
@cached_property
def itemvar_forms(self):
matches = defaultdict(list)
for sei in SubEventItem.objects.filter(
subevent__in=self.get_queryset()
).order_by().values('item', 'price', 'disabled').annotate(c=Count('*')):
).order_by().values('item', 'price', 'disabled', 'available_from', 'available_until').annotate(c=Count('*')):
matches['item', sei['item']].append(sei)
for sei in SubEventItemVariation.objects.filter(
subevent__in=self.get_queryset()
).order_by().values('variation', 'price', 'disabled').annotate(c=Count('*')):
).order_by().values('variation', 'price', 'disabled', 'available_from', 'available_until').annotate(c=Count('*')):
matches['variation', sei['variation']].append(sei)
total = self.cached_num
@@ -981,10 +1007,13 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
for v in i.variations.all():
m = matches['variation', v.pk]
if m and len(m) == 1 and m[0]['c'] == total:
inst = SubEventItemVariation(variation=v, disabled=m[0]['disabled'], price=m[0]['price'])
inst = SubEventItemVariation(
variation=v, disabled=m[0]['disabled'], price=m[0]['price'],
available_from=m[0]['available_from'], available_until=m[0]['available_until']
)
else:
inst = SubEventItemVariation(variation=v)
formlist.append(SubEventItemVariationForm(
formlist.append(self.itemvarformclass(
prefix='itemvar-{}'.format(v.pk),
item=i, variation=v,
instance=inst,
@@ -993,10 +1022,13 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
else:
m = matches['item', i.pk]
if m and len(m) == 1 and m[0]['c'] == total:
inst = SubEventItem(item=i, disabled=m[0]['disabled'], price=m[0]['price'])
inst = SubEventItem(
item=i, disabled=m[0]['disabled'], price=m[0]['price'],
available_from=m[0]['available_from'], available_until=m[0]['available_until']
)
else:
inst = SubEventItem(item=i)
formlist.append(SubEventItemForm(
formlist.append(self.itemformclass(
prefix='item-{}'.format(i.pk),
item=i,
instance=inst,
@@ -1405,12 +1437,16 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
u['price'] = f.cleaned_data.get('price')
if f.prefix + 'disabled' in self.request.POST.getlist('_bulk'):
u['disabled'] = f.cleaned_data.get('disabled')
if f.prefix + 'available_from' in self.request.POST.getlist('_bulk'):
u['available_from'] = f.cleaned_data.get('available_from')
if f.prefix + 'available_until' in self.request.POST.getlist('_bulk'):
u['available_until'] = f.cleaned_data.get('available_until')
if not u:
continue
if isinstance(f, SubEventItemForm):
if u.get('price') is None and not u.get('disabled'):
if u.get('price') is None and not u.get('disabled') and not u.get('available_from') and not u.get('available_until'):
SubEventItem.objects.filter(
subevent__in=self.get_queryset(),
item=f.instance.item,
@@ -1423,7 +1459,7 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
defaults=u
)
elif isinstance(f, SubEventItemVariationForm):
if u.get('price') is None and not u.get('disabled'):
if u.get('price') is None and not u.get('disabled') and not u.get('available_from') and not u.get('available_until'):
SubEventItemVariation.objects.filter(
subevent__in=self.get_queryset(),
variation=f.instance.variation,

View File

@@ -44,7 +44,7 @@ import pytz
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db.models import (
Count, Exists, IntegerField, OuterRef, Prefetch, Value,
Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value,
)
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
@@ -139,9 +139,9 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
queryset=ItemVariation.objects.using(settings.DATABASE_REPLICA).annotate(
subevent_disabled=Exists(
SubEventItemVariation.objects.filter(
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
variation_id=OuterRef('pk'),
subevent=subevent,
disabled=True,
)
),
).filter(
@@ -156,9 +156,9 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
has_variations=Count('variations'),
subevent_disabled=Exists(
SubEventItem.objects.filter(
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
item_id=OuterRef('pk'),
subevent=subevent,
disabled=True,
)
),
requires_seat=requires_seat,

View File

@@ -718,3 +718,17 @@ table td > .checkbox input[type="checkbox"] {
.pos-relative {
position: relative;
}
#subevent-bulk-create-form {
fieldset {
margin-bottom: 40px;
}
}
.subevent-itemvar-group {
label.control-label {
padding-top: 0;
}
label:not(.control-label) {
font-weight: normal;
}
}