2024-01-11 15:32:37 +00:00
|
|
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
2020-04-11 01:37:06 +00:00
|
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
2020-04-11 21:02:12 +00:00
|
|
|
|
2020-04-11 15:42:08 +00:00
|
|
|
import json
|
2020-04-27 18:25:02 +00:00
|
|
|
from datetime import date
|
2020-04-11 01:37:06 +00:00
|
|
|
|
2020-04-22 01:26:45 +00:00
|
|
|
from django.conf import settings
|
2020-04-11 01:37:06 +00:00
|
|
|
from django.contrib.auth.models import User
|
|
|
|
from django.db import models
|
2021-09-13 17:02:54 +00:00
|
|
|
from django.db.models import Q
|
2020-04-11 01:37:06 +00:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
2020-08-05 12:14:51 +00:00
|
|
|
from phonenumber_field.modelfields import PhoneNumberField
|
2020-07-25 17:40:30 +00:00
|
|
|
from member.models import Club, Membership
|
2020-04-16 22:48:54 +00:00
|
|
|
from note.models import MembershipTransaction
|
2020-07-25 17:40:30 +00:00
|
|
|
from permission.models import Role
|
2020-04-11 01:37:06 +00:00
|
|
|
|
|
|
|
|
2020-04-11 15:42:08 +00:00
|
|
|
class WEIClub(Club):
|
2020-04-11 01:37:06 +00:00
|
|
|
"""
|
2020-04-11 21:02:12 +00:00
|
|
|
The WEI is a club. Register to the WEI is equivalent than be member of the club.
|
2020-04-11 01:37:06 +00:00
|
|
|
"""
|
|
|
|
year = models.PositiveIntegerField(
|
|
|
|
unique=True,
|
2020-04-27 18:25:02 +00:00
|
|
|
default=date.today().year,
|
2020-04-11 01:37:06 +00:00
|
|
|
verbose_name=_("year"),
|
|
|
|
)
|
|
|
|
|
2020-04-11 21:02:12 +00:00
|
|
|
date_start = models.DateField(
|
|
|
|
verbose_name=_("date start"),
|
|
|
|
)
|
|
|
|
|
|
|
|
date_end = models.DateField(
|
|
|
|
verbose_name=_("date end"),
|
|
|
|
)
|
|
|
|
|
2024-01-11 15:32:37 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _("WEI")
|
|
|
|
verbose_name_plural = _("WEI")
|
|
|
|
|
2020-04-18 01:27:12 +00:00
|
|
|
@property
|
|
|
|
def is_current_wei(self):
|
2020-04-20 02:53:26 +00:00
|
|
|
"""
|
|
|
|
We consider that this is the current WEI iff there is no future WEI planned.
|
|
|
|
"""
|
2020-04-18 01:27:12 +00:00
|
|
|
return not WEIClub.objects.filter(date_start__gt=self.date_start).exists()
|
|
|
|
|
2020-04-11 15:42:08 +00:00
|
|
|
def update_membership_dates(self):
|
|
|
|
"""
|
|
|
|
We can't join the WEI next years.
|
|
|
|
"""
|
|
|
|
return
|
2020-04-11 01:37:06 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Bus(models.Model):
|
|
|
|
"""
|
|
|
|
The best bus for the best WEI
|
|
|
|
"""
|
|
|
|
wei = models.ForeignKey(
|
2020-04-11 15:42:08 +00:00
|
|
|
WEIClub,
|
2020-04-11 01:37:06 +00:00
|
|
|
on_delete=models.PROTECT,
|
|
|
|
related_name="buses",
|
|
|
|
verbose_name=_("WEI"),
|
|
|
|
)
|
|
|
|
|
|
|
|
name = models.CharField(
|
|
|
|
max_length=255,
|
|
|
|
verbose_name=_("name"),
|
|
|
|
)
|
|
|
|
|
2021-08-25 21:26:57 +00:00
|
|
|
size = models.IntegerField(
|
|
|
|
verbose_name=_("seat count in the bus"),
|
|
|
|
default=50,
|
|
|
|
)
|
|
|
|
|
2020-04-13 04:01:27 +00:00
|
|
|
description = models.TextField(
|
|
|
|
blank=True,
|
|
|
|
default="",
|
|
|
|
verbose_name=_("description"),
|
|
|
|
)
|
|
|
|
|
2020-04-19 18:35:49 +00:00
|
|
|
information_json = models.TextField(
|
|
|
|
default="{}",
|
|
|
|
verbose_name=_("survey information"),
|
|
|
|
help_text=_("Information about the survey for new members, encoded in JSON"),
|
|
|
|
)
|
|
|
|
|
2024-01-11 15:32:37 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _("Bus")
|
|
|
|
verbose_name_plural = _("Buses")
|
|
|
|
unique_together = ('wei', 'name',)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
2020-04-19 18:35:49 +00:00
|
|
|
@property
|
|
|
|
def information(self):
|
|
|
|
"""
|
|
|
|
The information about the survey for new members are stored in a dictionary that can evolve following the years.
|
|
|
|
The dictionary is stored as a JSON string.
|
|
|
|
"""
|
|
|
|
return json.loads(self.information_json)
|
|
|
|
|
|
|
|
@information.setter
|
|
|
|
def information(self, information):
|
|
|
|
"""
|
|
|
|
Store information as a JSON string
|
|
|
|
"""
|
2021-08-25 22:11:24 +00:00
|
|
|
self.information_json = json.dumps(information, indent=2)
|
2020-04-19 18:35:49 +00:00
|
|
|
|
2021-09-13 17:02:54 +00:00
|
|
|
@property
|
|
|
|
def suggested_first_year(self):
|
|
|
|
registrations = WEIRegistration.objects.filter(Q(membership__isnull=True) | Q(membership__bus__isnull=True),
|
|
|
|
first_year=True, wei=self.wei)
|
|
|
|
registrations = [r for r in registrations if 'selected_bus_pk' in r.information]
|
|
|
|
return sum(1 for r in registrations if r.information['selected_bus_pk'] == self.pk)
|
|
|
|
|
2020-04-11 01:37:06 +00:00
|
|
|
|
|
|
|
class BusTeam(models.Model):
|
|
|
|
"""
|
|
|
|
A bus has multiple teams
|
|
|
|
"""
|
|
|
|
bus = models.ForeignKey(
|
|
|
|
Bus,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
related_name="teams",
|
|
|
|
verbose_name=_("bus"),
|
|
|
|
)
|
|
|
|
|
|
|
|
name = models.CharField(
|
|
|
|
max_length=255,
|
2020-07-25 16:18:53 +00:00
|
|
|
verbose_name=_("name"),
|
2020-04-11 01:37:06 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
color = models.PositiveIntegerField( # Use a color picker to get the hexa code
|
|
|
|
verbose_name=_("color"),
|
|
|
|
help_text=_("The color of the T-Shirt, stored with its number equivalent"),
|
|
|
|
)
|
|
|
|
|
2020-04-13 04:01:27 +00:00
|
|
|
description = models.TextField(
|
|
|
|
blank=True,
|
|
|
|
default="",
|
|
|
|
verbose_name=_("description"),
|
|
|
|
)
|
|
|
|
|
2020-04-11 01:37:06 +00:00
|
|
|
class Meta:
|
|
|
|
unique_together = ('bus', 'name',)
|
|
|
|
verbose_name = _("Bus team")
|
|
|
|
verbose_name_plural = _("Bus teams")
|
|
|
|
|
2024-01-11 15:32:37 +00:00
|
|
|
def __str__(self):
|
|
|
|
return self.name + " (" + str(self.bus) + ")"
|
|
|
|
|
2020-04-11 01:37:06 +00:00
|
|
|
|
2020-04-11 15:42:08 +00:00
|
|
|
class WEIRole(Role):
|
2020-04-11 01:37:06 +00:00
|
|
|
"""
|
|
|
|
A Role for the WEI can be bus chief, team chief, free electron, ...
|
|
|
|
"""
|
2020-04-14 01:41:26 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _("WEI Role")
|
|
|
|
verbose_name_plural = _("WEI Roles")
|
2020-04-11 01:37:06 +00:00
|
|
|
|
|
|
|
|
2020-04-11 15:42:08 +00:00
|
|
|
class WEIRegistration(models.Model):
|
2020-04-11 01:37:06 +00:00
|
|
|
"""
|
|
|
|
Store personal data that can be useful for the WEI.
|
|
|
|
"""
|
|
|
|
user = models.ForeignKey(
|
|
|
|
User,
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
related_name="wei",
|
|
|
|
verbose_name=_("user"),
|
|
|
|
)
|
|
|
|
|
|
|
|
wei = models.ForeignKey(
|
2020-04-11 15:42:08 +00:00
|
|
|
WEIClub,
|
2020-04-11 01:37:06 +00:00
|
|
|
on_delete=models.PROTECT,
|
|
|
|
related_name="users",
|
|
|
|
verbose_name=_("WEI"),
|
|
|
|
)
|
|
|
|
|
2020-04-11 15:42:08 +00:00
|
|
|
soge_credit = models.BooleanField(
|
2020-04-12 01:31:08 +00:00
|
|
|
default=False,
|
2020-04-11 15:42:08 +00:00
|
|
|
verbose_name=_("Credit from Société générale"),
|
2020-04-11 01:37:06 +00:00
|
|
|
)
|
|
|
|
|
2020-04-12 01:31:08 +00:00
|
|
|
caution_check = models.BooleanField(
|
|
|
|
default=False,
|
|
|
|
verbose_name=_("Caution check given")
|
|
|
|
)
|
|
|
|
|
2020-04-11 01:37:06 +00:00
|
|
|
birth_date = models.DateField(
|
|
|
|
verbose_name=_("birth date"),
|
|
|
|
)
|
|
|
|
|
|
|
|
gender = models.CharField(
|
|
|
|
max_length=16,
|
|
|
|
choices=(
|
|
|
|
('male', _("Male")),
|
|
|
|
('female', _("Female")),
|
|
|
|
('nonbinary', _("Non binary")),
|
|
|
|
),
|
|
|
|
verbose_name=_("gender"),
|
|
|
|
)
|
|
|
|
|
2020-07-28 18:49:32 +00:00
|
|
|
clothing_cut = models.CharField(
|
|
|
|
max_length=16,
|
|
|
|
choices=(
|
|
|
|
('male', _("Male")),
|
|
|
|
('female', _("Female")),
|
2023-07-09 10:30:23 +00:00
|
|
|
('unisex', _("Unisex")),
|
2020-07-28 18:49:32 +00:00
|
|
|
),
|
2023-07-09 10:30:23 +00:00
|
|
|
default='unisex',
|
2020-07-28 18:49:32 +00:00
|
|
|
verbose_name=_("clothing cut"),
|
|
|
|
)
|
|
|
|
|
|
|
|
clothing_size = models.CharField(
|
|
|
|
max_length=4,
|
|
|
|
choices=(
|
2020-07-30 13:53:23 +00:00
|
|
|
('XS', "XS"),
|
2020-07-28 18:49:32 +00:00
|
|
|
('S', "S"),
|
|
|
|
('M', "M"),
|
|
|
|
('L', "L"),
|
|
|
|
('XL', "XL"),
|
|
|
|
('XXL', "XXL"),
|
|
|
|
),
|
|
|
|
verbose_name=_("clothing size"),
|
|
|
|
)
|
|
|
|
|
2020-04-11 01:37:06 +00:00
|
|
|
health_issues = models.TextField(
|
2020-04-12 02:29:44 +00:00
|
|
|
blank=True,
|
|
|
|
default="",
|
2020-04-11 01:37:06 +00:00
|
|
|
verbose_name=_("health issues"),
|
|
|
|
)
|
|
|
|
|
2024-08-28 22:05:44 +00:00
|
|
|
specific_diet = models.TextField(
|
|
|
|
blank=True,
|
|
|
|
default="",
|
|
|
|
verbose_name=_("specific diet"),
|
|
|
|
)
|
|
|
|
|
2020-04-11 01:37:06 +00:00
|
|
|
emergency_contact_name = models.CharField(
|
|
|
|
max_length=255,
|
|
|
|
verbose_name=_("emergency contact name"),
|
2023-07-09 12:57:48 +00:00
|
|
|
help_text=_("The emergency contact must not be a WEI participant")
|
2020-04-11 01:37:06 +00:00
|
|
|
)
|
|
|
|
|
2020-08-05 12:14:51 +00:00
|
|
|
emergency_contact_phone = PhoneNumberField(
|
2020-04-11 01:37:06 +00:00
|
|
|
max_length=32,
|
|
|
|
verbose_name=_("emergency contact phone"),
|
|
|
|
)
|
|
|
|
|
2020-04-16 22:48:54 +00:00
|
|
|
first_year = models.BooleanField(
|
|
|
|
default=False,
|
|
|
|
verbose_name=_("first year"),
|
|
|
|
help_text=_("Tells if the user is new in the school.")
|
|
|
|
)
|
|
|
|
|
2020-04-11 15:42:08 +00:00
|
|
|
information_json = models.TextField(
|
2020-04-12 01:31:08 +00:00
|
|
|
default="{}",
|
2020-04-11 15:42:08 +00:00
|
|
|
verbose_name=_("registration information"),
|
2020-09-13 10:40:10 +00:00
|
|
|
help_text=_("Information about the registration (buses for old members, survey for the new members), "
|
2020-04-11 15:42:08 +00:00
|
|
|
"encoded in JSON"),
|
2020-04-11 01:37:06 +00:00
|
|
|
)
|
|
|
|
|
2024-01-11 15:32:37 +00:00
|
|
|
class Meta:
|
|
|
|
unique_together = ('user', 'wei',)
|
|
|
|
verbose_name = _("WEI User")
|
|
|
|
verbose_name_plural = _("WEI Users")
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return str(self.user)
|
|
|
|
|
2020-04-11 15:42:08 +00:00
|
|
|
@property
|
|
|
|
def information(self):
|
|
|
|
"""
|
|
|
|
The information about the registration (the survey for the new members, the bus for the older members, ...)
|
|
|
|
are stored in a dictionary that can evolve following the years. The dictionary is stored as a JSON string.
|
|
|
|
"""
|
|
|
|
return json.loads(self.information_json)
|
|
|
|
|
|
|
|
@information.setter
|
|
|
|
def information(self, information):
|
|
|
|
"""
|
|
|
|
Store information as a JSON string
|
|
|
|
"""
|
2021-08-25 22:11:24 +00:00
|
|
|
self.information_json = json.dumps(information, indent=2)
|
2020-04-11 15:42:08 +00:00
|
|
|
|
2021-08-29 12:10:52 +00:00
|
|
|
@property
|
|
|
|
def fee(self):
|
|
|
|
bde = Club.objects.get(pk=1)
|
|
|
|
kfet = Club.objects.get(pk=2)
|
|
|
|
|
|
|
|
kfet_member = Membership.objects.filter(
|
|
|
|
club_id=kfet.id,
|
|
|
|
user=self.user,
|
|
|
|
date_start__gte=kfet.membership_start,
|
|
|
|
).exists()
|
|
|
|
bde_member = Membership.objects.filter(
|
|
|
|
club_id=bde.id,
|
|
|
|
user=self.user,
|
|
|
|
date_start__gte=bde.membership_start,
|
|
|
|
).exists()
|
|
|
|
|
|
|
|
fee = self.wei.membership_fee_paid if self.user.profile.paid \
|
|
|
|
else self.wei.membership_fee_unpaid
|
|
|
|
if not kfet_member:
|
|
|
|
fee += kfet.membership_fee_paid if self.user.profile.paid \
|
|
|
|
else kfet.membership_fee_unpaid
|
|
|
|
if not bde_member:
|
|
|
|
fee += bde.membership_fee_paid if self.user.profile.paid \
|
|
|
|
else bde.membership_fee_unpaid
|
|
|
|
|
|
|
|
return fee
|
|
|
|
|
2020-04-18 01:27:12 +00:00
|
|
|
@property
|
|
|
|
def is_validated(self):
|
|
|
|
try:
|
|
|
|
return self.membership is not None
|
2020-04-20 22:07:00 +00:00
|
|
|
except AttributeError:
|
2020-04-18 01:27:12 +00:00
|
|
|
return False
|
|
|
|
|
2020-04-11 15:42:08 +00:00
|
|
|
|
|
|
|
class WEIMembership(Membership):
|
|
|
|
bus = models.ForeignKey(
|
2020-04-11 01:37:06 +00:00
|
|
|
Bus,
|
|
|
|
on_delete=models.PROTECT,
|
2020-04-23 16:28:16 +00:00
|
|
|
related_name="memberships",
|
2020-04-11 15:42:08 +00:00
|
|
|
null=True,
|
|
|
|
default=None,
|
|
|
|
verbose_name=_("bus"),
|
2020-04-11 01:37:06 +00:00
|
|
|
)
|
|
|
|
|
2020-04-11 15:42:08 +00:00
|
|
|
team = models.ForeignKey(
|
|
|
|
BusTeam,
|
2020-04-11 01:37:06 +00:00
|
|
|
on_delete=models.PROTECT,
|
2020-04-11 15:42:08 +00:00
|
|
|
related_name="memberships",
|
2020-04-11 01:37:06 +00:00
|
|
|
null=True,
|
|
|
|
blank=True,
|
2020-04-11 15:42:08 +00:00
|
|
|
default=None,
|
|
|
|
verbose_name=_("team"),
|
2020-04-11 01:37:06 +00:00
|
|
|
)
|
|
|
|
|
2020-04-11 15:42:08 +00:00
|
|
|
registration = models.OneToOneField(
|
|
|
|
WEIRegistration,
|
2020-04-11 01:37:06 +00:00
|
|
|
on_delete=models.PROTECT,
|
|
|
|
null=True,
|
|
|
|
blank=True,
|
2020-04-11 15:42:08 +00:00
|
|
|
default=None,
|
|
|
|
related_name="membership",
|
|
|
|
verbose_name=_("WEI registration"),
|
2020-04-11 01:37:06 +00:00
|
|
|
)
|
2020-04-14 01:41:26 +00:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _("WEI membership")
|
|
|
|
verbose_name_plural = _("WEI memberships")
|
2020-04-14 02:46:52 +00:00
|
|
|
|
|
|
|
def make_transaction(self):
|
|
|
|
"""
|
|
|
|
Create Membership transaction associated to this membership.
|
|
|
|
"""
|
|
|
|
if not self.fee or MembershipTransaction.objects.filter(membership=self).exists():
|
|
|
|
return
|
|
|
|
|
|
|
|
if self.fee:
|
|
|
|
transaction = MembershipTransaction(
|
|
|
|
membership=self,
|
|
|
|
source=self.user.note,
|
|
|
|
destination=self.club.note,
|
|
|
|
quantity=1,
|
|
|
|
amount=self.fee,
|
|
|
|
reason="Adhésion WEI " + self.club.name,
|
|
|
|
valid=not self.registration.soge_credit # Soge transactions are by default invalidated
|
|
|
|
)
|
|
|
|
transaction._force_save = True
|
|
|
|
transaction.save(force_insert=True)
|
2020-04-22 01:26:45 +00:00
|
|
|
|
|
|
|
if self.registration.soge_credit and "treasury" in settings.INSTALLED_APPS:
|
|
|
|
# If the soge pays, then the transaction is unvalidated in a first time, then submitted for control
|
|
|
|
# to treasurers.
|
|
|
|
transaction.refresh_from_db()
|
|
|
|
from treasury.models import SogeCredit
|
2021-09-05 19:24:16 +00:00
|
|
|
soge_credit, created = SogeCredit.objects.get_or_create(user=self.user)
|
2020-04-22 01:26:45 +00:00
|
|
|
soge_credit.refresh_from_db()
|
|
|
|
transaction.save()
|
|
|
|
soge_credit.transactions.add(transaction)
|
|
|
|
soge_credit.save()
|
2021-09-05 19:24:16 +00:00
|
|
|
|
|
|
|
soge_credit.update_transactions()
|
|
|
|
soge_credit.save()
|
2021-09-07 11:04:09 +00:00
|
|
|
|
|
|
|
if soge_credit.valid and \
|
|
|
|
soge_credit.credit_transaction.total != sum(tr.total for tr in soge_credit.transactions.all()):
|
|
|
|
# The credit is already validated, but we add a new transaction (eg. for the WEI).
|
|
|
|
# Then we invalidate the transaction, update the credit transaction amount
|
|
|
|
# and re-validate the credit.
|
|
|
|
soge_credit.validate(True)
|
|
|
|
soge_credit.save()
|