models.py 32.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 preferences.models import CotisationsOption
50
from machines.models import regen
51
from re2o.field_permissions import FieldPermissionModelMixin
52
from re2o.mixins import AclMixin, RevMixin
53

54 55 56
from cotisations.utils import (
    find_payment_method, send_mail_invoice, send_mail_voucher
)
57
from cotisations.validators import check_no_balance
58

59

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

    # 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'),
89
                output_field=models.DecimalField()
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
90 91 92 93 94 95 96 97 98 99 100 101 102 103
            )
        )['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


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

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

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

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

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

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

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

199
    def can_view(self, user_request, *_args, **_kwargs):
200 201 202 203 204 205 206 207
        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
208 209 210
        else:
            return True, None

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

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

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

236 237 238
    def __init__(self, *args, **kwargs):
        super(Facture, self).__init__(*args, **kwargs)
        self.field_permissions = {
239
            'control': self.can_change_control,
240
        }
241
        self.__original_valid = self.valid
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
242 243
        self.__original_control = self.control

244 245 246 247 248 249
    def get_subscription(self):
        return Cotisation.objects.filter(
            vente__in=self.vente_set.filter(
                Q(type_cotisation='All') |
                Q(type_cotisation='Cotisation')
            )
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
250 251
        )

252 253
    def is_subscription(self):
        return bool(self.get_subscription())
254

255 256 257 258
    def save(self, *args, **kwargs):
        super(Facture, self).save(*args, **kwargs)
        if not self.__original_valid and self.valid:
            send_mail_invoice(self)
259 260 261 262
        if self.is_subscription() \
            and not self.__original_control \
            and self.control \
            and CotisationsOption.get_cached_value('send_voucher_mail'):
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
263
            send_mail_voucher(self)
264

265
    def __str__(self):
Dalahro's avatar
Dalahro committed
266
        return str(self.user) + ' ' + str(self.date)
267

268
@receiver(post_save, sender=Facture)
269
def facture_post_save(**kwargs):
270 271 272
    """
    Synchronise the LDAP user after an invoice has been saved.
    """
273
    facture = kwargs['instance']
274 275
    if facture.valid:
        user = facture.user
276 277
        user.set_active()
        user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
278

chirac's avatar
chirac committed
279

280
@receiver(post_delete, sender=Facture)
281
def facture_post_delete(**kwargs):
282 283 284
    """
    Synchronise the LDAP user after an invoice has been deleted.
    """
285
    user = kwargs['instance'].user
286
    user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
287

chirac's avatar
chirac committed
288

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
289 290 291
class CustomInvoice(BaseInvoice):
    class Meta:
        permissions = (
292
            ('view_custominvoice', _("Can view a custom invoice object")),
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
293 294 295
        )
    recipient = models.CharField(
        max_length=255,
296
        verbose_name=_("Recipient")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
297 298 299
    )
    payment = models.CharField(
        max_length=255,
300
        verbose_name=_("Payment type")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
301 302 303
    )
    address = models.CharField(
        max_length=255,
304
        verbose_name=_("Address")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
305 306
    )
    paid = models.BooleanField(
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
307 308
        verbose_name=_("Paid"),
        default=False
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
309
    )
310 311 312 313 314
    remark = models.TextField(
        verbose_name=_("Remark"),
        blank=True,
        null=True
    )
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
315 316


Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
class CostEstimate(CustomInvoice):
    class Meta:
        permissions = (
            ('view_costestimate', _("Can view a cost estimate object")),
        )
    validity = models.DurationField(
        verbose_name=_("Period of validity"),
        help_text="DD HH:MM:SS"
    )
    final_invoice = models.ForeignKey(
        CustomInvoice,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="origin_cost_estimate",
        primary_key=False
    )

    def create_invoice(self):
        """Create a CustomInvoice from the CostEstimate."""
        if self.final_invoice is not None:
            return self.final_invoice
        invoice = CustomInvoice()
        invoice.recipient = self.recipient
        invoice.payment = self.payment
        invoice.address = self.address
        invoice.paid = False
        invoice.remark = self.remark
        invoice.date = timezone.now()
        invoice.save()
        self.final_invoice = invoice
        self.save()
        for sale in self.vente_set.all():
            Vente.objects.create(
                facture=invoice,
                name=sale.name,
                prix=sale.prix,
                number=sale.number,
            )
        return invoice

    def can_delete(self, user_request, *args, **kwargs):
        if not user_request.has_perm('cotisations.delete_costestimate'):
            return False, _("You don't have the right "
                            "to delete a cost estimate.")
        if self.final_invoice is not None:
            return False, _("The cost estimate has an "
364
                            "invoice and can't be deleted.")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
365 366 367
        return True, None


368
# TODO : change Vente to Purchase
369
class Vente(RevMixin, AclMixin, models.Model):
370 371 372
    """
    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.
373

374 375 376 377 378 379 380
    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)
    """
381

382
    # TODO : change this to English
383
    COTISATION_TYPE = (
384 385 386
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
387 388
    )

389 390
    # TODO : change facture to invoice
    facture = models.ForeignKey(
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
391
        'BaseInvoice',
392
        on_delete=models.CASCADE,
393
        verbose_name=_("invoice")
394 395 396 397
    )
    # TODO : change number to amount for clarity
    number = models.IntegerField(
        validators=[MinValueValidator(1)],
398
        verbose_name=_("amount")
399 400 401 402
    )
    # TODO : change this field for a ForeinKey to Article
    name = models.CharField(
        max_length=255,
403
        verbose_name=_("article")
404 405 406 407 408 409
    )
    # 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,
410
        verbose_name=_("price"))
411
    # TODO : this field is not needed if you use Article ForeignKey
412
    duration = models.PositiveIntegerField(
413
        blank=True,
414
        null=True,
415
        verbose_name=_("duration (in months)")
416 417
    )
    # TODO : this field is not needed if you use Article ForeignKey
418 419 420 421
    type_cotisation = models.CharField(
        choices=COTISATION_TYPE,
        blank=True,
        null=True,
422
        max_length=255,
423
        verbose_name=_("subscription type")
424
    )
425

426 427
    class Meta:
        permissions = (
428 429
            ('view_vente', _("Can view a purchase object")),
            ('change_all_vente', _("Can edit all the previous purchases")),
430
        )
431 432
        verbose_name = _("purchase")
        verbose_name_plural = _("purchases")
433

434
    # TODO : change prix_total to total_price
435
    def prix_total(self):
436 437 438
        """
        Returns: the total of price for this amount of items.
        """
439 440
        return self.prix*self.number

441
    def update_cotisation(self):
442 443 444 445
        """
        Update the related object 'cotisation' if there is one. Based on the
        duration of the purchase.
        """
446 447
        if hasattr(self, 'cotisation'):
            cotisation = self.cotisation
chirac's avatar
chirac committed
448
            cotisation.date_end = cotisation.date_start + relativedelta(
449
                months=self.duration*self.number)
450 451 452
        return

    def create_cotis(self, date_start=False):
453 454 455 456 457
        """
        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
458 459 460 461
        try:
            invoice = self.facture.facture
        except Facture.DoesNotExist:
            return
462
        if not hasattr(self, 'cotisation') and self.type_cotisation:
chirac's avatar
chirac committed
463
            cotisation = Cotisation(vente=self)
464
            cotisation.type_cotisation = self.type_cotisation
465
            if date_start:
Gabriel Detraz's avatar
Gabriel Detraz committed
466
                end_cotisation = Cotisation.objects.filter(
467 468
                    vente__in=Vente.objects.filter(
                        facture__in=Facture.objects.filter(
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
469
                            user=invoice.user
470
                        ).exclude(valid=False))
471 472 473 474 475 476
                ).filter(
                    Q(type_cotisation='All') |
                    Q(type_cotisation=self.type_cotisation)
                ).filter(
                    date_start__lt=date_start
                ).aggregate(Max('date_end'))['date_end__max']
477
            elif self.type_cotisation == "Adhesion":
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
478
                end_cotisation = invoice.user.end_adhesion()
479
            else:
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
480
                end_cotisation = invoice.user.end_connexion()
481
            date_start = date_start or timezone.now()
482 483
            end_cotisation = end_cotisation or date_start
            date_max = max(end_cotisation, date_start)
484
            cotisation.date_start = date_max
chirac's avatar
chirac committed
485
            cotisation.date_end = cotisation.date_start + relativedelta(
486
                months=self.duration*self.number
487
            )
488 489 490
        return

    def save(self, *args, **kwargs):
491 492 493 494 495 496
        """
        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
497
        if self.type_cotisation and not self.duration:
498
            raise ValidationError(
499
                _("Duration must be specified for a subscription.")
500
            )
501 502
        self.update_cotisation()
        super(Vente, self).save(*args, **kwargs)
503

504
    def can_edit(self, user_request, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
505
        if not user_request.has_perm('cotisations.change_vente'):
506
            return False, _("You don't have the right to edit the purchases.")
507 508 509
        elif (not user_request.has_perm('cotisations.change_all_facture') and
              not self.facture.user.can_edit(
                  user_request, *args, **kwargs
510
        )[0]):
511 512
            return False, _("You don't have the right to edit this user's "
                            "purchases.")
513 514
        elif (not user_request.has_perm('cotisations.change_all_vente') and
              (self.facture.control or not self.facture.valid)):
515 516
            return False, _("You don't have the right to edit a purchase "
                            "already controlled or invalidated.")
517 518
        else:
            return True, None
519

520
    def can_delete(self, user_request, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
521
        if not user_request.has_perm('cotisations.delete_vente'):
522
            return False, _("You don't have the right to delete a purchase.")
523
        if not self.facture.user.can_edit(user_request, *args, **kwargs)[0]:
524 525
            return False, _("You don't have the right to delete this user's "
                            "purchases.")
526
        if self.facture.control or not self.facture.valid:
527 528
            return False, _("You don't have the right to delete a purchase "
                            "already controlled or invalidated.")
529 530
        else:
            return True, None
531

532 533 534
    def can_view(self, user_request, *_args, **_kwargs):
        if (not user_request.has_perm('cotisations.view_vente') and
                self.facture.user != user_request):
535
            return False, _("You don't have the right to view someone "
536
                            "else's purchase history.")
537 538
        else:
            return True, None
539

540
    def __str__(self):
541
        return str(self.name) + ' ' + str(self.facture)
542

chirac's avatar
chirac committed
543

544
# TODO : change vente to purchase
545
@receiver(post_save, sender=Vente)
546
def vente_post_save(**kwargs):
547 548 549 550 551
    """
    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
552 553 554 555
    try:
        purchase.facture.facture
    except Facture.DoesNotExist:
        return
Gabriel Detraz's avatar
Gabriel Detraz committed
556
    if hasattr(purchase, 'cotisation'):
557 558 559 560 561
        purchase.cotisation.vente = purchase
        purchase.cotisation.save()
    if purchase.type_cotisation:
        purchase.create_cotis()
        purchase.cotisation.save()
562
        user = purchase.facture.facture.user
563 564
        user.set_active()
        user.ldap_sync(base=True, access_refresh=True, mac_refresh=False)
565

chirac's avatar
chirac committed
566

567
# TODO : change vente to purchase
568
@receiver(post_delete, sender=Vente)
569
def vente_post_delete(**kwargs):
570 571 572 573
    """
    Synchronise the LDAP user after a purchase has been deleted.
    """
    purchase = kwargs['instance']
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
574 575 576 577
    try:
        invoice = purchase.facture.facture
    except Facture.DoesNotExist:
        return
578
    if purchase.type_cotisation:
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
579
        user = invoice.user
580
        user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
581

chirac's avatar
chirac committed
582

583
class Article(RevMixin, AclMixin, models.Model):
584
    """
585 586 587
    The definition of an article model. It represents a type of object
    that can be sold to the user.

588 589 590
    It's represented by:
        * a name
        * a price
591 592
        * a cotisation type (indicating if this article reprensents a
            cotisation or not)
593 594 595
        * a duration (if it is a cotisation)
        * a type of user (indicating what kind of user can buy this article)
    """
596

597
    # TODO : Either use TYPE or TYPES in both choices but not both
598
    USER_TYPES = (
599 600 601
        ('Adherent', _("Member")),
        ('Club', _("Club")),
        ('All', _("Both of them")),
602 603 604
    )

    COTISATION_TYPE = (
605 606 607
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
608 609
    )

610 611
    name = models.CharField(
        max_length=255,
612
        verbose_name=_("designation")
613 614 615 616 617
    )
    # TODO : change prix to price
    prix = models.DecimalField(
        max_digits=5,
        decimal_places=2,
618
        verbose_name=_("unit price")
619
    )
620
    duration = models.PositiveIntegerField(
David Sinquin's avatar
David Sinquin committed
621 622
        blank=True,
        null=True,
623
        validators=[MinValueValidator(0)],
624
        verbose_name=_("duration (in months)")
625
    )
626 627 628
    type_user = models.CharField(
        choices=USER_TYPES,
        default='All',
629
        max_length=255,
630
        verbose_name=_("type of users concerned")
631 632 633 634 635 636
    )
    type_cotisation = models.CharField(
        choices=COTISATION_TYPE,
        default=None,
        blank=True,
        null=True,
637
        max_length=255,
638
        verbose_name=_("subscription type")
639
    )
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
640
    available_for_everyone = models.BooleanField(
641
        default=False,
642
        verbose_name=_("is available for every user")
643
    )
chibrac's avatar
chibrac committed
644

645 646
    unique_together = ('name', 'type_user')

647 648
    class Meta:
        permissions = (
649 650
            ('view_article', _("Can view an article object")),
            ('buy_every_article', _("Can buy every article"))
651
        )
652 653
        verbose_name = "article"
        verbose_name_plural = "articles"
654

chibrac's avatar
chibrac committed
655
    def clean(self):
656 657
        if self.name.lower() == 'solde':
            raise ValidationError(
658
                _("Balance is a reserved article name.")
659
            )
660 661
        if self.type_cotisation and not self.duration:
            raise ValidationError(
662
                _("Duration must be specified for a subscription.")
663
            )
chibrac's avatar
chibrac committed
664

665 666 667
    def __str__(self):
        return self.name

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

671 672 673 674 675 676
        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
677 678 679 680
            message if the boolean is `False`.
        """
        return (
            self.available_for_everyone
681 682
            or user.has_perm('cotisations.buy_every_article')
            or user.has_perm('cotisations.add_facture'),
683
            _("You can't buy this article.")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
684 685 686
        )

    @classmethod
687 688
    def find_allowed_articles(cls, user, target_user):
        """Finds every allowed articles for an user, on a target user.
689

690 691
        Args:
            user: The user requesting articles.
692
            target_user: The user to sell articles
693
        """
694 695 696
        if target_user is None:
            objects_pool = cls.objects.filter(Q(type_user='All'))
        elif target_user.is_class_club:
697 698 699 700 701 702 703
            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')
            )
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
704
        if target_user is not None and not target_user.is_adherent():
705 706 707
            objects_pool = objects_pool.filter(
                Q(type_cotisation='All') | Q(type_cotisation='Adhesion')
            )
708
        if user.has_perm('cotisations.buy_every_article'):
709 710
            return objects_pool
        return objects_pool.filter(available_for_everyone=True)
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
711

chirac's avatar
chirac committed
712

713
class Banque(RevMixin, AclMixin, models.Model):
714 715 716 717 718 719 720
    """
    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.
    """
721

722 723 724
    name = models.CharField(
        max_length=255,
    )
725

726 727
    class Meta:
        permissions = (
728
            ('view_banque', _("Can view a bank object")),
729
        )
730 731
        verbose_name = _("bank")
        verbose_name_plural = _("banks")
732

733 734 735
    def __str__(self):
        return self.name

chirac's avatar
chirac committed
736

737
# TODO : change Paiement to Payment
738
class Paiement(RevMixin, AclMixin, models.Model):
739 740 741 742 743 744
    """
    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
    """
745

746 747 748
    # TODO : change moyen to method
    moyen = models.CharField(
        max_length=255,
749
        verbose_name=_("method")
750
    )
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
751
    available_for_everyone = models.BooleanField(
752
        default=False,
753
        verbose_name=_("is available for every user")
754
    )
755 756 757
    is_balance = models.BooleanField(
        default=False,
        editable=False,
758 759
        verbose_name=_("is user balance"),
        help_text=_("There should be only one balance payment method."),
760 761
        validators=[check_no_balance]
    )
762

763 764
    class Meta:
        permissions = (
765 766
            ('view_paiement', _("Can view a payment method object")),
            ('use_every_payment', _("Can use every payment method")),
767
        )
768 769
        verbose_name = _("payment method")
        verbose_name_plural = _("payment methods")
770

771 772 773
    def __str__(self):
        return self.moyen

chibrac's avatar
chibrac committed
774
    def clean(self):
775
        """l
776 777
        Override of the herited clean function to get a correct name
        """
chibrac's avatar
chibrac committed
778 779
        self.moyen = self.moyen.title()

780
    def end_payment(self, invoice, request, use_payment_method=True):
781
        """
782 783
        The general way of ending a payment.

784 785 786 787 788 789
        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)`
790

791 792
        Returns:
            An `HttpResponse`-like object.
793
        """
794 795 796
        payment_method = find_payment_method(self)
        if payment_method is not None and use_payment_method:
            return payment_method.end_payment(invoice, request)
797

798 799 800 801
        ## So make this invoice valid, trigger send mail
        invoice.valid = True
        invoice.save()

802 803 804 805 806
        # 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,
807 808
                _("The subscription of %(member_name)s was extended to"
                  " %(end_date)s.") % {
809 810
                    'member_name': invoice.user.pseudo,
                    'end_date': invoice.user.end_adhesion()
811 812 813 814 815 816
                }
            )
        # Else, only tell the invoice was created
        else:
            messages.success(
                request,
817
                _("The invoice was created.")
818 819 820
            )
        return redirect(reverse(
            'users:profil',
821
            kwargs={'userid': invoice.user.pk}
822 823
        ))

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

827 828 829 830 831
        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
832 833 834 835
            message if the boolean is `False`.
        """
        return (
            self.available_for_everyone
836 837
            or user.has_perm('cotisations.use_every_payment')
            or user.has_perm('cotisations.add_facture'),
838
            _("You can't use this payment method.")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
839 840 841 842
        )

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

845 846
        Args:
            user: The user requesting payment methods.
847 848 849 850
        """
        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
851

852 853 854 855
    def get_payment_method_name(self):
        p = find_payment_method(self)
        if p is not None:
            return p._meta.verbose_name
856
        return _("No custom payment method.")
857

chirac's avatar
chirac committed
858

859
class Cotisation(RevMixin, AclMixin, models.Model):
860 861 862 863 864 865 866 867 868 869
    """
    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)
    """
870

871
    COTISATION_TYPE = (
872 873 874
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
875 876
    )

877 878 879 880 881
    # TODO : change vente to purchase
    vente = models.OneToOneField(
        'Vente',
        on_delete=models.CASCADE,
        null=True,
882
        verbose_name=_("purchase")
883
    )
884 885 886
    type_cotisation = models.CharField(
        choices=COTISATION_TYPE,
        max_length=255,
887
        default='All',
888
        verbose_name=_("subscription type")
889 890
    )
    date_start = models.DateTimeField(
891
        verbose_name=_("start date")
892 893
    )
    date_end = models.DateTimeField(
894
        verbose_name=_("end date")
895
    )
896

897 898
    class Meta:
        permissions = (
899 900
            ('view_cotisation', _("Can view a subscription object")),
            ('change_all_cotisation', _("Can edit the previous subscriptions")),
901
        )
902 903
        verbose_name = _("subscription")
        verbose_name_plural = _("subscriptions")
904

905
    def can_edit(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
906
        if not user_request.has_perm('cotisations.change_cotisation'):
907
            return False, _("You don't have the right to edit a subscription.")
908 909 910
        elif not user_request.has_perm('cotisations.change_all_cotisation') \
                and (self.vente.facture.control or
                     not self.vente.facture.valid):
911
            return False, _("You don't have the right to edit a subscription "
912
                            "already controlled or invalidated.")
913 914
        else:
            return True, None
915

916
    def can_delete(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
917
        if not user_request.has_perm('cotisations.delete_cotisation'):
918
            return False, _("You don't have the right to delete a "
919
                            "subscription.")
920
        if self.vente.facture.control or not self.vente.facture.valid:
921
            return False, _("You don't have the right to delete a subscription "
922
                            "already controlled or invalidated.")
923 924
        else:
            return True, None
925

926
    def can_view(self, user_request, *_args, **_kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
927
        if not user_request.has_perm('cotisations.view_cotisation') and\
928
                self.vente.facture.user != user_request:
929 930
            return False, _("You don't have the right to view someone else's "
                            "subscription history.")
931 932
        else:
            return True, None
933

934
    def __str__(self):
935
        return str(self.vente)
936

chirac's avatar
chirac committed
937

938
@receiver(post_save, sender=Cotisation)
939
def cotisation_post_save(**_kwargs):
940 941 942 943
    """
    Mark some services as needing a regeneration after the edition of a
    cotisation. Indeed the membership status may have changed.
    """
944 945 946
    regen('dns')
    regen('dhcp')
    regen('mac_ip_list')
947
    regen('mailing')
948

chirac's avatar
chirac committed
949

950
@receiver(post_delete, sender=Cotisation)
951
def cotisation_post_delete(**_kwargs):
952 953 954 955
    """
    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
956
    regen('mac_ip_list')
957
    regen('mailing')