- Исправлен формат флага: special=1/0 вместо is_special="true"/"false" - Добавлен тестовый скрипт test_is_special.py для отладки API Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
163 lines
7.0 KiB
Python
163 lines
7.0 KiB
Python
from typing import Dict, Any, List, Optional
|
|
from decimal import Decimal
|
|
|
|
|
|
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
|
|
|
|
# Обработка флагов товара (как строки "true"/"false" согласно документации API)
|
|
if hasattr(product, 'is_new'):
|
|
data['is_new'] = "true" if product.is_new else "false"
|
|
if hasattr(product, 'is_popular'):
|
|
data['is_popular'] = "true" if product.is_popular else "false"
|
|
# special - автоматически при наличии скидки (формат: 1/0, не is_special)
|
|
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) |