models.py 17.4 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# -*- coding:utf-8 -*
#
# Copyright 2016,2017
# - Skia <skia@libskia.so>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 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 Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#

25
from django.urls import reverse
26
from django.core.exceptions import ValidationError
Skia's avatar
Skia committed
27
from django.core import validators
Skia's avatar
Skia committed
28
from django.db import models
29
from django.conf import settings
Skia's avatar
Skia committed
30
from django.utils.translation import ugettext_lazy as _
31
from django.template import defaultfilters
Skia's avatar
Skia committed
32

33
34
from phonenumber_field.modelfields import PhoneNumberField

Skia's avatar
Skia committed
35
from decimal import Decimal
Skia's avatar
Skia committed
36
from core.models import User, SithFile
37
from club.models import Club
Skia's avatar
Skia committed
38

Krophil's avatar
Krophil committed
39

Skia's avatar
Skia committed
40
41
42
43
class CurrencyField(models.DecimalField):
    """
    This is a custom database field used for currency
    """
Sli's avatar
Sli committed
44

Skia's avatar
Skia committed
45
    def __init__(self, *args, **kwargs):
Sli's avatar
Sli committed
46
47
        kwargs["max_digits"] = 12
        kwargs["decimal_places"] = 2
Skia's avatar
Skia committed
48
49
50
51
        super(CurrencyField, self).__init__(*args, **kwargs)

    def to_python(self, value):
        try:
Krophil's avatar
Krophil committed
52
            return super(CurrencyField, self).to_python(value).quantize(Decimal("0.01"))
Skia's avatar
Skia committed
53
        except AttributeError:
Krophil's avatar
Krophil committed
54
            return None
Skia's avatar
Skia committed
55

Sli's avatar
Sli committed
56

Skia's avatar
Skia committed
57
58
# Accounting classes

Krophil's avatar
Krophil committed
59

Skia's avatar
Skia committed
60
class Company(models.Model):
Sli's avatar
Sli committed
61
62
63
64
65
66
67
68
    name = models.CharField(_("name"), max_length=60)
    street = models.CharField(_("street"), max_length=60, blank=True)
    city = models.CharField(_("city"), max_length=60, blank=True)
    postcode = models.CharField(_("postcode"), max_length=10, blank=True)
    country = models.CharField(_("country"), max_length=32, blank=True)
    phone = PhoneNumberField(_("phone"), blank=True)
    email = models.EmailField(_("email"), blank=True)
    website = models.CharField(_("website"), max_length=64, blank=True)
Skia's avatar
Skia committed
69
70
71
72

    class Meta:
        verbose_name = _("company")

Krophil's avatar
Krophil committed
73
74
75
76
77
78
79
80
81
82
83
84
85
    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
        if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
            return True
        return False

    def can_be_edited_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
        for club in user.memberships.filter(end_date=None).all():
Sli's avatar
Sli committed
86
            if club and club.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
Krophil's avatar
Krophil committed
87
88
89
90
91
92
93
94
                return True
        return False

    def can_be_viewed_by(self, user):
        """
        Method to see if that object can be viewed by the given user
        """
        for club in user.memberships.filter(end_date=None).all():
Sli's avatar
Sli committed
95
            if club and club.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
Krophil's avatar
Krophil committed
96
97
98
                return True
        return False

Skia's avatar
Skia committed
99
    def get_absolute_url(self):
Sli's avatar
Sli committed
100
        return reverse("accounting:co_edit", kwargs={"co_id": self.id})
Skia's avatar
Skia committed
101
102
103

    def get_display_name(self):
        return self.name
Skia's avatar
Skia committed
104

105
106
107
    def __str__(self):
        return self.name

Krophil's avatar
Krophil committed
108

Skia's avatar
Skia committed
109
class BankAccount(models.Model):
Sli's avatar
Sli committed
110
111
112
    name = models.CharField(_("name"), max_length=30)
    iban = models.CharField(_("iban"), max_length=255, blank=True)
    number = models.CharField(_("account number"), max_length=255, blank=True)
113
114
115
116
117
118
    club = models.ForeignKey(
        Club,
        related_name="bank_accounts",
        verbose_name=_("club"),
        on_delete=models.CASCADE,
    )
Skia's avatar
Skia committed
119

Skia's avatar
Skia committed
120
121
    class Meta:
        verbose_name = _("Bank account")
Sli's avatar
Sli committed
122
        ordering = ["club", "name"]
Skia's avatar
Skia committed
123

Skia's avatar
Skia committed
124
125
126
127
    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
128
        if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
Skia's avatar
Skia committed
129
130
            return True
        m = self.club.get_membership_for(user)
Sli's avatar
Sli committed
131
        if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
Skia's avatar
Skia committed
132
133
            return True
        return False
134

Skia's avatar
Skia committed
135
    def get_absolute_url(self):
Sli's avatar
Sli committed
136
        return reverse("accounting:bank_details", kwargs={"b_account_id": self.id})
Skia's avatar
Skia committed
137

138
139
140
    def __str__(self):
        return self.name

Krophil's avatar
Krophil committed
141

Skia's avatar
Skia committed
142
class ClubAccount(models.Model):
Sli's avatar
Sli committed
143
    name = models.CharField(_("name"), max_length=30)
144
145
146
147
148
149
    club = models.ForeignKey(
        Club,
        related_name="club_account",
        verbose_name=_("club"),
        on_delete=models.CASCADE,
    )
Sli's avatar
Sli committed
150
    bank_account = models.ForeignKey(
151
152
153
154
        BankAccount,
        related_name="club_accounts",
        verbose_name=_("bank account"),
        on_delete=models.CASCADE,
Sli's avatar
Sli committed
155
    )
156

Skia's avatar
Skia committed
157
158
    class Meta:
        verbose_name = _("Club account")
Sli's avatar
Sli committed
159
        ordering = ["bank_account", "name"]
Skia's avatar
Skia committed
160

Skia's avatar
Skia committed
161
162
163
164
    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
165
        if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
Skia's avatar
Skia committed
166
167
168
169
170
171
172
173
            return True
        return False

    def can_be_edited_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
        m = self.club.get_membership_for(user)
Sli's avatar
Sli committed
174
        if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
Skia's avatar
Skia committed
175
176
177
178
179
180
181
182
            return True
        return False

    def can_be_viewed_by(self, user):
        """
        Method to see if that object can be viewed by the given user
        """
        m = self.club.get_membership_for(user)
Sli's avatar
Sli committed
183
        if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
Skia's avatar
Skia committed
184
185
186
            return True
        return False

Skia's avatar
Skia committed
187
188
189
190
191
192
    def has_open_journal(self):
        for j in self.journals.all():
            if not j.closed:
                return True
        return False

Skia's avatar
Skia committed
193
194
195
    def get_open_journal(self):
        return self.journals.filter(closed=False).first()

Skia's avatar
Skia committed
196
    def get_absolute_url(self):
Sli's avatar
Sli committed
197
        return reverse("accounting:club_details", kwargs={"c_account_id": self.id})
Skia's avatar
Skia committed
198

199
200
201
    def __str__(self):
        return self.name

Skia's avatar
Skia committed
202
    def get_display_name(self):
Sli's avatar
Sli committed
203
204
205
206
        return _("%(club_account)s on %(bank_account)s") % {
            "club_account": self.name,
            "bank_account": self.bank_account,
        }
Skia's avatar
Skia committed
207
208


Skia's avatar
Skia committed
209
class GeneralJournal(models.Model):
Skia's avatar
Skia committed
210
    """
211
    Class storing all the operations for a period of time
Skia's avatar
Skia committed
212
    """
Sli's avatar
Sli committed
213
214
215
216
217
218

    start_date = models.DateField(_("start date"))
    end_date = models.DateField(_("end date"), null=True, blank=True, default=None)
    name = models.CharField(_("name"), max_length=40)
    closed = models.BooleanField(_("is closed"), default=False)
    club_account = models.ForeignKey(
219
220
221
222
223
        ClubAccount,
        related_name="journals",
        null=False,
        verbose_name=_("club account"),
        on_delete=models.CASCADE,
Sli's avatar
Sli committed
224
225
226
    )
    amount = CurrencyField(_("amount"), default=0)
    effective_amount = CurrencyField(_("effective_amount"), default=0)
227

Skia's avatar
Skia committed
228
229
    class Meta:
        verbose_name = _("General journal")
Sli's avatar
Sli committed
230
        ordering = ["-start_date"]
Skia's avatar
Skia committed
231

Skia's avatar
Skia committed
232
233
234
235
    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
236
        if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
Skia's avatar
Skia committed
237
238
239
240
241
            return True
        if self.club_account.can_be_edited_by(user):
            return True
        return False

Krophil's avatar
Krophil committed
242
243
244
245
246
247
248
249
250
251
    def can_be_edited_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
        if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
            return True
        if self.club_account.can_be_edited_by(user):
            return True
        return False

Skia's avatar
Skia committed
252
    def can_be_viewed_by(self, user):
Krophil's avatar
Krophil committed
253
        return self.club_account.can_be_viewed_by(user)
Skia's avatar
Skia committed
254

Skia's avatar
Skia committed
255
    def get_absolute_url(self):
Sli's avatar
Sli committed
256
        return reverse("accounting:journal_details", kwargs={"j_id": self.id})
Skia's avatar
Skia committed
257

Skia's avatar
Skia committed
258
259
260
    def __str__(self):
        return self.name

Skia's avatar
Skia committed
261
262
263
264
    def update_amounts(self):
        self.amount = 0
        self.effective_amount = 0
        for o in self.operations.all():
Skia's avatar
Skia committed
265
            if o.accounting_type.movement_type == "CREDIT":
Skia's avatar
Skia committed
266
267
268
269
270
271
272
273
                if o.done:
                    self.effective_amount += o.amount
                self.amount += o.amount
            else:
                if o.done:
                    self.effective_amount -= o.amount
                self.amount -= o.amount
        self.save()
Skia's avatar
Skia committed
274

Krophil's avatar
Krophil committed
275

Skia's avatar
Skia committed
276
class Operation(models.Model):
277
278
    """
    An operation is a line in the journal, a debit or a credit
Skia's avatar
Skia committed
279
    """
Sli's avatar
Sli committed
280
281
282

    number = models.IntegerField(_("number"))
    journal = models.ForeignKey(
283
284
285
286
287
        GeneralJournal,
        related_name="operations",
        null=False,
        verbose_name=_("journal"),
        on_delete=models.CASCADE,
Sli's avatar
Sli committed
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
    )
    amount = CurrencyField(_("amount"))
    date = models.DateField(_("date"))
    remark = models.CharField(_("comment"), max_length=128, null=True, blank=True)
    mode = models.CharField(
        _("payment method"),
        max_length=255,
        choices=settings.SITH_ACCOUNTING_PAYMENT_METHOD,
    )
    cheque_number = models.CharField(
        _("cheque number"), max_length=32, default="", null=True, blank=True
    )
    invoice = models.ForeignKey(
        SithFile,
        related_name="operations",
        verbose_name=_("invoice"),
        null=True,
        blank=True,
306
        on_delete=models.CASCADE,
Sli's avatar
Sli committed
307
308
309
310
311
312
313
314
    )
    done = models.BooleanField(_("is done"), default=False)
    simpleaccounting_type = models.ForeignKey(
        "SimplifiedAccountingType",
        related_name="operations",
        verbose_name=_("simple type"),
        null=True,
        blank=True,
315
        on_delete=models.CASCADE,
Sli's avatar
Sli committed
316
317
318
319
320
321
322
    )
    accounting_type = models.ForeignKey(
        "AccountingType",
        related_name="operations",
        verbose_name=_("accounting type"),
        null=True,
        blank=True,
323
        on_delete=models.CASCADE,
Sli's avatar
Sli committed
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
    )
    label = models.ForeignKey(
        "Label",
        related_name="operations",
        verbose_name=_("label"),
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    target_type = models.CharField(
        _("target type"),
        max_length=10,
        choices=[
            ("USER", _("User")),
            ("CLUB", _("Club")),
            ("ACCOUNT", _("Account")),
            ("COMPANY", _("Company")),
            ("OTHER", _("Other")),
        ],
    )
    target_id = models.IntegerField(_("target id"), null=True, blank=True)
    target_label = models.CharField(
        _("target label"), max_length=32, default="", blank=True
    )
    linked_operation = models.OneToOneField(
        "self",
        related_name="operation_linked_to",
        verbose_name=_("linked operation"),
        null=True,
        blank=True,
        default=None,
355
        on_delete=models.CASCADE,
Sli's avatar
Sli committed
356
    )
Skia's avatar
Skia committed
357

358
    class Meta:
Sli's avatar
Sli committed
359
360
        unique_together = ("number", "journal")
        ordering = ["-number"]
361

Skia's avatar
Skia committed
362
363
364
365
366
367
    def __getattribute__(self, attr):
        if attr == "target":
            return self.get_target()
        else:
            return object.__getattribute__(self, attr)

368
369
    def clean(self):
        super(Operation, self).clean()
370
371
372
        if self.date is None:
            raise ValidationError(_("The date must be set."))
        elif self.date < self.journal.start_date:
Sli's avatar
Sli committed
373
374
375
376
377
378
379
380
381
382
383
            raise ValidationError(
                _(
                    """The date can not be before the start date of the journal, which is
%(start_date)s."""
                )
                % {
                    "start_date": defaultfilters.date(
                        self.journal.start_date, settings.DATE_FORMAT
                    )
                }
            )
Skia's avatar
Skia committed
384
385
386
        if self.target_type != "OTHER" and self.get_target() is None:
            raise ValidationError(_("Target does not exists"))
        if self.target_type == "OTHER" and self.target_label == "":
Sli's avatar
Sli committed
387
388
389
            raise ValidationError(
                _("Please add a target label if you set no existing target")
            )
Skia's avatar
Skia committed
390
        if not self.accounting_type and not self.simpleaccounting_type:
Sli's avatar
Sli committed
391
392
393
394
395
            raise ValidationError(
                _(
                    "You need to provide ether a simplified accounting type or a standard accounting type"
                )
            )
Skia's avatar
Skia committed
396
397
398
399
400
401
        if self.simpleaccounting_type:
            self.accounting_type = self.simpleaccounting_type.accounting_type

    @property
    def target(self):
        return self.get_target()
Skia's avatar
Skia committed
402
403
404
405
406
407
408
409
410
411
412
413

    def get_target(self):
        tar = None
        if self.target_type == "USER":
            tar = User.objects.filter(id=self.target_id).first()
        elif self.target_type == "CLUB":
            tar = Club.objects.filter(id=self.target_id).first()
        elif self.target_type == "ACCOUNT":
            tar = ClubAccount.objects.filter(id=self.target_id).first()
        elif self.target_type == "COMPANY":
            tar = Company.objects.filter(id=self.target_id).first()
        return tar
414

Skia's avatar
Skia committed
415
    def save(self):
416
417
        if self.number is None:
            self.number = self.journal.operations.count() + 1
Skia's avatar
Skia committed
418
419
        super(Operation, self).save()
        self.journal.update_amounts()
Skia's avatar
Skia committed
420

Skia's avatar
Skia committed
421
422
423
424
    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
425
        if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
Skia's avatar
Skia committed
426
            return True
Skia's avatar
Skia committed
427
428
        if self.journal.closed:
            return False
429
        m = self.journal.club_account.club.get_membership_for(user)
Sli's avatar
Sli committed
430
        if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]:
Skia's avatar
Skia committed
431
432
433
434
435
436
437
            return True
        return False

    def can_be_edited_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
Krophil's avatar
Krophil committed
438
439
440
441
442
        if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
            return True
        if self.journal.closed:
            return False
        m = self.journal.club_account.club.get_membership_for(user)
Sli's avatar
Sli committed
443
        if m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]:
Skia's avatar
Skia committed
444
445
446
            return True
        return False

Skia's avatar
Skia committed
447
    def get_absolute_url(self):
Sli's avatar
Sli committed
448
        return reverse("accounting:journal_details", kwargs={"j_id": self.journal.id})
Skia's avatar
Skia committed
449

Skia's avatar
Skia committed
450
    def __str__(self):
451
        return "%d € | %s | %s | %s" % (
Sli's avatar
Sli committed
452
453
454
455
            self.amount,
            self.date,
            self.accounting_type,
            self.done,
Krophil's avatar
Krophil committed
456
457
        )

Skia's avatar
Skia committed
458
459
460
461
462
463
464

class AccountingType(models.Model):
    """
    Class describing the accounting types.

    Thoses are numbers used in accounting to classify operations
    """
Sli's avatar
Sli committed
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483

    code = models.CharField(
        _("code"),
        max_length=16,
        validators=[
            validators.RegexValidator(
                r"^[0-9]*$", _("An accounting type code contains only numbers")
            )
        ],
    )
    label = models.CharField(_("label"), max_length=128)
    movement_type = models.CharField(
        _("movement type"),
        choices=[
            ("CREDIT", _("Credit")),
            ("DEBIT", _("Debit")),
            ("NEUTRAL", _("Neutral")),
        ],
        max_length=12,
Krophil's avatar
Krophil committed
484
    )
Skia's avatar
Skia committed
485

Skia's avatar
Skia committed
486
487
    class Meta:
        verbose_name = _("accounting type")
Sli's avatar
Sli committed
488
        ordering = ["movement_type", "code"]
Skia's avatar
Skia committed
489

Skia's avatar
Skia committed
490
491
492
493
    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
494
        if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
Skia's avatar
Skia committed
495
496
497
498
            return True
        return False

    def get_absolute_url(self):
Sli's avatar
Sli committed
499
        return reverse("accounting:type_list")
Skia's avatar
Skia committed
500
501

    def __str__(self):
Krophil's avatar
Krophil committed
502
503
        return self.code + " - " + self.get_movement_type_display() + " - " + self.label

Skia's avatar
Skia committed
504
505
506
507
508

class SimplifiedAccountingType(models.Model):
    """
    Class describing the simplified accounting types.
    """
Sli's avatar
Sli committed
509
510
511
512
513
514

    label = models.CharField(_("label"), max_length=128)
    accounting_type = models.ForeignKey(
        AccountingType,
        related_name="simplified_types",
        verbose_name=_("simplified accounting types"),
515
        on_delete=models.CASCADE,
Sli's avatar
Sli committed
516
    )
Skia's avatar
Skia committed
517
518
519

    class Meta:
        verbose_name = _("simplified type")
Sli's avatar
Sli committed
520
        ordering = ["accounting_type__movement_type", "accounting_type__code"]
Skia's avatar
Skia committed
521
522
523
524
525
526
527
528
529

    @property
    def movement_type(self):
        return self.accounting_type.movement_type

    def get_movement_type_display(self):
        return self.accounting_type.get_movement_type_display()

    def get_absolute_url(self):
Sli's avatar
Sli committed
530
        return reverse("accounting:simple_type_list")
Skia's avatar
Skia committed
531
532

    def __str__(self):
Sli's avatar
Sli committed
533
534
535
536
537
538
539
        return (
            self.get_movement_type_display()
            + " - "
            + self.accounting_type.code
            + " - "
            + self.label
        )
Krophil's avatar
Krophil committed
540

Skia's avatar
Skia committed
541

Skia's avatar
Skia committed
542
543
class Label(models.Model):
    """Label allow a club to sort its operations"""
Sli's avatar
Sli committed
544
545
546

    name = models.CharField(_("label"), max_length=64)
    club_account = models.ForeignKey(
547
548
549
550
        ClubAccount,
        related_name="labels",
        verbose_name=_("club account"),
        on_delete=models.CASCADE,
Sli's avatar
Sli committed
551
    )
Skia's avatar
Skia committed
552
553

    class Meta:
Sli's avatar
Sli committed
554
        unique_together = ("name", "club_account")
Skia's avatar
Skia committed
555
556
557
558
559

    def __str__(self):
        return "%s (%s)" % (self.name, self.club_account.name)

    def get_absolute_url(self):
Sli's avatar
Sli committed
560
561
562
        return reverse(
            "accounting:label_list", kwargs={"clubaccount_id": self.club_account.id}
        )
Skia's avatar
Skia committed
563
564
565
566
567
568
569
570
571

    def is_owned_by(self, user):
        return self.club_account.is_owned_by(user)

    def can_be_edited_by(self, user):
        return self.club_account.can_be_edited_by(user)

    def can_be_viewed_by(self, user):
        return self.club_account.can_be_viewed_by(user)