views.py 64.6 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.shortcuts import get_object_or_404
26
from django.http import Http404
Skia's avatar
Skia committed
27
from django.core.exceptions import PermissionDenied
Skia's avatar
Skia committed
28
from django.views.generic import ListView, DetailView, RedirectView, TemplateView
Sli's avatar
Sli committed
29
from django.views.generic.base import View
Sli's avatar
Sli committed
30
31
32
33
34
35
from django.views.generic.edit import (
    UpdateView,
    CreateView,
    DeleteView,
    ProcessFormView,
    FormMixin,
36
    FormView,
Sli's avatar
Sli committed
37
)
Skia's avatar
Skia committed
38
39
from django.forms.models import modelform_factory
from django.forms import CheckboxSelectMultiple
Skia's avatar
Skia committed
40
from django.core.urlresolvers import reverse_lazy, reverse
Skia's avatar
Skia committed
41
from django.http import HttpResponseRedirect, HttpResponse
Skia's avatar
Skia committed
42
from django.utils import timezone
Skia's avatar
Skia committed
43
from django import forms
Skia's avatar
Skia committed
44
from django.utils.translation import ugettext_lazy as _
45
from django.conf import settings
Skia's avatar
Skia committed
46
from django.db import DataError, transaction, models
Skia's avatar
Skia committed
47

Skia's avatar
Skia committed
48
import re
Skia's avatar
Skia committed
49
50
import pytz
from datetime import date, timedelta, datetime
51
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
Krophil's avatar
Krophil committed
52
from ajax_select import make_ajax_field
Skia's avatar
Skia committed
53

54
from core.views import CanViewMixin, TabedViewMixin, CanEditMixin
Krophil's avatar
Krophil committed
55
from core.views.forms import LoginForm, SelectDate, SelectDateTime
56
from core.models import User
Skia's avatar
Skia committed
57
from subscription.models import Subscription
Sli's avatar
Sli committed
58
59
60
from counter.models import (
    Counter,
    Customer,
61
    StudentCard,
Sli's avatar
Sli committed
62
63
64
65
66
67
68
69
70
    Product,
    Selling,
    Refilling,
    ProductType,
    CashRegisterSummary,
    CashRegisterSummaryItem,
    Eticket,
    Permanency,
)
Skia's avatar
Skia committed
71
from accounting.models import CurrencyField
Skia's avatar
Skia committed
72

Krophil's avatar
Krophil committed
73

Sli's avatar
Sli committed
74
class CounterAdminMixin(View):
Sli's avatar
Sli committed
75
76
77
    """
    This view is made to protect counter admin section
    """
Sli's avatar
Sli committed
78

Sli's avatar
Sli committed
79
80
81
    edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID]
    edit_club = []

Sli's avatar
Sli committed
82
    def _test_group(self, user):
Sli's avatar
Sli committed
83
        for g in self.edit_group:
Sli's avatar
Sli committed
84
85
86
87
            if user.is_in_group(g):
                return True
        return False

Sli's avatar
Sli committed
88
89
90
91
92
93
    def _test_club(self, user):
        for c in self.edit_club:
            if c.can_be_edited_by(user):
                return True
        return False

Sli's avatar
Sli committed
94
    def dispatch(self, request, *args, **kwargs):
Sli's avatar
Sli committed
95
96
97
98
99
        if not (
            request.user.is_root
            or self._test_group(request.user)
            or self._test_club(request.user)
        ):
Sli's avatar
Sli committed
100
            raise PermissionDenied
Sli's avatar
Sli committed
101
        return super(CounterAdminMixin, self).dispatch(request, *args, **kwargs)
Sli's avatar
Sli committed
102

Krophil's avatar
Krophil committed
103

104
105
106
107
108
109
110
111
112
113
114
115
class StudentCardForm(forms.ModelForm):
    """
    Form for adding student cards
    Only used for user profile since CounterClick is to complicated
    """

    class Meta:
        model = StudentCard
        fields = ["uid"]

    def clean(self):
        cleaned_data = super(StudentCardForm, self).clean()
116
117
        uid = cleaned_data.get("uid", None)
        if not uid or not StudentCard.is_valid(uid):
118
119
120
121
            raise forms.ValidationError(_("This uid is invalid"), code="invalid")
        return cleaned_data


122
class StudentCardDeleteView(DeleteView, CanEditMixin):
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
    """
    View used to delete a card from a user
    """

    model = StudentCard
    template_name = "core/delete_confirm.jinja"
    pk_url_kwarg = "card_id"

    def dispatch(self, request, *args, **kwargs):
        self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"])
        return super(StudentCardDeleteView, self).dispatch(request, *args, **kwargs)

    def get_success_url(self, **kwargs):
        return reverse_lazy(
            "core:user_prefs", kwargs={"user_id": self.customer.user.pk}
        )


Skia's avatar
Skia committed
141
class GetUserForm(forms.Form):
142
143
144
145
146
147
148
    """
    The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view,
    reverse function, or any other use.

    The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with
    some nickname, first name, or last name (TODO)
    """
Sli's avatar
Sli committed
149

150
151
152
    code = forms.CharField(
        label="Code", max_length=StudentCard.UID_SIZE, required=False
    )
Sli's avatar
Sli committed
153
154
155
    id = AutoCompleteSelectField(
        "users", required=False, label=_("Select user"), help_text=None
    )
Skia's avatar
Skia committed
156

Skia's avatar
Skia committed
157
    def as_p(self):
Sli's avatar
Sli committed
158
        self.fields["code"].widget.attrs["autofocus"] = True
Skia's avatar
Skia committed
159
160
        return super(GetUserForm, self).as_p()

161
162
    def clean(self):
        cleaned_data = super(GetUserForm, self).clean()
Skia's avatar
Skia committed
163
        cus = None
Sli's avatar
Sli committed
164
        if cleaned_data["code"] != "":
165
166
167
168
169
170
171
172
            if len(cleaned_data["code"]) == StudentCard.UID_SIZE:
                card = StudentCard.objects.filter(uid=cleaned_data["code"])
                if card.exists():
                    cus = card.first().customer
            if cus is None:
                cus = Customer.objects.filter(
                    account_id__iexact=cleaned_data["code"]
                ).first()
Sli's avatar
Sli committed
173
174
175
        elif cleaned_data["id"] is not None:
            cus = Customer.objects.filter(user=cleaned_data["id"]).first()
        if cus is None or not cus.can_buy:
Skia's avatar
Skia committed
176
            raise forms.ValidationError(_("User not found"))
Sli's avatar
Sli committed
177
178
        cleaned_data["user_id"] = cus.user.id
        cleaned_data["user"] = cus.user
179
180
        return cleaned_data

Krophil's avatar
Krophil committed
181

182
class RefillForm(forms.ModelForm):
Sli's avatar
Sli committed
183
184
185
186
187
    error_css_class = "error"
    required_css_class = "required"
    amount = forms.FloatField(
        min_value=0, widget=forms.NumberInput(attrs={"class": "focus"})
    )
Krophil's avatar
Krophil committed
188

189
190
    class Meta:
        model = Refilling
Sli's avatar
Sli committed
191
        fields = ["amount", "payment_method", "bank"]
192

Krophil's avatar
Krophil committed
193

Skia's avatar
Skia committed
194
195
class CounterTabsMixin(TabedViewMixin):
    def get_tabs_title(self):
Sli's avatar
Sli committed
196
        if hasattr(self.object, "stock_owner"):
197
198
199
            return self.object.stock_owner.counter
        else:
            return self.object
Krophil's avatar
Krophil committed
200

Skia's avatar
Skia committed
201
202
    def get_list_of_tabs(self):
        tab_list = []
Sli's avatar
Sli committed
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
        tab_list.append(
            {
                "url": reverse_lazy(
                    "counter:details",
                    kwargs={
                        "counter_id": self.object.stock_owner.counter.id
                        if hasattr(self.object, "stock_owner")
                        else self.object.id
                    },
                ),
                "slug": "counter",
                "name": _("Counter"),
            }
        )
        if (
            self.object.stock_owner.counter.type
            if hasattr(self.object, "stock_owner")
            else self.object.type == "BAR"
        ):
            tab_list.append(
                {
                    "url": reverse_lazy(
                        "counter:cash_summary",
                        kwargs={
                            "counter_id": self.object.stock_owner.counter.id
                            if hasattr(self.object, "stock_owner")
                            else self.object.id
                        },
                    ),
                    "slug": "cash_summary",
                    "name": _("Cash summary"),
                }
            )
            tab_list.append(
                {
                    "url": reverse_lazy(
                        "counter:last_ops",
                        kwargs={
                            "counter_id": self.object.stock_owner.counter.id
                            if hasattr(self.object, "stock_owner")
                            else self.object.id
                        },
                    ),
                    "slug": "last_ops",
                    "name": _("Last operations"),
                }
            )
Skia's avatar
Skia committed
250
            try:
Sli's avatar
Sli committed
251
252
253
254
255
256
257
258
259
260
261
262
263
264
                tab_list.append(
                    {
                        "url": reverse_lazy(
                            "stock:take_items",
                            kwargs={
                                "stock_id": self.object.stock.id
                                if hasattr(self.object, "stock")
                                else self.object.stock_owner.id
                            },
                        ),
                        "slug": "take_items_from_stock",
                        "name": _("Take items from stock"),
                    }
                )
Krophil's avatar
Krophil committed
265
266
            except:
                pass  # The counter just have no stock
Skia's avatar
Skia committed
267
268
        return tab_list

Krophil's avatar
Krophil committed
269

Sli's avatar
Sli committed
270
271
272
class CounterMain(
    CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin
):
Skia's avatar
Skia committed
273
274
275
    """
    The public (barman) view
    """
Sli's avatar
Sli committed
276

Skia's avatar
Skia committed
277
    model = Counter
Sli's avatar
Sli committed
278
    template_name = "counter/counter_main.jinja"
Skia's avatar
Skia committed
279
    pk_url_kwarg = "counter_id"
Sli's avatar
Sli committed
280
281
282
    form_class = (
        GetUserForm
    )  # Form to enter a client code and get the corresponding user id
Skia's avatar
Skia committed
283
    current_tab = "counter"
Skia's avatar
Skia committed
284

285
286
    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
Sli's avatar
Sli committed
287
288
289
290
291
292
293
294
295
296
297
298
        if self.object.type == "BAR" and not (
            "counter_token" in self.request.session.keys()
            and self.request.session["counter_token"] == self.object.token
        ):  # Check the token to avoid the bar to be stolen
            return HttpResponseRedirect(
                reverse_lazy(
                    "counter:details",
                    args=self.args,
                    kwargs={"counter_id": self.object.id},
                )
                + "?bad_location"
            )
299
300
        return super(CounterMain, self).post(request, *args, **kwargs)

Skia's avatar
Skia committed
301
    def get_context_data(self, **kwargs):
Skia's avatar
Skia committed
302
        """
303
        We handle here the login form for the barman
Skia's avatar
Skia committed
304
        """
Sli's avatar
Sli committed
305
        if self.request.method == "POST":
306
            self.object = self.get_object()
Skia's avatar
Skia committed
307
        self.object.update_activity()
Skia's avatar
Skia committed
308
        kwargs = super(CounterMain, self).get_context_data(**kwargs)
Sli's avatar
Sli committed
309
310
311
312
313
        kwargs["login_form"] = LoginForm()
        kwargs["login_form"].fields["username"].widget.attrs["autofocus"] = True
        kwargs[
            "login_form"
        ].cleaned_data = {}  # add_error fails if there are no cleaned_data
Skia's avatar
Skia committed
314
        if "credentials" in self.request.GET:
Sli's avatar
Sli committed
315
            kwargs["login_form"].add_error(None, _("Bad credentials"))
316
        if "sellers" in self.request.GET:
Sli's avatar
Sli committed
317
318
319
            kwargs["login_form"].add_error(None, _("User is not barman"))
        kwargs["form"] = self.get_form()
        kwargs["form"].cleaned_data = {}  # same as above
320
        if "bad_location" in self.request.GET:
Sli's avatar
Sli committed
321
322
323
324
325
            kwargs["form"].add_error(
                None, _("Bad location, someone is already logged in somewhere else")
            )
        if self.object.type == "BAR":
            kwargs["barmen"] = self.object.get_barmen_list()
Skia's avatar
Skia committed
326
        elif self.request.user.is_authenticated():
Sli's avatar
Sli committed
327
328
329
330
331
332
333
334
            kwargs["barmen"] = [self.request.user]
        if "last_basket" in self.request.session.keys():
            kwargs["last_basket"] = self.request.session.pop("last_basket")
            kwargs["last_customer"] = self.request.session.pop("last_customer")
            kwargs["last_total"] = self.request.session.pop("last_total")
            kwargs["new_customer_amount"] = self.request.session.pop(
                "new_customer_amount"
            )
Skia's avatar
Skia committed
335
336
        return kwargs

337
338
339
340
    def form_valid(self, form):
        """
        We handle here the redirection, passing the user id of the asked customer
        """
Sli's avatar
Sli committed
341
        self.kwargs["user_id"] = form.cleaned_data["user_id"]
342
343
344
        return super(CounterMain, self).form_valid(form)

    def get_success_url(self):
Sli's avatar
Sli committed
345
        return reverse_lazy("counter:click", args=self.args, kwargs=self.kwargs)
346

Krophil's avatar
Krophil committed
347

Skia's avatar
Skia committed
348
class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
Skia's avatar
Skia committed
349
350
    """
    The click view
351
352
    This is a detail view not to have to worry about loading the counter
    Everything is made by hand in the post method
Skia's avatar
Skia committed
353
    """
Sli's avatar
Sli committed
354

355
    model = Counter
Sli's avatar
Sli committed
356
    template_name = "counter/counter_click.jinja"
Skia's avatar
Skia committed
357
    pk_url_kwarg = "counter_id"
Skia's avatar
Skia committed
358
    current_tab = "counter"
359

360
    def dispatch(self, request, *args, **kwargs):
Sli's avatar
Sli committed
361
        self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
Sli's avatar
Sli committed
362
        obj = self.get_object()
363
364
        if not self.customer.can_buy:
            raise Http404
Sli's avatar
Sli committed
365
        if obj.type == "BAR":
Sli's avatar
Sli committed
366
367
368
369
370
371
372
            if (
                not (
                    "counter_token" in request.session.keys()
                    and request.session["counter_token"] == obj.token
                )
                or len(obj.get_barmen_list()) < 1
            ):
Sli's avatar
Sli committed
373
374
375
376
                raise PermissionDenied
        else:
            if not request.user.is_authenticated():
                raise PermissionDenied
377
378
        return super(CounterClick, self).dispatch(request, *args, **kwargs)

379
    def get(self, request, *args, **kwargs):
380
        """Simple get view"""
Sli's avatar
Sli committed
381
382
383
384
385
386
387
        if "basket" not in request.session.keys():  # Init the basket session entry
            request.session["basket"] = {}
            request.session["basket_total"] = 0
        request.session["not_enough"] = False  # Reset every variable
        request.session["too_young"] = False
        request.session["not_allowed"] = False
        request.session["no_age"] = False
388
        self.refill_form = None
389
        ret = super(CounterClick, self).get(request, *args, **kwargs)
Sli's avatar
Sli committed
390
391
392
        if (self.object.type != "BAR" and not request.user.is_authenticated()) or (
            self.object.type == "BAR" and len(self.object.get_barmen_list()) < 1
        ):  # Check that at least one barman is logged in
Krophil's avatar
Krophil committed
393
            ret = self.cancel(request)  # Otherwise, go to main view
394
        return ret
Skia's avatar
Skia committed
395
396

    def post(self, request, *args, **kwargs):
397
        """ Handle the many possibilities of the post request """
398
        self.object = self.get_object()
399
        self.refill_form = None
Sli's avatar
Sli committed
400
401
402
        if (self.object.type != "BAR" and not request.user.is_authenticated()) or (
            self.object.type == "BAR" and len(self.object.get_barmen_list()) < 1
        ):  # Check that at least one barman is logged in
403
            return self.cancel(request)
Sli's avatar
Sli committed
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
        if self.object.type == "BAR" and not (
            "counter_token" in self.request.session.keys()
            and self.request.session["counter_token"] == self.object.token
        ):  # Also check the token to avoid the bar to be stolen
            return HttpResponseRedirect(
                reverse_lazy(
                    "counter:details",
                    args=self.args,
                    kwargs={"counter_id": self.object.id},
                )
                + "?bad_location"
            )
        if "basket" not in request.session.keys():
            request.session["basket"] = {}
            request.session["basket_total"] = 0
        request.session["not_enough"] = False  # Reset every variable
        request.session["too_young"] = False
        request.session["not_allowed"] = False
        request.session["no_age"] = False
Sli's avatar
Sli committed
423
        request.session["not_valid_student_card_uid"] = False
Skia's avatar
Skia committed
424
425
426
427
428
        if self.object.type != "BAR":
            self.operator = request.user
        elif self.is_barman_price():
            self.operator = self.customer.user
        else:
Skia's avatar
Skia committed
429
            self.operator = self.object.get_random_barman()
430

Sli's avatar
Sli committed
431
        if "add_product" in request.POST["action"]:
432
            self.add_product(request)
Sli's avatar
Sli committed
433
434
        elif "add_student_card" in request.POST["action"]:
            self.add_student_card(request)
Sli's avatar
Sli committed
435
        elif "del_product" in request.POST["action"]:
436
            self.del_product(request)
Sli's avatar
Sli committed
437
        elif "refill" in request.POST["action"]:
Skia's avatar
Skia committed
438
            self.refill(request)
Sli's avatar
Sli committed
439
        elif "code" in request.POST["action"]:
Skia's avatar
Skia committed
440
            return self.parse_code(request)
Sli's avatar
Sli committed
441
        elif "cancel" in request.POST["action"]:
442
            return self.cancel(request)
Sli's avatar
Sli committed
443
        elif "finish" in request.POST["action"]:
444
445
446
447
            return self.finish(request)
        context = self.get_context_data(object=self.object)
        return self.render_to_response(context)

Skia's avatar
Skia committed
448
    def is_barman_price(self):
Sli's avatar
Sli committed
449
450
451
        if self.object.type == "BAR" and self.customer.user.id in [
            s.id for s in self.object.get_barmen_list()
        ]:
452
453
454
455
            return True
        else:
            return False

456
457
458
    def get_product(self, pid):
        return Product.objects.filter(pk=int(pid)).first()

459
    def get_price(self, pid):
460
        p = self.get_product(pid)
461
462
463
464
465
466
467
468
        if self.is_barman_price():
            price = p.special_selling_price
        else:
            price = p.selling_price
        return price

    def sum_basket(self, request):
        total = 0
Sli's avatar
Sli committed
469
470
        for pid, infos in request.session["basket"].items():
            total += infos["price"] * infos["qty"]
471
        return total / 100
472

Skia's avatar
Skia committed
473
474
475
    def get_total_quantity_for_pid(self, request, pid):
        pid = str(pid)
        try:
Sli's avatar
Sli committed
476
477
478
479
            return (
                request.session["basket"][pid]["qty"]
                + request.session["basket"][pid]["bonus_qty"]
            )
Skia's avatar
Skia committed
480
481
482
        except:
            return 0

Sli's avatar
Sli committed
483
484
    def compute_record_product(self, request, product=None):
        recorded = 0
Sli's avatar
Sli committed
485
        basket = request.session["basket"]
Sli's avatar
Sli committed
486
487
488
489
490
491
492
493
494
495

        if product:
            if product.is_record_product:
                recorded -= 1
            elif product.is_unrecord_product:
                recorded += 1

        for p in basket:
            bproduct = self.get_product(str(p))
            if bproduct.is_record_product:
Sli's avatar
Sli committed
496
                recorded -= basket[p]["qty"]
Sli's avatar
Sli committed
497
            elif bproduct.is_unrecord_product:
Sli's avatar
Sli committed
498
                recorded += basket[p]["qty"]
Sli's avatar
Sli committed
499
500
501
502
        return recorded

    def is_record_product_ok(self, request, product):
        return self.customer.can_record_more(
Sli's avatar
Sli committed
503
504
            self.compute_record_product(request, product)
        )
Sli's avatar
Sli committed
505

Krophil's avatar
Krophil committed
506
    def add_product(self, request, q=1, p=None):
507
508
509
510
511
        """
        Add a product to the basket
        q is the quantity passed as integer
        p is the product id, passed as an integer
        """
Sli's avatar
Sli committed
512
        pid = p or request.POST["product_id"]
Skia's avatar
Skia committed
513
        pid = str(pid)
514
        price = self.get_price(pid)
515
        total = self.sum_basket(request)
Skia's avatar
Skia committed
516
        product = self.get_product(pid)
517
        can_buy = False
518
519
520
521
522
523
        if not product.buying_groups.exists():
            can_buy = True
        else:
            for g in product.buying_groups.all():
                if self.customer.user.is_in_group(g.name):
                    can_buy = True
524
        if not can_buy:
Sli's avatar
Sli committed
525
            request.session["not_allowed"] = True
526
            return False
Krophil's avatar
Krophil committed
527
        bq = 0  # Bonus quantity, for trays
Sli's avatar
Sli committed
528
529
530
        if (
            product.tray
        ):  # Handle the tray to adjust the quantity q to add and the bonus quantity bq
Skia's avatar
Skia committed
531
            total_qty_mod_6 = self.get_total_quantity_for_pid(request, pid) % 6
Krophil's avatar
Krophil committed
532
            bq = int((total_qty_mod_6 + q) / 6)  # Integer division
Skia's avatar
Skia committed
533
            q -= bq
Sli's avatar
Sli committed
534
535
536
537
        if self.customer.amount < (
            total + round(q * float(price), 2)
        ):  # Check for enough money
            request.session["not_enough"] = True
Skia's avatar
Skia committed
538
            return False
Sli's avatar
Sli committed
539
540
541
542
        if product.is_unrecord_product and not self.is_record_product_ok(
            request, product
        ):
            request.session["not_allowed"] = True
Sli's avatar
Sli committed
543
            return False
Skia's avatar
Skia committed
544
        if product.limit_age >= 18 and not self.customer.user.date_of_birth:
Sli's avatar
Sli committed
545
            request.session["no_age"] = True
Skia's avatar
Skia committed
546
            return False
Sli's avatar
Sli committed
547
        if product.limit_age >= 18 and self.customer.user.is_banned_alcohol:
Sli's avatar
Sli committed
548
            request.session["not_allowed"] = True
Sli's avatar
Sli committed
549
            return False
550
        if self.customer.user.is_banned_counter:
Sli's avatar
Sli committed
551
            request.session["not_allowed"] = True
552
            return False
Sli's avatar
Sli committed
553
554
555
556
557
        if (
            self.customer.user.date_of_birth
            and self.customer.user.get_age() < product.limit_age
        ):  # Check if affordable
            request.session["too_young"] = True
558
            return False
Sli's avatar
Sli committed
559
560
561
        if pid in request.session["basket"]:  # Add if already in basket
            request.session["basket"][pid]["qty"] += q
            request.session["basket"][pid]["bonus_qty"] += bq
Krophil's avatar
Krophil committed
562
        else:  # or create if not
Sli's avatar
Sli committed
563
564
565
566
567
            request.session["basket"][pid] = {
                "qty": q,
                "price": int(price * 100),
                "bonus_qty": bq,
            }
568
        request.session.modified = True
Skia's avatar
Skia committed
569
        return True
570

Sli's avatar
Sli committed
571
572
573
574
575
576
    def add_student_card(self, request):
        """
        Add a new student card on the customer account
        """
        uid = request.POST["student_card_uid"]
        uid = str(uid)
577
        if not StudentCard.is_valid(uid):
Sli's avatar
Sli committed
578
579
580
            request.session["not_valid_student_card_uid"] = True
            return False

581
582
583
584
585
586
587
588
589
        if not (
            self.object.type == "BAR"
            and "counter_token" in request.session.keys()
            and request.session["counter_token"] == self.object.token
            and len(self.object.get_barmen_list()) > 0
        ):
            raise PermissionDenied

        StudentCard(customer=self.customer, uid=uid).save()
Sli's avatar
Sli committed
590
591
        return True

592
593
    def del_product(self, request):
        """ Delete a product from the basket """
Sli's avatar
Sli committed
594
        pid = str(request.POST["product_id"])
Skia's avatar
Skia committed
595
        product = self.get_product(pid)
Sli's avatar
Sli committed
596
597
598
599
600
601
602
        if pid in request.session["basket"]:
            if (
                product.tray
                and (self.get_total_quantity_for_pid(request, pid) % 6 == 0)
                and request.session["basket"][pid]["bonus_qty"]
            ):
                request.session["basket"][pid]["bonus_qty"] -= 1
Skia's avatar
Skia committed
603
            else:
Sli's avatar
Sli committed
604
605
606
                request.session["basket"][pid]["qty"] -= 1
            if request.session["basket"][pid]["qty"] <= 0:
                del request.session["basket"][pid]
607
        else:
Sli's avatar
Sli committed
608
            request.session["basket"][pid] = None
609
610
        request.session.modified = True

Skia's avatar
Skia committed
611
612
    def parse_code(self, request):
        """Parse the string entered by the barman"""
Sli's avatar
Sli committed
613
        string = str(request.POST["code"]).upper()
Skia's avatar
Skia committed
614
        if string == _("END"):
Skia's avatar
Skia committed
615
            return self.finish(request)
Skia's avatar
Skia committed
616
        elif string == _("CAN"):
Skia's avatar
Skia committed
617
618
619
620
            return self.cancel(request)
        regex = re.compile(r"^((?P<nb>[0-9]+)X)?(?P<code>[A-Z0-9]+)$")
        m = regex.match(string)
        if m is not None:
Sli's avatar
Sli committed
621
622
            nb = m.group("nb")
            code = m.group("code")
Skia's avatar
Skia committed
623
624
625
626
            if nb is None:
                nb = 1
            else:
                nb = int(nb)
Skia's avatar
Skia committed
627
            p = self.object.products.filter(code=code).first()
Skia's avatar
Skia committed
628
629
630
631
632
633
            if p is not None:
                while nb > 0 and not self.add_product(request, nb, p.id):
                    nb -= 1
        context = self.get_context_data(object=self.object)
        return self.render_to_response(context)

634
635
    def finish(self, request):
        """ Finish the click session, and validate the basket """
Skia's avatar
Skia committed
636
        with transaction.atomic():
Sli's avatar
Sli committed
637
            request.session["last_basket"] = []
Sli's avatar
Sli committed
638
639
640
            if self.sum_basket(request) > self.customer.amount:
                raise DataError(_("You have not enough money to buy all the basket"))

Sli's avatar
Sli committed
641
            for pid, infos in request.session["basket"].items():
Skia's avatar
Skia committed
642
643
644
645
646
647
                # This duplicates code for DB optimization (prevent to load many times the same object)
                p = Product.objects.filter(pk=pid).first()
                if self.is_barman_price():
                    uprice = p.special_selling_price
                else:
                    uprice = p.selling_price
Sli's avatar
Sli committed
648
649
650
651
652
653
654
655
656
657
658
659
660
                request.session["last_basket"].append(
                    "%d x %s" % (infos["qty"] + infos["bonus_qty"], p.name)
                )
                s = Selling(
                    label=p.name,
                    product=p,
                    club=p.club,
                    counter=self.object,
                    unit_price=uprice,
                    quantity=infos["qty"],
                    seller=self.operator,
                    customer=self.customer,
                )
Skia's avatar
Skia committed
661
                s.save()
Sli's avatar
Sli committed
662
663
664
665
666
667
668
669
670
671
672
                if infos["bonus_qty"]:
                    s = Selling(
                        label=p.name + " (Plateau)",
                        product=p,
                        club=p.club,
                        counter=self.object,
                        unit_price=0,
                        quantity=infos["bonus_qty"],
                        seller=self.operator,
                        customer=self.customer,
                    )
Skia's avatar
Skia committed
673
                    s.save()
Sli's avatar
Sli committed
674
675
                self.customer.recorded_products -= self.compute_record_product(request)
                self.customer.save()
Sli's avatar
Sli committed
676
677
678
679
            request.session["last_customer"] = self.customer.user.get_display_name()
            request.session["last_total"] = "%0.2f" % self.sum_basket(request)
            request.session["new_customer_amount"] = str(self.customer.amount)
            del request.session["basket"]
Skia's avatar
Skia committed
680
            request.session.modified = True
Sli's avatar
Sli committed
681
682
683
684
            kwargs = {"counter_id": self.object.id}
            return HttpResponseRedirect(
                reverse_lazy("counter:details", args=self.args, kwargs=kwargs)
            )
685
686
687

    def cancel(self, request):
        """ Cancel the click session """
Sli's avatar
Sli committed
688
689
690
691
692
        kwargs = {"counter_id": self.object.id}
        request.session.pop("basket", None)
        return HttpResponseRedirect(
            reverse_lazy("counter:details", args=self.args, kwargs=kwargs)
        )
693

Skia's avatar
Skia committed
694
695
    def refill(self, request):
        """Refill the customer's account"""
Sli's avatar
Sli committed
696
        if self.get_object().type == "BAR":
Sli's avatar
Sli committed
697
698
699
700
701
702
703
704
            form = RefillForm(request.POST)
            if form.is_valid():
                form.instance.counter = self.object
                form.instance.operator = self.operator
                form.instance.customer = self.customer
                form.instance.save()
            else:
                self.refill_form = form
705
        else:
Sli's avatar
Sli committed
706
            raise PermissionDenied
Skia's avatar
Skia committed
707

708
    def get_context_data(self, **kwargs):
709
        """ Add customer to the context """
710
        kwargs = super(CounterClick, self).get_context_data(**kwargs)
Sli's avatar
Sli committed
711
712
713
714
        kwargs["customer"] = self.customer
        kwargs["basket_total"] = self.sum_basket(self.request)
        kwargs["refill_form"] = self.refill_form or RefillForm()
        kwargs["categories"] = ProductType.objects.all()
Sli's avatar
Sli committed
715
        kwargs["student_card_max_uid_size"] = StudentCard.UID_SIZE
716
717
        return kwargs

Krophil's avatar
Krophil committed
718

Skia's avatar
Skia committed
719
class CounterLogin(RedirectView):
Skia's avatar
Skia committed
720
721
722
    """
    Handle the login of a barman

723
    Logged barmen are stored in the Permanency model
Skia's avatar
Skia committed
724
    """
Sli's avatar
Sli committed
725

Skia's avatar
Skia committed
726
    permanent = False
Krophil's avatar
Krophil committed
727

Skia's avatar
Skia committed
728
    def post(self, request, *args, **kwargs):
Skia's avatar
Skia committed
729
730
731
        """
        Register the logged user as barman for this counter
        """
Sli's avatar
Sli committed
732
733
        self.counter_id = kwargs["counter_id"]
        self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
Skia's avatar
Skia committed
734
        form = LoginForm(request, data=request.POST)
Skia's avatar
Skia committed
735
        self.errors = []
Skia's avatar
Skia committed
736
        if form.is_valid():
Sli's avatar
Sli committed
737
738
739
740
741
            user = User.objects.filter(username=form.cleaned_data["username"]).first()
            if (
                user in self.counter.sellers.all()
                and not user in self.counter.get_barmen_list()
            ):
742
743
                if len(self.counter.get_barmen_list()) <= 0:
                    self.counter.gen_token()
Sli's avatar
Sli committed
744
                request.session["counter_token"] = self.counter.token
745
                self.counter.add_barman(user)
Skia's avatar
Skia committed
746
            else:
747
                self.errors += ["sellers"]
Skia's avatar
Skia committed
748
        else:
Skia's avatar
Skia committed
749
            self.errors += ["credentials"]
Skia's avatar
Skia committed
750
751
752
        return super(CounterLogin, self).post(request, *args, **kwargs)

    def get_redirect_url(self, *args, **kwargs):
Sli's avatar
Sli committed
753
754
755
756
757
        return (
            reverse_lazy("counter:details", args=args, kwargs=kwargs)
            + "?"
            + "&".join(self.errors)
        )
Krophil's avatar
Krophil committed
758

Skia's avatar
Skia committed
759
760
761

class CounterLogout(RedirectView):
    permanent = False
Krophil's avatar
Krophil committed
762

Skia's avatar
Skia committed
763
    def post(self, request, *args, **kwargs):
Skia's avatar
Skia committed
764
765
766
        """
        Unregister the user from the barman
        """
Sli's avatar
Sli committed
767
768
        self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
        user = User.objects.filter(id=request.POST["user_id"]).first()
769
        self.counter.del_barman(user)
Skia's avatar
Skia committed
770
771
772
        return super(CounterLogout, self).post(request, *args, **kwargs)

    def get_redirect_url(self, *args, **kwargs):
Sli's avatar
Sli committed
773
774
        return reverse_lazy("counter:details", args=args, kwargs=kwargs)

Skia's avatar
Skia committed
775

Krophil's avatar
Krophil committed
776
777
# Counter admin views

778

Skia's avatar
Skia committed
779
class CounterAdminTabsMixin(TabedViewMixin):
780
781
    tabs_title = _("Counter administration")
    list_of_tabs = [
Sli's avatar
Sli committed
782
        {"url": reverse_lazy("stock:list"), "slug": "stocks", "name": _("Stocks")},
Krophil's avatar
Krophil committed
783
        {
Sli's avatar
Sli committed
784
785
786
            "url": reverse_lazy("counter:admin_list"),
            "slug": "counters",
            "name": _("Counters"),
Krophil's avatar
Krophil committed
787
788
        },
        {
Sli's avatar
Sli committed
789
790
791
            "url": reverse_lazy("counter:product_list"),
            "slug": "products",
            "name": _("Products"),
Krophil's avatar
Krophil committed
792
793
        },
        {
Sli's avatar
Sli committed
794
795
796
            "url": reverse_lazy("counter:product_list_archived"),
            "slug": "archive",
            "name": _("Archived products"),
Krophil's avatar
Krophil committed
797
798
        },
        {
Sli's avatar
Sli committed
799
800
801
            "url": reverse_lazy("counter:producttype_list"),
            "slug": "product_types",
            "name": _("Product types"),
Krophil's avatar
Krophil committed
802
803
        },
        {
Sli's avatar
Sli committed
804
805
806
            "url": reverse_lazy("counter:cash_summary_list"),
            "slug": "cash_summary",
            "name": _("Cash register summaries"),
Krophil's avatar
Krophil committed
807
808
        },
        {
Sli's avatar
Sli committed
809
810
811
            "url": reverse_lazy("counter:invoices_call"),
            "slug": "invoices_call",
            "name": _("Invoices call"),
Krophil's avatar
Krophil committed
812
813
        },
        {
Sli's avatar
Sli committed
814
815
816
            "url": reverse_lazy("counter:eticket_list"),
            "slug": "etickets",
            "name": _("Etickets"),
Krophil's avatar
Krophil committed
817
818
819
        },
    ]

820

Skia's avatar
Skia committed
821
class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
Skia's avatar
Skia committed
822
823
824
    """
    A list view for the admins
    """
Sli's avatar
Sli committed
825

Skia's avatar
Skia committed
826
    model = Counter
Sli's avatar
Sli committed
827
    template_name = "counter/counter_list.jinja"
828
    current_tab = "counters"
Skia's avatar
Skia committed
829

Krophil's avatar
Krophil committed
830

831
832
833
class CounterEditForm(forms.ModelForm):
    class Meta:
        model = Counter
Sli's avatar
Sli committed
834
835
836
837
        fields = ["sellers", "products"]

    sellers = make_ajax_field(Counter, "sellers", "users", help_text="")
    products = make_ajax_field(Counter, "products", "products", help_text="")
838

Krophil's avatar
Krophil committed
839

Sli's avatar
Sli committed
840
class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
841
842
843
    """
    Edit a counter's main informations (for the counter's manager)
    """
Sli's avatar
Sli committed
844

845
846
847
    model = Counter
    form_class = CounterEditForm
    pk_url_kwarg = "counter_id"
Sli's avatar
Sli committed
848
    template_name = "core/edit.jinja"
849
    current_tab = "counters"
850

Sli's avatar
Sli committed
851
852
    def dispatch(self, request, *args, **kwargs):
        obj = self.get_object()
Sli's avatar
Sli committed
853
        self.edit_club.append(obj.club)
Sli's avatar
Sli committed
854
855
        return super(CounterEditView, self).dispatch(request, *args, **kwargs)

856
    def get_success_url(self):
Sli's avatar
Sli committed
857
        return reverse_lazy("counter:admin", kwargs={"counter_id": self.object.id})
858

Krophil's avatar
Krophil committed
859

Sli's avatar
Sli committed
860
class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
Skia's avatar
Skia committed
861
    """
Skia's avatar
Skia committed
862
    Edit a counter's main informations (for the counter's admin)
Skia's avatar
Skia committed
863
    """
Sli's avatar
Sli committed
864

Skia's avatar
Skia committed
865
    model = Counter
Sli's avatar
Sli committed
866
    form_class = modelform_factory(Counter, fields=["name", "club", "type"])
Skia's avatar
Skia committed
867
    pk_url_kwarg = "counter_id"
Sli's avatar
Sli committed
868
    template_name = "core/edit.jinja"
869
    current_tab = "counters"
Skia's avatar
Skia committed
870

Krophil's avatar
Krophil committed
871

Sli's avatar
Sli committed
872
class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
Skia's avatar
Skia committed
873
    """
Skia's avatar
Skia committed
874
    Create a counter (for the admins)
Skia's avatar
Skia committed
875
    """
Sli's avatar
Sli committed
876

Skia's avatar
Skia committed
877
    model = Counter
Sli's avatar
Sli committed
878
879
880
881
882
883
    form_class = modelform_factory(
        Counter,
        fields=["name", "club", "type", "products"],
        widgets={"products": CheckboxSelectMultiple},
    )
    template_name = "core/create.jinja"
884
    current_tab = "counters"
Skia's avatar
Skia committed
885

Krophil's avatar
Krophil committed
886

Sli's avatar
Sli committed
887
class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView):
Skia's avatar
Skia committed
888
    """
Skia's avatar
Skia committed
889
    Delete a counter (for the admins)
Skia's avatar
Skia committed
890
    """
Sli's avatar
Sli committed
891

Skia's avatar
Skia committed
892
893
    model = Counter
    pk_url_kwarg = "counter_id"
Sli's avatar
Sli committed
894
895
    template_name = "core/delete_confirm.jinja"
    success_url = reverse_lazy("counter:admin_list")
896
    current_tab = "counters"
Skia's avatar
Skia committed
897

Sli's avatar
Sli committed
898

Skia's avatar
Skia committed
899
900
# Product management

Krophil's avatar
Krophil committed
901

Sli's avatar
Sli committed
902
class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
Skia's avatar
Skia committed
903
904
905
    """
    A list view for the admins
    """
Sli's avatar
Sli committed
906

Skia's avatar
Skia committed
907
    model = ProductType
Sli's avatar
Sli committed
908
    template_name = "counter/producttype_list.jinja"
909
    current_tab = "product_types"
Skia's avatar
Skia committed
910

Krophil's avatar
Krophil committed
911

Sli's avatar
Sli committed
912
class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
Skia's avatar
Skia committed
913
914
915
    """
    A create view for the admins
    """
Sli's avatar
Sli committed
916

Skia's avatar
Skia committed
917
    model = ProductType
Sli's avatar
Sli committed
918
919
    fields = ["name", "description", "comment", "icon"]
    template_name = "core/create.jinja"
920
    current_tab = "products"
Skia's avatar
Skia committed
921

Krophil's avatar
Krophil committed
922

Sli's avatar
Sli committed
923
class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
Skia's avatar
Skia committed
924
925
926
    """
    An edit view for the admins
    """
Sli's avatar
Sli committed
927

Skia's avatar
Skia committed
928
    model = ProductType
Sli's avatar
Sli committed
929
930
    template_name = "core/edit.jinja"
    fields = ["name", "description", "comment", "icon"]
Skia's avatar
Skia committed
931
    pk_url_kwarg = "type_id"
932
    current_tab = "products"
Skia's avatar
Skia committed
933

Krophil's avatar
Krophil committed
934

Sli's avatar
Sli committed
935
class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
Skia's avatar
Skia committed
936
937
938
    """
    A list view for the admins
    """
Sli's avatar
Sli committed
939

Skia's avatar
Skia committed
940
    model = Product
Sli's avatar
Sli committed
941
    template_name = "counter/product_list.jinja"
Skia's avatar
Skia committed
942
    queryset = Product.objects.filter(archived=True)
Sli's avatar
Sli committed
943
    ordering = ["name"]
944
    current_tab = "archive"
Skia's avatar
Skia committed
945

Krophil's avatar
Krophil committed
946

Sli's avatar
Sli committed
947
class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
Skia's avatar
Skia committed
948
949
950
    """
    A list view for the admins
    """
Sli's avatar
Sli committed
951

Skia's avatar
Skia committed
952
    model = Product
Sli's avatar
Sli committed
953
    template_name = "counter/product_list.jinja"
Skia's avatar
Skia committed
954
    queryset = Product.objects.filter(archived=False)
Sli's avatar
Sli committed
955
    ordering = ["name"]
956
    current_tab = "products"
Skia's avatar
Skia committed
957

Krophil's avatar
Krophil committed
958

959
960
961
class ProductEditForm(forms.ModelForm):
    class Meta:
        model = Product
Sli's avatar
Sli committed
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
        fields = [
            "name",
            "description",
            "product_type",
            "code",
            "parent_product",
            "buying_groups",
            "purchase_price",
            "selling_price",
            "special_selling_price",
            "icon",
            "club",
            "limit_age",
            "tray",
            "archived",
        ]

    parent_product = AutoCompleteSelectField(
        "products", show_help_text=False, label=_("Parent product"), required=False
    )
    buying_groups = AutoCompleteSelectMultipleField(
        "groups",
        show_help_text=False,
        help_text="",
        label=_("Buying groups"),
        required=False,
    )
    club = AutoCompleteSelectField("clubs", show_help_text=False)
    counters = AutoCompleteSelectMultipleField(
        "counters",
        show_help_text=False,
        help_text="",
        label=_