diff --git a/myproject/products/migrations/0003_add_variant_sku.py b/myproject/products/migrations/0003_add_variant_sku.py new file mode 100644 index 0000000..fb0682f --- /dev/null +++ b/myproject/products/migrations/0003_add_variant_sku.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.10 on 2025-12-30 08:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0002_add_configurable_sku_counter'), + ] + + operations = [ + migrations.AddField( + model_name='configurableproductoption', + name='variant_sku', + field=models.CharField(blank=True, help_text='Дополнительный артикул для внешних площадок. Генерируется автоматически.', max_length=50, verbose_name='Артикул варианта'), + ), + migrations.AddIndex( + model_name='configurableproductoption', + index=models.Index(fields=['variant_sku'], name='products_co_variant_2da938_idx'), + ), + ] diff --git a/myproject/products/migrations/0004_populate_variant_sku.py b/myproject/products/migrations/0004_populate_variant_sku.py new file mode 100644 index 0000000..0932277 --- /dev/null +++ b/myproject/products/migrations/0004_populate_variant_sku.py @@ -0,0 +1,41 @@ +# Generated by Django 5.0.10 on 2025-12-30 08:17 + +import re +from django.db import migrations + + +def populate_variant_sku(apps, schema_editor): + """ + Генерируем variant_sku для существующих вариантов. + Формат: {parent.sku}-V{counter} + """ + ConfigurableProductOption = apps.get_model('products', 'ConfigurableProductOption') + ConfigurableProduct = apps.get_model('products', 'ConfigurableProduct') + + # Получаем все родительские товары + for parent in ConfigurableProduct.objects.all(): + # Получаем все варианты этого родителя + options = ConfigurableProductOption.objects.filter(parent=parent).order_by('id') + + for idx, option in enumerate(options, start=1): + # Генерируем variant_sku только если он пустой + if not option.variant_sku: + option.variant_sku = f"{parent.sku}-V{idx}" + option.save(update_fields=['variant_sku']) + + +def reverse_populate(apps, schema_editor): + """Очистка variant_sku при откате миграции""" + ConfigurableProductOption = apps.get_model('products', 'ConfigurableProductOption') + ConfigurableProductOption.objects.all().update(variant_sku='') + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0003_add_variant_sku'), + ] + + operations = [ + migrations.RunPython(populate_variant_sku, reverse_populate), + ] diff --git a/myproject/products/models/kits.py b/myproject/products/models/kits.py index e933480..d4bbb5a 100644 --- a/myproject/products/models/kits.py +++ b/myproject/products/models/kits.py @@ -597,6 +597,12 @@ class ConfigurableProductOption(models.Model): default=False, verbose_name="Вариант по умолчанию" ) + variant_sku = models.CharField( + max_length=50, + blank=True, + verbose_name="Артикул варианта", + help_text="Дополнительный артикул для внешних площадок. Генерируется автоматически." + ) class Meta: verbose_name = "Вариант товара" @@ -606,6 +612,7 @@ class ConfigurableProductOption(models.Model): models.Index(fields=['kit']), models.Index(fields=['product']), models.Index(fields=['parent', 'is_default']), + models.Index(fields=['variant_sku']), ] constraints = [ # kit XOR product — один из двух должен быть заполнен @@ -622,6 +629,42 @@ class ConfigurableProductOption(models.Model): variant_name = self.kit.name if self.kit else (self.product.name if self.product else "N/A") return f"{self.parent.name} → {variant_name}" + def save(self, *args, **kwargs): + """При создании - генерируем variant_sku если не задан""" + if not self.variant_sku and self.parent_id: + # Генерируем артикул варианта + self.variant_sku = self._generate_variant_sku() + + super().save(*args, **kwargs) + + def _generate_variant_sku(self): + """ + Генерирует артикул варианта в формате {parent.sku}-V{counter}. + Счетчик не переиспользуется при удалении вариантов (защита интеграций). + """ + import re + + # Получаем все варианты родителя с заполненным variant_sku + existing_variants = ConfigurableProductOption.objects.filter( + parent=self.parent, + variant_sku__isnull=False + ).exclude(pk=self.pk).values_list('variant_sku', flat=True) + + # Извлекаем номера из существующих variant_sku + max_number = 0 + for sku in existing_variants: + # Ищем паттерн -V\d+ в конце строки + match = re.search(r'-V(\d+)$', sku) + if match: + number = int(match.group(1)) + max_number = max(max_number, number) + + # Следующий номер + next_number = max_number + 1 + + # Формируем артикул + return f"{self.parent.sku}-V{next_number}" + @property def variant(self): """Возвращает связанный вариант (kit или product)""" @@ -638,8 +681,8 @@ class ConfigurableProductOption(models.Model): return self.variant.name if self.variant else None @property - def variant_sku(self): - """SKU варианта""" + def variant_base_sku(self): + """Основной SKU варианта (Product/ProductKit)""" return self.variant.sku if self.variant else None @property diff --git a/myproject/products/templates/products/configurableproduct_detail.html b/myproject/products/templates/products/configurableproduct_detail.html index 0210aec..a23b862 100644 --- a/myproject/products/templates/products/configurableproduct_detail.html +++ b/myproject/products/templates/products/configurableproduct_detail.html @@ -90,6 +90,7 @@ Комплект Артикул + Артикул варианта Цена Атрибуты По умолчанию @@ -104,6 +105,9 @@ {{ option.kit.sku|default:"—" }} + + {{ option.variant_sku|default:"—" }} + {{ option.kit.actual_price }} руб. {{ option.attributes|default:"—" }} @@ -188,11 +192,14 @@

- Вариативный товар предназначен для экспорта на WooCommerce и подобные площадки как Variable Product. + Вариативный товар предназначен для экспорта на внешние площадки как Variable Product.

Каждый вариант — это отдельный ProductKit с собственной ценой, артикулом и атрибутами.

+

+ Артикул варианта — дополнительный SKU для интеграций, генерируется автоматически. +


Количество вариантов: {{ configurable_kit.options.count }}