Skip to content
Snippets Groups Projects
models.py 12.41 KiB
from datetime import datetime
from decimal import Decimal

from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.urls import reverse
from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _

from ckeditor.fields import RichTextField
from colorfield.fields import ColorField
from payments import PurchasedItem

from aleksis.apps.tezor.models.base import Client
from aleksis.apps.tezor.models.invoice import Invoice, InvoiceGroup
from aleksis.core.mixins import ExtensibleModel
from aleksis.core.models import Group, Person
from aleksis.core.util.core_helpers import generate_random_code, get_site_preferences
from aleksis.core.util.email import send_email


class RegistrationState(ExtensibleModel):

    name = models.CharField(verbose_name=_("Name"), max_length=255)
    colour = ColorField(blank=True, verbose_name=_("Colour"))

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


class Terms(ExtensibleModel):
    title = models.CharField(max_length=255, verbose_name=_("Title"))
    term = RichTextField(verbose_name=_("Term"))
    confirmation_text = models.TextField(verbose_name=_("Confirmation text"))

    def __str__(self) -> str:
        return self.title


class InfoMailing(ExtensibleModel):
    subject = models.CharField(max_length=255, verbose_name=_("subject"))
    text = RichTextField(verbose_name=_("Text"))
    reply_to = models.EmailField(verbose_name=_("Request replies to"), blank=True)

    active = models.BooleanField(verbose_name=_("Mailing is active"), default=False)

    sender = models.EmailField(verbose_name=_("Sender"), blank=True)
    send_to_person = models.BooleanField(verbose_name=_("Send to registered person"), default=True)
    send_to_guardians = models.BooleanField(verbose_name=_("Send to guardians"), default=False)

    def __str__(self) -> str:
        return self.subject

    @classmethod
    def get_active_mailings(cls):
        return cls.objects.filter(active=True)

    def send(self):
        for event in self.events.all():
            through = EventInfoMailingThrough.objects.get(info_mailing=self, event=event)
            sent_to = through.sent_to.all()

            for registration in event.registrations.all():
                if registration.person in sent_to:
                    continue

                subject = self.subject.format(
                    event=event, registration=registration, person=registration.person
                )
                body = self.text.format(
                    event=event, registration=registration, person=registration.person
                )

                if self.send_to_person:
                    to = [registration.person.email]
                    if self.send_to_guardians:
                        cc = registration.person.guardians.values_list("email", flat=True).all()
                    else:
                        cc = []
                elif self.send_to_guardians:
                    to = registration.person.guardians.values_list("email", flat=True).all()
                    cc = []

                sender = self.sender or get_site_preferences()["mail__address"]
                reply_to = self.reply_to or sender

                context = {"subject": subject, "body": body}
                send_email(
                    template_name="info_mailing",
                    context=context,
                    from_email=sender,
                    recipient_list=to,
                    cc=cc,
                    headers={
                        "Reply-To": reply_to,
                    },
                )

                through.sent_to.add(registration.person)


class Event(ExtensibleModel):
    # Event details
    display_name = models.CharField(verbose_name=_("Display name"), max_length=255)
    linked_group = models.OneToOneField(
        Group, on_delete=models.CASCADE, verbose_name=_("Group"), related_name="linked_event"
    )
    description = models.CharField(max_length=500, verbose_name=_("Description"))
    published = models.BooleanField(default=False, verbose_name=_("Publish"))
    place = models.CharField(max_length=50, verbose_name="Place")
    slug = models.SlugField(max_length=255, verbose_name=_("Slug"), blank=True)

    # Date details
    date_event = models.DateField(verbose_name=_("Date of event"))
    date_registration = models.DateField(verbose_name=_("Registration deadline"))
    date_retraction = models.DateField(verbose_name=_("Retraction deadline"))

    # Other details
    cost = models.IntegerField(verbose_name=_("Cost in €"))
    max_participants = models.PositiveSmallIntegerField(verbose_name=_("Maximum participants"))
    information = RichTextField(verbose_name=_("Information about the event"))
    terms = models.ManyToManyField(Terms, verbose_name=_("Terms"), related_name="event", blank=True)
    info_mailings = models.ManyToManyField(
        InfoMailing,
        verbose_name=_("Info mailings"),
        related_name="events",
        through="EventInfoMailingThrough",
        blank=True,
    )

    def save(self, *args, **kwargs):
        if not self.slug:
            if self.linked_group.short_name:
                self.slug = slugify(self.linked_group.short_name)
            else:
                self.slug = slugify(self.display_name)

        return super().save(*args, **kwargs)

    def __str__(self) -> str:
        return self.display_name

    def can_register(self, request=None):
        now = datetime.today().date()

        if request and request.user.is_authenticated:
            if request.user.person in self.linked_group.members.all():
                return False

            if EventRegistration.objects.filter(person=request.user.person).exists():
                return False

            if (
                Voucher.objects.filter(event=self, person=request.user.person, used=False).count()
                > 0
            ):
                return True

        if self.linked_group.members.count() >= self.max_participants:
            return False

        if self.date_registration:
            return self.date_registration >= now
        return self.date_event > now

    def get_absolute_url(self):
        return reverse("event_by_name", kwargs={"slug": self.slug})

    @property
    def booked_percentage(self):
        return self.linked_group.members.count() / self.max_participants * 100

    @property
    def members_persons(self):
        return self.linked_group.members.all()

    @property
    def owners_persons(self):
        return self.linked_group.owners.all()

    @classmethod
    def upcoming_published_events(cls):
        return Event.objects.filter(published=True, date_event__gte=now())


class EventInfoMailingThrough(ExtensibleModel):
    event = models.ForeignKey(Event, on_delete=models.CASCADE)
    info_mailing = models.ForeignKey(InfoMailing, on_delete=models.CASCADE)

    sent_to = models.ManyToManyField(
        Person,
        verbose_name=_("Sent to persons"),
        related_name="received_info_mailings",
        editable=False,
        blank=True,
    )


class Voucher(ExtensibleModel):
    class Meta:
        verbose_name = _("Vouchers")
        verbose_name_plural = _("Vouchers")

    code = models.CharField(max_length=255, blank=True, default="")
    event = models.ForeignKey(
        Event,
        related_name="vouchers",
        verbose_name=_("Event"),
        on_delete=models.CASCADE,
        null=True,
    )
    person = models.ForeignKey(
        Person,
        related_name="vouchers",
        verbose_name=_("Person"),
        on_delete=models.CASCADE,
    )
    discount = models.IntegerField(default=100)

    used = models.BooleanField(default=False)
    used_person_uid = models.ForeignKey(
        Person,
        on_delete=models.CASCADE,
        verbose_name=_("Used by"),
        related_name="used_vouchers",
        null=True,
    )
    deleted = models.BooleanField(default=False)

    def __str__(self) -> str:
        return self.code

    def save(self, *args, **kwargs):
        if not self.code:
            self.code = generate_random_code(5, 3)
        super().save(*args, **kwargs)


class EventRegistration(ExtensibleModel):

    event = models.ForeignKey(
        Event, on_delete=models.CASCADE, verbose_name=_("Event"), related_name="registrations"
    )
    person = models.ForeignKey(Person, on_delete=models.CASCADE, verbose_name=_("Person"))
    date_registered = models.DateTimeField(auto_now_add=True, verbose_name=_("Registration date"))

    school = models.CharField(verbose_name=_("Name of school"), max_length=255)
    school_class = models.CharField(verbose_name=_("School class"), max_length=255)
    school_place = models.CharField(verbose_name=_("Place of the school"), max_length=255)

    comment = models.TextField(verbose_name=_("Comment / remarks"), blank=True, default="")
    medical_information = models.TextField(
        verbose_name=_("Medical information / intolerances"), blank=True, default=""
    )
    voucher = models.ForeignKey(
        Voucher,
        on_delete=models.CASCADE,
        verbose_name=_("Voucher"),
        blank=True,
        null=True,
    )
    donation = models.PositiveIntegerField(verbose_name=_("Donation"), blank=True, null=True)
    accepted_terms = models.ManyToManyField(
        Terms,
        verbose_name=_("Accepted terms"),
        related_name="registrations",
    )

    states = models.ManyToManyField(
        RegistrationState, verbose_name=_("States"), related_name="registrations"
    )

    def get_invoice(self):
        # FIXME Maybe do not hard-code this
        client, __ = Client.objects.get_or_create(name="Teckids e.V.")
        group, __ = InvoiceGroup.objects.get_or_create(
            name="Hack'n'Fun-Veranstaltungen",
            client=client,
            defaults={
                "template_name": "paweljong/invoice_pdf.html",
            },
        )

        invoice, __ = Invoice.objects.get_or_create(
            for_content_type=ContentType.objects.get_for_model(self),
            for_object_id=self.pk,
            defaults={
                "group": group,
                "number": f"HNF-{self.date_registered.strftime('%Y-%m')}-{self.id}",
                "currency": "EUR",
                "total": self._get_total_amount()[0],
                "tax": self._get_total_amount()[1],
                "description": _("Participation of {} in event {}").format(
                    self.person.addressing_name, self.event.display_name
                ),
                "billing_first_name": self.person.first_name,
                "billing_last_name": self.person.last_name,
                "billing_address_1": f"{self.person.street} {self.person.housenumber}",
                "billing_city": self.person.place,
                "billing_postcode": self.person.postal_code,
                "billing_email": self.person.email,
            },
        )

        return invoice

    def get_purchased_items(self):
        # FIXME Maybe do not hard-code the tax rate and currency
        # First, return main amount
        yield PurchasedItem(
            name=self.event.display_name,
            quantity=1,
            price=Decimal(self.event.cost),
            currency="EUR",
            sku="",
            tax_rate=7,
        )

        # If a dnoation was made, add it
        if self.donation:
            yield PurchasedItem(
                name=_("Social Sponsoring / Extra Donation"),
                quantity=1,
                price=self.donation,
                currency="EUR",
                sku="",
                tax_rate=0,
            )

        # If a voucher was used, add it
        if self.voucher:
            yield PurchasedItem(
                name=_("Voucher / Granted discount"),
                quantity=1,
                price=-1 * self.voucher.discount * self.event.cost / 100,
                currency="EUR",
                sku="",
                tax_rate=7,
            )

    def _get_total_amount(self):
        total, tax = 0, 0
        for item in self.get_purchased_items():
            total += item.price
            tax += item.price / (item.tax_rate + 100) * item.tax_rate
        return total, tax

    def __str__(self) -> str:
        return f"{self.event}, {self.person.first_name} {self.person.last_name}"

    class Meta:
        verbose_name = _("Event registration")
        verbose_name_plural = _("Event registrations")
        constraints = [
            models.UniqueConstraint(
                fields=["person", "event"], name="unique_person_registration_per_event"
            )
        ]