Рефакторинг системы вариативных товаров и справочник атрибутов
Основные изменения: - Переименование 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,4 +1,4 @@
|
||||
# Generated by Django 5.0.10 on 2025-12-23 20:38
|
||||
# Generated by Django 5.0.10 on 2025-12-29 22:19
|
||||
|
||||
import django.db.models.deletion
|
||||
import products.models.photos
|
||||
@@ -28,6 +28,23 @@ class Migration(migrations.Migration):
|
||||
'verbose_name_plural': 'Компоненты комплектов',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductAttribute',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='Например: Длина стебля, Цвет, Размер', max_length=100, unique=True, verbose_name='Название')),
|
||||
('slug', models.SlugField(blank=True, help_text='Автоматически генерируется из названия', max_length=100, unique=True, verbose_name='Slug')),
|
||||
('description', models.TextField(blank=True, help_text='Опциональное описание атрибута', verbose_name='Описание')),
|
||||
('position', models.PositiveIntegerField(default=0, help_text='Порядок отображения в списке', verbose_name='Позиция')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Атрибут товара',
|
||||
'verbose_name_plural': 'Атрибуты товаров',
|
||||
'ordering': ['position', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductVariantGroup',
|
||||
fields=[
|
||||
@@ -68,7 +85,7 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConfigurableKitProduct',
|
||||
name='ConfigurableProduct',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, verbose_name='Название')),
|
||||
@@ -83,32 +100,19 @@ class Migration(migrations.Migration):
|
||||
('archived_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to=settings.AUTH_USER_MODEL, verbose_name='Архивировано пользователем')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Вариативный товар (из комплектов)',
|
||||
'verbose_name_plural': 'Вариативные товары (из комплектов)',
|
||||
'verbose_name': 'Вариативный товар',
|
||||
'verbose_name_plural': 'Вариативные товары',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConfigurableKitOption',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('attributes', models.JSONField(blank=True, default=dict, help_text='Структурированные атрибуты. Пример: {"length": "60", "color": "red"}', verbose_name='Атрибуты варианта')),
|
||||
('is_default', models.BooleanField(default=False, verbose_name='Вариант по умолчанию')),
|
||||
('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='products.configurablekitproduct', verbose_name='Родитель (вариативный товар)')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Вариант комплекта',
|
||||
'verbose_name_plural': 'Варианты комплектов',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConfigurableKitProductAttribute',
|
||||
name='ConfigurableProductAttribute',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='Например: Цвет, Размер, Длина', max_length=150, verbose_name='Название атрибута')),
|
||||
('option', models.CharField(help_text='Например: Красный, M, 60см', max_length=150, verbose_name='Значение опции')),
|
||||
('position', models.PositiveIntegerField(default=0, help_text='Меньше = выше в списке', verbose_name='Порядок отображения')),
|
||||
('visible', models.BooleanField(default=True, help_text='Показывать ли атрибут на странице товара', verbose_name='Видимый на витрине')),
|
||||
('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parent_attributes', to='products.configurablekitproduct', verbose_name='Родительский товар')),
|
||||
('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parent_attributes', to='products.configurableproduct', verbose_name='Родительский товар')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Атрибут вариативного товара',
|
||||
@@ -117,11 +121,24 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConfigurableKitOptionAttribute',
|
||||
name='ConfigurableProductOption',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('option', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attributes_set', to='products.configurablekitoption', verbose_name='Вариант')),
|
||||
('attribute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='products.configurablekitproductattribute', verbose_name='Значение атрибута')),
|
||||
('attributes', models.JSONField(blank=True, default=dict, help_text='Структурированные атрибуты. Пример: {"length": "60", "color": "red"}', verbose_name='Атрибуты варианта')),
|
||||
('is_default', models.BooleanField(default=False, verbose_name='Вариант по умолчанию')),
|
||||
('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='products.configurableproduct', verbose_name='Родитель (вариативный товар)')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Вариант товара',
|
||||
'verbose_name_plural': 'Варианты товаров',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ConfigurableProductOptionAttribute',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('attribute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='products.configurableproductattribute', verbose_name='Значение атрибута')),
|
||||
('option', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attributes_set', to='products.configurableproductoption', verbose_name='Вариант')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Атрибут варианта',
|
||||
@@ -215,6 +232,33 @@ class Migration(migrations.Migration):
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='configurableproductoption',
|
||||
name='product',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='as_configurable_option_in', to='products.product', verbose_name='Товар (вариант)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='configurableproductattribute',
|
||||
name='product',
|
||||
field=models.ForeignKey(blank=True, help_text='Какой Product связан с этим значением атрибута', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='as_attribute_value_in', to='products.product', verbose_name='Товар для этого значения'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductAttributeValue',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.CharField(help_text='Например: 50, 60, 70 (для длины) или Красный, Белый (для цвета)', max_length=100, verbose_name='Значение')),
|
||||
('slug', models.SlugField(blank=True, max_length=100, verbose_name='Slug')),
|
||||
('position', models.PositiveIntegerField(default=0, help_text='Порядок отображения в списке значений', verbose_name='Позиция')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
('attribute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='values', to='products.productattribute', verbose_name='Атрибут')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Значение атрибута',
|
||||
'verbose_name_plural': 'Значения атрибутов',
|
||||
'ordering': ['position', 'value'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductCategory',
|
||||
fields=[
|
||||
@@ -292,14 +336,14 @@ class Migration(migrations.Migration):
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='kit_items', to='products.productkit', verbose_name='Комплект'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='configurablekitproductattribute',
|
||||
model_name='configurableproductoption',
|
||||
name='kit',
|
||||
field=models.ForeignKey(blank=True, help_text='Какой ProductKit связан с этим значением атрибута', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='as_attribute_value_in', to='products.productkit', verbose_name='Комплект для этого значения'),
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='as_configurable_option_in', to='products.productkit', verbose_name='Комплект (вариант)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='configurablekitoption',
|
||||
model_name='configurableproductattribute',
|
||||
name='kit',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='as_configurable_option_in', to='products.productkit', verbose_name='Комплект (вариант)'),
|
||||
field=models.ForeignKey(blank=True, help_text='Какой ProductKit связан с этим значением атрибута', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='as_attribute_value_in', to='products.productkit', verbose_name='Комплект для этого значения'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductKitPhoto',
|
||||
@@ -386,15 +430,15 @@ class Migration(migrations.Migration):
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='products.productvariantgroup', verbose_name='Группа вариантов'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='configurablekitoptionattribute',
|
||||
index=models.Index(fields=['option'], name='products_co_option__93b9f7_idx'),
|
||||
model_name='configurableproductoptionattribute',
|
||||
index=models.Index(fields=['option'], name='products_co_option__bd950a_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='configurablekitoptionattribute',
|
||||
index=models.Index(fields=['attribute'], name='products_co_attribu_ccc6d9_idx'),
|
||||
model_name='configurableproductoptionattribute',
|
||||
index=models.Index(fields=['attribute'], name='products_co_attribu_705d5a_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='configurablekitoptionattribute',
|
||||
name='configurableproductoptionattribute',
|
||||
unique_together={('option', 'attribute')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
@@ -409,6 +453,14 @@ class Migration(migrations.Migration):
|
||||
model_name='costpricehistory',
|
||||
index=models.Index(fields=['reason'], name='products_co_reason_959ee1_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='productattributevalue',
|
||||
index=models.Index(fields=['attribute', 'position'], name='products_pr_attribu_460f9e_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='productattributevalue',
|
||||
unique_together={('attribute', 'value')},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='productcategory',
|
||||
index=models.Index(fields=['is_active'], name='products_pr_is_acti_226e74_idx'),
|
||||
@@ -438,36 +490,40 @@ class Migration(migrations.Migration):
|
||||
index=models.Index(fields=['quality_warning', 'category'], name='products_pr_quality_fc505a_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='configurablekitproductattribute',
|
||||
index=models.Index(fields=['parent', 'name'], name='products_co_parent__4a7869_idx'),
|
||||
model_name='configurableproductoption',
|
||||
index=models.Index(fields=['parent'], name='products_co_parent__36761a_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='configurablekitproductattribute',
|
||||
index=models.Index(fields=['parent', 'position'], name='products_co_parent__0904e2_idx'),
|
||||
model_name='configurableproductoption',
|
||||
index=models.Index(fields=['kit'], name='products_co_kit_id_9e9a00_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='configurablekitproductattribute',
|
||||
index=models.Index(fields=['kit'], name='products_co_kit_id_c5d506_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='configurablekitproductattribute',
|
||||
unique_together={('parent', 'name', 'option', 'kit')},
|
||||
model_name='configurableproductoption',
|
||||
index=models.Index(fields=['product'], name='products_co_product_4d77ae_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='configurablekitoption',
|
||||
index=models.Index(fields=['parent'], name='products_co_parent__56ecfa_idx'),
|
||||
model_name='configurableproductoption',
|
||||
index=models.Index(fields=['parent', 'is_default'], name='products_co_parent__be8830_idx'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='configurableproductoption',
|
||||
constraint=models.CheckConstraint(check=models.Q(models.Q(('kit__isnull', False), ('product__isnull', True)), models.Q(('kit__isnull', True), ('product__isnull', False)), _connector='OR'), name='configurable_option_kit_xor_product'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='configurablekitoption',
|
||||
index=models.Index(fields=['kit'], name='products_co_kit_id_3fa7fe_idx'),
|
||||
model_name='configurableproductattribute',
|
||||
index=models.Index(fields=['parent', 'name'], name='products_co_parent__78337c_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='configurablekitoption',
|
||||
index=models.Index(fields=['parent', 'is_default'], name='products_co_parent__ffa4ca_idx'),
|
||||
model_name='configurableproductattribute',
|
||||
index=models.Index(fields=['parent', 'position'], name='products_co_parent__90f012_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='configurablekitoption',
|
||||
unique_together={('parent', 'kit')},
|
||||
migrations.AddIndex(
|
||||
model_name='configurableproductattribute',
|
||||
index=models.Index(fields=['kit'], name='products_co_kit_id_db7ebb_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='configurableproductattribute',
|
||||
index=models.Index(fields=['product'], name='products_co_product_68c16a_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='productkitphoto',
|
||||
|
||||
Reference in New Issue
Block a user