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..b5a698cd 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 @@ -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 @@ -882,7 +877,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 +938,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 +1022,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.,