import logging from celery import shared_task from django.db import transaction from django_tenants.utils import schema_context from integrations.models import RecommerceIntegration from integrations.recommerce.services import RecommerceService from products.models import Product from integrations.recommerce.exceptions import RecommerceError, RecommerceAPIError logger = logging.getLogger(__name__) @shared_task(bind=True) def sync_products_batch_task(self, product_ids, options=None, schema_name=None): """ Celery задача для массовой синхронизации товаров с Recommerce. Args: product_ids (list): Список ID товаров (PK) для синхронизации options (dict): Настройки синхронизации - fields (list): Список полей для обновления ['price', 'count', 'content', 'images'] - create_if_missing (bool): Создавать товар, если он не найден (404) schema_name (str): Имя схемы тенанта для выполнения запросов """ if options is None: options = {} fields = options.get('fields', []) create_if_missing = options.get('create_if_missing', False) # Используем schema_context для выполнения запросов в правильной tenant схеме if schema_name: with schema_context(schema_name): return _do_sync(product_ids, fields, create_if_missing) else: return _do_sync(product_ids, fields, create_if_missing) def _do_sync(product_ids, fields, create_if_missing): """ Внутренняя функция для выполнения синхронизации в контексте схемы. Args: product_ids (list): Список ID товаров (PK) для синхронизации fields (list): Список полей для обновления ['price', 'count', 'content', 'images'] create_if_missing (bool): Создавать товар, если он не найден (404) """ # 1. Получаем интеграцию integration = RecommerceIntegration.objects.filter(is_active=True).first() if not integration or not integration.is_configured: msg = "Recommerce integration is not active or configured." logger.error(msg) return {"success": False, "error": msg} service = RecommerceService(integration) # 2. Получаем товары products = Product.objects.filter(pk__in=product_ids) results = { "total": len(product_ids), "success": 0, "failed": 0, "created": 0, "updated": 0, "errors": [] } logger.info(f"Starting Recommerce sync for {len(product_ids)} products. Fields: {fields}, create_if_missing: {create_if_missing}") for product in products: try: # Если fields пустой или содержит 'all' - обновляем всё (передаем None в сервис) # Иначе передаем список полей. # В сервисе update_product(fields=None) обновляет всё. # Маппинг опций фронтенда на логику сервиса # Фронт: ['price', 'count', 'content', 'images'] # Сервис: ожидает список полей или None (всё). # Если выбраны не все галочки, передаем конкретные поля. # Но 'content' и 'images' в сервисе могут не поддерживаться напрямую как ключи, # нужно смотреть реализацию to_api_product. # Пока передаем как есть, предполагая, что сервис или маппер разберется, # либо если выбрано "все", передаем None. # Упрощение: если выбраны все основные группы, считаем это полным обновлением is_full_update = False if 'content' in fields and 'images' in fields and 'price' in fields and 'count' in fields: is_full_update = True service_fields = None if is_full_update else fields # Если список полей пуст (ничего не выбрано), но задача запущена - странно, но пропустим if not is_full_update and not service_fields: logger.warning(f"Product {product.id}: No fields selected for update.") continue try: service.update_product(product, fields=service_fields) results["updated"] += 1 results["success"] += 1 except RecommerceAPIError as e: # Если товар не найден (404) и разрешено создание if e.status_code == 404 and create_if_missing: logger.info(f"Product {product.sku} not found. Creating...") service.create_product(product) results["created"] += 1 results["success"] += 1 else: raise e except Exception as e: results["failed"] += 1 error_msg = f"Product {product.sku} ({product.id}): {str(e)}" logger.error(error_msg) results["errors"].append(error_msg) logger.info(f"Recommerce sync completed. Results: {results}") return results