Files
octopus/myproject/products/forms.py
Andrey Smakotin 70f05abdb9 feat: Add SKU field to ProductKit form for kit creation
- 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>
2025-10-22 16:39:49 +03:00

265 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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