diff --git a/counter/forms.py b/counter/forms.py index 86862e10..1ba01caa 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -6,7 +6,6 @@ from datetime import date, datetime, timezone from dateutil.relativedelta import relativedelta from django import forms -from django.core.validators import MaxValueValidator from django.db.models import Exists, OuterRef, Q from django.forms import BaseModelFormSet from django.utils.timezone import now @@ -363,25 +362,6 @@ class ProductForm(forms.ModelForm): *args, product=self.instance, prefix="action", **kwargs ) - def formula_init(self, formula: ProductFormula): - """Part of the form initialisation specific to formula products.""" - self.fields["selling_price"].help_text = _( - "This product is a formula. " - "Its price cannot be greater than the price " - "of the products constituting it, which is %(price)s €" - ) % {"price": formula.max_selling_price} - self.fields["special_selling_price"].help_text = _( - "This product is a formula. " - "Its special price cannot be greater than the price " - "of the products constituting it, which is %(price)s €" - ) % {"price": formula.max_special_selling_price} - for key, price in ( - ("selling_price", formula.max_selling_price), - ("special_selling_price", formula.max_special_selling_price), - ): - self.fields[key].widget.attrs["max"] = price - self.fields[key].validators.append(MaxValueValidator(price)) - def is_valid(self): return ( super().is_valid() @@ -424,18 +404,6 @@ class ProductFormulaForm(forms.ModelForm): "the result and a part of the formula." ), ) - prices = [p.selling_price for p in cleaned_data["products"]] - special_prices = [p.special_selling_price for p in cleaned_data["products"]] - selling_price = cleaned_data["result"].selling_price - special_selling_price = cleaned_data["result"].special_selling_price - if selling_price > sum(prices) or special_selling_price > sum(special_prices): - self.add_error( - "result", - _( - "The result cannot be more expensive " - "than the total of the other products." - ), - ) return cleaned_data diff --git a/counter/models.py b/counter/models.py index 9cd08eee..e663ad33 100644 --- a/counter/models.py +++ b/counter/models.py @@ -448,10 +448,6 @@ class Product(models.Model): return True return any(user.is_in_group(pk=group.id) for group in buying_groups) - @property - def profit(self): - return self.selling_price - self.purchase_price - class PriceQuerySet(models.QuerySet): def for_user(self, user: User) -> Self: @@ -543,18 +539,6 @@ class ProductFormula(models.Model): def __str__(self): return self.result.name - @cached_property - def max_selling_price(self) -> float: - # iterating over all products is less efficient than doing - # a simple aggregation, but this method is likely to be used in - # coordination with `max_special_selling_price`, - # and Django caches the result of the `all` queryset. - return sum(p.selling_price for p in self.products.all()) - - @cached_property - def max_special_selling_price(self) -> float: - return sum(p.special_selling_price for p in self.products.all()) - class CounterQuerySet(models.QuerySet): def annotate_has_barman(self, user: User) -> Self: diff --git a/counter/static/bundled/counter/basket.ts b/counter/static/bundled/counter/basket.ts index 24161ea9..f15bc72a 100644 --- a/counter/static/bundled/counter/basket.ts +++ b/counter/static/bundled/counter/basket.ts @@ -1,11 +1,11 @@ -import type { Product } from "#counter:counter/types"; +import type { CounterItem } from "#counter:counter/types"; export class BasketItem { quantity: number; - product: Product; + product: CounterItem; errors: string[]; - constructor(product: Product, quantity: number) { + constructor(product: CounterItem, quantity: number) { this.quantity = quantity; this.product = product; this.errors = []; diff --git a/counter/static/bundled/counter/counter-click-index.ts b/counter/static/bundled/counter/counter-click-index.ts index 1d5ce0ff..c8cea6a3 100644 --- a/counter/static/bundled/counter/counter-click-index.ts +++ b/counter/static/bundled/counter/counter-click-index.ts @@ -2,6 +2,7 @@ import { AlertMessage } from "#core:utils/alert-message"; import { BasketItem } from "#counter:counter/basket"; import type { CounterConfig, + CounterItem, ErrorMessage, ProductFormula, } from "#counter:counter/types"; @@ -63,8 +64,10 @@ document.addEventListener("alpine:init", () => { }, checkFormulas() { + // Try to find a formula. + // A formula is found if all its elements are already in the basket const products = new Set( - Object.keys(this.basket).map((i: string) => Number.parseInt(i)), + Object.values(this.basket).map((item: BasketItem) => item.product.productId), ); const formula: ProductFormula = config.formulas.find((f: ProductFormula) => { return f.products.every((p: number) => products.has(p)); @@ -72,22 +75,29 @@ document.addEventListener("alpine:init", () => { if (formula === undefined) { return; } + // Now that the formula is found, remove the items composing it from the basket for (const product of formula.products) { - const key = product.toString(); + const key = Object.entries(this.basket).find( + ([_, i]: [string, BasketItem]) => i.product.productId === product, + )[0]; this.basket[key].quantity -= 1; if (this.basket[key].quantity <= 0) { this.removeFromBasket(key); } } + // Then add the result product of the formula to the basket + const result = Object.values(config.products) + .filter((item: CounterItem) => item.productId === formula.result) + .reduce((acc, curr) => (acc.price.amount < curr.price.amount ? acc : curr)); + this.addToBasket(result.price.id, 1); this.alertMessage.display( interpolate( gettext("Formula %(formula)s applied"), - { formula: config.products[formula.result.toString()].name }, + { formula: result.name }, true, ), { success: true }, ); - this.addToBasket(formula.result.toString(), 1); }, getBasketSize() { diff --git a/counter/static/bundled/counter/types.d.ts b/counter/static/bundled/counter/types.d.ts index 154304ff..cb4196cc 100644 --- a/counter/static/bundled/counter/types.d.ts +++ b/counter/static/bundled/counter/types.d.ts @@ -2,7 +2,7 @@ export type ErrorMessage = string; export interface InitialFormData { /* Used to refill the form when the backend raises an error */ - id?: keyof Record; + id?: keyof Record; quantity?: number; errors?: string[]; } @@ -15,7 +15,7 @@ export interface ProductFormula { export interface CounterConfig { customerBalance: number; customerId: number; - products: Record; + products: Record; formulas: ProductFormula[]; formInitial: InitialFormData[]; cancelUrl: string; @@ -26,10 +26,11 @@ interface Price { amount: number; } -export interface Product { +export interface CounterItem { + productId: number; + price: Price; code: string; name: string; - price: Price; hasTrayPrice: boolean; quantityForTrayPrice: number; } diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index 932cc965..44220599 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -202,7 +202,7 @@
- {% if not products %} + {% if not prices %}
{% trans %}No products available on this counter for this user{% endtrans %}
@@ -242,9 +242,11 @@ {{ super() }}