Price formset in the Product edit page

This commit is contained in:
imperosol
2026-03-05 19:07:42 +01:00
parent 5126dc2a82
commit 5f7a9fc600
3 changed files with 126 additions and 18 deletions

View File

@@ -123,7 +123,7 @@ class GroupController(ControllerBase):
) )
@paginate(PageNumberPaginationExtra, page_size=50) @paginate(PageNumberPaginationExtra, page_size=50)
def search_group(self, search: Annotated[str, MinLen(1)]): def search_group(self, search: Annotated[str, MinLen(1)]):
return Group.objects.filter(name__icontains=search).values() return Group.objects.filter(name__icontains=search).order_by("name").values()
DepthValue = Annotated[int, Ge(0), Le(10)] DepthValue = Annotated[int, Ge(0), Le(10)]

View File

@@ -1,6 +1,7 @@
import json import json
import math import math
import uuid import uuid
from collections import defaultdict
from datetime import date, datetime, timezone from datetime import date, datetime, timezone
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
@@ -35,6 +36,7 @@ from counter.models import (
Customer, Customer,
Eticket, Eticket,
InvoiceCall, InvoiceCall,
Price,
Product, Product,
ProductFormula, ProductFormula,
Refilling, Refilling,
@@ -292,7 +294,21 @@ ScheduledProductActionFormSet = forms.modelformset_factory(
can_delete=True, can_delete=True,
can_delete_extra=False, can_delete_extra=False,
extra=0, extra=0,
)
ProductPriceFormSet = forms.inlineformset_factory(
parent_model=Product,
model=Price,
fields=["amount", "label", "groups", "is_always_shown"],
widgets={
"groups": AutoCompleteSelectMultipleGroup,
"is_always_shown": forms.CheckboxInput(attrs={"class": "switch"}),
},
absolute_max=None,
can_delete_extra=False,
min_num=1, min_num=1,
extra=0,
) )
@@ -307,10 +323,7 @@ class ProductForm(forms.ModelForm):
"description", "description",
"product_type", "product_type",
"code", "code",
"buying_groups",
"purchase_price", "purchase_price",
"selling_price",
"special_selling_price",
"icon", "icon",
"club", "club",
"limit_age", "limit_age",
@@ -325,8 +338,8 @@ class ProductForm(forms.ModelForm):
} }
widgets = { widgets = {
"product_type": AutoCompleteSelect, "product_type": AutoCompleteSelect,
"buying_groups": AutoCompleteSelectMultipleGroup,
"club": AutoCompleteSelectClub, "club": AutoCompleteSelectClub,
"tray": forms.CheckboxInput(attrs={"class": "switch"}),
} }
counters = forms.ModelMultipleChoiceField( counters = forms.ModelMultipleChoiceField(
@@ -336,14 +349,18 @@ class ProductForm(forms.ModelForm):
queryset=Counter.objects.all(), queryset=Counter.objects.all(),
) )
def __init__(self, *args, instance=None, **kwargs): def __init__(self, *args, prefix: str | None = None, instance=None, **kwargs):
super().__init__(*args, instance=instance, **kwargs) super().__init__(*args, prefix=prefix, instance=instance, **kwargs)
self.fields["name"].widget.attrs["autofocus"] = "autofocus"
if self.instance.id: if self.instance.id:
self.fields["counters"].initial = self.instance.counters.all() self.fields["counters"].initial = self.instance.counters.all()
if hasattr(self.instance, "formula"): if hasattr(self.instance, "formula"):
self.formula_init(self.instance.formula) self.formula_init(self.instance.formula)
self.price_formset = ProductPriceFormSet(
*args, instance=self.instance, prefix="price", **kwargs
)
self.action_formset = ScheduledProductActionFormSet( self.action_formset = ScheduledProductActionFormSet(
*args, product=self.instance, **kwargs *args, product=self.instance, prefix="action", **kwargs
) )
def formula_init(self, formula: ProductFormula): def formula_init(self, formula: ProductFormula):
@@ -366,20 +383,25 @@ class ProductForm(forms.ModelForm):
self.fields[key].validators.append(MaxValueValidator(price)) self.fields[key].validators.append(MaxValueValidator(price))
def is_valid(self): def is_valid(self):
return super().is_valid() and self.action_formset.is_valid() return (
super().is_valid()
and self.price_formset.is_valid()
and self.action_formset.is_valid()
)
def save(self, *args, **kwargs) -> Product: def save(self, *args, **kwargs) -> Product:
product = super().save(*args, **kwargs) product = super().save(*args, **kwargs)
product.counters.set(self.cleaned_data["counters"]) product.counters.set(self.cleaned_data["counters"])
# if it's a creation, the product given in the formset
# wasn't a persisted instance.
# So if we tried to persist the related objects in the current state,
# they would be linked to no product, thus be completely useless
# To make it work, we have to replace
# the initial product with a persisted one
for form in self.action_formset: for form in self.action_formset:
# if it's a creation, the product given in the formset
# wasn't a persisted instance.
# So if we tried to persist the scheduled actions in the current state,
# they would be linked to no product, thus be completely useless
# To make it work, we have to replace
# the initial product with a persisted one
form.set_product(product) form.set_product(product)
self.action_formset.save() self.action_formset.save()
self.price_formset.save()
return product return product

View File

@@ -39,6 +39,44 @@
{% endmacro %} {% endmacro %}
{% macro price_form(form) %}
<fieldset>
{{ form.non_field_errors() }}
<div class="form-group row gap-2x">
<div>{{ form.amount.as_field_group() }}</div>
<div>
{{ form.label.errors }}
<label for="{{ form.label.id_for_label }}">{{ form.label.label }}</label>
{{ form.label }}
<span class="helptext">{{ form.label.help_text }}</span>
</div>
<div class="grow">{{ form.groups.as_field_group() }}</div>
</div>
<div class="form-group">
<div>
{{ form.is_always_shown.as_field_group() }}
</div>
</div>
{%- if form.DELETE -%}
<div class="form-group row gap">
{{ form.DELETE.as_field_group() }}
</div>
{%- else -%}
<br>
<button
class="btn btn-grey"
@click.prevent="removeForm($event.target.closest('fieldset').parentElement)"
>
<i class="fa fa-minus"></i> {% trans %}Remove price{% endtrans %}
</button>
{%- endif -%}
{%- for field in form.hidden_fields() -%}
{{ field }}
{%- endfor -%}
</fieldset>
<hr class="margin-bottom">
{% endmacro %}
{% block content %} {% block content %}
{% if object %} {% if object %}
<h2>{% trans name=object %}Edit product {{ name }}{% endtrans %}</h2> <h2>{% trans name=object %}Edit product {{ name }}{% endtrans %}</h2>
@@ -49,7 +87,54 @@
{% endif %} {% endif %}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ form.as_p() }} {{ form.non_field_errors() }}
<fieldset class="row gap">
<div>{{ form.name.as_field_group() }}</div>
<div>{{ form.code.as_field_group() }}</div>
</fieldset>
<fieldset>
<div class="form-group">{{ form.description.as_field_group() }}</div>
</fieldset>
<fieldset class="row gap">
<div>{{ form.club.as_field_group() }}</div>
<div>{{ form.product_type.as_field_group() }}</div>
</fieldset>
<fieldset><div>{{ form.icon.as_field_group() }}</div></fieldset>
<fieldset><div>{{ form.purchase_price.as_field_group() }}</div></fieldset>
<fieldset>
<div>{{ form.limit_age.as_field_group() }}</div>
</fieldset>
<fieldset>
<div class="row gap">
{{ form.tray }}
<div>
{{ form.tray.label_tag() }}
<span class="helptext">{{ form.tray.help_text }}</span>
</div>
</div>
</fieldset>
<fieldset><div>{{ form.counters.as_field_group() }}</div></fieldset>
<h3 class="margin-bottom">{% trans %}Prices{% endtrans %}</h3>
<div x-data="dynamicFormSet({ prefix: '{{ form.price_formset.prefix }}' })">
{{ form.price_formset.management_form }}
<div x-ref="formContainer">
{%- for form in form.price_formset.forms -%}
<div>
{{ price_form(form) }}
</div>
{%- endfor -%}
</div>
<template x-ref="formTemplate">
<div>
{{ price_form(form.price_formset.empty_form) }}
</div>
</template>
<button class="btn btn-grey" @click.prevent="addForm()">
<i class="fa fa-plus"></i> {% trans %}Add a price{% endtrans %}
</button>
</div>
<br /> <br />
@@ -64,7 +149,7 @@
</em> </em>
</p> </p>
<div x-data="dynamicFormSet" class="margin-bottom"> <div x-data="dynamicFormSet({ prefix: '{{ form.action_formset.prefix }}' })" class="margin-bottom">
{{ form.action_formset.management_form }} {{ form.action_formset.management_form }}
<div x-ref="formContainer"> <div x-ref="formContainer">
{%- for f in form.action_formset.forms -%} {%- for f in form.action_formset.forms -%}
@@ -78,6 +163,7 @@
<i class="fa fa-plus"></i>{% trans %}Add action{% endtrans %} <i class="fa fa-plus"></i>{% trans %}Add action{% endtrans %}
</button> </button>
</div> </div>
<div class="row gap margin-bottom">{{ form.archived.as_field_group() }}</div>
<p><input class="btn btn-blue" type="submit" value="{% trans %}Save{% endtrans %}" /></p> <p><input class="btn btn-blue" type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</form> </form>
{% endblock %} {% endblock %}