views.py 65 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
40
from django.urls 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):
Sli's avatar
Sli committed
118
            raise forms.ValidationError(_("This UID is invalid"), code="invalid")
119 120 121
        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"):
Lo-J's avatar
Lo-J committed
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
    form_class = (
Sli's avatar
Sli committed
281 282
        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()
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
                raise PermissionDenied
        else:
375
            if not request.user.is_authenticated:
Sli's avatar
Sli committed
376
                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)
390
        if (self.object.type != "BAR" and not request.user.is_authenticated) or (
Sli's avatar
Sli committed
391 392
            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
400
        if (self.object.type != "BAR" and not request.user.is_authenticated) or (
Sli's avatar
Sli committed
401 402
            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)
711 712 713 714 715 716 717
        kwargs["products"] = self.object.products.select_related("product_type")
        kwargs["categories"] = {}
        for product in kwargs["products"]:
            if product.product_type:
                kwargs["categories"].setdefault(product.product_type, []).append(
                    product
                )
Sli's avatar
Sli committed
718 719 720
        kwargs["customer"] = self.customer
        kwargs["basket_total"] = self.sum_basket(self.request)
        kwargs["refill_form"] = self.refill_form or RefillForm()
Sli's avatar
Sli committed
721
        kwargs["student_card_max_uid_size"] = StudentCard.UID_SIZE
722 723
        return kwargs

Krophil's avatar
Krophil committed
724

Skia's avatar
Skia committed
725
class CounterLogin(RedirectView):
Skia's avatar
Skia committed
726 727 728
    """
    Handle the login of a barman

729
    Logged barmen are stored in the Permanency model
Skia's avatar
Skia committed
730
    """
Sli's avatar
Sli committed
731

Skia's avatar
Skia committed
732
    permanent = False
Krophil's avatar
Krophil committed
733

Skia's avatar
Skia committed
734
    def post(self, request, *args, **kwargs):
Skia's avatar
Skia committed
735 736 737
        """
        Register the logged user as barman for this counter
        """
Sli's avatar
Sli committed
738 739
        self.counter_id = kwargs["counter_id"]
        self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
Skia's avatar
Skia committed
740
        form = LoginForm(request, data=request.POST)
Skia's avatar
Skia committed
741
        self.errors = []
Skia's avatar
Skia committed
742
        if form.is_valid():
Sli's avatar
Sli committed
743 744 745 746 747
            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()
            ):
748 749
                if len(self.counter.get_barmen_list()) <= 0:
                    self.counter.gen_token()
Sli's avatar
Sli committed
750
                request.session["counter_token"] = self.counter.token
751
                self.counter.add_barman(user)
Skia's avatar
Skia committed
752
            else:
753
                self.errors += ["sellers"]
Skia's avatar
Skia committed
754
        else:
Skia's avatar
Skia committed
755
            self.errors += ["credentials"]
Skia's avatar
Skia committed
756 757 758
        return super(CounterLogin, self).post(request, *args, **kwargs)

    def get_redirect_url(self, *args, **kwargs):
Sli's avatar
Sli committed
759 760 761 762 763
        return (
            reverse_lazy("counter:details", args=args, kwargs=kwargs)
            + "?"
            + "&".join(self.errors)
        )
Krophil's avatar
Krophil committed
764

Skia's avatar
Skia committed
765 766 767

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

Skia's avatar
Skia committed
769
    def post(self, request, *args, **kwargs):
Skia's avatar
Skia committed
770 771 772
        """
        Unregister the user from the barman
        """
Sli's avatar
Sli committed
773 774
        self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
        user = User.objects.filter(id=request.POST["user_id"]).first()
775
        self.counter.del_barman(user)
Skia's avatar
Skia committed
776 777 778
        return super(CounterLogout, self).post(request, *args, **kwargs)

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

Skia's avatar
Skia committed
781

Krophil's avatar
Krophil committed
782 783
# Counter admin views

784

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

826

Skia's avatar
Skia committed
827
class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
Skia's avatar
Skia committed
828 829 830
    """
    A list view for the admins
    """
Sli's avatar
Sli committed
831

Skia's avatar
Skia committed
832
    model = Counter
Sli's avatar
Sli committed
833
    template_name = "counter/counter_list.jinja"
834
    current_tab = "counters"
835

Krophil's avatar
Krophil committed
836

837 838 839
class CounterEditForm(forms.ModelForm):
    class Meta:
        model = Counter
Sli's avatar
Sli committed
840 841 842 843
        fields = ["sellers", "products"]

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

Krophil's avatar
Krophil committed
845

Sli's avatar
Sli committed
846
class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
847 848 849
    """
    Edit a counter's main informations (for the counter's manager)
    """
Sli's avatar
Sli committed
850

851 852 853
    model = Counter
    form_class = CounterEditForm
    pk_url_kwarg = "counter_id"
Sli's avatar
Sli committed
854
    template_name = "core/edit.jinja"
855
    current_tab = "counters"
856

Sli's avatar
Sli committed
857 858
    def dispatch(self, request, *args, **kwargs):
        obj = self.get_object()
Sli's avatar
Sli committed
859
        self.edit_club.append(obj.club)
Sli's avatar
Sli committed
860 861
        return super(CounterEditView, self).dispatch(request, *args, **kwargs)

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

Krophil's avatar
Krophil committed
865

Sli's avatar
Sli committed
866
class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
Skia's avatar
Skia committed
867
    """
Skia's avatar
Skia committed
868
    Edit a counter's main informations (for the counter's admin)
Skia's avatar
Skia committed
869
    """
Sli's avatar
Sli committed
870

Skia's avatar
Skia committed
871
    model = Counter
Sli's avatar
Sli committed
872
    form_class = modelform_factory(Counter, fields=["name", "club", "type"])
Skia's avatar
Skia committed
873
    pk_url_kwarg = "counter_id"
Sli's avatar
Sli committed
874
    template_name = "core/edit.jinja"
875
    current_tab = "counters"
Skia's avatar
Skia committed
876

Krophil's avatar
Krophil committed
877

Sli's avatar
Sli committed
878
class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
Skia's avatar
Skia committed
879
    """
Skia's avatar
Skia committed
880
    Create a counter (for the admins)
Skia's avatar
Skia committed
881
    """
Sli's avatar
Sli committed
882

Skia's avatar
Skia committed
883
    model = Counter
Sli's avatar
Sli committed
884 885 886 887 888 889
    form_class = modelform_factory(
        Counter,
        fields=["name", "club", "type", "products"],
        widgets={"products": CheckboxSelectMultiple},
    )
    template_name = "core/create.jinja"
890
    current_tab = "counters"
Skia's avatar
Skia committed
891

Krophil's avatar
Krophil committed
892

Sli's avatar
Sli committed
893
class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView):
Skia's avatar
Skia committed
894
    """
Skia's avatar
Skia committed
895
    Delete a counter (for the admins)
Skia's avatar
Skia committed
896
    """
Sli's avatar
Sli committed
897

Skia's avatar
Skia committed
898 899
    model = Counter
    pk_url_kwarg = "counter_id"
Sli's avatar
Sli committed
900 901
    template_name = "core/delete_confirm.jinja"
    success_url = reverse_lazy("counter:admin_list")
902
    current_tab = "counters"
Skia's avatar
Skia committed
903

Sli's avatar
Sli committed
904

Skia's avatar
Skia committed
905 906
# Product management

Krophil's avatar
Krophil committed
907

Sli's avatar
Sli committed
908
class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
Skia's avatar
Skia committed
909 910 911
    """
    A list view for the admins
    """
Sli's avatar
Sli committed
912

Skia's avatar
Skia committed
913
    model = ProductType
Sli's avatar
Sli committed
914
    template_name = "counter/producttype_list.jinja"
915
    current_tab = "product_types"
Skia's avatar
Skia committed
916

Krophil's avatar
Krophil committed
917

Sli's avatar
Sli committed
918
class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
Skia's avatar
Skia committed
919 920 921
    """
    A create view for the admins
    """
Sli's avatar
Sli committed
922

Skia's avatar
Skia committed
923
    model = ProductType
Sli's avatar
Sli committed
924 925
    fields = ["name", "description", "comment", "icon"]
    template_name = "core/create.jinja"
926
    current_tab = "products"
Skia's avatar
Skia committed
927

Krophil's avatar
Krophil committed
928

Sli's avatar
Sli committed
929
class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
Skia's avatar
Skia committed
930 931 932
    """
    An edit view for the admins
    """
Sli's avatar
Sli committed
933

Skia's avatar
Skia committed
934
    model = ProductType
Sli's avatar
Sli committed
935 936
    template_name = "core/edit.jinja"
    fields = ["name", "description", "comment", "icon"]
Skia's avatar
Skia committed
937
    pk_url_kwarg = "type_id"
938
    current_tab = "products"
Skia's avatar
Skia committed
939

Krophil's avatar
Krophil committed
940

Sli's avatar
Sli committed
941
class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
942 943 944
    """
    A list view for the admins
    """
Sli's avatar
Sli committed
945

946
    model = Product
Sli's avatar
Sli committed
947
    template_name = "counter/product_list.jinja"
948
    queryset = Product.objects.filter(archived=True)
Sli's avatar
Sli committed
949
    ordering = ["name"]
950
    current_tab = "archive"
951

Krophil's avatar
Krophil committed
952

Sli's avatar
Sli committed
953
class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
Skia's avatar
Skia committed
954 955 956
    """
    A list view for the admins
    """
Sli's avatar
Sli committed
957

Skia's avatar
Skia committed
958
    model = Product
Sli's avatar
Sli committed
959
    template_name = "counter/product_list.jinja"
960
    queryset = Product.objects.filter(archived=False)
Sli's avatar
Sli committed
961
    ordering = ["name"]
962
    current_tab = "products"
963

Krophil's avatar
Krophil committed
964

965 966 967
class ProductEditForm(forms.ModelForm):
    class Meta:
        model = Product
Sli's avatar
Sli committed
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
        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"),
993
        required=True,
Sli's avatar
Sli committed
994 995 996 997 998 999 1000 1001 1002
    )
    club = AutoCompleteSelectField("clubs", show_help_text=False)
    counters = AutoCompleteSelectMultipleField(
        "counters",
        show_help_text=False,
        help_text="",
        label=_("Counters"),
        required=False,
    )
1003 1004 1005 1006

    def __init__(self, *args, **kwargs):
        super(ProductEditForm, self).__init__(*args, **kwargs)
        if self.instance.id:
Sli's avatar
Sli committed
1007 1008 1009
            self.fields["counters"].initial = [
                str(c.id) for c in self.instance.counters.all()
            ]
1010 1011 1012

    def save(self, *args, **kwargs):
        ret = super(ProductEditForm, self).save(*args, **kwargs)
Sli's avatar
Sli committed
1013 1014
        if self.fields["counters"].initial:
            for cid in self.fields["counters"].initial:
1015 1016 1017
                c = Counter.objects.filter(id=int(cid)).first()
                c.products.remove(self.instance)
                c.save()
Sli's avatar
Sli committed
1018
        for cid in self.cleaned_data["counters"]:
1019 1020 1021 1022 1023
            c = Counter.objects.filter(id=int(cid)).first()
            c.products.add(self.instance)
            c.save()
        return ret

Krophil's avatar
Krophil committed
1024

Sli's avatar
Sli committed
1025
class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
Skia's avatar
Skia committed
1026 1027 1028
    """
    A create view for the admins
    """
Sli's avatar
Sli committed
1029

Skia's avatar
Skia committed
1030
    model = Product
1031
    form_class = ProductEditForm
Sli's avatar
Sli committed
1032
    template_name = "core/create.jinja"
1033
    current_tab = "products"
Skia's avatar
Skia committed
1034

Krophil's avatar
Krophil committed
1035

Sli's avatar
Sli committed
1036
class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
Skia's avatar
Skia committed
1037 1038 1039
    """
    An edit view for the admins
    """
Sli's avatar
Sli committed
1040

Skia's avatar
Skia committed
1041
    model = Product
1042
    form_class = ProductEditForm
Skia's avatar
Skia committed
1043
    pk_url_kwarg = "product_id"
Sli's avatar
Sli committed
1044
    template_name = "core/edit.jinja"
1045
    current_tab = "products"
Skia's avatar
Skia committed
1046

Krophil's avatar
Krophil committed
1047

Skia's avatar
Skia committed
1048
class RefillingDeleteView(DeleteView):
Skia's avatar
Skia committed
1049 1050 1051
    """
    Delete a refilling (for the admins)
    """