models.py 29.2 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 as _
45
from django.utils.translation import ugettext_lazy as _l
46 47 48
from django.urls import reverse
from django.shortcuts import redirect
from django.contrib import messages
49

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

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

57

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
58 59 60 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 101
class BaseInvoice(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
    date = models.DateTimeField(
        auto_now_add=True,
        verbose_name=_l("Date")
    )

    # 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


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

108 109 110 111 112 113 114 115 116 117 118 119 120 121
    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.
    """
122

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

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

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

169
    def can_edit(self, user_request, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
170
        if not user_request.has_perm('cotisations.change_facture'):
171
            return False, _("You don't have the right to edit an invoice.")
172 173 174 175 176 177 178 179
        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.")
180 181 182 183
        else:
            return True, None

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

195
    def can_view(self, user_request, *_args, **_kwargs):
196 197 198 199
        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.")
200
        elif not self.valid:
201
            return False, _("The invoice has been invalidated.")
202 203 204
        else:
            return True, None

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

214 215
    @staticmethod
    def can_create(user_request, *_args, **_kwargs):
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
216
        """Check if a user can create an invoice.
217 218 219 220 221

        :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.
        """
222 223 224 225
        if user_request.has_perm('cotisations.add_facture'):
            return True, None
        if len(Paiement.find_allowed_payments(user_request)) <= 0:
            return False, _("There are no payment types which you can use.")
226
        if len(Article.find_allowed_articles(user_request)) <= 0:
227
            return False, _("There are no article that you can buy.")
228
        return True, None
229

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

236
    def __str__(self):
Dalahro's avatar
Dalahro committed
237
        return str(self.user) + ' ' + str(self.date)
238

chirac's avatar
chirac committed
239

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

chirac's avatar
chirac committed
249

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

chirac's avatar
chirac committed
258

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


281
# TODO : change Vente to Purchase
282
class Vente(RevMixin, AclMixin, models.Model):
283 284 285
    """
    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.
286

287 288 289 290 291 292 293
    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)
    """
294

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

302 303
    # TODO : change facture to invoice
    facture = models.ForeignKey(
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
304
        'BaseInvoice',
305
        on_delete=models.CASCADE,
306
        verbose_name=_l("Invoice")
307 308 309 310
    )
    # TODO : change number to amount for clarity
    number = models.IntegerField(
        validators=[MinValueValidator(1)],
311
        verbose_name=_l("Amount")
312 313 314 315
    )
    # TODO : change this field for a ForeinKey to Article
    name = models.CharField(
        max_length=255,
316
        verbose_name=_l("Article")
317 318 319 320 321 322
    )
    # 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,
323
        verbose_name=_l("Price"))
324
    # TODO : this field is not needed if you use Article ForeignKey
325
    duration = models.PositiveIntegerField(
326
        blank=True,
327
        null=True,
328
        verbose_name=_l("Duration (in whole month)")
329 330
    )
    # TODO : this field is not needed if you use Article ForeignKey
331 332 333 334
    type_cotisation = models.CharField(
        choices=COTISATION_TYPE,
        blank=True,
        null=True,
335
        max_length=255,
336
        verbose_name=_l("Type of cotisation")
337
    )
338

339 340
    class Meta:
        permissions = (
341 342
            ('view_vente', _l("Can see a purchase's details")),
            ('change_all_vente', _l("Can edit all the previous purchases")),
343
        )
344 345
        verbose_name = _l("Purchase")
        verbose_name_plural = _l("Purchases")
346

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

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

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

    def save(self, *args, **kwargs):
404 405 406 407 408 409
        """
        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
410
        if self.type_cotisation and not self.duration:
411 412 413
            raise ValidationError(
                _("A cotisation should always have a duration.")
            )
414 415
        self.update_cotisation()
        super(Vente, self).save(*args, **kwargs)
416

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

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

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

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

chirac's avatar
chirac committed
456

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

chirac's avatar
chirac committed
478

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

chirac's avatar
chirac committed
494

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

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

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

    COTISATION_TYPE = (
517 518 519
        ('Connexion', _l("Connexion")),
        ('Adhesion', _l("Membership")),
        ('All', _l("Both of them")),
520 521
    )

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

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

559 560
    class Meta:
        permissions = (
561
            ('view_article', _l("Can see an article's details")),
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
562
            ('buy_every_article', _l("Can buy every_article"))
563
        )
564 565
        verbose_name = "Article"
        verbose_name_plural = "Articles"
566

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

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

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

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

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

    @classmethod
    def find_allowed_articles(cls, user):
600 601
        """Finds every allowed articles for an user.

602 603
        Args:
            user: The user requesting articles.
604 605 606 607
        """
        if user.has_perm('cotisations.buy_every_article'):
            return cls.objects.all()
        return cls.objects.filter(available_for_everyone=True)
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
608

chirac's avatar
chirac committed
609

610
class Banque(RevMixin, AclMixin, models.Model):
611 612 613 614 615 616 617
    """
    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.
    """
618

619 620
    name = models.CharField(
        max_length=255,
621
        verbose_name=_l("Name")
622
    )
623

624 625
    class Meta:
        permissions = (
626
            ('view_banque', _l("Can see a bank's details")),
627
        )
628 629
        verbose_name = _l("Bank")
        verbose_name_plural = _l("Banks")
630

631 632 633
    def __str__(self):
        return self.name

chirac's avatar
chirac committed
634

635
# TODO : change Paiement to Payment
636
class Paiement(RevMixin, AclMixin, models.Model):
637 638 639 640 641 642
    """
    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
    """
643

644 645 646
    # TODO : change moyen to method
    moyen = models.CharField(
        max_length=255,
647
        verbose_name=_l("Method")
648
    )
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
649
    available_for_everyone = models.BooleanField(
650
        default=False,
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
651
        verbose_name=_l("Is available for every user")
652
    )
653 654 655 656 657 658 659
    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]
    )
660

661 662
    class Meta:
        permissions = (
663
            ('view_paiement', _l("Can see a payement's details")),
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
664
            ('use_every_payment', _l("Can use every payement")),
665
        )
666 667
        verbose_name = _l("Payment method")
        verbose_name_plural = _l("Payment methods")
668

669 670 671
    def __str__(self):
        return self.moyen

chibrac's avatar
chibrac committed
672
    def clean(self):
673 674 675
        """
        Override of the herited clean function to get a correct name
        """
chibrac's avatar
chibrac committed
676 677
        self.moyen = self.moyen.title()

678
    def end_payment(self, invoice, request, use_payment_method=True):
679
        """
680 681
        The general way of ending a payment.

682 683 684 685 686 687
        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)`
688

689 690
        Returns:
            An `HttpResponse`-like object.
691
        """
692 693 694
        payment_method = find_payment_method(self)
        if payment_method is not None and use_payment_method:
            return payment_method.end_payment(invoice, request)
695 696 697 698 699 700 701 702

        # 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.") % {
703 704
                    'member_name': invoice.user.pseudo,
                    'end_date': invoice.user.end_adhesion()
705 706 707 708 709 710 711 712 713 714
                }
            )
        # Else, only tell the invoice was created
        else:
            messages.success(
                request,
                _("The invoice has been created.")
            )
        return redirect(reverse(
            'users:profil',
715
            kwargs={'userid': invoice.user.pk}
716 717
        ))

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
718 719 720
    def can_use_payment(self, user, *_args, **_kwargs):
        """Check if a user can use this payment.

721 722 723 724 725
        Args:
            self: The payment
            user: The user requesting usage
        Returns:
            A boolean stating if usage is granted and an explanation
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
726 727 728 729
            message if the boolean is `False`.
        """
        return (
            self.available_for_everyone
730 731
            or user.has_perm('cotisations.use_every_payment')
            or user.has_perm('cotisations.add_facture'),
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
732 733 734 735 736
            _("You cannot use this Payment.")
        )

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

739 740
        Args:
            user: The user requesting payment methods.
741 742 743 744
        """
        if user.has_perm('cotisations.use_every_payment'):
            return cls.objects.all()
        return cls.objects.filter(available_for_everyone=True)
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
745

746 747 748 749 750 751
    def get_payment_method_name(self):
        p = find_payment_method(self)
        if p is not None:
            return p._meta.verbose_name
        return _("No custom payment method")

chirac's avatar
chirac committed
752

753
class Cotisation(RevMixin, AclMixin, models.Model):
754 755 756 757 758 759 760 761 762 763
    """
    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)
    """
764

765
    COTISATION_TYPE = (
766 767 768
        ('Connexion', _l("Connexion")),
        ('Adhesion', _l("Membership")),
        ('All', _l("Both of them")),
769 770
    )

771 772 773 774 775
    # TODO : change vente to purchase
    vente = models.OneToOneField(
        'Vente',
        on_delete=models.CASCADE,
        null=True,
776
        verbose_name=_l("Purchase")
777
    )
778 779 780
    type_cotisation = models.CharField(
        choices=COTISATION_TYPE,
        max_length=255,
781
        default='All',
782
        verbose_name=_l("Type of cotisation")
783 784
    )
    date_start = models.DateTimeField(
785
        verbose_name=_l("Starting date")
786 787
    )
    date_end = models.DateTimeField(
788
        verbose_name=_l("Ending date")
789
    )
790

791 792
    class Meta:
        permissions = (
793 794
            ('view_cotisation', _l("Can see a cotisation's details")),
            ('change_all_cotisation', _l("Can edit the previous cotisations")),
795 796
        )

797
    def can_edit(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
798
        if not user_request.has_perm('cotisations.change_cotisation'):
799
            return False, _("You don't have the right to edit a cotisation.")
800 801 802 803 804
        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.")
805 806
        else:
            return True, None
807

808
    def can_delete(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
809
        if not user_request.has_perm('cotisations.delete_cotisation'):
810 811
            return False, _("You don't have the right to delete a "
                            "cotisation.")
812
        if self.vente.facture.control or not self.vente.facture.valid:
813 814
            return False, _("You don't have the right to delete a cotisation "
                            "already controlled or invalidated.")
815 816
        else:
            return True, None
817

818
    def can_view(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
819
        if not user_request.has_perm('cotisations.view_cotisation') and\
820 821 822
                self.vente.facture.user != user_request:
            return False, _("You don't have the right to see someone else's "
                            "cotisation history.")
823 824
        else:
            return True, None
825

826
    def __str__(self):
827
        return str(self.vente)
828

chirac's avatar
chirac committed
829

830
@receiver(post_save, sender=Cotisation)
831
def cotisation_post_save(**_kwargs):
832 833 834 835
    """
    Mark some services as needing a regeneration after the edition of a
    cotisation. Indeed the membership status may have changed.
    """
836 837 838
    regen('dns')
    regen('dhcp')
    regen('mac_ip_list')
839
    regen('mailing')
840

chirac's avatar
chirac committed
841

842
@receiver(post_delete, sender=Cotisation)
843
def cotisation_post_delete(**_kwargs):
844 845 846 847
    """
    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
848
    regen('mac_ip_list')
849
    regen('mailing')