2026-08-18 18:48:00 +03:00

277 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}"