Рефакторинг системы вариативных товаров и справочник атрибутов
Основные изменения: - Переименование 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:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user