Customer accounts & Memberships (#2024)

This commit is contained in:
Raphael Michel
2021-05-04 16:56:06 +02:00
committed by GitHub
parent 62e412bbc0
commit 8e79eb570e
116 changed files with 7975 additions and 279 deletions

View File

@@ -0,0 +1,168 @@
#
# 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 <https://pretix.eu/about/en/license>.
#
# 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
# <https://www.gnu.org/licenses/>.
#
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 jsonfallback.fields import FallbackJSONField
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,
)
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(
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)
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 = FallbackJSONField(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 dt >= self.date_start and dt <= self.date_end