From c76f32cf260cd6fd379d24c3aa3e5163c9c9204c Mon Sep 17 00:00:00 2001 From: Hugo Levy-Falk Date: Wed, 2 Oct 2019 23:42:54 +0200 Subject: [PATCH 1/3] Fix #222 --- .../migrations/0040_auto_20191002_2335.py | 26 +++++++++++++++++++ cotisations/models.py | 23 +++++++++++++--- .../templates/cotisations/aff_article.html | 2 ++ cotisations/views.py | 1 + 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 cotisations/migrations/0040_auto_20191002_2335.py 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/models.py b/cotisations/models.py index 3cb64ec3..b9d01b8d 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -460,6 +460,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 +498,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,7 +537,8 @@ 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 @@ -540,7 +549,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 +704,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 +744,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.") ) 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/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) From e0a65e4d044199d414ae2606813fe08fedf4420a Mon Sep 17 00:00:00 2001 From: Hugo Levy-Falk Date: Sun, 3 Nov 2019 21:46:18 +0100 Subject: [PATCH 2/3] Setup tests for Vente model. --- .../migrations/0041_auto_20191103_2131.py | 41 ++++++++++++++++ cotisations/payment_methods/balance/models.py | 2 +- cotisations/payment_methods/cheque/models.py | 2 +- cotisations/payment_methods/comnpay/models.py | 2 +- cotisations/payment_methods/free/models.py | 2 +- .../payment_methods/note_kfet/models.py | 2 +- cotisations/test_models.py | 49 +++++++++++++++++++ cotisations/tests.py | 28 ----------- 8 files changed, 95 insertions(+), 33 deletions(-) create mode 100644 cotisations/migrations/0041_auto_20191103_2131.py create mode 100644 cotisations/test_models.py delete mode 100644 cotisations/tests.py 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/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/test_models.py b/cotisations/test_models.py new file mode 100644 index 00000000..33f32a18 --- /dev/null +++ b/cotisations/test_models.py @@ -0,0 +1,49 @@ +from django.test import TestCase + +import datetime +from django.utils import timezone + +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 tearDown(self): + self.f.delete() + self.user.delete() + self.paiement.delete() + + 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. From 034b50bc58632abba6f1375e06ae394d4870f813 Mon Sep 17 00:00:00 2001 From: Hugo Levy-Falk Date: Mon, 4 Nov 2019 00:25:29 +0100 Subject: [PATCH 3/3] Fix #189 --- cotisations/models.py | 16 +++- cotisations/test_models.py | 43 ++++++++++ cotisations/test_views.py | 166 +++++++++++++++++++++++++++++++++++++ users/test_models.py | 51 ++++++++++++ 4 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 cotisations/test_views.py create mode 100644 users/test_models.py diff --git a/cotisations/models.py b/cotisations/models.py index b9d01b8d..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 \ @@ -540,7 +553,6 @@ class Vente(RevMixin, AclMixin, models.Model): months=(self.duration or 0)*self.number, days=(self.duration_days or 0)*self.number, ) - return def save(self, *args, **kwargs): """ @@ -1042,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/test_models.py b/cotisations/test_models.py index 33f32a18..ed53c0a1 100644 --- a/cotisations/test_models.py +++ b/cotisations/test_models.py @@ -2,6 +2,7 @@ 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 @@ -41,6 +42,48 @@ class VenteModelTests(TestCase): 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() 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/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) + )