models.py 19.8 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
7
from django.contrib.sites.shortcuts import get_current_site
Skia's avatar
Skia committed
8

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

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

21
22
23
24
25
26
27
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
28
    amount = CurrencyField(_('amount'))
29
30
31
32

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

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

Skia's avatar
Skia committed
38
39
40
41
42
43
    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
44

Skia's avatar
Skia committed
45
46
47
48
49
    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
50
51
52
53
54
55
56
57
    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()

58
59
60
61
    def get_absolute_url(self):
        return reverse('core:user_account', kwargs={'user_id': self.user.pk})

    def get_full_url(self):
Sli's avatar
Sli committed
62
        return ''.join(['https://', settings.SITH_URL, self.get_absolute_url()])
63
64


65
66
67
68
69
70
71
72
73
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
74
75
76
    class Meta:
        verbose_name = _('product type')

77
78
79
80
81
82
83
84
85
86
87
    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
88
89
90
    def get_absolute_url(self):
        return reverse('counter:producttype_list')

91
92
93
94
class Product(models.Model):
    """
    This describes a product, with all its related informations
    """
95
    name = models.CharField(_('name'), max_length=64)
96
    description = models.TextField(_('description'), blank=True)
97
98
    product_type = models.ForeignKey(ProductType, related_name='products', verbose_name=_("product type"), null=True, blank=True,
            on_delete=models.SET_NULL)
99
    code = models.CharField(_('code'), max_length=16, blank=True)
100
101
102
    purchase_price = CurrencyField(_('purchase price'))
    selling_price = CurrencyField(_('selling price'))
    special_selling_price = CurrencyField(_('special selling price'))
103
104
    icon = models.ImageField(upload_to='products', null=True, blank=True, verbose_name=_("icon"))
    club = models.ForeignKey(Club, related_name="products", verbose_name=_("club"))
105
106
    limit_age = models.IntegerField(_('limit age'), default=0)
    tray = models.BooleanField(_('tray price'), default=False)
107
108
    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
109
    buying_groups = models.ManyToManyField(Group, related_name='products', verbose_name=_("buying groups"), blank=True)
Skia's avatar
Skia committed
110
    archived = models.BooleanField(_("archived"), default=False)
111

Skia's avatar
Skia committed
112
113
114
    class Meta:
        verbose_name = _('product')

115
    def is_owned_by(self, user):
116
117
118
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
119
        if user.is_in_group(settings.SITH_GROUPS['accounting-admin']['name']) or user.is_in_group(settings.SITH_GROUPS['counter-admin']['name']):
120
121
122
123
            return True
        return False

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

Skia's avatar
Skia committed
126
127
128
    def get_absolute_url(self):
        return reverse('counter:product_list')

Skia's avatar
Skia committed
129
130
class Counter(models.Model):
    name = models.CharField(_('name'), max_length=30)
131
132
    club = models.ForeignKey(Club, related_name="counters", verbose_name=_("club"))
    products = models.ManyToManyField(Product, related_name="counters", verbose_name=_("products"), blank=True)
133
    type = models.CharField(_('counter type'),
Skia's avatar
Skia committed
134
            max_length=255,
Skia's avatar
Skia committed
135
            choices=[('BAR',_('Bar')), ('OFFICE',_('Office')), ('EBOUTIC',_('Eboutic'))])
136
    sellers = models.ManyToManyField(Subscriber, verbose_name=_('sellers'), related_name='counters', blank=True)
Skia's avatar
Skia committed
137
138
    edit_groups = models.ManyToManyField(Group, related_name="editable_counters", blank=True)
    view_groups = models.ManyToManyField(Group, related_name="viewable_counters", blank=True)
139
    token = models.CharField(_('token'), max_length=30, null=True, blank=True)
Skia's avatar
Skia committed
140

Skia's avatar
Skia committed
141
142
143
    class Meta:
        verbose_name = _('counter')

Skia's avatar
Skia committed
144
    def __getattribute__(self, name):
145
146
        if name == "edit_groups":
            return Group.objects.filter(name=self.club.unix_name+settings.SITH_BOARD_SUFFIX).all()
Skia's avatar
Skia committed
147
148
149
150
        return object.__getattribute__(self, name)

    def __str__(self):
        return self.name
Skia's avatar
Skia committed
151
152

    def get_absolute_url(self):
Skia's avatar
Skia committed
153
154
        if self.type == "EBOUTIC":
            return reverse('eboutic:main')
Skia's avatar
Skia committed
155
156
        return reverse('counter:details', kwargs={'counter_id': self.id})

157
    def is_owned_by(self, user):
158
159
160
        mem = self.club.get_membership_for(user)
        if mem and mem.role >= 7:
            return True
161
162
        return user.is_in_group(settings.SITH_GROUPS['counter-admin']['name'])

Skia's avatar
Skia committed
163
    def can_be_viewed_by(self, user):
164
165
166
167
        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
168

169
170
171
172
173
    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()

174
    def add_barman(self, user):
Skia's avatar
Skia committed
175
176
177
178
        """
        Logs a barman in to the given counter
        A user is stored as a tuple with its login time
        """
179
180
181
        Permanency(user=user, counter=self, start=timezone.now(), end=None).save()

    def del_barman(self, user):
Skia's avatar
Skia committed
182
183
184
        """
        Logs a barman out and store its permanency
        """
185
186
187
188
        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
189

Skia's avatar
Skia committed
190
    def get_barmen_list(self):
Skia's avatar
Skia committed
191
        """
Skia's avatar
Skia committed
192
        Returns the barman list as list of User
Skia's avatar
Skia committed
193
194
195

        Also handle the timeout of the barmen
        """
196
        pl = Permanency.objects.filter(counter=self, end=None).all()
197
        bl = []
198
199
200
        for p in pl:
            if timezone.now() - p.activity < timedelta(minutes=settings.SITH_BARMAN_TIMEOUT):
                bl.append(p.user)
201
            else:
202
203
                p.end = p.activity
                p.save()
204
205
        return bl

Skia's avatar
Skia committed
206
    def get_random_barman(self):
Skia's avatar
Skia committed
207
208
209
        """
        Return a random user being currently a barman
        """
Skia's avatar
Skia committed
210
        bl = self.get_barmen_list()
Skia's avatar
Skia committed
211
        return bl[random.randrange(0, len(bl))]
Skia's avatar
Skia committed
212

Skia's avatar
Skia committed
213
214
215
216
217
218
219
    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
220
    def is_open(self):
Skia's avatar
Skia committed
221
        return len(self.get_barmen_list()) > 0
Sli's avatar
Sli committed
222

Skia's avatar
Skia committed
223
    def barman_list(self):
Skia's avatar
Skia committed
224
225
226
        """
        Returns the barman id list
        """
Skia's avatar
Skia committed
227
228
        return [b.id for b in self.get_barmen_list()]

Skia's avatar
Skia committed
229
230
231
232
233
234
class Refilling(models.Model):
    """
    Handle the refilling
    """
    counter = models.ForeignKey(Counter, related_name="refillings", blank=False)
    amount = CurrencyField(_('amount'))
235
236
    operator = models.ForeignKey(User, related_name="refillings_as_operator", blank=False)
    customer = models.ForeignKey(Customer, related_name="refillings", blank=False)
237
    date = models.DateTimeField(_('date'))
Skia's avatar
Skia committed
238
    payment_method = models.CharField(_('payment method'), max_length=255,
239
            choices=settings.SITH_COUNTER_PAYMENT_METHOD, default='CASH')
240
    bank = models.CharField(_('bank'), max_length=255,
241
            choices=settings.SITH_COUNTER_BANK, default='OTHER')
Skia's avatar
Skia committed
242
    is_validated = models.BooleanField(_('is validated'), default=False)
Skia's avatar
Skia committed
243

Skia's avatar
Skia committed
244
245
246
    class Meta:
        verbose_name = _("refilling")

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

Skia's avatar
Skia committed
250
    def is_owned_by(self, user):
251
        return user.is_owner(self.counter) and self.payment_method != "CARD"
Skia's avatar
Skia committed
252

Skia's avatar
Skia committed
253
254
255
256
257
    def delete(self, *args, **kwargs):
        self.customer.amount -= self.amount
        self.customer.save()
        super(Refilling, self).delete(*args, **kwargs)

Skia's avatar
Skia committed
258
    def save(self, *args, **kwargs):
259
        if not self.date:
Skia's avatar
Skia committed
260
            self.date = timezone.now()
Skia's avatar
Skia committed
261
        self.full_clean()
Skia's avatar
Skia committed
262
263
264
265
        if not self.is_validated:
            self.customer.amount += self.amount
            self.customer.save()
            self.is_validated = True
Skia's avatar
Skia committed
266
        super(Refilling, self).save(*args, **kwargs)
Skia's avatar
Skia committed
267
268
269
270
271

class Selling(models.Model):
    """
    Handle the sellings
    """
272
    label = models.CharField(_("label"), max_length=64)
273
274
275
    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
276
277
    unit_price = CurrencyField(_('unit price'))
    quantity = models.IntegerField(_('quantity'))
278
279
280
281
282
    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
283
    is_validated = models.BooleanField(_('is validated'), default=False)
Skia's avatar
Skia committed
284

Skia's avatar
Skia committed
285
286
287
    class Meta:
        verbose_name = _("selling")

Skia's avatar
Skia committed
288
    def __str__(self):
Skia's avatar
Skia committed
289
        return "Selling: %d x %s (%f) for %s" % (self.quantity, self.label,
Skia's avatar
Skia committed
290
291
                self.quantity*self.unit_price, self.customer.user.get_display_name())

Skia's avatar
Skia committed
292
    def is_owned_by(self, user):
293
        return user.is_owner(self.counter) and self.payment_method != "CARD"
Skia's avatar
Skia committed
294

Skia's avatar
Skia committed
295
296
297
    def can_be_viewed_by(self, user):
        return user == self.customer.user

Skia's avatar
Skia committed
298
299
300
301
302
    def delete(self, *args, **kwargs):
        self.customer.amount += self.quantity * self.unit_price
        self.customer.save()
        super(Selling, self).delete(*args, **kwargs)

303
    def send_mail_customer(self):
Sli's avatar
Sli committed
304
        event = self.product.eticket.event_title or _("Unknown event")
Sli's avatar
Sli committed
305
        subject = _('Eticket bought for the event %(event)s') % {'event': event}
306
307
308
        message_html = _(
            "You bought an eticket for the event %(event)s.\nYou can download it on this page %(url)s."
        ) % {
Sli's avatar
Sli committed
309
            'event': event,
310
311
312
313
314
315
316
317
318
319
320
            'url':''.join((
                    '<a href="',
                    self.customer.get_full_url(),
                    '">',
                    self.customer.get_full_url(),
                    '</a>'
                ))
        }
        message_txt = _(
            "You bought an eticket for the event %(event)s.\nYou can download it on this page %(url)s."
        ) % {
Sli's avatar
Sli committed
321
            'event': event,
322
323
324
325
326
327
328
329
            'url': self.customer.get_full_url(),
        }
        self.customer.user.email_user(
            subject,
            message_txt,
            html_message=message_html 
        )

Skia's avatar
Skia committed
330
    def save(self, *args, **kwargs):
331
        if not self.date:
Skia's avatar
Skia committed
332
            self.date = timezone.now()
Skia's avatar
Skia committed
333
        self.full_clean()
Skia's avatar
Skia committed
334
335
336
337
        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
338
        if self.product and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER:
Skia's avatar
Skia committed
339
340
341
342
343
344
345
346
347
348
349
350
351
352
            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
353
        elif self.product and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS:
Skia's avatar
Skia committed
354
355
356
357
358
359
360
361
362
363
364
365
366
367
            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()
368
369
370
        try:
            if self.product.eticket:
                self.send_mail_customer()
Sli's avatar
Sli committed
371
        except: pass
Skia's avatar
Skia committed
372
373
        super(Selling, self).save(*args, **kwargs)

Skia's avatar
Skia committed
374
375
376
377
class Permanency(models.Model):
    """
    This class aims at storing a traceability of who was barman where and when
    """
378
379
    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
380
    start = models.DateTimeField(_('start date'))
381
382
    end = models.DateTimeField(_('end date'), null=True)
    activity = models.DateTimeField(_('last activity date'), auto_now=True)
Skia's avatar
Skia committed
383

Skia's avatar
Skia committed
384
385
386
    class Meta:
        verbose_name = _("permanency")

Skia's avatar
Skia committed
387
    def __str__(self):
Skia's avatar
Skia committed
388
389
390
391
392
        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
393

Skia's avatar
Skia committed
394
395
396
397
398
399
400
401
402
403
404
405
406
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())

407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
    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
443
444
445
446
447
448
449
450
    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
451
452
453
454
455
456
457
458
459
460
461
    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)

462
463
464
    def get_absolute_url(self):
        return reverse('counter:cash_summary_list')

Skia's avatar
Skia committed
465
466
467
468
469
470
471
472
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")
473

Skia's avatar
Skia committed
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
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()