models.py 29.9 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

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
57 58 59
class BaseInvoice(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
    date = models.DateTimeField(
        auto_now_add=True,
60
        verbose_name=_("Date")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK 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
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK 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):
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
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):
Dalahro's avatar
Dalahro 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 247 248
    if facture.valid and facture.vente_set.filter(Q(type_cotisation='All') | Q(type_cotisation='Adhesion')).exists():
        user.state = 0
        user.save()
249
    user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
250

chirac's avatar
chirac committed
251

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

chirac's avatar
chirac committed
260

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


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

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

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

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

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

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

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

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

    def save(self, *args, **kwargs):
406 407 408 409 410 411
        """
        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
412
        if self.type_cotisation and not self.duration:
413
            raise ValidationError(
414
                _("Duration must be specified for a subscription.")
415
            )
416 417
        self.update_cotisation()
        super(Vente, self).save(*args, **kwargs)
418

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

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

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

455
    def __str__(self):
456
        return str(self.name) + ' ' + str(self.facture)
457

chirac's avatar
chirac committed
458

459
# TODO : change vente to purchase
460
@receiver(post_save, sender=Vente)
461
def vente_post_save(**kwargs):
462 463 464 465 466
    """
    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
467 468 469 470
    try:
        purchase.facture.facture
    except Facture.DoesNotExist:
        return
Gabriel Detraz's avatar
Gabriel Detraz committed
471
    if hasattr(purchase, 'cotisation'):
472 473 474 475 476 477
        purchase.cotisation.vente = purchase
        purchase.cotisation.save()
    if purchase.type_cotisation:
        purchase.create_cotis()
        purchase.cotisation.save()
        user = purchase.facture.user
478
        user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
479 480 481 482 483
    if purchase.facture.valid and (purchase.type_cotisation == 'All' or purchase.type_cotisation == 'Adhesion'):
        user = purchase.facture.user
        if user.state == 3:
            user.state = 0
            user.save()
484

chirac's avatar
chirac committed
485

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

chirac's avatar
chirac committed
501

502
class Article(RevMixin, AclMixin, models.Model):
503
    """
504 505 506
    The definition of an article model. It represents a type of object
    that can be sold to the user.

507 508 509
    It's represented by:
        * a name
        * a price
510 511
        * a cotisation type (indicating if this article reprensents a
            cotisation or not)
512 513 514
        * a duration (if it is a cotisation)
        * a type of user (indicating what kind of user can buy this article)
    """
515

516
    # TODO : Either use TYPE or TYPES in both choices but not both
517
    USER_TYPES = (
518 519 520
        ('Adherent', _("Member")),
        ('Club', _("Club")),
        ('All', _("Both of them")),
521 522 523
    )

    COTISATION_TYPE = (
524 525 526
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
527 528
    )

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

564 565
    unique_together = ('name', 'type_user')

566 567
    class Meta:
        permissions = (
568 569
            ('view_article', _("Can view an article object")),
            ('buy_every_article', _("Can buy every article"))
570
        )
571 572
        verbose_name = "article"
        verbose_name_plural = "articles"
573

chibrac's avatar
chibrac committed
574
    def clean(self):
575 576
        if self.name.lower() == 'solde':
            raise ValidationError(
577
                _("Balance is a reserved article name.")
578
            )
579 580
        if self.type_cotisation and not self.duration:
            raise ValidationError(
581
                _("Duration must be specified for a subscription.")
582
            )
chibrac's avatar
chibrac committed
583

584 585 586
    def __str__(self):
        return self.name

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

590 591 592 593 594 595
        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
596 597 598 599
            message if the boolean is `False`.
        """
        return (
            self.available_for_everyone
600 601
            or user.has_perm('cotisations.buy_every_article')
            or user.has_perm('cotisations.add_facture'),
602
            _("You can't buy this article.")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
603 604 605
        )

    @classmethod
606 607
    def find_allowed_articles(cls, user, target_user):
        """Finds every allowed articles for an user, on a target user.
608

609 610
        Args:
            user: The user requesting articles.
611
            target_user: The user to sell articles
612
        """
613 614 615 616 617 618 619 620
        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')
            )
621
        if user.has_perm('cotisations.buy_every_article'):
622 623
            return objects_pool
        return objects_pool.filter(available_for_everyone=True)
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
624

chirac's avatar
chirac committed
625

626
class Banque(RevMixin, AclMixin, models.Model):
627 628 629 630 631 632 633
    """
    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.
    """
634

635 636 637
    name = models.CharField(
        max_length=255,
    )
638

639 640
    class Meta:
        permissions = (
641
            ('view_banque', _("Can view a bank object")),
642
        )
643 644
        verbose_name = _("bank")
        verbose_name_plural = _("banks")
645

646 647 648
    def __str__(self):
        return self.name

chirac's avatar
chirac committed
649

650
# TODO : change Paiement to Payment
651
class Paiement(RevMixin, AclMixin, models.Model):
652 653 654 655 656 657
    """
    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
    """
658

659 660 661
    # TODO : change moyen to method
    moyen = models.CharField(
        max_length=255,
662
        verbose_name=_("method")
663
    )
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
664
    available_for_everyone = models.BooleanField(
665
        default=False,
666
        verbose_name=_("is available for every user")
667
    )
668 669 670
    is_balance = models.BooleanField(
        default=False,
        editable=False,
671 672
        verbose_name=_("is user balance"),
        help_text=_("There should be only one balance payment method."),
673 674
        validators=[check_no_balance]
    )
675

676 677
    class Meta:
        permissions = (
678 679
            ('view_paiement', _("Can view a payment method object")),
            ('use_every_payment', _("Can use every payment method")),
680
        )
681 682
        verbose_name = _("payment method")
        verbose_name_plural = _("payment methods")
683

684 685 686
    def __str__(self):
        return self.moyen

chibrac's avatar
chibrac committed
687
    def clean(self):
688
        """l
689 690
        Override of the herited clean function to get a correct name
        """
chibrac's avatar
chibrac committed
691 692
        self.moyen = self.moyen.title()

693
    def end_payment(self, invoice, request, use_payment_method=True):
694
        """
695 696
        The general way of ending a payment.

697 698 699 700 701 702
        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)`
703

704 705
        Returns:
            An `HttpResponse`-like object.
706
        """
707 708 709
        payment_method = find_payment_method(self)
        if payment_method is not None and use_payment_method:
            return payment_method.end_payment(invoice, request)
710 711 712 713 714 715

        # 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,
716 717
                _("The subscription of %(member_name)s was extended to"
                  " %(end_date)s.") % {
718 719
                    'member_name': invoice.user.pseudo,
                    'end_date': invoice.user.end_adhesion()
720 721 722 723 724 725
                }
            )
        # Else, only tell the invoice was created
        else:
            messages.success(
                request,
726
                _("The invoice was created.")
727 728 729
            )
        return redirect(reverse(
            'users:profil',
730
            kwargs={'userid': invoice.user.pk}
731 732
        ))

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

736 737 738 739 740
        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
741 742 743 744
            message if the boolean is `False`.
        """
        return (
            self.available_for_everyone
745 746
            or user.has_perm('cotisations.use_every_payment')
            or user.has_perm('cotisations.add_facture'),
747
            _("You can't use this payment method.")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
748 749 750 751
        )

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

754 755
        Args:
            user: The user requesting payment methods.
756 757 758 759
        """
        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
760

761 762 763 764
    def get_payment_method_name(self):
        p = find_payment_method(self)
        if p is not None:
            return p._meta.verbose_name
765
        return _("No custom payment method.")
766

chirac's avatar
chirac committed
767

768
class Cotisation(RevMixin, AclMixin, models.Model):
769 770 771 772 773 774 775 776 777 778
    """
    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)
    """
779

780
    COTISATION_TYPE = (
781 782 783
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
784 785
    )

786 787 788 789 790
    # TODO : change vente to purchase
    vente = models.OneToOneField(
        'Vente',
        on_delete=models.CASCADE,
        null=True,
791
        verbose_name=_("purchase")
792
    )
793 794 795
    type_cotisation = models.CharField(
        choices=COTISATION_TYPE,
        max_length=255,
796
        default='All',
797
        verbose_name=_("subscription type")
798 799
    )
    date_start = models.DateTimeField(
800
        verbose_name=_("start date")
801 802
    )
    date_end = models.DateTimeField(
803
        verbose_name=_("end date")
804
    )
805

806 807
    class Meta:
        permissions = (
808 809
            ('view_cotisation', _("Can view a subscription object")),
            ('change_all_cotisation', _("Can edit the previous subscriptions")),
810
        )
811 812
        verbose_name = _("subscription")
        verbose_name_plural = _("subscriptions")
813

814
    def can_edit(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
815
        if not user_request.has_perm('cotisations.change_cotisation'):
816
            return False, _("You don't have the right to edit a subscription.")
817 818 819
        elif not user_request.has_perm('cotisations.change_all_cotisation') \
                and (self.vente.facture.control or
                     not self.vente.facture.valid):
820
            return False, _("You don't have the right to edit a subscription "
821
                            "already controlled or invalidated.")
822 823
        else:
            return True, None
824

825
    def can_delete(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
826
        if not user_request.has_perm('cotisations.delete_cotisation'):
827
            return False, _("You don't have the right to delete a "
828
                            "subscription.")
829
        if self.vente.facture.control or not self.vente.facture.valid:
830
            return False, _("You don't have the right to delete a subscription "
831
                            "already controlled or invalidated.")
832 833
        else:
            return True, None
834

835
    def can_view(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
836
        if not user_request.has_perm('cotisations.view_cotisation') and\
837
                self.vente.facture.user != user_request:
838 839
            return False, _("You don't have the right to view someone else's "
                            "subscription history.")
840 841
        else:
            return True, None
842

843
    def __str__(self):
844
        return str(self.vente)
845

chirac's avatar
chirac committed
846

847
@receiver(post_save, sender=Cotisation)
848
def cotisation_post_save(**_kwargs):
849 850 851 852
    """
    Mark some services as needing a regeneration after the edition of a
    cotisation. Indeed the membership status may have changed.
    """
853 854 855
    regen('dns')
    regen('dhcp')
    regen('mac_ip_list')
856
    regen('mailing')
857

chirac's avatar
chirac committed
858

859
@receiver(post_delete, sender=Cotisation)
860
def cotisation_post_delete(**_kwargs):
861 862 863 864
    """
    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
865
    regen('mac_ip_list')
866
    regen('mailing')
867