From df8b23a4a195a5ad3340ef15273afc7e2d7a4dc6 Mon Sep 17 00:00:00 2001 From: imperosol Date: Wed, 26 Nov 2025 13:22:43 +0100 Subject: [PATCH] product formulas management views --- counter/forms.py | 26 ++++++++++- counter/templates/counter/formula_list.jinja | 35 +++++++++++++++ counter/urls.py | 22 ++++++++++ counter/views/admin.py | 45 ++++++++++++++++++++ counter/views/mixins.py | 5 +++ 5 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 counter/templates/counter/formula_list.jinja diff --git a/counter/forms.py b/counter/forms.py index ed47232a..43f94720 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -5,6 +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.db.models import Exists, OuterRef, Q from django.forms import BaseModelFormSet from django.utils.timezone import now @@ -34,6 +35,7 @@ from counter.models import ( Eticket, InvoiceCall, Product, + ProductFormula, Refilling, ReturnableProduct, ScheduledProductAction, @@ -349,13 +351,33 @@ class ProductForm(forms.ModelForm): return product +class ProductFormulaForm(forms.ModelForm): + class Meta: + model = ProductFormula + fields = ["products", "result"] + widgets = { + "products": AutoCompleteSelectMultipleProduct, + "result": AutoCompleteSelectProduct, + } + + def clean(self): + cleaned_data = super().clean() + prices = [p.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.") + ) + return cleaned_data + + class ReturnableProductForm(forms.ModelForm): class Meta: model = ReturnableProduct fields = ["product", "returned_product", "max_return"] widgets = { - "product": AutoCompleteSelectProduct(), - "returned_product": AutoCompleteSelectProduct(), + "product": AutoCompleteSelectProduct, + "returned_product": AutoCompleteSelectProduct, } def save(self, commit: bool = True) -> ReturnableProduct: # noqa FBT diff --git a/counter/templates/counter/formula_list.jinja b/counter/templates/counter/formula_list.jinja new file mode 100644 index 00000000..dab19302 --- /dev/null +++ b/counter/templates/counter/formula_list.jinja @@ -0,0 +1,35 @@ +{% extends "core/base.jinja" %} + +{% block title %} + {% trans %}Product formulas{% endtrans %} +{% endblock %} + +{% block additional_css %} + + +{% endblock %} + +{% block content %} +
+

{% trans %}Product formulas{% endtrans %}

+

+ + {% trans %}New formula{% endtrans %} + + +

+ + +
+{% endblock %} diff --git a/counter/urls.py b/counter/urls.py index 67c7d950..9637ecd0 100644 --- a/counter/urls.py +++ b/counter/urls.py @@ -25,6 +25,10 @@ from counter.views.admin import ( CounterStatView, ProductCreateView, ProductEditView, + ProductFormulaCreateView, + ProductFormulaDeleteView, + ProductFormulaEditView, + ProductFormulaListView, ProductListView, ProductTypeCreateView, ProductTypeEditView, @@ -116,6 +120,24 @@ urlpatterns = [ ProductEditView.as_view(), name="product_edit", ), + path( + "admin/formula/", ProductFormulaListView.as_view(), name="product_formula_list" + ), + path( + "admin/formula/new/", + ProductFormulaCreateView.as_view(), + name="product_formula_create", + ), + path( + "admin/formula//edit", + ProductFormulaEditView.as_view(), + name="product_formula_edit", + ), + path( + "admin/formula//delete", + ProductFormulaDeleteView.as_view(), + name="product_formula_delete", + ), path( "admin/product-type/list/", ProductTypeListView.as_view(), diff --git a/counter/views/admin.py b/counter/views/admin.py index 24c112f5..ad48c435 100644 --- a/counter/views/admin.py +++ b/counter/views/admin.py @@ -34,11 +34,13 @@ from counter.forms import ( CloseCustomerAccountForm, CounterEditForm, ProductForm, + ProductFormulaForm, ReturnableProductForm, ) from counter.models import ( Counter, Product, + ProductFormula, ProductType, Refilling, ReturnableProduct, @@ -162,6 +164,49 @@ class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): current_tab = "products" +class ProductFormulaListView(CounterAdminTabsMixin, PermissionRequiredMixin, ListView): + model = ProductFormula + queryset = ProductFormula.objects.select_related("result") + template_name = "counter/formula_list.jinja" + current_tab = "formulas" + permission_required = "counter.view_productformula" + + +class ProductFormulaCreateView( + CounterAdminTabsMixin, PermissionRequiredMixin, CreateView +): + model = ProductFormula + form_class = ProductFormulaForm + pk_url_kwarg = "formula_id" + template_name = "core/create.jinja" + current_tab = "formulas" + success_url = reverse_lazy("counter:product_formula_list") + permission_required = "counter.add_productformula" + + +class ProductFormulaEditView( + CounterAdminTabsMixin, PermissionRequiredMixin, UpdateView +): + model = ProductFormula + form_class = ProductFormulaForm + pk_url_kwarg = "formula_id" + template_name = "core/edit.jinja" + current_tab = "formulas" + success_url = reverse_lazy("counter:product_formula_list") + permission_required = "counter.change_productformula" + + +class ProductFormulaDeleteView( + CounterAdminTabsMixin, PermissionRequiredMixin, DeleteView +): + model = ProductFormula + pk_url_kwarg = "formula_id" + template_name = "core/delete_confirm.jinja" + current_tab = "formulas" + success_url = reverse_lazy("counter:product_formula_list") + permission_required = "counter.delete_productformula" + + class ReturnableProductListView( CounterAdminTabsMixin, PermissionRequiredMixin, ListView ): diff --git a/counter/views/mixins.py b/counter/views/mixins.py index c7fabdd6..2cce25b4 100644 --- a/counter/views/mixins.py +++ b/counter/views/mixins.py @@ -100,6 +100,11 @@ class CounterAdminTabsMixin(TabedViewMixin): "slug": "products", "name": _("Products"), }, + { + "url": reverse_lazy("counter:product_formula_list"), + "slug": "formulas", + "name": _("Formulas"), + }, { "url": reverse_lazy("counter:product_type_list"), "slug": "product_types",