diff --git a/myproject/integrations/__init__.py b/myproject/integrations/__init__.py new file mode 100644 index 0000000..954aa37 --- /dev/null +++ b/myproject/integrations/__init__.py @@ -0,0 +1 @@ +default_app_config = 'integrations.apps.IntegrationsConfig' diff --git a/myproject/integrations/admin.py b/myproject/integrations/admin.py new file mode 100644 index 0000000..19da2d6 --- /dev/null +++ b/myproject/integrations/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin + + +# Регистрация конкретных интеграций будет здесь +# @admin.register(WooCommerceIntegration) +# class WooCommerceIntegrationAdmin(admin.ModelAdmin): +# pass diff --git a/myproject/integrations/apps.py b/myproject/integrations/apps.py new file mode 100644 index 0000000..261a572 --- /dev/null +++ b/myproject/integrations/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class IntegrationsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'integrations' + verbose_name = 'Интеграции' diff --git a/myproject/integrations/models/__init__.py b/myproject/integrations/models/__init__.py new file mode 100644 index 0000000..6b326eb --- /dev/null +++ b/myproject/integrations/models/__init__.py @@ -0,0 +1,3 @@ +from .base import BaseIntegration + +__all__ = ['BaseIntegration'] diff --git a/myproject/integrations/models/base.py b/myproject/integrations/models/base.py new file mode 100644 index 0000000..c7e9226 --- /dev/null +++ b/myproject/integrations/models/base.py @@ -0,0 +1,93 @@ +from django.db import models +from django.core.exceptions import ValidationError +from abc import ABC, abstractmethod + + +class IntegrationType(models.TextChoices): + MARKETPLACE = 'marketplace', 'Маркетплейс' + PAYMENT = 'payment', 'Платёжная система' + SHIPPING = 'shipping', 'Служба доставки' + + +class BaseIntegration(models.Model): + """ + Абстрактный базовый класс для всех интеграций. + Содержит общие поля и логику валидации. + """ + + integration_type = models.CharField( + max_length=20, + choices=IntegrationType.choices, + editable=False, + verbose_name="Тип интеграции" + ) + + is_active = models.BooleanField( + default=True, + verbose_name="Активна", + db_index=True, + help_text="Интеграция используется только если включена здесь и в системных настройках" + ) + + name = models.CharField( + max_length=100, + verbose_name="Название", + help_text="Произвольное название для удобства" + ) + + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата создания" + ) + + updated_at = models.DateTimeField( + auto_now=True, + verbose_name="Дата обновления" + ) + + # Общие credential поля (можно переопределить в наследниках) + api_key = models.CharField( + max_length=255, + blank=True, + verbose_name="API ключ", + help_text="Будет зашифрован при сохранении" + ) + + api_secret = models.CharField( + max_length=255, + blank=True, + verbose_name="API секрет", + help_text="Будет зашифрован при сохранении" + ) + + # Дополнительные настройки в JSON для гибкости + extra_config = models.JSONField( + default=dict, + blank=True, + verbose_name="Доп. настройки" + ) + + class Meta: + abstract = True + ordering = ['-is_active', 'name'] + indexes = [ + models.Index(fields=['is_active', 'integration_type']), + ] + + def __str__(self): + return f"{self.get_integration_type_display()}: {self.name}" + + @property + def is_configured(self) -> bool: + """ + Проверить, есть ли необходимые credentials. + Можно переопределить в наследниках. + """ + return bool(self.api_key) + + def clean(self): + """Валидацияcredentials""" + if self.is_active and not self.is_configured: + raise ValidationError({ + 'api_key': 'API ключ обязателен для активной интеграции' + }) diff --git a/myproject/integrations/services/__init__.py b/myproject/integrations/services/__init__.py new file mode 100644 index 0000000..a0b243a --- /dev/null +++ b/myproject/integrations/services/__init__.py @@ -0,0 +1,3 @@ +from .base import BaseIntegrationService + +__all__ = ['BaseIntegrationService'] diff --git a/myproject/integrations/services/base.py b/myproject/integrations/services/base.py new file mode 100644 index 0000000..ab7c475 --- /dev/null +++ b/myproject/integrations/services/base.py @@ -0,0 +1,48 @@ +from abc import ABC, abstractmethod +from typing import Tuple + + +class BaseIntegrationService(ABC): + """ + Базовый класс для всех интеграционных сервисов. + Определяет общий интерфейс для работы с внешними API. + """ + + def __init__(self, config): + """ + Args: + config: Экземпляр модели интеграции (наследник BaseIntegration) + """ + self.config = config + + @abstractmethod + def test_connection(self) -> Tuple[bool, str]: + """ + Проверить соединение с внешним API. + + Returns: + tuple: (success: bool, message: str) + """ + pass + + @abstractmethod + def sync(self) -> Tuple[bool, str]: + """ + Выполнить основную операцию синхронизации. + + Returns: + tuple: (success: bool, message: str) + """ + pass + + def is_available(self) -> bool: + """ + Проверить, готова ли интеграция к использованию. + + Returns: + bool: True если интеграция активна и настроена + """ + return ( + self.config.is_active and + self.config.is_configured + ) diff --git a/myproject/integrations/urls.py b/myproject/integrations/urls.py new file mode 100644 index 0000000..32c1d9f --- /dev/null +++ b/myproject/integrations/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from .views import IntegrationsListView + +app_name = 'integrations' + +urlpatterns = [ + path("", IntegrationsListView.as_view(), name="list"), + # Здесь будут добавляться endpoint'ы для интеграций + # path("test//", views.test_connection, name="test_connection"), + # path("webhook//", views.webhook, name="webhook"), +] diff --git a/myproject/integrations/views.py b/myproject/integrations/views.py new file mode 100644 index 0000000..99b1979 --- /dev/null +++ b/myproject/integrations/views.py @@ -0,0 +1,13 @@ +from django.views.generic import TemplateView +from user_roles.mixins import OwnerRequiredMixin + + +class IntegrationsListView(OwnerRequiredMixin, TemplateView): + """Страница настроек интеграций (доступна только владельцу)""" + template_name = "system_settings/integrations.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + from system_settings.models import IntegrationConfig + context['integration_choices'] = IntegrationConfig.INTEGRATION_CHOICES + return context diff --git a/myproject/myproject/settings.py b/myproject/myproject/settings.py index a44f70a..9efaa12 100644 --- a/myproject/myproject/settings.py +++ b/myproject/myproject/settings.py @@ -93,6 +93,7 @@ TENANT_APPS = [ 'pos', # POS Terminal 'discounts', # Скидки и промокоды 'system_settings', # Системные настройки компании (только для владельца) + 'integrations', # Интеграции с внешними сервисами # TODO: 'simple_history' - вернуть позже для истории изменений ] diff --git a/myproject/system_settings/migrations/0001_initial.py b/myproject/system_settings/migrations/0001_initial.py new file mode 100644 index 0000000..d00ea2b --- /dev/null +++ b/myproject/system_settings/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.10 on 2026-01-11 19:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='IntegrationConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('integration_id', models.CharField(choices=[('woocommerce', 'WooCommerce')], max_length=50, unique=True, verbose_name='Интеграция')), + ('is_enabled', models.BooleanField(default=False, help_text='Глобальное включение интеграции для тенанта', verbose_name='Включена')), + ('last_sync_at', models.DateTimeField(blank=True, null=True, verbose_name='Последняя синхронизация')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')), + ], + options={ + 'verbose_name': 'Настройка интеграции', + 'verbose_name_plural': 'Настройки интеграций', + 'ordering': ['integration_id'], + }, + ), + ] diff --git a/myproject/system_settings/models/__init__.py b/myproject/system_settings/models/__init__.py new file mode 100644 index 0000000..e8b18d6 --- /dev/null +++ b/myproject/system_settings/models/__init__.py @@ -0,0 +1,3 @@ +from .integration_config import IntegrationConfig + +__all__ = ['IntegrationConfig'] diff --git a/myproject/system_settings/models/integration_config.py b/myproject/system_settings/models/integration_config.py new file mode 100644 index 0000000..6719064 --- /dev/null +++ b/myproject/system_settings/models/integration_config.py @@ -0,0 +1,53 @@ +from django.db import models + + +class IntegrationConfig(models.Model): + """ + Глобальные тумблеры для включения/выключения интеграций. + Одна запись на доступную интеграцию. + """ + + INTEGRATION_CHOICES = [ + ('woocommerce', 'WooCommerce'), + # Здесь добавлять новые интеграции: + # ('shopify', 'Shopify'), + # ('telegram', 'Telegram'), + ] + + integration_id = models.CharField( + max_length=50, + choices=INTEGRATION_CHOICES, + unique=True, + verbose_name="Интеграция" + ) + + is_enabled = models.BooleanField( + default=False, + verbose_name="Включена", + help_text="Глобальное включение интеграции для тенанта" + ) + + last_sync_at = models.DateTimeField( + null=True, + blank=True, + verbose_name="Последняя синхронизация" + ) + + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата создания" + ) + + updated_at = models.DateTimeField( + auto_now=True, + verbose_name="Дата обновления" + ) + + class Meta: + verbose_name = "Настройка интеграции" + verbose_name_plural = "Настройки интеграций" + ordering = ['integration_id'] + + def __str__(self): + status = "вкл" if self.is_enabled else "выкл" + return f"{self.get_integration_id_display()}: {status}" diff --git a/myproject/system_settings/templates/system_settings/base_settings.html b/myproject/system_settings/templates/system_settings/base_settings.html index a270614..62392a2 100644 --- a/myproject/system_settings/templates/system_settings/base_settings.html +++ b/myproject/system_settings/templates/system_settings/base_settings.html @@ -25,7 +25,12 @@ Скидки - + diff --git a/myproject/system_settings/templates/system_settings/integrations.html b/myproject/system_settings/templates/system_settings/integrations.html new file mode 100644 index 0000000..87dcdff --- /dev/null +++ b/myproject/system_settings/templates/system_settings/integrations.html @@ -0,0 +1,66 @@ +{% extends "system_settings/base_settings.html" %} + +{% block title %}Интеграции{% endblock %} + +{% block settings_content %} +
+ +
+
+
+
Доступные интеграции
+
+
+ {% for value, label in integration_choices %} +
+
+ {{ label }} + Маркетплейс +
+
+ + +
+
+ {% endfor %} +
+
+
+ + +
+
+
+
Настройки интеграции
+
+
+
+ + + +

Выберите интеграцию слева для настройки

+ Здесь появится форма с настройками выбранной интеграции +
+
+
+
+
+ + + +{% endblock %} diff --git a/myproject/system_settings/urls.py b/myproject/system_settings/urls.py index e20a705..c197b0a 100644 --- a/myproject/system_settings/urls.py +++ b/myproject/system_settings/urls.py @@ -8,4 +8,5 @@ urlpatterns = [ path("", SystemSettingsView.as_view(), name="settings"), path("roles/", include('user_roles.urls', namespace='user_roles')), path("discounts/", include('discounts.urls', namespace='discounts')), + path("integrations/", include('integrations.urls', namespace='integrations')), ]