models.py 16.1 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.
#
#

Skia's avatar
Skia committed
25
from django.core.urlresolvers 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
44
45
46
47
48
49
50
class CurrencyField(models.DecimalField):
    """
    This is a custom database field used for currency
    """
    def __init__(self, *args, **kwargs):
        kwargs['max_digits'] = 12
        kwargs['decimal_places'] = 2
        super(CurrencyField, self).__init__(*args, **kwargs)

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

Skia's avatar
Skia committed
55
56
# Accounting classes

Krophil's avatar
Krophil committed
57

Skia's avatar
Skia committed
58
59
class Company(models.Model):
    name = models.CharField(_('name'), max_length=60)
60
61
62
63
64
65
66
    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
67
68
69
70

    class Meta:
        verbose_name = _("company")

Krophil's avatar
Krophil committed
71
72
73
74
75
76
77
78
79
80
81
82
83
    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():
84
            if club and club.role == settings.SITH_CLUB_ROLES_ID['Treasurer']:
Krophil's avatar
Krophil committed
85
86
87
88
89
90
91
92
                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():
93
            if club and club.role >= settings.SITH_CLUB_ROLES_ID['Treasurer']:
Krophil's avatar
Krophil committed
94
95
96
                return True
        return False

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

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

103
104
105
    def __str__(self):
        return self.name

Krophil's avatar
Krophil committed
106

Skia's avatar
Skia committed
107
class BankAccount(models.Model):
108
    name = models.CharField(_('name'), max_length=30)
109
    iban = models.CharField(_('iban'), max_length=255, blank=True)
110
    number = models.CharField(_('account number'), max_length=255, blank=True)
Skia's avatar
Skia committed
111
    club = models.ForeignKey(Club, related_name="bank_accounts", verbose_name=_("club"))
Skia's avatar
Skia committed
112

Skia's avatar
Skia committed
113
114
115
116
    class Meta:
        verbose_name = _("Bank account")
        ordering = ['club', 'name']

Skia's avatar
Skia committed
117
118
119
120
    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
121
        if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
Skia's avatar
Skia committed
122
123
            return True
        m = self.club.get_membership_for(user)
124
        if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID['Treasurer']:
Skia's avatar
Skia committed
125
126
            return True
        return False
127

Skia's avatar
Skia committed
128
129
130
    def get_absolute_url(self):
        return reverse('accounting:bank_details', kwargs={'b_account_id': self.id})

131
132
133
    def __str__(self):
        return self.name

Krophil's avatar
Krophil committed
134

Skia's avatar
Skia committed
135
class ClubAccount(models.Model):
136
    name = models.CharField(_('name'), max_length=30)
Skia's avatar
Skia committed
137
138
    club = models.ForeignKey(Club, related_name="club_account", verbose_name=_("club"))
    bank_account = models.ForeignKey(BankAccount, related_name="club_accounts", verbose_name=_("bank account"))
139

Skia's avatar
Skia committed
140
141
142
143
    class Meta:
        verbose_name = _("Club account")
        ordering = ['bank_account', 'name']

Skia's avatar
Skia committed
144
145
146
147
    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
148
        if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
Skia's avatar
Skia committed
149
150
151
152
153
154
155
156
            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)
157
        if m and m.role == settings.SITH_CLUB_ROLES_ID['Treasurer']:
Skia's avatar
Skia committed
158
159
160
161
162
163
164
165
            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)
166
        if m and m.role >= settings.SITH_CLUB_ROLES_ID['Treasurer']:
Skia's avatar
Skia committed
167
168
169
            return True
        return False

Skia's avatar
Skia committed
170
171
172
173
174
175
    def has_open_journal(self):
        for j in self.journals.all():
            if not j.closed:
                return True
        return False

Skia's avatar
Skia committed
176
177
178
    def get_open_journal(self):
        return self.journals.filter(closed=False).first()

Skia's avatar
Skia committed
179
180
181
    def get_absolute_url(self):
        return reverse('accounting:club_details', kwargs={'c_account_id': self.id})

182
183
184
    def __str__(self):
        return self.name

Skia's avatar
Skia committed
185
186
187
188
    def get_display_name(self):
        return _("%(club_account)s on %(bank_account)s") % {"club_account": self.name, "bank_account": self.bank_account}


Skia's avatar
Skia committed
189
class GeneralJournal(models.Model):
Skia's avatar
Skia committed
190
    """
191
    Class storing all the operations for a period of time
Skia's avatar
Skia committed
192
193
194
    """
    start_date = models.DateField(_('start date'))
    end_date = models.DateField(_('end date'), null=True, blank=True, default=None)
Skia's avatar
Skia committed
195
    name = models.CharField(_('name'), max_length=40)
Skia's avatar
Skia committed
196
    closed = models.BooleanField(_('is closed'), default=False)
Skia's avatar
Skia committed
197
    club_account = models.ForeignKey(ClubAccount, related_name="journals", null=False, verbose_name=_("club account"))
198
    amount = CurrencyField(_('amount'), default=0)
Skia's avatar
Skia committed
199
    effective_amount = CurrencyField(_('effective_amount'), default=0)
200

Skia's avatar
Skia committed
201
202
203
204
    class Meta:
        verbose_name = _("General journal")
        ordering = ['-start_date']

Skia's avatar
Skia committed
205
206
207
208
    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
209
        if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
Skia's avatar
Skia committed
210
211
212
213
214
            return True
        if self.club_account.can_be_edited_by(user):
            return True
        return False

Krophil's avatar
Krophil committed
215
216
217
218
219
220
221
222
223
224
    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
225
    def can_be_viewed_by(self, user):
Krophil's avatar
Krophil committed
226
        return self.club_account.can_be_viewed_by(user)
Skia's avatar
Skia committed
227

Skia's avatar
Skia committed
228
229
230
    def get_absolute_url(self):
        return reverse('accounting:journal_details', kwargs={'j_id': self.id})

Skia's avatar
Skia committed
231
232
233
    def __str__(self):
        return self.name

Skia's avatar
Skia committed
234
235
236
237
    def update_amounts(self):
        self.amount = 0
        self.effective_amount = 0
        for o in self.operations.all():
Skia's avatar
Skia committed
238
            if o.accounting_type.movement_type == "CREDIT":
Skia's avatar
Skia committed
239
240
241
242
243
244
245
246
                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
247

Krophil's avatar
Krophil committed
248

Skia's avatar
Skia committed
249
class Operation(models.Model):
250
251
    """
    An operation is a line in the journal, a debit or a credit
Skia's avatar
Skia committed
252
    """
253
    number = models.IntegerField(_('number'))
Skia's avatar
Skia committed
254
    journal = models.ForeignKey(GeneralJournal, related_name="operations", null=False, verbose_name=_("journal"))
255
    amount = CurrencyField(_('amount'))
256
    date = models.DateField(_('date'))
257
    remark = models.CharField(_('comment'), max_length=128, null=True, blank=True)
258
    mode = models.CharField(_('payment method'), max_length=255, choices=settings.SITH_ACCOUNTING_PAYMENT_METHOD)
Skia's avatar
Skia committed
259
    cheque_number = models.CharField(_('cheque number'), max_length=32, default="", null=True, blank=True)
Skia's avatar
Skia committed
260
    invoice = models.ForeignKey(SithFile, related_name='operations', verbose_name=_("invoice"), null=True, blank=True)
261
    done = models.BooleanField(_('is done'), default=False)
Skia's avatar
Skia committed
262
    simpleaccounting_type = models.ForeignKey('SimplifiedAccountingType', related_name="operations",
Krophil's avatar
Krophil committed
263
                                              verbose_name=_("simple type"), null=True, blank=True)
Skia's avatar
Skia committed
264
    accounting_type = models.ForeignKey('AccountingType', related_name="operations",
Krophil's avatar
Krophil committed
265
                                        verbose_name=_("accounting type"), null=True, blank=True)
Skia's avatar
Skia committed
266
    label = models.ForeignKey('Label', related_name="operations",
Krophil's avatar
Krophil committed
267
                              verbose_name=_("label"), null=True, blank=True, on_delete=models.SET_NULL)
Skia's avatar
Skia committed
268
    target_type = models.CharField(_('target type'), max_length=10,
Krophil's avatar
Krophil committed
269
                                   choices=[('USER', _('User')), ('CLUB', _('Club')), ('ACCOUNT', _('Account')), ('COMPANY', _('Company')), ('OTHER', _('Other'))])
Skia's avatar
Skia committed
270
271
    target_id = models.IntegerField(_('target id'), null=True, blank=True)
    target_label = models.CharField(_('target label'), max_length=32, default="", blank=True)
Skia's avatar
Skia committed
272
    linked_operation = models.OneToOneField('self', related_name='operation_linked_to', verbose_name=_("linked operation"),
Krophil's avatar
Krophil committed
273
                                            null=True, blank=True, default=None)
Skia's avatar
Skia committed
274

275
276
277
278
    class Meta:
        unique_together = ('number', 'journal')
        ordering = ['-number']

Skia's avatar
Skia committed
279
280
281
282
283
284
    def __getattribute__(self, attr):
        if attr == "target":
            return self.get_target()
        else:
            return object.__getattribute__(self, attr)

285
286
    def clean(self):
        super(Operation, self).clean()
287
288
289
        if self.date is None:
            raise ValidationError(_("The date must be set."))
        elif self.date < self.journal.start_date:
290
291
            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
292
293
294
295
        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 == "":
            raise ValidationError(_("Please add a target label if you set no existing target"))
Skia's avatar
Skia committed
296
297
298
299
300
301
302
303
        if not self.accounting_type and not self.simpleaccounting_type:
            raise ValidationError(_("You need to provide ether a simplified accounting type or a standard accounting type"))
        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
304
305
306
307
308
309
310
311
312
313
314
315

    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
316

Skia's avatar
Skia committed
317
    def save(self):
318
319
        if self.number is None:
            self.number = self.journal.operations.count() + 1
Skia's avatar
Skia committed
320
321
        super(Operation, self).save()
        self.journal.update_amounts()
Skia's avatar
Skia committed
322

Skia's avatar
Skia committed
323
324
325
326
    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
327
        if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
Skia's avatar
Skia committed
328
            return True
Skia's avatar
Skia committed
329
330
        if self.journal.closed:
            return False
331
        m = self.journal.club_account.club.get_membership_for(user)
Krophil's avatar
Krophil committed
332
        if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID['Treasurer']:
Skia's avatar
Skia committed
333
334
335
336
337
338
339
            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
340
341
342
343
344
345
        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)
        if m is not None and m.role == settings.SITH_CLUB_ROLES_ID['Treasurer']:
Skia's avatar
Skia committed
346
347
348
            return True
        return False

Skia's avatar
Skia committed
349
350
351
    def get_absolute_url(self):
        return reverse('accounting:journal_details', kwargs={'j_id': self.journal.id})

Skia's avatar
Skia committed
352
    def __str__(self):
353
        return "%d € | %s | %s | %s" % (
Krophil's avatar
Krophil committed
354
355
356
            self.amount, self.date, self.accounting_type, self.done,
        )

Skia's avatar
Skia committed
357
358
359
360
361
362
363

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

    Thoses are numbers used in accounting to classify operations
    """
Skia's avatar
Skia committed
364
    code = models.CharField(_('code'), max_length=16,
Krophil's avatar
Krophil committed
365
366
367
368
                            validators=[
        validators.RegexValidator(r'^[0-9]*$', _('An accounting type code contains only numbers')),
    ],
    )
Skia's avatar
Skia committed
369
    label = models.CharField(_('label'), max_length=128)
Skia's avatar
Skia committed
370
    movement_type = models.CharField(_('movement type'), choices=[('CREDIT', _('Credit')), ('DEBIT', _('Debit')),
Krophil's avatar
Krophil committed
371
                                                                  ('NEUTRAL', _('Neutral'))], max_length=12)
Skia's avatar
Skia committed
372

Skia's avatar
Skia committed
373
374
    class Meta:
        verbose_name = _("accounting type")
Skia's avatar
Skia committed
375
        ordering = ['movement_type', 'code']
Skia's avatar
Skia committed
376

Skia's avatar
Skia committed
377
378
379
380
    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
381
        if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
Skia's avatar
Skia committed
382
383
384
385
386
387
388
            return True
        return False

    def get_absolute_url(self):
        return reverse('accounting:type_list')

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

Skia's avatar
Skia committed
391
392
393
394
395
396
397

class SimplifiedAccountingType(models.Model):
    """
    Class describing the simplified accounting types.
    """
    label = models.CharField(_('label'), max_length=128)
    accounting_type = models.ForeignKey(AccountingType, related_name="simplified_types",
Krophil's avatar
Krophil committed
398
                                        verbose_name=_("simplified accounting types"))
Skia's avatar
Skia committed
399
400
401

    class Meta:
        verbose_name = _("simplified type")
Skia's avatar
Skia committed
402
        ordering = ['accounting_type__movement_type', 'accounting_type__code']
Skia's avatar
Skia committed
403
404
405
406
407
408
409
410
411
412
413
414

    @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):
        return reverse('accounting:simple_type_list')

    def __str__(self):
Krophil's avatar
Krophil committed
415
416
        return self.get_movement_type_display() + " - " + self.accounting_type.code + " - " + self.label

Skia's avatar
Skia committed
417

Skia's avatar
Skia committed
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
class Label(models.Model):
    """Label allow a club to sort its operations"""
    name = models.CharField(_('label'), max_length=64)
    club_account = models.ForeignKey(ClubAccount, related_name="labels", verbose_name=_("club account"))

    class Meta:
        unique_together = ('name', 'club_account')

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

    def get_absolute_url(self):
        return reverse('accounting:label_list', kwargs={'clubaccount_id': self.club_account.id})

    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)