feat(integrations): добавить поле primary_category и маппинг категорий для интеграций

Добавлена поддержка выбора основной категории (primary_category) для товаров и наборов, а также новая модель IntegrationCategoryMapping для связи категорий с внешними площадками. Теперь можно указать категорию товара, которая будет использоваться при экспорте на внешние площадки (Recommerce, WooCommerce и др.), с возможностью настройки маппинга категорий для каждого типа интеграции.
This commit is contained in:
2026-01-14 01:53:38 +03:00
parent 7fd361aaf8
commit 1fb280607a
14 changed files with 288 additions and 15 deletions

View File

@@ -1,5 +1,5 @@
from django.contrib import admin
from .models import RecommerceIntegration, WooCommerceIntegration
from .models import RecommerceIntegration, WooCommerceIntegration, IntegrationCategoryMapping
@admin.register(RecommerceIntegration)
@@ -30,3 +30,19 @@ class WooCommerceIntegrationAdmin(admin.ModelAdmin):
('Синхронизация', {'fields': ('auto_sync_products', 'import_orders')}),
('Служебное', {'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',)}),
)

View File

@@ -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')},
},
),
]

View File

@@ -4,6 +4,7 @@ from .marketplaces import (
WooCommerceIntegration,
RecommerceIntegration,
)
from .category_mappings import IntegrationCategoryMapping
__all__ = [
'BaseIntegration',
@@ -11,4 +12,5 @@ __all__ = [
'MarketplaceIntegration',
'WooCommerceIntegration',
'RecommerceIntegration',
'IntegrationCategoryMapping',
]

View 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}"

View File

@@ -48,9 +48,8 @@ def to_api_product(
data['name'] = product.name
if 'parent_category_sku' in fields:
# TODO: Добавить поле recommerce_category_sku в модель Product или Category
# Пока пытаемся взять из атрибута, если он есть
category_sku = getattr(product, 'recommerce_category_sku', None)
# Получаем категорию через primary_category или fallback на M2M categories
_, category_sku = product.get_category_for_integration('recommerce')
if category_sku:
data['parent_category_sku'] = category_sku