mirror of
https://github.com/ae-utbm/sith.git
synced 2024-12-22 07:41:14 +00:00
469 lines
18 KiB
Python
469 lines
18 KiB
Python
#
|
|
# Copyright 2023 © AE UTBM
|
|
# ae@utbm.fr / ae.info@utbm.fr
|
|
#
|
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
|
# https://ae.utbm.fr.
|
|
#
|
|
# You can find the source code of the website at https://github.com/ae-utbm/sith
|
|
#
|
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
|
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
|
#
|
|
#
|
|
import re
|
|
from http import HTTPStatus
|
|
from typing import TYPE_CHECKING
|
|
from urllib.parse import parse_qs
|
|
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.db import DataError, transaction
|
|
from django.db.models import F
|
|
from django.http import Http404, HttpResponseRedirect, JsonResponse
|
|
from django.shortcuts import get_object_or_404, redirect
|
|
from django.urls import reverse_lazy
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.views.generic import DetailView, FormView
|
|
|
|
from core.utils import FormFragmentTemplateData
|
|
from core.views import CanViewMixin
|
|
from counter.forms import RefillForm
|
|
from counter.models import Counter, Customer, Product, Selling
|
|
from counter.utils import is_logged_in_counter
|
|
from counter.views.mixins import CounterTabsMixin
|
|
from counter.views.student_card import StudentCardFormView
|
|
|
|
if TYPE_CHECKING:
|
|
from core.models import User
|
|
|
|
|
|
class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|
"""The click view
|
|
This is a detail view not to have to worry about loading the counter
|
|
Everything is made by hand in the post method.
|
|
"""
|
|
|
|
model = Counter
|
|
queryset = Counter.objects.annotate_is_open()
|
|
template_name = "counter/counter_click.jinja"
|
|
pk_url_kwarg = "counter_id"
|
|
current_tab = "counter"
|
|
|
|
def render_to_response(self, *args, **kwargs):
|
|
if self.is_ajax(self.request):
|
|
response = {"errors": []}
|
|
status = HTTPStatus.OK
|
|
|
|
if self.request.session["too_young"]:
|
|
response["errors"].append(_("Too young for that product"))
|
|
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
|
|
if self.request.session["not_allowed"]:
|
|
response["errors"].append(_("Not allowed for that product"))
|
|
status = HTTPStatus.FORBIDDEN
|
|
if self.request.session["no_age"]:
|
|
response["errors"].append(_("No date of birth provided"))
|
|
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
|
|
if self.request.session["not_enough"]:
|
|
response["errors"].append(_("Not enough money"))
|
|
status = HTTPStatus.PAYMENT_REQUIRED
|
|
|
|
if len(response["errors"]) > 1:
|
|
status = HTTPStatus.BAD_REQUEST
|
|
|
|
response["basket"] = self.request.session["basket"]
|
|
|
|
return JsonResponse(response, status=status)
|
|
|
|
else: # Standard HTML page
|
|
return super().render_to_response(*args, **kwargs)
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
|
|
obj: Counter = self.get_object()
|
|
if not self.customer.can_buy:
|
|
raise Http404
|
|
if obj.type != "BAR" and not request.user.is_authenticated:
|
|
raise PermissionDenied
|
|
if obj.type == "BAR" and (
|
|
"counter_token" not in request.session
|
|
or request.session["counter_token"] != obj.token
|
|
or len(obj.barmen_list) == 0
|
|
):
|
|
return redirect(obj)
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
"""Simple get view."""
|
|
if "basket" not in request.session: # 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
|
|
ret = super().get(request, *args, **kwargs)
|
|
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
|
self.object.type == "BAR" and len(self.object.barmen_list) == 0
|
|
): # Check that at least one barman is logged in
|
|
ret = self.cancel(request) # Otherwise, go to main view
|
|
return ret
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
"""Handle the many possibilities of the post request."""
|
|
self.object = self.get_object()
|
|
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
|
self.object.type == "BAR" and len(self.object.barmen_list) < 1
|
|
): # Check that at least one barman is logged in
|
|
return self.cancel(request)
|
|
if self.object.type == "BAR" and not (
|
|
"counter_token" in self.request.session
|
|
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:
|
|
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
|
|
if self.object.type != "BAR":
|
|
self.operator = request.user
|
|
elif self.object.customer_is_barman(self.customer):
|
|
self.operator = self.customer.user
|
|
else:
|
|
self.operator = self.object.get_random_barman()
|
|
action = self.request.POST.get("action", None)
|
|
if action is None:
|
|
action = parse_qs(request.body.decode()).get("action", [""])[0]
|
|
if action == "add_product":
|
|
self.add_product(request)
|
|
elif action == "del_product":
|
|
self.del_product(request)
|
|
elif action == "code":
|
|
return self.parse_code(request)
|
|
elif action == "cancel":
|
|
return self.cancel(request)
|
|
elif action == "finish":
|
|
return self.finish(request)
|
|
context = self.get_context_data(object=self.object)
|
|
return self.render_to_response(context)
|
|
|
|
def get_product(self, pid):
|
|
return Product.objects.filter(pk=int(pid)).first()
|
|
|
|
def get_price(self, pid):
|
|
p = self.get_product(pid)
|
|
if self.object.customer_is_barman(self.customer):
|
|
price = p.special_selling_price
|
|
else:
|
|
price = p.selling_price
|
|
return price
|
|
|
|
def sum_basket(self, request):
|
|
total = 0
|
|
for infos in request.session["basket"].values():
|
|
total += infos["price"] * infos["qty"]
|
|
return total / 100
|
|
|
|
def get_total_quantity_for_pid(self, request, pid):
|
|
pid = str(pid)
|
|
if pid not in request.session["basket"]:
|
|
return 0
|
|
return (
|
|
request.session["basket"][pid]["qty"]
|
|
+ request.session["basket"][pid]["bonus_qty"]
|
|
)
|
|
|
|
def compute_record_product(self, request, product=None):
|
|
recorded = 0
|
|
basket = request.session["basket"]
|
|
|
|
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:
|
|
recorded -= basket[p]["qty"]
|
|
elif bproduct.is_unrecord_product:
|
|
recorded += basket[p]["qty"]
|
|
return recorded
|
|
|
|
def is_record_product_ok(self, request, product):
|
|
return self.customer.can_record_more(
|
|
self.compute_record_product(request, product)
|
|
)
|
|
|
|
@staticmethod
|
|
def is_ajax(request):
|
|
# when using the fetch API, the django request.POST dict is empty
|
|
# this is but a wretched contrivance which strive to replace
|
|
# the deprecated django is_ajax() method
|
|
# and which must be replaced as soon as possible
|
|
# by a proper separation between the api endpoints of the counter
|
|
return len(request.POST) == 0 and len(request.body) != 0
|
|
|
|
def add_product(self, request, q=1, p=None):
|
|
"""Add a product to the basket
|
|
q is the quantity passed as integer
|
|
p is the product id, passed as an integer.
|
|
"""
|
|
pid = p or parse_qs(request.body.decode())["product_id"][0]
|
|
pid = str(pid)
|
|
price = self.get_price(pid)
|
|
total = self.sum_basket(request)
|
|
product: Product = self.get_product(pid)
|
|
user: User = self.customer.user
|
|
buying_groups = list(product.buying_groups.values_list("pk", flat=True))
|
|
can_buy = len(buying_groups) == 0 or any(
|
|
user.is_in_group(pk=group_id) for group_id in buying_groups
|
|
)
|
|
if not can_buy:
|
|
request.session["not_allowed"] = True
|
|
return False
|
|
bq = 0 # Bonus quantity, for trays
|
|
if (
|
|
product.tray
|
|
): # Handle the tray to adjust the quantity q to add and the bonus quantity bq
|
|
total_qty_mod_6 = self.get_total_quantity_for_pid(request, pid) % 6
|
|
bq = int((total_qty_mod_6 + q) / 6) # Integer division
|
|
q -= bq
|
|
if self.customer.amount < (
|
|
total + round(q * float(price), 2)
|
|
): # Check for enough money
|
|
request.session["not_enough"] = True
|
|
return False
|
|
if product.is_unrecord_product and not self.is_record_product_ok(
|
|
request, product
|
|
):
|
|
request.session["not_allowed"] = True
|
|
return False
|
|
if product.limit_age >= 18 and not user.date_of_birth:
|
|
request.session["no_age"] = True
|
|
return False
|
|
if product.limit_age >= 18 and user.is_banned_alcohol:
|
|
request.session["not_allowed"] = True
|
|
return False
|
|
if user.is_banned_counter:
|
|
request.session["not_allowed"] = True
|
|
return False
|
|
if (
|
|
user.date_of_birth and self.customer.user.get_age() < product.limit_age
|
|
): # Check if affordable
|
|
request.session["too_young"] = True
|
|
return False
|
|
if pid in request.session["basket"]: # Add if already in basket
|
|
request.session["basket"][pid]["qty"] += q
|
|
request.session["basket"][pid]["bonus_qty"] += bq
|
|
else: # or create if not
|
|
request.session["basket"][pid] = {
|
|
"qty": q,
|
|
"price": int(price * 100),
|
|
"bonus_qty": bq,
|
|
}
|
|
request.session.modified = True
|
|
return True
|
|
|
|
def del_product(self, request):
|
|
"""Delete a product from the basket."""
|
|
pid = parse_qs(request.body.decode())["product_id"][0]
|
|
product = self.get_product(pid)
|
|
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
|
|
else:
|
|
request.session["basket"][pid]["qty"] -= 1
|
|
if request.session["basket"][pid]["qty"] <= 0:
|
|
del request.session["basket"][pid]
|
|
request.session.modified = True
|
|
|
|
def parse_code(self, request):
|
|
"""Parse the string entered by the barman.
|
|
|
|
This can be of two forms :
|
|
- `<str>`, where the string is the code of the product
|
|
- `<int>X<str>`, where the integer is the quantity and str the code.
|
|
"""
|
|
string = parse_qs(request.body.decode()).get("code", [""])[0].upper()
|
|
if string == "FIN":
|
|
return self.finish(request)
|
|
elif string == "ANN":
|
|
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:
|
|
nb = m.group("nb")
|
|
code = m.group("code")
|
|
nb = int(nb) if nb is not None else 1
|
|
p = self.object.products.filter(code=code).first()
|
|
if p is not None:
|
|
self.add_product(request, nb, p.id)
|
|
context = self.get_context_data(object=self.object)
|
|
return self.render_to_response(context)
|
|
|
|
def finish(self, request):
|
|
"""Finish the click session, and validate the basket."""
|
|
with transaction.atomic():
|
|
request.session["last_basket"] = []
|
|
if self.sum_basket(request) > self.customer.amount:
|
|
raise DataError(_("You have not enough money to buy all the basket"))
|
|
|
|
for pid, infos in request.session["basket"].items():
|
|
# This duplicates code for DB optimization (prevent to load many times the same object)
|
|
p = Product.objects.filter(pk=pid).first()
|
|
if self.object.customer_is_barman(self.customer):
|
|
uprice = p.special_selling_price
|
|
else:
|
|
uprice = p.selling_price
|
|
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,
|
|
)
|
|
s.save()
|
|
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,
|
|
)
|
|
s.save()
|
|
self.customer.recorded_products -= self.compute_record_product(request)
|
|
self.customer.save()
|
|
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"]
|
|
request.session.modified = True
|
|
kwargs = {"counter_id": self.object.id}
|
|
return HttpResponseRedirect(
|
|
reverse_lazy("counter:details", args=self.args, kwargs=kwargs)
|
|
)
|
|
|
|
def cancel(self, request):
|
|
"""Cancel the click session."""
|
|
kwargs = {"counter_id": self.object.id}
|
|
request.session.pop("basket", None)
|
|
return HttpResponseRedirect(
|
|
reverse_lazy("counter:details", args=self.args, kwargs=kwargs)
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add customer to the context."""
|
|
kwargs = super().get_context_data(**kwargs)
|
|
products = self.object.products.select_related("product_type")
|
|
if self.object.customer_is_barman(self.customer):
|
|
products = products.annotate(price=F("special_selling_price"))
|
|
else:
|
|
products = products.annotate(price=F("selling_price"))
|
|
kwargs["products"] = products
|
|
kwargs["categories"] = {}
|
|
for product in kwargs["products"]:
|
|
if product.product_type:
|
|
kwargs["categories"].setdefault(product.product_type, []).append(
|
|
product
|
|
)
|
|
kwargs["customer"] = self.customer
|
|
kwargs["basket_total"] = self.sum_basket(self.request)
|
|
|
|
if self.object.type == "BAR":
|
|
kwargs["student_card_fragment"] = StudentCardFormView.get_template_data(
|
|
self.customer
|
|
).render(self.request)
|
|
|
|
if self.object.can_refill():
|
|
kwargs["refilling_fragment"] = RefillingCreateView.get_template_data(
|
|
self.customer
|
|
).render(self.request)
|
|
return kwargs
|
|
|
|
|
|
class RefillingCreateView(FormView):
|
|
"""This is a fragment only view which integrates with counter_click.jinja"""
|
|
|
|
form_class = RefillForm
|
|
template_name = "counter/fragments/create_refill.jinja"
|
|
|
|
@classmethod
|
|
def get_template_data(
|
|
cls, customer: Customer, *, form_instance: form_class | None = None
|
|
) -> FormFragmentTemplateData[form_class]:
|
|
return FormFragmentTemplateData(
|
|
form=form_instance if form_instance else cls.form_class(),
|
|
template=cls.template_name,
|
|
context={
|
|
"action": reverse_lazy(
|
|
"counter:refilling_create", kwargs={"customer_id": customer.pk}
|
|
),
|
|
},
|
|
)
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.customer: Customer = get_object_or_404(Customer, pk=kwargs["customer_id"])
|
|
if not self.customer.can_buy:
|
|
raise Http404
|
|
|
|
if not is_logged_in_counter(request):
|
|
raise PermissionDenied
|
|
|
|
self.counter: Counter = get_object_or_404(
|
|
Counter, token=request.session["counter_token"]
|
|
)
|
|
|
|
if not self.counter.can_refill():
|
|
raise PermissionDenied
|
|
|
|
if self.counter.customer_is_barman(self.customer):
|
|
self.operator = self.customer.user
|
|
else:
|
|
self.operator = self.counter.get_random_barman()
|
|
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def form_valid(self, form):
|
|
res = super().form_valid(form)
|
|
form.clean()
|
|
form.instance.counter = self.counter
|
|
form.instance.operator = self.operator
|
|
form.instance.customer = self.customer
|
|
form.instance.save()
|
|
return res
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
data = self.get_template_data(self.customer, form_instance=context["form"])
|
|
context.update(data.context)
|
|
return context
|
|
|
|
def get_success_url(self, **kwargs):
|
|
return self.request.path
|