8
0
Fork 0
mirror of https://gitlab.federez.net/re2o/re2o synced 2024-05-16 23:56:12 +00:00

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
This commit is contained in:
histausse 2020-09-21 01:21:46 +02:00 committed by Gabriel Detraz
parent 8338790eae
commit b317eceec3
11 changed files with 641 additions and 174 deletions

View file

@ -64,8 +64,10 @@ class VenteSerializer(NamespacedHMSerializer):
"number", "number",
"name", "name",
"prix", "prix",
"duration", "duration_connection",
"type_cotisation", "duration_days_connection",
"duration_membership",
"duration_days_membership",
"prix_total", "prix_total",
"api_url", "api_url",
) )
@ -77,7 +79,7 @@ class ArticleSerializer(NamespacedHMSerializer):
class Meta: class Meta:
model = cotisations.Article 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): class BanqueSerializer(NamespacedHMSerializer):
@ -104,7 +106,7 @@ class CotisationSerializer(NamespacedHMSerializer):
class Meta: class Meta:
model = cotisations.Cotisation 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): class ReminderUsersSerializer(UserSerializer):
@ -124,4 +126,4 @@ class ReminderSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = preferences.Reminder model = preferences.Reminder
fields = ("days", "message", "users_to_remind") fields = ("days", "message", "users_to_remind")

View file

@ -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)'),
),
]

View file

@ -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',
# ),
]

View file

@ -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',
),
]

View file

@ -283,7 +283,8 @@ class Facture(BaseInvoice):
"""Returns every subscription associated with this invoice.""" """Returns every subscription associated with this invoice."""
return Cotisation.objects.filter( return Cotisation.objects.filter(
vente__in=self.vente_set.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(): for purchase in self.vente_set.all():
if hasattr(purchase, "cotisation"): if hasattr(purchase, "cotisation"):
cotisation = purchase.cotisation cotisation = purchase.cotisation
if cotisation.type_cotisation == "Connexion": cotisation.date_start_con = date_con
cotisation.date_start = date_con date_con += relativedelta(
date_con += relativedelta( months=(purchase.duration_connection or 0) * purchase.number,
months=(purchase.duration or 0) * purchase.number, days=(purchase.duration_days_connection or 0) * purchase.number,
days=(purchase.duration_days or 0) * purchase.number, )
) cotisation.date_end_con = date_con
cotisation.date_end = date_con cotisation.date_start_memb = date_adh
elif cotisation.type_cotisation == "Adhesion": date_adh += relativedelta(
cotisation.date_start = date_adh months=(purchase.duration_membership or 0) * purchase.number,
date_adh += relativedelta( days=(purchase.duration_days_membership or 0) * purchase.number,
months=(purchase.duration or 0) * purchase.number, )
days=(purchase.duration_days or 0) * purchase.number, cotisation.date_end_memb = date_adh
)
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.save() cotisation.save()
purchase.facture = self purchase.facture = self
purchase.save() purchase.save()
@ -450,13 +436,6 @@ class Vente(RevMixin, AclMixin, models.Model):
the effect of the purchase on the time agreed for this user) 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 # TODO : change facture to invoice
facture = models.ForeignKey( facture = models.ForeignKey(
"BaseInvoice", on_delete=models.CASCADE, verbose_name=_("invoice") "BaseInvoice", on_delete=models.CASCADE, verbose_name=_("invoice")
@ -465,28 +444,31 @@ class Vente(RevMixin, AclMixin, models.Model):
number = models.IntegerField( number = models.IntegerField(
validators=[MinValueValidator(1)], verbose_name=_("amount") 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")) name = models.CharField(max_length=255, verbose_name=_("article"))
# TODO : change prix to price # TODO : change prix to price
# TODO : this field is not needed if you use Article ForeignKey # TODO : this field is not needed if you use Article ForeignKey
prix = models.DecimalField(max_digits=5, decimal_places=2, verbose_name=_("price")) prix = models.DecimalField(max_digits=5, decimal_places=2, verbose_name=_("price"))
# TODO : this field is not needed if you use Article ForeignKey # TODO : this field is not needed if you use Article ForeignKey
duration = models.PositiveIntegerField( duration_connection = models.PositiveIntegerField(
blank=True, null=True, verbose_name=_("duration (in months)") blank=True, null=True, verbose_name=_("duration of the connection (in months)")
) )
duration_days = models.PositiveIntegerField( duration_days_connection = models.PositiveIntegerField(
blank=True, blank=True,
null=True, null=True,
validators=[MinValueValidator(0)], 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 duration_membership = models.PositiveIntegerField(
type_cotisation = models.CharField( blank=True, null=True, verbose_name=_("duration of the membership (in months)")
choices=COTISATION_TYPE, )
duration_days_membership = models.PositiveIntegerField(
blank=True, blank=True,
null=True, null=True,
max_length=255, validators=[MinValueValidator(0)],
verbose_name=_("subscription type"), verbose_name=_("duration of the membership (in days, will be added to duration in months)"),
) )
class Meta: class Meta:
@ -511,13 +493,17 @@ class Vente(RevMixin, AclMixin, models.Model):
""" """
if hasattr(self, "cotisation"): if hasattr(self, "cotisation"):
cotisation = self.cotisation cotisation = self.cotisation
cotisation.date_end = cotisation.date_start + relativedelta( cotisation.date_end_memb = cotisation.date_start_memb + relativedelta(
months=(self.duration or 0) * self.number, months=(self.duration_membership or 0) * self.number,
days=(self.duration_days 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 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. 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 invoice = self.facture.facture
except Facture.DoesNotExist: except Facture.DoesNotExist:
return 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 = Cotisation(vente=self)
cotisation.type_cotisation = self.type_cotisation if date_start_con:
if date_start: cotisation.date_start_con = date_start_con
cotisation.date_start = date_start cotisation.date_end_con = cotisation.date_start_con + relativedelta(
cotisation.date_end = cotisation.date_start + relativedelta( months=(self.duration_connection or 0) * self.number,
months=(self.duration or 0) * self.number, days=(self.duration_days_connection or 0) * self.number,
days=(self.duration_days 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() self.save()
cotisation.save() cotisation.save()
else: else:
cotisation.date_start = invoice.date cotisation.date_start_con = invoice.date
cotisation.date_end = 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): 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 It also update the associated cotisation in the changes have some
effect on the user's cotisation 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() self.update_cotisation()
super(Vente, self).save(*args, **kwargs) super(Vente, self).save(*args, **kwargs)
@ -629,6 +621,13 @@ class Vente(RevMixin, AclMixin, models.Model):
def __str__(self): def __str__(self):
return str(self.name) + " " + str(self.facture) 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 # TODO : change vente to purchase
@receiver(post_save, sender=Vente) @receiver(post_save, sender=Vente)
@ -645,7 +644,7 @@ def vente_post_save(**kwargs):
if hasattr(purchase, "cotisation"): if hasattr(purchase, "cotisation"):
purchase.cotisation.vente = purchase purchase.cotisation.vente = purchase
purchase.cotisation.save() purchase.cotisation.save()
if purchase.type_cotisation: if purchase.test_membership_or_connection():
purchase.create_cotis() purchase.create_cotis()
purchase.cotisation.save() purchase.cotisation.save()
user = purchase.facture.facture.user user = purchase.facture.facture.user
@ -677,56 +676,54 @@ class Article(RevMixin, AclMixin, models.Model):
It's represented by: It's represented by:
* a name * a name
* a price * a price
* a cotisation type (indicating if this article reprensents a * a duration for the membership
cotisation or not) * a duration for the connection
* a duration (if it is a cotisation)
* a type of user (indicating what kind of user can buy this article) * 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 = ( USER_TYPES = (
("Adherent", _("Member")), ("Adherent", _("Member")),
("Club", _("Club")), ("Club", _("Club")),
("All", _("Both of them")), ("All", _("Both of them")),
) )
COTISATION_TYPE = (
("Connexion", _("Connection")),
("Adhesion", _("Membership")),
("All", _("Both of them")),
)
name = models.CharField(max_length=255, verbose_name=_("designation")) name = models.CharField(max_length=255, verbose_name=_("designation"))
# TODO : change prix to price # TODO : change prix to price
prix = models.DecimalField( prix = models.DecimalField(
max_digits=5, decimal_places=2, verbose_name=_("unit price") max_digits=5, decimal_places=2, verbose_name=_("unit price")
) )
duration = models.PositiveIntegerField(
duration_membership = models.PositiveIntegerField(
blank=True, blank=True,
null=True, null=True,
validators=[MinValueValidator(0)], 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, blank=True,
null=True, null=True,
validators=[MinValueValidator(0)], 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( type_user = models.CharField(
choices=USER_TYPES, choices=USER_TYPES,
default="All", default="All",
max_length=255, max_length=255,
verbose_name=_("type of users concerned"), 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( available_for_everyone = models.BooleanField(
default=False, verbose_name=_("is available for every user") default=False, verbose_name=_("is available for every user")
) )
@ -744,8 +741,6 @@ class Article(RevMixin, AclMixin, models.Model):
def clean(self): def clean(self):
if self.name.lower() == "solde": if self.name.lower() == "solde":
raise ValidationError(_("Solde is a reserved article name.")) 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): def __str__(self):
return self.name return self.name
@ -882,7 +877,7 @@ class Paiement(RevMixin, AclMixin, models.Model):
# In case a cotisation was bought, inform the user, the # In case a cotisation was bought, inform the user, the
# cotisation time has been extended too # 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( messages.success(
request, request,
_( _(
@ -943,31 +938,21 @@ class Cotisation(RevMixin, AclMixin, models.Model):
The model defining a cotisation. It holds information about the time a user The model defining a cotisation. It holds information about the time a user
is allowed when he has paid something. is allowed when he has paid something.
It characterised by : It characterised by :
* a date_start (the date when the cotisaiton begins/began * a date_start_memb (the date when the membership begins/began
* a date_end (the date when the cotisation ends/ended * a date_end_memb (the date when the membership ends/ended
* a type of cotisation (which indicates the implication of such * a date_start_con (the date when the connection begins/began)
cotisation) * a date_end_con (the date when the connection ends/ended)
* a purchase (the related objects this cotisation is linked to) * 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 # TODO : change vente to purchase
vente = models.OneToOneField( vente = models.OneToOneField(
"Vente", on_delete=models.CASCADE, null=True, verbose_name=_("purchase") "Vente", on_delete=models.CASCADE, null=True, verbose_name=_("purchase")
) )
type_cotisation = models.CharField( date_start_con = models.DateTimeField(verbose_name=_("start date for the connection"))
choices=COTISATION_TYPE, date_end_con = models.DateTimeField(verbose_name=_("end date for the connection"))
max_length=255, date_start_memb = models.DateTimeField(verbose_name=_("start date for the membership"))
default="All", date_end_memb = models.DateTimeField(verbose_name=_("end date for the membership"))
verbose_name=_("subscription type"),
)
date_start = models.DateTimeField(verbose_name=_("start date"))
date_end = models.DateTimeField(verbose_name=_("end date"))
class Meta: class Meta:
permissions = ( permissions = (
@ -1037,9 +1022,14 @@ class Cotisation(RevMixin, AclMixin, models.Model):
return ( return (
str(self.vente) str(self.vente)
+ "from " + "from "
+ str(self.date_start) + str(self.date_start_memb)
+ " to " + " 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."
) )

View file

@ -32,9 +32,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<th>{% trans "Article" %}</th> <th>{% trans "Article" %}</th>
<th>{% trans "Price" %}</th> <th>{% trans "Price" %}</th>
<th>{% trans "Subscription type" %}</th> <th>{% trans "Duration membership (in months)" %}</th>
<th>{% trans "Duration (in months)" %}</th> <th>{% trans "Duration membership (in days)" %}</th>
<th>{% trans "Duration (in days)" %}</th> <th>{% trans "Duration connection (in months)" %}</th>
<th>{% trans "Duration connection (in days)" %}</th>
<th>{% trans "Concerned users" %}</th> <th>{% trans "Concerned users" %}</th>
<th>{% trans "Available for everyone" %}</th> <th>{% trans "Available for everyone" %}</th>
<th></th> <th></th>
@ -44,9 +45,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<td>{{ article.name }}</td> <td>{{ article.name }}</td>
<td>{{ article.prix }}</td> <td>{{ article.prix }}</td>
<td>{{ article.type_cotisation }}</td> <td>{{ article.duration_membership }}</td>
<td>{{ article.duration }}</td> <td>{{ article.duration_days_membership }}</td>
<td>{{ article.duration_days }}</td> <td>{{ article.duration_connection }}</td>
<td>{{ article.duration_days_connection }}</td>
<td>{{ article.type_user }}</td> <td>{{ article.type_user }}</td>
<td>{{ article.available_for_everyone | tick }}</td> <td>{{ article.available_for_everyone | tick }}</td>
<td class="text-right"> <td class="text-right">

View file

@ -19,15 +19,17 @@ class VenteModelTests(TestCase):
def test_one_day_cotisation(self): def test_one_day_cotisation(self):
""" """
It should be possible to have one day membership. It should be possible to have one day membership.
Add one day of membership and one day of connection.
""" """
date = timezone.now() date = timezone.now()
purchase = Vente.objects.create( purchase = Vente.objects.create(
facture=self.f, facture=self.f,
number=1, number=1,
name="Test purchase", name="Test purchase",
duration=0, duration_connection=0,
duration_days=1, duration_days_connection=1,
type_cotisation="All", duration_membership=0,
duration_days_membership=1,
prix=0, prix=0,
) )
self.f.reorder_purchases() self.f.reorder_purchases()
@ -36,48 +38,66 @@ class VenteModelTests(TestCase):
datetime.timedelta(days=1), datetime.timedelta(days=1),
delta=datetime.timedelta(seconds=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): def test_one_month_cotisation(self):
""" """
It should be possible to have one day membership. It should be possible to have one day membership.
Add one mounth of membership and one mounth of connection
""" """
date = timezone.now() date = timezone.now()
Vente.objects.create( Vente.objects.create(
facture=self.f, facture=self.f,
number=1, number=1,
name="Test purchase", name="Test purchase",
duration=1, duration_connection=1,
duration_days=0, duration_days_connection=0,
type_cotisation="All", duration_membership=1,
duration_days_membership=0,
prix=0, prix=0,
) )
self.f.reorder_purchases() 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) expected_end = date + relativedelta(months=1)
self.assertEqual(end.day, expected_end.day) self.assertEqual(end_con.day, expected_end.day)
self.assertEqual(end.month, expected_end.month) self.assertEqual(end_con.month, expected_end.month)
self.assertEqual(end.year, expected_end.year) 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): def test_one_month_and_one_week_cotisation(self):
""" """
It should be possible to have one day membership. 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() date = timezone.now()
Vente.objects.create( Vente.objects.create(
facture=self.f, facture=self.f,
number=1, number=1,
name="Test purchase", name="Test purchase",
duration=1, duration_connection=1,
duration_days=7, duration_days_connection=7,
type_cotisation="All", duration_membership=1,
duration_days_membership=7,
prix=0, prix=0,
) )
self.f.reorder_purchases() 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) expected_end = date + relativedelta(months=1, days=7)
self.assertEqual(end.day, expected_end.day) self.assertEqual(end_con.day, expected_end.day)
self.assertEqual(end.month, expected_end.month) self.assertEqual(end_con.month, expected_end.month)
self.assertEqual(end.year, expected_end.year) 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): def test_date_start_cotisation(self):
""" """
@ -87,15 +107,140 @@ class VenteModelTests(TestCase):
facture=self.f, facture=self.f,
number=1, number=1,
name="Test purchase", name="Test purchase",
duration=0, duration_connection=0,
duration_days=1, duration_days_connection=1,
type_cotisation = 'All', duration_membership=0,
duration_deys_membership=1,
prix=0 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() 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): def tearDown(self):
self.f.delete() self.f.delete()
@ -121,9 +266,10 @@ class FactureModelTests(TestCase):
facture=invoice1, facture=invoice1,
number=1, number=1,
name="Test purchase", name="Test purchase",
duration=1, duration_connection=1,
duration_days=0, duration_days_connection=0,
type_cotisation="All", duration_membership=1,
duration_days_membership=0,
prix=0, prix=0,
) )
invoice1.reorder_purchases() invoice1.reorder_purchases()
@ -134,16 +280,20 @@ class FactureModelTests(TestCase):
facture=invoice2, facture=invoice2,
number=1, number=1,
name="Test purchase", name="Test purchase",
duration=1, duration_connection=1,
duration_days=0, duration_days_connection=0,
type_cotisation="All", duration_membership=1,
duration_days_membership=0,
prix=0, prix=0,
) )
invoice1.reorder_purchases() invoice1.reorder_purchases()
delta = relativedelta(self.user.end_connexion(), date) delta_con = relativedelta(self.user.end_connexion(), date)
delta.microseconds = 0 delta_memb = relativedelta(self.user.end_adhesion(), date)
delta_con.microseconds = 0
delta_memb.microseconds = 0
try: 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: except Exception as e:
invoice1.delete() invoice1.delete()
invoice2.delete() invoice2.delete()

View file

@ -38,25 +38,28 @@ class NewFactureTests(TestCase):
self.article_one_day = Article.objects.create( self.article_one_day = Article.objects.create(
name="One day", name="One day",
prix=0, prix=0,
duration=0, duration_connection=0,
duration_days=1, duration_days_connection=1,
type_cotisation="All", duration_membership=0,
duration_days_membership=1,
available_for_everyone=True, available_for_everyone=True,
) )
self.article_one_month = Article.objects.create( self.article_one_month = Article.objects.create(
name="One day", name="One mounth",
prix=0, prix=0,
duration=1, duration_connection=1,
duration_days=0, duration_days_connection=0,
type_cotisation="All", duration_membership=1,
duration_days_membership=0,
available_for_everyone=True, available_for_everyone=True,
) )
self.article_one_month_and_one_week = Article.objects.create( self.article_one_month_and_one_week = Article.objects.create(
name="One day", name="One mounth and one week",
prix=0, prix=0,
duration=1, duration_connection=1,
duration_days=7, duration_days_connection=7,
type_cotisation="All", duration_membership=1,
duration_days_membership=7,
available_for_everyone=True, available_for_everyone=True,
) )
self.client.login(username="testUser", password="plopiplop") self.client.login(username="testUser", password="plopiplop")

View file

@ -105,8 +105,8 @@ def send_mail_voucher(invoice, request=None):
"lastname": invoice.user.surname, "lastname": invoice.user.surname,
"email": invoice.user.email, "email": invoice.user.email,
"phone": invoice.user.telephone, "phone": invoice.user.telephone,
"date_end": invoice.get_subscription().latest("date_end").date_end, "date_end": invoice.get_subscription().latest("date_end").date_end_memb,
"date_begin": invoice.get_subscription().earliest("date_start").date_start, "date_begin": invoice.get_subscription().earliest("date_start").date_start_memb,
} }
templatename = CotisationsOption.get_cached_value( templatename = CotisationsOption.get_cached_value(
"voucher_template" "voucher_template"
@ -118,7 +118,7 @@ def send_mail_voucher(invoice, request=None):
"name": "{} {}".format(invoice.user.name, invoice.user.surname), "name": "{} {}".format(invoice.user.name, invoice.user.surname),
"asso_email": AssoOption.get_cached_value("contact"), "asso_email": AssoOption.get_cached_value("contact"),
"asso_name": AssoOption.get_cached_value("name"), "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( mail = EmailMessage(

View file

@ -130,11 +130,12 @@ def new_facture(request, user, userid):
facture=new_invoice_instance, facture=new_invoice_instance,
name=article.name, name=article.name,
prix=article.prix, prix=article.prix,
type_cotisation=article.type_cotisation, duration_connection=article.duration_connection,
duration=article.duration, duration_days_connection=article.duration_days_connection,
duration_days=article.duration_days, duration_membership=article.duration_membership,
duration_days_membership=article.duration_days_membership,
number=quantity, number=quantity,
) )
purchases.append(new_purchase) purchases.append(new_purchase)
p = find_payment_method(new_invoice_instance.paiement) p = find_payment_method(new_invoice_instance.paiement)
if hasattr(p, "check_price"): if hasattr(p, "check_price"):
@ -262,8 +263,10 @@ def new_custom_invoice(request):
facture=new_invoice_instance, facture=new_invoice_instance,
name=article.name, name=article.name,
prix=article.prix, prix=article.prix,
type_cotisation=article.type_cotisation, duration_membership=article.duration_membership,
duration=article.duration, duration_days_membership=article.duration_membership,
duration_connection=article.duration_connection,
duration_days_connection=article.duration_days_connection,
number=quantity, number=quantity,
) )
discount_form.apply_to_invoice(new_invoice_instance) discount_form.apply_to_invoice(new_invoice_instance)

View file

@ -703,8 +703,7 @@ class User(
facture__in=Facture.objects.filter(user=self).exclude(valid=False) facture__in=Facture.objects.filter(user=self).exclude(valid=False)
) )
) )
.filter(Q(type_cotisation="All") | Q(type_cotisation="Adhesion")) .aggregate(models.Max("date_end_memb"))["date_end_memb__max"]
.aggregate(models.Max("date_end"))["date_end__max"]
) )
return date_max return date_max
@ -724,8 +723,7 @@ class User(
facture__in=Facture.objects.filter(user=self).exclude(valid=False) facture__in=Facture.objects.filter(user=self).exclude(valid=False)
) )
) )
.filter(Q(type_cotisation="All") | Q(type_cotisation="Connexion")) .aggregate(models.Max("date_end_con"))["date_end_con__max"]
.aggregate(models.Max("date_end"))["date_end__max"]
) )
return date_max return date_max
@ -746,6 +744,10 @@ class User(
return False return False
else: else:
return True 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): def is_connected(self):
"""Methods, calculate and returns if the user has a valid membership AND a """Methods, calculate and returns if the user has a valid membership AND a
@ -765,6 +767,10 @@ class User(
return False return False
else: else:
return self.is_adherent() 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): def end_ban(self):
"""Methods, calculate and returns the end of a ban value date """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.state == self.STATE_NOT_YET_ACTIVE:
if self.facture_set.filter(valid=True).filter( 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"): ).exists() or OptionalUser.get_cached_value("all_users_active"):
self.state = self.STATE_ACTIVE self.state = self.STATE_ACTIVE
self.save() self.save()