From 9cd3796527924bbd56e9094bdc08fb8536785434 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Tue, 20 Jan 2026 23:05:18 +0300 Subject: [PATCH] =?UTF-8?q?feat(woocommerce):=20=D1=80=D0=B5=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=BA=D1=83=20=D1=81=D0=BE=D0=B5=D0=B4=D0=B8?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=20WooCommerce=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлена реализация метода 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 --- .../services/marketplaces/woocommerce.py | 49 +++++++++++++++++-- myproject/integrations/views.py | 27 +++++++++- .../inventory/services/sale_processor.py | 8 ++- myproject/inventory/signals.py | 8 ++- 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/myproject/integrations/services/marketplaces/woocommerce.py b/myproject/integrations/services/marketplaces/woocommerce.py index 364f258..27d4ce9 100644 --- a/myproject/integrations/services/marketplaces/woocommerce.py +++ b/myproject/integrations/services/marketplaces/woocommerce.py @@ -1,3 +1,4 @@ +import requests from typing import Tuple from .base import MarketplaceService @@ -5,16 +6,58 @@ from .base import MarketplaceService class WooCommerceService(MarketplaceService): """Сервис для работы с 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]: - """Проверить соединение с WooCommerce API""" + """ + Проверить соединение с WooCommerce API. + + Использует endpoint /wp-json/wc/v3/ для проверки. + Аутентификация через HTTP Basic Auth. + """ if not self.config.store_url: return False, 'Не указан URL магазина' if not self.config.consumer_key or not self.config.consumer_secret: return False, 'Не указаны ключи API' - # TODO: реализовать проверку соединения с WooCommerce API - return True, 'Соединение успешно (заглушка)' + url = self._get_api_url() + + 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]: """Выполнить синхронизацию с WooCommerce""" diff --git a/myproject/integrations/views.py b/myproject/integrations/views.py index 9b0739b..5f32330 100644 --- a/myproject/integrations/views.py +++ b/myproject/integrations/views.py @@ -170,8 +170,8 @@ def get_integration_service(integration_id: str, instance): from .services.marketplaces.recommerce import RecommerceService return RecommerceService(instance) elif integration_id == 'woocommerce': - # TODO: WooCommerceService - return None + from .services.marketplaces.woocommerce import WooCommerceService + return WooCommerceService(instance) elif integration_id == 'glm': from .services.ai_services.glm_service import GLMIntegrationService return GLMIntegrationService(instance) @@ -386,6 +386,29 @@ def get_form_fields_meta(model): '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) else: # Для других интеграций - все редактируемые поля diff --git a/myproject/inventory/services/sale_processor.py b/myproject/inventory/services/sale_processor.py index 3f5f08b..8900b1a 100644 --- a/myproject/inventory/services/sale_processor.py +++ b/myproject/inventory/services/sale_processor.py @@ -35,8 +35,12 @@ class SaleProcessor: """ # Определяем цену продажи из заказа или из товара if order and reservation.order_item: - # Цена из OrderItem - sale_price = reservation.order_item.price + item = reservation.order_item + # Пересчитываем цену в базовые единицы + 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: # Цена из товара sale_price = reservation.product.actual_price or Decimal('0') diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py index 0ee4c7a..f4ae3d1 100644 --- a/myproject/inventory/signals.py +++ b/myproject/inventory/signals.py @@ -480,12 +480,18 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs): 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 = SaleProcessor.create_sale( product=product, warehouse=warehouse, quantity=sale_quantity, - sale_price=Decimal(str(item.price)), + sale_price=base_price, order=instance, document_number=instance.order_number, sales_unit=item.sales_unit # Передаем sales_unit в Sale