diff --git a/myproject/integrations/admin.py b/myproject/integrations/admin.py index 19da2d6..4e1eda0 100644 --- a/myproject/integrations/admin.py +++ b/myproject/integrations/admin.py @@ -1,7 +1,44 @@ from django.contrib import admin +from system_settings.models import IntegrationConfig -# Регистрация конкретных интеграций будет здесь +@admin.register(IntegrationConfig) +class IntegrationConfigAdmin(admin.ModelAdmin): + """Админка для настроек интеграций (тумблеров)""" + list_display = ['get_integration_id_display', 'is_enabled', 'last_sync_at', 'updated_at'] + list_filter = ['is_enabled', 'integration_id'] + search_fields = ['integration_id'] + list_editable = ['is_enabled'] + + def has_add_permission(self, request): + """Запретить добавление новых интеграций вручную""" + return False + + def has_delete_permission(self, request, obj=None): + """Запретить удаление интеграций""" + return False + + +# Регистрация конкретных интеграций (когда будут готовы): +# # @admin.register(WooCommerceIntegration) # class WooCommerceIntegrationAdmin(admin.ModelAdmin): -# pass +# list_display = ['name', 'store_url', 'is_active', 'is_configured', 'updated_at'] +# list_filter = ['is_active', 'auto_sync_products'] +# fieldsets = ( +# ('Основное', {'fields': ('name', 'is_active')}), +# ('API настройки', {'fields': ('store_url', 'consumer_key', 'consumer_secret')}), +# ('Синхронизация', {'fields': ('auto_sync_products', 'import_orders')}), +# ) +# +# +# @admin.register(RecommerceIntegration) +# class RecommerceIntegrationAdmin(admin.ModelAdmin): +# list_display = ['name', 'merchant_id', 'is_active', 'is_configured', 'updated_at'] +# list_filter = ['is_active', 'sync_prices', 'sync_stock'] +# fieldsets = ( +# ('Основное', {'fields': ('name', 'is_active')}), +# ('API настройки', {'fields': ('store_url', 'api_endpoint', 'api_token', 'merchant_id')}), +# ('Синхронизация', {'fields': ('auto_sync_products', 'import_orders', 'sync_prices', 'sync_stock')}), +# ) + diff --git a/myproject/integrations/models/__init__.py b/myproject/integrations/models/__init__.py index 6b326eb..163e3cf 100644 --- a/myproject/integrations/models/__init__.py +++ b/myproject/integrations/models/__init__.py @@ -1,3 +1,14 @@ -from .base import BaseIntegration +from .base import BaseIntegration, IntegrationType +from .marketplaces import ( + MarketplaceIntegration, + WooCommerceIntegration, + RecommerceIntegration, +) -__all__ = ['BaseIntegration'] +__all__ = [ + 'BaseIntegration', + 'IntegrationType', + 'MarketplaceIntegration', + 'WooCommerceIntegration', + 'RecommerceIntegration', +] diff --git a/myproject/integrations/models/marketplaces/__init__.py b/myproject/integrations/models/marketplaces/__init__.py new file mode 100644 index 0000000..ea6e8da --- /dev/null +++ b/myproject/integrations/models/marketplaces/__init__.py @@ -0,0 +1,9 @@ +from .base import MarketplaceIntegration +from .woocommerce import WooCommerceIntegration +from .recommerce import RecommerceIntegration + +__all__ = [ + 'MarketplaceIntegration', + 'WooCommerceIntegration', + 'RecommerceIntegration', +] diff --git a/myproject/integrations/models/marketplaces/base.py b/myproject/integrations/models/marketplaces/base.py new file mode 100644 index 0000000..d5a2126 --- /dev/null +++ b/myproject/integrations/models/marketplaces/base.py @@ -0,0 +1,42 @@ +from django.db import models +from ..base import BaseIntegration, IntegrationType + + +class MarketplaceIntegration(BaseIntegration): + """ + Базовая модель для интеграций с маркетплейсами. + Наследует BaseIntegration и добавляет специфичные поля. + """ + + integration_type = models.CharField( + max_length=20, + choices=IntegrationType.choices, + default=IntegrationType.MARKETPLACE, + editable=False + ) + + # URL магазина + store_url = models.URLField( + blank=True, + verbose_name="URL магазина", + help_text="Адрес магазина (например, https://shop.example.com)" + ) + + # Автоматическая синхронизация товаров + auto_sync_products = models.BooleanField( + default=False, + verbose_name="Авто-синхронизация товаров", + help_text="Автоматически обновлять товары на маркетплейсе" + ) + + # Импорт заказов + import_orders = models.BooleanField( + default=False, + verbose_name="Импорт заказов", + help_text="Импортировать заказы с маркетплейса" + ) + + class Meta: + abstract = True + verbose_name = "Интеграция с маркетплейсом" + verbose_name_plural = "Интеграции с маркетплейсами" diff --git a/myproject/integrations/models/marketplaces/recommerce.py b/myproject/integrations/models/marketplaces/recommerce.py new file mode 100644 index 0000000..dda604d --- /dev/null +++ b/myproject/integrations/models/marketplaces/recommerce.py @@ -0,0 +1,59 @@ +from django.db import models +from .base import MarketplaceIntegration + + +class RecommerceIntegration(MarketplaceIntegration): + """ + Интеграция с Recommerce. + Recommerce - сервис для управления товарами на маркетплейсах. + """ + + # API endpoint (может отличаться от store_url) + api_endpoint = models.URLField( + blank=True, + verbose_name="API Endpoint", + help_text="URL API Recommerce (если отличается от URL магазина)" + ) + + # API токен (основной метод авторизации) + api_token = models.CharField( + max_length=500, + blank=True, + verbose_name="API Токен", + help_text="Токен авторизации Recommerce API" + ) + + # ID магазина в системе Recommerce + merchant_id = models.CharField( + max_length=100, + blank=True, + verbose_name="ID магазина", + help_text="Идентификатор магазина в Recommerce" + ) + + # Синхронизация цен + sync_prices = models.BooleanField( + default=True, + verbose_name="Синхронизировать цены", + help_text="Обновлять цены на маркетплейсе" + ) + + # Синхронизация остатков + sync_stock = models.BooleanField( + default=True, + verbose_name="Синхронизировать остатки", + help_text="Обновлять остатки на маркетплейсе" + ) + + class Meta: + verbose_name = "Recommerce" + verbose_name_plural = "Recommerce" + managed = False # Пока заготовка - без создания таблицы + + def __str__(self): + return f"Recommerce: {self.name or self.merchant_id}" + + @property + def is_configured(self) -> bool: + """Recommerce требует api_token""" + return bool(self.api_token) diff --git a/myproject/integrations/models/marketplaces/woocommerce.py b/myproject/integrations/models/marketplaces/woocommerce.py new file mode 100644 index 0000000..780beed --- /dev/null +++ b/myproject/integrations/models/marketplaces/woocommerce.py @@ -0,0 +1,40 @@ +from django.db import models +from .base import MarketplaceIntegration + + +class WooCommerceIntegration(MarketplaceIntegration): + """Интеграция с WooCommerce""" + + # WooCommerce-specific credentials + consumer_key = models.CharField( + max_length=255, + blank=True, + verbose_name="Consumer Key" + ) + + consumer_secret = models.CharField( + max_length=255, + blank=True, + verbose_name="Consumer Secret" + ) + + # API версия (WooCommerce REST API v1, v2, v3) + api_version = models.CharField( + max_length=10, + default='v3', + blank=True, + verbose_name="Версия API" + ) + + class Meta: + verbose_name = "WooCommerce" + verbose_name_plural = "WooCommerce" + managed = False # Пока заготовка - без создания таблицы + + def __str__(self): + return f"WooCommerce: {self.name or self.store_url}" + + @property + def is_configured(self) -> bool: + """WooCommerce требует consumer_key и consumer_secret""" + return bool(self.consumer_key and self.consumer_secret) diff --git a/myproject/integrations/services/__init__.py b/myproject/integrations/services/__init__.py index a0b243a..20a6eee 100644 --- a/myproject/integrations/services/__init__.py +++ b/myproject/integrations/services/__init__.py @@ -1,3 +1,13 @@ from .base import BaseIntegrationService +from .marketplaces import ( + MarketplaceService, + WooCommerceService, + RecommerceService, +) -__all__ = ['BaseIntegrationService'] +__all__ = [ + 'BaseIntegrationService', + 'MarketplaceService', + 'WooCommerceService', + 'RecommerceService', +] diff --git a/myproject/integrations/services/marketplaces/__init__.py b/myproject/integrations/services/marketplaces/__init__.py new file mode 100644 index 0000000..6f8bb33 --- /dev/null +++ b/myproject/integrations/services/marketplaces/__init__.py @@ -0,0 +1,9 @@ +from .base import MarketplaceService +from .woocommerce import WooCommerceService +from .recommerce import RecommerceService + +__all__ = [ + 'MarketplaceService', + 'WooCommerceService', + 'RecommerceService', +] diff --git a/myproject/integrations/services/marketplaces/base.py b/myproject/integrations/services/marketplaces/base.py new file mode 100644 index 0000000..b96c709 --- /dev/null +++ b/myproject/integrations/services/marketplaces/base.py @@ -0,0 +1,40 @@ +from typing import Tuple +import requests +from ..base import BaseIntegrationService + + +class MarketplaceService(BaseIntegrationService): + """ + Базовый сервис для маркетплейсов. + Содержит общие методы для работы с API маркетплейсов. + """ + + def _get_headers(self) -> dict: + """Получить заголовки для API запросов""" + return { + 'Content-Type': 'application/json', + 'User-Agent': 'MyProject/1.0', + } + + def _make_request(self, url: str, method: str = 'GET', **kwargs) -> Tuple[bool, dict, str]: + """ + Сделать HTTP запрос к API. + + Returns: + tuple: (success, response_data, error_message) + """ + try: + response = requests.request(method, url, headers=self._get_headers(), timeout=30, **kwargs) + + if response.status_code in [200, 201]: + return True, response.json(), '' + else: + error_msg = f"HTTP {response.status_code}: {response.text}" + return False, {}, error_msg + + except requests.exceptions.Timeout: + return False, {}, 'Таймаут соединения' + except requests.exceptions.ConnectionError: + return False, {}, 'Ошибка соединения' + except Exception as e: + return False, {}, str(e) diff --git a/myproject/integrations/services/marketplaces/recommerce.py b/myproject/integrations/services/marketplaces/recommerce.py new file mode 100644 index 0000000..ad09f18 --- /dev/null +++ b/myproject/integrations/services/marketplaces/recommerce.py @@ -0,0 +1,142 @@ +from typing import Tuple +from .base import MarketplaceService + + +class RecommerceService(MarketplaceService): + """ + Сервис для работы с Recommerce API. + Recommerce - агрегатор маркетплейсов (WB, Ozon, Яндекс и др.) + """ + + API_BASE_URL = "https://api.recommerce.ru" # Пример, нужно уточнить + + def _get_headers(self) -> dict: + """Получить заголовки с токеном авторизации""" + headers = super()._get_headers() + if self.config.api_token: + headers['Authorization'] = f'Bearer {self.config.api_token}' + return headers + + def _get_api_url(self, path: str) -> str: + """Получить полный URL для API endpoint""" + base = self.config.api_endpoint or self.API_BASE_URL + return f"{base.rstrip('/')}/{path.lstrip('/')}" + + def test_connection(self) -> Tuple[bool, str]: + """ + Проверить соединение с Recommerce API. + + Returns: + tuple: (success, message) + """ + if not self.config.api_token: + return False, 'Не указан API токен' + + # Проверка соединения через endpoint информации о магазине + if self.config.merchant_id: + url = self._get_api_url(f'/merchants/{self.config.merchant_id}') + else: + url = self._get_api_url('/merchants/me') + + success, data, error = self._make_request(url) + + if success: + merchant_name = data.get('name', 'Магазин') + return True, f'Соединение установлено: {merchant_name}' + else: + return False, f'Ошибка соединения: {error}' + + def sync(self) -> Tuple[bool, str]: + """ + Выполнить синхронизацию с Recommerce. + + Returns: + tuple: (success, message) + """ + if not self.is_available(): + return False, 'Интеграция не настроена или отключена' + + # TODO: реализовать полную синхронизацию + # - Загрузка товаров с маркетплейсов + # - Обновление цен + # - Обновление остатков + # - Загрузка заказов + + return True, 'Синхронизация запущена (заглушка)' + + def fetch_products(self) -> Tuple[bool, list, str]: + """ + Получить товары с Recommerce. + + Returns: + tuple: (success, products, error_message) + """ + url = self._get_api_url('/products') + success, data, error = self._make_request(url) + + if success: + products = data.get('items', []) + return True, products, '' + else: + return False, [], error + + def push_product(self, product_data: dict) -> Tuple[bool, str]: + """ + Отправить товар на Recommerce. + + Args: + product_data: Данные товара для отправки + + Returns: + tuple: (success, message) + """ + url = self._get_api_url('/products') + success, data, error = self._make_request(url, method='POST', json=product_data) + + if success: + product_id = data.get('id', '') + return True, f'Товар отправлен: ID={product_id}' + else: + return False, f'Ошибка отправки: {error}' + + def update_stock(self, product_id: str, quantity: int) -> Tuple[bool, str]: + """ + Обновить остаток товара. + + Args: + product_id: ID товара + quantity: Количество + + Returns: + tuple: (success, message) + """ + url = self._get_api_url(f'/products/{product_id}/stock') + success, data, error = self._make_request( + url, method='PATCH', json={'quantity': quantity} + ) + + if success: + return True, f'Остаток обновлён: {quantity} шт.' + else: + return False, f'Ошибка обновления: {error}' + + def update_price(self, product_id: str, price: float) -> Tuple[bool, str]: + """ + Обновить цену товара. + + Args: + product_id: ID товара + price: Новая цена + + Returns: + tuple: (success, message) + """ + url = self._get_api_url(f'/products/{product_id}/price') + success, data, error = self._make_request( + url, method='PATCH', json={'price': price} + ) + + if success: + return True, f'Цена обновлена: {price} руб.' + else: + return False, f'Ошибка обновления: {error}' diff --git a/myproject/integrations/services/marketplaces/woocommerce.py b/myproject/integrations/services/marketplaces/woocommerce.py new file mode 100644 index 0000000..364f258 --- /dev/null +++ b/myproject/integrations/services/marketplaces/woocommerce.py @@ -0,0 +1,32 @@ +from typing import Tuple +from .base import MarketplaceService + + +class WooCommerceService(MarketplaceService): + """Сервис для работы с WooCommerce API""" + + def test_connection(self) -> Tuple[bool, str]: + """Проверить соединение с WooCommerce API""" + 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, 'Соединение успешно (заглушка)' + + def sync(self) -> Tuple[bool, str]: + """Выполнить синхронизацию с WooCommerce""" + # TODO: реализовать синхронизацию + return True, 'Синхронизация запущена (заглушка)' + + def fetch_orders(self, limit: int = 50): + """Получить заказы с WooCommerce""" + # TODO: реализовать + pass + + def push_products(self, products): + """Отправить товары на WooCommerce""" + # TODO: реализовать + pass diff --git a/myproject/system_settings/models/integration_config.py b/myproject/system_settings/models/integration_config.py index 6719064..5bc8aae 100644 --- a/myproject/system_settings/models/integration_config.py +++ b/myproject/system_settings/models/integration_config.py @@ -9,6 +9,7 @@ class IntegrationConfig(models.Model): INTEGRATION_CHOICES = [ ('woocommerce', 'WooCommerce'), + ('recommerce', 'Recommerce'), # Здесь добавлять новые интеграции: # ('shopify', 'Shopify'), # ('telegram', 'Telegram'),