models.py 29.5 KB
Newer Older
1
# -*- mode: python; coding: utf-8 -*-
2 3 4 5 6 7 8
# 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  Goulven Kermarec
# Copyright © 2017  Augustin Lemesle
9
# Copyright © 2018  Hugo Levy-Falk
10 11 12 13 14 15 16 17 18 19 20 21 22 23
#
# 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.
24
"""
25 26 27 28
The database models for the 'cotisation' app of re2o.
The goal is to keep the main actions here, i.e. the 'clean' and 'save'
function are higly reposnsible for the changes, checking the coherence of the
data and the good behaviour in general for not breaking the database.
29

30 31
For further details on each of those models, see the documentation details for
each.
32
"""
33

34
from __future__ import unicode_literals
35
from dateutil.relativedelta import relativedelta
36

37
from django.db import models
38
from django.db.models import Q, Max
39 40
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
chibrac's avatar
chibrac committed
41
from django.forms import ValidationError
42
from django.core.validators import MinValueValidator
43
from django.utils import timezone
44
from django.utils.translation import ugettext_lazy as _
45 46 47
from django.urls import reverse
from django.shortcuts import redirect
from django.contrib import messages
48

49
from machines.models import regen
50
from re2o.field_permissions import FieldPermissionModelMixin
51
from re2o.mixins import AclMixin, RevMixin
52

53
from cotisations.utils import find_payment_method
54
from cotisations.validators import check_no_balance
55

56

Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
57 58 59
class BaseInvoice(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
    date = models.DateTimeField(
        auto_now_add=True,
60
        verbose_name=_("Date")
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
    )

    # TODO : change prix to price
    def prix(self):
        """
        Returns: the raw price without the quantities.
        Deprecated, use :total_price instead.
        """
        price = Vente.objects.filter(
            facture=self
        ).aggregate(models.Sum('prix'))['prix__sum']
        return price

    # TODO : change prix to price
    def prix_total(self):
        """
        Returns: the total price for an invoice. Sum all the articles' prices
        and take the quantities into account.
        """
        # TODO : change Vente to somethingelse
        return Vente.objects.filter(
            facture=self
        ).aggregate(
            total=models.Sum(
                models.F('prix')*models.F('number'),
                output_field=models.FloatField()
            )
        )['total'] or 0

    def name(self):
        """
        Returns : a string with the name of all the articles in the invoice.
        Used for reprensenting the invoice with a string.
        """
        name = ' - '.join(Vente.objects.filter(
            facture=self
        ).values_list('name', flat=True))
        return name


101
# TODO : change facture to invoice
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
102
class Facture(BaseInvoice):
103 104 105
    """
    The model for an invoice. It reprensents the fact that a user paid for
    something (it can be multiple article paid at once).
106

107 108 109 110 111 112 113 114 115 116 117 118 119 120
    An invoice is linked to :
        * one or more purchases (one for each article sold that time)
        * a user (the one who bought those articles)
        * a payment method (the one used by the user)
        * (if applicable) a bank
        * (if applicable) a cheque number.
    Every invoice is dated throught the 'date' value.
    An invoice has a 'controlled' value (default : False) which means that
    someone with high enough rights has controlled that invoice and taken it
    into account. It also has a 'valid' value (default : True) which means
    that someone with high enough rights has decided that this invoice was not
    valid (thus it's like the user never paid for his articles). It may be
    necessary in case of non-payment.
    """
121

122
    user = models.ForeignKey('users.User', on_delete=models.PROTECT)
123
    # TODO : change paiement to payment
124
    paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT)
125
    # TODO : change banque to bank
chirac's avatar
chirac committed
126
    banque = models.ForeignKey(
127 128 129
        'Banque',
        on_delete=models.PROTECT,
        blank=True,
130 131 132 133 134 135
        null=True
    )
    # TODO : maybe change to cheque nummber because not evident
    cheque = models.CharField(
        max_length=255,
        blank=True,
136
        verbose_name=_("cheque number")
137 138 139 140
    )
    # TODO : change name to validity for clarity
    valid = models.BooleanField(
        default=True,
141
        verbose_name=_("validated")
142 143 144 145
    )
    # TODO : changed name to controlled for clarity
    control = models.BooleanField(
        default=False,
146
        verbose_name=_("controlled")
147
    )
148

149 150
    class Meta:
        abstract = False
151
        permissions = (
152
            # TODO : change facture to invoice
153
            ('change_facture_control',
154
             _("Can edit the \"controlled\" state")),
155
            ('view_facture',
156
             _("Can view an invoice object")),
157
            ('change_all_facture',
158
             _("Can edit all the previous invoices")),
159
        )
160 161
        verbose_name = _("invoice")
        verbose_name_plural = _("invoices")
162

163 164 165 166 167
    def linked_objects(self):
        """Return linked objects : machine and domain.
        Usefull in history display"""
        return self.vente_set.all()

168
    def can_edit(self, user_request, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
169
        if not user_request.has_perm('cotisations.change_facture'):
170
            return False, _("You don't have the right to edit an invoice.")
171 172 173 174 175 176 177 178
        elif not user_request.has_perm('cotisations.change_all_facture') and \
                not self.user.can_edit(user_request, *args, **kwargs)[0]:
            return False, _("You don't have the right to edit this user's "
                            "invoices.")
        elif not user_request.has_perm('cotisations.change_all_facture') and \
                (self.control or not self.valid):
            return False, _("You don't have the right to edit an invoice "
                            "already controlled or invalidated.")
179 180 181 182
        else:
            return True, None

    def can_delete(self, user_request, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
183
        if not user_request.has_perm('cotisations.delete_facture'):
184
            return False, _("You don't have the right to delete an invoice.")
185
        if not self.user.can_edit(user_request, *args, **kwargs)[0]:
186 187
            return False, _("You don't have the right to delete this user's "
                            "invoices.")
188
        if self.control or not self.valid:
189 190
            return False, _("You don't have the right to delete an invoice "
                            "already controlled or invalidated.")
191 192 193
        else:
            return True, None

194
    def can_view(self, user_request, *_args, **_kwargs):
195 196
        if not user_request.has_perm('cotisations.view_facture') and \
                self.user != user_request:
197
            return False, _("You don't have the right to view someone else's "
198
                            "invoices history.")
199
        elif not self.valid:
200
            return False, _("The invoice has been invalidated.")
201 202 203
        else:
            return True, None

204
    @staticmethod
205 206 207
    def can_change_control(user_request, *_args, **_kwargs):
        """ Returns True if the user can change the 'controlled' status of
        this invoice """
208 209 210 211
        return (
            user_request.has_perm('cotisations.change_facture_control'),
            _("You don't have the right to edit the \"controlled\" state.")
        )
212

213 214
    @staticmethod
    def can_create(user_request, *_args, **_kwargs):
215
        """Check if a user can create an invoice.
216 217 218 219 220

        :param user_request: The user who wants to create an invoice.
        :return: a message and a boolean which is True if the user can create
            an invoice or if the `options.allow_self_subscription` is set.
        """
221 222 223
        if user_request.has_perm('cotisations.add_facture'):
            return True, None
        if len(Paiement.find_allowed_payments(user_request)) <= 0:
224
            return False, _("There are no payment method which you can use.")
225
        if len(Article.find_allowed_articles(user_request, user_request)) <= 0:
226
            return False, _("There are no article that you can buy.")
227
        return True, None
228

229 230 231
    def __init__(self, *args, **kwargs):
        super(Facture, self).__init__(*args, **kwargs)
        self.field_permissions = {
232
            'control': self.can_change_control,
233
        }
234

235
    def __str__(self):
Lemesle Augustin's avatar
Lemesle Augustin committed
236
        return str(self.user) + ' ' + str(self.date)
237

chirac's avatar
chirac committed
238

239
@receiver(post_save, sender=Facture)
240
def facture_post_save(**kwargs):
241 242 243
    """
    Synchronise the LDAP user after an invoice has been saved.
    """
244 245
    facture = kwargs['instance']
    user = facture.user
246
    user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
247

chirac's avatar
chirac committed
248

249
@receiver(post_delete, sender=Facture)
250
def facture_post_delete(**kwargs):
251 252 253
    """
    Synchronise the LDAP user after an invoice has been deleted.
    """
254
    user = kwargs['instance'].user
255
    user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
256

chirac's avatar
chirac committed
257

Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
258 259 260
class CustomInvoice(BaseInvoice):
    class Meta:
        permissions = (
261
            ('view_custominvoice', _("Can view a custom invoice object")),
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
262 263 264
        )
    recipient = models.CharField(
        max_length=255,
265
        verbose_name=_("Recipient")
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
266 267 268
    )
    payment = models.CharField(
        max_length=255,
269
        verbose_name=_("Payment type")
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
270 271 272
    )
    address = models.CharField(
        max_length=255,
273
        verbose_name=_("Address")
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
274 275
    )
    paid = models.BooleanField(
276
        verbose_name=_("Paid")
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
277 278 279
    )


280
# TODO : change Vente to Purchase
281
class Vente(RevMixin, AclMixin, models.Model):
282 283 284
    """
    The model defining a purchase. It consist of one type of article being
    sold. In particular there may be multiple purchases in a single invoice.
285

286 287 288 289 290 291 292
    It's reprensentated by:
        * an amount (the number of items sold)
        * an invoice (whose the purchase is part of)
        * an article
        * (if applicable) a cotisation (which holds some informations about
            the effect of the purchase on the time agreed for this user)
    """
293

294
    # TODO : change this to English
295
    COTISATION_TYPE = (
296 297 298
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
299 300
    )

301 302
    # TODO : change facture to invoice
    facture = models.ForeignKey(
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
303
        'BaseInvoice',
304
        on_delete=models.CASCADE,
305
        verbose_name=_("invoice")
306 307 308 309
    )
    # TODO : change number to amount for clarity
    number = models.IntegerField(
        validators=[MinValueValidator(1)],
310
        verbose_name=_("amount")
311 312 313 314
    )
    # TODO : change this field for a ForeinKey to Article
    name = models.CharField(
        max_length=255,
315
        verbose_name=_("article")
316 317 318 319 320 321
    )
    # TODO : change prix to price
    # TODO : this field is not needed if you use Article ForeignKey
    prix = models.DecimalField(
        max_digits=5,
        decimal_places=2,
322
        verbose_name=_("price"))
323
    # TODO : this field is not needed if you use Article ForeignKey
324
    duration = models.PositiveIntegerField(
325
        blank=True,
326
        null=True,
327
        verbose_name=_("duration (in months)")
328 329
    )
    # TODO : this field is not needed if you use Article ForeignKey
330 331 332 333
    type_cotisation = models.CharField(
        choices=COTISATION_TYPE,
        blank=True,
        null=True,
334
        max_length=255,
335
        verbose_name=_("subscription type")
336
    )
337

338 339
    class Meta:
        permissions = (
340 341
            ('view_vente', _("Can view a purchase object")),
            ('change_all_vente', _("Can edit all the previous purchases")),
342
        )
343 344
        verbose_name = _("purchase")
        verbose_name_plural = _("purchases")
345

346
    # TODO : change prix_total to total_price
347
    def prix_total(self):
348 349 350
        """
        Returns: the total of price for this amount of items.
        """
351 352
        return self.prix*self.number

353
    def update_cotisation(self):
354 355 356 357
        """
        Update the related object 'cotisation' if there is one. Based on the
        duration of the purchase.
        """
358 359
        if hasattr(self, 'cotisation'):
            cotisation = self.cotisation
chirac's avatar
chirac committed
360
            cotisation.date_end = cotisation.date_start + relativedelta(
361
                months=self.duration*self.number)
362 363 364
        return

    def create_cotis(self, date_start=False):
365 366 367 368 369
        """
        Update and create a 'cotisation' related object if there is a
        cotisation_type defined (which means the article sold represents
        a cotisation)
        """
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
370 371 372 373
        try:
            invoice = self.facture.facture
        except Facture.DoesNotExist:
            return
374
        if not hasattr(self, 'cotisation') and self.type_cotisation:
chirac's avatar
chirac committed
375
            cotisation = Cotisation(vente=self)
376
            cotisation.type_cotisation = self.type_cotisation
377
            if date_start:
Gabriel Detraz's avatar
Gabriel Detraz committed
378
                end_cotisation = Cotisation.objects.filter(
379 380
                    vente__in=Vente.objects.filter(
                        facture__in=Facture.objects.filter(
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
381
                            user=invoice.user
382
                        ).exclude(valid=False))
383 384 385 386 387 388
                ).filter(
                    Q(type_cotisation='All') |
                    Q(type_cotisation=self.type_cotisation)
                ).filter(
                    date_start__lt=date_start
                ).aggregate(Max('date_end'))['date_end__max']
389
            elif self.type_cotisation == "Adhesion":
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
390
                end_cotisation = invoice.user.end_adhesion()
391
            else:
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
392
                end_cotisation = invoice.user.end_connexion()
393
            date_start = date_start or timezone.now()
394 395
            end_cotisation = end_cotisation or date_start
            date_max = max(end_cotisation, date_start)
396
            cotisation.date_start = date_max
chirac's avatar
chirac committed
397
            cotisation.date_end = cotisation.date_start + relativedelta(
398
                months=self.duration*self.number
399
            )
400 401 402
        return

    def save(self, *args, **kwargs):
403 404 405 406 407 408
        """
        Save a purchase object and check if all the fields are coherents
        It also update the associated cotisation in the changes have some
        effect on the user's cotisation
        """
        # Checking that if a cotisation is specified, there is also a duration
409
        if self.type_cotisation and not self.duration:
410
            raise ValidationError(
411
                _("Duration must be specified for a subscription.")
412
            )
413 414
        self.update_cotisation()
        super(Vente, self).save(*args, **kwargs)
415

416
    def can_edit(self, user_request, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
417
        if not user_request.has_perm('cotisations.change_vente'):
418
            return False, _("You don't have the right to edit the purchases.")
419 420 421
        elif (not user_request.has_perm('cotisations.change_all_facture') and
              not self.facture.user.can_edit(
                  user_request, *args, **kwargs
422
        )[0]):
423 424
            return False, _("You don't have the right to edit this user's "
                            "purchases.")
425 426
        elif (not user_request.has_perm('cotisations.change_all_vente') and
              (self.facture.control or not self.facture.valid)):
427 428
            return False, _("You don't have the right to edit a purchase "
                            "already controlled or invalidated.")
429 430
        else:
            return True, None
431

432
    def can_delete(self, user_request, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
433
        if not user_request.has_perm('cotisations.delete_vente'):
434
            return False, _("You don't have the right to delete a purchase.")
435
        if not self.facture.user.can_edit(user_request, *args, **kwargs)[0]:
436 437
            return False, _("You don't have the right to delete this user's "
                            "purchases.")
438
        if self.facture.control or not self.facture.valid:
439 440
            return False, _("You don't have the right to delete a purchase "
                            "already controlled or invalidated.")
441 442
        else:
            return True, None
443

444 445 446
    def can_view(self, user_request, *_args, **_kwargs):
        if (not user_request.has_perm('cotisations.view_vente') and
                self.facture.user != user_request):
447
            return False, _("You don't have the right to view someone "
448
                            "else's purchase history.")
449 450
        else:
            return True, None
451

452
    def __str__(self):
453
        return str(self.name) + ' ' + str(self.facture)
454

chirac's avatar
chirac committed
455

456
# TODO : change vente to purchase
457
@receiver(post_save, sender=Vente)
458
def vente_post_save(**kwargs):
459 460 461 462 463
    """
    Creates a 'cotisation' related object if needed and synchronise the
    LDAP user when a purchase has been saved.
    """
    purchase = kwargs['instance']
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
464 465 466 467
    try:
        purchase.facture.facture
    except Facture.DoesNotExist:
        return
Gabriel Detraz's avatar
Gabriel Detraz committed
468
    if hasattr(purchase, 'cotisation'):
469 470 471 472 473 474
        purchase.cotisation.vente = purchase
        purchase.cotisation.save()
    if purchase.type_cotisation:
        purchase.create_cotis()
        purchase.cotisation.save()
        user = purchase.facture.user
475
        user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
476

chirac's avatar
chirac committed
477

478
# TODO : change vente to purchase
479
@receiver(post_delete, sender=Vente)
480
def vente_post_delete(**kwargs):
481 482 483 484
    """
    Synchronise the LDAP user after a purchase has been deleted.
    """
    purchase = kwargs['instance']
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
485 486 487 488
    try:
        invoice = purchase.facture.facture
    except Facture.DoesNotExist:
        return
489
    if purchase.type_cotisation:
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
490
        user = invoice.user
491
        user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
492

chirac's avatar
chirac committed
493

494
class Article(RevMixin, AclMixin, models.Model):
495
    """
496 497 498
    The definition of an article model. It represents a type of object
    that can be sold to the user.

499 500 501
    It's represented by:
        * a name
        * a price
502 503
        * a cotisation type (indicating if this article reprensents a
            cotisation or not)
504 505 506
        * a duration (if it is a cotisation)
        * a type of user (indicating what kind of user can buy this article)
    """
507

508
    # TODO : Either use TYPE or TYPES in both choices but not both
509
    USER_TYPES = (
510 511 512
        ('Adherent', _("Member")),
        ('Club', _("Club")),
        ('All', _("Both of them")),
513 514 515
    )

    COTISATION_TYPE = (
516 517 518
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
519 520
    )

521 522
    name = models.CharField(
        max_length=255,
523
        verbose_name=_("designation")
524 525 526 527 528
    )
    # TODO : change prix to price
    prix = models.DecimalField(
        max_digits=5,
        decimal_places=2,
529
        verbose_name=_("unit price")
530
    )
531
    duration = models.PositiveIntegerField(
David Sinquin's avatar
David Sinquin committed
532 533
        blank=True,
        null=True,
534
        validators=[MinValueValidator(0)],
535
        verbose_name=_("duration (in months)")
536
    )
537 538 539
    type_user = models.CharField(
        choices=USER_TYPES,
        default='All',
540
        max_length=255,
541
        verbose_name=_("type of users concerned")
542 543 544 545 546 547
    )
    type_cotisation = models.CharField(
        choices=COTISATION_TYPE,
        default=None,
        blank=True,
        null=True,
548
        max_length=255,
549
        verbose_name=_("subscription type")
550
    )
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
551
    available_for_everyone = models.BooleanField(
552
        default=False,
553
        verbose_name=_("is available for every user")
554
    )
chibrac's avatar
chibrac committed
555

556 557
    unique_together = ('name', 'type_user')

558 559
    class Meta:
        permissions = (
560 561
            ('view_article', _("Can view an article object")),
            ('buy_every_article', _("Can buy every article"))
562
        )
563 564
        verbose_name = "article"
        verbose_name_plural = "articles"
565

chibrac's avatar
chibrac committed
566
    def clean(self):
567 568
        if self.name.lower() == 'solde':
            raise ValidationError(
569
                _("Balance is a reserved article name.")
570
            )
571 572
        if self.type_cotisation and not self.duration:
            raise ValidationError(
573
                _("Duration must be specified for a subscription.")
574
            )
chibrac's avatar
chibrac committed
575

576 577 578
    def __str__(self):
        return self.name

Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
579 580 581
    def can_buy_article(self, user, *_args, **_kwargs):
        """Check if a user can buy this article.

582 583 584 585 586 587
        Args:
            self: The article
            user: The user requesting buying

        Returns:
            A boolean stating if usage is granted and an explanation
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
588 589 590 591
            message if the boolean is `False`.
        """
        return (
            self.available_for_everyone
592 593
            or user.has_perm('cotisations.buy_every_article')
            or user.has_perm('cotisations.add_facture'),
594
            _("You can't buy this article.")
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
595 596 597
        )

    @classmethod
598 599
    def find_allowed_articles(cls, user, target_user):
        """Finds every allowed articles for an user, on a target user.
600

601 602
        Args:
            user: The user requesting articles.
603
            target_user: The user to sell articles
604
        """
605 606 607 608 609 610 611 612
        if target_user.is_class_club:
            objects_pool = cls.objects.filter(
                Q(type_user='All') | Q(type_user='Club')
            )
        else:
            objects_pool = cls.objects.filter(
                Q(type_user='All') | Q(type_user='Adherent')
            )
613
        if user.has_perm('cotisations.buy_every_article'):
614 615
            return objects_pool
        return objects_pool.filter(available_for_everyone=True)
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
616

chirac's avatar
chirac committed
617

618
class Banque(RevMixin, AclMixin, models.Model):
619 620 621 622 623 624 625
    """
    The model defining a bank. It represents a user's bank. It's mainly used
    for statistics by regrouping the user under their bank's name and avoid
    the use of a simple name which leads (by experience) to duplicates that
    only differs by a capital letter, a space, a misspelling, ... That's why
    it's easier to use simple object for the banks.
    """
626

627 628 629
    name = models.CharField(
        max_length=255,
    )
630

631 632
    class Meta:
        permissions = (
633
            ('view_banque', _("Can view a bank object")),
634
        )
635 636
        verbose_name = _("bank")
        verbose_name_plural = _("banks")
637

638 639 640
    def __str__(self):
        return self.name

chirac's avatar
chirac committed
641

642
# TODO : change Paiement to Payment
643
class Paiement(RevMixin, AclMixin, models.Model):
644 645 646 647 648 649
    """
    The model defining a payment method. It is how the user is paying for the
    invoice. It's easier to know this information when doing the accouts.
    It is represented by:
        * a name
    """
650

651 652 653
    # TODO : change moyen to method
    moyen = models.CharField(
        max_length=255,
654
        verbose_name=_("method")
655
    )
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
656
    available_for_everyone = models.BooleanField(
657
        default=False,
658
        verbose_name=_("is available for every user")
659
    )
660 661 662
    is_balance = models.BooleanField(
        default=False,
        editable=False,
663 664
        verbose_name=_("is user balance"),
        help_text=_("There should be only one balance payment method."),
665 666
        validators=[check_no_balance]
    )
667

668 669
    class Meta:
        permissions = (
670 671
            ('view_paiement', _("Can view a payment method object")),
            ('use_every_payment', _("Can use every payment method")),
672
        )
673 674
        verbose_name = _("payment method")
        verbose_name_plural = _("payment methods")
675

676 677 678
    def __str__(self):
        return self.moyen

chibrac's avatar
chibrac committed
679
    def clean(self):
680
        """l
681 682
        Override of the herited clean function to get a correct name
        """
chibrac's avatar
chibrac committed
683 684
        self.moyen = self.moyen.title()

685
    def end_payment(self, invoice, request, use_payment_method=True):
686
        """
687 688
        The general way of ending a payment.

689 690 691 692 693 694
        Args:
            invoice: The invoice being created.
            request: Request sent by the user.
            use_payment_method: If this flag is set to True and`self` has
                an attribute `payment_method`, returns the result of
                `self.payment_method.end_payment(invoice, request)`
695

696 697
        Returns:
            An `HttpResponse`-like object.
698
        """
699 700 701
        payment_method = find_payment_method(self)
        if payment_method is not None and use_payment_method:
            return payment_method.end_payment(invoice, request)
702 703 704 705 706 707

        # In case a cotisation was bought, inform the user, the
        # cotisation time has been extended too
        if any(sell.type_cotisation for sell in invoice.vente_set.all()):
            messages.success(
                request,
708 709
                _("The subscription of %(member_name)s was extended to"
                  " %(end_date)s.") % {
710 711
                    'member_name': invoice.user.pseudo,
                    'end_date': invoice.user.end_adhesion()
712 713 714 715 716 717
                }
            )
        # Else, only tell the invoice was created
        else:
            messages.success(
                request,
718
                _("The invoice was created.")
719 720 721
            )
        return redirect(reverse(
            'users:profil',
722
            kwargs={'userid': invoice.user.pk}
723 724
        ))

Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
725 726 727
    def can_use_payment(self, user, *_args, **_kwargs):
        """Check if a user can use this payment.

728 729 730 731 732
        Args:
            self: The payment
            user: The user requesting usage
        Returns:
            A boolean stating if usage is granted and an explanation
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
733 734 735 736
            message if the boolean is `False`.
        """
        return (
            self.available_for_everyone
737 738
            or user.has_perm('cotisations.use_every_payment')
            or user.has_perm('cotisations.add_facture'),
739
            _("You can't use this payment method.")
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
740 741 742 743
        )

    @classmethod
    def find_allowed_payments(cls, user):
744 745
        """Finds every allowed payments for an user.

746 747
        Args:
            user: The user requesting payment methods.
748 749 750 751
        """
        if user.has_perm('cotisations.use_every_payment'):
            return cls.objects.all()
        return cls.objects.filter(available_for_everyone=True)
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
752

753 754 755 756
    def get_payment_method_name(self):
        p = find_payment_method(self)
        if p is not None:
            return p._meta.verbose_name
757
        return _("No custom payment method.")
758

chirac's avatar
chirac committed
759

760
class Cotisation(RevMixin, AclMixin, models.Model):
761 762 763 764 765 766 767 768 769 770
    """
    The model defining a cotisation. It holds information about the time a user
    is allowed when he has paid something.
    It characterised by :
        * a date_start (the date when the cotisaiton begins/began
        * a date_end (the date when the cotisation ends/ended
        * a type of cotisation (which indicates the implication of such
            cotisation)
        * a purchase (the related objects this cotisation is linked to)
    """
771

772
    COTISATION_TYPE = (
773 774 775
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
776 777
    )

778 779 780 781 782
    # TODO : change vente to purchase
    vente = models.OneToOneField(
        'Vente',
        on_delete=models.CASCADE,
        null=True,
783
        verbose_name=_("purchase")
784
    )
785 786 787
    type_cotisation = models.CharField(
        choices=COTISATION_TYPE,
        max_length=255,
788
        default='All',
789
        verbose_name=_("subscription type")
790 791
    )
    date_start = models.DateTimeField(
792
        verbose_name=_("start date")
793 794
    )
    date_end = models.DateTimeField(
795
        verbose_name=_("end date")
796
    )
797

798 799
    class Meta:
        permissions = (
800 801
            ('view_cotisation', _("Can view a subscription object")),
            ('change_all_cotisation', _("Can edit the previous subscriptions")),
802
        )
803 804
        verbose_name = _("subscription")
        verbose_name_plural = _("subscriptions")
805

806
    def can_edit(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
807
        if not user_request.has_perm('cotisations.change_cotisation'):
808
            return False, _("You don't have the right to edit a subscription.")
809 810 811
        elif not user_request.has_perm('cotisations.change_all_cotisation') \
                and (self.vente.facture.control or
                     not self.vente.facture.valid):
812
            return False, _("You don't have the right to edit a subscription "
813
                            "already controlled or invalidated.")
814 815
        else:
            return True, None
816

817
    def can_delete(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
818
        if not user_request.has_perm('cotisations.delete_cotisation'):
819
            return False, _("You don't have the right to delete a "
820
                            "subscription.")
821
        if self.vente.facture.control or not self.vente.facture.valid:
822
            return False, _("You don't have the right to delete a subscription "
823
                            "already controlled or invalidated.")
824 825
        else:
            return True, None
826

827
    def can_view(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
828
        if not user_request.has_perm('cotisations.view_cotisation') and\
829
                self.vente.facture.user != user_request:
830 831
            return False, _("You don't have the right to view someone else's "
                            "subscription history.")
832 833
        else:
            return True, None
834

835
    def __str__(self):
836
        return str(self.vente)
837

chirac's avatar
chirac committed
838

839
@receiver(post_save, sender=Cotisation)
840
def cotisation_post_save(**_kwargs):
841 842 843 844
    """
    Mark some services as needing a regeneration after the edition of a
    cotisation. Indeed the membership status may have changed.
    """
845 846 847
    regen('dns')
    regen('dhcp')
    regen('mac_ip_list')
848
    regen('mailing')
849

chirac's avatar
chirac committed
850

851
@receiver(post_delete, sender=Cotisation)
852
def cotisation_post_delete(**_kwargs):
853 854 855 856
    """
    Mark some services as needing a regeneration after the deletion of a
    cotisation. Indeed the membership status may have changed.
    """
root's avatar
root committed
857
    regen('mac_ip_list')
858
    regen('mailing')
859