- Added 'sku' field to ProductKitForm meta fields list - Added SKU label in form labels - Added SKU widget styling in __init__ method with helpful placeholder - Updated productkit_form.html template to display SKU field after name, before description - Updated form field filtering to exclude 'sku' from dynamic loop to prevent duplication 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
265 lines
12 KiB
Python
265 lines
12 KiB
Python
from django import forms
|
||
from django.forms import inlineformset_factory
|
||
from .models import Product, ProductKit, ProductCategory, ProductTag, ProductPhoto, KitItem, ProductKitPhoto, ProductCategoryPhoto
|
||
|
||
|
||
class ProductForm(forms.ModelForm):
|
||
"""
|
||
Форма для создания и редактирования товара.
|
||
Поле photos НЕ включено в форму - файлы обрабатываются отдельно в view.
|
||
"""
|
||
categories = forms.ModelMultipleChoiceField(
|
||
queryset=ProductCategory.objects.filter(is_active=True),
|
||
widget=forms.CheckboxSelectMultiple,
|
||
required=False,
|
||
label="Категории"
|
||
)
|
||
|
||
tags = forms.ModelMultipleChoiceField(
|
||
queryset=ProductTag.objects.all(),
|
||
widget=forms.CheckboxSelectMultiple,
|
||
required=False,
|
||
label="Теги"
|
||
)
|
||
|
||
class Meta:
|
||
model = Product
|
||
fields = [
|
||
'name', 'sku', 'description', 'categories',
|
||
'tags', 'unit', 'cost_price', 'sale_price', 'is_active'
|
||
]
|
||
labels = {
|
||
'name': 'Название',
|
||
'sku': 'Артикул',
|
||
'description': 'Описание',
|
||
'categories': 'Категории',
|
||
'tags': 'Теги',
|
||
'unit': 'Единица измерения',
|
||
'cost_price': 'Себестоимость',
|
||
'sale_price': 'Цена продажи',
|
||
'is_active': 'Активен'
|
||
}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
# Make fields more user-friendly
|
||
self.fields['name'].widget.attrs.update({
|
||
'class': 'form-control form-control-lg fw-semibold',
|
||
'placeholder': 'Введите название товара'
|
||
})
|
||
self.fields['sku'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'placeholder': 'Артикул (необязательно, будет сгенерирован автоматически)'
|
||
})
|
||
self.fields['description'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'rows': 3
|
||
})
|
||
self.fields['cost_price'].widget.attrs.update({'class': 'form-control'})
|
||
self.fields['sale_price'].widget.attrs.update({'class': 'form-control'})
|
||
self.fields['unit'].widget.attrs.update({'class': 'form-control'})
|
||
self.fields['is_active'].widget.attrs.update({'class': 'form-check-input'})
|
||
|
||
|
||
class ProductKitForm(forms.ModelForm):
|
||
"""
|
||
Форма для создания и редактирования комплекта.
|
||
"""
|
||
categories = forms.ModelMultipleChoiceField(
|
||
queryset=ProductCategory.objects.filter(is_active=True),
|
||
widget=forms.CheckboxSelectMultiple,
|
||
required=False,
|
||
label="Категории"
|
||
)
|
||
|
||
tags = forms.ModelMultipleChoiceField(
|
||
queryset=ProductTag.objects.all(),
|
||
widget=forms.CheckboxSelectMultiple,
|
||
required=False,
|
||
label="Теги"
|
||
)
|
||
|
||
class Meta:
|
||
model = ProductKit
|
||
fields = [
|
||
'name', 'sku', 'description', 'categories',
|
||
'tags', 'pricing_method', 'fixed_price', 'markup_percent', 'markup_amount', 'is_active'
|
||
]
|
||
labels = {
|
||
'name': 'Название',
|
||
'sku': 'Артикул',
|
||
'description': 'Описание',
|
||
'categories': 'Категории',
|
||
'tags': 'Теги',
|
||
'pricing_method': 'Метод ценообразования',
|
||
'fixed_price': 'Фиксированная цена',
|
||
'markup_percent': 'Процент наценки',
|
||
'markup_amount': 'Фиксированная наценка',
|
||
'is_active': 'Активен'
|
||
}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
# Make fields more user-friendly
|
||
self.fields['name'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'placeholder': 'Введите название комплекта'
|
||
})
|
||
self.fields['sku'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'placeholder': 'Артикул (необязательно, будет сгенерирован автоматически)'
|
||
})
|
||
self.fields['description'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'rows': 3
|
||
})
|
||
self.fields['pricing_method'].widget.attrs.update({'class': 'form-control'})
|
||
self.fields['fixed_price'].widget.attrs.update({'class': 'form-control'})
|
||
self.fields['markup_percent'].widget.attrs.update({'class': 'form-control'})
|
||
self.fields['markup_amount'].widget.attrs.update({'class': 'form-control'})
|
||
self.fields['is_active'].widget.attrs.update({'class': 'form-check-input'})
|
||
|
||
|
||
class KitItemForm(forms.ModelForm):
|
||
"""
|
||
Форма для одного компонента комплекта.
|
||
Валидирует, что указан либо product, либо variant_group (но не оба).
|
||
Если обе поля пусты - это пустая форма, которая будет удалена.
|
||
"""
|
||
class Meta:
|
||
model = KitItem
|
||
fields = ['product', 'variant_group', 'quantity', 'notes']
|
||
labels = {
|
||
'product': 'Конкретный товар',
|
||
'variant_group': 'Группа вариантов',
|
||
'quantity': 'Количество',
|
||
'notes': 'Примечание'
|
||
}
|
||
widgets = {
|
||
'product': forms.Select(attrs={'class': 'form-control'}),
|
||
'variant_group': forms.Select(attrs={'class': 'form-control'}),
|
||
'quantity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001', 'min': '0'}),
|
||
'notes': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Опциональное примечание'}),
|
||
}
|
||
|
||
def clean(self):
|
||
cleaned_data = super().clean()
|
||
product = cleaned_data.get('product')
|
||
variant_group = cleaned_data.get('variant_group')
|
||
|
||
# Если оба поля пусты - это пустая форма (не валидируем, она будет удалена)
|
||
if not product and not variant_group:
|
||
return cleaned_data
|
||
|
||
# Валидация: должен быть указан либо product, либо variant_group (но не оба)
|
||
if product and variant_group:
|
||
raise forms.ValidationError(
|
||
"Нельзя указывать одновременно товар и группу вариантов. Выберите что-то одно."
|
||
)
|
||
|
||
return cleaned_data
|
||
|
||
|
||
# Формсет для создания комплектов (с пустой формой для удобства)
|
||
KitItemFormSetCreate = inlineformset_factory(
|
||
ProductKit,
|
||
KitItem,
|
||
form=KitItemForm,
|
||
fields=['id', 'product', 'variant_group', 'quantity', 'notes'],
|
||
extra=1, # Показать 1 пустую форму для первого компонента
|
||
can_delete=True, # Разрешить удаление компонентов
|
||
min_num=0, # Минимум 0 компонентов (можно создать пустой комплект)
|
||
validate_min=False, # Не требовать минимум компонентов
|
||
can_delete_extra=True, # Разрешить удалять дополнительные формы
|
||
)
|
||
|
||
# Формсет для редактирования комплектов (без пустых форм, только существующие компоненты)
|
||
KitItemFormSetUpdate = inlineformset_factory(
|
||
ProductKit,
|
||
KitItem,
|
||
form=KitItemForm,
|
||
fields=['id', 'product', 'variant_group', 'quantity', 'notes'],
|
||
extra=0, # НЕ показывать пустые формы при редактировании
|
||
can_delete=True, # Разрешить удаление компонентов
|
||
min_num=0, # Минимум 0 компонентов
|
||
validate_min=False, # Не требовать минимум компонентов
|
||
can_delete_extra=True, # Разрешить удалять дополнительные формы
|
||
)
|
||
|
||
# Для обратной совместимости (если где-то еще используется KitItemFormSet)
|
||
KitItemFormSet = KitItemFormSetCreate
|
||
|
||
|
||
class ProductCategoryForm(forms.ModelForm):
|
||
"""
|
||
Форма для создания и редактирования категории товаров.
|
||
Поле photos НЕ включено в форму - файлы обрабатываются отдельно в view.
|
||
"""
|
||
parent = forms.ModelChoiceField(
|
||
queryset=ProductCategory.objects.filter(is_active=True),
|
||
required=False,
|
||
empty_label="Нет (корневая категория)",
|
||
label="Родительская категория",
|
||
widget=forms.Select(attrs={'class': 'form-control'})
|
||
)
|
||
|
||
class Meta:
|
||
model = ProductCategory
|
||
fields = ['name', 'sku', 'slug', 'parent', 'is_active']
|
||
labels = {
|
||
'name': 'Название',
|
||
'sku': 'Артикул',
|
||
'slug': 'URL-идентификатор',
|
||
'parent': 'Родительская категория',
|
||
'is_active': 'Активна'
|
||
}
|
||
help_texts = {
|
||
'sku': 'Оставьте пустым для автоматической генерации (CAT-XXXX)',
|
||
'slug': 'Оставьте пустым для автоматической генерации из названия',
|
||
}
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
# Make fields more user-friendly
|
||
self.fields['name'].widget.attrs.update({
|
||
'class': 'form-control form-control-lg fw-semibold',
|
||
'placeholder': 'Введите название категории'
|
||
})
|
||
self.fields['sku'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'placeholder': 'CAT-XXXX (автоматически)'
|
||
})
|
||
self.fields['slug'].widget.attrs.update({
|
||
'class': 'form-control',
|
||
'placeholder': 'url-identifier (автоматически)'
|
||
})
|
||
self.fields['slug'].required = False # Делаем поле необязательным
|
||
self.fields['is_active'].widget.attrs.update({'class': 'form-check-input'})
|
||
|
||
# Исключаем текущую категорию и её потомков из списка родительских
|
||
# (чтобы не создать циклическую зависимость)
|
||
if self.instance and self.instance.pk:
|
||
# Получаем все потомки текущей категории
|
||
descendants = self._get_descendants(self.instance)
|
||
# Исключаем текущую категорию и все её потомки
|
||
exclude_ids = [self.instance.pk] + [cat.pk for cat in descendants]
|
||
self.fields['parent'].queryset = ProductCategory.objects.filter(
|
||
is_active=True
|
||
).exclude(pk__in=exclude_ids)
|
||
|
||
def clean_slug(self):
|
||
"""Преобразуем пустую строку в None для автогенерации slug"""
|
||
slug = self.cleaned_data.get('slug')
|
||
if slug == '' or slug is None:
|
||
return None
|
||
return slug
|
||
|
||
def _get_descendants(self, category):
|
||
"""Рекурсивно получает всех потомков категории"""
|
||
descendants = []
|
||
children = category.children.all()
|
||
for child in children:
|
||
descendants.append(child)
|
||
descendants.extend(self._get_descendants(child))
|
||
return descendants
|