diff --git a/cotisations/migrations/0040_auto_20191002_2335.py b/cotisations/migrations/0040_auto_20191002_2335.py
new file mode 100644
index 00000000..56c99f69
--- /dev/null
+++ b/cotisations/migrations/0040_auto_20191002_2335.py
@@ -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)'),
+ ),
+ ]
diff --git a/cotisations/migrations/0041_auto_20191103_2131.py b/cotisations/migrations/0041_auto_20191103_2131.py
new file mode 100644
index 00000000..09f763ed
--- /dev/null
+++ b/cotisations/migrations/0041_auto_20191103_2131.py
@@ -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'),
+ ),
+ ]
diff --git a/cotisations/models.py b/cotisations/models.py
index 3cb64ec3..77337a84 100644
--- a/cotisations/models.py
+++ b/cotisations/models.py
@@ -290,9 +290,22 @@ class Facture(BaseInvoice):
"""Returns True if this invoice contains at least one subscribtion."""
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):
super(Facture, self).save(*args, **kwargs)
if not self.__original_valid and self.valid:
+ self.reorder_purchases()
send_mail_invoice(self)
if self.is_subscription() \
and not self.__original_control \
@@ -460,6 +473,12 @@ class Vente(RevMixin, AclMixin, models.Model):
null=True,
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
type_cotisation = models.CharField(
choices=COTISATION_TYPE,
@@ -492,7 +511,9 @@ class Vente(RevMixin, AclMixin, models.Model):
if hasattr(self, 'cotisation'):
cotisation = self.cotisation
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 create_cotis(self, date_start=False):
@@ -529,9 +550,9 @@ class Vente(RevMixin, AclMixin, models.Model):
date_max = max(end_cotisation, date_start)
cotisation.date_start = date_max
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):
"""
@@ -540,7 +561,7 @@ class Vente(RevMixin, AclMixin, models.Model):
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:
+ if self.type_cotisation and not (self.duration or self.duration_days):
raise ValidationError(
_("Duration must be specified for a subscription.")
)
@@ -695,6 +716,12 @@ class Article(RevMixin, AclMixin, models.Model):
validators=[MinValueValidator(0)],
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(
choices=USER_TYPES,
default='All',
@@ -729,7 +756,7 @@ class Article(RevMixin, AclMixin, models.Model):
raise ValidationError(
_("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(
_("Duration must be specified for a subscription.")
)
@@ -1027,7 +1054,7 @@ class Cotisation(RevMixin, AclMixin, models.Model):
return True, None, None
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)
diff --git a/cotisations/payment_methods/balance/models.py b/cotisations/payment_methods/balance/models.py
index 221cca3e..e3e05ea4 100644
--- a/cotisations/payment_methods/balance/models.py
+++ b/cotisations/payment_methods/balance/models.py
@@ -40,7 +40,7 @@ class BalancePayment(PaymentMethodMixin, models.Model):
payment = models.OneToOneField(
Paiement,
on_delete=models.CASCADE,
- related_name='payment_method',
+ related_name='payment_method_balance',
editable=False
)
minimum_balance = models.DecimalField(
diff --git a/cotisations/payment_methods/cheque/models.py b/cotisations/payment_methods/cheque/models.py
index 8f00ff46..a834bb93 100644
--- a/cotisations/payment_methods/cheque/models.py
+++ b/cotisations/payment_methods/cheque/models.py
@@ -38,7 +38,7 @@ class ChequePayment(PaymentMethodMixin, models.Model):
payment = models.OneToOneField(
Paiement,
on_delete=models.CASCADE,
- related_name='payment_method',
+ related_name='payment_method_cheque',
editable=False
)
diff --git a/cotisations/payment_methods/comnpay/models.py b/cotisations/payment_methods/comnpay/models.py
index 7fac089a..a5568f22 100644
--- a/cotisations/payment_methods/comnpay/models.py
+++ b/cotisations/payment_methods/comnpay/models.py
@@ -41,7 +41,7 @@ class ComnpayPayment(PaymentMethodMixin, models.Model):
payment = models.OneToOneField(
Paiement,
on_delete=models.CASCADE,
- related_name='payment_method',
+ related_name='payment_method_comnpay',
editable=False
)
payment_credential = models.CharField(
diff --git a/cotisations/payment_methods/free/models.py b/cotisations/payment_methods/free/models.py
index 46ecca87..2931faad 100644
--- a/cotisations/payment_methods/free/models.py
+++ b/cotisations/payment_methods/free/models.py
@@ -38,7 +38,7 @@ class FreePayment(PaymentMethodMixin, models.Model):
payment = models.OneToOneField(
Paiement,
on_delete=models.CASCADE,
- related_name='payment_method',
+ related_name='payment_method_free',
editable=False
)
diff --git a/cotisations/payment_methods/note_kfet/models.py b/cotisations/payment_methods/note_kfet/models.py
index be54bd54..0e2ebea1 100644
--- a/cotisations/payment_methods/note_kfet/models.py
+++ b/cotisations/payment_methods/note_kfet/models.py
@@ -42,7 +42,7 @@ class NotePayment(PaymentMethodMixin, models.Model):
payment = models.OneToOneField(
Paiement,
on_delete = models.CASCADE,
- related_name = 'payment_method',
+ related_name = 'payment_method_note',
editable = False
)
server = models.CharField(
diff --git a/cotisations/templates/cotisations/aff_article.html b/cotisations/templates/cotisations/aff_article.html
index df187abb..7ead24dc 100644
--- a/cotisations/templates/cotisations/aff_article.html
+++ b/cotisations/templates/cotisations/aff_article.html
@@ -34,6 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Price" %} |
{% trans "Subscription type" %} |
{% trans "Duration (in months)" %} |
+ {% trans "Duration (in days)" %} |
{% trans "Concerned users" %} |
{% trans "Available for everyone" %} |
|
@@ -45,6 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{{ article.prix }} |
{{ article.type_cotisation }} |
{{ article.duration }} |
+ {{ article.duration_days }} |
{{ article.type_user }} |
{{ article.available_for_everyone | tick }} |
diff --git a/cotisations/test_models.py b/cotisations/test_models.py
new file mode 100644
index 00000000..ed53c0a1
--- /dev/null
+++ b/cotisations/test_models.py
@@ -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()
+
+
diff --git a/cotisations/test_views.py b/cotisations/test_views.py
new file mode 100644
index 00000000..ba24c4d6
--- /dev/null
+++ b/cotisations/test_views.py
@@ -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)
diff --git a/cotisations/tests.py b/cotisations/tests.py
deleted file mode 100644
index b2ecee78..00000000
--- a/cotisations/tests.py
+++ /dev/null
@@ -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.
diff --git a/cotisations/views.py b/cotisations/views.py
index 437d5df1..2a262704 100644
--- a/cotisations/views.py
+++ b/cotisations/views.py
@@ -139,6 +139,7 @@ def new_facture(request, user, userid):
prix=article.prix,
type_cotisation=article.type_cotisation,
duration=article.duration,
+ duration_days=article.duration_days,
number=quantity
)
purchases.append(new_purchase)
diff --git a/users/test_models.py b/users/test_models.py
new file mode 100644
index 00000000..e9cb7d7b
--- /dev/null
+++ b/users/test_models.py
@@ -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)
+ )
|