import uuid from django.conf import settings from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models class SourceSystem(models.Model): """External system that supplies observations and business events.""" code = models.SlugField("Код", max_length=64, unique=True) name = models.CharField("Название", max_length=128) is_active = models.BooleanField("Активен", default=True) metadata = models.JSONField("Настройки без секретов", default=dict, blank=True) created_at = models.DateTimeField("Создано", auto_now_add=True) updated_at = models.DateTimeField("Обновлено", auto_now=True) class Meta: verbose_name = "Источник данных" verbose_name_plural = "Источники данных" ordering = ["name"] def __str__(self): return self.name class SourceRecord(models.Model): """Latest source payload retained with provenance for repeatable imports.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) source = models.ForeignKey( SourceSystem, on_delete=models.PROTECT, related_name="records", verbose_name="Источник", ) entity_type = models.CharField("Тип объекта", max_length=64) external_id = models.CharField("Внешний ID", max_length=255) payload = models.JSONField("Исходные данные", default=dict) payload_hash = models.CharField("Хеш данных", max_length=64, editable=False) source_updated_at = models.DateTimeField( "Изменено в источнике", null=True, blank=True ) received_at = models.DateTimeField("Получено", auto_now=True) created_at = models.DateTimeField("Создано", auto_now_add=True) class Meta: verbose_name = "Исходная запись" verbose_name_plural = "Исходные записи" constraints = [ models.UniqueConstraint( fields=["source", "entity_type", "external_id"], name="commerce_source_record_identity", ) ] indexes = [ models.Index( fields=["source", "entity_type", "source_updated_at"], name="commerce_source_record_sync", ) ] def __str__(self): return f"{self.source.code}:{self.entity_type}:{self.external_id}" class ProductBinding(models.Model): """Maps an external product/offer identifier to the canonical product.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) product = models.ForeignKey( "commerce.Product", on_delete=models.CASCADE, related_name="bindings", verbose_name="Товар", ) source = models.ForeignKey( SourceSystem, on_delete=models.PROTECT, related_name="product_bindings", verbose_name="Источник", ) external_id = models.CharField("Внешний ID", max_length=255) external_sku = models.CharField( "Артикул в источнике", max_length=128, blank=True, db_index=True ) metadata = models.JSONField( "Нормализованные данные источника", default=dict, blank=True, ) is_primary = models.BooleanField("Основная привязка", default=False) created_at = models.DateTimeField("Создано", auto_now_add=True) updated_at = models.DateTimeField("Обновлено", auto_now=True) class Meta: verbose_name = "Привязка товара" verbose_name_plural = "Привязки товаров" constraints = [ models.UniqueConstraint( fields=["source", "external_id"], name="commerce_binding_source_external", ), ] indexes = [ models.Index( fields=["source", "external_sku"], name="commerce_binding_source_sku", ) ] def __str__(self): return f"{self.product.sku} ← {self.source.code}:{self.external_id}" class IntegrationProvider(models.TextChoices): MOYSKLAD = "moysklad", "МойСклад" WILDBERRIES = "wildberries", "Wildberries" class IntegrationStatus(models.TextChoices): DISCONNECTED = "disconnected", "Не подключено" READY = "ready", "Подключено" QUEUED = "queued", "В очереди" SYNCING = "syncing", "Синхронизация" ERROR = "error", "Ошибка" class IntegrationSyncStage(models.TextChoices): CATALOG = "catalog", "Каталог" IMAGES = "images", "Изображения" VARIANT_LINKS = "variant_links", "Связи вариантов" STOCKS = "stocks", "Остатки" SALES = "sales", "Продажи" RETURNS = "returns", "Возвраты" FINALIZING = "finalizing", "Завершение" class IntegrationConnection(models.Model): """Encrypted credentials and synchronization state for an external source.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) source = models.OneToOneField( SourceSystem, on_delete=models.PROTECT, related_name="connection", verbose_name="Источник", ) provider = models.CharField( "Провайдер", max_length=32, choices=IntegrationProvider.choices, unique=True, ) auth_type = models.CharField( "Способ авторизации", max_length=16, choices=[("token", "Токен"), ("basic", "Логин и пароль")], default="token", ) encrypted_credentials = models.TextField("Зашифрованные реквизиты") status = models.CharField( "Статус", max_length=16, choices=IntegrationStatus.choices, default=IntegrationStatus.DISCONNECTED, ) account_name = models.CharField("Учётная запись", max_length=255, blank=True) last_error = models.TextField("Последняя ошибка", blank=True) last_tested_at = models.DateTimeField("Доступ проверен", null=True, blank=True) last_sync_started_at = models.DateTimeField( "Синхронизация начата", null=True, blank=True ) last_sync_finished_at = models.DateTimeField( "Синхронизация завершена", null=True, blank=True ) last_stats = models.JSONField("Результат синхронизации", default=dict, blank=True) task_id = models.CharField("ID фоновой задачи", max_length=255, blank=True) auto_sync = models.BooleanField("Автоматическая синхронизация", default=True) sync_interval_minutes = models.PositiveIntegerField( "Интервал синхронизации, минут", default=15, validators=[MinValueValidator(5), MaxValueValidator(1440)], ) next_sync_at = models.DateTimeField( "Следующая синхронизация", null=True, blank=True, db_index=True ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="commerce_connections_created", verbose_name="Создал", ) created_at = models.DateTimeField("Создано", auto_now_add=True) updated_at = models.DateTimeField("Обновлено", auto_now=True) class Meta: verbose_name = "Подключение интеграции" verbose_name_plural = "Подключения интеграций" def __str__(self): return f"{self.get_provider_display()} · {self.get_status_display()}" class IntegrationSyncRun(models.Model): """Persistent progress and result of one full connector scan.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) connection = models.ForeignKey( IntegrationConnection, on_delete=models.CASCADE, related_name="sync_runs", verbose_name="Подключение", ) status = models.CharField( "Статус", max_length=16, choices=[ ("queued", "В очереди"), ("running", "Выполняется"), ("success", "Завершено"), ("error", "Ошибка"), ], default="queued", ) task_id = models.CharField("ID фоновой задачи", max_length=255, blank=True) stage = models.CharField( "Этап", max_length=32, choices=IntegrationSyncStage.choices, blank=True, default="", ) stage_processed = models.PositiveIntegerField("Обработано на этапе", default=0) stage_total = models.PositiveIntegerField("Всего на этапе", default=0) last_progress_at = models.DateTimeField( "Последний прогресс", null=True, blank=True, ) processed = models.PositiveIntegerField("Обработано", default=0) created_products = models.PositiveIntegerField("Создано товаров", default=0) matched_products = models.PositiveIntegerField("Сопоставлено", default=0) stock_snapshots = models.PositiveIntegerField("Снимков остатков", default=0) images = models.PositiveIntegerField("Изображений", default=0) image_errors = models.PositiveIntegerField("Ошибок изображений", default=0) sales = models.PositiveIntegerField("Продаж и возвратов", default=0) costs = models.PositiveIntegerField("Расходов", default=0) error = models.TextField("Ошибка", blank=True) started_at = models.DateTimeField("Начато", null=True, blank=True) finished_at = models.DateTimeField("Завершено", null=True, blank=True) created_at = models.DateTimeField("Создано", auto_now_add=True) class Meta: verbose_name = "Запуск синхронизации" verbose_name_plural = "Запуски синхронизации" ordering = ["-created_at"] indexes = [ models.Index( fields=["connection", "-created_at"], name="commerce_sync_connection_date", ), models.Index( fields=["status", "created_at"], name="commerce_sync_status_created", ), models.Index( fields=["status", "last_progress_at"], name="commerce_sync_status_progress", ), ] def __str__(self): return f"{self.connection.provider} · {self.status} · {self.created_at}"