models.py 18.5 KB
Newer Older
Skia's avatar
Skia committed
1
from django.db import models, DataError
Skia's avatar
Skia committed
2
from django.utils.translation import ugettext_lazy as _
3
from django.utils import timezone
Skia's avatar
Skia committed
4
from django.conf import settings
Skia's avatar
Skia committed
5
from django.core.urlresolvers import reverse
Skia's avatar
Skia committed
6
from django.forms import ValidationError
Skia's avatar
Skia committed
7

Skia's avatar
Skia committed
8
from datetime import timedelta
Skia's avatar
Skia committed
9
10
import random
import string
Skia's avatar
Skia committed
11
12
import os
import base64
13

Skia's avatar
Skia committed
14
from club.models import Club
15
16
from accounting.models import CurrencyField
from core.models import Group, User
Skia's avatar
Skia committed
17
from subscription.models import Subscriber, Subscription
18
from subscription.views import get_subscriber
Skia's avatar
Skia committed
19

20
21
22
23
24
25
26
class Customer(models.Model):
    """
    This class extends a user to make a customer. It adds some basic customers informations, such as the accound ID, and
    is used by other accounting classes as reference to the customer, rather than using User
    """
    user = models.OneToOneField(User, primary_key=True)
    account_id = models.CharField(_('account id'), max_length=10, unique=True)
Skia's avatar
Skia committed
27
    amount = CurrencyField(_('amount'))
28
29
30
31

    class Meta:
        verbose_name = _('customer')
        verbose_name_plural = _('customers')
Skia's avatar
Skia committed
32
        ordering = ['account_id',]
33
34

    def __str__(self):
Skia's avatar
Skia committed
35
        return "%s - %s" % (self.user.username, self.account_id)
36

Skia's avatar
Skia committed
37
38
39
40
41
42
    def generate_account_id(number):
        number = str(number)
        letter = random.choice(string.ascii_lowercase)
        while Customer.objects.filter(account_id=number+letter).exists():
            letter = random.choice(string.ascii_lowercase)
        return number+letter
43

Skia's avatar
Skia committed
44
45
46
47
48
    def save(self, *args, **kwargs):
        if self.amount < 0:
            raise ValidationError(_("Not enough money"))
        super(Customer, self).save(*args, **kwargs)

Skia's avatar
Skia committed
49
50
51
52
53
54
55
56
    def recompute_amount(self):
        self.amount = 0
        for r in self.refillings.all():
            self.amount += r.amount
        for s in self.buyings.filter(payment_method="SITH_ACCOUNT"):
            self.amount -= s.quantity * s.unit_price
            self.save()

57
58
59
60
61
62
63
64
65
class ProductType(models.Model):
    """
    This describes a product type
    Useful only for categorizing, changes are made at the product level for now
    """
    name = models.CharField(_('name'), max_length=30)
    description = models.TextField(_('description'), null=True, blank=True)
    icon = models.ImageField(upload_to='products', null=True, blank=True)

Skia's avatar
Skia committed
66
67
68
    class Meta:
        verbose_name = _('product type')

69
70
71
72
73
74
75
76
77
78
79
    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_GROUPS['accounting-admin']['name']):
            return True
        return False

    def __str__(self):
        return self.name

Skia's avatar
Skia committed
80
81
82
    def get_absolute_url(self):
        return reverse('counter:producttype_list')

83
84
85
86
class Product(models.Model):
    """
    This describes a product, with all its related informations
    """
87
    name = models.CharField(_('name'), max_length=64)
88
    description = models.TextField(_('description'), blank=True)
89
90
    product_type = models.ForeignKey(ProductType, related_name='products', verbose_name=_("product type"), null=True, blank=True,
            on_delete=models.SET_NULL)
91
    code = models.CharField(_('code'), max_length=16, blank=True)
92
93
94
    purchase_price = CurrencyField(_('purchase price'))
    selling_price = CurrencyField(_('selling price'))
    special_selling_price = CurrencyField(_('special selling price'))
95
96
    icon = models.ImageField(upload_to='products', null=True, blank=True, verbose_name=_("icon"))
    club = models.ForeignKey(Club, related_name="products", verbose_name=_("club"))
97
98
    limit_age = models.IntegerField(_('limit age'), default=0)
    tray = models.BooleanField(_('tray price'), default=False)
99
100
    parent_product = models.ForeignKey('self', related_name='children_products', verbose_name=_("parent product"), null=True,
            blank=True, on_delete=models.SET_NULL)
Skia's avatar
Skia committed
101
    buying_groups = models.ManyToManyField(Group, related_name='products', verbose_name=_("buying groups"), blank=True)
Skia's avatar
Skia committed
102
    archived = models.BooleanField(_("archived"), default=False)
103

Skia's avatar
Skia committed
104
105
106
    class Meta:
        verbose_name = _('product')

107
    def is_owned_by(self, user):
108
109
110
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
111
        if user.is_in_group(settings.SITH_GROUPS['accounting-admin']['name']) or user.is_in_group(settings.SITH_GROUPS['counter-admin']['name']):
112
113
114
115
            return True
        return False

    def __str__(self):
Skia's avatar
Skia committed
116
        return "%s (%s)" % (self.name, self.code)
117

Skia's avatar
Skia committed
118
119
120
    def get_absolute_url(self):
        return reverse('counter:product_list')

Skia's avatar
Skia committed
121
122
class Counter(models.Model):
    name = models.CharField(_('name'), max_length=30)
123
124
    club = models.ForeignKey(Club, related_name="counters", verbose_name=_("club"))
    products = models.ManyToManyField(Product, related_name="counters", verbose_name=_("products"), blank=True)
125
    type = models.CharField(_('counter type'),
Skia's avatar
Skia committed
126
            max_length=255,
Skia's avatar
Skia committed
127
            choices=[('BAR',_('Bar')), ('OFFICE',_('Office')), ('EBOUTIC',_('Eboutic'))])
128
    sellers = models.ManyToManyField(Subscriber, verbose_name=_('sellers'), related_name='counters', blank=True)
Skia's avatar
Skia committed
129
130
    edit_groups = models.ManyToManyField(Group, related_name="editable_counters", blank=True)
    view_groups = models.ManyToManyField(Group, related_name="viewable_counters", blank=True)
131
    token = models.CharField(_('token'), max_length=30, null=True, blank=True)
Skia's avatar
Skia committed
132

Skia's avatar
Skia committed
133
134
135
    class Meta:
        verbose_name = _('counter')

Skia's avatar
Skia committed
136
    def __getattribute__(self, name):
137
138
        if name == "edit_groups":
            return Group.objects.filter(name=self.club.unix_name+settings.SITH_BOARD_SUFFIX).all()
Skia's avatar
Skia committed
139
140
141
142
        return object.__getattribute__(self, name)

    def __str__(self):
        return self.name
Skia's avatar
Skia committed
143
144

    def get_absolute_url(self):
Skia's avatar
Skia committed
145
146
        if self.type == "EBOUTIC":
            return reverse('eboutic:main')
Skia's avatar
Skia committed
147
148
        return reverse('counter:details', kwargs={'counter_id': self.id})

149
    def is_owned_by(self, user):
150
151
152
        mem = self.club.get_membership_for(user)
        if mem and mem.role >= 7:
            return True
153
154
        return user.is_in_group(settings.SITH_GROUPS['counter-admin']['name'])

Skia's avatar
Skia committed
155
    def can_be_viewed_by(self, user):
156
157
158
159
        if self.type == "BAR" or self.type == "EBOUTIC":
            return True
        sub = get_subscriber(request.user)
        return user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) or sub in self.sellers
160

161
162
163
164
165
    def gen_token(self):
        """Generate a new token for this counter"""
        self.token = ''.join(random.choice(string.ascii_letters + string.digits) for x in range(30))
        self.save()

166
    def add_barman(self, user):
Skia's avatar
Skia committed
167
168
169
170
        """
        Logs a barman in to the given counter
        A user is stored as a tuple with its login time
        """
171
172
173
        Permanency(user=user, counter=self, start=timezone.now(), end=None).save()

    def del_barman(self, user):
Skia's avatar
Skia committed
174
175
176
        """
        Logs a barman out and store its permanency
        """
177
178
179
180
        perm = Permanency.objects.filter(counter=self, user=user, end=None).all()
        for p in perm:
            p.end = p.activity
            p.save()
Skia's avatar
Skia committed
181

Skia's avatar
Skia committed
182
    def get_barmen_list(self):
Skia's avatar
Skia committed
183
        """
Skia's avatar
Skia committed
184
        Returns the barman list as list of User
Skia's avatar
Skia committed
185
186
187

        Also handle the timeout of the barmen
        """
188
        pl = Permanency.objects.filter(counter=self, end=None).all()
189
        bl = []
190
191
192
        for p in pl:
            if timezone.now() - p.activity < timedelta(minutes=settings.SITH_BARMAN_TIMEOUT):
                bl.append(p.user)
193
            else:
194
195
                p.end = p.activity
                p.save()
196
197
        return bl

Skia's avatar
Skia committed
198
    def get_random_barman(self):
Skia's avatar
Skia committed
199
200
201
        """
        Return a random user being currently a barman
        """
Skia's avatar
Skia committed
202
        bl = self.get_barmen_list()
Skia's avatar
Skia committed
203
        return bl[random.randrange(0, len(bl))]
Skia's avatar
Skia committed
204

Skia's avatar
Skia committed
205
206
207
208
209
210
211
    def update_activity(self):
        """
        Update the barman activity to prevent timeout
        """
        for p in Permanency.objects.filter(counter=self, end=None).all():
            p.save() # Update activity

Sli's avatar
Sli committed
212
    def is_open(self):
Skia's avatar
Skia committed
213
        return len(self.get_barmen_list()) > 0
Sli's avatar
Sli committed
214

Skia's avatar
Skia committed
215
    def barman_list(self):
Skia's avatar
Skia committed
216
217
218
        """
        Returns the barman id list
        """
Skia's avatar
Skia committed
219
220
        return [b.id for b in self.get_barmen_list()]

Skia's avatar
Skia committed
221
222
223
224
225
226
class Refilling(models.Model):
    """
    Handle the refilling
    """
    counter = models.ForeignKey(Counter, related_name="refillings", blank=False)
    amount = CurrencyField(_('amount'))
227
228
    operator = models.ForeignKey(User, related_name="refillings_as_operator", blank=False)
    customer = models.ForeignKey(Customer, related_name="refillings", blank=False)
229
    date = models.DateTimeField(_('date'))
Skia's avatar
Skia committed
230
    payment_method = models.CharField(_('payment method'), max_length=255,
231
            choices=settings.SITH_COUNTER_PAYMENT_METHOD, default='CASH')
232
    bank = models.CharField(_('bank'), max_length=255,
233
            choices=settings.SITH_COUNTER_BANK, default='OTHER')
Skia's avatar
Skia committed
234
    is_validated = models.BooleanField(_('is validated'), default=False)
Skia's avatar
Skia committed
235

Skia's avatar
Skia committed
236
237
238
    class Meta:
        verbose_name = _("refilling")

Skia's avatar
Skia committed
239
    def __str__(self):
Skia's avatar
Skia committed
240
        return "Refilling: %.2f for %s" % (self.amount, self.customer.user.get_display_name())
Skia's avatar
Skia committed
241

Skia's avatar
Skia committed
242
    def is_owned_by(self, user):
243
        return user.is_owner(self.counter) and self.payment_method != "CARD"
Skia's avatar
Skia committed
244

Skia's avatar
Skia committed
245
246
247
248
249
    def delete(self, *args, **kwargs):
        self.customer.amount -= self.amount
        self.customer.save()
        super(Refilling, self).delete(*args, **kwargs)

Skia's avatar
Skia committed
250
    def save(self, *args, **kwargs):
251
        if not self.date:
Skia's avatar
Skia committed
252
            self.date = timezone.now()
Skia's avatar
Skia committed
253
        self.full_clean()
Skia's avatar
Skia committed
254
255
256
257
        if not self.is_validated:
            self.customer.amount += self.amount
            self.customer.save()
            self.is_validated = True
Skia's avatar
Skia committed
258
        super(Refilling, self).save(*args, **kwargs)
Skia's avatar
Skia committed
259
260
261
262
263

class Selling(models.Model):
    """
    Handle the sellings
    """
264
    label = models.CharField(_("label"), max_length=64)
265
266
267
    product = models.ForeignKey(Product, related_name="sellings", null=True, blank=True, on_delete=models.SET_NULL)
    counter = models.ForeignKey(Counter, related_name="sellings", null=True, blank=False, on_delete=models.SET_NULL)
    club = models.ForeignKey(Club, related_name="sellings", null=True, blank=False, on_delete=models.SET_NULL)
Skia's avatar
Skia committed
268
269
    unit_price = CurrencyField(_('unit price'))
    quantity = models.IntegerField(_('quantity'))
270
271
272
273
274
    seller = models.ForeignKey(User, related_name="sellings_as_operator", null=True, blank=False, on_delete=models.SET_NULL)
    customer = models.ForeignKey(Customer, related_name="buyings", null=True, blank=False, on_delete=models.SET_NULL)
    date = models.DateTimeField(_('date'))
    payment_method = models.CharField(_('payment method'), max_length=255,
            choices=[('SITH_ACCOUNT', _('Sith account')), ('CARD', _('Credit card'))], default='SITH_ACCOUNT')
Skia's avatar
Skia committed
275
    is_validated = models.BooleanField(_('is validated'), default=False)
Skia's avatar
Skia committed
276

Skia's avatar
Skia committed
277
278
279
    class Meta:
        verbose_name = _("selling")

Skia's avatar
Skia committed
280
    def __str__(self):
Skia's avatar
Skia committed
281
        return "Selling: %d x %s (%f) for %s" % (self.quantity, self.label,
Skia's avatar
Skia committed
282
283
                self.quantity*self.unit_price, self.customer.user.get_display_name())

Skia's avatar
Skia committed
284
    def is_owned_by(self, user):
285
        return user.is_owner(self.counter) and self.payment_method != "CARD"
Skia's avatar
Skia committed
286

Skia's avatar
Skia committed
287
288
289
    def can_be_viewed_by(self, user):
        return user == self.customer.user

Skia's avatar
Skia committed
290
291
292
293
294
    def delete(self, *args, **kwargs):
        self.customer.amount += self.quantity * self.unit_price
        self.customer.save()
        super(Selling, self).delete(*args, **kwargs)

Skia's avatar
Skia committed
295
    def save(self, *args, **kwargs):
296
        if not self.date:
Skia's avatar
Skia committed
297
            self.date = timezone.now()
Skia's avatar
Skia committed
298
        self.full_clean()
Skia's avatar
Skia committed
299
300
301
302
        if not self.is_validated:
            self.customer.amount -= self.quantity * self.unit_price
            self.customer.save()
            self.is_validated = True
Skia's avatar
Skia committed
303
        if self.product and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER:
Skia's avatar
Skia committed
304
305
306
307
308
309
310
311
312
313
314
315
316
317
            s = Subscriber.objects.filter(id=self.customer.user.id).first()
            sub = Subscription(
                    member=s,
                    subscription_type='un-semestre',
                    payment_method="EBOUTIC",
                    location="EBOUTIC",
                    )
            sub.subscription_start = Subscription.compute_start()
            sub.subscription_start = Subscription.compute_start(
                duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type]['duration'])
            sub.subscription_end = Subscription.compute_end(
                    duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type]['duration'],
                    start=sub.subscription_start)
            sub.save()
Skia's avatar
Skia committed
318
        elif self.product and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS:
Skia's avatar
Skia committed
319
320
321
322
323
324
325
326
327
328
329
330
331
332
            s = Subscriber.objects.filter(id=self.customer.user.id).first()
            sub = Subscription(
                    member=s,
                    subscription_type='deux-semestres',
                    payment_method="EBOUTIC",
                    location="EBOUTIC",
                    )
            sub.subscription_start = Subscription.compute_start()
            sub.subscription_start = Subscription.compute_start(
                duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type]['duration'])
            sub.subscription_end = Subscription.compute_end(
                    duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type]['duration'],
                    start=sub.subscription_start)
            sub.save()
Skia's avatar
Skia committed
333
334
        super(Selling, self).save(*args, **kwargs)

Skia's avatar
Skia committed
335
336
337
338
class Permanency(models.Model):
    """
    This class aims at storing a traceability of who was barman where and when
    """
339
340
    user = models.ForeignKey(User, related_name="permanencies", verbose_name=_("user"))
    counter = models.ForeignKey(Counter, related_name="permanencies", verbose_name=_("counter"))
Skia's avatar
Skia committed
341
    start = models.DateTimeField(_('start date'))
342
343
    end = models.DateTimeField(_('end date'), null=True)
    activity = models.DateTimeField(_('last activity date'), auto_now=True)
Skia's avatar
Skia committed
344

Skia's avatar
Skia committed
345
346
347
    class Meta:
        verbose_name = _("permanency")

Skia's avatar
Skia committed
348
    def __str__(self):
Skia's avatar
Skia committed
349
350
351
352
353
        return "%s in %s from %s (last activity: %s) to %s" % (self.user, self.counter,
                self.start.strftime("%Y-%m-%d %H:%M:%S"),
                self.activity.strftime("%Y-%m-%d %H:%M:%S"),
                self.end.strftime("%Y-%m-%d %H:%M:%S") if self.end else "",
                )
Skia's avatar
Skia committed
354

Skia's avatar
Skia committed
355
356
357
358
359
360
361
362
363
364
365
366
367
class CashRegisterSummary(models.Model):
    user = models.ForeignKey(User, related_name="cash_summaries", verbose_name=_("user"))
    counter = models.ForeignKey(Counter, related_name="cash_summaries", verbose_name=_("counter"))
    date = models.DateTimeField(_('date'))
    comment = models.TextField(_('comment'), null=True, blank=True)
    emptied = models.BooleanField(_('emptied'), default=False)

    class Meta:
        verbose_name = _("cash register summary")

    def __str__(self):
        return "At %s by %s - Total: %s €" % (self.counter, self.user, self.get_total())

368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
    def __getattribute__(self, name):
        if name[:5] == 'check':
            checks = self.items.filter(check=True).order_by('value').all()
        if name == 'ten_cents':
            return self.items.filter(value=0.1, check=False).first()
        elif name == 'twenty_cents':
            return self.items.filter(value=0.2, check=False).first()
        elif name == 'fifty_cents':
            return self.items.filter(value=0.5, check=False).first()
        elif name == 'one_euro':
            return self.items.filter(value=1, check=False).first()
        elif name == 'two_euros':
            return self.items.filter(value=2, check=False).first()
        elif name == 'five_euros':
            return self.items.filter(value=5, check=False).first()
        elif name == 'ten_euros':
            return self.items.filter(value=10, check=False).first()
        elif name == 'twenty_euros':
            return self.items.filter(value=20, check=False).first()
        elif name == 'fifty_euros':
            return self.items.filter(value=50, check=False).first()
        elif name == 'hundred_euros':
            return self.items.filter(value=100, check=False).first()
        elif name == 'check_1':
            return checks[0] if 0 < len(checks) else None
        elif name == 'check_2':
            return checks[1] if 1 < len(checks) else None
        elif name == 'check_3':
            return checks[2] if 2 < len(checks) else None
        elif name == 'check_4':
            return checks[3] if 3 < len(checks) else None
        elif name == 'check_5':
            return checks[4] if 4 < len(checks) else None
        else:
            return object.__getattribute__(self, name)

Skia's avatar
Skia committed
404
405
406
407
408
409
410
411
    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_GROUPS['counter-admin']['name']):
            return True
        return False

Skia's avatar
Skia committed
412
413
414
415
416
417
418
419
420
421
422
    def get_total(self):
        t = 0
        for it in self.items.all():
            t += it.quantity * it.value
        return t

    def save(self, *args, **kwargs):
        if not self.id:
            self.date = timezone.now()
        return super(CashRegisterSummary, self).save(*args, **kwargs)

423
424
425
    def get_absolute_url(self):
        return reverse('counter:cash_summary_list')

Skia's avatar
Skia committed
426
427
428
429
430
431
432
433
class CashRegisterSummaryItem(models.Model):
    cash_summary = models.ForeignKey(CashRegisterSummary, related_name="items", verbose_name=_("cash summary"))
    value = CurrencyField(_("value"))
    quantity = models.IntegerField(_('quantity'), default=0)
    check = models.BooleanField(_('check'), default=False)

    class Meta:
        verbose_name = _("cash register summary item")
434

Skia's avatar
Skia committed
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
class Eticket(models.Model):
    """
    Eticket can be linked to a product an allows PDF generation
    """
    product = models.OneToOneField(Product, related_name='eticket', verbose_name=_("product"))
    banner = models.ImageField(upload_to='etickets', null=True, blank=True, verbose_name=_("banner"))
    event_date = models.DateField(_('event date'), null=True, blank=True)
    event_title = models.CharField(_('event title'), max_length=64, null=True, blank=True)
    secret = models.CharField(_('secret'), max_length=64, unique=True)

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

    def get_absolute_url(self):
        return reverse('counter:eticket_list')

    def save(self, *args, **kwargs):
        if not self.id:
            self.secret = base64.b64encode(os.urandom(32))
        return super(Eticket, self).save(*args, **kwargs)

    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
        return user.is_in_group(settings.SITH_GROUPS['counter-admin']['name'])

    def get_hash(self, string):
        import hashlib, hmac
        return hmac.new(bytes(self.secret, 'utf-8'), bytes(string, 'utf-8'), hashlib.sha1).hexdigest()