8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-25 22:22:26 +00:00

Merge branch '222-allow-short-time-membership' into 'dev'

Fix #222

See merge request federez/re2o!454
This commit is contained in:
chirac 2019-11-04 16:45:37 +01:00
commit 180ba11a51
14 changed files with 417 additions and 39 deletions

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.23 on 2019-10-02 21:35
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0039_freepayment'),
]
operations = [
migrations.AddField(
model_name='article',
name='duration_days',
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='duration (in days, will be added to duration in months)'),
),
migrations.AddField(
model_name='vente',
name='duration_days',
field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='duration (in days, will be added to duration in months)'),
),
]

View file

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.23 on 2019-11-03 20:31
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0040_auto_20191002_2335'),
]
operations = [
migrations.AlterField(
model_name='balancepayment',
name='payment',
field=models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method_balance', to='cotisations.Paiement'),
),
migrations.AlterField(
model_name='chequepayment',
name='payment',
field=models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method_cheque', to='cotisations.Paiement'),
),
migrations.AlterField(
model_name='comnpaypayment',
name='payment',
field=models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method_comnpay', to='cotisations.Paiement'),
),
migrations.AlterField(
model_name='freepayment',
name='payment',
field=models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method_free', to='cotisations.Paiement'),
),
migrations.AlterField(
model_name='notepayment',
name='payment',
field=models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method_note', to='cotisations.Paiement'),
),
]

View file

@ -290,9 +290,22 @@ class Facture(BaseInvoice):
"""Returns True if this invoice contains at least one subscribtion.""" """Returns True if this invoice contains at least one subscribtion."""
return bool(self.get_subscription()) return bool(self.get_subscription())
def reorder_purchases(self):
date = self.date
for purchase in self.vente_set.all():
if hasattr(purchase, 'cotisation'):
cotisation = purchase.cotisation
cotisation.date_start = date
date += relativedelta(
months=(purchase.duration or 0)*purchase.number,
days=(purchase.duration_days or 0)*purchase.number,
)
purchase.save()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super(Facture, self).save(*args, **kwargs) super(Facture, self).save(*args, **kwargs)
if not self.__original_valid and self.valid: if not self.__original_valid and self.valid:
self.reorder_purchases()
send_mail_invoice(self) send_mail_invoice(self)
if self.is_subscription() \ if self.is_subscription() \
and not self.__original_control \ and not self.__original_control \
@ -460,6 +473,12 @@ class Vente(RevMixin, AclMixin, models.Model):
null=True, null=True,
verbose_name=_("duration (in months)") verbose_name=_("duration (in months)")
) )
duration_days = models.PositiveIntegerField(
blank=True,
null=True,
validators=[MinValueValidator(0)],
verbose_name=_("duration (in days, will be added to duration in months)")
)
# TODO : this field is not needed if you use Article ForeignKey # TODO : this field is not needed if you use Article ForeignKey
type_cotisation = models.CharField( type_cotisation = models.CharField(
choices=COTISATION_TYPE, choices=COTISATION_TYPE,
@ -492,7 +511,9 @@ 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 = cotisation.date_start + relativedelta(
months=self.duration*self.number) months=(self.duration or 0)*self.number,
days=(self.duration_days or 0)*self.number,
)
return return
def create_cotis(self, date_start=False): def create_cotis(self, date_start=False):
@ -529,9 +550,9 @@ class Vente(RevMixin, AclMixin, models.Model):
date_max = max(end_cotisation, date_start) date_max = max(end_cotisation, date_start)
cotisation.date_start = date_max cotisation.date_start = date_max
cotisation.date_end = cotisation.date_start + relativedelta( cotisation.date_end = cotisation.date_start + relativedelta(
months=self.duration*self.number months=(self.duration or 0)*self.number,
days=(self.duration_days or 0)*self.number,
) )
return
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" """
@ -540,7 +561,7 @@ class Vente(RevMixin, AclMixin, models.Model):
effect on the user's cotisation effect on the user's cotisation
""" """
# Checking that if a cotisation is specified, there is also a duration # Checking that if a cotisation is specified, there is also a duration
if self.type_cotisation and not self.duration: if self.type_cotisation and not (self.duration or self.duration_days):
raise ValidationError( raise ValidationError(
_("Duration must be specified for a subscription.") _("Duration must be specified for a subscription.")
) )
@ -695,6 +716,12 @@ class Article(RevMixin, AclMixin, models.Model):
validators=[MinValueValidator(0)], validators=[MinValueValidator(0)],
verbose_name=_("duration (in months)") verbose_name=_("duration (in months)")
) )
duration_days = models.PositiveIntegerField(
blank=True,
null=True,
validators=[MinValueValidator(0)],
verbose_name=_("duration (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',
@ -729,7 +756,7 @@ class Article(RevMixin, AclMixin, models.Model):
raise ValidationError( raise ValidationError(
_("Balance is a reserved article name.") _("Balance is a reserved article name.")
) )
if self.type_cotisation and not self.duration: if self.type_cotisation and not (self.duration or self.duration_days):
raise ValidationError( raise ValidationError(
_("Duration must be specified for a subscription.") _("Duration must be specified for a subscription.")
) )
@ -1027,7 +1054,7 @@ class Cotisation(RevMixin, AclMixin, models.Model):
return True, None, None return True, None, None
def __str__(self): def __str__(self):
return str(self.vente) return str(self.vente) + "from " + str(self.date_start) + " to " + str(self.date_end)
@receiver(post_save, sender=Cotisation) @receiver(post_save, sender=Cotisation)

View file

@ -40,7 +40,7 @@ class BalancePayment(PaymentMethodMixin, models.Model):
payment = models.OneToOneField( payment = models.OneToOneField(
Paiement, Paiement,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='payment_method', related_name='payment_method_balance',
editable=False editable=False
) )
minimum_balance = models.DecimalField( minimum_balance = models.DecimalField(

View file

@ -38,7 +38,7 @@ class ChequePayment(PaymentMethodMixin, models.Model):
payment = models.OneToOneField( payment = models.OneToOneField(
Paiement, Paiement,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='payment_method', related_name='payment_method_cheque',
editable=False editable=False
) )

View file

@ -41,7 +41,7 @@ class ComnpayPayment(PaymentMethodMixin, models.Model):
payment = models.OneToOneField( payment = models.OneToOneField(
Paiement, Paiement,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='payment_method', related_name='payment_method_comnpay',
editable=False editable=False
) )
payment_credential = models.CharField( payment_credential = models.CharField(

View file

@ -38,7 +38,7 @@ class FreePayment(PaymentMethodMixin, models.Model):
payment = models.OneToOneField( payment = models.OneToOneField(
Paiement, Paiement,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='payment_method', related_name='payment_method_free',
editable=False editable=False
) )

View file

@ -42,7 +42,7 @@ class NotePayment(PaymentMethodMixin, models.Model):
payment = models.OneToOneField( payment = models.OneToOneField(
Paiement, Paiement,
on_delete = models.CASCADE, on_delete = models.CASCADE,
related_name = 'payment_method', related_name = 'payment_method_note',
editable = False editable = False
) )
server = models.CharField( server = models.CharField(

View file

@ -34,6 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>{% trans "Price" %}</th> <th>{% trans "Price" %}</th>
<th>{% trans "Subscription type" %}</th> <th>{% trans "Subscription type" %}</th>
<th>{% trans "Duration (in months)" %}</th> <th>{% trans "Duration (in months)" %}</th>
<th>{% trans "Duration (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>
@ -45,6 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ article.prix }}</td> <td>{{ article.prix }}</td>
<td>{{ article.type_cotisation }}</td> <td>{{ article.type_cotisation }}</td>
<td>{{ article.duration }}</td> <td>{{ article.duration }}</td>
<td>{{ article.duration_days }}</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

@ -0,0 +1,92 @@
from django.test import TestCase
import datetime
from django.utils import timezone
from dateutil.relativedelta import relativedelta
from users.models import User
from .models import Vente, Facture, Cotisation, Paiement
class VenteModelTests(TestCase):
def setUp(self):
self.user = User.objects.create(
pseudo="testUser",
email="test@example.org"
)
self.paiement = Paiement.objects.create(
moyen="test payment"
)
self.f = Facture.objects.create(
user=self.user,
paiement=self.paiement,
valid=True
)
def test_one_day_cotisation(self):
"""
It should be possible to have one day membership.
"""
date = timezone.now()
purchase = Vente.objects.create(
facture=self.f,
number=1,
name="Test purchase",
duration=0,
duration_days=1,
type_cotisation="All",
prix=0,
)
self.assertAlmostEqual(
self.user.end_connexion() - 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.
"""
date = timezone.now()
purchase = Vente.objects.create(
facture=self.f,
number=1,
name="Test purchase",
duration=1,
duration_days=0,
type_cotisation="All",
prix=0,
)
delta = relativedelta(self.user.end_connexion(), date)
delta.microseconds=0
self.assertEqual(
delta,
relativedelta(months=1),
)
def test_one_month_and_one_week_cotisation(self):
"""
It should be possible to have one day membership.
"""
date = timezone.now()
purchase = Vente.objects.create(
facture=self.f,
number=1,
name="Test purchase",
duration=1,
duration_days=7,
type_cotisation="All",
prix=0,
)
delta = relativedelta(self.user.end_connexion(), date)
delta.microseconds=0
self.assertEqual(
delta,
relativedelta(months=1, days=7),
)
def tearDown(self):
self.f.delete()
self.user.delete()
self.paiement.delete()

166
cotisations/test_views.py Normal file
View file

@ -0,0 +1,166 @@
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import Permission
import datetime
from dateutil.relativedelta import relativedelta
from django.utils import timezone
from users.models import Adherent
from .models import Vente, Facture, Cotisation, Paiement, Article
class NewFactureTests(TestCase):
def tearDown(self):
self.user.facture_set.all().delete()
self.user.delete()
self.paiement.delete()
self.article_one_day.delete()
self.article_one_month.delete()
self.article_one_month_and_one_week.delete()
def setUp(self):
self.user = Adherent.objects.create(
pseudo="testUser",
email="test@example.org",
)
self.user.set_password('plopiplop')
self.user.user_permissions.set(
[
Permission.objects.get_by_natural_key("add_facture", "cotisations", "Facture"),
Permission.objects.get_by_natural_key("use_every_payment", "cotisations", "Paiement"),
]
)
self.user.save()
self.paiement = Paiement.objects.create(
moyen="test payment",
)
self.article_one_day = Article.objects.create(
name="One day",
prix=0,
duration=0,
duration_days=1,
type_cotisation='All',
available_for_everyone=True
)
self.article_one_month = Article.objects.create(
name="One day",
prix=0,
duration=1,
duration_days=0,
type_cotisation='All',
available_for_everyone=True
)
self.article_one_month_and_one_week = Article.objects.create(
name="One day",
prix=0,
duration=1,
duration_days=7,
type_cotisation='All',
available_for_everyone=True
)
self.client.login(
username="testUser",
password="plopiplop"
)
def test_invoice_with_one_day(self):
data = {
"Facture-paiement": self.paiement.pk,
"form-TOTAL_FORMS": 1,
"form-INITIAL_FORMS": 0,
"form-MIN_NUM_FORMS": 0,
"form-MAX_NUM_FORMS": 1000,
"form-0-article": 1,
"form-0-quantity": 1,
}
date = timezone.now()
response = self.client.post(reverse('cotisations:new-facture', kwargs={'userid':self.user.pk}), data)
self.assertEqual(
response.status_code,
302
)
self.assertEqual(
response.url,
"/users/profil/%d"%self.user.pk
)
self.assertAlmostEqual(
self.user.end_connexion() - date,
datetime.timedelta(days=1),
delta=datetime.timedelta(seconds=1)
)
def test_invoice_with_one_month(self):
data = {
"Facture-paiement": self.paiement.pk,
"form-TOTAL_FORMS": 1,
"form-INITIAL_FORMS": 0,
"form-MIN_NUM_FORMS": 0,
"form-MAX_NUM_FORMS": 1000,
"form-0-article": 2,
"form-0-quantity": 1,
}
date = timezone.now()
response = self.client.post(reverse('cotisations:new-facture', kwargs={'userid':self.user.pk}), data)
self.assertEqual(
response.status_code,
302
)
self.assertEqual(
response.url,
"/users/profil/%d"%self.user.pk
)
delta = relativedelta(self.user.end_connexion(), date)
delta.microseconds=0
self.assertEqual(
delta,
relativedelta(months=1),
)
def test_invoice_with_one_month_and_one_week(self):
data = {
"Facture-paiement": self.paiement.pk,
"form-TOTAL_FORMS": 2,
"form-INITIAL_FORMS": 0,
"form-MIN_NUM_FORMS": 0,
"form-MAX_NUM_FORMS": 1000,
"form-0-article": 1,
"form-0-quantity": 7,
"form-1-article": 2,
"form-1-quantity": 1,
}
date = timezone.now()
response = self.client.post(reverse('cotisations:new-facture', kwargs={'userid':self.user.pk}), data)
self.assertEqual(
response.status_code,
302
)
self.assertEqual(
response.url,
"/users/profil/%d"%self.user.pk
)
invoice = self.user.facture_set.first()
delta = relativedelta(self.user.end_connexion(), date)
delta.microseconds=0
self.assertEqual(
delta,
relativedelta(months=1, days=7),
)
def test_several_articles_creates_several_purchases(self):
data = {
"Facture-paiement": self.paiement.pk,
"form-TOTAL_FORMS": 2,
"form-INITIAL_FORMS": 0,
"form-MIN_NUM_FORMS": 0,
"form-MAX_NUM_FORMS": 1000,
"form-0-article": 2,
"form-0-quantity": 1,
"form-1-article": 2,
"form-1-quantity": 1,
}
response = self.client.post(reverse('cotisations:new-facture', kwargs={'userid':self.user.pk}), data)
f = self.user.facture_set.first()
self.assertEqual(f.vente_set.count(), 2)

View file

@ -1,28 +0,0 @@
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Lara Kermarec
# Copyright © 2017 Augustin Lemesle
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""cotisations.tests
The tests for the Cotisations module.
"""
# from django.test import TestCase
# Create your tests here.

View file

@ -139,6 +139,7 @@ def new_facture(request, user, userid):
prix=article.prix, prix=article.prix,
type_cotisation=article.type_cotisation, type_cotisation=article.type_cotisation,
duration=article.duration, duration=article.duration,
duration_days=article.duration_days,
number=quantity number=quantity
) )
purchases.append(new_purchase) purchases.append(new_purchase)

51
users/test_models.py Normal file
View file

@ -0,0 +1,51 @@
from django.test import TestCase
import datetime
from django.utils import timezone
from users.models import User
from cotisations.models import Vente, Facture, Paiement
class UserModelTests(TestCase):
def setUp(self):
self.user = User.objects.create(
pseudo="testUser"
)
def tearDown(self):
self.user.facture_set.all().delete()
self.user.delete()
def test_multiple_cotisations_are_taken_into_account(self):
paiement = Paiement.objects.create(
moyen="test payment"
)
invoice = Facture.objects.create(
user=self.user,
paiement=paiement,
valid=True
)
date = timezone.now()
purchase1 = Vente.objects.create(
facture=invoice,
number=1,
name="Test purchase",
duration=0,
duration_days=1,
type_cotisation="All",
prix=0,
)
purchase2 = Vente.objects.create(
facture=invoice,
number=1,
name="Test purchase",
duration=0,
duration_days=1,
type_cotisation="All",
prix=0,
)
self.assertAlmostEqual(
self.user.end_connexion() - date,
datetime.timedelta(days=2),
delta=datetime.timedelta(seconds=1)
)