from django import forms from django.forms import inlineformset_factory from .models import Product, ProductKit, ProductCategory, ProductTag, ProductPhoto, KitItem, ProductKitPhoto, ProductCategoryPhoto, ProductVariantGroup, ProductVariantGroupItem 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', 'short_description', 'categories', 'tags', 'unit', 'cost_price', 'price', 'sale_price', 'is_active' ] labels = { 'name': 'Название', 'sku': 'Артикул', 'description': 'Описание', 'short_description': 'Краткое описание', 'categories': 'Категории', 'tags': 'Теги', 'unit': 'Единица измерения', 'cost_price': 'Себестоимость', '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['short_description'].widget.attrs.update({ 'class': 'form-control', 'rows': 2, 'placeholder': 'Краткое описание для превью и площадок' }) self.fields['cost_price'].widget.attrs.update({'class': 'form-control'}) self.fields['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', 'short_description', 'categories', 'tags', 'sale_price', 'price_adjustment_type', 'price_adjustment_value', 'is_active' ] labels = { 'name': 'Название', 'sku': 'Артикул', 'description': 'Описание', 'short_description': 'Краткое описание', 'categories': 'Категории', 'tags': 'Теги', 'sale_price': 'Цена со скидкой', 'price_adjustment_type': 'Как изменить итоговую цену', 'price_adjustment_value': 'Значение корректировки', '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['short_description'].widget.attrs.update({ 'class': 'form-control', 'rows': 2, 'placeholder': 'Краткое описание для превью и площадок' }) self.fields['sale_price'].widget.attrs.update({'class': 'form-control'}) self.fields['price_adjustment_type'].widget.attrs.update({'class': 'form-control'}) self.fields['price_adjustment_value'].widget.attrs.update({ 'class': 'form-control', 'step': '0.01', 'placeholder': '0' }) self.fields['is_active'].widget.attrs.update({'class': 'form-check-input'}) def clean(self): """ Валидация формы комплекта. Проверяет: 1. Что если выбран тип корректировки, указано значение 2. Что заполнено максимум одно поле корректировки (увеличение или уменьшение) """ cleaned_data = super().clean() adjustment_type = cleaned_data.get('price_adjustment_type') adjustment_value = cleaned_data.get('price_adjustment_value') # Если выбран тип корректировки (не 'none'), значение обязательно if adjustment_type and adjustment_type != 'none': if not adjustment_value or adjustment_value == 0: raise forms.ValidationError( 'Укажите значение корректировки цены (> 0)' ) return cleaned_data class KitItemForm(forms.ModelForm): """ Форма для одного компонента комплекта. Валидирует, что указан либо product, либо variant_group (но не оба). Если обе поля пусты - это пустая форма, которая будет удалена. """ class Meta: model = KitItem fields = ['product', 'variant_group', 'quantity'] labels = { 'product': 'Конкретный товар', 'variant_group': 'Группа вариантов', 'quantity': 'Количество' } 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'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Устанавливаем значение по умолчанию для quantity = 1 if not self.instance.pk: # Только для новых форм (создание, не редактирование) self.fields['quantity'].initial = 1 def clean(self): cleaned_data = super().clean() product = cleaned_data.get('product') variant_group = cleaned_data.get('variant_group') quantity = cleaned_data.get('quantity') # Если оба поля пусты - это пустая форма (не валидируем, она будет удалена) if not product and not variant_group: # Для пустых форм обнуляем количество cleaned_data['quantity'] = None return cleaned_data # Валидация: должен быть указан либо product, либо variant_group (но не оба) if product and variant_group: raise forms.ValidationError( "Нельзя указывать одновременно товар и группу вариантов. Выберите что-то одно." ) # Валидация: если выбран товар/группа, количество обязательно и должно быть > 0 if (product or variant_group): if not quantity or quantity <= 0: raise forms.ValidationError('Необходимо указать количество больше 0') return cleaned_data # Кастомный базовый формсет с валидацией на дубликаты class BaseKitItemFormSet(forms.BaseInlineFormSet): def clean(self): """Проверка на дубликаты товаров в комплекте""" if any(self.errors): # Не проверяем дубликаты если есть другие ошибки return products = [] variant_groups = [] for form in self.forms: if self.can_delete and self._should_delete_form(form): continue product = form.cleaned_data.get('product') variant_group = form.cleaned_data.get('variant_group') # Проверка дубликатов товаров if product: if product in products: raise forms.ValidationError( f'Товар "{product.name}" добавлен в комплект более одного раза. ' f'Каждый товар может быть добавлен только один раз.' ) products.append(product) # Проверка дубликатов групп вариантов if variant_group: if variant_group in variant_groups: raise forms.ValidationError( f'Группа вариантов "{variant_group.name}" добавлена более одного раза. ' f'Каждая группа может быть добавлена только один раз.' ) variant_groups.append(variant_group) # Формсет для создания комплектов (с пустой формой для удобства) KitItemFormSetCreate = inlineformset_factory( ProductKit, KitItem, form=KitItemForm, formset=BaseKitItemFormSet, fields=['id', 'product', 'variant_group', 'quantity'], extra=1, # Показать 1 пустую форму для первого компонента can_delete=True, # Разрешить удаление компонентов min_num=0, # Минимум 0 компонентов (можно создать пустой комплект) validate_min=False, # Не требовать минимум компонентов can_delete_extra=True, # Разрешить удалять дополнительные формы ) # Формсет для редактирования комплектов (без пустых форм, только существующие компоненты) KitItemFormSetUpdate = inlineformset_factory( ProductKit, KitItem, form=KitItemForm, formset=BaseKitItemFormSet, fields=['id', 'product', 'variant_group', 'quantity'], 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 # ==================== VARIANT GROUP FORMS ==================== class ProductVariantGroupForm(forms.ModelForm): """ Форма для создания и редактирования группы вариантов товара. """ class Meta: model = ProductVariantGroup fields = ['name', 'description'] labels = { 'name': 'Название группы', 'description': 'Описание (для какого букета или ситуации создана)' } widgets = { 'name': forms.TextInput(attrs={ 'class': 'form-control form-control-lg fw-semibold', 'placeholder': 'Например: Роза красная Freedom' }), 'description': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Описание группы и её использования' }), } class ProductVariantGroupItemForm(forms.ModelForm): """ Форма для добавления товара в группу с приоритетом. """ class Meta: model = ProductVariantGroupItem fields = ['product', 'priority'] labels = { 'product': 'Товар', 'priority': 'Приоритет' } widgets = { 'product': forms.Select(attrs={'class': 'form-control form-control-sm'}), 'priority': forms.NumberInput(attrs={ 'class': 'form-control form-control-sm', 'min': '1', 'placeholder': '1, 2, 3...' }), } # Базовый формсет с валидацией на дубликаты class BaseProductVariantGroupItemFormSet(forms.BaseInlineFormSet): def clean(self): """Проверка на дубликаты товаров в группе""" if any(self.errors): return products = [] for form in self.forms: if self.can_delete and self._should_delete_form(form): continue product = form.cleaned_data.get('product') # Проверка дубликатов товаров if product: if product in products: raise forms.ValidationError( f'Товар "{product.name}" уже добавлен в группу. ' f'Каждый товар может быть добавлен только один раз.' ) products.append(product) # Формсет для создания групп (с пустой формой для добавления товара) ProductVariantGroupItemFormSetCreate = inlineformset_factory( ProductVariantGroup, ProductVariantGroupItem, form=ProductVariantGroupItemForm, formset=BaseProductVariantGroupItemFormSet, fields=['product', 'priority'], extra=1, # Показать 1 пустую форму для первого товара can_delete=True, # Разрешить удаление товаров min_num=0, # Минимум 0 товаров (можно создать пустую группу) validate_min=False, # Не требовать минимум товаров can_delete_extra=True, # Разрешить удалять дополнительные формы ) # Формсет для редактирования групп (без пустых форм, только существующие товары) ProductVariantGroupItemFormSetUpdate = inlineformset_factory( ProductVariantGroup, ProductVariantGroupItem, form=ProductVariantGroupItemForm, formset=BaseProductVariantGroupItemFormSet, fields=['product', 'priority'], extra=0, # НЕ показывать пустые формы при редактировании can_delete=True, # Разрешить удаление товаров min_num=0, # Минимум 0 товаров validate_min=False, # Не требовать минимум товаров can_delete_extra=True, # Разрешить удалять дополнительные формы )