feat: упростить создание заказов и рефакторинг единиц измерения

- Добавить inline-редактирование цен в списке товаров
- Оптимизировать карточки товаров в POS-терминале
- Рефакторинг моделей единиц измерения
- Миграция unit -> base_unit в SalesUnit
- Улучшить UI форм создания/редактирования товаров

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-17 03:34:43 +03:00
parent 928b340486
commit 2f1f0621e6
24 changed files with 1079 additions and 227 deletions

View File

@@ -156,8 +156,8 @@ class ProductForm(SKUUniqueMixin, forms.ModelForm):
self.fields['base_unit'].queryset = UnitOfMeasure.objects.filter(
is_active=True
).order_by('position', 'code')
self.fields['base_unit'].required = False
self.fields['base_unit'].help_text = 'Базовая единица для учета товара на складе. На основе этой единицы можно создать единицы продажи.'
self.fields['base_unit'].required = True
self.fields['base_unit'].help_text = 'Базовая единица хранения и закупки. На её основе создаются единицы продажи.'
# Маркетинговые флаги (switch-стиль)
for flag_field in ['is_new', 'is_popular', 'is_special']:
@@ -1085,13 +1085,12 @@ class ProductSalesUnitForm(forms.ModelForm):
class Meta:
model = ProductSalesUnit
fields = [
'product', 'unit', 'name', 'conversion_factor',
'product', 'name', 'conversion_factor',
'price', 'sale_price', 'min_quantity', 'quantity_step',
'is_default', 'is_active', 'position'
]
labels = {
'product': 'Товар',
'unit': 'Единица измерения',
'name': 'Название',
'conversion_factor': 'Коэффициент конверсии',
'price': 'Цена продажи',
@@ -1104,7 +1103,6 @@ class ProductSalesUnitForm(forms.ModelForm):
}
widgets = {
'product': forms.Select(attrs={'class': 'form-control'}),
'unit': forms.Select(attrs={'class': 'form-control'}),
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Например: Ветка большая, Стебель средний'
@@ -1155,11 +1153,6 @@ class ProductSalesUnitForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Фильтруем только активные единицы измерения
self.fields['unit'].queryset = UnitOfMeasure.objects.filter(
is_active=True
).order_by('position', 'code')
# Фильтруем только активные товары
self.fields['product'].queryset = Product.objects.filter(
status='active'
@@ -1167,3 +1160,150 @@ class ProductSalesUnitForm(forms.ModelForm):
# Сделать sale_price необязательным
self.fields['sale_price'].required = False
class UnitOfMeasureForm(forms.ModelForm):
"""
Форма для создания и редактирования единицы измерения
"""
class Meta:
model = UnitOfMeasure
fields = ['code', 'name', 'short_name', 'position', 'is_active']
labels = {
'code': 'Код',
'name': 'Название',
'short_name': 'Сокращение',
'position': 'Порядок сортировки',
'is_active': 'Активна',
}
widgets = {
'code': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'шт, кг, банч'
}),
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Штука, Килограмм, Банч'
}),
'short_name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'шт., кг., бан.'
}),
'position': forms.NumberInput(attrs={
'class': 'form-control',
'value': '0'
}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}
help_texts = {
'code': 'Короткий уникальный код (используется в системе)',
'name': 'Полное название для отображения',
'short_name': 'Сокращённое название для таблиц',
'position': 'Порядок в списках (меньше = выше)',
}
# === INLINE FORMSET ДЛЯ ЕДИНИЦ ПРОДАЖИ ===
class ProductSalesUnitInlineForm(forms.ModelForm):
"""
Форма единицы продажи для inline редактирования в форме товара
"""
class Meta:
model = ProductSalesUnit
fields = [
'name', 'conversion_factor',
'price', 'sale_price', 'min_quantity', 'quantity_step',
'is_default', 'is_active', 'position'
]
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Ветка большая'
}),
'conversion_factor': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.000001',
'min': '0.000001',
'placeholder': '15.0'
}),
'price': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0',
}),
'sale_price': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0',
}),
'min_quantity': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.001',
'min': '0.001',
'value': '1'
}),
'quantity_step': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.001',
'min': '0.001',
'value': '1'
}),
'is_default': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'position': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'style': 'width: 60px;'
}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['sale_price'].required = False
def has_changed(self):
"""
Считаем форму неизмененной, если это новая форма без заполненных полей.
Это позволяет избежать ошибок валидации для пустых добавленных форм.
"""
# Если это существующая запись - используем стандартную логику
if self.instance.pk:
return super().has_changed()
# Для новых форм проверяем, есть ли заполненные данные
try:
# Проверяем ключевые поля
cleaned_data = getattr(self, 'cleaned_data', {})
if cleaned_data.get('name'):
return True
if cleaned_data.get('price'):
return True
# Если cleaned_data ещё нет, проверяем raw data
data = self.data if hasattr(self, 'data') else {}
prefix = self.prefix
name_field = f'{prefix}-name'
price_field = f'{prefix}-price'
if data.get(name_field):
return True
if data.get(price_field):
return True
# Форма пустая - считаем неизмененной
return False
except Exception:
# При ошибке используем стандартную логику
return super().has_changed()
# Inline formset для единиц продажи
ProductSalesUnitFormSet = inlineformset_factory(
Product,
ProductSalesUnit,
form=ProductSalesUnitInlineForm,
extra=1,
can_delete=True,
min_num=0,
validate_min=False,
)