class ItemCategory(LoggedModel): """ Items can be sorted into these categories. :param event: The event this category belongs to :type event: Event :param name: The name of this category :type name: str :param position: An integer, used for sorting :type position: int """ event = models.ForeignKey( Event, on_delete=models.CASCADE, related_name='categories', ) name = I18nCharField( max_length=255, verbose_name=_("Category name"), ) description = I18nTextField( blank=True, verbose_name=_("Category description") ) position = models.IntegerField( default=0 ) class Meta: verbose_name = _("Product category") verbose_name_plural = _("Product categories") ordering = ('position', 'id') def __str__(self): return str(self.name) def delete(self, *args, **kwargs): super().delete(*args, **kwargs) if self.event: self.event.get_cache().clear() def save(self, *args, **kwargs): super().save(*args, **kwargs) if self.event: self.event.get_cache().clear() @property def sortkey(self): return self.position, self.id def __lt__(self, other) -> bool: return self.sortkey < other.sortkey
class Item(LoggedModel): """ An item is a thing which can be sold. It belongs to an event and may or may not belong to a category. Items are often also called 'products' but are named 'items' internally due to historic reasons. :param event: The event this belongs to. :type event: Event :param category: The category this belongs to. May be null. :type category: ItemCategory :param name: The name of this item: :type name: str :param active: Whether this item is being sold :type active: bool :param description: A short description :type description: str :param default_price: The item's default price :type default_price: decimal.Decimal :param tax_rate: The VAT tax that is included in this item's price (in %) :type tax_rate: decimal.Decimal :param admission: ``True``, if this item allows persons to enter the event (as opposed to e.g. merchandise) :type admission: bool :param picture: A product picture to be shown next to the product description. :type picture: File :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 """ event = models.ForeignKey( Event, on_delete=models.PROTECT, related_name="items", verbose_name=_("Event"), ) category = models.ForeignKey( ItemCategory, on_delete=models.PROTECT, related_name="items", blank=True, null=True, verbose_name=_("Category"), ) name = I18nCharField( max_length=255, verbose_name=_("Item name"), ) active = models.BooleanField( default=True, verbose_name=_("Active"), ) description = I18nTextField( verbose_name=_("Description"), help_text=_("This is shown below the product name in lists."), null=True, blank=True, ) default_price = models.DecimalField(verbose_name=_("Default price"), max_digits=7, decimal_places=2, null=True) tax_rate = models.DecimalField(null=True, blank=True, verbose_name=_("Taxes included in percent"), max_digits=7, decimal_places=2) admission = models.BooleanField( verbose_name=_("Is an admission ticket"), help_text=_( 'Whether or not buying this product allows a person to enter ' 'your event'), default=False) position = models.IntegerField(default=0) picture = models.ImageField(verbose_name=_("Product picture"), null=True, blank=True, upload_to=itempicture_upload_to) 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.')) class Meta: verbose_name = _("Product") verbose_name_plural = _("Products") ordering = ("category__position", "category", "position") def __str__(self): return str(self.name) def save(self, *args, **kwargs): super().save(*args, **kwargs) if self.event: self.event.get_cache().clear() def delete(self, *args, **kwargs): super().delete(*args, **kwargs) if self.event: self.event.get_cache().clear() def is_available(self) -> bool: """ Returns whether this item is available according to its ``active`` flag and its ``available_from`` and ``available_until`` fields """ if not self.active: return False if self.available_from and self.available_from > now(): return False if self.available_until and self.available_until < now(): return False return True def check_quotas(self): """ This method is used to determine whether this Item is currently available for sale. :returns: any of the return codes of :py:meth:`Quota.availability()`. :raises ValueError: if you call this on an item which has variations associated with it. Please use the method on the ItemVariation object you are interested in. """ if self.variations.count() > 0: # NOQA raise ValueError( 'Do not call this directly on items which have variations ' 'but call this on their ItemVariation objects') return min([q.availability() for q in self.quotas.all()], key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize))
class Question(LoggedModel): """ A question is an input field that can be used to extend a ticket by custom information, e.g. "Attendee age". A question can allow one o several input types, currently: * a number (``TYPE_NUMBER``) * a one-line string (``TYPE_STRING``) * a multi-line string (``TYPE_TEXT``) * a boolean (``TYPE_BOOLEAN``) :param event: The event this question belongs to :type event: Event :param question: The question text. This will be displayed next to the input field. :type question: str :param type: One of the above types :param required: Whether answering this question is required for submiting an order including items associated with this question. :type required: bool :param items: A set of ``Items`` objects that this question should be applied to """ TYPE_NUMBER = "N" TYPE_STRING = "S" TYPE_TEXT = "T" TYPE_BOOLEAN = "B" TYPE_CHOICES = ( (TYPE_NUMBER, _("Number")), (TYPE_STRING, _("Text (one line)")), (TYPE_TEXT, _("Multiline text")), (TYPE_BOOLEAN, _("Yes/No")), ) event = models.ForeignKey(Event, related_name="questions") question = I18nTextField(verbose_name=_("Question")) type = models.CharField(max_length=5, choices=TYPE_CHOICES, verbose_name=_("Question type")) required = models.BooleanField(default=False, verbose_name=_("Required question")) items = models.ManyToManyField( Item, related_name='questions', verbose_name=_("Products"), blank=True, help_text=_( 'This question will be asked to buyers of the selected products')) class Meta: verbose_name = _("Question") verbose_name_plural = _("Questions") def __str__(self): return str(self.question) def delete(self, *args, **kwargs): super().delete(*args, **kwargs) if self.event: self.event.get_cache().clear() def save(self, *args, **kwargs): super().save(*args, **kwargs) if self.event: self.event.get_cache().clear()
class Item(LoggedModel): """ An item is a thing which can be sold. It belongs to an event and may or may not belong to a category. Items are often also called 'products' but are named 'items' internally due to historic reasons. :param event: The event this item belongs to :type event: Event :param category: The category this belongs to. May be null. :type category: ItemCategory :param name: The name of this item :type name: str :param active: Whether this item is being sold. :type active: bool :param description: A short description :type description: str :param default_price: The item's default price :type default_price: decimal.Decimal :param tax_rate: The VAT tax that is included in this item's price (in %) :type tax_rate: decimal.Decimal :param admission: ``True``, if this item allows persons to enter the event (as opposed to e.g. merchandise) :type admission: bool :param picture: A product picture to be shown next to the product description :type picture: File :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 :param require_voucher: If set to ``True``, this item can only be bought using a voucher. :type require_voucher: bool :param hide_without_voucher: If set to ``True``, this item is only visible and available when a voucher is used. :type hide_without_voucher: bool :param allow_cancel: If set to ``False``, an order with this product can not be canceled by the user. :type allow_cancel: bool """ event = models.ForeignKey( Event, on_delete=models.PROTECT, related_name="items", verbose_name=_("Event"), ) category = models.ForeignKey( ItemCategory, on_delete=models.PROTECT, related_name="items", blank=True, null=True, verbose_name=_("Category"), ) name = I18nCharField( max_length=255, verbose_name=_("Item name"), ) active = models.BooleanField( default=True, verbose_name=_("Active"), ) description = I18nTextField( verbose_name=_("Description"), help_text=_("This is shown below the product name in lists."), null=True, blank=True, ) default_price = models.DecimalField(verbose_name=_("Default price"), max_digits=7, decimal_places=2, null=True) free_price = models.BooleanField( default=False, verbose_name=_("Free price input"), help_text= _("If this option is active, your users can choose the price themselves. The price configured above " "is then interpreted as the minimum price a user has to enter. You could use this e.g. to collect " "additional donations for your event.")) tax_rate = models.DecimalField(verbose_name=_("Taxes included in percent"), max_digits=7, decimal_places=2, default=Decimal('0.00')) admission = models.BooleanField( verbose_name=_("Is an admission ticket"), help_text=_( 'Whether or not buying this product allows a person to enter ' 'your event'), default=False) position = models.IntegerField(default=0) picture = models.ImageField(verbose_name=_("Product picture"), null=True, blank=True, upload_to=itempicture_upload_to) 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.')) require_voucher = models.BooleanField( verbose_name=_('This product can only be bought using a voucher.'), default=False, help_text=_( 'To buy this product, the user needs a voucher that applies to this product ' 'either directly or via a quota.')) hide_without_voucher = models.BooleanField( verbose_name= _('This product will only be shown if a voucher matching the product is redeemed.' ), default=False, help_text= _('This product will be hidden from the event page until the user enters a voucher ' 'code that is specifically tied to this product (and not via a quota).' )) allow_cancel = models.BooleanField( verbose_name=_('Allow product to be canceled'), default=True, help_text=_( 'If you deactivate this, an order including this product might not be canceled by the user. ' 'It may still be canceled by you.')) class Meta: verbose_name = _("Product") verbose_name_plural = _("Products") ordering = ("category__position", "category", "position") def __str__(self): return str(self.name) def save(self, *args, **kwargs): super().save(*args, **kwargs) if self.event: self.event.get_cache().clear() def delete(self, *args, **kwargs): super().delete(*args, **kwargs) if self.event: self.event.get_cache().clear() def is_available(self, now_dt: datetime = None) -> bool: """ Returns whether this item is available according to its ``active`` flag and its ``available_from`` and ``available_until`` fields """ now_dt = now_dt or now() if not self.active: 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 check_quotas(self, ignored_quotas=None, _cache=None): """ This method is used to determine whether this Item is currently available for sale. :param ignored_quotas: If a collection if quota objects is given here, those quotas will be ignored in the calculation. If this leads to no quotas being checked at all, this method will return unlimited availability. :returns: any of the return codes of :py:meth:`Quota.availability()`. :raises ValueError: if you call this on an item which has variations associated with it. Please use the method on the ItemVariation object you are interested in. """ check_quotas = set(self.quotas.all()) if ignored_quotas: check_quotas -= set(ignored_quotas) if not check_quotas: return Quota.AVAILABILITY_OK, sys.maxsize if self.variations.count() > 0: # NOQA raise ValueError( 'Do not call this directly on items which have variations ' 'but call this on their ItemVariation objects') return min([q.availability(_cache=_cache) for q in check_quotas], key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize)) @cached_property def has_variations(self): return self.variations.exists()
class Item(Versionable): """ An item is a thing which can be sold. It belongs to an event and may or may not belong to a category. Items are often also called 'products' but are named 'items' internally due to historic reasons. It has a default price which might by overriden by restrictions. :param event: The event this belongs to. :type event: Event :param category: The category this belongs to. May be null. :type category: ItemCategory :param name: The name of this item: :type name: str :param active: Whether this item is being sold :type active: bool :param description: A short description :type description: str :param default_price: The item's default price :type default_price: decimal.Decimal :param tax_rate: The VAT tax that is included in this item's price (in %) :type tax_rate: decimal.Decimal :param admission: ``True``, if this item allows persons to enter the event (as opposed to e.g. merchandise) :type admission: bool :param picture: A product picture to be shown next to the product description. :type picture: File """ event = VersionedForeignKey( Event, on_delete=models.PROTECT, related_name="items", verbose_name=_("Event"), ) category = VersionedForeignKey( ItemCategory, on_delete=models.PROTECT, related_name="items", blank=True, null=True, verbose_name=_("Category"), ) name = I18nCharField( max_length=255, verbose_name=_("Item name"), ) active = models.BooleanField( default=True, verbose_name=_("Active"), ) description = I18nTextField( verbose_name=_("Description"), help_text=_("This is shown below the product name in lists."), null=True, blank=True, ) default_price = models.DecimalField(verbose_name=_("Default price"), max_digits=7, decimal_places=2, null=True) tax_rate = models.DecimalField(null=True, blank=True, verbose_name=_("Taxes included in percent"), max_digits=7, decimal_places=2) admission = models.BooleanField( verbose_name=_("Is an admission ticket"), help_text=_( 'Whether or not buying this product allows a person to enter ' 'your event'), default=False) position = models.IntegerField(default=0) picture = models.ImageField(verbose_name=_("Product picture"), null=True, blank=True, upload_to=itempicture_upload_to) class Meta: verbose_name = _("Product") verbose_name_plural = _("Products") ordering = ("category__position", "category", "position") def __str__(self): return str(self.name) def save(self, *args, **kwargs): super().save(*args, **kwargs) if self.event: self.event.get_cache().clear() def delete(self, *args, **kwargs): super().delete(*args, **kwargs) if self.event: self.event.get_cache().clear() def get_all_variations(self, use_cache: bool = False) -> "list[VariationDict]": """ This method returns a list containing all variations of this item. The list contains one VariationDict per variation, where the Proprty IDs are keys and the PropertyValue objects are values. If an ItemVariation object exists, it is available in the dictionary via the special key 'variation'. VariationDicts differ from dicts only by specifying some extra methods. :param use_cache: If this parameter is set to ``True``, a second call to this method on the same model instance won't query the database again but return the previous result again. :type use_cache: bool """ if use_cache and hasattr(self, '_get_all_variations_cache'): return self._get_all_variations_cache all_variations = self.variations.all().prefetch_related("values") all_properties = self.properties.all().prefetch_related("values") variations_cache = {} for var in all_variations: key = [] for v in var.values.all(): key.append((v.prop_id, v.identity)) key = tuple(sorted(key)) variations_cache[key] = var result = [] for comb in product(*[prop.values.all() for prop in all_properties]): if len(comb) == 0: result.append(VariationDict()) continue key = [] var = VariationDict() for v in comb: key.append((v.prop.identity, v.identity)) var[v.prop.identity] = v key = tuple(sorted(key)) if key in variations_cache: var['variation'] = variations_cache[key] result.append(var) self._get_all_variations_cache = result return result def _get_all_generated_variations(self): propids = set([p.identity for p in self.properties.all()]) if len(propids) == 0: variations = [VariationDict()] else: all_variations = list( self.variations.annotate(qc=Count('quotas')).filter( qc__gt=0).prefetch_related("values", "values__prop", "quotas__event")) variations = [] for var in all_variations: values = list(var.values.all()) # Make sure we don't expose stale ItemVariation objects which are # still around altough they have an old set of properties if set([v.prop.identity for v in values]) != propids: continue vardict = VariationDict() for v in values: vardict[v.prop.identity] = v vardict['variation'] = var variations.append(vardict) return variations def get_all_available_variations(self, use_cache: bool = False): """ This method returns a list of all variations which are theoretically possible for sale. It DOES call all activated restriction plugins, and it DOES only return variations which DO have an ItemVariation object, as all variations without one CAN NOT be part of a Quota and therefore can never be available for sale. The only exception is the empty variation for items without properties, which never has an ItemVariation object. This DOES NOT take into account quotas itself. Use ``is_available`` on the ItemVariation objects (or the Item it self, if it does not have variations) to determine availability by the terms of quotas. It is recommended to call:: .prefetch_related('properties', 'variations__values__prop') when retrieving Item objects you are going to use this method on. """ if use_cache and hasattr(self, '_get_all_available_variations_cache'): return self._get_all_available_variations_cache from pretix.base.signals import determine_availability variations = self._get_all_generated_variations() responses = determine_availability.send(self.event, item=self, variations=variations, context=None, cache=self.event.get_cache()) for i, var in enumerate(variations): var['available'] = var[ 'variation'].active if 'variation' in var else True if 'variation' in var: if var['variation'].default_price: var['price'] = var['variation'].default_price else: var['price'] = self.default_price else: var['price'] = self.default_price # It is possible, that *multiple* restriction plugins change the default price. # In this case, the cheapest one wins. As soon as there is a restriction # that changes the price, the default price has no effect. newprice = None for rec, response in responses: if 'available' in response[i] and not response[i]['available']: var['available'] = False break if 'price' in response[i] and response[i]['price'] is not None \ and (newprice is None or response[i]['price'] < newprice): newprice = response[i]['price'] var['price'] = newprice or var['price'] variations = [var for var in variations if var['available']] self._get_all_available_variations_cache = variations return variations def check_quotas(self): """ This method is used to determine whether this Item is currently available for sale. :returns: any of the return codes of :py:meth:`Quota.availability()`. :raises ValueError: if you call this on an item which has properties associated with it. Please use the method on the ItemVariation object you are interested in. """ if self.properties.count() > 0: # NOQA raise ValueError( 'Do not call this directly on items which have properties ' 'but call this on their ItemVariation objects') return min([q.availability() for q in self.quotas.all()], key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize)) def check_restrictions(self): """ This method is used to determine whether this ItemVariation is restricted in sale by any restriction plugins. :returns: * ``False``, if the item is unavailable * the item's price, otherwise :raises ValueError: if you call this on an item which has properties associated with it. Please use the method on the ItemVariation object you are interested in. """ if self.properties.count() > 0: # NOQA raise ValueError( 'Do not call this directly on items which have properties ' 'but call this on their ItemVariation objects') from pretix.base.signals import determine_availability vd = VariationDict() responses = determine_availability.send(self.event, item=self, variations=[vd], context=None, cache=self.event.get_cache()) price = self.default_price for rec, response in responses: if 'available' in response[0] and not response[0]['available']: return False elif 'price' in response[0] and response[0][ 'price'] is not None and response[0]['price'] < price: price = response[0]['price'] return price