models.py 30.4 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, send_mail_invoice
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
    )

    # 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'),
86
                output_field=models.DecimalField()
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
87 88 89 90 91 92 93 94 95 96 97 98 99 100
            )
        )['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
    )
    # TODO : change name to validity for clarity
    valid = models.BooleanField(
140
        default=False,
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 186
        elif not user_request.has_perm('cotisations.change_all_facture') and \
                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 190
        elif not user_request.has_perm('cotisations.change_all_facture') and \
                (self.control or not self.valid):
191 192
            return False, _("You don't have the right to delete an invoice "
                            "already controlled or invalidated.")
193 194 195
        else:
            return True, None

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

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

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

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

233 234 235
    def __init__(self, *args, **kwargs):
        super(Facture, self).__init__(*args, **kwargs)
        self.field_permissions = {
236
            'control': self.can_change_control,
237
        }
238
        self.__original_valid = self.valid
239

240 241 242 243 244
    def save(self, *args, **kwargs):
        super(Facture, self).save(*args, **kwargs)
        if not self.__original_valid and self.valid:
            send_mail_invoice(self)

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

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

chirac's avatar
chirac committed
259

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

chirac's avatar
chirac committed
268

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
269 270 271
class CustomInvoice(BaseInvoice):
    class Meta:
        permissions = (
272
            ('view_custominvoice', _("Can view a custom invoice object")),
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
273 274 275
        )
    recipient = models.CharField(
        max_length=255,
276
        verbose_name=_("Recipient")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
277 278 279
    )
    payment = models.CharField(
        max_length=255,
280
        verbose_name=_("Payment type")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
281 282 283
    )
    address = models.CharField(
        max_length=255,
284
        verbose_name=_("Address")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
285 286
    )
    paid = models.BooleanField(
287
        verbose_name=_("Paid")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
288 289 290
    )


291
# TODO : change Vente to Purchase
292
class Vente(RevMixin, AclMixin, models.Model):
293 294 295
    """
    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.
296

297 298 299 300 301 302 303
    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)
    """
304

305
    # TODO : change this to English
306
    COTISATION_TYPE = (
307 308 309
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
310 311
    )

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

349 350
    class Meta:
        permissions = (
351 352
            ('view_vente', _("Can view a purchase object")),
            ('change_all_vente', _("Can edit all the previous purchases")),
353
        )
354 355
        verbose_name = _("purchase")
        verbose_name_plural = _("purchases")
356

357
    # TODO : change prix_total to total_price
358
    def prix_total(self):
359 360 361
        """
        Returns: the total of price for this amount of items.
        """
362 363
        return self.prix*self.number

364
    def update_cotisation(self):
365 366 367 368
        """
        Update the related object 'cotisation' if there is one. Based on the
        duration of the purchase.
        """
369 370
        if hasattr(self, 'cotisation'):
            cotisation = self.cotisation
chirac's avatar
chirac committed
371
            cotisation.date_end = cotisation.date_start + relativedelta(
372
                months=self.duration*self.number)
373 374 375
        return

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

    def save(self, *args, **kwargs):
414 415 416 417 418 419
        """
        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
420
        if self.type_cotisation and not self.duration:
421
            raise ValidationError(
422
                _("Duration must be specified for a subscription.")
423
            )
424 425
        self.update_cotisation()
        super(Vente, self).save(*args, **kwargs)
426

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

443
    def can_delete(self, user_request, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
444
        if not user_request.has_perm('cotisations.delete_vente'):
445
            return False, _("You don't have the right to delete a purchase.")
446
        if not self.facture.user.can_edit(user_request, *args, **kwargs)[0]:
447 448
            return False, _("You don't have the right to delete this user's "
                            "purchases.")
449
        if self.facture.control or not self.facture.valid:
450 451
            return False, _("You don't have the right to delete a purchase "
                            "already controlled or invalidated.")
452 453
        else:
            return True, None
454

455 456 457
    def can_view(self, user_request, *_args, **_kwargs):
        if (not user_request.has_perm('cotisations.view_vente') and
                self.facture.user != user_request):
458
            return False, _("You don't have the right to view someone "
459
                            "else's purchase history.")
460 461
        else:
            return True, None
462

463
    def __str__(self):
464
        return str(self.name) + ' ' + str(self.facture)
465

chirac's avatar
chirac committed
466

467
# TODO : change vente to purchase
468
@receiver(post_save, sender=Vente)
469
def vente_post_save(**kwargs):
470 471 472 473 474
    """
    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
475 476 477 478
    try:
        purchase.facture.facture
    except Facture.DoesNotExist:
        return
Gabriel Detraz's avatar
Gabriel Detraz committed
479
    if hasattr(purchase, 'cotisation'):
480 481 482 483 484
        purchase.cotisation.vente = purchase
        purchase.cotisation.save()
    if purchase.type_cotisation:
        purchase.create_cotis()
        purchase.cotisation.save()
485
        user = purchase.facture.facture.user
486 487
        user.set_active()
        user.ldap_sync(base=True, access_refresh=True, mac_refresh=False)
488

chirac's avatar
chirac committed
489

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

chirac's avatar
chirac committed
505

506
class Article(RevMixin, AclMixin, models.Model):
507
    """
508 509 510
    The definition of an article model. It represents a type of object
    that can be sold to the user.

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

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

    COTISATION_TYPE = (
528 529 530
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
531 532
    )

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

568 569
    unique_together = ('name', 'type_user')

570 571
    class Meta:
        permissions = (
572 573
            ('view_article', _("Can view an article object")),
            ('buy_every_article', _("Can buy every article"))
574
        )
575 576
        verbose_name = "article"
        verbose_name_plural = "articles"
577

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

588 589 590
    def __str__(self):
        return self.name

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

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

    @classmethod
610 611
    def find_allowed_articles(cls, user, target_user):
        """Finds every allowed articles for an user, on a target user.
612

613 614
        Args:
            user: The user requesting articles.
615
            target_user: The user to sell articles
616
        """
617 618 619
        if target_user is None:
            objects_pool = cls.objects.filter(Q(type_user='All'))
        elif target_user.is_class_club:
620 621 622 623 624 625 626
            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')
            )
627 628 629 630
        if not target_user.is_adherent():
            objects_pool = objects_pool.filter(
                Q(type_cotisation='All') | Q(type_cotisation='Adhesion')
            )
631
        if user.has_perm('cotisations.buy_every_article'):
632 633
            return objects_pool
        return objects_pool.filter(available_for_everyone=True)
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
634

chirac's avatar
chirac committed
635

636
class Banque(RevMixin, AclMixin, models.Model):
637 638 639 640 641 642 643
    """
    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.
    """
644

645 646 647
    name = models.CharField(
        max_length=255,
    )
648

649 650
    class Meta:
        permissions = (
651
            ('view_banque', _("Can view a bank object")),
652
        )
653 654
        verbose_name = _("bank")
        verbose_name_plural = _("banks")
655

656 657 658
    def __str__(self):
        return self.name

chirac's avatar
chirac committed
659

660
# TODO : change Paiement to Payment
661
class Paiement(RevMixin, AclMixin, models.Model):
662 663 664 665 666 667
    """
    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
    """
668

669 670 671
    # TODO : change moyen to method
    moyen = models.CharField(
        max_length=255,
672
        verbose_name=_("method")
673
    )
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
674
    available_for_everyone = models.BooleanField(
675
        default=False,
676
        verbose_name=_("is available for every user")
677
    )
678 679 680
    is_balance = models.BooleanField(
        default=False,
        editable=False,
681 682
        verbose_name=_("is user balance"),
        help_text=_("There should be only one balance payment method."),
683 684
        validators=[check_no_balance]
    )
685

686 687
    class Meta:
        permissions = (
688 689
            ('view_paiement', _("Can view a payment method object")),
            ('use_every_payment', _("Can use every payment method")),
690
        )
691 692
        verbose_name = _("payment method")
        verbose_name_plural = _("payment methods")
693

694 695 696
    def __str__(self):
        return self.moyen

chibrac's avatar
chibrac committed
697
    def clean(self):
698
        """l
699 700
        Override of the herited clean function to get a correct name
        """
chibrac's avatar
chibrac committed
701 702
        self.moyen = self.moyen.title()

703
    def end_payment(self, invoice, request, use_payment_method=True):
704
        """
705 706
        The general way of ending a payment.

707 708 709 710 711 712
        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)`
713

714 715
        Returns:
            An `HttpResponse`-like object.
716
        """
717 718 719
        payment_method = find_payment_method(self)
        if payment_method is not None and use_payment_method:
            return payment_method.end_payment(invoice, request)
720

721 722 723 724
        ## So make this invoice valid, trigger send mail
        invoice.valid = True
        invoice.save()

725 726 727 728 729
        # 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,
730 731
                _("The subscription of %(member_name)s was extended to"
                  " %(end_date)s.") % {
732 733
                    'member_name': invoice.user.pseudo,
                    'end_date': invoice.user.end_adhesion()
734 735 736 737 738 739
                }
            )
        # Else, only tell the invoice was created
        else:
            messages.success(
                request,
740
                _("The invoice was created.")
741 742 743
            )
        return redirect(reverse(
            'users:profil',
744
            kwargs={'userid': invoice.user.pk}
745 746
        ))

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

750 751 752 753 754
        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
755 756 757 758
            message if the boolean is `False`.
        """
        return (
            self.available_for_everyone
759 760
            or user.has_perm('cotisations.use_every_payment')
            or user.has_perm('cotisations.add_facture'),
761
            _("You can't use this payment method.")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
762 763 764 765
        )

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

768 769
        Args:
            user: The user requesting payment methods.
770 771 772 773
        """
        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
774

775 776 777 778
    def get_payment_method_name(self):
        p = find_payment_method(self)
        if p is not None:
            return p._meta.verbose_name
779
        return _("No custom payment method.")
780

chirac's avatar
chirac committed
781

782
class Cotisation(RevMixin, AclMixin, models.Model):
783 784 785 786 787 788 789 790 791 792
    """
    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)
    """
793

794
    COTISATION_TYPE = (
795 796 797
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
798 799
    )

800 801 802 803 804
    # TODO : change vente to purchase
    vente = models.OneToOneField(
        'Vente',
        on_delete=models.CASCADE,
        null=True,
805
        verbose_name=_("purchase")
806
    )
807 808 809
    type_cotisation = models.CharField(
        choices=COTISATION_TYPE,
        max_length=255,
810
        default='All',
811
        verbose_name=_("subscription type")
812 813
    )
    date_start = models.DateTimeField(
814
        verbose_name=_("start date")
815 816
    )
    date_end = models.DateTimeField(
817
        verbose_name=_("end date")
818
    )
819

820 821
    class Meta:
        permissions = (
822 823
            ('view_cotisation', _("Can view a subscription object")),
            ('change_all_cotisation', _("Can edit the previous subscriptions")),
824
        )
825 826
        verbose_name = _("subscription")
        verbose_name_plural = _("subscriptions")
827

828
    def can_edit(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
829
        if not user_request.has_perm('cotisations.change_cotisation'):
830
            return False, _("You don't have the right to edit a subscription.")
831 832 833
        elif not user_request.has_perm('cotisations.change_all_cotisation') \
                and (self.vente.facture.control or
                     not self.vente.facture.valid):
834
            return False, _("You don't have the right to edit a subscription "
835
                            "already controlled or invalidated.")
836 837
        else:
            return True, None
838

839
    def can_delete(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
840
        if not user_request.has_perm('cotisations.delete_cotisation'):
841
            return False, _("You don't have the right to delete a "
842
                            "subscription.")
843
        if self.vente.facture.control or not self.vente.facture.valid:
844
            return False, _("You don't have the right to delete a subscription "
845
                            "already controlled or invalidated.")
846 847
        else:
            return True, None
848

849
    def can_view(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
850
        if not user_request.has_perm('cotisations.view_cotisation') and\
851
                self.vente.facture.user != user_request:
852 853
            return False, _("You don't have the right to view someone else's "
                            "subscription history.")
854 855
        else:
            return True, None
856

857
    def __str__(self):
858
        return str(self.vente)
859

chirac's avatar
chirac committed
860

861
@receiver(post_save, sender=Cotisation)
862
def cotisation_post_save(**_kwargs):
863 864 865 866
    """
    Mark some services as needing a regeneration after the edition of a
    cotisation. Indeed the membership status may have changed.
    """
867 868 869
    regen('dns')
    regen('dhcp')
    regen('mac_ip_list')
870
    regen('mailing')
871

chirac's avatar
chirac committed
872

873
@receiver(post_delete, sender=Cotisation)
874
def cotisation_post_delete(**_kwargs):
875 876 877 878
    """
    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
879
    regen('mac_ip_list')
880
    regen('mailing')