Exemplo n.º 1
0
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
Exemplo n.º 2
0
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))
Exemplo n.º 3
0
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()
Exemplo n.º 4
0
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()
Exemplo n.º 5
0
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