From d41a3a524a5d1c0273b3cc8635422c9e052b6fa3 Mon Sep 17 00:00:00 2001 From: imperosol Date: Thu, 11 Jun 2026 14:21:50 +0200 Subject: [PATCH] max amount for eboutic refills --- counter/fields.py | 3 +- counter/forms.py | 36 ++++++++++--------- counter/models.py | 4 +-- counter/views/click.py | 8 ++--- .../static/bundled/eboutic/eboutic-index.ts | 31 +++++++++++----- eboutic/templates/eboutic/eboutic_main.jinja | 25 +++++++++++-- eboutic/views.py | 30 +++++++++++++--- 7 files changed, 97 insertions(+), 40 deletions(-) diff --git a/counter/fields.py b/counter/fields.py index 0bde4801..caf3a584 100644 --- a/counter/fields.py +++ b/counter/fields.py @@ -32,7 +32,8 @@ class CurrencyField(models.DecimalField): res.append(MinValueValidator(self.min_value)) return [*super().validators, *res] - def check(self, **kwargs): + def check(self, **kwargs): # pragma: no cover + # this is executed during runserver, but won't run in prod errors = super().check(**kwargs) for name, val in ("min_value", self.min_value), ("max_value", self.max_value): if not val: diff --git a/counter/forms.py b/counter/forms.py index 18efa724..1794e2ba 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -565,16 +565,7 @@ class BasketItemForm(forms.Form): quantity = forms.IntegerField(min_value=1, required=True) price_id = forms.IntegerField(min_value=0, required=True) - def __init__( - self, - customer: Customer, - counter: Counter, - allowed_prices: dict[int, Price], - *args, - **kwargs, - ): - self.customer = customer # Used by formset - self.counter = counter # Used by formset + def __init__(self, allowed_prices: dict[int, Price], *args, **kwargs): self.allowed_prices = allowed_prices super().__init__(*args, **kwargs) @@ -609,6 +600,11 @@ class BasketItemForm(forms.Form): class BaseBasketForm(forms.BaseFormSet): + def __init__(self, *args, customer: Customer, counter: Counter, **kwargs): + super().__init__(*args, **kwargs) + self.customer = customer + self.counter = counter + def clean(self): self.forms = [form for form in self.forms if form.cleaned_data != {}] @@ -617,8 +613,9 @@ class BaseBasketForm(forms.BaseFormSet): self._check_forms_have_errors() self._check_product_are_unique() - self._check_recorded_products(self[0].customer) - self._check_enough_money(self[0].counter, self[0].customer) + self._check_recorded_products() + self._check_enough_money() + self._check_refills() def _check_forms_have_errors(self): if any(len(form.errors) > 0 for form in self): @@ -629,12 +626,12 @@ class BaseBasketForm(forms.BaseFormSet): if len(price_ids) != len(self.forms): raise forms.ValidationError(_("Duplicated product entries.")) - def _check_enough_money(self, counter: Counter, customer: Customer): + def _check_enough_money(self): self.total_price = sum([data["total_price"] for data in self.cleaned_data]) - if self.total_price > customer.amount: + if self.total_price > self.customer.amount: raise forms.ValidationError(_("Not enough money")) - def _check_recorded_products(self, customer: Customer): + def _check_recorded_products(self): """Check for, among other things, ecocups and pitchers""" items = defaultdict(int) for form in self.forms: @@ -643,7 +640,7 @@ class BaseBasketForm(forms.BaseFormSet): returnables = list( ReturnableProduct.objects.filter( Q(product_id__in=ids) | Q(returned_product_id__in=ids) - ).annotate_balance_for(customer) + ).annotate_balance_for(self.customer) ) limit_reached = [] for returnable in returnables: @@ -662,6 +659,13 @@ class BaseBasketForm(forms.BaseFormSet): % ", ".join([str(p) for p in limit_reached]) ) + def _check_refills(self): + refill_type_id = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING + if any(f.price.product.product_type_id == refill_type_id for f in self.forms): + raise ValidationError( + _("Refill bonds cannot be purchased outside of the eboutic") + ) + BasketForm = forms.formset_factory( BasketItemForm, formset=BaseBasketForm, absolute_max=None, min_num=1 diff --git a/counter/models.py b/counter/models.py index fb180191..cd88e7e5 100644 --- a/counter/models.py +++ b/counter/models.py @@ -827,7 +827,7 @@ class Refilling(models.Model): counter = models.ForeignKey( Counter, related_name="refillings", blank=False, on_delete=models.CASCADE ) - amount = CurrencyField(_("amount"), min_value=0.01) + amount: CurrencyField = CurrencyField(_("amount"), min_value=0.01) operator = models.ForeignKey( User, related_name="refillings_as_operator", @@ -883,7 +883,7 @@ class Refilling(models.Model): def clean(self): super().clean() - if self.amount + self.customer.amount > settings.SITH_ACCOUNT_MAX_MONEY: + if (self.amount + self.customer.amount) > settings.SITH_ACCOUNT_MAX_MONEY: raise ValidationError( _("There cannot be more than %(money)d€ on an AE account") % {"money": settings.SITH_ACCOUNT_MAX_MONEY} diff --git a/counter/views/click.py b/counter/views/click.py index 7633327d..9ec678ae 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -73,13 +73,13 @@ class CounterClick( current_tab = "counter" def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["form_kwargs"] = { + return super().get_form_kwargs() | { "customer": self.customer, "counter": self.object, - "allowed_prices": {price.id: price for price in self.prices}, + "form_kwargs": { + "allowed_prices": {price.id: price for price in self.prices} + }, } - return kwargs def dispatch(self, request, *args, **kwargs): self.customer = get_object_or_404(Customer, user_id=self.kwargs["user_id"]) diff --git a/eboutic/static/bundled/eboutic/eboutic-index.ts b/eboutic/static/bundled/eboutic/eboutic-index.ts index 59a258e3..e2bbe9ec 100644 --- a/eboutic/static/bundled/eboutic/eboutic-index.ts +++ b/eboutic/static/bundled/eboutic/eboutic-index.ts @@ -5,10 +5,11 @@ interface BasketItem { name: string; quantity: number; unitPrice: number; + isRefill: boolean; } const BASKET_CACHE_KEY = "basket"; -const BASKET_CACHE_VERSION = 1; +const BASKET_CACHE_VERSION = 2; document.addEventListener("alpine:init", () => { Alpine.data("basket", (validPrices: number[], lastPurchaseTime?: number) => ({ @@ -21,7 +22,7 @@ document.addEventListener("alpine:init", () => { }); document .getElementById("id_form-TOTAL_FORMS") - .setAttribute(":value", "basket.length"); + ?.setAttribute(":value", "basket.length"); }, loadBasket(): BasketItem[] { @@ -32,8 +33,8 @@ document.addEventListener("alpine:init", () => { return []; } if ( - lastPurchaseTime !== null && - localStorage.basketTimestamp !== undefined && + lastPurchaseTime && + localStorage.basketTimestamp && new Date(lastPurchaseTime) >= new Date(Number.parseInt(localStorage.basketTimestamp, 10)) ) { @@ -64,6 +65,15 @@ document.addEventListener("alpine:init", () => { ); }, + getTotalRefill() { + return this.basket + .filter((item) => item.isRefill) + .reduce( + (acc: number, item: BasketItem) => acc + item.quantity * item.unitPrice, + 0, + ); + }, + /** * Add 1 to the quantity of an item in the basket * @param {BasketItem} item @@ -86,7 +96,7 @@ document.addEventListener("alpine:init", () => { if (this.basket[index].quantity === 0) { this.basket = this.basket.filter( - (e: BasketItem) => e.priceId !== this.basket[index].id, + (e: BasketItem) => e.priceId !== this.basket[index].priceId, ); } }, @@ -103,14 +113,16 @@ document.addEventListener("alpine:init", () => { * @param id The id of the product to add * @param name The name of the product * @param price The unit price of the product + * @param isRefill true if the product is a refill bond * @returns The created item */ - createItem(id: number, name: string, price: number): BasketItem { + createItem(id: number, name: string, price: number, isRefill: boolean): BasketItem { const newItem = { priceId: id, name, quantity: 0, unitPrice: price, + isRefill, } as BasketItem; this.basket.push(newItem); @@ -125,16 +137,17 @@ document.addEventListener("alpine:init", () => { * @param id The id of the product to add * @param name The name of the product * @param price The unit price of the product + * @param isRefill true if the product is a refill bond */ - addFromCatalog(id: number, name: string, price: number) { - let item = this.basket.find((e: BasketItem) => e.priceId === id); + addFromCatalog(id: number, name: string, price: number, isRefill: boolean) { + const item = this.basket.find((e: BasketItem) => e.priceId === id); // if the item is not in the basket, we create it // else we add + 1 to it if (item) { this.add(item); } else { - item = this.createItem(id, name, price); + this.createItem(id, name, price, isRefill); } }, })); diff --git a/eboutic/templates/eboutic/eboutic_main.jinja b/eboutic/templates/eboutic/eboutic_main.jinja index 4962d5ce..fe5227d7 100644 --- a/eboutic/templates/eboutic/eboutic_main.jinja +++ b/eboutic/templates/eboutic/eboutic_main.jinja @@ -58,6 +58,17 @@ {% endif %} +