Sith/counter/views/click.py

338 lines
12 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 math
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import F
from django.forms import (
BaseFormSet,
Form,
IntegerField,
ValidationError,
formset_factory,
)
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, resolve_url
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin
from core.models import User
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
def get_operator(counter: Counter, customer: Customer) -> User:
if counter.customer_is_barman(customer):
return customer.user
return counter.get_random_barman()
class ProductForm(Form):
quantity = IntegerField(min_value=1)
id = IntegerField(min_value=0)
def __init__(
self,
*args,
customer: Customer | None = None,
counter: Counter | None = None,
**kwargs,
):
self.customer = customer
self.counter = counter
super().__init__(*args, **kwargs)
def clean(self):
cleaned_data = super().clean()
if len(self.errors) > 0:
return
if self.customer is None or self.counter is None:
raise RuntimeError(
f"{self} has been initialized without customer or counter"
)
user = self.customer.user
# We store self.product so we can use it later on the formset validation
self.product = self.counter.products.filter(id=cleaned_data["id"]).first()
if self.product is None:
raise ValidationError(
_(
"Product %(product)s doesn't exist or isn't available on this counter"
)
% {"product": cleaned_data["id"]}
)
# Test alcohoolic products
if self.product.limit_age >= 18:
if not user.date_of_birth:
raise ValidationError(_("Too young for that product"))
if user.is_banned_alcohol:
raise ValidationError(_("Not allowed for that product"))
if user.date_of_birth and self.customer.user.get_age() < self.product.limit_age:
raise ValidationError(_("Too young for that product"))
if user.is_banned_counter:
raise ValidationError(_("Not allowed for that product"))
# Compute prices
cleaned_data["bonus_quantity"] = 0
if self.product.tray:
cleaned_data["bonus_quantity"] = math.floor(
cleaned_data["quantity"] / Product.QUANTITY_FOR_TRAY_PRICE
)
cleaned_data["unit_price"] = self.product.get_actual_price(
self.counter, self.customer
)
cleaned_data["total_price"] = cleaned_data["unit_price"] * (
cleaned_data["quantity"] - cleaned_data["bonus_quantity"]
)
return cleaned_data
class BaseBasketForm(BaseFormSet):
def clean(self):
super().clean()
if len(self) == 0:
return
self._check_forms_have_errors()
self._check_recorded_products(self[0].customer)
self._check_enough_money(self[0].counter, self[0].customer)
def _check_forms_have_errors(self):
for form in self:
if len(form.errors):
raise ValidationError(_("Submmited basket is invalid"))
def _check_enough_money(self, counter: Counter, customer: Customer):
self.total_price = sum([data["total_price"] for data in self.cleaned_data])
if self.total_price > customer.amount:
raise ValidationError(_("Not enough money"))
def _check_recorded_products(self, customer: Customer):
"""Check for, among other things, ecocups and pitchers"""
self.total_recordings = 0
for form in self:
# form.product is stored by the clean step of each formset form
if form.product.is_record_product:
self.total_recordings -= form.cleaned_data["quantity"]
if form.product.is_unrecord_product:
self.total_recordings += form.cleaned_data["quantity"]
if not customer.can_record_more(self.total_recordings):
raise ValidationError(_("This user have reached his recording limit"))
BasketForm = formset_factory(
ProductForm, formset=BaseBasketForm, absolute_max=None, min_num=1
)
class CounterClick(CounterTabsMixin, CanViewMixin, SingleObjectMixin, FormView):
"""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()
form_class = BasketForm
template_name = "counter/counter_click.jinja"
pk_url_kwarg = "counter_id"
current_tab = "counter"
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["form_kwargs"] = {
"customer": self.customer,
"counter": self.object,
}
return 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) # Redirect to counter
return super().dispatch(request, *args, **kwargs)
def form_valid(self, formset):
ret = super().form_valid(formset)
if len(formset) == 0:
return ret
operator = get_operator(self.object, self.customer)
with transaction.atomic():
self.request.session["last_basket"] = []
for form in formset:
self.request.session["last_basket"].append(
f"{form.cleaned_data['quantity']} x {form.product.name}"
)
Selling(
label=form.product.name,
product=form.product,
club=form.product.club,
counter=self.object,
unit_price=form.cleaned_data["unit_price"],
quantity=form.cleaned_data["quantity"]
- form.cleaned_data["bonus_quantity"],
seller=operator,
customer=self.customer,
).save()
if form.cleaned_data["bonus_quantity"] > 0:
Selling(
label=f"{form.product.name} (Plateau)",
product=form.product,
club=form.product.club,
counter=self.object,
unit_price=0,
quantity=form.cleaned_data["bonus_quantity"],
seller=operator,
customer=self.customer,
).save()
self.customer.recorded_products -= formset.total_recordings
self.customer.save()
# Add some info for the main counter view to display
self.request.session["last_customer"] = self.customer.user.get_display_name()
self.request.session["last_total"] = f"{formset.total_price:0.2f}"
self.request.session["new_customer_amount"] = str(self.customer.amount)
return ret
def get_success_url(self):
return resolve_url(self.object)
def get_product(self, pid):
return Product.objects.filter(pk=int(pid)).first()
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")
# Optimisation to bulk edit prices instead of `calling get_actual_price` on everything
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
# To get all forms errors to the javascript, we create a list of error list
kwargs["form_errors"] = []
for field_errors in kwargs["form"].errors:
kwargs["form_errors"].append(list(field_errors.values()))
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
self.operator = get_operator(self.counter, self.customer)
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