Files
Sith/counter/views/click.py
T
2026-09-07 13:32:13 +02:00

282 lines
10 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 random
from collections import defaultdict
from typing import TYPE_CHECKING
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Q
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, resolve_url
from django.urls import reverse
from django.utils.translation import gettext as _
from django.views.generic import CreateView, FormView
from django.views.generic.detail import SingleObjectMixin
from core.auth.mixins import CanViewMixin
from core.views.mixins import FragmentMixin, UseFragmentsMixin
from counter.forms import BasketForm, RefillForm
from counter.models import (
Counter,
Customer,
ProductFormula,
Refilling,
ReturnableProduct,
Selling,
)
from counter.utils import is_logged_in_counter
from counter.views.mixins import CounterTabsMixin
from counter.views.student_card import StudentCardFormFragment
if TYPE_CHECKING:
from django.utils.safestring import SafeString
from ninja.main import HttpRequest
from core.models import User
def get_operator(request: HttpRequest, counter: Counter, customer: Customer) -> User:
if counter.type != "BAR":
return request.user
if counter.customer_is_barman(customer):
return customer.user
return random.choice(list(request.barmen))
class CounterClick(
CounterTabsMixin, UseFragmentsMixin, 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.exclude(type="EBOUTIC")
.annotate_is_open()
.select_related("club")
)
form_class = BasketForm
template_name = "counter/counter_click.jinja"
pk_url_kwarg = "counter_id"
current_tab = "counter"
def get_form_kwargs(self):
return super().get_form_kwargs() | {
"customer": self.customer,
"counter": self.object,
"form_kwargs": {
"allowed_prices": {price.id: price for price in self.prices}
},
}
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 or self.customer.user.is_banned_counter:
return redirect(obj) # Redirect to counter
if obj.type == "OFFICE" and (
request.user.is_anonymous
or not (
obj.sellers.contains(request.user)
or obj.club.has_rights_in_club(request.user)
)
):
# To be able to click on an office counter,
# a user must either be in the board of the club that own the counter
# or a seller of this counter.
raise PermissionDenied
if obj.type == "BAR" and not (
request.barmen and request.barmen.issubset(set(obj.barmen_list))
):
messages.error(request, _("You cannot click users on this counter"))
return redirect(obj) # Redirect to counter
self.prices = list(obj.get_prices_for(self.customer))
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.request, self.object, self.customer)
with transaction.atomic():
self.request.session["last_basket"] = []
# We sort items from cheap to expensive
# This is important because some items have a negative price
# Negative priced items gives money to the customer and should
# be processed first so that we don't throw a not enough money error
for form in sorted(formset, key=lambda form: form.price.amount):
self.request.session["last_basket"].append(
f"{form.cleaned_data['quantity']} x {form.price.full_label}"
)
common_kwargs = {
"product": form.price.product,
"club_id": form.price.product.club_id,
"counter": self.object,
"seller": operator,
"customer": self.customer,
}
Selling(
**common_kwargs,
label=form.price.full_label,
unit_price=form.price.amount,
quantity=form.cleaned_data["quantity"]
- form.cleaned_data["bonus_quantity"],
).save()
if form.cleaned_data["bonus_quantity"] > 0:
Selling(
**common_kwargs,
label=f"{form.price.full_label} (Plateau)",
unit_price=0,
quantity=form.cleaned_data["bonus_quantity"],
).save()
self.customer.update_returnable_balance()
# 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 _update_returnable_balance(self, formset):
ids = [form.cleaned_data["id"] for form in formset]
returnables = list(
ReturnableProduct.objects.filter(
Q(product_id__in=ids) | Q(returned_product_id__in=ids)
).annotate_balance_for(self.customer)
)
for returnable in returnables:
cons_quantity = next(
(
form.cleaned_data["quantity"]
for form in formset
if form.cleaned_data["id"] == returnable.product_id
),
0,
)
dcons_quantity = next(
(
form.cleaned_data["quantity"]
for form in formset
if form.cleaned_data["id"] == returnable.returned_product_id
),
0,
)
self.customer.return_balances.update_or_create(
returnable=returnable,
defaults={
"balance": returnable.balance + cons_quantity - dcons_quantity
},
)
def get_success_url(self):
return resolve_url(self.object)
def get_fragment_context_data(self) -> dict[str, SafeString]:
res = super().get_fragment_context_data()
if self.object.type == "BAR":
res["student_card_fragment"] = StudentCardFormFragment.as_fragment()(
self.request, customer=self.customer
)
if self.object.can_refill():
res["refilling_fragment"] = RefillingCreateView.as_fragment()(
self.request, customer=self.customer, counter=self.object
)
return res
def get_context_data(self, **kwargs):
"""Add customer to the context."""
kwargs = super().get_context_data(**kwargs)
kwargs["prices"] = self.prices
kwargs["formulas"] = ProductFormula.objects.filter(
result__in=[p.product_id for p in self.prices]
).prefetch_related("products")
kwargs["categories"] = defaultdict(list)
for price in self.prices:
kwargs["categories"][price.product.product_type].append(price)
kwargs["customer"] = self.customer
kwargs["cancel_url"] = self.get_success_url()
# To get all forms errors to the javascript, we create a list of error list
kwargs["form_errors"] = [
list(field_error.values()) for field_error in kwargs["form"].errors
]
return kwargs
class RefillingCreateView(FragmentMixin, CreateView):
"""This is a fragment only view which integrates with counter_click.jinja"""
model = Refilling
form_class = RefillForm
template_name = "counter/fragments/create_refill.jinja"
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, id=self.kwargs["counter_id"])
if not (
request.barmen
and request.barmen.issubset(self.counter.barmen_list)
and self.counter.can_refill()
):
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
def render_fragment(self, request, **kwargs) -> SafeString:
self.customer = kwargs.pop("customer")
self.counter = kwargs.pop("counter")
self.object = None
return super().render_fragment(request, **kwargs)
def get_form_kwargs(self):
return super().get_form_kwargs() | {
"counter": self.counter,
"operator": get_operator(self.request, self.counter, self.customer),
"customer": self.customer,
}
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["action"] = reverse(
"counter:refilling_create",
kwargs={"customer_id": self.customer.pk, "counter_id": self.counter.pk},
)
return kwargs
def get_success_url(self, **kwargs):
return self.request.path