models.py 20.2 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
Lo-J's avatar
Lo-J committed
14
import datetime
15

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

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

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

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

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

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

59 60 61 62
    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
63
        return ''.join(['https://', settings.SITH_URL, self.get_absolute_url()])
64 65


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Lo-J's avatar
Lo-J committed
224 225
    def is_inactive(self):
        """
226
        Returns True if the counter self is inactive from 10 minutes, else False 
Lo-J's avatar
Lo-J committed
227 228
        """
        if (self.is_open()):
229
            return ((timezone.now() - self.permanencies.model.objects.order_by('-activity').first().activity) > datetime.timedelta(minutes=10))
Lo-J's avatar
Lo-J committed
230 231 232
        else:
            return False

Skia's avatar
Skia committed
233
    def barman_list(self):
Skia's avatar
Skia committed
234 235 236
        """
        Returns the barman id list
        """
Skia's avatar
Skia committed
237 238
        return [b.id for b in self.get_barmen_list()]

Skia's avatar
Skia committed
239 240 241 242 243 244
class Refilling(models.Model):
    """
    Handle the refilling
    """
    counter = models.ForeignKey(Counter, related_name="refillings", blank=False)
    amount = CurrencyField(_('amount'))
245 246
    operator = models.ForeignKey(User, related_name="refillings_as_operator", blank=False)
    customer = models.ForeignKey(Customer, related_name="refillings", blank=False)
247
    date = models.DateTimeField(_('date'))
Skia's avatar
Skia committed
248
    payment_method = models.CharField(_('payment method'), max_length=255,
249
            choices=settings.SITH_COUNTER_PAYMENT_METHOD, default='CASH')
250
    bank = models.CharField(_('bank'), max_length=255,
251
            choices=settings.SITH_COUNTER_BANK, default='OTHER')
Skia's avatar
Skia committed
252
    is_validated = models.BooleanField(_('is validated'), default=False)
Skia's avatar
Skia committed
253

Skia's avatar
Skia committed
254 255 256
    class Meta:
        verbose_name = _("refilling")

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

Skia's avatar
Skia committed
260
    def is_owned_by(self, user):
261
        return user.is_owner(self.counter) and self.payment_method != "CARD"
Skia's avatar
Skia committed
262

Skia's avatar
Skia committed
263 264 265 266 267
    def delete(self, *args, **kwargs):
        self.customer.amount -= self.amount
        self.customer.save()
        super(Refilling, self).delete(*args, **kwargs)

Skia's avatar
Skia committed
268
    def save(self, *args, **kwargs):
269
        if not self.date:
Skia's avatar
Skia committed
270
            self.date = timezone.now()
Skia's avatar
Skia committed
271
        self.full_clean()
Skia's avatar
Skia committed
272 273 274 275
        if not self.is_validated:
            self.customer.amount += self.amount
            self.customer.save()
            self.is_validated = True
Skia's avatar
Skia committed
276
        super(Refilling, self).save(*args, **kwargs)
Skia's avatar
Skia committed
277 278 279 280 281

class Selling(models.Model):
    """
    Handle the sellings
    """
282
    label = models.CharField(_("label"), max_length=64)
283 284 285
    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
286 287
    unit_price = CurrencyField(_('unit price'))
    quantity = models.IntegerField(_('quantity'))
288 289 290 291 292
    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
293
    is_validated = models.BooleanField(_('is validated'), default=False)
Skia's avatar
Skia committed
294

Skia's avatar
Skia committed
295 296 297
    class Meta:
        verbose_name = _("selling")

Skia's avatar
Skia committed
298
    def __str__(self):
Skia's avatar
Skia committed
299
        return "Selling: %d x %s (%f) for %s" % (self.quantity, self.label,
Skia's avatar
Skia committed
300 301
                self.quantity*self.unit_price, self.customer.user.get_display_name())

Skia's avatar
Skia committed
302
    def is_owned_by(self, user):
303
        return user.is_owner(self.counter) and self.payment_method != "CARD"
Skia's avatar
Skia committed
304

Skia's avatar
Skia committed
305 306 307
    def can_be_viewed_by(self, user):
        return user == self.customer.user

Skia's avatar
Skia committed
308 309 310 311 312
    def delete(self, *args, **kwargs):
        self.customer.amount += self.quantity * self.unit_price
        self.customer.save()
        super(Selling, self).delete(*args, **kwargs)

313
    def send_mail_customer(self):
Sli's avatar
Sli committed
314
        event = self.product.eticket.event_title or _("Unknown event")
Sli's avatar
Sli committed
315
        subject = _('Eticket bought for the event %(event)s') % {'event': event}
316 317 318
        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
319
            'event': event,
320 321 322 323 324 325 326 327 328 329 330
            '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
331
            'event': event,
332 333 334 335 336 337 338 339
            'url': self.customer.get_full_url(),
        }
        self.customer.user.email_user(
            subject,
            message_txt,
            html_message=message_html 
        )

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

Skia's avatar
Skia committed
384 385 386 387
class Permanency(models.Model):
    """
    This class aims at storing a traceability of who was barman where and when
    """
388 389
    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
390
    start = models.DateTimeField(_('start date'))
391 392
    end = models.DateTimeField(_('end date'), null=True)
    activity = models.DateTimeField(_('last activity date'), auto_now=True)
Skia's avatar
Skia committed
393

Skia's avatar
Skia committed
394 395 396
    class Meta:
        verbose_name = _("permanency")

Skia's avatar
Skia committed
397
    def __str__(self):
Skia's avatar
Skia committed
398 399 400 401 402
        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
403

Skia's avatar
Skia committed
404 405 406 407 408 409 410 411 412 413 414 415 416
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())

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 443 444 445 446 447 448 449 450 451 452
    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
453 454 455 456 457 458 459 460
    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
461 462 463 464 465 466 467 468 469 470 471
    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)

472 473 474
    def get_absolute_url(self):
        return reverse('counter:cash_summary_list')

Skia's avatar
Skia committed
475 476 477 478 479 480 481 482
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")
483

Skia's avatar
Skia committed
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
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()