from typing import Dict, Any, List, Optional from decimal import Decimal # Константа для "бесконечного" наличия в Recommerce # Используется вместо "inf", который API не принимает RECOMMERCE_INFINITY_COUNT = 999999 def to_api_product( product: Any, stock_count: Optional[int] = None, fields: Optional[List[str]] = None, for_create: bool = False ) -> Dict[str, Any]: """ Преобразование внутреннего товара в формат Recommerce API. Recommerce API использует form-data для POST запросов, поэтому вложенные поля представляются в плоском формате: price[amount], price[currency] Args: product: Экземпляр модели Product stock_count: Остаток товара (вычисляется отдельно, т.к. зависит от склада) fields: Список полей для экспорта. Если None - экспортируются все базовые поля. Поддерживаемые поля: 'sku', 'name', 'price', 'description', 'count', 'images'. for_create: Если True - включает все обязательные поля для создания товара. Returns: Dict: Плоский словарь для form-data запроса """ data = {} # Если поля не указаны, берем базовый набор (без тяжелых полей типа картинок) if fields is None: fields = ['sku', 'name', 'price'] # Для создания товара добавляем все обязательные поля if for_create: fields = list(set(fields) | {'sku', 'name', 'price', 'parent_category_sku'}) # SKU (Артикул) - обязательное поле для идентификации sku = getattr(product, 'sku', str(product.id)) if 'sku' in fields: data['sku'] = sku if 'name' in fields: data['name'] = product.name if 'parent_category_sku' in fields: # TODO: Добавить поле recommerce_category_sku в модель Product или Category # Пока пытаемся взять из атрибута, если он есть category_sku = getattr(product, 'recommerce_category_sku', None) if category_sku: data['parent_category_sku'] = category_sku # Вычисляем has_discount до блока price (нужно для is_special) has_discount = product.sale_price and product.price and product.sale_price < product.price if 'price' in fields: # Recommerce ожидает price[amount] и price[currency] в form-data формате # Значения передаём как строки (так работает в проверенном скрипте) if has_discount: # Есть скидка: текущая цена = sale_price, старая = price data['price[amount]'] = str(float(product.sale_price)) data['price[currency]'] = 'BYN' # price_old - как в рабочем скрипте (не old_price из документации) data['price_old[amount]'] = str(float(product.price)) data['price_old[currency]'] = 'BYN' else: # Нет скидки: только основная цена data['price[amount]'] = str(float(product.price or 0)) data['price[currency]'] = 'BYN' # Важно: передаем "0" для price_old, чтобы сбросить зачеркнутую цену (как в рабочем скрипте) data['price_old[amount]'] = "0" data['price_old[currency]'] = 'BYN' if 'content' in fields: # content включает название и описание data['name'] = product.name data['description'] = product.description or '' if 'description' in fields: data['description'] = product.description or '' if 'count' in fields and stock_count is not None: data['count'] = stock_count if 'images' in fields: # Recommerce ожидает images[] для массива картинок if hasattr(product, 'photos'): for idx, photo in enumerate(product.photos.all()): if photo.image: data[f'images[{idx}]'] = photo.image.url # Обработка маркетинговых флагов (формат: 1/0 для Recommerce API) # Добавляем только если поле указано в fields ИЛИ fields=None (полное обновление) if fields is None or 'is_new' in fields: if hasattr(product, 'is_new'): data['new'] = 1 if product.is_new else 0 if fields is None or 'is_popular' in fields: if hasattr(product, 'is_popular'): data['popular'] = 1 if product.is_popular else 0 # special - из модели is_special ИЛИ автоматически при наличии скидке if fields is None or 'is_special' in fields: if hasattr(product, 'is_special'): data['special'] = 1 if (product.is_special or has_discount) else 0 else: data['special'] = 1 if has_discount else 0 return data def from_api_order(data: Dict[str, Any]) -> Dict[str, Any]: """ Преобразование заказа из Recommerce в DTO (словарь) для создания внутреннего заказа. Args: data: JSON заказа от Recommerce Returns: Dict: Структурированные данные для создания Order """ # Извлекаем основные поля recommerce_id = data.get('id') customer_data = data.get('customer', {}) delivery_data = data.get('delivery', {}) items_data = data.get('products', []) # Формируем DTO return { 'external_id': str(recommerce_id), 'source': 'recommerce', 'status_external': data.get('state'), 'date_created': data.get('date'), # Unix timestamp 'customer': { 'name': customer_data.get('name'), 'phone': customer_data.get('phone'), 'email': customer_data.get('email'), 'address': _extract_address(data), }, 'items': [ { 'sku': item.get('sku'), 'quantity': item.get('count', 1), 'price': item.get('price', {}).get('amount', 0), 'name': item.get('title'), } for item in items_data ], 'total_amount': data.get('total_amount', {}).get('amount'), 'notes': data.get('note'), } def _extract_address(data: Dict[str, Any]) -> str: """Вспомогательная функция для сборки адреса""" delivery = data.get('delivery', {}) fields = delivery.get('fields', []) # Recommerce может хранить адрес в полях доставки address_parts = [] # Пробуем найти стандартные поля for field in fields: if field.get('value'): address_parts.append(f"{field.get('name')}: {field.get('value')}") if not address_parts: # Fallback если адрес в другом месте или не задан return "Адрес не указан в стандартных полях" return ", ".join(address_parts)