diff --git a/core/models.py b/core/models.py index 347e9bd4..c8375727 100644 --- a/core/models.py +++ b/core/models.py @@ -578,14 +578,6 @@ class User(AbstractUser): return "%s (%s)" % (self.get_full_name(), self.nick_name) return self.get_full_name() - def get_age(self): - """Returns the age.""" - today = timezone.now() - born = self.date_of_birth - return ( - today.year - born.year - ((today.month, today.day) < (born.month, born.day)) - ) - def get_family( self, godfathers_depth: NonNegativeInt = 4, diff --git a/core/static/core/style.scss b/core/static/core/style.scss index 61eb71e0..cf1cedc6 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -208,6 +208,7 @@ body { a.btn { display: inline-block; } + .btn { font-size: 15px; font-weight: normal; @@ -336,7 +337,8 @@ body { margin-left: -125px; box-sizing: border-box; position: fixed; - z-index: 1; + z-index: 10; + /* to get on top of tomselect */ left: 50%; top: 60px; text-align: center; @@ -431,12 +433,17 @@ body { flex-wrap: wrap; $col-gap: 1rem; + $row-gap: 0.5rem; + &.gap { column-gap: var($col-gap); + row-gap: var($row-gap); } + @for $i from 2 through 5 { &.gap-#{$i}x { column-gap: $i * $col-gap; + row-gap: $i * $row-gap; } } } @@ -1242,40 +1249,6 @@ u, text-decoration: underline; } -#bar-ui { - padding: 0.4em; - display: flex; - flex-wrap: wrap; - flex-direction: row-reverse; - - #products { - flex-basis: 100%; - margin: 0.2em; - overflow: auto; - } - - #click_form { - flex: auto; - margin: 0.2em; - } - - #user_info { - flex: auto; - padding: 0.5em; - margin: 0.2em; - height: 100%; - background: $secondary-neutral-light-color; - - img { - max-width: 70%; - } - - input { - background: white; - } - } -} - /*-----------------------------USER PROFILE----------------------------*/ .user_mini_profile { diff --git a/core/templates/core/macros.jinja b/core/templates/core/macros.jinja index e624e87a..84c5b05a 100644 --- a/core/templates/core/macros.jinja +++ b/core/templates/core/macros.jinja @@ -60,7 +60,7 @@ {% endif %} {% if user.date_of_birth %}
{% trans %}Basket: {% endtrans %}
-- Total: - - € -
- - -Client : Richard Batsbak - Nouveau montant : 3.60" in str(
- response_content
+ @classmethod
+ def set_age(cls, user: User, age: int):
+ user.date_of_birth = localdate().replace(year=localdate().year - age)
+ user.save()
+
+ def submit_basket(
+ self,
+ user: User,
+ basket: list[BasketItem],
+ counter: Counter | None = None,
+ client: Client | None = None,
+ ) -> HttpResponse:
+ used_counter = counter if counter is not None else self.counter
+ used_client = client if client is not None else self.client
+ data = {
+ "form-TOTAL_FORMS": str(len(basket)),
+ "form-INITIAL_FORMS": "0",
+ }
+ for index, item in enumerate(basket):
+ data.update(item.to_form(index))
+ return used_client.post(
+ reverse(
+ "counter:click",
+ kwargs={"counter_id": used_counter.id, "user_id": user.id},
+ ),
+ data,
)
- self.client.post(
- reverse("counter:login", kwargs={"counter_id": self.mde.id}),
- {"username": self.sli.username, "password": "plop"},
+ def refill_user(self, user: User, amount: Decimal | int):
+ baker.make(Refilling, amount=amount, customer=user.customer, is_validated=False)
+
+ def test_click_eboutic_failure(self):
+ eboutic = baker.make(Counter, type="EBOUTIC")
+ self.client.force_login(self.club_admin)
+ assert (
+ self.submit_basket(
+ self.customer,
+ [BasketItem(self.stamps.id, 5)],
+ counter=eboutic,
+ ).status_code
+ == 404
)
- response = self.client.post(
- refill_url,
- {
- "amount": "5",
- "payment_method": "CASH",
- "bank": "OTHER",
- },
- HTTP_REFERER=counter_url,
- )
- assert response.status_code == 302
+ def test_click_office_success(self):
+ self.refill_user(self.customer, 10)
+ self.client.force_login(self.club_admin)
- self.client.post(
- reverse("counter:login", kwargs={"counter_id": self.foyer.id}),
- {"username": self.krophil.username, "password": "plop"},
+ assert (
+ self.submit_basket(
+ self.customer,
+ [BasketItem(self.stamps.id, 5)],
+ counter=self.club_counter,
+ ).status_code
+ == 302
+ )
+ assert self.updated_amount(self.customer) == Decimal("2.5")
+
+ # Test no special price on office counter
+ self.refill_user(self.club_admin, 10)
+
+ assert (
+ self.submit_basket(
+ self.club_admin,
+ [BasketItem(self.stamps.id, 1)],
+ counter=self.club_counter,
+ ).status_code
+ == 302
)
- response = self.client.get(
- reverse("counter:details", kwargs={"counter_id": self.foyer.id})
+ assert self.updated_amount(self.club_admin) == Decimal("8.5")
+
+ def test_click_bar_success(self):
+ self.refill_user(self.customer, 10)
+ self.login_in_bar(self.barmen)
+
+ assert (
+ self.submit_basket(
+ self.customer,
+ [
+ BasketItem(self.beer.id, 2),
+ BasketItem(self.snack.id, 1),
+ ],
+ ).status_code
+ == 302
)
- counter_token = re.search(
- r'name="counter_token" value="([^"]*)"', str(response.content)
- ).group(1)
+ assert self.updated_amount(self.customer) == Decimal("5.5")
- response = self.client.post(
- reverse("counter:details", kwargs={"counter_id": self.foyer.id}),
- {"code": self.richard.customer.account_id, "counter_token": counter_token},
- )
- counter_url = response.get("location")
- refill_url = reverse(
- "counter:refilling_create",
- kwargs={
- "customer_id": self.richard.customer.pk,
- },
+ # Test barmen special price
+
+ self.refill_user(self.barmen, 10)
+
+ assert (
+ self.submit_basket(self.barmen, [BasketItem(self.beer.id, 1)])
+ ).status_code == 302
+
+ assert self.updated_amount(self.barmen) == Decimal("9")
+
+ def test_click_tray_price(self):
+ self.refill_user(self.customer, 20)
+ self.login_in_bar(self.barmen)
+
+ # Not applying tray price
+ assert (
+ self.submit_basket(
+ self.customer,
+ [
+ BasketItem(self.beer_tap.id, 2),
+ ],
+ ).status_code
+ == 302
)
- response = self.client.post(
- refill_url,
- {
- "amount": "5",
- "payment_method": "CASH",
- "bank": "OTHER",
- },
- HTTP_REFERER=counter_url,
+ assert self.updated_amount(self.customer) == Decimal("17")
+
+ # Applying tray price
+ assert (
+ self.submit_basket(
+ self.customer,
+ [
+ BasketItem(self.beer_tap.id, 7),
+ ],
+ ).status_code
+ == 302
)
- assert response.status_code == 403 # Krophil is not board admin
+
+ assert self.updated_amount(self.customer) == Decimal("8")
+
+ def test_click_alcool_unauthorized(self):
+ self.login_in_bar()
+
+ for user in [self.underage_customer, self.banned_alcohol_customer]:
+ self.refill_user(user, 10)
+
+ # Buy product without age limit
+ assert (
+ self.submit_basket(
+ user,
+ [
+ BasketItem(self.snack.id, 2),
+ ],
+ ).status_code
+ == 302
+ )
+
+ assert self.updated_amount(user) == Decimal("7")
+
+ # Buy product without age limit
+ assert (
+ self.submit_basket(
+ user,
+ [
+ BasketItem(self.beer.id, 2),
+ ],
+ ).status_code
+ == 200
+ )
+
+ assert self.updated_amount(user) == Decimal("7")
+
+ def test_click_unauthorized_customer(self):
+ self.login_in_bar()
+
+ for user in [
+ self.banned_counter_customer,
+ self.customer_old_can_not_buy,
+ ]:
+ self.refill_user(user, 10)
+ resp = self.submit_basket(
+ user,
+ [
+ BasketItem(self.snack.id, 2),
+ ],
+ )
+ assert resp.status_code == 302
+ assert resp.url == resolve_url(self.counter)
+
+ assert self.updated_amount(user) == Decimal("10")
+
+ def test_click_user_without_customer(self):
+ self.login_in_bar()
+ assert (
+ self.submit_basket(
+ self.customer_can_not_buy,
+ [
+ BasketItem(self.snack.id, 2),
+ ],
+ ).status_code
+ == 404
+ )
+
+ def test_click_allowed_old_subscriber(self):
+ self.login_in_bar()
+ self.refill_user(self.customer_old_can_buy, 10)
+ assert (
+ self.submit_basket(
+ self.customer_old_can_buy,
+ [
+ BasketItem(self.snack.id, 2),
+ ],
+ ).status_code
+ == 302
+ )
+
+ assert self.updated_amount(self.customer_old_can_buy) == Decimal("7")
+
+ def test_click_wrong_counter(self):
+ self.login_in_bar()
+ self.refill_user(self.customer, 10)
+ assert (
+ self.submit_basket(
+ self.customer,
+ [
+ BasketItem(self.snack.id, 2),
+ ],
+ counter=self.other_counter,
+ ).status_code
+ == 302 # Redirect to counter main
+ )
+
+ # We want to test sending requests from another counter while
+ # we are currently registered to another counter
+ # so we connect to a counter and
+ # we create a new client, in order to check
+ # that using a client not logged to a counter
+ # where another client is logged still isn't authorized.
+ client = Client()
+ assert (
+ self.submit_basket(
+ self.customer,
+ [
+ BasketItem(self.snack.id, 2),
+ ],
+ counter=self.counter,
+ client=client,
+ ).status_code
+ == 302 # Redirect to counter main
+ )
+
+ assert self.updated_amount(self.customer) == Decimal("10")
+
+ def test_click_not_connected(self):
+ self.refill_user(self.customer, 10)
+ assert (
+ self.submit_basket(
+ self.customer,
+ [
+ BasketItem(self.snack.id, 2),
+ ],
+ ).status_code
+ == 302 # Redirect to counter main
+ )
+
+ assert (
+ self.submit_basket(
+ self.customer,
+ [
+ BasketItem(self.snack.id, 2),
+ ],
+ counter=self.club_counter,
+ ).status_code
+ == 403
+ )
+
+ assert self.updated_amount(self.customer) == Decimal("10")
+
+ def test_click_product_not_in_counter(self):
+ self.refill_user(self.customer, 10)
+ self.login_in_bar()
+
+ assert (
+ self.submit_basket(
+ self.customer,
+ [
+ BasketItem(self.stamps.id, 2),
+ ],
+ ).status_code
+ == 200
+ )
+ assert self.updated_amount(self.customer) == Decimal("10")
+
+ def test_click_product_invalid(self):
+ self.refill_user(self.customer, 10)
+ self.login_in_bar()
+
+ for item in [
+ BasketItem("-1", 2),
+ BasketItem(self.beer.id, -1),
+ BasketItem(None, 1),
+ BasketItem(self.beer.id, None),
+ BasketItem(None, None),
+ ]:
+ assert (
+ self.submit_basket(
+ self.customer,
+ [item],
+ ).status_code
+ == 200
+ )
+
+ assert self.updated_amount(self.customer) == Decimal("10")
+
+ def test_click_not_enough_money(self):
+ self.refill_user(self.customer, 10)
+ self.login_in_bar()
+
+ assert (
+ self.submit_basket(
+ self.customer,
+ [
+ BasketItem(self.beer_tap.id, 5),
+ BasketItem(self.beer.id, 10),
+ ],
+ ).status_code
+ == 200
+ )
+
+ assert self.updated_amount(self.customer) == Decimal("10")
def test_annotate_has_barman_queryset(self):
"""Test if the custom queryset method `annotate_has_barman` works as intended."""
- self.sli.counters.set([self.foyer, self.mde])
- counters = Counter.objects.annotate_has_barman(self.sli)
+ counters = Counter.objects.annotate_has_barman(self.barmen)
for counter in counters:
- if counter.name in ("Foyer", "MDE"):
+ if counter in (self.counter, self.other_counter):
assert counter.has_annotated_barman
else:
assert not counter.has_annotated_barman
@@ -436,4 +859,4 @@ class TestClubCounterClickAccess(TestCase):
self.counter.sellers.add(self.user)
self.client.force_login(self.user)
res = self.client.get(self.click_url)
- assert res.status_code == 200
+ assert res.status_code == 403
diff --git a/counter/views/click.py b/counter/views/click.py
index 9542b467..f324a9fc 100644
--- a/counter/views/click.py
+++ b/counter/views/click.py
@@ -12,20 +12,26 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
-import re
-from http import HTTPStatus
-from typing import TYPE_CHECKING
-from urllib.parse import parse_qs
+import math
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.db import transaction
+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 DetailView, FormView
+from django.views.generic import FormView
+from django.views.generic.detail import SingleObjectMixin
+from ninja.main import HttpRequest
+from core.models import User
from core.utils import FormFragmentTemplateData
from core.views import CanViewMixin
from counter.forms import RefillForm
@@ -34,11 +40,102 @@ 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
+
+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 counter.get_random_barman()
-class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
+class ProductForm(Form):
+ quantity = IntegerField(min_value=1)
+ id = IntegerField(min_value=0)
+
+ def __init__(
+ self,
+ customer: Customer,
+ counter: Counter,
+ allowed_products: dict[int, Product],
+ *args,
+ **kwargs,
+ ):
+ self.customer = customer # Used by formset
+ self.counter = counter # Used by formset
+ self.allowed_products = allowed_products
+ super().__init__(*args, **kwargs)
+
+ def clean_id(self):
+ data = self.cleaned_data["id"]
+
+ # We store self.product so we can use it later on the formset validation
+ # And also in the global clean
+ self.product = self.allowed_products.get(data, None)
+ if self.product is None:
+ raise ValidationError(
+ _("The selected product isn't available for this user")
+ )
+
+ return data
+
+ def clean(self):
+ cleaned_data = super().clean()
+ if len(self.errors) > 0:
+ return
+
+ # 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["total_price"] = self.product.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):
+ if any(len(form.errors) > 0 for form in self):
+ 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.
@@ -46,346 +143,102 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
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 render_to_response(self, *args, **kwargs):
- if self.is_ajax(self.request):
- response = {"errors": []}
- status = HTTPStatus.OK
+ def get_queryset(self):
+ return super().get_queryset().exclude(type="EBOUTIC").annotate_is_open()
- 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 get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ kwargs["form_kwargs"] = {
+ "customer": self.customer,
+ "counter": self.object,
+ "allowed_products": {product.id: product for product in self.products},
+ }
+ 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
+
+ if not self.customer.can_buy or self.customer.user.is_banned_counter:
+ return redirect(obj) # Redirect to counter
+
+ if obj.type == "OFFICE" and (
+ obj.sellers.filter(pk=request.user.pk).exists()
+ or not obj.club.has_rights_in_club(request.user)
):
- return redirect(obj)
+ raise PermissionDenied
+
+ if obj.type == "BAR" and (
+ not obj.is_open
+ or "counter_token" not in request.session
+ or request.session["counter_token"] != obj.token
+ ):
+ return redirect(obj) # Redirect to counter
+
+ self.products = obj.get_products_for(self.customer)
+
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 form_valid(self, formset):
+ ret = super().form_valid(formset)
- 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)
+ if len(formset) == 0:
+ return ret
- 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 :
- - `[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."""
+ operator = get_operator(self.request, self.object, self.customer)
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"))
+ self.request.session["last_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)
+ for form in formset:
+ self.request.session["last_basket"].append(
+ f"{form.cleaned_data['quantity']} x {form.product.name}"
)
- s = Selling(
- label=p.name,
- product=p,
- club=p.club,
+
+ Selling(
+ label=form.product.name,
+ product=form.product,
+ club=form.product.club,
counter=self.object,
- unit_price=uprice,
- quantity=infos["qty"],
- seller=self.operator,
+ unit_price=form.product.price,
+ quantity=form.cleaned_data["quantity"]
+ - form.cleaned_data["bonus_quantity"],
+ seller=operator,
customer=self.customer,
- )
- s.save()
- if infos["bonus_qty"]:
- s = Selling(
- label=p.name + " (Plateau)",
- product=p,
- club=p.club,
+ ).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=infos["bonus_qty"],
- seller=self.operator,
+ quantity=form.cleaned_data["bonus_quantity"],
+ seller=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)
- )
+ ).save()
- 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)
- )
+ 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_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["products"] = self.products
kwargs["categories"] = {}
for product in kwargs["products"]:
if product.product_type:
@@ -393,8 +246,12 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
product
)
kwargs["customer"] = self.customer
- kwargs["basket_total"] = self.sum_basket(self.request)
+ 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
+ ]
if self.object.type == "BAR":
kwargs["student_card_fragment"] = StudentCardFormView.get_template_data(
self.customer
@@ -404,6 +261,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
kwargs["refilling_fragment"] = RefillingCreateView.get_template_data(
self.customer
).render(self.request)
+
return kwargs
@@ -442,10 +300,7 @@ class RefillingCreateView(FormView):
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()
+ self.operator = get_operator(request, self.counter, self.customer)
return super().dispatch(request, *args, **kwargs)
diff --git a/counter/views/home.py b/counter/views/home.py
index 60cc5a5a..d66b0969 100644
--- a/counter/views/home.py
+++ b/counter/views/home.py
@@ -43,6 +43,9 @@ class CounterMain(
)
current_tab = "counter"
+ def get_queryset(self):
+ return super().get_queryset().exclude(type="EBOUTIC")
+
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.type == "BAR" and not (
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 3e15ddf0..d6c2e813 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-12-21 02:15+0100\n"
+"POT-Creation-Date: 2024-12-23 02:37+0100\n"
"PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal