Добавлена автогенерация и валидация уникальности артикулов для всех типов товаров
- Добавлен миксин 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 Код стал чистым, без дублирования логики валидации.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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='Тип счетчика'),
|
||||
),
|
||||
]
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
Физическое удаление вариативного товара из БД.
|
||||
|
||||
@@ -87,6 +87,7 @@ input[name*="DELETE"] {
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.sku.id_for_label }}" class="form-label">Артикул</label>
|
||||
{{ form.sku }}
|
||||
<div class="form-text">{{ form.fields.sku.help_text }}</div>
|
||||
{% if form.sku.errors %}
|
||||
<div class="text-danger small">{{ form.sku.errors.0 }}</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user