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

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