models.py 16.2 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
51
52
class CurrencyField(models.DecimalField):
    """
    This is a custom database field used for currency
    """
    __metaclass__ = models.SubfieldBase

    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
53
            return super(CurrencyField, self).to_python(value).quantize(Decimal("0.01"))
Skia's avatar
Skia committed
54
        except AttributeError:
Krophil's avatar
Krophil committed
55
            return None
Skia's avatar
Skia committed
56

Skia's avatar
Skia committed
57
58
# Accounting classes

Krophil's avatar
Krophil committed
59

Skia's avatar
Skia committed
60
61
class Company(models.Model):
    name = models.CharField(_('name'), max_length=60)
62
63
64
65
66
67
68
    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():
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():
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
100
101
102
103
    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
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):
110
    name = models.CharField(_('name'), max_length=30)
111
    iban = models.CharField(_('iban'), max_length=255, blank=True)
112
    number = models.CharField(_('account number'), max_length=255, blank=True)
Skia's avatar
Skia committed
113
    club = models.ForeignKey(Club, related_name="bank_accounts", verbose_name=_("club"))
Skia's avatar
Skia committed
114

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

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

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

133
134
135
    def __str__(self):
        return self.name

Krophil's avatar
Krophil committed
136

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

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

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

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

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

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

184
185
186
    def __str__(self):
        return self.name

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

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

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

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

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

Skia's avatar
Skia committed
233
234
235
    def __str__(self):
        return self.name

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

Krophil's avatar
Krophil committed
250

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

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

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

287
288
    def clean(self):
        super(Operation, self).clean()
289
290
291
        if self.date is None:
            raise ValidationError(_("The date must be set."))
        elif self.date < self.journal.start_date:
292
293
            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
294
295
296
297
        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
298
299
300
301
302
303
304
305
        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
306
307
308
309
310
311
312
313
314
315
316
317

    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
318

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

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

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

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

Skia's avatar
Skia committed
359
360
361
362
363
364
365

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

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

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

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

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

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

Skia's avatar
Skia committed
393
394
395
396
397
398
399

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
400
                                        verbose_name=_("simplified accounting types"))
Skia's avatar
Skia committed
401
402
403

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

    @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
417
418
        return self.get_movement_type_display() + " - " + self.accounting_type.code + " - " + self.label

Skia's avatar
Skia committed
419

Skia's avatar
Skia committed
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
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)