max amount for eboutic refills

This commit is contained in:
imperosol
2026-06-11 14:21:50 +02:00
parent 39bbbc8878
commit d41a3a524a
7 changed files with 97 additions and 40 deletions
+2 -1
View File
@@ -32,7 +32,8 @@ class CurrencyField(models.DecimalField):
res.append(MinValueValidator(self.min_value)) res.append(MinValueValidator(self.min_value))
return [*super().validators, *res] 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) errors = super().check(**kwargs)
for name, val in ("min_value", self.min_value), ("max_value", self.max_value): for name, val in ("min_value", self.min_value), ("max_value", self.max_value):
if not val: if not val:
+20 -16
View File
@@ -565,16 +565,7 @@ class BasketItemForm(forms.Form):
quantity = forms.IntegerField(min_value=1, required=True) quantity = forms.IntegerField(min_value=1, required=True)
price_id = forms.IntegerField(min_value=0, required=True) price_id = forms.IntegerField(min_value=0, required=True)
def __init__( def __init__(self, allowed_prices: dict[int, Price], *args, **kwargs):
self,
customer: Customer,
counter: Counter,
allowed_prices: dict[int, Price],
*args,
**kwargs,
):
self.customer = customer # Used by formset
self.counter = counter # Used by formset
self.allowed_prices = allowed_prices self.allowed_prices = allowed_prices
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -609,6 +600,11 @@ class BasketItemForm(forms.Form):
class BaseBasketForm(forms.BaseFormSet): 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): def clean(self):
self.forms = [form for form in self.forms if form.cleaned_data != {}] 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_forms_have_errors()
self._check_product_are_unique() self._check_product_are_unique()
self._check_recorded_products(self[0].customer) self._check_recorded_products()
self._check_enough_money(self[0].counter, self[0].customer) self._check_enough_money()
self._check_refills()
def _check_forms_have_errors(self): def _check_forms_have_errors(self):
if any(len(form.errors) > 0 for form in 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): if len(price_ids) != len(self.forms):
raise forms.ValidationError(_("Duplicated product entries.")) 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]) 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")) 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""" """Check for, among other things, ecocups and pitchers"""
items = defaultdict(int) items = defaultdict(int)
for form in self.forms: for form in self.forms:
@@ -643,7 +640,7 @@ class BaseBasketForm(forms.BaseFormSet):
returnables = list( returnables = list(
ReturnableProduct.objects.filter( ReturnableProduct.objects.filter(
Q(product_id__in=ids) | Q(returned_product_id__in=ids) Q(product_id__in=ids) | Q(returned_product_id__in=ids)
).annotate_balance_for(customer) ).annotate_balance_for(self.customer)
) )
limit_reached = [] limit_reached = []
for returnable in returnables: for returnable in returnables:
@@ -662,6 +659,13 @@ class BaseBasketForm(forms.BaseFormSet):
% ", ".join([str(p) for p in limit_reached]) % ", ".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( BasketForm = forms.formset_factory(
BasketItemForm, formset=BaseBasketForm, absolute_max=None, min_num=1 BasketItemForm, formset=BaseBasketForm, absolute_max=None, min_num=1
+2 -2
View File
@@ -827,7 +827,7 @@ class Refilling(models.Model):
counter = models.ForeignKey( counter = models.ForeignKey(
Counter, related_name="refillings", blank=False, on_delete=models.CASCADE 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( operator = models.ForeignKey(
User, User,
related_name="refillings_as_operator", related_name="refillings_as_operator",
@@ -883,7 +883,7 @@ class Refilling(models.Model):
def clean(self): def clean(self):
super().clean() 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( raise ValidationError(
_("There cannot be more than %(money)d€ on an AE account") _("There cannot be more than %(money)d€ on an AE account")
% {"money": settings.SITH_ACCOUNT_MAX_MONEY} % {"money": settings.SITH_ACCOUNT_MAX_MONEY}
+4 -4
View File
@@ -73,13 +73,13 @@ class CounterClick(
current_tab = "counter" current_tab = "counter"
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() return super().get_form_kwargs() | {
kwargs["form_kwargs"] = {
"customer": self.customer, "customer": self.customer,
"counter": self.object, "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): def dispatch(self, request, *args, **kwargs):
self.customer = get_object_or_404(Customer, user_id=self.kwargs["user_id"]) self.customer = get_object_or_404(Customer, user_id=self.kwargs["user_id"])
@@ -5,10 +5,11 @@ interface BasketItem {
name: string; name: string;
quantity: number; quantity: number;
unitPrice: number; unitPrice: number;
isRefill: boolean;
} }
const BASKET_CACHE_KEY = "basket"; const BASKET_CACHE_KEY = "basket";
const BASKET_CACHE_VERSION = 1; const BASKET_CACHE_VERSION = 2;
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("basket", (validPrices: number[], lastPurchaseTime?: number) => ({ Alpine.data("basket", (validPrices: number[], lastPurchaseTime?: number) => ({
@@ -21,7 +22,7 @@ document.addEventListener("alpine:init", () => {
}); });
document document
.getElementById("id_form-TOTAL_FORMS") .getElementById("id_form-TOTAL_FORMS")
.setAttribute(":value", "basket.length"); ?.setAttribute(":value", "basket.length");
}, },
loadBasket(): BasketItem[] { loadBasket(): BasketItem[] {
@@ -32,8 +33,8 @@ document.addEventListener("alpine:init", () => {
return []; return [];
} }
if ( if (
lastPurchaseTime !== null && lastPurchaseTime &&
localStorage.basketTimestamp !== undefined && localStorage.basketTimestamp &&
new Date(lastPurchaseTime) >= new Date(lastPurchaseTime) >=
new Date(Number.parseInt(localStorage.basketTimestamp, 10)) 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 * Add 1 to the quantity of an item in the basket
* @param {BasketItem} item * @param {BasketItem} item
@@ -86,7 +96,7 @@ document.addEventListener("alpine:init", () => {
if (this.basket[index].quantity === 0) { if (this.basket[index].quantity === 0) {
this.basket = this.basket.filter( 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 id The id of the product to add
* @param name The name of the product * @param name The name of the product
* @param price The unit price 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 * @returns The created item
*/ */
createItem(id: number, name: string, price: number): BasketItem { createItem(id: number, name: string, price: number, isRefill: boolean): BasketItem {
const newItem = { const newItem = {
priceId: id, priceId: id,
name, name,
quantity: 0, quantity: 0,
unitPrice: price, unitPrice: price,
isRefill,
} as BasketItem; } as BasketItem;
this.basket.push(newItem); this.basket.push(newItem);
@@ -125,16 +137,17 @@ document.addEventListener("alpine:init", () => {
* @param id The id of the product to add * @param id The id of the product to add
* @param name The name of the product * @param name The name of the product
* @param price The unit price 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) { addFromCatalog(id: number, name: string, price: number, isRefill: boolean) {
let item = this.basket.find((e: BasketItem) => e.priceId === id); const item = this.basket.find((e: BasketItem) => e.priceId === id);
// if the item is not in the basket, we create it // if the item is not in the basket, we create it
// else we add + 1 to it // else we add + 1 to it
if (item) { if (item) {
this.add(item); this.add(item);
} else { } else {
item = this.createItem(id, name, price); this.createItem(id, name, price, isRefill);
} }
}, },
})); }));
+22 -3
View File
@@ -58,6 +58,17 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
<template x-if="(getTotalRefill() + {{ customer_amount }}) > {{ settings.SITH_ACCOUNT_MAX_MONEY }}">
<div class="alert alert-red">
<div class="alert-main">
{% trans trimmed limit=settings.SITH_ACCOUNT_MAX_MONEY %}
You cannot purchase the current basket,
because it would put your AE account balance
above the {{ limit }}€ limit
{% endtrans %}
</div>
</div>
</template>
<ul class="item-list"> <ul class="item-list">
{# Starting money #} {# Starting money #}
<li> <li>
@@ -109,9 +120,12 @@
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
{% trans %}Clear{% endtrans %} {% trans %}Clear{% endtrans %}
</button> </button>
<button class="btn btn-blue"> <button
class="btn btn-blue"
:disabled="(getTotalRefill() + {{ customer_amount }}) > {{ settings.SITH_ACCOUNT_MAX_MONEY }}"
>
<i class="fa fa-check"></i> <i class="fa fa-check"></i>
<input type="submit" value="{% trans %}Validate{% endtrans %}"/> {% trans %}Validate{% endtrans %}
</button> </button>
</div> </div>
</form> </form>
@@ -199,7 +213,12 @@
id="{{ price.id }}" id="{{ price.id }}"
class="card clickable shadow" class="card clickable shadow"
:class="{selected: basket.some((i) => i.priceId === {{ price.id }})}" :class="{selected: basket.some((i) => i.priceId === {{ price.id }})}"
@click='addFromCatalog({{ price.id }}, {{ price.full_label|tojson }}, {{ price.amount }})' @click='addFromCatalog(
{{ price.id }},
{{ price.full_label|tojson }},
{{ price.amount }},
{{ (price.product.product_type_id == settings.SITH_COUNTER_PRODUCTTYPE_REFILLING)|lower }}
)'
{% if price.sold_out %}disabled{% endif %} {% if price.sold_out %}disabled{% endif %}
> >
{% if price.product.icon %} {% if price.product.icon %}
+23 -3
View File
@@ -70,6 +70,26 @@ class BaseEbouticBasketForm(BaseBasketForm):
# Disable money check # Disable money check
... ...
def _check_refills(self):
"""Check that this basket won't put customer balance above the limit."""
refill_type_id = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
total_refill = sum(
f.price.amount * f.cleaned_data["quantity"]
for f in self.forms
if f.price.product.product_type_id == refill_type_id
)
total_other = sum(
f.price.amount * f.cleaned_data["quantity"]
for f in self.forms
if f.price.product.product_type_id != refill_type_id
)
limit = settings.SITH_ACCOUNT_MAX_MONEY
if (total_refill - total_other + self.customer.amount) > limit:
raise ValidationError(
_("There cannot be more than %(money)d€ on an AE account")
% {"money": limit}
)
EbouticBasketForm = forms.formset_factory( EbouticBasketForm = forms.formset_factory(
BasketItemForm, formset=BaseEbouticBasketForm, absolute_max=None, min_num=1 BasketItemForm, formset=BaseEbouticBasketForm, absolute_max=None, min_num=1
@@ -88,15 +108,15 @@ class EbouticMainView(LoginRequiredMixin, FormView):
form_class = EbouticBasketForm form_class = EbouticBasketForm
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() return super().get_form_kwargs() | {
kwargs["form_kwargs"] = {
"customer": self.customer, "customer": self.customer,
"counter": get_eboutic(), "counter": get_eboutic(),
"form_kwargs": {
"allowed_prices": { "allowed_prices": {
price.id: price for price in self.prices if not price.sold_out price.id: price for price in self.prices if not price.sold_out
}
}, },
} }
return kwargs
def form_valid(self, formset): def form_valid(self, formset):
if len(formset) == 0: if len(formset) == 0: