models.py 26.4 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
29
from django.urls 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
167
168
    club = models.ForeignKey(
        Club, related_name="products", verbose_name=_("club"), on_delete=models.CASCADE
    )
Sli's avatar
Sli committed
169
170
171
172
173
174
175
176
177
178
179
180
181
    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
182
    archived = models.BooleanField(_("archived"), default=False)
183

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

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

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

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

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

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

Krophil's avatar
Krophil committed
211

Skia's avatar
Skia committed
212
class Counter(models.Model):
Sli's avatar
Sli committed
213
    name = models.CharField(_("name"), max_length=30)
214
215
216
    club = models.ForeignKey(
        Club, related_name="counters", verbose_name=_("club"), on_delete=models.CASCADE
    )
Sli's avatar
Sli committed
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
    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
235

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

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

    def __str__(self):
        return self.name
Skia's avatar
Skia committed
248
249

    def get_absolute_url(self):
Skia's avatar
Skia committed
250
        if self.type == "EBOUTIC":
Sli's avatar
Sli committed
251
252
            return reverse("eboutic:main")
        return reverse("counter:details", kwargs={"counter_id": self.id})
Skia's avatar
Skia committed
253

254
    def is_owned_by(self, user):
255
256
257
        mem = self.club.get_membership_for(user)
        if mem and mem.role >= 7:
            return True
Skia's avatar
Skia committed
258
        return user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID)
259

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

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

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

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

291
292
293
294
    @cached_property
    def barmen_list(self):
        return self.get_barmen_list()

Skia's avatar
Skia committed
295
    def get_barmen_list(self):
Skia's avatar
Skia committed
296
        """
Skia's avatar
Skia committed
297
        Returns the barman list as list of User
Skia's avatar
Skia committed
298
299
300

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

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

Skia's avatar
Skia committed
320
321
322
323
324
    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
325
            p.save()  # Update activity
Skia's avatar
Skia committed
326

Sli's avatar
Sli committed
327
    def is_open(self):
328
        return len(self.barmen_list) > 0
Sli's avatar
Sli committed
329

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

Skia's avatar
Skia committed
339
    def barman_list(self):
Skia's avatar
Skia committed
340
341
342
        """
        Returns the barman id list
        """
Skia's avatar
Skia committed
343
344
        return [b.id for b in self.get_barmen_list()]

Krophil's avatar
Krophil committed
345

Skia's avatar
Skia committed
346
347
348
349
class Refilling(models.Model):
    """
    Handle the refilling
    """
Sli's avatar
Sli committed
350

351
352
353
    counter = models.ForeignKey(
        Counter, related_name="refillings", blank=False, on_delete=models.CASCADE
    )
Sli's avatar
Sli committed
354
355
    amount = CurrencyField(_("amount"))
    operator = models.ForeignKey(
356
357
358
359
360
361
362
        User,
        related_name="refillings_as_operator",
        blank=False,
        on_delete=models.CASCADE,
    )
    customer = models.ForeignKey(
        Customer, related_name="refillings", blank=False, on_delete=models.CASCADE
Sli's avatar
Sli committed
363
364
365
366
367
368
369
370
371
372
373
374
    )
    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
375

Skia's avatar
Skia committed
376
377
378
    class Meta:
        verbose_name = _("refilling")

Skia's avatar
Skia committed
379
    def __str__(self):
Sli's avatar
Sli committed
380
381
382
383
        return "Refilling: %.2f for %s" % (
            self.amount,
            self.customer.user.get_display_name(),
        )
Skia's avatar
Skia committed
384

Skia's avatar
Skia committed
385
    def is_owned_by(self, user):
386
        return user.is_owner(self.counter) and self.payment_method != "CARD"
Skia's avatar
Skia committed
387

Skia's avatar
Skia committed
388
389
390
391
392
    def delete(self, *args, **kwargs):
        self.customer.amount -= self.amount
        self.customer.save()
        super(Refilling, self).delete(*args, **kwargs)

Skia's avatar
Skia committed
393
    def save(self, *args, **kwargs):
394
        if not self.date:
Skia's avatar
Skia committed
395
            self.date = timezone.now()
Skia's avatar
Skia committed
396
        self.full_clean()
Skia's avatar
Skia committed
397
398
399
400
        if not self.is_validated:
            self.customer.amount += self.amount
            self.customer.save()
            self.is_validated = True
401
        if self.customer.user.preferences.notify_on_refill:
Sli's avatar
Sli committed
402
403
404
405
406
407
408
409
410
411
412
413
414
            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
415
        super(Refilling, self).save(*args, **kwargs)
Skia's avatar
Skia committed
416

Krophil's avatar
Krophil committed
417

Skia's avatar
Skia committed
418
419
420
421
class Selling(models.Model):
    """
    Handle the sellings
    """
Sli's avatar
Sli committed
422

423
    label = models.CharField(_("label"), max_length=64)
Sli's avatar
Sli committed
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
454
455
456
457
458
459
460
461
462
463
464
    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
465

Skia's avatar
Skia committed
466
467
468
    class Meta:
        verbose_name = _("selling")

Skia's avatar
Skia committed
469
    def __str__(self):
Sli's avatar
Sli committed
470
471
472
473
474
475
        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
476

Skia's avatar
Skia committed
477
    def is_owned_by(self, user):
478
        return user.is_owner(self.counter) and self.payment_method != "CARD"
Skia's avatar
Skia committed
479

Skia's avatar
Skia committed
480
481
482
    def can_be_viewed_by(self, user):
        return user == self.customer.user

Skia's avatar
Skia committed
483
    def delete(self, *args, **kwargs):
484
485
486
        if self.payment_method == "SITH_ACCOUNT":
            self.customer.amount += self.quantity * self.unit_price
            self.customer.save()
Skia's avatar
Skia committed
487
488
        super(Selling, self).delete(*args, **kwargs)

489
    def send_mail_customer(self):
Sli's avatar
Sli committed
490
        event = self.product.eticket.event_title or _("Unknown event")
Sli's avatar
Sli committed
491
        subject = _("Eticket bought for the event %(event)s") % {"event": event}
492
493
494
        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
495
496
497
498
499
500
501
502
503
504
            "event": event,
            "url": "".join(
                (
                    '<a href="',
                    self.customer.get_full_url(),
                    '">',
                    self.customer.get_full_url(),
                    "</a>",
                )
            ),
505
506
507
        }
        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
508
509
        ) % {"event": event, "url": self.customer.get_full_url()}
        self.customer.user.email_user(subject, message_txt, html_message=message_html)
510

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

Krophil's avatar
Krophil committed
592

Skia's avatar
Skia committed
593
594
595
596
class Permanency(models.Model):
    """
    This class aims at storing a traceability of who was barman where and when
    """
Sli's avatar
Sli committed
597

598
599
600
601
602
603
    user = models.ForeignKey(
        User,
        related_name="permanencies",
        verbose_name=_("user"),
        on_delete=models.CASCADE,
    )
Sli's avatar
Sli committed
604
    counter = models.ForeignKey(
605
606
607
608
        Counter,
        related_name="permanencies",
        verbose_name=_("counter"),
        on_delete=models.CASCADE,
Sli's avatar
Sli committed
609
610
611
612
    )
    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
613

Skia's avatar
Skia committed
614
615
616
    class Meta:
        verbose_name = _("permanency")

Skia's avatar
Skia committed
617
    def __str__(self):
Sli's avatar
Sli committed
618
619
620
621
622
623
624
        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
625

Skia's avatar
Skia committed
626

Skia's avatar
Skia committed
627
class CashRegisterSummary(models.Model):
Sli's avatar
Sli committed
628
    user = models.ForeignKey(
629
630
631
632
        User,
        related_name="cash_summaries",
        verbose_name=_("user"),
        on_delete=models.CASCADE,
Sli's avatar
Sli committed
633
634
    )
    counter = models.ForeignKey(
635
636
637
638
        Counter,
        related_name="cash_summaries",
        verbose_name=_("counter"),
        on_delete=models.CASCADE,
Sli's avatar
Sli committed
639
640
641
642
    )
    date = models.DateTimeField(_("date"))
    comment = models.TextField(_("comment"), null=True, blank=True)
    emptied = models.BooleanField(_("emptied"), default=False)
Skia's avatar
Skia committed
643
644
645
646
647
648
649

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

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

650
    def __getattribute__(self, name):
Sli's avatar
Sli committed
651
652
653
        if name[:5] == "check":
            checks = self.items.filter(check=True).order_by("value").all()
        if name == "ten_cents":
654
            return self.items.filter(value=0.1, check=False).first()
Sli's avatar
Sli committed
655
        elif name == "twenty_cents":
656
            return self.items.filter(value=0.2, check=False).first()
Sli's avatar
Sli committed
657
        elif name == "fifty_cents":
658
            return self.items.filter(value=0.5, check=False).first()
Sli's avatar
Sli committed
659
        elif name == "one_euro":
660
            return self.items.filter(value=1, check=False).first()
Sli's avatar
Sli committed
661
        elif name == "two_euros":
662
            return self.items.filter(value=2, check=False).first()
Sli's avatar
Sli committed
663
        elif name == "five_euros":
664
            return self.items.filter(value=5, check=False).first()
Sli's avatar
Sli committed
665
        elif name == "ten_euros":
666
            return self.items.filter(value=10, check=False).first()
Sli's avatar
Sli committed
667
        elif name == "twenty_euros":
668
            return self.items.filter(value=20, check=False).first()
Sli's avatar
Sli committed
669
        elif name == "fifty_euros":
670
            return self.items.filter(value=50, check=False).first()
Sli's avatar
Sli committed
671
        elif name == "hundred_euros":
672
            return self.items.filter(value=100, check=False).first()
Sli's avatar
Sli committed
673
        elif name == "check_1":
674
            return checks[0] if 0 < len(checks) else None
Sli's avatar
Sli committed
675
        elif name == "check_2":
676
            return checks[1] if 1 < len(checks) else None
Sli's avatar
Sli committed
677
        elif name == "check_3":
678
            return checks[2] if 2 < len(checks) else None
Sli's avatar
Sli committed
679
        elif name == "check_4":
680
            return checks[3] if 3 < len(checks) else None
Sli's avatar
Sli committed
681
        elif name == "check_5":
682
683
684
685
            return checks[4] if 4 < len(checks) else None
        else:
            return object.__getattribute__(self, name)

Skia's avatar
Skia committed
686
687
688
689
    def is_owned_by(self, user):
        """
        Method to see if that object can be edited by the given user
        """
Skia's avatar
Skia committed
690
        if user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID):
Skia's avatar
Skia committed
691
692
693
            return True
        return False

Skia's avatar
Skia committed
694
695
696
697
698
699
700
701
702
703
704
    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)

705
    def get_absolute_url(self):
Sli's avatar
Sli committed
706
        return reverse("counter:cash_summary_list")
707

Krophil's avatar
Krophil committed
708

Skia's avatar
Skia committed
709
class CashRegisterSummaryItem(models.Model):
Sli's avatar
Sli committed
710
    cash_summary = models.ForeignKey(
711
712
713
714
        CashRegisterSummary,
        related_name="items",
        verbose_name=_("cash summary"),
        on_delete=models.CASCADE,
Sli's avatar
Sli committed
715
    )
Skia's avatar
Skia committed
716
    value = CurrencyField(_("value"))
Sli's avatar
Sli committed
717
718
    quantity = models.IntegerField(_("quantity"), default=0)
    check = models.BooleanField(_("check"), default=False)
Skia's avatar
Skia committed
719
720
721

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

Krophil's avatar
Krophil committed
723

Skia's avatar
Skia committed
724
725
726
727
class Eticket(models.Model):
    """
    Eticket can be linked to a product an allows PDF generation
    """
Sli's avatar
Sli committed
728
729
730
731
732
733
734
735
736
737
738
739

    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
740
741
742
743
744

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

    def get_absolute_url(self):
Sli's avatar
Sli committed
745
        return reverse("counter:eticket_list")
Skia's avatar
Skia committed
746
747
748
749
750
751
752
753
754
755

    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
756
        return user.is_in_group(settings.SITH_GROUP_COUNTER_ADMIN_ID)
Skia's avatar
Skia committed
757
758

    def get_hash(self, string):
Krophil's avatar
Krophil committed
759
760
        import hashlib
        import hmac
Sli's avatar
Sli committed
761
762
763
764

        return hmac.new(
            bytes(self.secret, "utf-8"), bytes(string, "utf-8"), hashlib.sha1
        ).hexdigest()
765
766
767
768
769
770
771
772
773
774
775
776


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

777
778
779
    @staticmethod
    def is_valid(uid):
        return (
780
            (uid.isupper() or uid.isnumeric())
781
            and len(uid) == StudentCard.UID_SIZE
782
            and not StudentCard.objects.filter(uid=uid).exists()
783
784
785
        )

    @staticmethod
786
    def can_create(customer, user):
787
788
        return user.pk == customer.user.pk or user.is_board_member or user.is_root

789
    def can_be_edited_by(self, obj):
790
        if isinstance(obj, User):
791
            return StudentCard.can_create(self.customer, obj)
792
793
        return False

Sli's avatar
Sli committed
794
795
796
    uid = models.CharField(
        _("uid"), max_length=14, unique=True, validators=[MinLengthValidator(4)]
    )
797
798
799
800
801
802
    customer = models.ForeignKey(
        Customer,
        related_name="student_cards",
        verbose_name=_("student cards"),
        null=False,
        blank=False,
803
        on_delete=models.CASCADE,
804
    )