diff --git a/myproject/products/forms.py b/myproject/products/forms.py index ff2bedc..7c59b92 100644 --- a/myproject/products/forms.py +++ b/myproject/products/forms.py @@ -1,6 +1,10 @@ from django import forms from django.forms import inlineformset_factory -from .models import Product, ProductKit, ProductCategory, ProductTag, ProductPhoto, KitItem, ProductKitPhoto, ProductCategoryPhoto, ProductVariantGroup, ProductVariantGroupItem +from .models import ( + Product, ProductKit, ProductCategory, ProductTag, ProductPhoto, KitItem, + ProductKitPhoto, ProductCategoryPhoto, ProductVariantGroup, ProductVariantGroupItem, + ConfigurableKitProduct, ConfigurableKitOption, ConfigurableKitProductAttribute +) class ProductForm(forms.ModelForm): @@ -577,3 +581,229 @@ class ProductTagForm(forms.ModelForm): if slug == '' or slug is None: return None return slug + + +# ==================== CONFIGURABLE KIT FORMS ==================== + +class ConfigurableKitProductForm(forms.ModelForm): + """ + Форма для создания и редактирования вариативного товара. + """ + class Meta: + model = ConfigurableKitProduct + fields = ['name', 'sku', 'description', 'short_description', 'status'] + labels = { + 'name': 'Название', + 'sku': 'Артикул', + 'description': 'Полное описание', + 'short_description': 'Краткое описание', + 'status': 'Статус' + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['name'].widget.attrs.update({ + 'class': 'form-control', + 'placeholder': 'Введите название вариативного товара' + }) + self.fields['sku'].widget.attrs.update({ + 'class': 'form-control', + 'placeholder': 'Артикул (необязательно)' + }) + self.fields['description'].widget.attrs.update({ + 'class': 'form-control', + 'rows': 5 + }) + self.fields['short_description'].widget.attrs.update({ + 'class': 'form-control', + 'rows': 3, + 'placeholder': 'Краткое описание для экспорта' + }) + self.fields['status'].widget.attrs.update({'class': 'form-select'}) + + +class ConfigurableKitOptionForm(forms.ModelForm): + """ + Форма для добавления варианта (комплекта) к вариативному товару. + """ + kit = forms.ModelChoiceField( + queryset=ProductKit.objects.filter(status='active', is_temporary=False).order_by('name'), + required=True, + label="Комплект", + widget=forms.Select(attrs={'class': 'form-select'}) + ) + + class Meta: + model = ConfigurableKitOption + fields = ['kit', 'attributes', 'is_default'] + labels = { + 'kit': 'Комплект', + 'attributes': 'Атрибуты варианта', + 'is_default': 'По умолчанию' + } + widgets = { + 'attributes': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Например: Количество:15;Длина:60см' + }), + 'is_default': forms.CheckboxInput(attrs={ + 'class': 'form-check-input is-default-switch', + 'role': 'switch' + }) + } + + +class BaseConfigurableKitOptionFormSet(forms.BaseInlineFormSet): + def clean(self): + """Проверка на дубликаты комплектов в вариативном товаре""" + if any(self.errors): + return + + kits = [] + default_count = 0 + + for form in self.forms: + if self.can_delete and self._should_delete_form(form): + continue + + kit = form.cleaned_data.get('kit') + is_default = form.cleaned_data.get('is_default') + + # Проверка дубликатов комплектов + if kit: + if kit in kits: + raise forms.ValidationError( + f'Комплект "{kit.name}" добавлен более одного раза. ' + f'Каждый комплект может быть добавлен только один раз.' + ) + kits.append(kit) + + # Считаем количество "is_default" + if is_default: + default_count += 1 + + # Проверяем, что не более одного "is_default" + if default_count > 1: + raise forms.ValidationError( + 'Можно установить только один вариант как "по умолчанию".' + ) + + +# Формсет для создания вариативного товара +ConfigurableKitOptionFormSetCreate = inlineformset_factory( + ConfigurableKitProduct, + ConfigurableKitOption, + form=ConfigurableKitOptionForm, + formset=BaseConfigurableKitOptionFormSet, + fields=['kit', 'attributes', 'is_default'], + extra=1, # Показать 1 пустую форму + can_delete=True, + min_num=0, + validate_min=False, + can_delete_extra=True, +) + +# Формсет для редактирования вариативного товара +ConfigurableKitOptionFormSetUpdate = inlineformset_factory( + ConfigurableKitProduct, + ConfigurableKitOption, + form=ConfigurableKitOptionForm, + formset=BaseConfigurableKitOptionFormSet, + fields=['kit', 'attributes', 'is_default'], + extra=0, # НЕ показывать пустые формы + can_delete=True, + min_num=0, + validate_min=False, + can_delete_extra=True, +) + + +# === Формы для атрибутов родительского вариативного товара === + +class ConfigurableKitProductAttributeForm(forms.ModelForm): + """ + Форма для добавления атрибута родительского товара. + Пример: name="Цвет", option="Красный" + """ + class Meta: + model = ConfigurableKitProductAttribute + fields = ['name', 'option', 'position', 'visible'] + labels = { + 'name': 'Название атрибута', + 'option': 'Значение опции', + 'position': 'Порядок', + 'visible': 'Видимый' + } + widgets = { + 'name': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Например: Цвет, Размер, Длина' + }), + 'option': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Например: Красный, M, 60см' + }), + 'position': forms.NumberInput(attrs={ + 'class': 'form-control', + 'min': '0', + 'value': '0' + }), + 'visible': forms.CheckboxInput(attrs={ + 'class': 'form-check-input' + }) + } + + +class BaseConfigurableKitProductAttributeFormSet(forms.BaseInlineFormSet): + def clean(self): + """Проверка на дубликаты атрибутов""" + if any(self.errors): + return + + attributes = [] + + for form in self.forms: + if self.can_delete and self._should_delete_form(form): + continue + + name = form.cleaned_data.get('name') + option = form.cleaned_data.get('option') + + # Проверка дубликатов + if name and option: + attr_tuple = (name.strip(), option.strip()) + if attr_tuple in attributes: + raise forms.ValidationError( + f'Атрибут "{name}: {option}" добавлен более одного раза. ' + f'Каждая комбинация атрибут-значение должна быть уникальной.' + ) + attributes.append(attr_tuple) + + +# Формсет для создания атрибутов родительского товара +ConfigurableKitProductAttributeFormSetCreate = inlineformset_factory( + ConfigurableKitProduct, + ConfigurableKitProductAttribute, + form=ConfigurableKitProductAttributeForm, + formset=BaseConfigurableKitProductAttributeFormSet, + fields=['name', 'option', 'position', 'visible'], + extra=1, + can_delete=True, + min_num=0, + validate_min=False, + can_delete_extra=True, +) + +# Формсет для редактирования атрибутов родительского товара +ConfigurableKitProductAttributeFormSetUpdate = inlineformset_factory( + ConfigurableKitProduct, + ConfigurableKitProductAttribute, + form=ConfigurableKitProductAttributeForm, + formset=BaseConfigurableKitProductAttributeFormSet, + fields=['name', 'option', 'position', 'visible'], + extra=0, + can_delete=True, + min_num=0, + validate_min=False, + can_delete_extra=True, +) diff --git a/myproject/products/migrations/0002_configurablekitproduct_configurablekitoption.py b/myproject/products/migrations/0002_configurablekitproduct_configurablekitoption.py new file mode 100644 index 0000000..a7c904e --- /dev/null +++ b/myproject/products/migrations/0002_configurablekitproduct_configurablekitoption.py @@ -0,0 +1,52 @@ +# Generated by Django 5.0.10 on 2025-11-17 19:29 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ConfigurableKitProduct', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Название')), + ('sku', models.CharField(blank=True, db_index=True, max_length=100, null=True, verbose_name='Артикул')), + ('slug', models.SlugField(blank=True, max_length=200, unique=True, verbose_name='URL-идентификатор')), + ('description', models.TextField(blank=True, null=True, verbose_name='Описание')), + ('short_description', models.TextField(blank=True, help_text='Используется для карточек товаров, превью и площадок', null=True, verbose_name='Краткое описание')), + ('status', models.CharField(choices=[('active', 'Активный'), ('archived', 'Архивный'), ('discontinued', 'Снят')], db_index=True, default='active', max_length=20, verbose_name='Статус')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')), + ('archived_at', models.DateTimeField(blank=True, null=True, verbose_name='Время архивирования')), + ('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': 'Конфигурируемые товары (из комплектов)', + }, + ), + migrations.CreateModel( + name='ConfigurableKitOption', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('attributes', models.TextField(blank=True, verbose_name='Атрибуты варианта (для внешних площадок)')), + ('is_default', models.BooleanField(default=False, verbose_name='Вариант по умолчанию')), + ('kit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='as_configurable_option_in', to='products.productkit', 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': 'Варианты комплектов', + 'indexes': [models.Index(fields=['parent'], name='products_co_parent__56ecfa_idx'), models.Index(fields=['kit'], name='products_co_kit_id_3fa7fe_idx'), models.Index(fields=['parent', 'is_default'], name='products_co_parent__ffa4ca_idx')], + 'unique_together': {('parent', 'kit')}, + }, + ), + ] diff --git a/myproject/products/migrations/0003_alter_configurablekitproduct_options_and_more.py b/myproject/products/migrations/0003_alter_configurablekitproduct_options_and_more.py new file mode 100644 index 0000000..6455340 --- /dev/null +++ b/myproject/products/migrations/0003_alter_configurablekitproduct_options_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.10 on 2025-11-17 19:30 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0002_configurablekitproduct_configurablekitoption'), + ] + + operations = [ + migrations.AlterModelOptions( + name='configurablekitproduct', + options={'verbose_name': 'Вариативный товар (из комплектов)', 'verbose_name_plural': 'Вариативные товары (из комплектов)'}, + ), + migrations.AlterField( + model_name='configurablekitoption', + name='parent', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='products.configurablekitproduct', verbose_name='Родитель (вариативный товар)'), + ), + ] diff --git a/myproject/products/migrations/0004_configurablekitproductattribute.py b/myproject/products/migrations/0004_configurablekitproductattribute.py new file mode 100644 index 0000000..15886f9 --- /dev/null +++ b/myproject/products/migrations/0004_configurablekitproductattribute.py @@ -0,0 +1,32 @@ +# Generated by Django 5.0.10 on 2025-11-17 21:38 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0003_alter_configurablekitproduct_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ConfigurableKitProductAttribute', + 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='Родительский товар')), + ], + options={ + 'verbose_name': 'Атрибут вариативного товара', + 'verbose_name_plural': 'Атрибуты вариативных товаров', + 'ordering': ['parent', 'position', 'name', 'option'], + 'indexes': [models.Index(fields=['parent', 'name'], name='products_co_parent__4a7869_idx'), models.Index(fields=['parent', 'position'], name='products_co_parent__0904e2_idx')], + 'unique_together': {('parent', 'name', 'option')}, + }, + ), + ] diff --git a/myproject/products/models/__init__.py b/myproject/products/models/__init__.py index 4b2c9b7..ad6da9e 100644 --- a/myproject/products/models/__init__.py +++ b/myproject/products/models/__init__.py @@ -32,7 +32,7 @@ from .variants import ProductVariantGroup, ProductVariantGroupItem from .products import Product # Комплекты -from .kits import ProductKit, KitItem, KitItemPriority +from .kits import ProductKit, KitItem, KitItemPriority, ConfigurableKitProduct, ConfigurableKitOption, ConfigurableKitProductAttribute # Фотографии from .photos import BasePhoto, ProductPhoto, ProductKitPhoto, ProductCategoryPhoto, PhotoProcessingStatus @@ -63,6 +63,9 @@ __all__ = [ 'ProductKit', 'KitItem', 'KitItemPriority', + 'ConfigurableKitProduct', + 'ConfigurableKitOption', + 'ConfigurableKitProductAttribute', # Photos 'BasePhoto', diff --git a/myproject/products/models/kits.py b/myproject/products/models/kits.py index efae9df..d36d066 100644 --- a/myproject/products/models/kits.py +++ b/myproject/products/models/kits.py @@ -378,3 +378,113 @@ class KitItemPriority(models.Model): def __str__(self): return f"{self.product.name} (приоритет {self.priority})" + + +class ConfigurableKitProduct(BaseProductEntity): + """ + Вариативный товар, объединяющий несколько наших ProductKit + как варианты для внешних площадок (WooCommerce и подобные). + """ + class Meta: + verbose_name = "Вариативный товар (из комплектов)" + verbose_name_plural = "Вариативные товары (из комплектов)" + # Уникальность активного имени наследуется из BaseProductEntity + + def __str__(self): + return self.name + + def delete(self, *args, **kwargs): + """ + Физическое удаление вариативного товара из БД. + При удалении удаляются только связи (ConfigurableKitOption), + но сами ProductKit остаются нетронутыми благодаря CASCADE на уровне связей. + """ + # Используем super() для вызова стандартного Django delete, минуя BaseProductEntity.delete() + super(BaseProductEntity, self).delete(*args, **kwargs) + + +class ConfigurableKitProductAttribute(models.Model): + """ + Атрибут родительского вариативного товара. + Определяет схему атрибутов для экспорта на WooCommerce и подобные площадки. + Например: name="Цвет", option="Красный" или name="Размер", option="M". + """ + parent = models.ForeignKey( + ConfigurableKitProduct, + on_delete=models.CASCADE, + related_name='parent_attributes', + verbose_name="Родительский товар" + ) + name = models.CharField( + max_length=150, + verbose_name="Название атрибута", + help_text="Например: Цвет, Размер, Длина" + ) + option = models.CharField( + max_length=150, + verbose_name="Значение опции", + help_text="Например: Красный, M, 60см" + ) + position = models.PositiveIntegerField( + default=0, + verbose_name="Порядок отображения", + help_text="Меньше = выше в списке" + ) + visible = models.BooleanField( + default=True, + verbose_name="Видимый на витрине", + help_text="Показывать ли атрибут на странице товара" + ) + + class Meta: + verbose_name = "Атрибут вариативного товара" + verbose_name_plural = "Атрибуты вариативных товаров" + ordering = ['parent', 'position', 'name', 'option'] + unique_together = [['parent', 'name', 'option']] + indexes = [ + models.Index(fields=['parent', 'name']), + models.Index(fields=['parent', 'position']), + ] + + def __str__(self): + return f"{self.parent.name} - {self.name}: {self.option}" + + +class ConfigurableKitOption(models.Model): + """ + Отдельный вариант внутри ConfigurableKitProduct, указывающий на конкретный ProductKit. + Атрибуты варианта хранятся простым текстом (можно расширить до JSON позже). + """ + parent = models.ForeignKey( + ConfigurableKitProduct, + on_delete=models.CASCADE, + related_name='options', + verbose_name="Родитель (вариативный товар)" + ) + kit = models.ForeignKey( + ProductKit, + on_delete=models.CASCADE, + related_name='as_configurable_option_in', + verbose_name="Комплект (вариант)" + ) + attributes = models.TextField( + blank=True, + verbose_name="Атрибуты варианта (для внешних площадок)" + ) + is_default = models.BooleanField( + default=False, + verbose_name="Вариант по умолчанию" + ) + + class Meta: + verbose_name = "Вариант комплекта" + verbose_name_plural = "Варианты комплектов" + unique_together = [['parent', 'kit']] + indexes = [ + models.Index(fields=['parent']), + models.Index(fields=['kit']), + models.Index(fields=['parent', 'is_default']), + ] + + def __str__(self): + return f"{self.parent.name} → {self.kit.name}" diff --git a/myproject/products/static/products/js/configurablekit_detail.js b/myproject/products/static/products/js/configurablekit_detail.js new file mode 100644 index 0000000..b4c6dac --- /dev/null +++ b/myproject/products/static/products/js/configurablekit_detail.js @@ -0,0 +1,274 @@ +/** + * Управление вариантами вариативного товара (ConfigurableKitProduct) + */ + +document.addEventListener('DOMContentLoaded', function() { + // Проверяем наличие данных конфигурации + const configDataEl = document.getElementById('configurableKitData'); + if (!configDataEl) return; + + const config = JSON.parse(configDataEl.textContent); + const modal = new bootstrap.Modal(document.getElementById('addOptionModal')); + + // Элементы формы + const saveBtn = document.getElementById('saveOptionBtn'); + const kitSelect = document.getElementById('kitSelect'); + const attributesInput = document.getElementById('attributesInput'); + const isDefaultCheck = document.getElementById('isDefaultCheck'); + const errorDiv = document.getElementById('addOptionError'); + const spinner = document.getElementById('saveOptionSpinner'); + + // Добавление варианта + saveBtn.addEventListener('click', async function() { + const kitId = kitSelect.value; + if (!kitId) { + showError('Пожалуйста, выберите комплект'); + return; + } + + // Показываем спиннер + spinner.classList.remove('d-none'); + saveBtn.disabled = true; + hideError(); + + const formData = new FormData(); + formData.append('kit_id', kitId); + formData.append('attributes', attributesInput.value.trim()); + formData.append('is_default', isDefaultCheck.checked ? 'true' : 'false'); + formData.append('csrfmiddlewaretoken', getCsrfToken()); + + try { + const response = await fetch(config.addOptionUrl, { + method: 'POST', + body: formData + }); + + const data = await response.json(); + + if (data.success) { + // Добавляем строку в таблицу + addOptionToTable(data.option); + + // Закрываем модальное окно + modal.hide(); + + // Очищаем форму + resetForm(); + + // Показываем уведомление + showSuccessMessage('Вариант успешно добавлен'); + } else { + showError(data.error || 'Ошибка при добавлении варианта'); + } + } catch (error) { + console.error('Error:', error); + showError('Произошла ошибка при сохранении'); + } finally { + spinner.classList.add('d-none'); + saveBtn.disabled = false; + } + }); + + // Удаление варианта + document.addEventListener('click', async function(e) { + if (e.target.closest('.remove-option-btn')) { + const btn = e.target.closest('.remove-option-btn'); + const optionId = btn.dataset.optionId; + + if (!confirm('Вы уверены, что хотите удалить этот вариант?')) { + return; + } + + btn.disabled = true; + + try { + const url = config.removeOptionUrlTemplate.replace('{optionId}', optionId); + const response = await fetch(url, { + method: 'POST', + headers: { + 'X-CSRFToken': getCsrfToken() + } + }); + + const data = await response.json(); + + if (data.success) { + // Удаляем строку из таблицы + const row = btn.closest('tr'); + row.remove(); + + // Проверяем, есть ли ещё варианты + checkIfTableEmpty(); + + showSuccessMessage('Вариант удалён'); + } else { + showError(data.error || 'Ошибка при удалении'); + btn.disabled = false; + } + } catch (error) { + console.error('Error:', error); + alert('Произошла ошибка при удалении'); + btn.disabled = false; + } + } + }); + + // Установка по умолчанию + document.addEventListener('click', async function(e) { + if (e.target.closest('.set-default-btn')) { + const btn = e.target.closest('.set-default-btn'); + const optionId = btn.dataset.optionId; + + btn.disabled = true; + + try { + const url = config.setDefaultUrlTemplate.replace('{optionId}', optionId); + const response = await fetch(url, { + method: 'POST', + headers: { + 'X-CSRFToken': getCsrfToken() + } + }); + + const data = await response.json(); + + if (data.success) { + // Обновляем UI + updateDefaultBadges(optionId); + showSuccessMessage('Вариант установлен как по умолчанию'); + } else { + showError(data.error || 'Ошибка при установке'); + btn.disabled = false; + } + } catch (error) { + console.error('Error:', error); + alert('Произошла ошибка'); + btn.disabled = false; + } + } + }); + + // Вспомогательные функции + function addOptionToTable(option) { + const tbody = document.querySelector('#optionsTable tbody'); + const noOptionsMsg = document.getElementById('noOptionsMessage'); + + // Если таблицы нет, создаём её + if (!tbody) { + const container = document.getElementById('optionsTableContainer'); + if (noOptionsMsg) noOptionsMsg.remove(); + + container.innerHTML = ` +
| Комплект | +Артикул | +Цена | +Атрибуты | +По умолчанию | +Действия | +
|---|
Нет вариантов. Нажмите "Добавить вариант" для добавления.
'; + } + } + + function resetForm() { + kitSelect.value = ''; + attributesInput.value = ''; + isDefaultCheck.checked = false; + hideError(); + } + + function showError(message) { + errorDiv.textContent = message; + errorDiv.classList.remove('d-none'); + } + + function hideError() { + errorDiv.classList.add('d-none'); + } + + function getCsrfToken() { + return document.querySelector('[name=csrfmiddlewaretoken]').value; + } + + function showSuccessMessage(message) { + // Простое alert, можно заменить на toast-уведомление + const alertDiv = document.createElement('div'); + alertDiv.className = 'alert alert-success alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3'; + alertDiv.style.zIndex = '9999'; + alertDiv.innerHTML = ` + ${message} + + `; + document.body.appendChild(alertDiv); + + setTimeout(() => { + alertDiv.remove(); + }, 3000); + } + + // Сброс формы при закрытии модального окна + document.getElementById('addOptionModal').addEventListener('hidden.bs.modal', function() { + resetForm(); + }); +}); diff --git a/myproject/products/templates/products/configurablekit_confirm_delete.html b/myproject/products/templates/products/configurablekit_confirm_delete.html new file mode 100644 index 0000000..83572d7 --- /dev/null +++ b/myproject/products/templates/products/configurablekit_confirm_delete.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} +{% load static %} + +{% block title %}Удаление вариативного товара{% endblock %} + +{% block content %} ++ Вы уверены, что хотите удалить вариативный товар {{ object.name }}? +
+ + {% if object.options.count > 0 %} +| Название: | +{{ configurable_kit.name }} | +
|---|---|
| Артикул: | +{{ configurable_kit.sku|default:"—" }} | +
| Статус: | ++ {% if configurable_kit.status == 'active' %} + Активный + {% elif configurable_kit.status == 'archived' %} + Архивный + {% else %} + Снятый + {% endif %} + | +
| Краткое описание: | +{{ configurable_kit.short_description|default:"—" }} | +
| Описание: | +{{ configurable_kit.description|default:"—" }} | +
| Дата создания: | +{{ configurable_kit.created_at|date:"d.m.Y H:i" }} | +
| Дата обновления: | +{{ configurable_kit.updated_at|date:"d.m.Y H:i" }} | +
| Комплект | +Артикул | +Цена | +Атрибуты | +По умолчанию | +
|---|---|---|---|---|
| + + {{ option.kit.name }} + + | +{{ option.kit.sku|default:"—" }} | +{{ option.kit.actual_price }} руб. | +{{ option.attributes|default:"—" }} | ++ {% if option.is_default %} + Да + {% else %} + Нет + {% endif %} + | +
+ Нет вариантов. Перейдите в режим редактирования для добавления. +
+ {% endif %} +| Название атрибута | +Значение опции | +Порядок | +Видимый | +
|---|---|---|---|
| {{ attr.name }} | +{{ attr.option }} | +{{ attr.position }} | ++ {% if attr.visible %} + Да + {% else %} + Нет + {% endif %} + | +
+ Нет атрибутов. Перейдите в режим редактирования для добавления. +
+ {% endif %} ++ Вариативный товар предназначен для экспорта на WooCommerce и подобные площадки как Variable Product. +
++ Каждый вариант — это отдельный ProductKit с собственной ценой, артикулом и атрибутами. +
++ Количество вариантов: {{ configurable_kit.options.count }} +
++ Атрибутов товара: {{ configurable_kit.parent_attributes.count }} +
+| Название | +Артикул | +Статус | +Вариантов | +Дата создания | +Действия | +
|---|---|---|---|---|---|
| + + {{ item.name }} + + | ++ {{ item.sku|default:"-" }} + | ++ {% if item.status == 'active' %} + Активный + {% elif item.status == 'archived' %} + Архивный + {% else %} + Снятый + {% endif %} + | ++ {{ item.options.count }} + | +{{ item.created_at|date:"d.m.Y H:i" }} | ++ + + + + + + + + + | +
| + Нет вариативных товаров. Создать первый + | +|||||