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

Krophil's avatar
Krophil committed
25
from django.db import models
Skia's avatar
Skia committed
26
from django.utils.translation import ugettext_lazy as _
27
from django.utils import timezone
Skia's avatar
Skia committed
28
from django.conf import settings
Skia's avatar
Skia committed
29
from django.core.urlresolvers import reverse
Sli's avatar
Sli committed
30
from django.core.validators import MinLengthValidator
Skia's avatar
Skia committed
31
from django.forms import ValidationError
32
from django.utils.functional import cached_property
Sli's avatar
Sli committed
33
from django.core.exceptions import PermissionDenied
Skia's avatar
Skia committed
34

35
from datetime import timedelta, date
Skia's avatar
Skia committed
36
37
import random
import string
Skia's avatar
Skia committed
38
39
import os
import base64
40
import datetime
41

Skia's avatar
Skia committed
42
from club.models import Club
43
from accounting.models import CurrencyField
Skia's avatar
Skia committed
44
from core.models import Group, User, Notification
Skia's avatar
Skia committed
45
from subscription.models import Subscription
Skia's avatar
Skia committed
46

Krophil's avatar
Krophil committed
47

48
49
50
51
52
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
    """
Sli's avatar
Sli committed
53

54
    user = models.OneToOneField(User, primary_key=True)
Sli's avatar
Sli committed
55
56
57
    account_id = models.CharField(_("account id"), max_length=10, unique=True)
    amount = CurrencyField(_("amount"))
    recorded_products = models.IntegerField(_("recorded product"), default=0)
58
59

    class Meta:
Sli's avatar
Sli committed
60
61
62
        verbose_name = _("customer")
        verbose_name_plural = _("customers")
        ordering = ["account_id"]
63
64

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

Sli's avatar
Sli committed
67
68
    @property
    def can_record(self):
Sli's avatar
Sli committed
69
        return self.recorded_products > -settings.SITH_ECOCUP_LIMIT
Sli's avatar
Sli committed
70
71

    def can_record_more(self, number):
Sli's avatar
Sli committed
72
        return self.recorded_products - number >= -settings.SITH_ECOCUP_LIMIT
Sli's avatar
Sli committed
73

74
75
    @property
    def can_buy(self):
Sli's avatar
Sli committed
76
77
78
79
80
81
        return self.user.subscriptions.last() and (
            date.today()
            - self.user.subscriptions.order_by("subscription_end")
            .last()
            .subscription_end
        ) < timedelta(days=90)
82

Skia's avatar
Skia committed
83
84
85
    def generate_account_id(number):
        number = str(number)
        letter = random.choice(string.ascii_lowercase)
Krophil's avatar
Krophil committed
86
        while Customer.objects.filter(account_id=number + letter).exists():
Skia's avatar
Skia committed
87
            letter = random.choice(string.ascii_lowercase)
Krophil's avatar
Krophil committed
88
        return number + letter
89

Sli's avatar
Sli committed
90
    def save(self, allow_negative=False, is_selling=False, *args, **kwargs):
Sli's avatar
Sli committed
91
92
93
94
95
        """
            is_selling : tell if the current action is a selling
            allow_negative : ignored if not a selling. Allow a selling to put the account in negative
            Those two parameters avoid blocking the save method of a customer if his account is negative
        """
Sli's avatar
Sli committed
96
        if self.amount < 0 and (is_selling and not allow_negative):
Skia's avatar
Skia committed
97
98
99
            raise ValidationError(_("Not enough money"))
        super(Customer, self).save(*args, **kwargs)

Skia's avatar
Skia committed
100
101
102
103
104
105
106
107
    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()

108
    def get_absolute_url(self):
Sli's avatar
Sli committed
109
        return reverse("core:user_account", kwargs={"user_id": self.user.pk})
110
111

    def get_full_url(self):
Sli's avatar
Sli committed
112
        return "".join(["https://", settings.SITH_URL, self.get_absolute_url()])
113
114


115
116
117
118
119
class ProductType(models.Model):
    """
    This describes a product type
    Useful only for categorizing, changes are made at the product level for now
    """
Sli's avatar
Sli committed
120
121
122
123
124

    name = models.CharField(_("name"), max_length=30)
    description = models.TextField(_("description"), null=True, blank=True)
    comment = models.TextField(_("comment"), null=True, blank=True)
    icon = models.ImageField(upload_to="products", null=True, blank=True)
125

Skia's avatar
Skia committed
126
    class Meta:
Sli's avatar
Sli committed
127
        verbose_name = _("product type")
Skia's avatar
Skia committed
128

129
130
131
132
    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
133
        if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
134
135
136
137
138
139
            return True
        return False

    def __str__(self):
        return self.name

Skia's avatar
Skia committed
140
    def get_absolute_url(self):
Sli's avatar
Sli committed
141
        return reverse("counter:producttype_list")
Skia's avatar
Skia committed
142

Krophil's avatar
Krophil committed
143

144
145
146
147
class Product(models.Model):
    """
    This describes a product, with all its related informations
    """
Sli's avatar
Sli committed
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165

    name = models.CharField(_("name"), max_length=64)
    description = models.TextField(_("description"), blank=True)
    product_type = models.ForeignKey(
        ProductType,
        related_name="products",
        verbose_name=_("product type"),
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    code = models.CharField(_("code"), max_length=16, blank=True)
    purchase_price = CurrencyField(_("purchase price"))
    selling_price = CurrencyField(_("selling price"))
    special_selling_price = CurrencyField(_("special selling price"))
    icon = models.ImageField(
        upload_to="products", null=True, blank=True, verbose_name=_("icon")
    )
166
    club = models.ForeignKey(Club, related_name="products", verbose_name=_("club"))
Sli's avatar
Sli committed
167
168
169
170
171
172
173
174
175
176
177
178
179
    limit_age = models.IntegerField(_("limit age"), default=0)
    tray = models.BooleanField(_("tray price"), default=False)
    parent_product = models.ForeignKey(
        "self",
        related_name="children_products",
        verbose_name=_("parent product"),
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    buying_groups = models.ManyToManyField(
        Group, related_name="products", verbose_name=_("buying groups"), blank=True
    )
Skia's avatar
Skia committed
180
    archived = models.BooleanField(_("archived"), default=False)
181

Skia's avatar
Skia committed
182
    class Meta:
Sli's avatar
Sli committed
183
        verbose_name = _("product")
Skia's avatar
Skia committed
184

Sli's avatar
Sli committed
185
186
    @property
    def is_record_product(self):
Sli's avatar
Sli committed
187
        return settings.SITH_ECOCUP_CONS == self.id
Sli's avatar
Sli committed
188
189
190

    @property
    def is_unrecord_product(self):
Sli's avatar
Sli committed
191
        return settings.SITH_ECOCUP_DECO == self.id
Sli's avatar
Sli committed
192

193
    def is_owned_by(self, user):
194
195
196
        """
        Method to see if that object can be edited by the given user
        """
Sli's avatar
Sli committed
197
198
199
        if user.is_in_group(
            settings.SITH_GROUP_ACCOUNTING_ADMIN_ID
        ) or user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID):
200
201
202
203
            return True
        return False

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

Skia's avatar
Skia committed
206
    def get_absolute_url(self):
Sli's avatar
Sli committed
207
        return reverse("counter:product_list")
Skia's avatar
Skia committed
208

Krophil's avatar
Krophil committed
209

Skia's avatar
Skia committed
210
class Counter(models.Model):
Sli's avatar
Sli committed
211
    name = models.CharField(_("name"), max_length=30)
212
    club = models.ForeignKey(Club, related_name="counters", verbose_name=_("club"))
Sli's avatar
Sli committed
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
    products = models.ManyToManyField(
        Product, related_name="counters", verbose_name=_("products"), blank=True
    )
    type = models.CharField(
        _("counter type"),
        max_length=255,
        choices=[("BAR", _("Bar")), ("OFFICE", _("Office")), ("EBOUTIC", _("Eboutic"))],
    )
    sellers = models.ManyToManyField(
        User, verbose_name=_("sellers"), related_name="counters", blank=True
    )
    edit_groups = models.ManyToManyField(
        Group, related_name="editable_counters", blank=True
    )
    view_groups = models.ManyToManyField(
        Group, related_name="viewable_counters", blank=True
    )
    token = models.CharField(_("token"), max_length=30, null=True, blank=True)
Skia's avatar
Skia committed
231

Skia's avatar
Skia committed
232
    class Meta:
Sli's avatar
Sli committed
233
        verbose_name = _("counter")
Skia's avatar
Skia committed
234

Skia's avatar
Skia committed
235
    def __getattribute__(self, name):
236
        if name == "edit_groups":
Sli's avatar
Sli committed
237
238
239
            return Group.objects.filter(
                name=self.club.unix_name + settings.SITH_BOARD_SUFFIX
            ).all()
Skia's avatar
Skia committed
240
241
242
243
        return object.__getattribute__(self, name)

    def __str__(self):
        return self.name
Skia's avatar
Skia committed
244
245

    def get_absolute_url(self):
Skia's avatar
Skia committed
246
        if self.type == "EBOUTIC":
Sli's avatar
Sli committed
247
248
            return reverse("eboutic:main")
        return reverse("counter:details", kwargs={"counter_id": self.id})
Skia's avatar
Skia committed
249

250
    def is_owned_by(self, user):
251
252
253
        mem = self.club.get_membership_for(user)
        if mem and mem.role >= 7:
            return True
Skia's avatar
Skia committed
254
        return user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID)
255

Skia's avatar
Skia committed
256
    def can_be_viewed_by(self, user):
Skia's avatar
Skia committed
257
        if self.type == "BAR":
258
            return True
Sli's avatar
Sli committed
259
260
261
262
        return (
            user.is_in_group(settings.SITH_MAIN_BOARD_GROUP)
            or user in self.sellers.all()
        )
263

264
265
    def gen_token(self):
        """Generate a new token for this counter"""
Sli's avatar
Sli committed
266
267
268
        self.token = "".join(
            random.choice(string.ascii_letters + string.digits) for x in range(30)
        )
269
270
        self.save()

271
    def add_barman(self, user):
Skia's avatar
Skia committed
272
273
274
275
        """
        Logs a barman in to the given counter
        A user is stored as a tuple with its login time
        """
276
277
278
        Permanency(user=user, counter=self, start=timezone.now(), end=None).save()

    def del_barman(self, user):
Skia's avatar
Skia committed
279
280
281
        """
        Logs a barman out and store its permanency
        """
282
283
284
285
        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
286

287
288
289
290
    @cached_property
    def barmen_list(self):
        return self.get_barmen_list()

Skia's avatar
Skia committed
291
    def get_barmen_list(self):
Skia's avatar
Skia committed
292
        """
Skia's avatar
Skia committed
293
        Returns the barman list as list of User
Skia's avatar
Skia committed
294
295
296

        Also handle the timeout of the barmen
        """
297
        pl = Permanency.objects.filter(counter=self, end=None).all()
298
        bl = []
299
        for p in pl:
Sli's avatar
Sli committed
300
301
302
            if timezone.now() - p.activity < timedelta(
                minutes=settings.SITH_BARMAN_TIMEOUT
            ):
303
                bl.append(p.user)
304
            else:
305
306
                p.end = p.activity
                p.save()
307
308
        return bl

Skia's avatar
Skia committed
309
    def get_random_barman(self):
Skia's avatar
Skia committed
310
311
312
        """
        Return a random user being currently a barman
        """
Skia's avatar
Skia committed
313
        bl = self.get_barmen_list()
Skia's avatar
Skia committed
314
        return bl[random.randrange(0, len(bl))]
Skia's avatar
Skia committed
315

Skia's avatar
Skia committed
316
317
318
319
320
    def update_activity(self):
        """
        Update the barman activity to prevent timeout
        """
        for p in Permanency.objects.filter(counter=self, end=None).all():
Krophil's avatar
Krophil committed
321
            p.save()  # Update activity
Skia's avatar
Skia committed
322

Sli's avatar
Sli committed
323
    def is_open(self):
324
        return len(self.barmen_list) > 0
Sli's avatar
Sli committed
325

326
327
    def is_inactive(self):
        """
328
        Returns True if the counter self is inactive from SITH_COUNTER_MINUTE_INACTIVE's value minutes, else False
329
        """
Sli's avatar
Sli committed
330
331
332
333
        return self.is_open() and (
            (timezone.now() - self.permanencies.order_by("-activity").first().activity)
            > datetime.timedelta(minutes=settings.SITH_COUNTER_MINUTE_INACTIVE)
        )
334

Skia's avatar
Skia committed
335
    def barman_list(self):
Skia's avatar
Skia committed
336
337
338
        """
        Returns the barman id list
        """
Skia's avatar
Skia committed
339
340
        return [b.id for b in self.get_barmen_list()]

Krophil's avatar
Krophil committed
341

Skia's avatar
Skia committed
342
343
344
345
class Refilling(models.Model):
    """
    Handle the refilling
    """
Sli's avatar
Sli committed
346

Skia's avatar
Skia committed
347
    counter = models.ForeignKey(Counter, related_name="refillings", blank=False)
Sli's avatar
Sli committed
348
349
350
351
    amount = CurrencyField(_("amount"))
    operator = models.ForeignKey(
        User, related_name="refillings_as_operator", blank=False
    )
352
    customer = models.ForeignKey(Customer, related_name="refillings", blank=False)
Sli's avatar
Sli committed
353
354
355
356
357
358
359
360
361
362
363
    date = models.DateTimeField(_("date"))
    payment_method = models.CharField(
        _("payment method"),
        max_length=255,
        choices=settings.SITH_COUNTER_PAYMENT_METHOD,
        default="CASH",
    )
    bank = models.CharField(
        _("bank"), max_length=255, choices=settings.SITH_COUNTER_BANK, default="OTHER"
    )
    is_validated = models.BooleanField(_("is validated"), default=False)
Skia's avatar
Skia committed
364

Skia's avatar
Skia committed
365
366
367
    class Meta:
        verbose_name = _("refilling")

Skia's avatar
Skia committed
368
    def __str__(self):
Sli's avatar
Sli committed
369
370
371
372
        return "Refilling: %.2f for %s" % (
            self.amount,
            self.customer.user.get_display_name(),
        )
Skia's avatar
Skia committed
373

Skia's avatar
Skia committed
374
    def is_owned_by(self, user):
375
        return user.is_owner(self.counter) and self.payment_method != "CARD"
Skia's avatar
Skia committed
376

Skia's avatar
Skia committed
377
378
379
380
381
    def delete(self, *args, **kwargs):
        self.customer.amount -= self.amount
        self.customer.save()
        super(Refilling, self).delete(*args, **kwargs)

Skia's avatar
Skia committed
382
    def save(self, *args, **kwargs):
383
        if not self.date:
Skia's avatar
Skia committed
384
            self.date = timezone.now()
Skia's avatar
Skia committed
385
        self.full_clean()
Skia's avatar
Skia committed
386
387
388
389
        if not self.is_validated:
            self.customer.amount += self.amount
            self.customer.save()
            self.is_validated = True
390
        if self.customer.user.preferences.notify_on_refill:
Sli's avatar
Sli committed
391
392
393
394
395
396
397
398
399
400
401
402
403
            Notification(
                user=self.customer.user,
                url=reverse(
                    "core:user_account_detail",
                    kwargs={
                        "user_id": self.customer.user.id,
                        "year": self.date.year,
                        "month": self.date.month,
                    },
                ),
                param=str(self.amount),
                type="REFILLING",
            ).save()
Skia's avatar
Skia committed
404
        super(Refilling, self).save(*args, **kwargs)
Skia's avatar
Skia committed
405

Krophil's avatar
Krophil committed
406

Skia's avatar
Skia committed
407
408
409
410
class Selling(models.Model):
    """
    Handle the sellings
    """
Sli's avatar
Sli committed
411

412
    label = models.CharField(_("label"), max_length=64)
Sli's avatar
Sli committed
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
443
444
445
446
447
448
449
450
451
452
453
    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
    )
    unit_price = CurrencyField(_("unit price"))
    quantity = models.IntegerField(_("quantity"))
    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",
    )
    is_validated = models.BooleanField(_("is validated"), default=False)
Skia's avatar
Skia committed
454

Skia's avatar
Skia committed
455
456
457
    class Meta:
        verbose_name = _("selling")

Skia's avatar
Skia committed
458
    def __str__(self):
Sli's avatar
Sli committed
459
460
461
462
463
464
        return "Selling: %d x %s (%f) for %s" % (
            self.quantity,
            self.label,
            self.quantity * self.unit_price,
            self.customer.user.get_display_name(),
        )
Skia's avatar
Skia committed
465

Skia's avatar
Skia committed
466
    def is_owned_by(self, user):
467
        return user.is_owner(self.counter) and self.payment_method != "CARD"
Skia's avatar
Skia committed
468

Skia's avatar
Skia committed
469
470
471
    def can_be_viewed_by(self, user):
        return user == self.customer.user

Skia's avatar
Skia committed
472
    def delete(self, *args, **kwargs):
473
474
475
        if self.payment_method == "SITH_ACCOUNT":
            self.customer.amount += self.quantity * self.unit_price
            self.customer.save()
Skia's avatar
Skia committed
476
477
        super(Selling, self).delete(*args, **kwargs)

478
    def send_mail_customer(self):
Sli's avatar
Sli committed
479
        event = self.product.eticket.event_title or _("Unknown event")
Sli's avatar
Sli committed
480
        subject = _("Eticket bought for the event %(event)s") % {"event": event}
481
482
483
        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
484
485
486
487
488
489
490
491
492
493
            "event": event,
            "url": "".join(
                (
                    '<a href="',
                    self.customer.get_full_url(),
                    '">',
                    self.customer.get_full_url(),
                    "</a>",
                )
            ),
494
495
496
        }
        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
497
498
        ) % {"event": event, "url": self.customer.get_full_url()}
        self.customer.user.email_user(subject, message_txt, html_message=message_html)
499

Sli's avatar
Sli committed
500
    def save(self, allow_negative=False, *args, **kwargs):
Sli's avatar
Sli committed
501
502
503
        """
            allow_negative : Allow this selling to use more money than available for this user
        """
504
        if not self.date:
Skia's avatar
Skia committed
505
            self.date = timezone.now()
Skia's avatar
Skia committed
506
        self.full_clean()
Skia's avatar
Skia committed
507
508
        if not self.is_validated:
            self.customer.amount -= self.quantity * self.unit_price
Sli's avatar
Sli committed
509
            self.customer.save(allow_negative=allow_negative, is_selling=True)
Skia's avatar
Skia committed
510
            self.is_validated = True
511
        u = User.objects.filter(id=self.customer.user.id).first()
512
        if u.was_subscribed:
Sli's avatar
Sli committed
513
514
515
516
            if (
                self.product
                and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER
            ):
517
                sub = Subscription(
Krophil's avatar
Krophil committed
518
                    member=u,
Sli's avatar
Sli committed
519
                    subscription_type="un-semestre",
Krophil's avatar
Krophil committed
520
521
522
                    payment_method="EBOUTIC",
                    location="EBOUTIC",
                )
523
524
                sub.subscription_start = Subscription.compute_start()
                sub.subscription_start = Subscription.compute_start(
Sli's avatar
Sli committed
525
526
527
528
                    duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][
                        "duration"
                    ]
                )
529
                sub.subscription_end = Subscription.compute_end(
Sli's avatar
Sli committed
530
531
532
533
534
                    duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][
                        "duration"
                    ],
                    start=sub.subscription_start,
                )
535
                sub.save()
Sli's avatar
Sli committed
536
537
538
539
            elif (
                self.product
                and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS
            ):
540
541
                u = User.objects.filter(id=self.customer.user.id).first()
                sub = Subscription(
Krophil's avatar
Krophil committed
542
                    member=u,
Sli's avatar
Sli committed
543
                    subscription_type="deux-semestres",
Krophil's avatar
Krophil committed
544
545
546
                    payment_method="EBOUTIC",
                    location="EBOUTIC",
                )
547
548
                sub.subscription_start = Subscription.compute_start()
                sub.subscription_start = Subscription.compute_start(
Sli's avatar
Sli committed
549
550
551
552
                    duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][
                        "duration"
                    ]
                )
553
                sub.subscription_end = Subscription.compute_end(
Sli's avatar
Sli committed
554
555
556
557
558
                    duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][
                        "duration"
                    ],
                    start=sub.subscription_start,
                )
559
                sub.save()
560
561
562
        try:
            if self.product.eticket:
                self.send_mail_customer()
Krophil's avatar
Krophil committed
563
564
        except:
            pass
565
566
567
        if self.customer.user.preferences.notify_on_click:
            Notification(
                user=self.customer.user,
Sli's avatar
Sli committed
568
569
570
571
572
573
574
575
                url=reverse(
                    "core:user_account_detail",
                    kwargs={
                        "user_id": self.customer.user.id,
                        "year": self.date.year,
                        "month": self.date.month,
                    },
                ),
576
577
578
                param="%d x %s" % (self.quantity, self.label),
                type="SELLING",
            ).save()
Skia's avatar
Skia committed
579
580
        super(Selling, self).save(*args, **kwargs)

Krophil's avatar
Krophil committed
581

Skia's avatar
Skia committed
582
583
584
585
class Permanency(models.Model):
    """
    This class aims at storing a traceability of who was barman where and when
    """
Sli's avatar
Sli committed
586

587
    user = models.ForeignKey(User, related_name="permanencies", verbose_name=_("user"))
Sli's avatar
Sli committed
588
589
590
591
592
593
    counter = models.ForeignKey(
        Counter, related_name="permanencies", verbose_name=_("counter")
    )
    start = models.DateTimeField(_("start date"))
    end = models.DateTimeField(_("end date"), null=True, db_index=True)
    activity = models.DateTimeField(_("last activity date"), auto_now=True)
Skia's avatar
Skia committed
594

Skia's avatar
Skia committed
595
596
597
    class Meta:
        verbose_name = _("permanency")

Skia's avatar
Skia committed
598
    def __str__(self):
Sli's avatar
Sli committed
599
600
601
602
603
604
605
        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 "",
        )
Krophil's avatar
Krophil committed
606

Skia's avatar
Skia committed
607

Skia's avatar
Skia committed
608
class CashRegisterSummary(models.Model):
Sli's avatar
Sli committed
609
610
611
612
613
614
615
616
617
    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)
Skia's avatar
Skia committed
618
619
620
621
622
623
624

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

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

625
    def __getattribute__(self, name):
Sli's avatar
Sli committed
626
627
628
        if name[:5] == "check":
            checks = self.items.filter(check=True).order_by("value").all()
        if name == "ten_cents":
629
            return self.items.filter(value=0.1, check=False).first()
Sli's avatar
Sli committed
630
        elif name == "twenty_cents":
631
            return self.items.filter(value=0.2, check=False).first()
Sli's avatar
Sli committed
632
        elif name == "fifty_cents":
633
            return self.items.filter(value=0.5, check=False).first()
Sli's avatar
Sli committed
634
        elif name == "one_euro":
635
            return self.items.filter(value=1, check=False).first()
Sli's avatar
Sli committed
636
        elif name == "two_euros":
637
            return self.items.filter(value=2, check=False).first()
Sli's avatar
Sli committed
638
        elif name == "five_euros":
639
            return self.items.filter(value=5, check=False).first()
Sli's avatar
Sli committed
640
        elif name == "ten_euros":
641
            return self.items.filter(value=10, check=False).first()
Sli's avatar
Sli committed
642
        elif name == "twenty_euros":
643
            return self.items.filter(value=20, check=False).first()
Sli's avatar
Sli committed
644
        elif name == "fifty_euros":
645
            return self.items.filter(value=50, check=False).first()
Sli's avatar
Sli committed
646
        elif name == "hundred_euros":
647
            return self.items.filter(value=100, check=False).first()
Sli's avatar
Sli committed
648
        elif name == "check_1":
649
            return checks[0] if 0 < len(checks) else None
Sli's avatar
Sli committed
650
        elif name == "check_2":
651
            return checks[1] if 1 < len(checks) else None
Sli's avatar
Sli committed
652
        elif name == "check_3":
653
            return checks[2] if 2 < len(checks) else None
Sli's avatar
Sli committed
654
        elif name == "check_4":
655
            return checks[3] if 3 < len(checks) else None
Sli's avatar
Sli committed
656
        elif name == "check_5":
657
658
659
660
            return checks[4] if 4 < len(checks) else None
        else:
            return object.__getattribute__(self, name)

Skia's avatar
Skia committed
661
662
663
664
    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
665
        if user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID):
Skia's avatar
Skia committed
666
667
668
            return True
        return False

Skia's avatar
Skia committed
669
670
671
672
673
674
675
676
677
678
679
    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)

680
    def get_absolute_url(self):
Sli's avatar
Sli committed
681
        return reverse("counter:cash_summary_list")
682

Krophil's avatar
Krophil committed
683

Skia's avatar
Skia committed
684
class CashRegisterSummaryItem(models.Model):
Sli's avatar
Sli committed
685
686
687
    cash_summary = models.ForeignKey(
        CashRegisterSummary, related_name="items", verbose_name=_("cash summary")
    )
Skia's avatar
Skia committed
688
    value = CurrencyField(_("value"))
Sli's avatar
Sli committed
689
690
    quantity = models.IntegerField(_("quantity"), default=0)
    check = models.BooleanField(_("check"), default=False)
Skia's avatar
Skia committed
691
692
693

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

Krophil's avatar
Krophil committed
695

Skia's avatar
Skia committed
696
697
698
699
class Eticket(models.Model):
    """
    Eticket can be linked to a product an allows PDF generation
    """
Sli's avatar
Sli committed
700
701
702
703
704
705
706
707
708
709
710
711

    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)
Skia's avatar
Skia committed
712
713
714
715
716

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

    def get_absolute_url(self):
Sli's avatar
Sli committed
717
        return reverse("counter:eticket_list")
Skia's avatar
Skia committed
718
719
720
721
722
723
724
725
726
727

    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
        """
Skia's avatar
Skia committed
728
        return user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID)
Skia's avatar
Skia committed
729
730

    def get_hash(self, string):
Krophil's avatar
Krophil committed
731
732
        import hashlib
        import hmac
Sli's avatar
Sli committed
733
734
735
736

        return hmac.new(
            bytes(self.secret, "utf-8"), bytes(string, "utf-8"), hashlib.sha1
        ).hexdigest()
737
738
739
740
741
742
743
744
745
746
747
748


class StudentCard(models.Model):
    """
    Alternative way to connect a customer into a counter
    We are using Mifare DESFire EV1 specs since it's used for izly cards
    https://www.nxp.com/docs/en/application-note/AN10927.pdf
    UID is 7 byte long that means 14 hexa characters
    """

    UID_SIZE = 14

749
750
751
    @staticmethod
    def is_valid(uid):
        return (
752
753
            len(uid) == StudentCard.UID_SIZE
            and not StudentCard.objects.filter(uid=uid).exists()
754
755
756
        )

    @staticmethod
757
    def can_create(customer, user):
758
759
        return user.pk == customer.user.pk or user.is_board_member or user.is_root

760
    def can_be_edited_by(self, obj):
761
        if isinstance(obj, User):
762
            return StudentCard.can_create(self.customer, obj)
763
764
        return False

Sli's avatar
Sli committed
765
766
767
    uid = models.CharField(
        _("uid"), max_length=14, unique=True, validators=[MinLengthValidator(4)]
    )
768
769
770
771
772
773
774
    customer = models.ForeignKey(
        Customer,
        related_name="student_cards",
        verbose_name=_("student cards"),
        null=False,
        blank=False,
    )