feat(woocommerce): реализовать проверку соединения с WooCommerce API

- Добавлена реализация метода test_connection() с обработкой различных HTTP статусов
- Реализованы вспомогательные методы _get_api_url() и _get_auth() для работы с API
- Добавлена интеграция WooCommerceService в get_integration_service()
- Настроены поля формы для WooCommerceIntegration в get_form_fields_meta()

fix(inventory): исправить расчет цены продажи в базовых единицах

- Исправлен расчет sale_price в SaleProcessor с учетом conversion_factor_snapshot
- Обновлен расчет цены в сигнале create_sale_on_order_completion для корректной работы с sales_unit
This commit is contained in:
2026-01-20 23:05:18 +03:00
parent 271ac66098
commit 9cd3796527
4 changed files with 84 additions and 8 deletions

View File

@@ -1,3 +1,4 @@
import requests
from typing import Tuple from typing import Tuple
from .base import MarketplaceService from .base import MarketplaceService
@@ -5,16 +6,58 @@ from .base import MarketplaceService
class WooCommerceService(MarketplaceService): class WooCommerceService(MarketplaceService):
"""Сервис для работы с WooCommerce API""" """Сервис для работы с WooCommerce API"""
def _get_api_url(self) -> str:
"""Получить базовый URL для WooCommerce REST API"""
base = self.config.store_url.rstrip('/')
# WooCommerce REST API v3 endpoint
return f"{base}/wp-json/wc/v3/"
def _get_auth(self) -> tuple:
"""Получить кортеж для Basic Auth (consumer_key, consumer_secret)"""
return (self.config.consumer_key or '', self.config.consumer_secret or '')
def test_connection(self) -> Tuple[bool, str]: def test_connection(self) -> Tuple[bool, str]:
"""Проверить соединение с WooCommerce API""" """
Проверить соединение с WooCommerce API.
Использует endpoint /wp-json/wc/v3/ для проверки.
Аутентификация через HTTP Basic Auth.
"""
if not self.config.store_url: if not self.config.store_url:
return False, 'Не указан URL магазина' return False, 'Не указан URL магазина'
if not self.config.consumer_key or not self.config.consumer_secret: if not self.config.consumer_key or not self.config.consumer_secret:
return False, 'Не указаны ключи API' return False, 'Не указаны ключи API'
# TODO: реализовать проверку соединения с WooCommerce API url = self._get_api_url()
return True, 'Соединение успешно (заглушка)'
try:
# Пытаемся получить список товаров (limit=1) для проверки авторизации
# Это более надёжный способ проверки, чем просто обращение к корню API
response = requests.get(
f"{url}products",
params={'per_page': 1},
auth=self._get_auth(),
timeout=15
)
if response.status_code == 200:
return True, 'Соединение установлено успешно'
elif response.status_code == 401:
return False, 'Неверные ключи API (Consumer Key/Secret)'
elif response.status_code == 403:
return False, 'Доступ запрещён. Проверьте права API ключа'
elif response.status_code == 404:
return False, 'WooCommerce REST API не найден. Проверьте, что WooCommerce установлен и активирован'
else:
return False, f'Ошибка соединения: HTTP {response.status_code}'
except requests.exceptions.Timeout:
return False, 'Таймаут соединения (15 сек)'
except requests.exceptions.ConnectionError:
return False, 'Не удалось подключиться к серверу. Проверьте URL магазина'
except Exception as e:
return False, f'Ошибка: {str(e)}'
def sync(self) -> Tuple[bool, str]: def sync(self) -> Tuple[bool, str]:
"""Выполнить синхронизацию с WooCommerce""" """Выполнить синхронизацию с WooCommerce"""

View File

@@ -170,8 +170,8 @@ def get_integration_service(integration_id: str, instance):
from .services.marketplaces.recommerce import RecommerceService from .services.marketplaces.recommerce import RecommerceService
return RecommerceService(instance) return RecommerceService(instance)
elif integration_id == 'woocommerce': elif integration_id == 'woocommerce':
# TODO: WooCommerceService from .services.marketplaces.woocommerce import WooCommerceService
return None return WooCommerceService(instance)
elif integration_id == 'glm': elif integration_id == 'glm':
from .services.ai_services.glm_service import GLMIntegrationService from .services.ai_services.glm_service import GLMIntegrationService
return GLMIntegrationService(instance) return GLMIntegrationService(instance)
@@ -386,6 +386,29 @@ def get_form_fields_meta(model):
'choices': getattr(field, 'choices', []) 'choices': getattr(field, 'choices', [])
} }
fields.append(field_info)
# Для WooCommerce показываем только базовые поля для подключения
elif model.__name__ == 'WooCommerceIntegration':
basic_fields = ['store_url', 'consumer_key', 'consumer_secret']
for field_name in editable_fields:
if field_name in basic_fields:
field = model._meta.get_field(field_name)
field_info = {
'name': field_name,
'label': getattr(field, 'verbose_name', field_name),
'help_text': getattr(field, 'help_text', ''),
'required': not getattr(field, 'blank', True),
'type': 'text',
}
# Определить тип поля
if 'BooleanField' in field.__class__.__name__:
field_info['type'] = 'checkbox'
elif 'URLField' in field.__class__.__name__:
field_info['type'] = 'url'
elif 'secret' in field_name.lower() or 'key' in field_name.lower():
field_info['type'] = 'password'
fields.append(field_info) fields.append(field_info)
else: else:
# Для других интеграций - все редактируемые поля # Для других интеграций - все редактируемые поля

View File

@@ -35,8 +35,12 @@ class SaleProcessor:
""" """
# Определяем цену продажи из заказа или из товара # Определяем цену продажи из заказа или из товара
if order and reservation.order_item: if order and reservation.order_item:
# Цена из OrderItem item = reservation.order_item
sale_price = reservation.order_item.price # Пересчитываем цену в базовые единицы
if item.sales_unit and item.conversion_factor_snapshot:
sale_price = Decimal(str(item.price)) * item.conversion_factor_snapshot
else:
sale_price = item.price
else: else:
# Цена из товара # Цена из товара
sale_price = reservation.product.actual_price or Decimal('0') sale_price = reservation.product.actual_price or Decimal('0')

View File

@@ -480,12 +480,18 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
f"Используем quantity_in_base_units: {sale_quantity}" f"Используем quantity_in_base_units: {sale_quantity}"
) )
# Пересчитываем цену в базовые единицы
if item.sales_unit and item.conversion_factor_snapshot:
base_price = Decimal(str(item.price)) * item.conversion_factor_snapshot
else:
base_price = Decimal(str(item.price))
# Создаем Sale (с автоматическим FIFO-списанием) # Создаем Sale (с автоматическим FIFO-списанием)
sale = SaleProcessor.create_sale( sale = SaleProcessor.create_sale(
product=product, product=product,
warehouse=warehouse, warehouse=warehouse,
quantity=sale_quantity, quantity=sale_quantity,
sale_price=Decimal(str(item.price)), sale_price=base_price,
order=instance, order=instance,
document_number=instance.order_number, document_number=instance.order_number,
sales_unit=item.sales_unit # Передаем sales_unit в Sale sales_unit=item.sales_unit # Передаем sales_unit в Sale