From ff784f36f8b9c29a2f69d6032871eba01e2ba865 Mon Sep 17 00:00:00 2001 From: histausse Date: Mon, 21 Sep 2020 01:21:46 +0200 Subject: [PATCH] Split the membership duration from the connection duration changes: Article: remove COTISATION_TYPE, duration(_days), type_cotisation add duration(_days)_connection, duration(_days)_membership Vente: remove COTISATION_TYPE, duration(_days), type_cotisation add duration(_days)_connection, duration(_days)_membership add method `test_membership_or_connection()` to replace `bool(type_cotisation)` Cotisation: remove COTISATION_TYPE, date_start, date_end, type_cotisation add date_start_con, date_end_con, date_start_memb, date_end_memb create_cotis(date_start=False) -> create_cotis(date_start_con=False, date_start_memb=False) + migration + changes to use the new models in the remaining of the code --- cotisations/api/serializers.py | 12 +- ...043_separation_membership_connection_p1.py | 117 ++++++++++ ...044_separation_membership_connection_p2.py | 140 ++++++++++++ ...045_separation_membership_connection_p3.py | 53 +++++ cotisations/models.py | 214 +++++++++--------- .../templates/cotisations/aff_article.html | 14 +- cotisations/test_models.py | 212 ++++++++++++++--- cotisations/test_views.py | 25 +- cotisations/utils.py | 6 +- cotisations/views.py | 15 +- users/models.py | 17 +- 11 files changed, 647 insertions(+), 178 deletions(-) create mode 100644 cotisations/migrations/0043_separation_membership_connection_p1.py create mode 100644 cotisations/migrations/0044_separation_membership_connection_p2.py create mode 100644 cotisations/migrations/0045_separation_membership_connection_p3.py diff --git a/cotisations/api/serializers.py b/cotisations/api/serializers.py index d33c9f7e..c3b25ef7 100644 --- a/cotisations/api/serializers.py +++ b/cotisations/api/serializers.py @@ -64,8 +64,10 @@ class VenteSerializer(NamespacedHMSerializer): "number", "name", "prix", - "duration", - "type_cotisation", + "duration_connection", + "duration_days_connection", + "duration_membership", + "duration_days_membership", "prix_total", "api_url", ) @@ -77,7 +79,7 @@ class ArticleSerializer(NamespacedHMSerializer): class Meta: model = cotisations.Article - fields = ("name", "prix", "duration", "type_user", "type_cotisation", "api_url") + fields = ("name", "prix", "duration_membership", "duration_days_membership", "duration_connection", "duration_days_connection", "type_user", "api_url") class BanqueSerializer(NamespacedHMSerializer): @@ -104,7 +106,7 @@ class CotisationSerializer(NamespacedHMSerializer): class Meta: model = cotisations.Cotisation - fields = ("vente", "type_cotisation", "date_start", "date_end", "api_url") + fields = ("vente", "type_cotisation", "date_start_con", "date_end_con", "date_start_memb", "date_end_memb", "api_url") class ReminderUsersSerializer(UserSerializer): @@ -124,4 +126,4 @@ class ReminderSerializer(serializers.ModelSerializer): class Meta: model = preferences.Reminder - fields = ("days", "message", "users_to_remind") \ No newline at end of file + fields = ("days", "message", "users_to_remind") diff --git a/cotisations/migrations/0043_separation_membership_connection_p1.py b/cotisations/migrations/0043_separation_membership_connection_p1.py new file mode 100644 index 00000000..7639dc5d --- /dev/null +++ b/cotisations/migrations/0043_separation_membership_connection_p1.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-09-20 17:19 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0042_auto_20191120_0159'), + ] + + operations = [ +# migrations.RemoveField( +# model_name='article', +# name='duration', +# ), +# migrations.RemoveField( +# model_name='article', +# name='duration_days', +# ), +# migrations.RemoveField( +# model_name='article', +# name='type_cotisation', +# ), +# migrations.RemoveField( +# model_name='cotisation', +# name='date_end', +# ), +# migrations.RemoveField( +# model_name='cotisation', +# name='date_start', +# ), +# migrations.RemoveField( +# model_name='cotisation', +# name='type_cotisation', +# ), +# migrations.RemoveField( +# model_name='vente', +# name='duration', +# ), +# migrations.RemoveField( +# model_name='vente', +# name='duration_days', +# ), +# migrations.RemoveField( +# model_name='vente', +# name='type_cotisation', +# ), + migrations.AddField( + model_name='article', + name='duration_connection', + field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='duration of the connection (in months)'), + ), + migrations.AddField( + model_name='article', + name='duration_days_connection', + field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='duration of the connection (in days, will be added to duration in months)'), + ), + migrations.AddField( + model_name='article', + name='duration_days_membership', + field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='duration of the membership (in days, will be added to duration in months)'), + ), + migrations.AddField( + model_name='article', + name='duration_membership', + field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='duration of the membership (in months)'), + ), + migrations.AddField( + model_name='cotisation', + name='date_end_con', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='end date for the connection'), + preserve_default=False, + ), + migrations.AddField( + model_name='cotisation', + name='date_end_memb', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='end date for the membership'), + preserve_default=False, + ), + migrations.AddField( + model_name='cotisation', + name='date_start_con', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='start date for the connection'), + preserve_default=False, + ), + migrations.AddField( + model_name='cotisation', + name='date_start_memb', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='start date for the membership'), + preserve_default=False, + ), + migrations.AddField( + model_name='vente', + name='duration_connection', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='duration of the connection (in months)'), + ), + migrations.AddField( + model_name='vente', + name='duration_days_connection', + field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='duration of the connection (in days, will be added to duration in months)'), + ), + migrations.AddField( + model_name='vente', + name='duration_days_membership', + field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='duration of the membership (in days, will be added to duration in months)'), + ), + migrations.AddField( + model_name='vente', + name='duration_membership', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='duration of the membership (in months)'), + ), + ] diff --git a/cotisations/migrations/0044_separation_membership_connection_p2.py b/cotisations/migrations/0044_separation_membership_connection_p2.py new file mode 100644 index 00000000..87dea8e8 --- /dev/null +++ b/cotisations/migrations/0044_separation_membership_connection_p2.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-09-20 17:19 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0043_separation_membership_connection_p1'), + ] + + def split_dates(apps, schema_editor): + db_alias = schema_editor.connection.alias + cotisation = apps.get_model("cotisations", "Cotisation") + cotisations = cotisation.objects.using(db_alias).all() + for cotis in cotisations: + cotis.date_start_con = cotis.date_start + cotis.date_start_memb = cotis.date_start + cotis.date_end_con = cotis.date_end + cotis.date_end_memb = cotis.date_end + if cotis.type_cotisation == 'Connexion': + cotis.date_end_memb = cotis.date_start + if cotis.type_cotisation == 'Adhesion': + cotis.date_end_con = cotis.date_start + cotis.save() + + + + def split_duration_articles_and_ventes(apps, schema_editor): + def split_duration(e): + e.duration_membership = e.duration + e.duration_connection = e.duration + e.duration_days_membership = e.duration_days + e.duration_days_connection = e.duration_days + if e.type_cotisation == 'Connexion': + e.duration_membership = 0 + e.duration_days_membership = 0 + if e.type_cotisation == 'Adhesion': + e.duration_connection = 0 + e.duration_days_connection = 0 + e.save() + db_alias = schema_editor.connection.alias + article = apps.get_model("cotisations", "Article") + vente = apps.get_model("cotisations", "Vente") + for a in article.objects.using(db_alias).all(): + split_duration(a) + for v in vente.objects.using(db_alias).all(): + split_duration(v) + + def unsplit_dates(apps, schema_editor): + db_alias = schema_editor.connection.alias + cotisation = apps.get_model("cotisations", "Cotisation") + cotisations = cotisation.objects.using(db_alias).all() + for cotis in cotisations: + connection = cotis.date_start_con != cotis.date_end_con + adhesion = cotis.date_start_memb != cotis.date_end_memb + cotis.date_start = cotis.date_start_con + cotis.date_end = max(cotis.date_end_con, cotis.date_end_memb) + if connection: + cotis.type_cotisation = 'Connexion' + if adhesion: + cotis.type_cotisation = 'Adhesion' + if connection and adhesion: + cotis.type_cotisation = 'All' + if not (connection or adhesion): + cotis.type_cotisation = None + cotis.save() + + + + def unsplit_duration_articles_and_ventes(apps, schema_editor): + def unsplit_duration(e): + e.duration = max(e.duration_membership, e.duration_connection) + e.duration_days = max(e.duration_days_membership, e.duration_days_connection) + connection = not (((e.duration_connection == 0) or (e.duration_connection__isnull)) and \ + ((e.duration_days_connection == 0) or (e.duration_days_connection__isnull))) + membership = not (((e.duration_membership == 0) or (e.duration_membership__isnull)) and \ + ((e.duration_days_membership == 0) or (e.duration_days_membership__isnull))) + if connection: + e.type_cotisation = 'Connection' + if membership: + e.type_cotisation = 'Adhesion' + if connection and membership: + e.type_cotisation = 'All' + if not (connection or membership): + e.type_cotisation = None + e.save() + db_alias = schema_editor.connection.alias + article = apps.get_model("cotisations", "Article") + vente = apps.get_model("cotisations", "Vente") + for a in article.objects.using(db_alias).all(): + unsplit_duration(a) + for v in vente.objects.using(db_alias).all(): + unsplit_duration(v) + + + operations = [ + migrations.RunPython(split_dates, unsplit_dates), + migrations.RunPython(split_duration_articles_and_ventes, unsplit_duration_articles_and_ventes), +# migrations.RemoveField( +# model_name='article', +# name='duration', +# ), +# migrations.RemoveField( +# model_name='article', +# name='duration_days', +# ), +# migrations.RemoveField( +# model_name='article', +# name='type_cotisation', +# ), +# migrations.RemoveField( +# model_name='cotisation', +# name='date_end', +# ), +# migrations.RemoveField( +# model_name='cotisation', +# name='date_start', +# ), +# migrations.RemoveField( +# model_name='cotisation', +# name='type_cotisation', +# ), +# migrations.RemoveField( +# model_name='vente', +# name='duration', +# ), +# migrations.RemoveField( +# model_name='vente', +# name='duration_days', +# ), +# migrations.RemoveField( +# model_name='vente', +# name='type_cotisation', +# ), + ] diff --git a/cotisations/migrations/0045_separation_membership_connection_p3.py b/cotisations/migrations/0045_separation_membership_connection_p3.py new file mode 100644 index 00000000..db5432d0 --- /dev/null +++ b/cotisations/migrations/0045_separation_membership_connection_p3.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-09-20 17:19 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0044_separation_membership_connection_p2'), + ] + + operations = [ + migrations.RemoveField( + model_name='article', + name='duration', + ), + migrations.RemoveField( + model_name='article', + name='duration_days', + ), + migrations.RemoveField( + model_name='article', + name='type_cotisation', + ), + migrations.RemoveField( + model_name='cotisation', + name='date_end', + ), + migrations.RemoveField( + model_name='cotisation', + name='date_start', + ), + migrations.RemoveField( + model_name='cotisation', + name='type_cotisation', + ), + migrations.RemoveField( + model_name='vente', + name='duration', + ), + migrations.RemoveField( + model_name='vente', + name='duration_days', + ), + migrations.RemoveField( + model_name='vente', + name='type_cotisation', + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index dc415624..efe3008e 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -283,7 +283,8 @@ class Facture(BaseInvoice): """Returns every subscription associated with this invoice.""" return Cotisation.objects.filter( vente__in=self.vente_set.filter( - Q(type_cotisation="All") | Q(type_cotisation="Adhesion") + ~(Q(duration_membership__isnull=True) | Q(duration_membership=0)) |\ + ~(Q(duration_days_membership__isnull=True) | Q(duration_days_membership=0)) ) ) @@ -297,33 +298,18 @@ class Facture(BaseInvoice): for purchase in self.vente_set.all(): if hasattr(purchase, "cotisation"): cotisation = purchase.cotisation - if cotisation.type_cotisation == "Connexion": - cotisation.date_start = date_con - date_con += relativedelta( - months=(purchase.duration or 0) * purchase.number, - days=(purchase.duration_days or 0) * purchase.number, - ) - cotisation.date_end = date_con - elif cotisation.type_cotisation == "Adhesion": - cotisation.date_start = date_adh - date_adh += relativedelta( - months=(purchase.duration or 0) * purchase.number, - days=(purchase.duration_days or 0) * purchase.number, - ) - cotisation.date_end = date_adh - else: # it is assumed that adhesion is required for a connexion - date = min(date_adh, date_con) - cotisation.date_start = date - date_adh += relativedelta( - months=(purchase.duration or 0) * purchase.number, - days=(purchase.duration_days or 0) * purchase.number, - ) - date_con += relativedelta( - months=(purchase.duration or 0) * purchase.number, - days=(purchase.duration_days or 0) * purchase.number, - ) - date = max(date_adh, date_con) - cotisation.date_end = date + cotisation.date_start_con = date_con + date_con += relativedelta( + months=(purchase.duration_connection or 0) * purchase.number, + days=(purchase.duration_days_connection or 0) * purchase.number, + ) + cotisation.date_end_con = date_con + cotisation.date_start_memb = date_adh + date_adh += relativedelta( + months=(purchase.duration_membership or 0) * purchase.number, + days=(purchase.duration_days_membership or 0) * purchase.number, + ) + cotisation.date_end_memb = date_adh cotisation.save() purchase.facture = self purchase.save() @@ -450,13 +436,6 @@ class Vente(RevMixin, AclMixin, models.Model): the effect of the purchase on the time agreed for this user) """ - # TODO : change this to English - COTISATION_TYPE = ( - ("Connexion", _("Connection")), - ("Adhesion", _("Membership")), - ("All", _("Both of them")), - ) - # TODO : change facture to invoice facture = models.ForeignKey( "BaseInvoice", on_delete=models.CASCADE, verbose_name=_("invoice") @@ -465,28 +444,31 @@ class Vente(RevMixin, AclMixin, models.Model): number = models.IntegerField( validators=[MinValueValidator(1)], verbose_name=_("amount") ) - # TODO : change this field for a ForeinKey to Article + # TODO : change this field for a ForeinKey to Article + # Note: With a foreign key, modifing an Article modifis the Purchase, wich is bad. + # To use a foreign key, you need to make Article read only name = models.CharField(max_length=255, verbose_name=_("article")) # TODO : change prix to price # TODO : this field is not needed if you use Article ForeignKey prix = models.DecimalField(max_digits=5, decimal_places=2, verbose_name=_("price")) # TODO : this field is not needed if you use Article ForeignKey - duration = models.PositiveIntegerField( - blank=True, null=True, verbose_name=_("duration (in months)") + duration_connection = models.PositiveIntegerField( + blank=True, null=True, verbose_name=_("duration of the connection (in months)") ) - duration_days = models.PositiveIntegerField( + duration_days_connection = models.PositiveIntegerField( blank=True, null=True, validators=[MinValueValidator(0)], - verbose_name=_("duration (in days, will be added to duration in months)"), + verbose_name=_("duration of the connection (in days, will be added to duration in months)"), ) - # TODO : this field is not needed if you use Article ForeignKey - type_cotisation = models.CharField( - choices=COTISATION_TYPE, + duration_membership = models.PositiveIntegerField( + blank=True, null=True, verbose_name=_("duration of the membership (in months)") + ) + duration_days_membership = models.PositiveIntegerField( blank=True, null=True, - max_length=255, - verbose_name=_("subscription type"), + validators=[MinValueValidator(0)], + verbose_name=_("duration of the membership (in days, will be added to duration in months)"), ) class Meta: @@ -511,13 +493,17 @@ class Vente(RevMixin, AclMixin, models.Model): """ if hasattr(self, "cotisation"): cotisation = self.cotisation - cotisation.date_end = cotisation.date_start + relativedelta( - months=(self.duration or 0) * self.number, - days=(self.duration_days or 0) * self.number, + cotisation.date_end_memb = cotisation.date_start_memb + relativedelta( + months=(self.duration_membership or 0) * self.number, + days=(self.duration_days_membership or 0) * self.number, + ) + cotisation.date_end_con = cotisation.date_start_con + relativedelta( + months=(self.duration_connection or 0) * self.number, + days=(self.duration_days_connection or 0) * self.number, ) return - def create_cotis(self, date_start=False): + def create_cotis(self, date_start_con=False, date_start_memb=False): """ Creates a cotisation without initializing the dates (start and end ar set to self.facture.facture.date) and without saving it. You should use Facture.reorder_purchases to set the right dates. """ @@ -525,20 +511,29 @@ class Vente(RevMixin, AclMixin, models.Model): invoice = self.facture.facture except Facture.DoesNotExist: return - if not hasattr(self, "cotisation") and self.type_cotisation: + if not hasattr(self, "cotisation") and (self.duration_membership or self.duration_days_membership): cotisation = Cotisation(vente=self) - cotisation.type_cotisation = self.type_cotisation - if date_start: - cotisation.date_start = date_start - cotisation.date_end = cotisation.date_start + relativedelta( - months=(self.duration or 0) * self.number, - days=(self.duration_days or 0) * self.number, + if date_start_con: + cotisation.date_start_con = date_start_con + cotisation.date_end_con = cotisation.date_start_con + relativedelta( + months=(self.duration_connection or 0) * self.number, + days=(self.duration_days_connection or 0) * self.number, + ) + self.save() + cotisation.save() + if date_start_memb: + cotisation.date_start_memb = date_start_memb + cotisation.date_end_memb = cotisation.date_start_memb + relativedelta( + months=(self.duration_membership or 0) * self.number, + days=(self.duration_days_membership or 0) * self.number, ) self.save() cotisation.save() else: - cotisation.date_start = invoice.date - cotisation.date_end = invoice.date + cotisation.date_start_con = invoice.date + cotisation.date_start_memb = invoice.date + cotisation.date_end_con = invoice.date + cotisation.date_end_memb = invoice.date def save(self, *args, **kwargs): """ @@ -546,9 +541,6 @@ class Vente(RevMixin, AclMixin, models.Model): It also update the associated cotisation in the changes have some effect on the user's cotisation """ - # Checking that if a cotisation is specified, there is also a duration - if self.type_cotisation and not (self.duration or self.duration_days): - raise ValidationError(_("Duration must be specified for a subscription.")) self.update_cotisation() super(Vente, self).save(*args, **kwargs) @@ -629,6 +621,13 @@ class Vente(RevMixin, AclMixin, models.Model): def __str__(self): return str(self.name) + " " + str(self.facture) + def test_membership_or_connection(self): + """ Test if the purchase include membership or connecton + """ + return self.duration_membership or \ + self.duration_days_membership or \ + self.duration_connection or \ + self.duration_days_connection # TODO : change vente to purchase @receiver(post_save, sender=Vente) @@ -645,7 +644,7 @@ def vente_post_save(**kwargs): if hasattr(purchase, "cotisation"): purchase.cotisation.vente = purchase purchase.cotisation.save() - if purchase.type_cotisation: + if purchase.test_membership_or_connection(): purchase.create_cotis() purchase.cotisation.save() user = purchase.facture.facture.user @@ -664,7 +663,7 @@ def vente_post_delete(**kwargs): invoice = purchase.facture.facture except Facture.DoesNotExist: return - if purchase.type_cotisation: + if purchase.test_membership_or_connection(): user = invoice.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) @@ -677,56 +676,54 @@ class Article(RevMixin, AclMixin, models.Model): It's represented by: * a name * a price - * a cotisation type (indicating if this article reprensents a - cotisation or not) - * a duration (if it is a cotisation) + * a duration for the membership + * a duration for the connection * a type of user (indicating what kind of user can buy this article) """ - # TODO : Either use TYPE or TYPES in both choices but not both USER_TYPES = ( ("Adherent", _("Member")), ("Club", _("Club")), ("All", _("Both of them")), ) - COTISATION_TYPE = ( - ("Connexion", _("Connection")), - ("Adhesion", _("Membership")), - ("All", _("Both of them")), - ) - name = models.CharField(max_length=255, verbose_name=_("designation")) # TODO : change prix to price prix = models.DecimalField( max_digits=5, decimal_places=2, verbose_name=_("unit price") ) - duration = models.PositiveIntegerField( + + duration_membership = models.PositiveIntegerField( blank=True, null=True, validators=[MinValueValidator(0)], - verbose_name=_("duration (in months)"), + verbose_name=_("duration of the membership (in months)") ) - duration_days = models.PositiveIntegerField( + duration_days_membership = models.PositiveIntegerField( blank=True, null=True, validators=[MinValueValidator(0)], - verbose_name=_("duration (in days, will be added to duration in months)"), + verbose_name=_("duration of the membership (in days, will be added to duration in months)"), ) + duration_connection = models.PositiveIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(0)], + verbose_name=_("duration of the connection (in months)") + ) + duration_days_connection = models.PositiveIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(0)], + verbose_name=_("duration of the connection (in days, will be added to duration in months)"), + ) + type_user = models.CharField( choices=USER_TYPES, default="All", max_length=255, verbose_name=_("type of users concerned"), ) - type_cotisation = models.CharField( - choices=COTISATION_TYPE, - default=None, - blank=True, - null=True, - max_length=255, - verbose_name=_("subscription type"), - ) available_for_everyone = models.BooleanField( default=False, verbose_name=_("is available for every user") ) @@ -744,8 +741,6 @@ class Article(RevMixin, AclMixin, models.Model): def clean(self): if self.name.lower() == "solde": raise ValidationError(_("Solde is a reserved article name.")) - if self.type_cotisation and not (self.duration or self.duration_days): - raise ValidationError(_("Duration must be specified for a subscription.")) def __str__(self): return self.name @@ -790,9 +785,11 @@ class Article(RevMixin, AclMixin, models.Model): ) if target_user is not None and not target_user.is_adherent(): objects_pool = objects_pool.filter( - Q(type_cotisation="All") - | Q(type_cotisation="Adhesion") - | Q(type_cotisation__isnull=True) + Q(duration_membership__gt=0) + |Q(duration_days_membership__gt=0) + |~(Q(duration_connection__gt=0) # BAD nonstandard hardcoded comportmant + |Q(duration_days_connection__gt=0) + ) ) if user.has_perm("cotisations.buy_every_article"): return objects_pool @@ -882,7 +879,7 @@ class Paiement(RevMixin, AclMixin, models.Model): # In case a cotisation was bought, inform the user, the # cotisation time has been extended too - if any(sell.type_cotisation for sell in invoice.vente_set.all()): + if any(sell.test_membership_or_connection() for sell in invoice.vente_set.all()): messages.success( request, _( @@ -943,31 +940,21 @@ class Cotisation(RevMixin, AclMixin, models.Model): The model defining a cotisation. It holds information about the time a user is allowed when he has paid something. It characterised by : - * a date_start (the date when the cotisaiton begins/began - * a date_end (the date when the cotisation ends/ended - * a type of cotisation (which indicates the implication of such - cotisation) + * a date_start_memb (the date when the membership begins/began + * a date_end_memb (the date when the membership ends/ended + * a date_start_con (the date when the connection begins/began) + * a date_end_con (the date when the connection ends/ended) * a purchase (the related objects this cotisation is linked to) """ - COTISATION_TYPE = ( - ("Connexion", _("Connection")), - ("Adhesion", _("Membership")), - ("All", _("Both of them")), - ) - # TODO : change vente to purchase vente = models.OneToOneField( "Vente", on_delete=models.CASCADE, null=True, verbose_name=_("purchase") ) - type_cotisation = models.CharField( - choices=COTISATION_TYPE, - max_length=255, - default="All", - verbose_name=_("subscription type"), - ) - date_start = models.DateTimeField(verbose_name=_("start date")) - date_end = models.DateTimeField(verbose_name=_("end date")) + date_start_con = models.DateTimeField(verbose_name=_("start date for the connection")) + date_end_con = models.DateTimeField(verbose_name=_("end date for the connection")) + date_start_memb = models.DateTimeField(verbose_name=_("start date for the membership")) + date_end_memb = models.DateTimeField(verbose_name=_("end date for the membership")) class Meta: permissions = ( @@ -1037,9 +1024,14 @@ class Cotisation(RevMixin, AclMixin, models.Model): return ( str(self.vente) + "from " - + str(self.date_start) + + str(self.date_start_memb) + " to " - + str(self.date_end) + + str(self.date_end_memb) + + " for membership, " + + str(self.date_start_con) + + " to " + + str(self.date_end_con) + + " for the connection." ) diff --git a/cotisations/templates/cotisations/aff_article.html b/cotisations/templates/cotisations/aff_article.html index 7ead24dc..f53a71d2 100644 --- a/cotisations/templates/cotisations/aff_article.html +++ b/cotisations/templates/cotisations/aff_article.html @@ -32,9 +32,10 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Article" %} {% trans "Price" %} - {% trans "Subscription type" %} - {% trans "Duration (in months)" %} - {% trans "Duration (in days)" %} + {% trans "Duration membership (in months)" %} + {% trans "Duration membership (in days)" %} + {% trans "Duration connection (in months)" %} + {% trans "Duration connection (in days)" %} {% trans "Concerned users" %} {% trans "Available for everyone" %} @@ -44,9 +45,10 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ article.name }} {{ article.prix }} - {{ article.type_cotisation }} - {{ article.duration }} - {{ article.duration_days }} + {{ article.duration_membership }} + {{ article.duration_days_membership }} + {{ article.duration_connection }} + {{ article.duration_days_connection }} {{ article.type_user }} {{ article.available_for_everyone | tick }} diff --git a/cotisations/test_models.py b/cotisations/test_models.py index 6e90d882..08c4a3b8 100644 --- a/cotisations/test_models.py +++ b/cotisations/test_models.py @@ -19,15 +19,17 @@ class VenteModelTests(TestCase): def test_one_day_cotisation(self): """ It should be possible to have one day membership. + Add one day of membership and one day of connection. """ date = timezone.now() purchase = Vente.objects.create( facture=self.f, number=1, name="Test purchase", - duration=0, - duration_days=1, - type_cotisation="All", + duration_connection=0, + duration_days_connection=1, + duration_membership=0, + duration_days_membership=1, prix=0, ) self.f.reorder_purchases() @@ -36,48 +38,66 @@ class VenteModelTests(TestCase): datetime.timedelta(days=1), delta=datetime.timedelta(seconds=1), ) + self.assertAlmostEqual( + self.user.end_adhesion() - date, + datetime.timedelta(days=1), + delta=datetime.timedelta(seconds=1), + ) def test_one_month_cotisation(self): """ It should be possible to have one day membership. + Add one mounth of membership and one mounth of connection """ date = timezone.now() Vente.objects.create( facture=self.f, number=1, name="Test purchase", - duration=1, - duration_days=0, - type_cotisation="All", + duration_connection=1, + duration_days_connection=0, + duration_membership=1, + duration_days_membership=0, prix=0, ) self.f.reorder_purchases() - end = self.user.end_connexion() + end_con = self.user.end_connexion() + end_memb = self.user.end_adhesion() expected_end = date + relativedelta(months=1) - self.assertEqual(end.day, expected_end.day) - self.assertEqual(end.month, expected_end.month) - self.assertEqual(end.year, expected_end.year) + self.assertEqual(end_con.day, expected_end.day) + self.assertEqual(end_con.month, expected_end.month) + self.assertEqual(end_con.year, expected_end.year) + self.assertEqual(end_memb.day, expected_end.day) + self.assertEqual(end_memb.month, expected_end.month) + self.assertEqual(end_memb.year, expected_end.year) def test_one_month_and_one_week_cotisation(self): """ It should be possible to have one day membership. + Add one mounth and one week of membership and one mounth + and one week of connection """ date = timezone.now() Vente.objects.create( facture=self.f, number=1, name="Test purchase", - duration=1, - duration_days=7, - type_cotisation="All", + duration_connection=1, + duration_days_connection=7, + duration_membership=1, + duration_days_membership=7, prix=0, ) self.f.reorder_purchases() - end = self.user.end_connexion() + end_con = self.user.end_connexion() + end_memb = self.user.end_adhesion() expected_end = date + relativedelta(months=1, days=7) - self.assertEqual(end.day, expected_end.day) - self.assertEqual(end.month, expected_end.month) - self.assertEqual(end.year, expected_end.year) + self.assertEqual(end_con.day, expected_end.day) + self.assertEqual(end_con.month, expected_end.month) + self.assertEqual(end_con.year, expected_end.year) + self.assertEqual(end_memb.day, expected_end.day) + self.assertEqual(end_memb.month, expected_end.month) + self.assertEqual(end_memb.year, expected_end.year) def test_date_start_cotisation(self): """ @@ -87,15 +107,140 @@ class VenteModelTests(TestCase): facture=self.f, number=1, name="Test purchase", - duration=0, - duration_days=1, - type_cotisation = 'All', + duration_connection=0, + duration_days_connection=1, + duration_membership=0, + duration_deys_membership=1, prix=0 ) - v.create_cotis(date_start=timezone.make_aware(datetime.datetime(1998, 10, 16))) + v.create_cotis(date_start_con=timezone.make_aware(datetime.datetime(1998, 10, 16)), date_start_memb=timezone.make_aware(datetime.datetime(1998, 10, 16))) v.save() - self.assertEqual(v.cotisation.date_end, timezone.make_aware(datetime.datetime(1998, 10, 17))) + self.assertEqual(v.cotisation.date_end_con, timezone.make_aware(datetime.datetime(1998, 10, 17))) + self.assertEqual(v.cotisation.date_end_memb, timezone.make_aware(datetime.datetime(1998, 10, 17))) + def test_one_day_cotisation_membership_only(self): + """ + It should be possible to have one day membership without connection. + Add one day of membership and no connection. + """ + date = timezone.now() + purchase = Vente.objects.create( + facture=self.f, + number=1, + name="Test purchase", + duration_connection=0, + duration_days_connection=0, + duration_membership=0, + duration_days_membership=1, + prix=0, + ) + self.f.reorder_purchases() + self.assertEqual( + self.user.end_connexion(), + None, + ) + self.assertAlmostEqual( + self.user.end_adhesion() - date, + datetime.timedelta(days=1), + delta=datetime.timedelta(seconds=1), + ) + + def test_one_month_cotisation_membership_only(self): + """ + It should be possible to have one month membership. + Add one mounth of membership and no connection + """ + date = timezone.now() + Vente.objects.create( + facture=self.f, + number=1, + name="Test purchase", + duration_connection=0, + duration_days_connection=0, + duration_membership=1, + duration_days_membership=0, + prix=0, + ) + self.f.reorder_purchases() + end_con = self.user.end_connexion() + end_memb = self.user.end_adhesion() + expected_end = date + relativedelta(months=1) + self.assertEqual(end_con, None) + self.assertEqual(end_memb.day, expected_end.day) + self.assertEqual(end_memb.month, expected_end.month) + self.assertEqual(end_memb.year, expected_end.year) + + def test_one_month_and_one_week_cotisation_membership_only(self): + """ + It should be possible to have one mounth and one week membership. + Add one mounth and one week of membership and no connection. + """ + date = timezone.now() + Vente.objects.create( + facture=self.f, + number=1, + name="Test purchase", + duration_connection=0, + duration_days_connection=0, + duration_membership=1, + duration_days_membership=7, + prix=0, + ) + self.f.reorder_purchases() + end_con = self.user.end_connexion() + end_memb = self.user.end_adhesion() + expected_end = date + relativedelta(months=1, days=7) + self.assertEqual(end_con, None) + self.assertEqual(end_memb.day, expected_end.day) + self.assertEqual(end_memb.month, expected_end.month) + self.assertEqual(end_memb.year, expected_end.year) + + def test_date_start_cotisation_membership_only(self): + """ + It should be possible to add a cotisation with a specific start date + """ + v = Vente( + facture=self.f, + number=1, + name="Test purchase", + duration_connection=0, + duration_days_connection=0, + duration_membership=0, + duration_days_membership=1, + prix=0 + ) + v.create_cotis(date_start_con=timezone.make_aware(datetime.datetime(1998, 10, 16)), date_start_memb=timezone.make_aware(datetime.datetime(1998, 10, 16))) + v.save() + self.assertEqual(v.cotisation.date_end_con, timezone.make_aware(datetime.datetime(1998, 10, 17))) + self.assertEqual(v.cotisation.date_end_memb, timezone.make_aware(datetime.datetime(1998, 10, 16))) + + def test_cotisation_membership_diff_connection(self): + """ + It should be possible to have purchase a membership longer + than the connection. + """ + date = timezone.now() + Vente.objects.create( + facture=self.f, + number=1, + name="Test purchase", + duration_connection=1, + duration_days_connection=0, + duration_membership=2, + duration_days_membership=0, + prix=0, + ) + self.f.reorder_purchases() + end_con = self.user.end_connexion() + end_memb = self.user.end_adhesion() + expected_end_con = date + relativedelta(months=1) + expected_end_memb = date + relativedelta(months=2) + self.assertEqual(end_con.day, expected_end_con.day) + self.assertEqual(end_con.month, expected_end_con.month) + self.assertEqual(end_con.year, expected_end_con.year) + self.assertEqual(end_memb.day, expected_end_memb.day) + self.assertEqual(end_memb.month, expected_end_memb.month) + self.assertEqual(end_memb.year, expected_end_memb.year) def tearDown(self): self.f.delete() @@ -121,9 +266,10 @@ class FactureModelTests(TestCase): facture=invoice1, number=1, name="Test purchase", - duration=1, - duration_days=0, - type_cotisation="All", + duration_connection=1, + duration_days_connection=0, + duration_membership=1, + duration_days_membership=0, prix=0, ) invoice1.reorder_purchases() @@ -134,16 +280,20 @@ class FactureModelTests(TestCase): facture=invoice2, number=1, name="Test purchase", - duration=1, - duration_days=0, - type_cotisation="All", + duration_connection=1, + duration_days_connection=0, + duration_membership=1, + duration_days_membership=0, prix=0, ) invoice1.reorder_purchases() - delta = relativedelta(self.user.end_connexion(), date) - delta.microseconds = 0 + delta_con = relativedelta(self.user.end_connexion(), date) + delta_memb = relativedelta(self.user.end_adhesion(), date) + delta_con.microseconds = 0 + delta_memb.microseconds = 0 try: - self.assertEqual(delta, relativedelta(months=2)) + self.assertEqual(delta_con, relativedelta(months=2)) + self.assertEqual(delta_memb, relativedelta(months=2)) except Exception as e: invoice1.delete() invoice2.delete() diff --git a/cotisations/test_views.py b/cotisations/test_views.py index f0c739b0..c15848cb 100644 --- a/cotisations/test_views.py +++ b/cotisations/test_views.py @@ -38,25 +38,28 @@ class NewFactureTests(TestCase): self.article_one_day = Article.objects.create( name="One day", prix=0, - duration=0, - duration_days=1, - type_cotisation="All", + duration_connection=0, + duration_days_connection=1, + duration_membership=0, + duration_days_membership=1, available_for_everyone=True, ) self.article_one_month = Article.objects.create( - name="One day", + name="One mounth", prix=0, - duration=1, - duration_days=0, - type_cotisation="All", + duration_connection=1, + duration_days_connection=0, + duration_membership=1, + duration_days_membership=0, available_for_everyone=True, ) self.article_one_month_and_one_week = Article.objects.create( - name="One day", + name="One mounth and one week", prix=0, - duration=1, - duration_days=7, - type_cotisation="All", + duration_connection=1, + duration_days_connection=7, + duration_membership=1, + duration_days_membership=7, available_for_everyone=True, ) self.client.login(username="testUser", password="plopiplop") diff --git a/cotisations/utils.py b/cotisations/utils.py index c7ca6187..47a83885 100644 --- a/cotisations/utils.py +++ b/cotisations/utils.py @@ -105,8 +105,8 @@ def send_mail_voucher(invoice, request=None): "lastname": invoice.user.surname, "email": invoice.user.email, "phone": invoice.user.telephone, - "date_end": invoice.get_subscription().latest("date_end").date_end, - "date_begin": invoice.get_subscription().earliest("date_start").date_start, + "date_end": invoice.get_subscription().latest("date_end").date_end_memb, + "date_begin": invoice.get_subscription().earliest("date_start").date_start_memb, } templatename = CotisationsOption.get_cached_value( "voucher_template" @@ -118,7 +118,7 @@ def send_mail_voucher(invoice, request=None): "name": "{} {}".format(invoice.user.name, invoice.user.surname), "asso_email": AssoOption.get_cached_value("contact"), "asso_name": AssoOption.get_cached_value("name"), - "date_end": invoice.get_subscription().latest("date_end").date_end, + "date_end": invoice.get_subscription().latest("date_end_memb").date_end_memb, } mail = EmailMessage( diff --git a/cotisations/views.py b/cotisations/views.py index 680b9483..b90b39e4 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -130,11 +130,12 @@ def new_facture(request, user, userid): facture=new_invoice_instance, name=article.name, prix=article.prix, - type_cotisation=article.type_cotisation, - duration=article.duration, - duration_days=article.duration_days, + duration_connection=article.duration_connection, + duration_days_connection=article.duration_days_connection, + duration_membership=article.duration_membership, + duration_days_membership=article.duration_days_membership, number=quantity, - ) + ) purchases.append(new_purchase) p = find_payment_method(new_invoice_instance.paiement) if hasattr(p, "check_price"): @@ -262,8 +263,10 @@ def new_custom_invoice(request): facture=new_invoice_instance, name=article.name, prix=article.prix, - type_cotisation=article.type_cotisation, - duration=article.duration, + duration_membership=article.duration_membership, + duration_days_membership=article.duration_membership, + duration_connection=article.duration_connection, + duration_days_connection=article.duration_days_connection, number=quantity, ) discount_form.apply_to_invoice(new_invoice_instance) diff --git a/users/models.py b/users/models.py index b6d09b7b..f4e8c0fd 100755 --- a/users/models.py +++ b/users/models.py @@ -703,8 +703,7 @@ class User( facture__in=Facture.objects.filter(user=self).exclude(valid=False) ) ) - .filter(Q(type_cotisation="All") | Q(type_cotisation="Adhesion")) - .aggregate(models.Max("date_end"))["date_end__max"] + .aggregate(models.Max("date_end_memb"))["date_end_memb__max"] ) return date_max @@ -724,8 +723,7 @@ class User( facture__in=Facture.objects.filter(user=self).exclude(valid=False) ) ) - .filter(Q(type_cotisation="All") | Q(type_cotisation="Connexion")) - .aggregate(models.Max("date_end"))["date_end__max"] + .aggregate(models.Max("date_end_con"))["date_end_con__max"] ) return date_max @@ -746,6 +744,10 @@ class User( return False else: return True + # it looks wrong, we should check if there is a cotisation where + # were date_start_memb < timezone.now() < date_end_memb, + # in case the user purshased a cotisation starting in the futur + # somehow def is_connected(self): """Methods, calculate and returns if the user has a valid membership AND a @@ -765,6 +767,10 @@ class User( return False else: return self.is_adherent() + # it looks wrong, we should check if there is a cotisation where + # were date_start_con < timezone.now() < date_end_con, + # in case the user purshased a cotisation starting in the futur + # somehow def end_ban(self): """Methods, calculate and returns the end of a ban value date @@ -926,7 +932,8 @@ class User( """ if self.state == self.STATE_NOT_YET_ACTIVE: if self.facture_set.filter(valid=True).filter( - Q(vente__type_cotisation="All") | Q(vente__type_cotisation="Adhesion") + ~(Q(vente__duration_membership__isnull=True) | Q(vente__duration_membership=0)) | \ + ~(Q(vente__duration_days_membership__isnull=True) | Q(vente__duration_days_membership=0)) ).exists() or OptionalUser.get_cached_value("all_users_active"): self.state = self.STATE_ACTIVE self.save()