models.py 33.6 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
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
239 240 241 242 243 244 245 246 247 248
        self.__original_control = self.control

    def get_subscribtion(self):
        return self.vent_set.filter(
            Q(type_cotisation='All') |
            Q(type_cotisation='Cotisation')
        )

    def is_subscribtion(self):
        return bool(self.get_subscribtion())
249

250 251 252 253
    def save(self, *args, **kwargs):
        super(Facture, self).save(*args, **kwargs)
        if not self.__original_valid and self.valid:
            send_mail_invoice(self)
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
254 255
        if self.is_subscribtion() and not self.__original_control and self.control:
            send_mail_voucher(self)
256

257
    def __str__(self):
Dalahro's avatar
Dalahro committed
258
        return str(self.user) + ' ' + str(self.date)
259

260
@receiver(post_save, sender=Facture)
261
def facture_post_save(**kwargs):
262 263 264
    """
    Synchronise the LDAP user after an invoice has been saved.
    """
265
    facture = kwargs['instance']
266 267
    if facture.valid:
        user = facture.user
268 269
        user.set_active()
        user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
270 271 272 273
    if facture.control:
        user = facture.user
        if user.is_adherent():
            user.notif_subscription_accepted()
274

chirac's avatar
chirac committed
275

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

chirac's avatar
chirac committed
284

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


Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
313 314 315 316 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
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 "
360
                            "invoice and can't be deleted.")
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
361 362 363
        return True, None


364
# TODO : change Vente to Purchase
365
class Vente(RevMixin, AclMixin, models.Model):
366 367 368
    """
    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.
369

370 371 372 373 374 375 376
    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)
    """
377

378
    # TODO : change this to English
379
    COTISATION_TYPE = (
380 381 382
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
383 384
    )

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

422 423
    class Meta:
        permissions = (
424 425
            ('view_vente', _("Can view a purchase object")),
            ('change_all_vente', _("Can edit all the previous purchases")),
426
        )
427 428
        verbose_name = _("purchase")
        verbose_name_plural = _("purchases")
429

430
    # TODO : change prix_total to total_price
431
    def prix_total(self):
432 433 434
        """
        Returns: the total of price for this amount of items.
        """
435 436
        return self.prix*self.number

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

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

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

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

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

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

536
    def __str__(self):
537
        return str(self.name) + ' ' + str(self.facture)
538

chirac's avatar
chirac committed
539

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

chirac's avatar
chirac committed
562

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

chirac's avatar
chirac committed
578

579
class Article(RevMixin, AclMixin, models.Model):
580
    """
581 582 583
    The definition of an article model. It represents a type of object
    that can be sold to the user.

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

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

    COTISATION_TYPE = (
601 602 603
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
604 605
    )

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

641 642
    unique_together = ('name', 'type_user')

643 644
    class Meta:
        permissions = (
645 646
            ('view_article', _("Can view an article object")),
            ('buy_every_article', _("Can buy every article"))
647
        )
648 649
        verbose_name = "article"
        verbose_name_plural = "articles"
650

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

661 662 663
    def __str__(self):
        return self.name

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

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

    @classmethod
683 684
    def find_allowed_articles(cls, user, target_user):
        """Finds every allowed articles for an user, on a target user.
685

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

chirac's avatar
chirac committed
708

709
class Banque(RevMixin, AclMixin, models.Model):
710 711 712 713 714 715 716
    """
    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.
    """
717

718 719 720
    name = models.CharField(
        max_length=255,
    )
721

722 723
    class Meta:
        permissions = (
724
            ('view_banque', _("Can view a bank object")),
725
        )
726 727
        verbose_name = _("bank")
        verbose_name_plural = _("banks")
728

729 730 731
    def __str__(self):
        return self.name

chirac's avatar
chirac committed
732

733
# TODO : change Paiement to Payment
734
class Paiement(RevMixin, AclMixin, models.Model):
735 736 737 738 739 740
    """
    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
    """
741

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

759 760
    class Meta:
        permissions = (
761 762
            ('view_paiement', _("Can view a payment method object")),
            ('use_every_payment', _("Can use every payment method")),
763
        )
764 765
        verbose_name = _("payment method")
        verbose_name_plural = _("payment methods")
766

767 768 769
    def __str__(self):
        return self.moyen

chibrac's avatar
chibrac committed
770
    def clean(self):
771
        """l
772 773
        Override of the herited clean function to get a correct name
        """
chibrac's avatar
chibrac committed
774 775
        self.moyen = self.moyen.title()

776
    def end_payment(self, invoice, request, use_payment_method=True):
777
        """
778 779
        The general way of ending a payment.

780 781 782 783 784 785
        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)`
786

787 788
        Returns:
            An `HttpResponse`-like object.
789
        """
790 791 792
        payment_method = find_payment_method(self)
        if payment_method is not None and use_payment_method:
            return payment_method.end_payment(invoice, request)
793

794 795 796 797
        ## So make this invoice valid, trigger send mail
        invoice.valid = True
        invoice.save()

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

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

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

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

841 842
        Args:
            user: The user requesting payment methods.
843 844 845 846
        """
        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
847

848 849 850 851
    def get_payment_method_name(self):
        p = find_payment_method(self)
        if p is not None:
            return p._meta.verbose_name
852
        return _("No custom payment method.")
853

chirac's avatar
chirac committed
854

855
class Cotisation(RevMixin, AclMixin, models.Model):
856 857 858 859 860 861 862 863 864 865
    """
    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)
    """
866

867
    COTISATION_TYPE = (
868 869 870
        ('Connexion', _("Connection")),
        ('Adhesion', _("Membership")),
        ('All', _("Both of them")),
871 872
    )

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

893 894
    class Meta:
        permissions = (
895 896
            ('view_cotisation', _("Can view a subscription object")),
            ('change_all_cotisation', _("Can edit the previous subscriptions")),
897
        )
898 899
        verbose_name = _("subscription")
        verbose_name_plural = _("subscriptions")
900

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

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

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

930
    def __str__(self):
931
        return str(self.vente)
932

chirac's avatar
chirac committed
933

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

chirac's avatar
chirac committed
945

946
@receiver(post_delete, sender=Cotisation)
947
def cotisation_post_delete(**_kwargs):
948 949 950 951
    """
    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
952
    regen('mac_ip_list')
953
    regen('mailing')
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989


class DocumentTemplate(RevMixin, AclMixin, models.Model):
    """Represent a template in order to create documents such as invoice or
    subscribtion voucher.
    """
    template = models.FileField(
        upload_to='templates/',
        verbose_name=_('template')
    )
    name = models.CharField(
        max_length=255,
        verbose_name=_('name')
    )

    class Meta:
        verbose_name = _("document template")
        verbose_name_plural = _("document templates")

    def __str__(self):
        return str(self.name)


class Voucher(RevMixin, AclMixin, models.Model):
    """A Subscription Voucher."""
    user = models.ForeignKey(
        'users.User',
        on_delete=models.CASCADE,
        verbose_name=_("user")
    )

    class Meta:
        verbose_name = _("subscription voucher")

    def __str__(self):
        return "voucher {} {}".format(self.user, self.date)