models.py 29 KB
Newer Older
1
# -*- mode: python; coding: utf-8 -*-
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# 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
#
# 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.
23
"""
24 25 26 27
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.
28

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

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

36
from django.db import models
37
from django.db.models import Q, Max
38 39
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
chibrac's avatar
chibrac committed
40
from django.forms import ValidationError
41
from django.core.validators import MinValueValidator
42
from django.utils import timezone
43
from django.utils.translation import ugettext as _
44
from django.utils.translation import ugettext_lazy as _l
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 54
from cotisations.utils import find_payment_method

55

56
# TODO : change facture to invoice
57
class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
58 59 60
    """
    The model for an invoice. It reprensents the fact that a user paid for
    something (it can be multiple article paid at once).
61

62 63 64 65 66 67 68 69 70 71 72 73 74 75
    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.
    """
76

77
    user = models.ForeignKey('users.User', on_delete=models.PROTECT)
78
    # TODO : change paiement to payment
79
    paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT)
80
    # TODO : change banque to bank
chirac's avatar
chirac committed
81
    banque = models.ForeignKey(
82 83 84
        'Banque',
        on_delete=models.PROTECT,
        blank=True,
85 86 87 88 89 90
        null=True
    )
    # TODO : maybe change to cheque nummber because not evident
    cheque = models.CharField(
        max_length=255,
        blank=True,
91
        verbose_name=_l("Cheque number")
92 93 94
    )
    date = models.DateTimeField(
        auto_now_add=True,
95
        verbose_name=_l("Date")
96 97 98 99
    )
    # TODO : change name to validity for clarity
    valid = models.BooleanField(
        default=True,
100
        verbose_name=_l("Validated")
101 102 103 104
    )
    # TODO : changed name to controlled for clarity
    control = models.BooleanField(
        default=False,
105
        verbose_name=_l("Controlled")
106
    )
107

108 109
    class Meta:
        abstract = False
110
        permissions = (
111
            # TODO : change facture to invoice
112
            ('change_facture_control',
113
             _l("Can change the \"controlled\" state")),
114 115 116
            # TODO : seems more likely to be call create_facture_pdf
            # or create_invoice_pdf
            ('change_facture_pdf',
117
             _l("Can create a custom PDF invoice")),
118
            ('view_facture',
119
             _l("Can see an invoice's details")),
120
            ('change_all_facture',
121
             _l("Can edit all the previous invoices")),
122
        )
123 124
        verbose_name = _l("Invoice")
        verbose_name_plural = _l("Invoices")
125

126 127 128 129 130
    def linked_objects(self):
        """Return linked objects : machine and domain.
        Usefull in history display"""
        return self.vente_set.all()

131
    # TODO : change prix to price
132
    def prix(self):
133 134 135 136 137
        """
        Returns: the raw price without the quantities.
        Deprecated, use :total_price instead.
        """
        price = Vente.objects.filter(
138
            facture=self
139
        ).aggregate(models.Sum('prix'))['prix__sum']
140
        return price
141

142
    # TODO : change prix to price
Dalahro's avatar
Dalahro committed
143
    def prix_total(self):
144 145 146 147
        """
        Returns: the total price for an invoice. Sum all the articles' prices
        and take the quantities into account.
        """
148
        # TODO : change Vente to somethingelse
chirac's avatar
chirac committed
149
        return Vente.objects.filter(
150
            facture=self
151 152 153 154 155 156
        ).aggregate(
            total=models.Sum(
                models.F('prix')*models.F('number'),
                output_field=models.FloatField()
            )
        )['total']
Dalahro's avatar
Dalahro committed
157

158
    def name(self):
159 160 161 162
        """
        Returns : a string with the name of all the articles in the invoice.
        Used for reprensenting the invoice with a string.
        """
chirac's avatar
chirac committed
163 164
        name = ' - '.join(Vente.objects.filter(
            facture=self
165
        ).values_list('name', flat=True))
166 167
        return name

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 197 198
        if not user_request.has_perm('cotisations.view_facture') and \
                self.user != user_request:
            return False, _("You don't have the right to see someone else's "
                            "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
    @staticmethod
214 215
    def can_change_pdf(user_request, *_args, **_kwargs):
        """ Returns True if the user can change this invoice """
216 217 218 219
        return (
            user_request.has_perm('cotisations.change_facture_pdf'),
            _("You don't have the right to edit an invoice.")
        )
220

221 222
    @staticmethod
    def can_create(user_request, *_args, **_kwargs):
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
223
        """Check if a user can create an invoice.
224 225 226 227 228

        :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.
        """
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
229 230
        nb_payments = len(Paiement.find_allowed_payments(user_request))
        nb_articles = len(Article.find_allowed_articles(user_request))
231
        return (
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
232 233
            user_request.has_perm('cotisations.add_facture')
            or (nb_payments*nb_articles),
234 235 236
            _("You don't have the right to create an invoice.")
        )

237 238 239
    def __init__(self, *args, **kwargs):
        super(Facture, self).__init__(*args, **kwargs)
        self.field_permissions = {
240
            'control': self.can_change_control,
241
        }
242

243
    def __str__(self):
Dalahro's avatar
Dalahro committed
244
        return str(self.user) + ' ' + str(self.date)
245

chirac's avatar
chirac committed
246

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

chirac's avatar
chirac committed
256

257
@receiver(post_delete, sender=Facture)
258
def facture_post_delete(**kwargs):
259 260 261
    """
    Synchronise the LDAP user after an invoice has been deleted.
    """
262
    user = kwargs['instance'].user
263
    user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
264

chirac's avatar
chirac committed
265

266
# TODO : change Vente to Purchase
267
class Vente(RevMixin, AclMixin, models.Model):
268 269 270
    """
    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.
271

272 273 274 275 276 277 278
    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)
    """
279

280
    # TODO : change this to English
281
    COTISATION_TYPE = (
282 283 284
        ('Connexion', _l("Connexion")),
        ('Adhesion', _l("Membership")),
        ('All', _l("Both of them")),
285 286
    )

287 288 289 290
    # TODO : change facture to invoice
    facture = models.ForeignKey(
        'Facture',
        on_delete=models.CASCADE,
291
        verbose_name=_l("Invoice")
292 293 294 295
    )
    # TODO : change number to amount for clarity
    number = models.IntegerField(
        validators=[MinValueValidator(1)],
296
        verbose_name=_l("Amount")
297 298 299 300
    )
    # TODO : change this field for a ForeinKey to Article
    name = models.CharField(
        max_length=255,
301
        verbose_name=_l("Article")
302 303 304 305 306 307
    )
    # 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,
308
        verbose_name=_l("Price"))
309
    # TODO : this field is not needed if you use Article ForeignKey
310
    duration = models.PositiveIntegerField(
311
        blank=True,
312
        null=True,
313
        verbose_name=_l("Duration (in whole month)")
314 315
    )
    # TODO : this field is not needed if you use Article ForeignKey
316 317 318 319
    type_cotisation = models.CharField(
        choices=COTISATION_TYPE,
        blank=True,
        null=True,
320
        max_length=255,
321
        verbose_name=_l("Type of cotisation")
322
    )
323

324 325
    class Meta:
        permissions = (
326 327
            ('view_vente', _l("Can see a purchase's details")),
            ('change_all_vente', _l("Can edit all the previous purchases")),
328
        )
329 330
        verbose_name = _l("Purchase")
        verbose_name_plural = _l("Purchases")
331

332
    # TODO : change prix_total to total_price
333
    def prix_total(self):
334 335 336
        """
        Returns: the total of price for this amount of items.
        """
337 338
        return self.prix*self.number

339
    def update_cotisation(self):
340 341 342 343
        """
        Update the related object 'cotisation' if there is one. Based on the
        duration of the purchase.
        """
344 345
        if hasattr(self, 'cotisation'):
            cotisation = self.cotisation
chirac's avatar
chirac committed
346
            cotisation.date_end = cotisation.date_start + relativedelta(
347
                months=self.duration*self.number)
348 349 350
        return

    def create_cotis(self, date_start=False):
351 352 353 354 355
        """
        Update and create a 'cotisation' related object if there is a
        cotisation_type defined (which means the article sold represents
        a cotisation)
        """
356
        if not hasattr(self, 'cotisation') and self.type_cotisation:
chirac's avatar
chirac committed
357
            cotisation = Cotisation(vente=self)
358
            cotisation.type_cotisation = self.type_cotisation
359
            if date_start:
Gabriel Detraz's avatar
Gabriel Detraz committed
360
                end_cotisation = Cotisation.objects.filter(
361 362 363 364
                    vente__in=Vente.objects.filter(
                        facture__in=Facture.objects.filter(
                            user=self.facture.user
                        ).exclude(valid=False))
365 366 367 368 369 370
                ).filter(
                    Q(type_cotisation='All') |
                    Q(type_cotisation=self.type_cotisation)
                ).filter(
                    date_start__lt=date_start
                ).aggregate(Max('date_end'))['date_end__max']
371
            elif self.type_cotisation == "Adhesion":
372
                end_cotisation = self.facture.user.end_adhesion()
373
            else:
374
                end_cotisation = self.facture.user.end_connexion()
375
            date_start = date_start or timezone.now()
376 377
            end_cotisation = end_cotisation or date_start
            date_max = max(end_cotisation, date_start)
378
            cotisation.date_start = date_max
chirac's avatar
chirac committed
379
            cotisation.date_end = cotisation.date_start + relativedelta(
380
                months=self.duration*self.number
381
            )
382 383 384
        return

    def save(self, *args, **kwargs):
385 386 387 388 389 390
        """
        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
391
        if self.type_cotisation and not self.duration:
392 393 394
            raise ValidationError(
                _("A cotisation should always have a duration.")
            )
395 396
        self.update_cotisation()
        super(Vente, self).save(*args, **kwargs)
397

398
    def can_edit(self, user_request, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
399
        if not user_request.has_perm('cotisations.change_vente'):
400
            return False, _("You don't have the right to edit the purchases.")
401 402 403
        elif (not user_request.has_perm('cotisations.change_all_facture') and
              not self.facture.user.can_edit(
                  user_request, *args, **kwargs
404
        )[0]):
405 406
            return False, _("You don't have the right to edit this user's "
                            "purchases.")
407 408
        elif (not user_request.has_perm('cotisations.change_all_vente') and
              (self.facture.control or not self.facture.valid)):
409 410
            return False, _("You don't have the right to edit a purchase "
                            "already controlled or invalidated.")
411 412
        else:
            return True, None
413

414
    def can_delete(self, user_request, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
415
        if not user_request.has_perm('cotisations.delete_vente'):
416
            return False, _("You don't have the right to delete a purchase.")
417
        if not self.facture.user.can_edit(user_request, *args, **kwargs)[0]:
418 419
            return False, _("You don't have the right to delete this user's "
                            "purchases.")
420
        if self.facture.control or not self.facture.valid:
421 422
            return False, _("You don't have the right to delete a purchase "
                            "already controlled or invalidated.")
423 424
        else:
            return True, None
425

426 427 428
    def can_view(self, user_request, *_args, **_kwargs):
        if (not user_request.has_perm('cotisations.view_vente') and
                self.facture.user != user_request):
429 430
            return False, _("You don't have the right to see someone "
                            "else's purchase history.")
431 432
        else:
            return True, None
433

434
    def __str__(self):
435
        return str(self.name) + ' ' + str(self.facture)
436

chirac's avatar
chirac committed
437

438
# TODO : change vente to purchase
439
@receiver(post_save, sender=Vente)
440
def vente_post_save(**kwargs):
441 442 443 444 445
    """
    Creates a 'cotisation' related object if needed and synchronise the
    LDAP user when a purchase has been saved.
    """
    purchase = kwargs['instance']
Gabriel Detraz's avatar
Gabriel Detraz committed
446
    if hasattr(purchase, 'cotisation'):
447 448 449 450 451 452
        purchase.cotisation.vente = purchase
        purchase.cotisation.save()
    if purchase.type_cotisation:
        purchase.create_cotis()
        purchase.cotisation.save()
        user = purchase.facture.user
453
        user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
454

chirac's avatar
chirac committed
455

456
# TODO : change vente to purchase
457
@receiver(post_delete, sender=Vente)
458
def vente_post_delete(**kwargs):
459 460 461 462 463 464
    """
    Synchronise the LDAP user after a purchase has been deleted.
    """
    purchase = kwargs['instance']
    if purchase.type_cotisation:
        user = purchase.facture.user
465
        user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
466

chirac's avatar
chirac committed
467

468
class Article(RevMixin, AclMixin, models.Model):
469
    """
470 471 472
    The definition of an article model. It represents a type of object
    that can be sold to the user.

473 474 475
    It's represented by:
        * a name
        * a price
476 477
        * a cotisation type (indicating if this article reprensents a
            cotisation or not)
478 479 480
        * a duration (if it is a cotisation)
        * a type of user (indicating what kind of user can buy this article)
    """
481

482
    # TODO : Either use TYPE or TYPES in both choices but not both
483
    USER_TYPES = (
484 485 486
        ('Adherent', _l("Member")),
        ('Club', _l("Club")),
        ('All', _l("Both of them")),
487 488 489
    )

    COTISATION_TYPE = (
490 491 492
        ('Connexion', _l("Connexion")),
        ('Adhesion', _l("Membership")),
        ('All', _l("Both of them")),
493 494
    )

495 496
    name = models.CharField(
        max_length=255,
497
        verbose_name=_l("Designation")
498 499 500 501 502
    )
    # TODO : change prix to price
    prix = models.DecimalField(
        max_digits=5,
        decimal_places=2,
503
        verbose_name=_l("Unitary price")
504
    )
505
    duration = models.PositiveIntegerField(
David Sinquin's avatar
David Sinquin committed
506 507
        blank=True,
        null=True,
508
        validators=[MinValueValidator(0)],
509
        verbose_name=_l("Duration (in whole month)")
510
    )
511 512 513
    type_user = models.CharField(
        choices=USER_TYPES,
        default='All',
514
        max_length=255,
515
        verbose_name=_l("Type of users concerned")
516 517 518 519 520 521
    )
    type_cotisation = models.CharField(
        choices=COTISATION_TYPE,
        default=None,
        blank=True,
        null=True,
522
        max_length=255,
523
        verbose_name=_l("Type of cotisation")
524
    )
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
525
    available_for_everyone = models.BooleanField(
526
        default=False,
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
527
        verbose_name=_l("Is available for every user")
528
    )
chibrac's avatar
chibrac committed
529

530 531
    unique_together = ('name', 'type_user')

532 533
    class Meta:
        permissions = (
534
            ('view_article', _l("Can see an article's details")),
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
535
            ('buy_every_article', _l("Can buy every_article"))
536
        )
537 538
        verbose_name = "Article"
        verbose_name_plural = "Articles"
539

chibrac's avatar
chibrac committed
540
    def clean(self):
541 542 543 544
        if self.name.lower() == 'solde':
            raise ValidationError(
                _("Solde is a reserved article name")
            )
545 546
        if self.type_cotisation and not self.duration:
            raise ValidationError(
547
                _("Duration must be specified for a cotisation")
548
            )
chibrac's avatar
chibrac committed
549

550 551 552
    def __str__(self):
        return self.name

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570
    def can_buy_article(self, user, *_args, **_kwargs):
        """Check if a user can buy this article.

        :param self: The article
        :param user: The user requesting buying
        :returns: A boolean stating if usage is granted and an explanation
            message if the boolean is `False`.
        """
        return (
            self.available_for_everyone
            or user.has_perm('cotisations.buy_every_article'),
            _("You cannot use this Payment.")
        )

    @classmethod
    def find_allowed_articles(cls, user):
        return [p for p in cls.objects.all() if p.can_buy_article(user)[0]]

chirac's avatar
chirac committed
571

572
class Banque(RevMixin, AclMixin, models.Model):
573 574 575 576 577 578 579
    """
    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.
    """
580

581 582
    name = models.CharField(
        max_length=255,
583
        verbose_name=_l("Name")
584
    )
585

586 587
    class Meta:
        permissions = (
588
            ('view_banque', _l("Can see a bank's details")),
589
        )
590 591
        verbose_name = _l("Bank")
        verbose_name_plural = _l("Banks")
592

593 594 595
    def __str__(self):
        return self.name

chirac's avatar
chirac committed
596

597 598 599 600 601 602 603 604 605 606 607
def check_no_balance():
    """This functions checks that no Paiement with is_balance=True exists

    :raises ValidationError: if such a Paiement exists.
    """
    p = Paiement.objects.filter(is_balance=True)
    if len(p)>0:
        raise ValidationError(
            _("There are already payment method(s) for user balance")
        )

608
# TODO : change Paiement to Payment
609
class Paiement(RevMixin, AclMixin, models.Model):
610 611 612 613 614 615 616 617
    """
    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
        * a type (used for the type 'cheque' which implies the use of a bank
            and an account number in related models)
    """
618

619
    PAYMENT_TYPES = (
620 621
        (0, _l("Standard")),
        (1, _l("Cheque")),
622
    )
623

624 625 626
    # TODO : change moyen to method
    moyen = models.CharField(
        max_length=255,
627
        verbose_name=_l("Method")
628 629 630 631
    )
    type_paiement = models.IntegerField(
        choices=PAYMENT_TYPES,
        default=0,
632
        verbose_name=_l("Payment type")
633
    )
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
634
    available_for_everyone = models.BooleanField(
635
        default=False,
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
636
        verbose_name=_l("Is available for every user")
637
    )
638 639 640 641 642 643 644
    is_balance = models.BooleanField(
        default=False,
        editable=False,
        verbose_name=_l("Is user balance"),
        help_text=_l("There should be only one balance payment method."),
        validators=[check_no_balance]
    )
645

646 647
    class Meta:
        permissions = (
648
            ('view_paiement', _l("Can see a payement's details")),
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
649
            ('use_every_payment', _l("Can use every payement")),
650
        )
651 652
        verbose_name = _l("Payment method")
        verbose_name_plural = _l("Payment methods")
653

654 655 656
    def __str__(self):
        return self.moyen

chibrac's avatar
chibrac committed
657
    def clean(self):
658 659 660
        """
        Override of the herited clean function to get a correct name
        """
chibrac's avatar
chibrac committed
661 662
        self.moyen = self.moyen.title()

Gabriel Detraz's avatar
Gabriel Detraz committed
663
    def save(self, *args, **kwargs):
664 665 666 667
        """
        Override of the herited save function to be sure only one payment
        method of type 'cheque' exists.
        """
Gabriel Detraz's avatar
Gabriel Detraz committed
668
        if Paiement.objects.filter(type_paiement=1).count() > 1:
669 670 671
            raise ValidationError(
                _("You cannot have multiple payment method of type cheque")
            )
Gabriel Detraz's avatar
Gabriel Detraz committed
672 673
        super(Paiement, self).save(*args, **kwargs)

674
    def end_payment(self, invoice, request, use_payment_method=True):
675
        """
676 677 678 679 680 681 682 683 684
        The general way of ending a payment.

        :param invoice: The invoice being created.
        :param request: Request sended by the user.
        :param use_payment_method: If `self` has an attribute `payment_method`,
            returns the result of
            `self.payment_method.end_payment(invoice, request)`

        :returns: An `HttpResponse`-like object.
685
        """
686 687 688
        payment_method = find_payment_method(self)
        if payment_method is not None and use_payment_method:
            return payment_method.end_payment(invoice, request)
689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708

        # 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,
                _("The cotisation of %(member_name)s has been \
                extended to %(end_date)s.") % {
                    'member_name': request.user.pseudo,
                    'end_date': request.user.end_adhesion()
                }
            )
        # Else, only tell the invoice was created
        else:
            messages.success(
                request,
                _("The invoice has been created.")
            )
        return redirect(reverse(
            'users:profil',
709
            kwargs={'userid': invoice.user.pk}
710 711
        ))

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729
    def can_use_payment(self, user, *_args, **_kwargs):
        """Check if a user can use this payment.

        :param self: The payment
        :param user: The user requesting usage
        :returns: A boolean stating if usage is granted and an explanation
            message if the boolean is `False`.
        """
        return (
            self.available_for_everyone
            or user.has_perm('cotisations.use_every_payment'),
            _("You cannot use this Payment.")
        )

    @classmethod
    def find_allowed_payments(cls, user):
        return [p for p in cls.objects.all() if p.can_use_payment(user)[0]]

chirac's avatar
chirac committed
730

731
class Cotisation(RevMixin, AclMixin, models.Model):
732 733 734 735 736 737 738 739 740 741
    """
    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)
    """
742

743
    COTISATION_TYPE = (
744 745 746
        ('Connexion', _l("Connexion")),
        ('Adhesion', _l("Membership")),
        ('All', _l("Both of them")),
747 748
    )

749 750 751 752 753
    # TODO : change vente to purchase
    vente = models.OneToOneField(
        'Vente',
        on_delete=models.CASCADE,
        null=True,
754
        verbose_name=_l("Purchase")
755
    )
756 757 758
    type_cotisation = models.CharField(
        choices=COTISATION_TYPE,
        max_length=255,
759
        default='All',
760
        verbose_name=_l("Type of cotisation")
761 762
    )
    date_start = models.DateTimeField(
763
        verbose_name=_l("Starting date")
764 765
    )
    date_end = models.DateTimeField(
766
        verbose_name=_l("Ending date")
767
    )
768

769 770
    class Meta:
        permissions = (
771 772
            ('view_cotisation', _l("Can see a cotisation's details")),
            ('change_all_cotisation', _l("Can edit the previous cotisations")),
773 774
        )

775
    def can_edit(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
776
        if not user_request.has_perm('cotisations.change_cotisation'):
777
            return False, _("You don't have the right to edit a cotisation.")
778 779 780 781 782
        elif not user_request.has_perm('cotisations.change_all_cotisation') \
                and (self.vente.facture.control or
                     not self.vente.facture.valid):
            return False, _("You don't have the right to edit a cotisation "
                            "already controlled or invalidated.")
783 784
        else:
            return True, None
785

786
    def can_delete(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
787
        if not user_request.has_perm('cotisations.delete_cotisation'):
788 789
            return False, _("You don't have the right to delete a "
                            "cotisation.")
790
        if self.vente.facture.control or not self.vente.facture.valid:
791 792
            return False, _("You don't have the right to delete a cotisation "
                            "already controlled or invalidated.")
793 794
        else:
            return True, None
795

796
    def can_view(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
797
        if not user_request.has_perm('cotisations.view_cotisation') and\
798 799 800
                self.vente.facture.user != user_request:
            return False, _("You don't have the right to see someone else's "
                            "cotisation history.")
801 802
        else:
            return True, None
803

804
    def __str__(self):
805
        return str(self.vente)
806

chirac's avatar
chirac committed
807

808
@receiver(post_save, sender=Cotisation)
809
def cotisation_post_save(**_kwargs):
810 811 812 813
    """
    Mark some services as needing a regeneration after the edition of a
    cotisation. Indeed the membership status may have changed.
    """
814 815 816
    regen('dns')
    regen('dhcp')
    regen('mac_ip_list')
817
    regen('mailing')
818

chirac's avatar
chirac committed
819

820
@receiver(post_delete, sender=Cotisation)
821
def cotisation_post_delete(**_kwargs):
822 823 824 825
    """
    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
826
    regen('mac_ip_list')
827
    regen('mailing')