feat(integrations): добавить поле primary_category и маппинг категорий для интеграций
Добавлена поддержка выбора основной категории (primary_category) для товаров и наборов, а также новая модель IntegrationCategoryMapping для связи категорий с внешними площадками. Теперь можно указать категорию товара, которая будет использоваться при экспорте на внешние площадки (Recommerce, WooCommerce и др.), с возможностью настройки маппинга категорий для каждого типа интеграции.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import RecommerceIntegration, WooCommerceIntegration
|
from .models import RecommerceIntegration, WooCommerceIntegration, IntegrationCategoryMapping
|
||||||
|
|
||||||
|
|
||||||
@admin.register(RecommerceIntegration)
|
@admin.register(RecommerceIntegration)
|
||||||
@@ -30,3 +30,19 @@ class WooCommerceIntegrationAdmin(admin.ModelAdmin):
|
|||||||
('Синхронизация', {'fields': ('auto_sync_products', 'import_orders')}),
|
('Синхронизация', {'fields': ('auto_sync_products', 'import_orders')}),
|
||||||
('Служебное', {'fields': ('created_at', 'updated_at'), 'classes': ('collapse',)}),
|
('Служебное', {'fields': ('created_at', 'updated_at'), 'classes': ('collapse',)}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(IntegrationCategoryMapping)
|
||||||
|
class IntegrationCategoryMappingAdmin(admin.ModelAdmin):
|
||||||
|
"""Админка для маппинга категорий на внешние площадки"""
|
||||||
|
list_display = ['category', 'integration_type', 'external_category_sku', 'external_category_name', 'updated_at']
|
||||||
|
list_filter = ['integration_type']
|
||||||
|
search_fields = ['category__name', 'category__sku', 'external_category_sku', 'external_category_name']
|
||||||
|
autocomplete_fields = ['category']
|
||||||
|
readonly_fields = ['created_at', 'updated_at']
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Связь', {'fields': ('category', 'integration_type')}),
|
||||||
|
('Внешняя категория', {'fields': ('external_category_sku', 'external_category_name')}),
|
||||||
|
('Служебное', {'fields': ('created_at', 'updated_at'), 'classes': ('collapse',)}),
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.0.10 on 2026-01-13 21:08
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('integrations', '0001_add_integration_models'),
|
||||||
|
('products', '0004_configurableproduct_primary_category_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='IntegrationCategoryMapping',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('integration_type', models.CharField(choices=[('recommerce', 'Recommerce'), ('woocommerce', 'WooCommerce')], db_index=True, max_length=20, verbose_name='Интеграция')),
|
||||||
|
('external_category_sku', models.CharField(help_text='SKU или ID категории на внешней площадке', max_length=100, verbose_name='Артикул категории во внешней системе')),
|
||||||
|
('external_category_name', models.CharField(blank=True, help_text='Для справки, не обязательно', max_length=200, verbose_name='Название категории во внешней системе')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создано')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлено')),
|
||||||
|
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integration_mappings', to='products.productcategory', verbose_name='Категория')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Маппинг категории',
|
||||||
|
'verbose_name_plural': 'Маппинги категорий',
|
||||||
|
'indexes': [models.Index(fields=['integration_type', 'external_category_sku'], name='integration_integra_450473_idx')],
|
||||||
|
'unique_together': {('category', 'integration_type')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -4,6 +4,7 @@ from .marketplaces import (
|
|||||||
WooCommerceIntegration,
|
WooCommerceIntegration,
|
||||||
RecommerceIntegration,
|
RecommerceIntegration,
|
||||||
)
|
)
|
||||||
|
from .category_mappings import IntegrationCategoryMapping
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'BaseIntegration',
|
'BaseIntegration',
|
||||||
@@ -11,4 +12,5 @@ __all__ = [
|
|||||||
'MarketplaceIntegration',
|
'MarketplaceIntegration',
|
||||||
'WooCommerceIntegration',
|
'WooCommerceIntegration',
|
||||||
'RecommerceIntegration',
|
'RecommerceIntegration',
|
||||||
|
'IntegrationCategoryMapping',
|
||||||
]
|
]
|
||||||
|
|||||||
65
myproject/integrations/models/category_mappings.py
Normal file
65
myproject/integrations/models/category_mappings.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
Модели для маппинга категорий на внешние площадки.
|
||||||
|
"""
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationCategoryMapping(models.Model):
|
||||||
|
"""
|
||||||
|
Маппинг внутренней категории на внешнюю категорию маркетплейса.
|
||||||
|
|
||||||
|
Позволяет связать категории товаров с их аналогами на внешних площадках
|
||||||
|
(Recommerce, WooCommerce и др.).
|
||||||
|
"""
|
||||||
|
|
||||||
|
INTEGRATION_CHOICES = [
|
||||||
|
('recommerce', 'Recommerce'),
|
||||||
|
('woocommerce', 'WooCommerce'),
|
||||||
|
]
|
||||||
|
|
||||||
|
category = models.ForeignKey(
|
||||||
|
'products.ProductCategory',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='integration_mappings',
|
||||||
|
verbose_name="Категория"
|
||||||
|
)
|
||||||
|
|
||||||
|
integration_type = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=INTEGRATION_CHOICES,
|
||||||
|
verbose_name="Интеграция",
|
||||||
|
db_index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
external_category_sku = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
verbose_name="Артикул категории во внешней системе",
|
||||||
|
help_text="SKU или ID категории на внешней площадке"
|
||||||
|
)
|
||||||
|
|
||||||
|
external_category_name = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Название категории во внешней системе",
|
||||||
|
help_text="Для справки, не обязательно"
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name="Создано"
|
||||||
|
)
|
||||||
|
updated_at = models.DateTimeField(
|
||||||
|
auto_now=True,
|
||||||
|
verbose_name="Обновлено"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Маппинг категории"
|
||||||
|
verbose_name_plural = "Маппинги категорий"
|
||||||
|
unique_together = [['category', 'integration_type']]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['integration_type', 'external_category_sku']),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.category.name} → {self.get_integration_type_display()}:{self.external_category_sku}"
|
||||||
@@ -48,9 +48,8 @@ def to_api_product(
|
|||||||
data['name'] = product.name
|
data['name'] = product.name
|
||||||
|
|
||||||
if 'parent_category_sku' in fields:
|
if 'parent_category_sku' in fields:
|
||||||
# TODO: Добавить поле recommerce_category_sku в модель Product или Category
|
# Получаем категорию через primary_category или fallback на M2M categories
|
||||||
# Пока пытаемся взять из атрибута, если он есть
|
_, category_sku = product.get_category_for_integration('recommerce')
|
||||||
category_sku = getattr(product, 'recommerce_category_sku', None)
|
|
||||||
if category_sku:
|
if category_sku:
|
||||||
data['parent_category_sku'] = category_sku
|
data['parent_category_sku'] = category_sku
|
||||||
|
|
||||||
|
|||||||
@@ -396,7 +396,7 @@ class ProductAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
|||||||
search_fields = ('name', 'sku', 'description', 'search_keywords')
|
search_fields = ('name', 'sku', 'description', 'search_keywords')
|
||||||
filter_horizontal = ('categories', 'tags', 'variant_groups')
|
filter_horizontal = ('categories', 'tags', 'variant_groups')
|
||||||
readonly_fields = ('photo_preview_large', 'archived_at', 'archived_by', 'cost_price', 'cost_price_details_display')
|
readonly_fields = ('photo_preview_large', 'archived_at', 'archived_by', 'cost_price', 'cost_price_details_display')
|
||||||
autocomplete_fields = ['base_unit']
|
autocomplete_fields = ['base_unit', 'primary_category']
|
||||||
actions = [
|
actions = [
|
||||||
restore_items,
|
restore_items,
|
||||||
delete_selected,
|
delete_selected,
|
||||||
@@ -408,7 +408,7 @@ class ProductAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Основная информация', {
|
('Основная информация', {
|
||||||
'fields': ('name', 'sku', 'variant_suffix', 'description', 'short_description', 'categories', 'base_unit', 'unit', 'price', 'sale_price')
|
'fields': ('name', 'sku', 'variant_suffix', 'description', 'short_description', 'categories', 'primary_category', 'base_unit', 'unit', 'price', 'sale_price')
|
||||||
}),
|
}),
|
||||||
('Себестоимость', {
|
('Себестоимость', {
|
||||||
'fields': ('cost_price_details_display',),
|
'fields': ('cost_price_details_display',),
|
||||||
@@ -589,6 +589,7 @@ class ProductKitAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
|||||||
prepopulated_fields = {'slug': ('name',)}
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
filter_horizontal = ('categories', 'tags')
|
filter_horizontal = ('categories', 'tags')
|
||||||
readonly_fields = ('photo_preview_large', 'base_price', 'archived_at', 'archived_by', 'order')
|
readonly_fields = ('photo_preview_large', 'base_price', 'archived_at', 'archived_by', 'order')
|
||||||
|
autocomplete_fields = ['primary_category']
|
||||||
actions = [
|
actions = [
|
||||||
show_poor_quality_photos,
|
show_poor_quality_photos,
|
||||||
show_excellent_quality_photos,
|
show_excellent_quality_photos,
|
||||||
@@ -597,7 +598,7 @@ class ProductKitAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Основная информация', {
|
('Основная информация', {
|
||||||
'fields': ('name', 'sku', 'slug', 'description', 'short_description', 'categories')
|
'fields': ('name', 'sku', 'slug', 'description', 'short_description', 'categories', 'primary_category')
|
||||||
}),
|
}),
|
||||||
('Ценообразование', {
|
('Ценообразование', {
|
||||||
'fields': ('base_price', 'price', 'sale_price', 'price_adjustment_type', 'price_adjustment_value'),
|
'fields': ('base_price', 'price', 'sale_price', 'price_adjustment_type', 'price_adjustment_value'),
|
||||||
@@ -1062,10 +1063,11 @@ class ConfigurableProductAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
|||||||
search_fields = ('name', 'sku', 'description')
|
search_fields = ('name', 'sku', 'description')
|
||||||
readonly_fields = ('created_at', 'updated_at', 'slug')
|
readonly_fields = ('created_at', 'updated_at', 'slug')
|
||||||
inlines = [ConfigurableProductOptionInline, ConfigurableProductAttributeInline]
|
inlines = [ConfigurableProductOptionInline, ConfigurableProductAttributeInline]
|
||||||
|
autocomplete_fields = ['primary_category']
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Основная информация', {
|
('Основная информация', {
|
||||||
'fields': ('name', 'sku', 'slug', 'status')
|
'fields': ('name', 'sku', 'slug', 'status', 'primary_category')
|
||||||
}),
|
}),
|
||||||
('Описание', {
|
('Описание', {
|
||||||
'fields': ('short_description', 'description')
|
'fields': ('short_description', 'description')
|
||||||
|
|||||||
@@ -87,6 +87,15 @@ class ProductForm(SKUUniqueMixin, forms.ModelForm):
|
|||||||
label="Категории"
|
label="Категории"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
primary_category = forms.ModelChoiceField(
|
||||||
|
queryset=ProductCategory.objects.filter(is_active=True),
|
||||||
|
required=False,
|
||||||
|
empty_label="Не выбрана",
|
||||||
|
label="Основная категория",
|
||||||
|
help_text="Используется для интеграций с внешними площадками",
|
||||||
|
widget=forms.Select(attrs={'class': 'form-select'})
|
||||||
|
)
|
||||||
|
|
||||||
tags = forms.ModelMultipleChoiceField(
|
tags = forms.ModelMultipleChoiceField(
|
||||||
queryset=ProductTag.objects.filter(is_active=True),
|
queryset=ProductTag.objects.filter(is_active=True),
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=forms.CheckboxSelectMultiple,
|
||||||
@@ -98,7 +107,7 @@ class ProductForm(SKUUniqueMixin, forms.ModelForm):
|
|||||||
model = Product
|
model = Product
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'sku', 'description', 'short_description', 'categories',
|
'name', 'sku', 'description', 'short_description', 'categories',
|
||||||
'tags', 'base_unit', 'price', 'sale_price', 'status',
|
'primary_category', 'tags', 'base_unit', 'price', 'sale_price', 'status',
|
||||||
'is_new', 'is_popular', 'is_special'
|
'is_new', 'is_popular', 'is_special'
|
||||||
]
|
]
|
||||||
labels = {
|
labels = {
|
||||||
@@ -193,6 +202,15 @@ class ProductKitForm(SKUUniqueMixin, forms.ModelForm):
|
|||||||
label="Категории"
|
label="Категории"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
primary_category = forms.ModelChoiceField(
|
||||||
|
queryset=ProductCategory.objects.filter(is_active=True),
|
||||||
|
required=False,
|
||||||
|
empty_label="Не выбрана",
|
||||||
|
label="Основная категория",
|
||||||
|
help_text="Используется для интеграций с внешними площадками",
|
||||||
|
widget=forms.Select(attrs={'class': 'form-select'})
|
||||||
|
)
|
||||||
|
|
||||||
tags = forms.ModelMultipleChoiceField(
|
tags = forms.ModelMultipleChoiceField(
|
||||||
queryset=ProductTag.objects.filter(is_active=True),
|
queryset=ProductTag.objects.filter(is_active=True),
|
||||||
widget=forms.CheckboxSelectMultiple,
|
widget=forms.CheckboxSelectMultiple,
|
||||||
@@ -204,7 +222,7 @@ class ProductKitForm(SKUUniqueMixin, forms.ModelForm):
|
|||||||
model = ProductKit
|
model = ProductKit
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'sku', 'description', 'short_description', 'categories',
|
'name', 'sku', 'description', 'short_description', 'categories',
|
||||||
'tags', 'sale_price', 'price_adjustment_type', 'price_adjustment_value', 'status'
|
'primary_category', 'tags', 'sale_price', 'price_adjustment_type', 'price_adjustment_value', 'status'
|
||||||
]
|
]
|
||||||
labels = {
|
labels = {
|
||||||
'name': 'Название',
|
'name': 'Название',
|
||||||
@@ -671,9 +689,18 @@ class ConfigurableProductForm(SKUUniqueMixin, forms.ModelForm):
|
|||||||
"""
|
"""
|
||||||
Форма для создания и редактирования вариативного товара.
|
Форма для создания и редактирования вариативного товара.
|
||||||
"""
|
"""
|
||||||
|
primary_category = forms.ModelChoiceField(
|
||||||
|
queryset=ProductCategory.objects.filter(is_active=True),
|
||||||
|
required=False,
|
||||||
|
empty_label="Не выбрана",
|
||||||
|
label="Основная категория",
|
||||||
|
help_text="Используется для интеграций с внешними площадками",
|
||||||
|
widget=forms.Select(attrs={'class': 'form-select'})
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConfigurableProduct
|
model = ConfigurableProduct
|
||||||
fields = ['name', 'sku', 'description', 'short_description', 'status']
|
fields = ['name', 'sku', 'description', 'short_description', 'primary_category', 'status']
|
||||||
labels = {
|
labels = {
|
||||||
'name': 'Название',
|
'name': 'Название',
|
||||||
'sku': 'Артикул',
|
'sku': 'Артикул',
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 5.0.10 on 2026-01-13 21:08
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0003_add_marketing_flags'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='configurableproduct',
|
||||||
|
name='primary_category',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Используется для интеграций с внешними площадками', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_%(class)ss', to='products.productcategory', verbose_name='Основная категория'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='primary_category',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Используется для интеграций с внешними площадками', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_%(class)ss', to='products.productcategory', verbose_name='Основная категория'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='productkit',
|
||||||
|
name='primary_category',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Используется для интеграций с внешними площадками', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_%(class)ss', to='products.productcategory', verbose_name='Основная категория'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -207,6 +207,17 @@ class BaseProductEntity(models.Model):
|
|||||||
verbose_name="Архивировано пользователем"
|
verbose_name="Архивировано пользователем"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Основная категория для интеграций
|
||||||
|
primary_category = models.ForeignKey(
|
||||||
|
'ProductCategory',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='primary_%(class)ss',
|
||||||
|
verbose_name="Основная категория",
|
||||||
|
help_text="Используется для интеграций с внешними площадками"
|
||||||
|
)
|
||||||
|
|
||||||
# Manager
|
# Manager
|
||||||
objects = models.Manager()
|
objects = models.Manager()
|
||||||
|
|
||||||
@@ -265,6 +276,43 @@ class BaseProductEntity(models.Model):
|
|||||||
"""Возвращает True если товар активен"""
|
"""Возвращает True если товар активен"""
|
||||||
return self.status == 'active'
|
return self.status == 'active'
|
||||||
|
|
||||||
|
def get_category_for_integration(self, integration_type: str = 'recommerce'):
|
||||||
|
"""
|
||||||
|
Получить категорию товара для конкретной интеграции.
|
||||||
|
|
||||||
|
Приоритет:
|
||||||
|
1. primary_category (если указана и есть маппинг)
|
||||||
|
2. Первая категория из categories с существующим маппингом (если есть M2M)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
integration_type: Тип интеграции ('recommerce', 'woocommerce')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (ProductCategory, external_sku) или (None, None)
|
||||||
|
"""
|
||||||
|
from integrations.models import IntegrationCategoryMapping
|
||||||
|
|
||||||
|
# Приоритет 1: primary_category
|
||||||
|
if self.primary_category:
|
||||||
|
mapping = IntegrationCategoryMapping.objects.filter(
|
||||||
|
category=self.primary_category,
|
||||||
|
integration_type=integration_type
|
||||||
|
).first()
|
||||||
|
if mapping:
|
||||||
|
return self.primary_category, mapping.external_category_sku
|
||||||
|
|
||||||
|
# Приоритет 2: первая категория из M2M (если есть)
|
||||||
|
if hasattr(self, 'categories'):
|
||||||
|
for category in self.categories.all():
|
||||||
|
mapping = IntegrationCategoryMapping.objects.filter(
|
||||||
|
category=category,
|
||||||
|
integration_type=integration_type
|
||||||
|
).first()
|
||||||
|
if mapping:
|
||||||
|
return category, mapping.external_category_sku
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Автогенерация slug из name если не задан.
|
Автогенерация slug из name если не задан.
|
||||||
|
|||||||
@@ -109,6 +109,21 @@ input[name*="DELETE"] {
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Основная категория для интеграций -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="id_primary_category" class="form-label">
|
||||||
|
Основная категория
|
||||||
|
<small class="text-muted">(для интеграций)</small>
|
||||||
|
</label>
|
||||||
|
{{ form.primary_category }}
|
||||||
|
{% if form.primary_category.help_text %}
|
||||||
|
<div class="form-text">{{ form.primary_category.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.primary_category.errors %}
|
||||||
|
<div class="text-danger small">{{ form.primary_category.errors.0 }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-0">
|
<div class="mb-0">
|
||||||
<label for="{{ form.status.id_for_label }}" class="form-label">Статус</label>
|
<label for="{{ form.status.id_for_label }}" class="form-label">Статус</label>
|
||||||
{{ form.status }}
|
{{ form.status }}
|
||||||
|
|||||||
@@ -467,6 +467,21 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Основная категория для интеграций -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="id_primary_category" class="form-label">
|
||||||
|
Основная категория
|
||||||
|
<small class="text-muted">(для интеграций)</small>
|
||||||
|
</label>
|
||||||
|
{{ form.primary_category }}
|
||||||
|
{% if form.primary_category.help_text %}
|
||||||
|
<div class="form-text">{{ form.primary_category.help_text }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.primary_category.errors %}
|
||||||
|
<div class="text-danger">{{ form.primary_category.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Теги -->
|
<!-- Теги -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{{ form.tags.label_tag }}
|
{{ form.tags.label_tag }}
|
||||||
|
|||||||
@@ -183,6 +183,17 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Основная категория для интеграций -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="id_primary_category" class="form-label small mb-1 text-muted">
|
||||||
|
Основная категория <small>(для интеграций)</small>
|
||||||
|
</label>
|
||||||
|
{{ form.primary_category }}
|
||||||
|
{% if form.primary_category.errors %}
|
||||||
|
<div class="text-danger small mt-1">{{ form.primary_category.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-0">
|
<div class="mb-0">
|
||||||
<label class="form-label small mb-1 text-muted">{{ form.tags.label }}</label>
|
<label class="form-label small mb-1 text-muted">{{ form.tags.label }}</label>
|
||||||
<div class="compact-checkboxes">
|
<div class="compact-checkboxes">
|
||||||
|
|||||||
@@ -184,6 +184,17 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Основная категория для интеграций -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="id_primary_category" class="form-label small mb-1 text-muted">
|
||||||
|
Основная категория <small>(для интеграций)</small>
|
||||||
|
</label>
|
||||||
|
{{ form.primary_category }}
|
||||||
|
{% if form.primary_category.errors %}
|
||||||
|
<div class="text-danger small mt-1">{{ form.primary_category.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-0">
|
<div class="mb-0">
|
||||||
<label class="form-label small mb-1 text-muted">{{ form.tags.label }}</label>
|
<label class="form-label small mb-1 text-muted">{{ form.tags.label }}</label>
|
||||||
<div class="compact-checkboxes">
|
<div class="compact-checkboxes">
|
||||||
|
|||||||
@@ -294,9 +294,9 @@
|
|||||||
<a href="{% url 'products:productkit-detail' item.pk %}">{{ item.name }}</a>
|
<a href="{% url 'products:productkit-detail' item.pk %}">{{ item.name }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
{% if item.is_new %}<span class="badge bg-warning text-dark" title="Новинка"><i class="bi bi-stars"></i></span>{% endif %}
|
{% if item.is_new %}<span class="badge bg-warning text-dark" style="font-size: 0.65rem;">Новинка</span>{% endif %}
|
||||||
{% if item.is_popular %}<span class="badge bg-danger" title="Популярный"><i class="bi bi-fire"></i></span>{% endif %}
|
{% if item.is_popular %}<span class="badge bg-danger" style="font-size: 0.65rem;">Популярный</span>{% endif %}
|
||||||
{% if item.is_special %}<span class="badge bg-success" title="Спецпредложение"><i class="bi bi-percent"></i></span>{% endif %}
|
{% if item.is_special %}<span class="badge bg-success" style="font-size: 0.65rem;">Акция</span>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td><code class="small">{{ item.sku }}</code></td>
|
<td><code class="small">{{ item.sku }}</code></td>
|
||||||
|
|||||||
Reference in New Issue
Block a user