import copy from django import forms from django.core.exceptions import ValidationError from django.db.models import Q from django.utils.timezone import now from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from pretix.base.forms import I18nModelForm from pretix.base.models import Item, ItemVariation, Quota, Voucher from pretix.control.forms import SplitDateTimePickerWidget from pretix.control.signals import voucher_form_validation class VoucherForm(I18nModelForm): itemvar = forms.ChoiceField( label=_("Product"), help_text=_( "This product is added to the user's cart if the voucher is redeemed." ), required=True ) class Meta: model = Voucher localized_fields = '__all__' fields = [ 'code', 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment', 'max_usages', 'price_mode', 'subevent' ] field_classes = { 'valid_until': forms.SplitDateTimeField, } widgets = { 'valid_until': SplitDateTimePickerWidget(), } def __init__(self, *args, **kwargs): instance = kwargs.get('instance') initial = kwargs.get('initial') if instance: self.initial_instance_data = copy.copy(instance) try: if instance.variation: initial['itemvar'] = '%d-%d' % (instance.item.pk, instance.variation.pk) elif instance.item: initial['itemvar'] = str(instance.item.pk) elif instance.quota: initial['itemvar'] = 'q-%d' % instance.quota.pk except Item.DoesNotExist: pass else: self.initial_instance_data = None super().__init__(*args, **kwargs) if instance.event.has_subevents: self.fields['subevent'].queryset = instance.event.subevents.all() elif 'subevent': del self.fields['subevent'] choices = [] for i in self.instance.event.items.prefetch_related('variations').all(): variations = list(i.variations.all()) if variations: choices.append((str(i.pk), _('{product} – Any variation').format(product=i.name))) for v in variations: choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i.name, v.value))) else: choices.append((str(i.pk), i.name)) for q in self.instance.event.quotas.all(): choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q))) self.fields['itemvar'].choices = choices def clean(self): data = super().clean() if not self._errors: itemid = quotaid = None iv = self.data.get('itemvar', '') if iv.startswith('q-'): quotaid = iv[2:] elif '-' in iv: itemid, varid = iv.split('-') else: itemid, varid = iv, None if itemid: self.instance.item = Item.objects.get(pk=itemid, event=self.instance.event) if varid: self.instance.variation = ItemVariation.objects.get(pk=varid, item=self.instance.item) else: self.instance.variation = None self.instance.quota = None if self.instance.item.category and self.instance.item.category.is_addon: raise ValidationError(_('It is currently not possible to create vouchers for add-on products.')) else: self.instance.quota = Quota.objects.get(pk=quotaid, event=self.instance.event) self.instance.item = None self.instance.variation = None if data.get('max_usages', 0) < self.instance.redeemed: raise ValidationError( _('This voucher has already been redeemed %(redeemed)s times. You cannot reduce the maximum number of ' 'usages below this number.'), params={ 'redeemed': self.instance.redeemed } ) if 'codes' in data: data['codes'] = [a.strip() for a in data.get('codes', '').strip().split("\n") if a] cnt = len(data['codes']) * data['max_usages'] else: cnt = data['max_usages'] if self.instance.event.has_subevents and data['block_quota'] and not data.get('subevent'): raise ValidationError(pgettext_lazy( 'subevent', 'If you want this voucher to block quota, you need to select a specific date.' )) if self._clean_quota_needs_checking(data): self._clean_quota_check(data, cnt) if 'code' in data and Voucher.objects.filter(Q(code=data['code']) & Q(event=self.instance.event) & ~Q(pk=self.instance.pk)).exists(): raise ValidationError(_('A voucher with this code already exists.')) voucher_form_validation.send(sender=self.instance.event, form=self, data=data) return data def _clean_quota_needs_checking(self, data): # We only need to check for quota on vouchers that are now blocking quota and haven't # before (or have blocked a different quota before) if data.get('block_quota', False): is_valid = data.get('valid_until') is None or data.get('valid_until') >= now() if not is_valid: # If the voucher is not valid, it won't block any quota return False if not self.instance.pk: # This is a new voucher return True if not self.initial_instance_data.block_quota: # Change from nonblocking to blocking return True if not self._clean_was_valid(): # This voucher has been expired and is now valid again and therefore blocks quota again return True if data.get('itemvar') != self.initial.get('itemvar'): # The voucher has been reassigned to a different item, variation or quota return True if data.get('subevent') != self.initial.get('subevent'): # The voucher has been reassigned to a different subevent return True return False def _clean_was_valid(self): return self.initial_instance_data.valid_until is None or self.initial_instance_data.valid_until >= now() def _clean_quota_get_ignored(self): quotas = set() if self.initial_instance_data and self.initial_instance_data.block_quota and self._clean_was_valid(): if self.initial_instance_data.quota: quotas.add(self.initial_instance_data.quota) elif self.initial_instance_data.variation: quotas |= set(self.initial_instance_data.variation.quotas.filter( subevent=self.initial_instance_data.subevent)) elif self.initial_instance_data.item: quotas |= set(self.initial_instance_data.item.quotas.filter( subevent=self.initial_instance_data.subevent)) return quotas def _clean_quota_check(self, data, cnt): old_quotas = self._clean_quota_get_ignored() if self.instance.quota: if self.instance.quota in old_quotas: return else: avail = self.instance.quota.availability() elif self.instance.item and self.instance.item.has_variations and not self.instance.variation: raise ValidationError(_('You can only block quota if you specify a specific product variation. ' 'Otherwise it might be unclear which quotas to block.')) elif self.instance.item and self.instance.variation: avail = self.instance.variation.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent')) elif self.instance.item and not self.instance.item.has_variations: avail = self.instance.item.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent')) else: raise ValidationError(_('You need to specify either a quota or a product.')) if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < cnt): raise ValidationError(_('You cannot create a voucher that blocks quota as the selected product or ' 'quota is currently sold out or completely reserved.')) def save(self, commit=True): super().save(commit) return ['item'] class VoucherBulkForm(VoucherForm): codes = forms.CharField( widget=forms.Textarea, label=_("Codes"), help_text=_( "Add one voucher code per line. We suggest that you copy this list and save it into a file." ), required=True ) class Meta: model = Voucher localized_fields = '__all__' fields = [ 'valid_until', 'block_quota', 'allow_ignore_quota', 'value', 'tag', 'comment', 'max_usages', 'price_mode', 'subevent' ] field_classes = { 'valid_until': forms.SplitDateTimeField, } widgets = { 'valid_until': SplitDateTimePickerWidget(), } labels = { 'max_usages': _('Maximum usages per voucher') } help_texts = { 'max_usages': _('Number of times times EACH of these vouchers can be redeemed.') } def clean(self): data = super().clean() if Voucher.objects.filter(code__in=data['codes'], event=self.instance.event).exists(): raise ValidationError(_('A voucher with one of this codes already exists.')) return data def save(self, event, *args, **kwargs): objs = [] for code in self.cleaned_data['codes']: obj = copy.copy(self.instance) obj.event = event obj.code = code data = dict(self.cleaned_data) data['code'] = code data['bulk'] = True del data['codes'] obj.save() objs.append(obj) return objs