diff --git a/apps/activity/models.py b/apps/activity/models.py index f8e5fab9..6a070a3e 100644 --- a/apps/activity/models.py +++ b/apps/activity/models.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later import os @@ -123,6 +123,14 @@ class Activity(models.Model): verbose_name=_('open'), ) + class Meta: + verbose_name = _("activity") + verbose_name_plural = _("activities") + unique_together = ("name", "date_start", "date_end",) + + def __str__(self): + return self.name + @transaction.atomic def save(self, *args, **kwargs): """ @@ -144,14 +152,6 @@ class Activity(models.Model): if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities() return ret - def __str__(self): - return self.name - - class Meta: - verbose_name = _("activity") - verbose_name_plural = _("activities") - unique_together = ("name", "date_start", "date_end",) - class Entry(models.Model): """ @@ -252,14 +252,13 @@ class Guest(models.Model): verbose_name=_("inviter"), ) - @property - def has_entry(self): - try: - if self.entry: - return True - return False - except AttributeError: - return False + class Meta: + verbose_name = _("guest") + verbose_name_plural = _("guests") + unique_together = ("activity", "last_name", "first_name", ) + + def __str__(self): + return self.first_name + " " + self.last_name @transaction.atomic def save(self, force_insert=False, force_update=False, using=None, update_fields=None): @@ -290,13 +289,14 @@ class Guest(models.Model): return super().save(force_insert, force_update, using, update_fields) - def __str__(self): - return self.first_name + " " + self.last_name - - class Meta: - verbose_name = _("guest") - verbose_name_plural = _("guests") - unique_together = ("activity", "last_name", "first_name", ) + @property + def has_entry(self): + try: + if self.entry: + return True + return False + except AttributeError: + return False class GuestTransaction(Transaction): diff --git a/apps/logs/models.py b/apps/logs/models.py index 65a23486..8f2c2eda 100644 --- a/apps/logs/models.py +++ b/apps/logs/models.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.conf import settings @@ -76,9 +76,6 @@ class Changelog(models.Model): verbose_name=_('timestamp'), ) - def delete(self, using=None, keep_parents=False): - raise ValidationError(_("Logs cannot be destroyed.")) - class Meta: verbose_name = _("changelog") verbose_name_plural = _("changelogs") @@ -86,3 +83,6 @@ class Changelog(models.Model): def __str__(self): return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format( action=self.get_action_display(), model=str(self.model), timestamp=str(self.timestamp)) + + def delete(self, using=None, keep_parents=False): + raise ValidationError(_("Logs cannot be destroyed.")) diff --git a/apps/member/models.py b/apps/member/models.py index 5e0da5bc..a18b18f0 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later import datetime @@ -28,7 +28,6 @@ class Profile(models.Model): We do not want to patch the Django Contrib :model:`auth.User`model; so this model add an user profile with additional information. """ - user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, @@ -139,6 +138,17 @@ class Profile(models.Model): default=False ) + class Meta: + verbose_name = _('user profile') + verbose_name_plural = _('user profile') + indexes = [models.Index(fields=['user'])] + + def __str__(self): + return str(self.user) + + def get_absolute_url(self): + return reverse('member:user_detail', args=(self.user_id,)) + @property def ens_year(self): """ @@ -163,17 +173,6 @@ class Profile(models.Model): return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists() return False - class Meta: - verbose_name = _('user profile') - verbose_name_plural = _('user profile') - indexes = [models.Index(fields=['user'])] - - def get_absolute_url(self): - return reverse('member:user_detail', args=(self.user_id,)) - - def __str__(self): - return str(self.user) - def send_email_validation_link(self): subject = "[Note Kfet] " + str(_("Activate your Note Kfet account")) token = email_validation_token.make_token(self.user) @@ -205,9 +204,11 @@ class Club(models.Model): max_length=255, unique=True, ) + email = models.EmailField( verbose_name=_('email'), ) + parent_club = models.ForeignKey( 'self', null=True, @@ -258,6 +259,27 @@ class Club(models.Model): help_text=_('Maximal date of a membership, after which members must renew it.'), ) + class Meta: + verbose_name = _("club") + verbose_name_plural = _("clubs") + + def __str__(self): + return self.name + + @transaction.atomic + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + if not self.require_memberships: + self.membership_fee_paid = 0 + self.membership_fee_unpaid = 0 + self.membership_duration = None + self.membership_start = None + self.membership_end = None + super().save(force_insert, force_update, update_fields) + + def get_absolute_url(self): + return reverse_lazy('member:club_detail', args=(self.pk,)) + def update_membership_dates(self): """ This function is called each time the club detail view is displayed. @@ -278,27 +300,6 @@ class Club(models.Model): self._force_save = True self.save(force_update=True) - @transaction.atomic - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): - if not self.require_memberships: - self.membership_fee_paid = 0 - self.membership_fee_unpaid = 0 - self.membership_duration = None - self.membership_start = None - self.membership_end = None - super().save(force_insert, force_update, update_fields) - - class Meta: - verbose_name = _("club") - verbose_name_plural = _("clubs") - - def __str__(self): - return self.name - - def get_absolute_url(self): - return reverse_lazy('member:club_detail', args=(self.pk,)) - class Membership(models.Model): """ @@ -338,6 +339,66 @@ class Membership(models.Model): verbose_name=_('fee'), ) + class Meta: + verbose_name = _('membership') + verbose_name_plural = _('memberships') + indexes = [models.Index(fields=['user'])] + + def __str__(self): + return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, ) + + @transaction.atomic + def save(self, *args, **kwargs): + """ + Calculate fee and end date before saving the membership and creating the transaction if needed. + """ + # Ensure that club membership dates are valid + old_membership_start = self.club.membership_start + self.club.update_membership_dates() + if self.club.membership_start != old_membership_start: + self.club.save() + + created = not self.pk + if not created: + for role in self.roles.all(): + club = role.for_club + if club is not None: + if club.pk != self.club_id: + raise ValidationError(_('The role {role} does not apply to the club {club}.') + .format(role=role.name, club=club.name)) + else: + if Membership.objects.filter( + user=self.user, + club=self.club, + date_start__lte=self.date_start, + date_end__gte=self.date_start, + ).exists(): + raise ValidationError(_('User is already a member of the club')) + + if self.club.parent_club is not None: + # Check that the user is already a member of the parent club if the membership is created + if not Membership.objects.filter( + user=self.user, + club=self.club.parent_club, + date_start__gte=self.club.parent_club.membership_start, + ).exists(): + if hasattr(self, '_force_renew_parent') and self._force_renew_parent: + self.renew_parent() + else: + raise ValidationError(_('User is not a member of the parent club') + + ' ' + self.club.parent_club.name) + + self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid + + self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \ + if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242) + if self.club.membership_end is not None and self.date_end > self.club.membership_end: + self.date_end = self.club.membership_end + + super().save(*args, **kwargs) + + self.make_transaction() + @property def valid(self): """ @@ -415,58 +476,6 @@ class Membership(models.Model): parent_membership.roles.set(Role.objects.filter(name="Membre de club").all()) parent_membership.save() - @transaction.atomic - def save(self, *args, **kwargs): - """ - Calculate fee and end date before saving the membership and creating the transaction if needed. - """ - # Ensure that club membership dates are valid - old_membership_start = self.club.membership_start - self.club.update_membership_dates() - if self.club.membership_start != old_membership_start: - self.club.save() - - created = not self.pk - if not created: - for role in self.roles.all(): - club = role.for_club - if club is not None: - if club.pk != self.club_id: - raise ValidationError(_('The role {role} does not apply to the club {club}.') - .format(role=role.name, club=club.name)) - else: - if Membership.objects.filter( - user=self.user, - club=self.club, - date_start__lte=self.date_start, - date_end__gte=self.date_start, - ).exists(): - raise ValidationError(_('User is already a member of the club')) - - if self.club.parent_club is not None: - # Check that the user is already a member of the parent club if the membership is created - if not Membership.objects.filter( - user=self.user, - club=self.club.parent_club, - date_start__gte=self.club.parent_club.membership_start, - ).exists(): - if hasattr(self, '_force_renew_parent') and self._force_renew_parent: - self.renew_parent() - else: - raise ValidationError(_('User is not a member of the parent club') - + ' ' + self.club.parent_club.name) - - self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid - - self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \ - if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242) - if self.club.membership_end is not None and self.date_end > self.club.membership_end: - self.date_end = self.club.membership_end - - super().save(*args, **kwargs) - - self.make_transaction() - def make_transaction(self): """ Create Membership transaction associated to this membership. @@ -504,11 +513,3 @@ class Membership(models.Model): soge_credit.save() else: transaction.save(force_insert=True) - - def __str__(self): - return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, ) - - class Meta: - verbose_name = _('membership') - verbose_name_plural = _('memberships') - indexes = [models.Index(fields=['user'])] diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py index 6db9e5f8..a1697fd9 100644 --- a/apps/note/models/notes.py +++ b/apps/note/models/notes.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later import unicodedata @@ -293,6 +293,11 @@ class Alias(models.Model): def __str__(self): return self.name + @transaction.atomic + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + @staticmethod def normalize(string): """ @@ -321,11 +326,6 @@ class Alias(models.Model): pass self.normalized_name = normalized_name - @transaction.atomic - def save(self, *args, **kwargs): - self.clean() - super().save(*args, **kwargs) - def delete(self, using=None, keep_parents=False): if self.name == str(self.note): raise ValidationError(_("You can't delete your main alias."), diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index 8ec7bf0a..0fc299b0 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.core.exceptions import ValidationError @@ -59,6 +59,7 @@ class TransactionTemplate(models.Model): amount = models.PositiveIntegerField( verbose_name=_('amount'), ) + category = models.ForeignKey( TemplateCategory, on_delete=models.PROTECT, @@ -87,12 +88,12 @@ class TransactionTemplate(models.Model): verbose_name = _("transaction template") verbose_name_plural = _("transaction templates") - def get_absolute_url(self): - return reverse('note:template_update', args=(self.pk,)) - def __str__(self): return self.name + def get_absolute_url(self): + return reverse('note:template_update', args=(self.pk,)) + class Transaction(PolymorphicModel): """ @@ -101,7 +102,6 @@ class Transaction(PolymorphicModel): amount is store in centimes of currency, making it a positive integer value. (from someone to someone else) """ - source = models.ForeignKey( Note, on_delete=models.PROTECT, @@ -166,6 +166,50 @@ class Transaction(PolymorphicModel): models.Index(fields=['destination']), ] + def __str__(self): + return self.__class__.__name__ + " from " + str(self.source) + " to " + str(self.destination) + " of "\ + + pretty_money(self.quantity * self.amount) + ("" if self.valid else " invalid") + + @transaction.atomic + def save(self, *args, **kwargs): + """ + When saving, also transfer money between two notes + """ + if self.source.pk == self.destination.pk: + # When source == destination, no money is transferred and no transaction is created + return + + self.source = Note.objects.select_for_update().get(pk=self.source_id) + self.destination = Note.objects.select_for_update().get(pk=self.destination_id) + + # Check that the amounts stay between big integer bounds + diff_source, diff_dest = self.validate() + + if not (hasattr(self, '_force_save') and self._force_save) \ + and (not self.source.is_active or not self.destination.is_active): + raise ValidationError(_("The transaction can't be saved since the source note " + "or the destination note is not active.")) + + # If the aliases are not entered, we assume that the used alias is the name of the note + if not self.source_alias: + self.source_alias = str(self.source) + + if not self.destination_alias: + self.destination_alias = str(self.destination) + + # We save first the transaction, in case of the user has no right to transfer money + super().save(*args, **kwargs) + + # Save notes + self.source.refresh_from_db() + self.source.balance += diff_source + self.source._force_save = True + self.source.save() + self.destination.refresh_from_db() + self.destination.balance += diff_dest + self.destination._force_save = True + self.destination.save() + def validate(self): previous_source_balance = self.source.balance previous_dest_balance = self.destination.balance @@ -208,46 +252,6 @@ class Transaction(PolymorphicModel): return source_balance - previous_source_balance, dest_balance - previous_dest_balance - @transaction.atomic - def save(self, *args, **kwargs): - """ - When saving, also transfer money between two notes - """ - if self.source.pk == self.destination.pk: - # When source == destination, no money is transferred and no transaction is created - return - - self.source = Note.objects.select_for_update().get(pk=self.source_id) - self.destination = Note.objects.select_for_update().get(pk=self.destination_id) - - # Check that the amounts stay between big integer bounds - diff_source, diff_dest = self.validate() - - if not (hasattr(self, '_force_save') and self._force_save) \ - and (not self.source.is_active or not self.destination.is_active): - raise ValidationError(_("The transaction can't be saved since the source note " - "or the destination note is not active.")) - - # If the aliases are not entered, we assume that the used alias is the name of the note - if not self.source_alias: - self.source_alias = str(self.source) - - if not self.destination_alias: - self.destination_alias = str(self.destination) - - # We save first the transaction, in case of the user has no right to transfer money - super().save(*args, **kwargs) - - # Save notes - self.source.refresh_from_db() - self.source.balance += diff_source - self.source._force_save = True - self.source.save() - self.destination.refresh_from_db() - self.destination.balance += diff_dest - self.destination._force_save = True - self.destination.save() - @property def total(self): return self.amount * self.quantity @@ -256,46 +260,40 @@ class Transaction(PolymorphicModel): def type(self): return _('Transfer') - def __str__(self): - return self.__class__.__name__ + " from " + str(self.source) + " to " + str(self.destination) + " of "\ - + pretty_money(self.quantity * self.amount) + ("" if self.valid else " invalid") - class RecurrentTransaction(Transaction): """ Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`. """ - template = models.ForeignKey( TransactionTemplate, on_delete=models.PROTECT, ) + class Meta: + verbose_name = _("recurrent transaction") + verbose_name_plural = _("recurrent transactions") + + @transaction.atomic + def save(self, *args, **kwargs): + self.clean() + return super().save(*args, **kwargs) + def clean(self): if self.template.destination != self.destination and not (hasattr(self, '_force_save') and self._force_save): raise ValidationError( _("The destination of this transaction must equal to the destination of the template.")) return super().clean() - @transaction.atomic - def save(self, *args, **kwargs): - self.clean() - return super().save(*args, **kwargs) - @property def type(self): return _('Template') - class Meta: - verbose_name = _("recurrent transaction") - verbose_name_plural = _("recurrent transactions") - class SpecialTransaction(Transaction): """ Special type of :model:`note.Transaction` associated to transactions with special notes """ - last_name = models.CharField( max_length=255, verbose_name=_("name"), @@ -312,6 +310,15 @@ class SpecialTransaction(Transaction): blank=True, ) + class Meta: + verbose_name = _("Special transaction") + verbose_name_plural = _("Special transactions") + + @transaction.atomic + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + @property def type(self): return _('Credit') if isinstance(self.source, NoteSpecial) else _("Debit") @@ -328,11 +335,6 @@ class SpecialTransaction(Transaction): raise ValidationError(_("A special transaction is only possible between a" " Note associated to a payment method and a User or a Club")) - @transaction.atomic - def save(self, *args, **kwargs): - self.clean() - super().save(*args, **kwargs) - @staticmethod def validate_payment_form(form): """ @@ -363,17 +365,11 @@ class SpecialTransaction(Transaction): return not error - class Meta: - verbose_name = _("Special transaction") - verbose_name_plural = _("Special transactions") - class MembershipTransaction(Transaction): """ Special type of :model:`note.Transaction` associated to a :model:`member.Membership`. - """ - membership = models.OneToOneField( 'member.Membership', on_delete=models.PROTECT, diff --git a/apps/permission/models.py b/apps/permission/models.py index c05dcc4b..fbb46a90 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later import functools @@ -26,6 +26,15 @@ class InstancedPermission: self.mask = mask self.kwargs = kwargs + def __repr__(self): + if self.field: + return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query) + else: + return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query) + + def __str__(self): + return self.__repr__() + def applies(self, obj, permission_type, field_name=None): """ Returns True if the permission applies to @@ -84,21 +93,11 @@ class InstancedPermission: # noinspection PyProtectedMember self.query = Permission._about(self.raw_query, **self.kwargs) - def __repr__(self): - if self.field: - return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query) - else: - return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query) - - def __str__(self): - return self.__repr__() - class PermissionMask(models.Model): """ Permissions that are hidden behind a mask """ - rank = models.PositiveSmallIntegerField( unique=True, verbose_name=_('rank'), @@ -110,13 +109,13 @@ class PermissionMask(models.Model): verbose_name=_('description'), ) - def __str__(self): - return self.description - class Meta: verbose_name = _("permission mask") verbose_name_plural = _("permission masks") + def __str__(self): + return self.description + class Permission(models.Model): @@ -194,16 +193,19 @@ class Permission(models.Model): verbose_name = _("permission") verbose_name_plural = _("permissions") - def clean(self): - self.query = json.dumps(json.loads(self.query)) - if self.field and self.type not in {'view', 'change'}: - raise ValidationError(_("Specifying field applies only to view and change permission types.")) + def __str__(self): + return self.description @transaction.atomic def save(self, **kwargs): self.full_clean() super().save() + def clean(self): + self.query = json.dumps(json.loads(self.query)) + if self.field and self.type not in {'view', 'change'}: + raise ValidationError(_("Specifying field applies only to view and change permission types.")) + @staticmethod def compute_f(oper, **kwargs): if isinstance(oper, list): @@ -317,9 +319,6 @@ class Permission(models.Model): # query = self._about(query, **kwargs) return InstancedPermission(self.model, query, self.type, self.field, self.mask, **kwargs) - def __str__(self): - return self.description - class Role(models.Model): """ @@ -344,9 +343,9 @@ class Role(models.Model): default=None, ) - def __str__(self): - return self.name - class Meta: verbose_name = _("role permissions") verbose_name_plural = _("role permissions") + + def __str__(self): + return self.name diff --git a/apps/treasury/models.py b/apps/treasury/models.py index e788e479..ce7150c4 100644 --- a/apps/treasury/models.py +++ b/apps/treasury/models.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from datetime import date @@ -20,7 +20,6 @@ class Invoice(models.Model): """ An invoice model that can generates a true invoice. """ - id = models.PositiveIntegerField( primary_key=True, verbose_name=_("Invoice identifier"), @@ -81,6 +80,13 @@ class Invoice(models.Model): verbose_name=_("tex source"), ) + class Meta: + verbose_name = _("invoice") + verbose_name_plural = _("invoices") + + def __str__(self): + return _("Invoice #{id}").format(id=self.id) + @transaction.atomic def save(self, *args, **kwargs): """ @@ -111,19 +117,11 @@ class Invoice(models.Model): return super().save(*args, **kwargs) - class Meta: - verbose_name = _("invoice") - verbose_name_plural = _("invoices") - - def __str__(self): - return _("Invoice #{id}").format(id=self.id) - class Product(models.Model): """ Product that appears on an invoice. """ - invoice = models.ForeignKey( Invoice, on_delete=models.CASCADE, @@ -147,6 +145,13 @@ class Product(models.Model): verbose_name=_("Unit price"), ) + class Meta: + verbose_name = _("product") + verbose_name_plural = _("products") + + def __str__(self): + return f"{self.designation} ({self.invoice})" + @property def amount_euros(self): return "{:.2f}".format(self.amount / 100) @@ -159,37 +164,28 @@ class Product(models.Model): def total_euros(self): return "{:.2f}".format(self.total / 100) - class Meta: - verbose_name = _("product") - verbose_name_plural = _("products") - - def __str__(self): - return f"{self.designation} ({self.invoice})" - class RemittanceType(models.Model): """ Store what kind of remittances can be stored. """ - note = models.OneToOneField( NoteSpecial, on_delete=models.CASCADE, ) - def __str__(self): - return str(self.note) - class Meta: verbose_name = _("remittance type") verbose_name_plural = _("remittance types") + def __str__(self): + return str(self.note) + class Remittance(models.Model): """ Treasurers want to regroup checks or bank transfers in bank remittances. """ - date = models.DateTimeField( default=timezone.now, verbose_name=_("Date"), @@ -215,6 +211,17 @@ class Remittance(models.Model): verbose_name = _("remittance") verbose_name_plural = _("remittances") + def __str__(self): + return _("Remittance #{:d}: {}").format(self.id, self.comment, ) + + @transaction.atomic + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + # Check if all transactions have the right type. + if self.transactions.exists() and self.transactions.filter(~Q(source=self.remittance_type.note)).exists(): + raise ValidationError("All transactions in a remittance must have the same type") + + return super().save(force_insert, force_update, using, update_fields) + @property def transactions(self): """ @@ -237,17 +244,6 @@ class Remittance(models.Model): """ return sum(transaction.total for transaction in self.transactions.all()) - @transaction.atomic - def save(self, force_insert=False, force_update=False, using=None, update_fields=None): - # Check if all transactions have the right type. - if self.transactions.exists() and self.transactions.filter(~Q(source=self.remittance_type.note)).exists(): - raise ValidationError("All transactions in a remittance must have the same type") - - return super().save(force_insert, force_update, using, update_fields) - - def __str__(self): - return _("Remittance #{:d}: {}").format(self.id, self.comment, ) - class SpecialTransactionProxy(models.Model): """ @@ -255,7 +251,6 @@ class SpecialTransactionProxy(models.Model): That's why we create a proxy in this app, to link special transactions and remittances. If it isn't very clean, it does what we want. """ - transaction = models.OneToOneField( SpecialTransaction, on_delete=models.CASCADE, @@ -301,6 +296,43 @@ class SogeCredit(models.Model): null=True, ) + class Meta: + verbose_name = _("Credit from the Société générale") + verbose_name_plural = _("Credits from the Société générale") + + def __str__(self): + return _("Soge credit for {user}").format(user=str(self.user)) + + @transaction.atomic + def save(self, *args, **kwargs): + # This is a pre-registered user that declared that a SoGé account was opened. + # No note exists yet. + if not NoteUser.objects.filter(user=self.user).exists(): + return super().save(*args, **kwargs) + + if not self.credit_transaction: + credit_transaction = SpecialTransaction( + source=NoteSpecial.objects.get(special_type="Virement bancaire"), + destination=self.user.note, + quantity=1, + amount=0, + reason="Crédit société générale", + last_name=self.user.last_name, + first_name=self.user.first_name, + bank="Société générale", + valid=False, + ) + credit_transaction._force_save = True + credit_transaction.save() + credit_transaction.refresh_from_db() + self.credit_transaction = credit_transaction + elif not self.valid: + self.credit_transaction.amount = self.amount + self.credit_transaction._force_save = True + self.credit_transaction.save() + + return super().save(*args, **kwargs) + @property def valid(self): return self.credit_transaction and self.credit_transaction.valid @@ -390,36 +422,6 @@ class SogeCredit(models.Model): tr._force_save = True tr.save() - @transaction.atomic - def save(self, *args, **kwargs): - # This is a pre-registered user that declared that a SoGé account was opened. - # No note exists yet. - if not NoteUser.objects.filter(user=self.user).exists(): - return super().save(*args, **kwargs) - - if not self.credit_transaction: - credit_transaction = SpecialTransaction( - source=NoteSpecial.objects.get(special_type="Virement bancaire"), - destination=self.user.note, - quantity=1, - amount=0, - reason="Crédit société générale", - last_name=self.user.last_name, - first_name=self.user.first_name, - bank="Société générale", - valid=False, - ) - credit_transaction._force_save = True - credit_transaction.save() - credit_transaction.refresh_from_db() - self.credit_transaction = credit_transaction - elif not self.valid: - self.credit_transaction.amount = self.amount - self.credit_transaction._force_save = True - self.credit_transaction.save() - - return super().save(*args, **kwargs) - def delete(self, **kwargs): """ Deleting a SogeCredit is equivalent to say that the Société générale didn't pay. @@ -447,10 +449,3 @@ class SogeCredit(models.Model): self.credit_transaction._force_save = True self.credit_transaction.save() super().delete(**kwargs) - - class Meta: - verbose_name = _("Credit from the Société générale") - verbose_name_plural = _("Credits from the Société générale") - - def __str__(self): - return _("Soge credit for {user}").format(user=str(self.user)) diff --git a/apps/wei/migrations/0008_auto_20240111_1545.py b/apps/wei/migrations/0008_auto_20240111_1545.py new file mode 100644 index 00000000..838302aa --- /dev/null +++ b/apps/wei/migrations/0008_auto_20240111_1545.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.28 on 2024-01-11 14:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wei', '0007_help_text_emergency_contact'), + ] + + operations = [ + migrations.AlterField( + model_name='weiclub', + name='year', + field=models.PositiveIntegerField(default=2024, unique=True, verbose_name='year'), + ), + ] diff --git a/apps/wei/models.py b/apps/wei/models.py index 6b05609f..76fd465d 100644 --- a/apps/wei/models.py +++ b/apps/wei/models.py @@ -1,4 +1,4 @@ -# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later import json @@ -33,6 +33,10 @@ class WEIClub(Club): verbose_name=_("date end"), ) + class Meta: + verbose_name = _("WEI") + verbose_name_plural = _("WEI") + @property def is_current_wei(self): """ @@ -46,10 +50,6 @@ class WEIClub(Club): """ return - class Meta: - verbose_name = _("WEI") - verbose_name_plural = _("WEI") - class Bus(models.Model): """ @@ -84,6 +84,14 @@ class Bus(models.Model): 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): """ @@ -106,14 +114,6 @@ class Bus(models.Model): 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) - def __str__(self): - return self.name - - class Meta: - verbose_name = _("Bus") - verbose_name_plural = _("Buses") - unique_together = ('wei', 'name',) - class BusTeam(models.Model): """ @@ -142,20 +142,19 @@ class BusTeam(models.Model): verbose_name=_("description"), ) - def __str__(self): - return self.name + " (" + str(self.bus) + ")" - 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") @@ -165,7 +164,6 @@ class WEIRegistration(models.Model): """ Store personal data that can be useful for the WEI. """ - user = models.ForeignKey( User, on_delete=models.PROTECT, @@ -258,6 +256,14 @@ class WEIRegistration(models.Model): "encoded in JSON"), ) + 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): """ @@ -307,14 +313,6 @@ class WEIRegistration(models.Model): except AttributeError: return False - def __str__(self): - return str(self.user) - - class Meta: - unique_together = ('user', 'wei',) - verbose_name = _("WEI User") - verbose_name_plural = _("WEI Users") - class WEIMembership(Membership): bus = models.ForeignKey(