Рефакторинг системы вариативных товаров и справочник атрибутов

Основные изменения:
- Переименование ConfigurableKitProduct → ConfigurableProduct
- Добавлена поддержка Product как варианта (не только ProductKit)
- Создан справочник атрибутов (ProductAttribute, ProductAttributeValue)
- CRUD для управления атрибутами с inline редактированием значений
- Пересозданы миграции с нуля для всех приложений
- Добавлена ссылка на атрибуты в навигацию

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 01:44:34 +03:00
parent 277a514a82
commit 79ff523adb
36 changed files with 1597 additions and 951 deletions

View File

@@ -1,9 +1,10 @@
from django import forms
from django.forms import inlineformset_factory
from .models import (
Product, ProductKit, ProductCategory, ProductTag, ProductPhoto, KitItem,
Product, ProductKit, ProductCategory, ProductTag, ProductPhoto, KitItem,
ProductKitPhoto, ProductCategoryPhoto, ProductVariantGroup, ProductVariantGroupItem,
ConfigurableKitProduct, ConfigurableKitOption, ConfigurableKitProductAttribute
ConfigurableProduct, ConfigurableProductOption, ConfigurableProductAttribute,
ProductAttribute, ProductAttributeValue
)
@@ -583,12 +584,12 @@ class ProductTagForm(forms.ModelForm):
# ==================== CONFIGURABLE KIT FORMS ====================
class ConfigurableKitProductForm(forms.ModelForm):
class ConfigurableProductForm(forms.ModelForm):
"""
Форма для создания и редактирования вариативного товара.
"""
class Meta:
model = ConfigurableKitProduct
model = ConfigurableProduct
fields = ['name', 'sku', 'description', 'short_description', 'status']
labels = {
'name': 'Название',
@@ -620,7 +621,7 @@ class ConfigurableKitProductForm(forms.ModelForm):
self.fields['status'].widget.attrs.update({'class': 'form-select'})
class ConfigurableKitOptionForm(forms.ModelForm):
class ConfigurableProductOptionForm(forms.ModelForm):
"""
Форма для добавления варианта (комплекта) к вариативному товару.
Атрибуты варианта выбираются динамически на основе parent_attributes.
@@ -633,8 +634,8 @@ class ConfigurableKitOptionForm(forms.ModelForm):
)
class Meta:
model = ConfigurableKitOption
# Убрали 'attributes' - он будет заполняться через ConfigurableKitOptionAttribute
model = ConfigurableProductOption
# Убрали 'attributes' - он будет заполняться через ConfigurableProductOptionAttribute
fields = ['kit', 'is_default']
labels = {
'kit': 'Комплект',
@@ -653,7 +654,7 @@ class ConfigurableKitOptionForm(forms.ModelForm):
"""
super().__init__(*args, **kwargs)
# Получаем instance (ConfigurableKitOption)
# Получаем instance (ConfigurableProductOption)
if self.instance and self.instance.parent_id:
parent = self.instance.parent
# Получаем все уникальные названия атрибутов родителя
@@ -683,7 +684,7 @@ class ConfigurableKitOptionForm(forms.ModelForm):
self.fields[field_name].initial = current_attr.attribute
class BaseConfigurableKitOptionFormSet(forms.BaseInlineFormSet):
class BaseConfigurableProductOptionFormSet(forms.BaseInlineFormSet):
def clean(self):
"""Проверка на дубликаты комплектов и что все атрибуты заполнены"""
if any(self.errors):
@@ -756,12 +757,12 @@ class BaseConfigurableKitOptionFormSet(forms.BaseInlineFormSet):
# Формсет для создания вариативного товара
ConfigurableKitOptionFormSetCreate = inlineformset_factory(
ConfigurableKitProduct,
ConfigurableKitOption,
form=ConfigurableKitOptionForm,
formset=BaseConfigurableKitOptionFormSet,
fields=['kit', 'is_default'], # Убрали 'attributes' - заполняется через ConfigurableKitOptionAttribute
ConfigurableProductOptionFormSetCreate = inlineformset_factory(
ConfigurableProduct,
ConfigurableProductOption,
form=ConfigurableProductOptionForm,
formset=BaseConfigurableProductOptionFormSet,
fields=['kit', 'is_default'], # Убрали 'attributes' - заполняется через ConfigurableProductOptionAttribute
extra=0, # Не требуем пустые формы (варианты скрыты в UI)
can_delete=True,
min_num=0,
@@ -770,12 +771,12 @@ ConfigurableKitOptionFormSetCreate = inlineformset_factory(
)
# Формсет для редактирования вариативного товара
ConfigurableKitOptionFormSetUpdate = inlineformset_factory(
ConfigurableKitProduct,
ConfigurableKitOption,
form=ConfigurableKitOptionForm,
formset=BaseConfigurableKitOptionFormSet,
fields=['kit', 'is_default'], # Убрали 'attributes' - заполняется через ConfigurableKitOptionAttribute
ConfigurableProductOptionFormSetUpdate = inlineformset_factory(
ConfigurableProduct,
ConfigurableProductOption,
form=ConfigurableProductOptionForm,
formset=BaseConfigurableProductOptionFormSet,
fields=['kit', 'is_default'], # Убрали 'attributes' - заполняется через ConfigurableProductOptionAttribute
extra=0, # НЕ показывать пустые формы
can_delete=True,
min_num=0,
@@ -786,7 +787,7 @@ ConfigurableKitOptionFormSetUpdate = inlineformset_factory(
# === Формы для атрибутов родительского вариативного товара ===
class ConfigurableKitProductAttributeForm(forms.ModelForm):
class ConfigurableProductAttributeForm(forms.ModelForm):
"""
Форма для добавления атрибута родительского товара в карточном интерфейсе.
На фронтенде: одна карточка параметра (имя + позиция + видимость)
@@ -796,10 +797,10 @@ class ConfigurableKitProductAttributeForm(forms.ModelForm):
- name: "Длина"
- position: 0
- visible: True
- values: [50, 60, 70] (будут созданы как отдельные ConfigurableKitProductAttribute)
- values: [50, 60, 70] (будут созданы как отдельные ConfigurableProductAttribute)
"""
class Meta:
model = ConfigurableKitProductAttribute
model = ConfigurableProductAttribute
fields = ['name', 'position', 'visible']
labels = {
'name': 'Название параметра',
@@ -822,7 +823,7 @@ class ConfigurableKitProductAttributeForm(forms.ModelForm):
}
class BaseConfigurableKitProductAttributeFormSet(forms.BaseInlineFormSet):
class BaseConfigurableProductAttributeFormSet(forms.BaseInlineFormSet):
def clean(self):
"""Проверка на дубликаты параметров и что у каждого параметра есть значения"""
if any(self.errors):
@@ -850,11 +851,11 @@ class BaseConfigurableKitProductAttributeFormSet(forms.BaseInlineFormSet):
# Формсет для создания атрибутов родительского товара (карточный интерфейс)
ConfigurableKitProductAttributeFormSetCreate = inlineformset_factory(
ConfigurableKitProduct,
ConfigurableKitProductAttribute,
form=ConfigurableKitProductAttributeForm,
formset=BaseConfigurableKitProductAttributeFormSet,
ConfigurableProductAttributeFormSetCreate = inlineformset_factory(
ConfigurableProduct,
ConfigurableProductAttribute,
form=ConfigurableProductAttributeForm,
formset=BaseConfigurableProductAttributeFormSet,
# Убрали 'option' - значения будут добавляться через JavaScript в карточку
fields=['name', 'position', 'visible'],
extra=1,
@@ -865,11 +866,11 @@ ConfigurableKitProductAttributeFormSetCreate = inlineformset_factory(
)
# Формсет для редактирования атрибутов родительского товара
ConfigurableKitProductAttributeFormSetUpdate = inlineformset_factory(
ConfigurableKitProduct,
ConfigurableKitProductAttribute,
form=ConfigurableKitProductAttributeForm,
formset=BaseConfigurableKitProductAttributeFormSet,
ConfigurableProductAttributeFormSetUpdate = inlineformset_factory(
ConfigurableProduct,
ConfigurableProductAttribute,
form=ConfigurableProductAttributeForm,
formset=BaseConfigurableProductAttributeFormSet,
# Убрали 'option' - значения будут добавляться через JavaScript в карточку
fields=['name', 'position', 'visible'],
extra=0,
@@ -878,3 +879,86 @@ ConfigurableKitProductAttributeFormSetUpdate = inlineformset_factory(
validate_min=False,
can_delete_extra=True,
)
# ==========================================
# Формы для справочника атрибутов
# ==========================================
class ProductAttributeForm(forms.ModelForm):
"""Форма для создания и редактирования атрибута"""
class Meta:
model = ProductAttribute
fields = ['name', 'slug', 'description', 'position']
labels = {
'name': 'Название',
'slug': 'Slug (URL)',
'description': 'Описание',
'position': 'Позиция'
}
help_texts = {
'slug': 'Оставьте пустым для автогенерации'
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['name'].widget.attrs.update({
'class': 'form-control',
'placeholder': 'Например: Длина стебля'
})
self.fields['slug'].widget.attrs.update({
'class': 'form-control',
'placeholder': 'Автоматически'
})
self.fields['slug'].required = False
self.fields['description'].widget.attrs.update({
'class': 'form-control',
'rows': 2,
'placeholder': 'Опциональное описание'
})
self.fields['position'].widget.attrs.update({
'class': 'form-control',
'style': 'width: 100px;'
})
class ProductAttributeValueForm(forms.ModelForm):
"""Форма для значения атрибута (inline)"""
class Meta:
model = ProductAttributeValue
fields = ['value', 'slug', 'position']
labels = {
'value': 'Значение',
'slug': 'Slug',
'position': 'Позиция'
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['value'].widget.attrs.update({
'class': 'form-control form-control-sm',
'placeholder': 'Например: 50'
})
self.fields['slug'].widget.attrs.update({
'class': 'form-control form-control-sm',
'placeholder': 'Авто'
})
self.fields['slug'].required = False
self.fields['position'].widget.attrs.update({
'class': 'form-control form-control-sm',
'style': 'width: 70px;'
})
ProductAttributeValueFormSet = inlineformset_factory(
ProductAttribute,
ProductAttributeValue,
form=ProductAttributeValueForm,
fields=['value', 'slug', 'position'],
extra=3,
can_delete=True,
min_num=0,
validate_min=False,
)