From b0659524adf022e24063d6fe9b4e9b25ed374377 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 26 Nov 2025 17:08:18 +0100 Subject: [PATCH] add checks on ProductForm --- counter/forms.py | 44 +++++++++++++++++++++++++++++++++++++++----- counter/models.py | 13 +++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/counter/forms.py b/counter/forms.py index 43f94720..6a1b83e4 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -5,7 +5,7 @@ from datetime import date, datetime, timezone from dateutil.relativedelta import relativedelta from django import forms -from django.core.exceptions import ValidationError +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 @@ -318,7 +318,6 @@ class ProductForm(forms.ModelForm): } counters = forms.ModelMultipleChoiceField( - help_text=None, label=_("Counters"), required=False, widget=AutoCompleteSelectMultipleCounter, @@ -329,10 +328,31 @@ class ProductForm(forms.ModelForm): super().__init__(*args, instance=instance, **kwargs) if self.instance.id: self.fields["counters"].initial = self.instance.counters.all() + if hasattr(self.instance, "formula"): + self.formula_init(self.instance.formula) self.action_formset = ScheduledProductActionFormSet( *args, product=self.instance, **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() and self.action_formset.is_valid() @@ -362,11 +382,25 @@ class ProductFormulaForm(forms.ModelForm): def clean(self): cleaned_data = super().clean() + if cleaned_data["result"] in cleaned_data["products"]: + self.add_error( + None, + _( + "The same product cannot be at the same time " + "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 - if selling_price > sum(prices): - raise ValidationError( - _("This formula is more expensive than its constituant products.") + 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 0141ff16..21f24ee9 100644 --- a/counter/models.py +++ b/counter/models.py @@ -464,6 +464,7 @@ class ProductFormula(models.Model): ) result = models.OneToOneField( Product, + related_name="formula", on_delete=models.CASCADE, verbose_name=_("result product"), help_text=_("The product got with the formula."), @@ -472,6 +473,18 @@ 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: