feat(products): реализована система единиц продажи на фронтенде
Добавлена полноценная интеграция единиц измерения (UoM) для продажи товаров в разных единицах с автоматическим пересчётом цен и остатков. ## Основные изменения: ### Backend - Расширен API поиска товаров (api_views.py): добавлена сериализация sales_units - Создан новый endpoint get_product_sales_units_api для загрузки единиц с остатками - Добавлено поле sales_unit в OrderItemForm и SaleForm с валидацией - Созданы CRUD views для управления единицами продажи (uom_views.py) - Обновлена ProductForm: использует base_unit вместо устаревшего unit ### Frontend - Создан модуль sales-units.js с функциями для работы с единицами - Интегрирован в select2-product-search.js: автозагрузка единиц при выборе товара - Добавлены контейнеры для единиц в order_form.html и sale_form.html - Реализовано автоматическое обновление цены при смене единицы продажи - При выборе базовой единицы цена возвращается к базовой цене товара ### UI - Добавлены страницы управления единицами продажи в навбар - Созданы шаблоны: sales_unit_list.html, sales_unit_form.html, sales_unit_delete.html - Добавлены фильтры по товару, единице, активности и дефолтности ## Исправленные ошибки: - Порядок инициализации: обработчики устанавливаются ДО триггера события change - Цена корректно обновляется при выборе единицы продажи - При выборе "Базовая единица" возвращается базовая цена товара 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -456,13 +456,14 @@ class OrderItemForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = OrderItem
|
||||
fields = ['product', 'product_kit', 'quantity', 'price', 'is_custom_price']
|
||||
fields = ['product', 'product_kit', 'sales_unit', 'quantity', 'price', 'is_custom_price']
|
||||
# ВАЖНО: НЕ включаем 'id' в fields - это предотвращает ошибку валидации
|
||||
widgets = {
|
||||
'quantity': forms.NumberInput(attrs={'min': 1, 'value': 1}),
|
||||
# Скрываем поля product и product_kit - они будут заполняться через JS
|
||||
'product': forms.HiddenInput(),
|
||||
'product_kit': forms.HiddenInput(),
|
||||
'sales_unit': forms.HiddenInput(), # Управляется через JS
|
||||
'is_custom_price': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
@@ -478,6 +479,9 @@ class OrderItemForm(forms.ModelForm):
|
||||
self.fields['product'].required = False
|
||||
self.fields['product_kit'].required = False
|
||||
|
||||
# Поле sales_unit опционально (управляется через JS)
|
||||
self.fields['sales_unit'].required = False
|
||||
|
||||
# Поле цены заполняется автоматически, но можно редактировать вручную
|
||||
self.fields['price'].widget.attrs.update({
|
||||
'placeholder': 'Цена',
|
||||
@@ -506,6 +510,7 @@ class OrderItemForm(forms.ModelForm):
|
||||
cleaned_data = super().clean()
|
||||
product = cleaned_data.get('product')
|
||||
product_kit = cleaned_data.get('product_kit')
|
||||
sales_unit = cleaned_data.get('sales_unit')
|
||||
quantity = cleaned_data.get('quantity')
|
||||
|
||||
# Пустая форма - это нормально (будет удалена)
|
||||
@@ -525,6 +530,17 @@ class OrderItemForm(forms.ModelForm):
|
||||
if not quantity or quantity <= 0:
|
||||
raise forms.ValidationError('Необходимо указать количество больше 0')
|
||||
|
||||
# Валидация единицы продажи
|
||||
if sales_unit:
|
||||
if product and sales_unit.product_id != product.id:
|
||||
raise forms.ValidationError('Единица продажи не принадлежит товару')
|
||||
|
||||
if quantity:
|
||||
try:
|
||||
sales_unit.validate_quantity(quantity)
|
||||
except ValidationError as e:
|
||||
raise forms.ValidationError(str(e))
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
{{ item_form.id }}
|
||||
{{ item_form.product }} <!-- Hidden field -->
|
||||
{{ item_form.product_kit }} <!-- Hidden field -->
|
||||
{{ item_form.sales_unit }} <!-- Hidden field -->
|
||||
{{ item_form.is_custom_price }} <!-- Hidden field -->
|
||||
|
||||
<div class="row align-items-end">
|
||||
@@ -229,6 +230,9 @@
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Контейнер для единиц продажи (управляется JS) -->
|
||||
<div class="sales-unit-container mb-2" style="display: none;"></div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="mb-2">
|
||||
@@ -295,6 +299,7 @@
|
||||
<input type="hidden" name="items-__prefix__-id" id="id_items-__prefix__-id">
|
||||
<input type="hidden" name="items-__prefix__-product" id="id_items-__prefix__-product">
|
||||
<input type="hidden" name="items-__prefix__-product_kit" id="id_items-__prefix__-product_kit">
|
||||
<input type="hidden" name="items-__prefix__-sales_unit" id="id_items-__prefix__-sales_unit">
|
||||
<input type="hidden" name="items-__prefix__-is_custom_price" id="id_items-__prefix__-is_custom_price" value="false">
|
||||
|
||||
<div class="row align-items-end">
|
||||
@@ -308,6 +313,9 @@
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Контейнер для единиц продажи (управляется JS) -->
|
||||
<div class="sales-unit-container mb-2" style="display: none;"></div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="mb-2">
|
||||
@@ -1809,7 +1817,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Подключение модуля Select2 для поиска товаров/комплектов -->
|
||||
<!-- Подключение модулей для работы с единицами продажи и поиском товаров -->
|
||||
<script src="{% static 'products/js/sales-units.js' %}"></script>
|
||||
<script src="{% static 'products/js/select2-product-search.js' %}"></script>
|
||||
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user