mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-30 23:39:54 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			443 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			443 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
 | |
| # SPDX-License-Identifier: GPL-3.0-or-later
 | |
| 
 | |
| import json
 | |
| from datetime import date
 | |
| 
 | |
| from django.conf import settings
 | |
| from django.contrib.auth.models import User
 | |
| from django.db import models
 | |
| from django.db.models import Q
 | |
| from django.utils.translation import gettext_lazy as _
 | |
| from phonenumber_field.modelfields import PhoneNumberField
 | |
| from member.models import Club, Membership
 | |
| from note.models import MembershipTransaction
 | |
| from permission.models import Role
 | |
| 
 | |
| 
 | |
| class WEIClub(Club):
 | |
|     """
 | |
|     The WEI is a club. Register to the WEI is equivalent than be member of the club.
 | |
|     """
 | |
|     year = models.PositiveIntegerField(
 | |
|         unique=True,
 | |
|         default=date.today().year,
 | |
|         verbose_name=_("year"),
 | |
|     )
 | |
| 
 | |
|     date_start = models.DateField(
 | |
|         verbose_name=_("date start"),
 | |
|     )
 | |
| 
 | |
|     date_end = models.DateField(
 | |
|         verbose_name=_("date end"),
 | |
|     )
 | |
| 
 | |
|     deposit_amount = models.PositiveIntegerField(
 | |
|         verbose_name=_("deposit amount"),
 | |
|         default=0,
 | |
|     )
 | |
| 
 | |
|     fee_soge_credit = models.PositiveIntegerField(
 | |
|         verbose_name=_("membership fee (soge credit)"),
 | |
|         default=0,
 | |
|     )
 | |
| 
 | |
|     class Meta:
 | |
|         verbose_name = _("WEI")
 | |
|         verbose_name_plural = _("WEI")
 | |
| 
 | |
|     @property
 | |
|     def is_current_wei(self):
 | |
|         """
 | |
|         We consider that this is the current WEI iff there is no future WEI planned.
 | |
|         """
 | |
|         return not WEIClub.objects.filter(date_start__gt=self.date_start).exists()
 | |
| 
 | |
|     def update_membership_dates(self):
 | |
|         """
 | |
|         We can't join the WEI next years.
 | |
|         """
 | |
|         return
 | |
| 
 | |
| 
 | |
| class Bus(models.Model):
 | |
|     """
 | |
|     The best bus for the best WEI
 | |
|     """
 | |
|     wei = models.ForeignKey(
 | |
|         WEIClub,
 | |
|         on_delete=models.PROTECT,
 | |
|         related_name="buses",
 | |
|         verbose_name=_("WEI"),
 | |
|     )
 | |
| 
 | |
|     name = models.CharField(
 | |
|         max_length=255,
 | |
|         verbose_name=_("name"),
 | |
|     )
 | |
| 
 | |
|     size = models.IntegerField(
 | |
|         verbose_name=_("seat count in the bus"),
 | |
|         default=50,
 | |
|     )
 | |
| 
 | |
|     club = models.OneToOneField(
 | |
|         Club,
 | |
|         null=True,
 | |
|         blank=True,
 | |
|         on_delete=models.SET_NULL,
 | |
|         related_name="bus",
 | |
|         verbose_name=_("club"),
 | |
|     )
 | |
| 
 | |
|     description = models.TextField(
 | |
|         blank=True,
 | |
|         default="",
 | |
|         verbose_name=_("description"),
 | |
|     )
 | |
| 
 | |
|     information_json = models.TextField(
 | |
|         default="{}",
 | |
|         verbose_name=_("survey information"),
 | |
|         help_text=_("Information about the survey for new members, encoded in JSON"),
 | |
|     )
 | |
| 
 | |
|     class Meta:
 | |
|         verbose_name = _("Bus")
 | |
|         verbose_name_plural = _("Buses")
 | |
|         unique_together = ('wei', 'name',)
 | |
| 
 | |
|     def __str__(self):
 | |
|         return self.name
 | |
| 
 | |
|     @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
 | |
|         """
 | |
|         self.information_json = json.dumps(information, indent=2)
 | |
| 
 | |
|     @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)
 | |
| 
 | |
| 
 | |
| 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,
 | |
|         verbose_name=_("name"),
 | |
|     )
 | |
| 
 | |
|     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"),
 | |
|     )
 | |
| 
 | |
|     description = models.TextField(
 | |
|         blank=True,
 | |
|         default="",
 | |
|         verbose_name=_("description"),
 | |
|     )
 | |
| 
 | |
|     class Meta:
 | |
|         unique_together = ('bus', 'name',)
 | |
|         verbose_name = _("Bus team")
 | |
|         verbose_name_plural = _("Bus teams")
 | |
| 
 | |
|     def __str__(self):
 | |
|         return self.name + " (" + str(self.bus) + ")"
 | |
| 
 | |
| 
 | |
| class WEIRole(Role):
 | |
|     """
 | |
|     A Role for the WEI can be bus chief, team chief, free electron, ...
 | |
|     """
 | |
|     class Meta:
 | |
|         verbose_name = _("WEI Role")
 | |
|         verbose_name_plural = _("WEI Roles")
 | |
| 
 | |
| 
 | |
| class WEIRegistration(models.Model):
 | |
|     """
 | |
|     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(
 | |
|         WEIClub,
 | |
|         on_delete=models.PROTECT,
 | |
|         related_name="users",
 | |
|         verbose_name=_("WEI"),
 | |
|     )
 | |
| 
 | |
|     soge_credit = models.BooleanField(
 | |
|         default=False,
 | |
|         verbose_name=_("Credit from Société générale"),
 | |
|     )
 | |
| 
 | |
|     deposit_given = models.BooleanField(
 | |
|         default=False,
 | |
|         verbose_name=_("Deposit given")
 | |
|     )
 | |
| 
 | |
|     deposit_type = models.CharField(
 | |
|         max_length=16,
 | |
|         choices=(
 | |
|             ('check', _("Check")),
 | |
|             ('note', _("Note transaction")),
 | |
|         ),
 | |
|         default='check',
 | |
|         verbose_name=_("deposit type"),
 | |
|     )
 | |
| 
 | |
|     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"),
 | |
|     )
 | |
| 
 | |
|     clothing_cut = models.CharField(
 | |
|         max_length=16,
 | |
|         choices=(
 | |
|             ('male', _("Male")),
 | |
|             ('female', _("Female")),
 | |
|             ('unisex', _("Unisex")),
 | |
|         ),
 | |
|         default='unisex',
 | |
|         verbose_name=_("clothing cut"),
 | |
|     )
 | |
| 
 | |
|     clothing_size = models.CharField(
 | |
|         max_length=4,
 | |
|         choices=(
 | |
|             ('XS', "XS"),
 | |
|             ('S', "S"),
 | |
|             ('M', "M"),
 | |
|             ('L', "L"),
 | |
|             ('XL', "XL"),
 | |
|             ('XXL', "XXL"),
 | |
|         ),
 | |
|         verbose_name=_("clothing size"),
 | |
|     )
 | |
| 
 | |
|     health_issues = models.TextField(
 | |
|         blank=True,
 | |
|         default="",
 | |
|         verbose_name=_("health issues"),
 | |
|     )
 | |
| 
 | |
|     emergency_contact_name = models.CharField(
 | |
|         max_length=255,
 | |
|         verbose_name=_("emergency contact name"),
 | |
|         help_text=_("The emergency contact must not be a WEI participant")
 | |
|     )
 | |
| 
 | |
|     emergency_contact_phone = PhoneNumberField(
 | |
|         max_length=32,
 | |
|         verbose_name=_("emergency contact phone"),
 | |
|     )
 | |
| 
 | |
|     first_year = models.BooleanField(
 | |
|         default=False,
 | |
|         verbose_name=_("first year"),
 | |
|         help_text=_("Tells if the user is new in the school.")
 | |
|     )
 | |
| 
 | |
|     information_json = models.TextField(
 | |
|         default="{}",
 | |
|         verbose_name=_("registration information"),
 | |
|         help_text=_("Information about the registration (buses for old members, survey for the new members), "
 | |
|                     "encoded in JSON"),
 | |
|     )
 | |
| 
 | |
|     fee = models.PositiveIntegerField(
 | |
|         default=0,
 | |
|         verbose_name=_('fee'),
 | |
|         blank=True,
 | |
|     )
 | |
| 
 | |
|     class Meta:
 | |
|         unique_together = ('user', 'wei',)
 | |
|         verbose_name = _("WEI User")
 | |
|         verbose_name_plural = _("WEI Users")
 | |
| 
 | |
|     def __str__(self):
 | |
|         return str(self.user)
 | |
| 
 | |
|     @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
 | |
|         """
 | |
|         self.information_json = json.dumps(information, indent=2)
 | |
| 
 | |
|     @property
 | |
|     def is_validated(self):
 | |
|         try:
 | |
|             return self.membership is not None
 | |
|         except AttributeError:
 | |
|             return False
 | |
| 
 | |
|     @property
 | |
|     def validation_status(self):
 | |
|         """
 | |
|         Define an order to have easier access to validatable registrations
 | |
|         """
 | |
|         if self.fee + (self.wei.deposit_amount if self.deposit_type == 'note' else 0) > self.user.note.balance:
 | |
|             return 2
 | |
|         elif self.first_year:
 | |
|             return 1
 | |
|         else:
 | |
|             return 0
 | |
| 
 | |
|     def calculate_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.fee_soge_credit if self.soge_credit \
 | |
|             else 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
 | |
| 
 | |
|     def save(self, *args, **kwargs):
 | |
|         self.fee = self.calculate_fee()
 | |
|         super().save(*args, **kwargs)
 | |
| 
 | |
| 
 | |
| class WEIMembership(Membership):
 | |
|     bus = models.ForeignKey(
 | |
|         Bus,
 | |
|         on_delete=models.PROTECT,
 | |
|         related_name="memberships",
 | |
|         null=True,
 | |
|         default=None,
 | |
|         verbose_name=_("bus"),
 | |
|     )
 | |
| 
 | |
|     team = models.ForeignKey(
 | |
|         BusTeam,
 | |
|         on_delete=models.PROTECT,
 | |
|         related_name="memberships",
 | |
|         null=True,
 | |
|         blank=True,
 | |
|         default=None,
 | |
|         verbose_name=_("team"),
 | |
|     )
 | |
| 
 | |
|     registration = models.OneToOneField(
 | |
|         WEIRegistration,
 | |
|         on_delete=models.PROTECT,
 | |
|         null=True,
 | |
|         blank=True,
 | |
|         default=None,
 | |
|         related_name="membership",
 | |
|         verbose_name=_("WEI registration"),
 | |
|     )
 | |
| 
 | |
|     class Meta:
 | |
|         verbose_name = _("WEI membership")
 | |
|         verbose_name_plural = _("WEI memberships")
 | |
| 
 | |
|     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)
 | |
| 
 | |
|             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
 | |
|                 soge_credit, created = SogeCredit.objects.get_or_create(user=self.user)
 | |
|                 soge_credit.refresh_from_db()
 | |
|                 transaction.save()
 | |
|                 soge_credit.transactions.add(transaction)
 | |
|                 soge_credit.save()
 | |
| 
 | |
|                 soge_credit.update_transactions()
 | |
|                 soge_credit.save()
 | |
| 
 | |
|                 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()
 |