diff --git a/myproject/products/forms.py b/myproject/products/forms.py index 8087fd0..aa9f1db 100644 --- a/myproject/products/forms.py +++ b/myproject/products/forms.py @@ -8,7 +8,51 @@ from .models import ( ) -class ProductForm(forms.ModelForm): +class SKUUniqueMixin: + """ + Миксин для валидации уникальности артикула среди всех моделей. + Проверяет уникальность SKU среди: Product, ProductKit, ProductCategory, ConfigurableProduct. + """ + + def clean_sku(self): + """ + Проверяет уникальность артикула среди всех моделей с артикулами. + Если SKU пустой - пропускаем (будет сгенерирован автоматически). + """ + sku = self.cleaned_data.get('sku') + + # Пустое значение - ок, будет автогенерация + if not sku or sku.strip() == '': + return None + + sku = sku.strip() + + # Проверяем во всех моделях + models_to_check = [ + (Product, 'товар'), + (ProductKit, 'комплект'), + (ProductCategory, 'категория'), + (ConfigurableProduct, 'вариативный товар'), + ] + + for model, model_name in models_to_check: + queryset = model.objects.filter(sku=sku) + + # Исключаем текущий объект при редактировании + if self.instance.pk and isinstance(self.instance, model): + queryset = queryset.exclude(pk=self.instance.pk) + + if queryset.exists(): + existing = queryset.first() + raise forms.ValidationError( + f'Артикул "{sku}" уже используется в {model_name} "{existing.name}". ' + f'Пожалуйста, выберите другой артикул.' + ) + + return sku + + +class ProductForm(SKUUniqueMixin, forms.ModelForm): """ Форма для создания и редактирования товара. Поле photos НЕ включено в форму - файлы обрабатываются отдельно в view. @@ -71,10 +115,9 @@ class ProductForm(forms.ModelForm): self.fields['unit'].widget.attrs.update({'class': 'form-control'}) self.fields['status'].widget.attrs.update({'class': 'form-control'}) - def clean(self): + def clean_name(self): """Валидация уникальности имени для активных товаров""" - cleaned_data = super().clean() - name = cleaned_data.get('name') + name = self.cleaned_data.get('name') if name: # Проверяем уникальность имени среди активных товаров @@ -88,15 +131,15 @@ class ProductForm(forms.ModelForm): existing = existing.exclude(pk=self.instance.pk) if existing.exists(): - self.add_error('name', + raise forms.ValidationError( f'Товар с названием "{name}" уже существует. ' f'Пожалуйста, используйте другое название.' ) - return cleaned_data + return name -class ProductKitForm(forms.ModelForm): +class ProductKitForm(SKUUniqueMixin, forms.ModelForm): """ Форма для создания и редактирования комплекта. Цена комплекта вычисляется автоматически из цен компонентов. @@ -163,17 +206,10 @@ class ProductKitForm(forms.ModelForm): }) self.fields['status'].widget.attrs.update({'class': 'form-control'}) - def clean(self): - """ - Валидация формы комплекта. - Проверяет: - 1. Уникальность имени для активных комплектов - 2. Что если выбран тип корректировки, указано значение - """ - cleaned_data = super().clean() + def clean_name(self): + """Валидация уникальности имени для активных комплектов""" + name = self.cleaned_data.get('name') - # Проверяем уникальность имени среди активных комплектов - name = cleaned_data.get('name') if name: existing = ProductKit.objects.filter( name=name, @@ -185,11 +221,17 @@ class ProductKitForm(forms.ModelForm): existing = existing.exclude(pk=self.instance.pk) if existing.exists(): - self.add_error('name', + raise forms.ValidationError( f'Комплект с названием "{name}" уже существует. ' f'Пожалуйста, используйте другое название.' ) + return name + + def clean(self): + """Дополнительная валидация: если выбран тип корректировки, указано значение""" + cleaned_data = super().clean() + adjustment_type = cleaned_data.get('price_adjustment_type') adjustment_value = cleaned_data.get('price_adjustment_value') @@ -323,7 +365,7 @@ KitItemFormSetUpdate = inlineformset_factory( KitItemFormSet = KitItemFormSetCreate -class ProductCategoryForm(forms.ModelForm): +class ProductCategoryForm(SKUUniqueMixin, forms.ModelForm): """ Форма для создания и редактирования категории товаров. Поле photos НЕ включено в форму - файлы обрабатываются отдельно в view. @@ -380,10 +422,9 @@ class ProductCategoryForm(forms.ModelForm): is_active=True ).exclude(pk__in=exclude_ids) - def clean(self): + def clean_name(self): """Валидация уникальности имени для активных категорий""" - cleaned_data = super().clean() - name = cleaned_data.get('name') + name = self.cleaned_data.get('name') if name: # Проверяем уникальность имени среди активных категорий @@ -396,12 +437,12 @@ class ProductCategoryForm(forms.ModelForm): existing = existing.exclude(pk=self.instance.pk) if existing.exists(): - self.add_error('name', + raise forms.ValidationError( f'Категория с названием "{name}" уже существует. ' f'Пожалуйста, используйте другое название.' ) - return cleaned_data + return name def clean_slug(self): """Преобразуем пустую строку в None для автогенерации slug""" @@ -584,7 +625,7 @@ class ProductTagForm(forms.ModelForm): # ==================== CONFIGURABLE KIT FORMS ==================== -class ConfigurableProductForm(forms.ModelForm): +class ConfigurableProductForm(SKUUniqueMixin, forms.ModelForm): """ Форма для создания и редактирования вариативного товара. """ @@ -598,6 +639,9 @@ class ConfigurableProductForm(forms.ModelForm): 'short_description': 'Краткое описание', 'status': 'Статус' } + help_texts = { + 'sku': 'Оставьте пустым для автогенерации в формате VAR-XXXXXX', + } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -607,8 +651,9 @@ class ConfigurableProductForm(forms.ModelForm): }) self.fields['sku'].widget.attrs.update({ 'class': 'form-control', - 'placeholder': 'Артикул (необязательно)' + 'placeholder': 'VAR-XXXXXX (автогенерация)' }) + self.fields['sku'].required = False self.fields['description'].widget.attrs.update({ 'class': 'form-control', 'rows': 5 diff --git a/myproject/products/migrations/0002_add_configurable_sku_counter.py b/myproject/products/migrations/0002_add_configurable_sku_counter.py new file mode 100644 index 0000000..1653c71 --- /dev/null +++ b/myproject/products/migrations/0002_add_configurable_sku_counter.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.10 on 2025-12-30 07:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='skucounter', + name='counter_type', + field=models.CharField(choices=[('product', 'Product Counter'), ('kit', 'Kit Counter'), ('category', 'Category Counter'), ('configurable', 'Configurable Product Counter')], max_length=20, unique=True, verbose_name='Тип счетчика'), + ), + ] diff --git a/myproject/products/models/base.py b/myproject/products/models/base.py index 66af855..611a6d1 100644 --- a/myproject/products/models/base.py +++ b/myproject/products/models/base.py @@ -22,6 +22,7 @@ class SKUCounter(models.Model): ('product', 'Product Counter'), ('kit', 'Kit Counter'), ('category', 'Category Counter'), + ('configurable', 'Configurable Product Counter'), ] counter_type = models.CharField( diff --git a/myproject/products/models/kits.py b/myproject/products/models/kits.py index 1345b60..e933480 100644 --- a/myproject/products/models/kits.py +++ b/myproject/products/models/kits.py @@ -452,6 +452,16 @@ class ConfigurableProduct(BaseProductEntity): def __str__(self): return self.name + def save(self, *args, **kwargs): + """При сохранении - генерируем артикул если не задан""" + # Генерация артикула для новых вариативных товаров + if not self.sku: + from ..utils.sku_generator import generate_configurable_sku + self.sku = generate_configurable_sku() + + # Вызов родительского save (генерация slug и т.д.) + super().save(*args, **kwargs) + def delete(self, *args, **kwargs): """ Физическое удаление вариативного товара из БД. diff --git a/myproject/products/templates/products/configurableproduct_form.html b/myproject/products/templates/products/configurableproduct_form.html index 01335a3..6698c0d 100644 --- a/myproject/products/templates/products/configurableproduct_form.html +++ b/myproject/products/templates/products/configurableproduct_form.html @@ -87,6 +87,7 @@ input[name*="DELETE"] {