Esempio n. 1
0
class Property(Versionable):
    """
    A property is a modifier which can be applied to an Item. For example
    'Size' would be a property associated with the item 'T-Shirt'.

    :param event: The event this belongs to
    :type event: Event
    :param name: The name of this property.
    :type name: str
    """

    event = VersionedForeignKey(Event, related_name="properties")
    item = VersionedForeignKey(Item,
                               related_name='properties',
                               null=True,
                               blank=True)
    name = I18nCharField(max_length=250, verbose_name=_("Property name"))

    class Meta:
        verbose_name = _("Product property")
        verbose_name_plural = _("Product properties")

    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()
Esempio n. 2
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
Esempio n. 3
0
class PropertyValue(Versionable):
    """
    A value of a property. If the property would be 'T-Shirt size',
    this could be 'M' or 'L'.

    :param prop: The property this value is a valid option for.
    :type prop: Property
    :param value: The value, as a human-readable string
    :type value: str
    :param position: An integer, used for sorting
    :type position: int
    """

    prop = VersionedForeignKey(Property,
                               on_delete=models.CASCADE,
                               related_name="values")
    value = I18nCharField(
        max_length=250,
        verbose_name=_("Value"),
    )
    position = models.IntegerField(default=0)

    class Meta:
        verbose_name = _("Property value")
        verbose_name_plural = _("Property values")
        ordering = ("position", "version_birth_date")

    def __str__(self):
        return "%s: %s" % (self.prop.name, self.value)

    def delete(self, *args, **kwargs):
        super().delete(*args, **kwargs)
        if self.prop:
            self.prop.event.get_cache().clear()

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        if self.prop:
            self.prop.event.get_cache().clear()

    @property
    def sortkey(self):
        return self.position, self.version_birth_date

    def __lt__(self, other):
        return self.sortkey < other.sortkey
Esempio 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 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))
Esempio n. 5
0
class ItemVariation(models.Model):
    """
    A variation of a product. For example, if your item is 'T-Shirt'
    then an example for a variation would be 'T-Shirt XL'.

    :param item: The item this variation belongs to
    :type item: Item
    :param value: A string defining this variation
    :param active: Whether this value is to be sold.
    :type active: bool
    :param default_price: This variation's default price
    :type default_price: decimal.Decimal
    """
    item = models.ForeignKey(Item, related_name='variations')
    value = I18nCharField(max_length=255, verbose_name=_('Description'))
    active = models.BooleanField(
        default=True,
        verbose_name=_("Active"),
    )
    position = models.PositiveIntegerField(default=0,
                                           verbose_name=_("Position"))
    default_price = models.DecimalField(
        decimal_places=2,
        max_digits=7,
        null=True,
        blank=True,
        verbose_name=_("Default price"),
    )

    class Meta:
        verbose_name = _("Product variation")
        verbose_name_plural = _("Product variations")
        ordering = ("position", "id")

    def __str__(self):
        return str(self.value)

    def delete(self, *args, **kwargs):
        super().delete(*args, **kwargs)
        if self.item:
            self.item.event.get_cache().clear()

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        if self.item:
            self.item.event.get_cache().clear()

    def check_quotas(self) -> Tuple[int, int]:
        """
        This method is used to determine whether this ItemVariation is currently
        available for sale in terms of quotas.

        :returns: any of the return codes of :py:meth:`Quota.availability()`.
        """
        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 __lt__(self, other):
        if self.position == other.position:
            return self.id < other.id
        return self.position < other.position
Esempio n. 6
0
class Event(LoggedModel):
    """
    This model represents an event. An event is anything you can buy
    tickets for.

    :param organizer: The organizer this event belongs to
    :type organizer: Organizer
    :param name: This events full title
    :type name: str
    :param slug: A short, alphanumeric, all-lowercase name for use in URLs. The slug has to
                 be unique among the events of the same organizer.
    :type slug: str
    :param currency: The currency of all prices and payments of this event
    :type currency: str
    :param date_from: The datetime this event starts
    :type date_from: datetime
    :param date_to: The datetime this event ends
    :type date_to: datetime
    :param presale_start: No tickets will be sold before this date.
    :type presale_start: datetime
    :param presale_end: No tickets will be sold before this date.
    :type presale_end: datetime
    :param plugins: A comma-separated list of plugin names that are active for this
                    event.
    :type plugins: str
    """

    organizer = models.ForeignKey(Organizer,
                                  related_name="events",
                                  on_delete=models.PROTECT)
    name = I18nCharField(
        max_length=200,
        verbose_name=_("Name"),
    )
    slug = models.SlugField(
        max_length=50,
        db_index=True,
        help_text=_(
            "Should be short, only contain lowercase letters and numbers, and must be unique among your events. "
            "This is being used in addresses and bank transfer references."),
        validators=[
            RegexValidator(
                regex="^[a-zA-Z0-9.-]+$",
                message=
                _("The slug may only contain letters, numbers, dots and dashes."
                  ),
            )
        ],
        verbose_name=_("Slug"),
    )
    permitted = models.ManyToManyField(
        User,
        through='EventPermission',
        related_name="events",
    )
    currency = models.CharField(max_length=10,
                                verbose_name=_("Default currency"),
                                default=settings.DEFAULT_CURRENCY)
    date_from = models.DateTimeField(verbose_name=_("Event start time"))
    date_to = models.DateTimeField(null=True,
                                   blank=True,
                                   verbose_name=_("Event end time"))
    is_public = models.BooleanField(
        default=False,
        verbose_name=_("Visible in public lists"),
        help_text=_(
            "If selected, this event may show up on the ticket system's start page "
            "or an organization profile."))
    presale_end = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("End of presale"),
        help_text=_("No products will be sold after this date."),
    )
    presale_start = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("Start of presale"),
        help_text=_("No products will be sold before this date."),
    )
    plugins = models.TextField(
        null=True,
        blank=True,
        verbose_name=_("Plugins"),
    )

    class Meta:
        verbose_name = _("Event")
        verbose_name_plural = _("Events")
        ordering = ("date_from", "name")

    def __str__(self):
        return str(self.name)

    def save(self, *args, **kwargs):
        obj = super().save(*args, **kwargs)
        self.get_cache().clear()
        return obj

    def clean(self):
        if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
            raise ValidationError({
                'presale_end':
                _('The end of the presale period has to be later than it\'s start.'
                  )
            })
        if self.date_from and self.date_to and self.date_from > self.date_to:
            raise ValidationError({
                'date_to':
                _('The end of the event has to be later than it\'s start.')
            })
        super().clean()

    def get_plugins(self) -> "list[str]":
        """
        Get the names of the plugins activated for this event as a list.
        """
        if self.plugins is None:
            return []
        return self.plugins.split(",")

    def get_date_from_display(self) -> str:
        """
        Returns a formatted string containing the start date of the event with respect
        to the current locale and to the ``show_times`` setting.
        """
        return _date(
            self.date_from,
            "DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT")

    def get_date_to_display(self) -> str:
        """
        Returns a formatted string containing the start date of the event with respect
        to the current locale and to the ``show_times`` setting. Returns an empty string
        if ``show_date_to`` is ``False``.
        """
        if not self.settings.show_date_to:
            return ""
        return _date(
            self.date_to,
            "DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT")

    def get_cache(self) -> "pretix.base.cache.ObjectRelatedCache":
        """
        Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
        Django's built-in cache backends, but puts you into an isolated environment for
        this event, so you don't have to prefix your cache keys. In addition, the cache
        is being cleared every time the event or one of its related objects change.
        """
        from pretix.base.cache import ObjectRelatedCache

        return ObjectRelatedCache(self)

    @cached_property
    def settings(self) -> SettingsProxy:
        """
        Returns an object representing this event's settings
        """
        return SettingsProxy(self, type=EventSetting, parent=self.organizer)

    @property
    def presale_has_ended(self):
        if self.presale_end and now() > self.presale_end:
            return True
        return False

    @property
    def presale_is_running(self):
        if self.presale_start and now() < self.presale_start:
            return False
        if self.presale_end and now() > self.presale_end:
            return False
        return True

    def lock(self):
        """
        Returns a contextmanager that can be used to lock an event for bookings
        """
        from pretix.base.services import locking

        return locking.LockManager(self)

    def get_mail_backend(self, force_custom=False):
        if self.settings.smtp_use_custom or force_custom:
            return EmailBackend(host=self.settings.smtp_host,
                                port=self.settings.smtp_port,
                                username=self.settings.smtp_username,
                                password=self.settings.smtp_password,
                                use_tls=self.settings.smtp_use_tls,
                                use_ssl=self.settings.smtp_use_ssl,
                                fail_silently=False)
        else:
            return get_connection(fail_silently=False)
Esempio n. 7
0
class Event(LoggedModel):
    """
    This model represents an event. An event is anything you can buy
    tickets for.

    :param organizer: The organizer this event belongs to
    :type organizer: Organizer
    :param name: This event's full title
    :type name: str
    :param slug: A short, alphanumeric, all-lowercase name for use in URLs. The slug has to
                 be unique among the events of the same organizer.
    :type slug: str
    :param live: Whether or not the shop is publicly accessible
    :type live: bool
    :param currency: The currency of all prices and payments of this event
    :type currency: str
    :param date_from: The datetime this event starts
    :type date_from: datetime
    :param date_to: The datetime this event ends
    :type date_to: datetime
    :param presale_start: No tickets will be sold before this date.
    :type presale_start: datetime
    :param presale_end: No tickets will be sold after this date.
    :type presale_end: datetime
    :param location: venue
    :type location: str
    :param plugins: A comma-separated list of plugin names that are active for this
                    event.
    :type plugins: str
    """

    settings_namespace = 'event'
    organizer = models.ForeignKey(Organizer,
                                  related_name="events",
                                  on_delete=models.PROTECT)
    name = I18nCharField(
        max_length=200,
        verbose_name=_("Name"),
    )
    slug = models.SlugField(
        max_length=50,
        db_index=True,
        help_text=
        _("Should be short, only contain lowercase letters and numbers, and must be unique among your events. "
          "This will be used in order codes, invoice numbers, links and bank transfer references."
          ),
        validators=[
            RegexValidator(
                regex="^[a-zA-Z0-9.-]+$",
                message=
                _("The slug may only contain letters, numbers, dots and dashes."
                  ),
            ),
            EventSlugBlacklistValidator()
        ],
        verbose_name=_("Short form"),
    )
    live = models.BooleanField(default=False, verbose_name=_("Shop is live"))
    permitted = models.ManyToManyField(
        User,
        through='EventPermission',
        related_name="events",
    )
    currency = models.CharField(max_length=10,
                                verbose_name=_("Default currency"),
                                default=settings.DEFAULT_CURRENCY)
    date_from = models.DateTimeField(verbose_name=_("Event start time"))
    date_to = models.DateTimeField(null=True,
                                   blank=True,
                                   verbose_name=_("Event end time"))
    is_public = models.BooleanField(
        default=False,
        verbose_name=_("Visible in public lists"),
        help_text=_(
            "If selected, this event may show up on the ticket system's start page "
            "or an organization profile."))
    presale_end = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("End of presale"),
        help_text=_("No products will be sold after this date."),
    )
    presale_start = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("Start of presale"),
        help_text=_("No products will be sold before this date."),
    )
    location = I18nCharField(
        null=True,
        blank=True,
        max_length=200,
        verbose_name=_("Location"),
    )
    plugins = models.TextField(
        null=True,
        blank=True,
        verbose_name=_("Plugins"),
    )

    class Meta:
        verbose_name = _("Event")
        verbose_name_plural = _("Events")
        ordering = ("date_from", "name")

    def __str__(self):
        return str(self.name)

    def save(self, *args, **kwargs):
        obj = super().save(*args, **kwargs)
        self.get_cache().clear()
        return obj

    def clean(self):
        if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
            raise ValidationError({
                'presale_end':
                _('The end of the presale period has to be later than its start.'
                  )
            })
        if self.date_from and self.date_to and self.date_from > self.date_to:
            raise ValidationError({
                'date_to':
                _('The end of the event has to be later than its start.')
            })
        super().clean()

    def get_plugins(self) -> "list[str]":
        """
        Returns the names of the plugins activated for this event as a list.
        """
        if self.plugins is None:
            return []
        return self.plugins.split(",")

    def get_date_from_display(self, tz=None) -> str:
        """
        Returns a formatted string containing the start date of the event with respect
        to the current locale and to the ``show_times`` setting.
        """
        tz = tz or pytz.timezone(self.settings.timezone)
        return _date(
            self.date_from.astimezone(tz),
            "DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT")

    def get_date_to_display(self, tz=None) -> str:
        """
        Returns a formatted string containing the start date of the event with respect
        to the current locale and to the ``show_times`` setting. Returns an empty string
        if ``show_date_to`` is ``False``.
        """
        tz = tz or pytz.timezone(self.settings.timezone)
        if not self.settings.show_date_to or not self.date_to:
            return ""
        return _date(
            self.date_to.astimezone(tz),
            "DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT")

    def get_date_range_display(self, tz=None) -> str:
        tz = tz or pytz.timezone(self.settings.timezone)
        if not self.settings.show_date_to or not self.date_to:
            return _date(self.date_from.astimezone(tz), "DATE_FORMAT")
        return daterange(self.date_from.astimezone(tz),
                         self.date_to.astimezone(tz))

    def get_cache(self) -> "pretix.base.cache.ObjectRelatedCache":
        """
        Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
        Django's built-in cache backends, but puts you into an isolated environment for
        this event, so you don't have to prefix your cache keys. In addition, the cache
        is being cleared every time the event or one of its related objects change.
        """
        from pretix.base.cache import ObjectRelatedCache

        return ObjectRelatedCache(self)

    @cached_property
    def settings(self) -> SettingsProxy:
        """
        Returns an object representing this event's settings.
        """
        try:
            return SettingsProxy(self,
                                 type=EventSetting,
                                 parent=self.organizer)
        except Organizer.DoesNotExist:
            # Should only happen when creating new events
            return SettingsProxy(self, type=EventSetting)

    @property
    def presale_has_ended(self):
        if self.presale_end and now() > self.presale_end:
            return True
        return False

    @property
    def presale_is_running(self):
        if self.presale_start and now() < self.presale_start:
            return False
        if self.presale_end and now() > self.presale_end:
            return False
        return True

    def lock(self):
        """
        Returns a contextmanager that can be used to lock an event for bookings.
        """
        from pretix.base.services import locking

        return locking.LockManager(self)

    def get_mail_backend(self, force_custom=False):
        if self.settings.smtp_use_custom or force_custom:
            return CustomSMTPBackend(host=self.settings.smtp_host,
                                     port=self.settings.smtp_port,
                                     username=self.settings.smtp_username,
                                     password=self.settings.smtp_password,
                                     use_tls=self.settings.smtp_use_tls,
                                     use_ssl=self.settings.smtp_use_ssl,
                                     fail_silently=False)
        else:
            return get_connection(fail_silently=False)

    @property
    def payment_term_last(self):
        tz = pytz.timezone(self.settings.timezone)
        return make_aware(
            datetime.combine(
                self.settings.get('payment_term_last', as_type=date),
                time(hour=23, minute=59, second=59)), tz)

    def copy_data_from(self, other):
        from . import ItemCategory, Item, Question, Quota
        self.plugins = other.plugins
        self.save()

        category_map = {}
        for c in ItemCategory.objects.filter(event=other):
            category_map[c.pk] = c
            c.pk = None
            c.event = self
            c.save()

        item_map = {}
        variation_map = {}
        for i in Item.objects.filter(
                event=other).prefetch_related('variations'):
            vars = list(i.variations.all())
            item_map[i.pk] = i
            i.pk = None
            i.event = self
            if i.picture:
                i.picture.save(i.picture.name, i.picture)
            if i.category_id:
                i.category = category_map[i.category_id]
            i.save()
            for v in vars:
                variation_map[v.pk] = v
                v.pk = None
                v.item = i
                v.save()

        for q in Quota.objects.filter(event=other).prefetch_related(
                'items', 'variations'):
            items = list(q.items.all())
            vars = list(q.variations.all())
            q.pk = None
            q.event = self
            q.save()
            for i in items:
                q.items.add(item_map[i.pk])
            for v in vars:
                q.variations.add(variation_map[v.pk])

        for q in Question.objects.filter(event=other).prefetch_related(
                'items', 'options'):
            items = list(q.items.all())
            opts = list(q.options.all())
            q.pk = None
            q.event = self
            q.save()
            for i in items:
                q.items.add(item_map[i.pk])
            for o in opts:
                o.pk = None
                o.question = q
                o.save()

        for s in EventSetting.objects.filter(object=other):
            s.object = self
            s.pk = None
            if s.value.startswith('file://'):
                fi = default_storage.open(s.value[7:], 'rb')
                nonce = get_random_string(length=8)
                fname = '%s/%s/%s.%s.%s' % (self.organizer.slug,
                                            self.slug, s.key, nonce,
                                            s.value.split('.')[-1])
                newname = default_storage.save(fname, fi)
                s.value = 'file://' + newname
            s.save()
Esempio n. 8
0
class QuestionOption(models.Model):
    question = models.ForeignKey('Question', related_name='options')
    answer = I18nCharField(verbose_name=_('Answer'))

    def __str__(self):
        return str(self.answer)
Esempio n. 9
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()
Esempio n. 10
0
class ItemVariation(models.Model):
    """
    A variation of a product. For example, if your item is 'T-Shirt'
    then an example for a variation would be 'T-Shirt XL'.

    :param item: The item this variation belongs to
    :type item: Item
    :param value: A string defining this variation
    :type value: str
    :param active: Whether this variation is being sold.
    :type active: bool
    :param default_price: This variation's default price
    :type default_price: decimal.Decimal
    """
    item = models.ForeignKey(Item, related_name='variations')
    value = I18nCharField(max_length=255, verbose_name=_('Description'))
    active = models.BooleanField(
        default=True,
        verbose_name=_("Active"),
    )
    position = models.PositiveIntegerField(default=0,
                                           verbose_name=_("Position"))
    default_price = models.DecimalField(
        decimal_places=2,
        max_digits=7,
        null=True,
        blank=True,
        verbose_name=_("Default price"),
    )

    class Meta:
        verbose_name = _("Product variation")
        verbose_name_plural = _("Product variations")
        ordering = ("position", "id")

    def __str__(self):
        return str(self.value)

    @property
    def price(self):
        return self.default_price if self.default_price is not None else self.item.default_price

    @property
    def net_price(self):
        tax_value = round_decimal(self.price * (1 - 100 /
                                                (100 + self.item.tax_rate)))
        return self.price - tax_value

    def delete(self, *args, **kwargs):
        super().delete(*args, **kwargs)
        if self.item:
            self.item.event.get_cache().clear()

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        if self.item:
            self.item.event.get_cache().clear()

    def check_quotas(self,
                     ignored_quotas=None,
                     count_waitinglist=True,
                     _cache=None) -> Tuple[int, int]:
        """
        This method is used to determine whether this ItemVariation is currently
        available for sale in terms of quotas.

        :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.
        :param count_waitinglist: If ``False``, waiting list entries will be ignored for quota calculation.
        :returns: any of the return codes of :py:meth:`Quota.availability()`.
        """
        check_quotas = set(self.quotas.all())
        if ignored_quotas:
            check_quotas -= set(ignored_quotas)
        if not check_quotas:
            return Quota.AVAILABILITY_OK, sys.maxsize
        return min([
            q.availability(count_waitinglist=count_waitinglist, _cache=_cache)
            for q in check_quotas
        ],
                   key=lambda s: (s[0], s[1]
                                  if s[1] is not None else sys.maxsize))

    def __lt__(self, other):
        if self.position == other.position:
            return self.id < other.id
        return self.position < other.position
Esempio n. 11
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