From 577401447bb50c03984d2c15d018f30de8618471 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Tue, 30 Dec 2025 10:47:03 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B0=D0=B2=D1=82=D0=BE=D0=B3=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B8=20=D0=B2=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B4=D0=B0=D1=86=D0=B8=D1=8F=20=D1=83=D0=BD=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20=D0=B0=D1=80?= =?UTF-8?q?=D1=82=D0=B8=D0=BA=D1=83=D0=BB=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=B2=D1=81=D0=B5=D1=85=20=D1=82=D0=B8=D0=BF=D0=BE=D0=B2=20?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен миксин SKUUniqueMixin для единообразной валидации артикулов - Валидация проверяет уникальность SKU среди Product, ProductKit, ProductCategory, ConfigurableProduct - Реализована автогенерация артикулов для ConfigurableProduct (формат VAR-XXXXXX) - Добавлен новый тип счетчика 'configurable' в SKUCounter - Обновлены формы Product, ProductKit, ProductCategory, ConfigurableProduct - Рефакторинг методов clean() в формах: валидация имени вынесена в clean_name() - Добавлена функция generate_configurable_sku() в sku_generator.py - Обновлена функция ensure_sku_unique() для проверки ConfigurableProduct - Добавлен метод save() в модель ConfigurableProduct для автогенерации SKU - Обновлен шаблон configurableproduct_form.html с отображением help_text для SKU Код стал чистым, без дублирования логики валидации. --- myproject/products/forms.py | 97 ++++++++++++++----- .../0002_add_configurable_sku_counter.py | 18 ++++ myproject/products/models/base.py | 1 + myproject/products/models/kits.py | 10 ++ .../products/configurableproduct_form.html | 1 + myproject/products/utils/sku_generator.py | 38 +++++++- 6 files changed, 135 insertions(+), 30 deletions(-) create mode 100644 myproject/products/migrations/0002_add_configurable_sku_counter.py 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"] {
{{ form.sku }} +
{{ form.fields.sku.help_text }}
{% if form.sku.errors %}
{{ form.sku.errors.0 }}
{% endif %} diff --git a/myproject/products/utils/sku_generator.py b/myproject/products/utils/sku_generator.py index 0d5a0b8..b40b069 100644 --- a/myproject/products/utils/sku_generator.py +++ b/myproject/products/utils/sku_generator.py @@ -5,12 +5,14 @@ New SKU format: - Products: PROD-XXXXXX or PROD-XXXXXX-VARIANT - Kits: KIT-XXXXXX - Categories: CAT-XXXX +- Configurable Products: VAR-XXXXXX Examples: - PROD-000001 - PROD-000002-50 - KIT-000001 - CAT-0001 +- VAR-000001 """ import re from string import ascii_uppercase @@ -71,13 +73,13 @@ def ensure_sku_unique(base_sku, exclude_id=None, model_type=None): Args: base_sku (str): Базовый артикул для проверки - exclude_id (int): ID товара/комплекта/категории, который нужно исключить из проверки - model_type (str): Тип модели ('product', 'kit', 'category') для исключения из проверки + exclude_id (int): ID товара/комплекта/категории/вариативного товара, который нужно исключить из проверки + model_type (str): Тип модели ('product', 'kit', 'category', 'configurable') для исключения из проверки Returns: str: Уникальный артикул """ - from products.models import Product, ProductKit, ProductCategory + from products.models import Product, ProductKit, ProductCategory, ConfigurableProduct # Проверяем, существует ли базовый артикул sku = base_sku @@ -99,7 +101,12 @@ def ensure_sku_unique(base_sku, exclude_id=None, model_type=None): category_exists = category_exists.exclude(id=exclude_id) category_exists = category_exists.exists() - return product_exists or kit_exists or category_exists + configurable_exists = ConfigurableProduct.objects.filter(sku=sku_to_check) + if model_type == 'configurable' and exclude_id: + configurable_exists = configurable_exists.exclude(id=exclude_id) + configurable_exists = configurable_exists.exists() + + return product_exists or kit_exists or category_exists or configurable_exists # Если базовый артикул свободен, возвращаем его if not sku_exists(sku): @@ -210,3 +217,26 @@ def generate_category_sku(): unique_sku = ensure_sku_unique(base_sku, model_type='category') return unique_sku + + +def generate_configurable_sku(): + """ + Генерирует уникальный артикул для вариативного товара. + + Формат: VAR-XXXXXX + + Returns: + str: Сгенерированный артикул + """ + from products.models import SKUCounter + + # Получаем следующий номер из глобального счетчика + next_number = SKUCounter.get_next_value('configurable') + + # Форматируем номер с ведущими нулями (6 цифр) + base_sku = f"VAR-{next_number:06d}" + + # Обеспечиваем уникальность + unique_sku = ensure_sku_unique(base_sku, model_type='configurable') + + return unique_sku