feat: упростить создание заказов и рефакторинг единиц измерения
- Добавить inline-редактирование цен в списке товаров - Оптимизировать карточки товаров в POS-терминале - Рефакторинг моделей единиц измерения - Миграция unit -> base_unit в SalesUnit - Улучшить UI форм создания/редактирования товаров Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user