feat(integrations): добавлена заготовка интеграции Recommerce
- Создана структура marketplaces/ для маркетплейсов - Модели: MarketplaceIntegration, WooCommerceIntegration, RecommerceIntegration - Сервисы: MarketplaceService, WooCommerceService, RecommerceService - RecommerceService содержит методы для работы с API: - test_connection(), sync(), fetch_products() - push_product(), update_stock(), update_price() - IntegrationConfig обновлён с новой интеграцией Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,44 @@
|
|||||||
from django.contrib import admin
|
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)
|
# @admin.register(WooCommerceIntegration)
|
||||||
# class WooCommerceIntegrationAdmin(admin.ModelAdmin):
|
# 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')}),
|
||||||
|
# )
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
]
|
||||||
|
|||||||
9
myproject/integrations/models/marketplaces/__init__.py
Normal file
9
myproject/integrations/models/marketplaces/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from .base import MarketplaceIntegration
|
||||||
|
from .woocommerce import WooCommerceIntegration
|
||||||
|
from .recommerce import RecommerceIntegration
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'MarketplaceIntegration',
|
||||||
|
'WooCommerceIntegration',
|
||||||
|
'RecommerceIntegration',
|
||||||
|
]
|
||||||
42
myproject/integrations/models/marketplaces/base.py
Normal file
42
myproject/integrations/models/marketplaces/base.py
Normal file
@@ -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 = "Интеграции с маркетплейсами"
|
||||||
59
myproject/integrations/models/marketplaces/recommerce.py
Normal file
59
myproject/integrations/models/marketplaces/recommerce.py
Normal file
@@ -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)
|
||||||
40
myproject/integrations/models/marketplaces/woocommerce.py
Normal file
40
myproject/integrations/models/marketplaces/woocommerce.py
Normal file
@@ -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)
|
||||||
@@ -1,3 +1,13 @@
|
|||||||
from .base import BaseIntegrationService
|
from .base import BaseIntegrationService
|
||||||
|
from .marketplaces import (
|
||||||
|
MarketplaceService,
|
||||||
|
WooCommerceService,
|
||||||
|
RecommerceService,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = ['BaseIntegrationService']
|
__all__ = [
|
||||||
|
'BaseIntegrationService',
|
||||||
|
'MarketplaceService',
|
||||||
|
'WooCommerceService',
|
||||||
|
'RecommerceService',
|
||||||
|
]
|
||||||
|
|||||||
9
myproject/integrations/services/marketplaces/__init__.py
Normal file
9
myproject/integrations/services/marketplaces/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from .base import MarketplaceService
|
||||||
|
from .woocommerce import WooCommerceService
|
||||||
|
from .recommerce import RecommerceService
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'MarketplaceService',
|
||||||
|
'WooCommerceService',
|
||||||
|
'RecommerceService',
|
||||||
|
]
|
||||||
40
myproject/integrations/services/marketplaces/base.py
Normal file
40
myproject/integrations/services/marketplaces/base.py
Normal file
@@ -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)
|
||||||
142
myproject/integrations/services/marketplaces/recommerce.py
Normal file
142
myproject/integrations/services/marketplaces/recommerce.py
Normal file
@@ -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}'
|
||||||
32
myproject/integrations/services/marketplaces/woocommerce.py
Normal file
32
myproject/integrations/services/marketplaces/woocommerce.py
Normal file
@@ -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
|
||||||
@@ -9,6 +9,7 @@ class IntegrationConfig(models.Model):
|
|||||||
|
|
||||||
INTEGRATION_CHOICES = [
|
INTEGRATION_CHOICES = [
|
||||||
('woocommerce', 'WooCommerce'),
|
('woocommerce', 'WooCommerce'),
|
||||||
|
('recommerce', 'Recommerce'),
|
||||||
# Здесь добавлять новые интеграции:
|
# Здесь добавлять новые интеграции:
|
||||||
# ('shopify', 'Shopify'),
|
# ('shopify', 'Shopify'),
|
||||||
# ('telegram', 'Telegram'),
|
# ('telegram', 'Telegram'),
|
||||||
|
|||||||
Reference in New Issue
Block a user