# # This file is part of pretix (Community Edition). # # Copyright (C) 2014-2020 Raphael Michel and contributors # Copyright (C) 2020-2021 rami.io GmbH and contributors # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General # Public License as published by the Free Software Foundation in version 3 of the License. # # ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are # applicable granting you additional permissions and placing additional restrictions on your usage of this software. # Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive # this file, see . # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # from django.db import models from django.db.models import Count, OuterRef, Subquery, Value from django.db.models.functions import Coalesce from django.utils.formats import date_format from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from django_scopes import ScopedManager, scopes_disabled from i18nfield.fields import I18nCharField from pretix.base.models import Customer from pretix.base.models.base import LoggedModel from pretix.base.models.organizer import Organizer from pretix.base.settings import PERSON_NAME_SCHEMES class MembershipType(LoggedModel): id = models.BigAutoField(primary_key=True) organizer = models.ForeignKey(Organizer, related_name='membership_types', on_delete=models.CASCADE) name = I18nCharField( verbose_name=_('Name'), ) transferable = models.BooleanField( verbose_name=_('Membership is transferable'), help_text=_('If this is selected, the membership can be used to purchase tickets for multiple persons. If not, ' 'the attendee name always needs to stay the same.'), default=False ) allow_parallel_usage = models.BooleanField( verbose_name=_('Parallel usage is allowed'), help_text=_('If this is selected, the membership can be used to purchase tickets for events happening at the same time. Note ' 'that this will only check for an identical start time of the events, not for any overlap between events.'), default=False ) max_usages = models.PositiveIntegerField( verbose_name=_("Maximum usages"), help_text=_("Number of times this membership can be used in a purchase."), null=True, blank=True, ) class Meta: ordering = ('id',) def __str__(self): return str(self.name) def allow_delete(self): return not self.memberships.exists() and not self.granted_by.exists() class MembershipQuerySet(models.QuerySet): @scopes_disabled() # no scoping of subquery def with_usages(self, ignored_order=None): from . import Order, OrderPosition sq = OrderPosition.all.filter( used_membership_id=OuterRef('pk'), canceled=False, ).exclude( order__status=Order.STATUS_CANCELED ) if ignored_order: sq = sq.exclude(order__id=ignored_order.pk) return self.annotate( usages=Coalesce( Subquery( sq.order_by().values('used_membership_id').annotate( c=Count('*') ).values('c') ), Value(0), ) ) def active(self, ev): return self.filter( canceled=False, date_start__lte=ev.date_from, date_end__gte=ev.date_from ) class MembershipQuerySetManager(ScopedManager(organizer='customer__organizer').__class__): def __init__(self): super().__init__() self._queryset_class = MembershipQuerySet def with_usages(self, ignored_order=None): return self.get_queryset().with_usages(ignored_order) def active(self, ev): return self.get_queryset().active(ev) class Membership(models.Model): id = models.BigAutoField(primary_key=True) testmode = models.BooleanField( verbose_name=_('Test mode'), default=False ) canceled = models.BooleanField( verbose_name=_('Canceled'), default=False ) customer = models.ForeignKey( Customer, related_name='memberships', on_delete=models.PROTECT ) membership_type = models.ForeignKey( MembershipType, verbose_name=_('Membership type'), related_name='memberships', on_delete=models.PROTECT ) granted_in = models.ForeignKey( 'OrderPosition', related_name='granted_memberships', on_delete=models.PROTECT, null=True, blank=True, ) date_start = models.DateTimeField( verbose_name=_('Start date') ) date_end = models.DateTimeField( verbose_name=_('End date') ) attendee_name_parts = models.JSONField(default=dict, null=True) objects = MembershipQuerySetManager() class Meta: ordering = "-date_end", "-date_start", "membership_type" def __str__(self): ds = date_format(self.date_start, 'SHORT_DATE_FORMAT') de = date_format(self.date_end, 'SHORT_DATE_FORMAT') return f'{self.membership_type.name}: {self.attendee_name} ({ds} – {de})' @property def attendee_name(self): if not self.attendee_name_parts: return None if '_legacy' in self.attendee_name_parts: return self.attendee_name_parts['_legacy'] if '_scheme' in self.attendee_name_parts: scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']] else: scheme = PERSON_NAME_SCHEMES[self.customer.organizer.settings.name_scheme] return scheme['concatenation'](self.attendee_name_parts).strip() def is_valid(self, ev=None): if ev: dt = ev.date_from else: dt = now() return not self.canceled and dt >= self.date_start and dt <= self.date_end def allow_delete(self): return self.testmode and not self.orderposition_set.exists()