models.py 33 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
    def get_subscription(self):
245
        """Returns every subscription associated with this invoice."""
246 247 248
        return Cotisation.objects.filter(
            vente__in=self.vente_set.filter(
                Q(type_cotisation='All') |
249
                Q(type_cotisation='Adhesion')
250
            )
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
251 252
        )

253
    def is_subscription(self):
254
        """Returns True if this invoice contains at least one subscribtion."""
255
        return bool(self.get_subscription())
256

257 258 259 260
    def save(self, *args, **kwargs):
        super(Facture, self).save(*args, **kwargs)
        if not self.__original_valid and self.valid:
            send_mail_invoice(self)
261
        if self.is_subscription() \
262 263 264
                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
265
            send_mail_voucher(self)
266

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

270

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

chirac's avatar
chirac committed
282

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

chirac's avatar
chirac committed
291

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


Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
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 364 365 366
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 "
367
                            "invoice and can't be deleted.")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
368 369 370
        return True, None


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

377 378 379 380 381 382 383
    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)
    """
384

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

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

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

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

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

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

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

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

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

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

543
    def __str__(self):
544
        return str(self.name) + ' ' + str(self.facture)
545

chirac's avatar
chirac committed
546

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

chirac's avatar
chirac committed
569

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

chirac's avatar
chirac committed
585

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

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

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

    COTISATION_TYPE = (
608 609 610
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
611 612
    )

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

648 649
    unique_together = ('name', 'type_user')

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

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

668 669 670
    def __str__(self):
        return self.name

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

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

    @classmethod
690 691
    def find_allowed_articles(cls, user, target_user):
        """Finds every allowed articles for an user, on a target user.
692

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

chirac's avatar
chirac committed
715

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

725 726 727
    name = models.CharField(
        max_length=255,
    )
728

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

736 737 738
    def __str__(self):
        return self.name

chirac's avatar
chirac committed
739

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

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

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

774 775 776
    def __str__(self):
        return self.moyen

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

783
    def end_payment(self, invoice, request, use_payment_method=True):
784
        """
785 786
        The general way of ending a payment.

787 788 789 790 791 792
        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)`
793

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

801
        # So make this invoice valid, trigger send mail
802 803 804
        invoice.valid = True
        invoice.save()

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

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

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

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

848 849
        Args:
            user: The user requesting payment methods.
850 851 852 853
        """
        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
854

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

chirac's avatar
chirac committed
861

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

874
    COTISATION_TYPE = (
875 876 877
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
878 879
    )

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

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

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

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

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

937
    def __str__(self):
938
        return str(self.vente)
939

chirac's avatar
chirac committed
940

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

chirac's avatar
chirac committed
952

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