From 05501c90d9ce4fd739442c805162acae2933727c Mon Sep 17 00:00:00 2001 From: Hugo Levy-Falk Date: Sun, 15 Dec 2019 15:23:53 +0000 Subject: [PATCH] Fix Overlapping invoices. --- cotisations/models.py | 73 ++++++++++++++++++------------------- cotisations/test_models.py | 75 +++++++++++++++++++++++++++++++++----- cotisations/views.py | 2 + 3 files changed, 103 insertions(+), 47 deletions(-) diff --git a/cotisations/models.py b/cotisations/models.py index c2d23171..9865e905 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -292,15 +292,40 @@ class Facture(BaseInvoice): return bool(self.get_subscription()) def reorder_purchases(self): - date = self.date + date_adh = max(self.date, self.user.end_adhesion()) + date_con = max(self.date, self.user.end_connexion()) 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, - ) + if cotisation.type_cotisation == 'Connexion': + cotisation.date_start = date_con + date_con += relativedelta( + months=(purchase.duration or 0) * purchase.number, + days=(purchase.duration_days or 0) * purchase.number, + ) + cotisation.date_end = date_con + elif cotisation.type_cotisation == 'Adhesion': + cotisation.date_start = date_adh + date_adh += relativedelta( + months=(purchase.duration or 0) * purchase.number, + days=(purchase.duration_days or 0) * purchase.number, + ) + cotisation.date_end = date_adh + else: # it is assumed that adhesion is required for a connexion + date = min(date_adh, date_con) + cotisation.date_start = date + date_adh += relativedelta( + months=(purchase.duration or 0) * purchase.number, + days=(purchase.duration_days or 0) * purchase.number, + ) + date_con += relativedelta( + months=(purchase.duration or 0) * purchase.number, + days=(purchase.duration_days or 0) * purchase.number, + ) + date = max(date_adh, date_con) + cotisation.date_end = date + cotisation.save() + purchase.facture = self purchase.save() def save(self, *args, **kwargs): @@ -486,11 +511,9 @@ class Vente(RevMixin, AclMixin, models.Model): ) return - def create_cotis(self, date_start=False): + def create_cotis(self): """ - Update and create a 'cotisation' related object if there is a - cotisation_type defined (which means the article sold represents - a cotisation) + 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. """ try: invoice = self.facture.facture @@ -499,34 +522,8 @@ class Vente(RevMixin, AclMixin, models.Model): if not hasattr(self, "cotisation") and self.type_cotisation: cotisation = Cotisation(vente=self) cotisation.type_cotisation = self.type_cotisation - if date_start: - end_cotisation = ( - Cotisation.objects.filter( - vente__in=Vente.objects.filter( - facture__in=Facture.objects.filter( - user=invoice.user - ).exclude(valid=False) - ) - ) - .filter( - Q(type_cotisation="All") - | Q(type_cotisation=self.type_cotisation) - ) - .filter(date_start__lt=date_start) - .aggregate(Max("date_end"))["date_end__max"] - ) - elif self.type_cotisation == "Adhesion": - end_cotisation = invoice.user.end_adhesion() - else: - end_cotisation = invoice.user.end_connexion() - date_start = date_start or timezone.now() - end_cotisation = end_cotisation or date_start - date_max = max(end_cotisation, date_start) - cotisation.date_start = date_max - cotisation.date_end = cotisation.date_start + relativedelta( - months=(self.duration or 0) * self.number, - days=(self.duration_days or 0) * self.number, - ) + cotisation.date_start = invoice.date + cotisation.date_end = invoice.date def save(self, *args, **kwargs): """ diff --git a/cotisations/test_models.py b/cotisations/test_models.py index f162ddf1..bc86b7a7 100644 --- a/cotisations/test_models.py +++ b/cotisations/test_models.py @@ -10,7 +10,7 @@ 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.user = User.objects.create(pseudo="testUserPlop", 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 @@ -30,6 +30,7 @@ class VenteModelTests(TestCase): type_cotisation="All", prix=0, ) + self.f.reorder_purchases() self.assertAlmostEqual( self.user.end_connexion() - date, datetime.timedelta(days=1), @@ -41,7 +42,7 @@ class VenteModelTests(TestCase): It should be possible to have one day membership. """ date = timezone.now() - purchase = Vente.objects.create( + Vente.objects.create( facture=self.f, number=1, name="Test purchase", @@ -50,16 +51,19 @@ class VenteModelTests(TestCase): type_cotisation="All", prix=0, ) - delta = relativedelta(self.user.end_connexion(), date) - delta.microseconds = 0 - self.assertEqual(delta, relativedelta(months=1)) + self.f.reorder_purchases() + end = self.user.end_connexion() + expected_end = date + relativedelta(months=1) + self.assertEqual(end.day, expected_end.day) + self.assertEqual(end.month, expected_end.month) + self.assertEqual(end.year, expected_end.year) 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( + Vente.objects.create( facture=self.f, number=1, name="Test purchase", @@ -68,11 +72,64 @@ class VenteModelTests(TestCase): type_cotisation="All", prix=0, ) - delta = relativedelta(self.user.end_connexion(), date) - delta.microseconds = 0 - self.assertEqual(delta, relativedelta(months=1, days=7)) + self.f.reorder_purchases() + end = self.user.end_connexion() + expected_end = date + relativedelta(months=1, days=7) + self.assertEqual(end.day, expected_end.day) + self.assertEqual(end.month, expected_end.month) + self.assertEqual(end.year, expected_end.year) def tearDown(self): self.f.delete() self.user.delete() self.paiement.delete() + + +class FactureModelTests(TestCase): + def setUp(self): + self.user = User.objects.create(pseudo="testUserPlop", email="test@example.org") + self.paiement = Paiement.objects.create(moyen="test payment") + def tearDown(self): + self.user.delete() + self.paiement.delete() + def test_cotisations_prolongation(self): + """When user already have one valid cotisation, the new one should be + added at the end of the existing one.""" + date = timezone.now() + invoice1 = Facture.objects.create( + user=self.user, paiement=self.paiement, valid=True + ) + Vente.objects.create( + facture=invoice1, + number=1, + name="Test purchase", + duration=1, + duration_days=0, + type_cotisation="All", + prix=0, + ) + invoice1.reorder_purchases() + invoice2 = Facture.objects.create( + user=self.user, paiement=self.paiement, valid=True + ) + Vente.objects.create( + facture=invoice2, + number=1, + name="Test purchase", + duration=1, + duration_days=0, + type_cotisation="All", + prix=0, + ) + invoice1.reorder_purchases() + delta = relativedelta(self.user.end_connexion(), date) + delta.microseconds = 0 + try: + self.assertEqual(delta, relativedelta(months=2)) + except Exception as e: + invoice1.delete() + invoice2.delete() + raise e + invoice1.delete() + invoice2.delete() + diff --git a/cotisations/views.py b/cotisations/views.py index 562326e3..c39aa815 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -144,6 +144,8 @@ def new_facture(request, user, userid): price_ok = True if price_ok: new_invoice_instance.save() + # Saving purchases so the invoice can find them. Invoice + # will modify them after being validated to put the right dates. for p in purchases: p.facture = new_invoice_instance p.save()