import uuid from pathlib import Path from django.db import models from django.db.models import Q from django.db.models.functions import Lower def product_image_upload_to(instance, filename): return f"commerce/products/{instance.product_id}/original/{Path(filename).name}" def product_thumbnail_upload_to(instance, filename): return f"commerce/products/{instance.product_id}/thumbnails/{Path(filename).name}" class ProductKind(models.TextChoices): PRODUCT = "product", "Товар" SERVICE = "service", "Услуга" BUNDLE = "bundle", "Комплект" class Product(models.Model): """Canonical sellable or stockable SKU used by every data source.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) parent = models.ForeignKey( "self", on_delete=models.PROTECT, null=True, blank=True, related_name="variants", verbose_name="Родительский товар", ) kind = models.CharField( "Тип", max_length=20, choices=ProductKind.choices, default=ProductKind.PRODUCT, ) sku = models.CharField("Внутренний артикул", max_length=128, unique=True) name = models.CharField("Название", max_length=255) barcode = models.CharField("Штрихкод", max_length=64, blank=True, db_index=True) description = models.TextField("Описание", blank=True) is_active = models.BooleanField("Активен", default=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", "sku"] constraints = [ models.CheckConstraint( condition=~Q(parent=models.F("id")), name="commerce_product_not_own_parent", ), models.UniqueConstraint( Lower("sku"), name="commerce_product_sku_case_insensitive" ), ] indexes = [ models.Index( fields=["is_active", "name"], name="commerce_product_active_name" ), ] def __str__(self): return f"{self.sku} — {self.name}" class ProductImage(models.Model): """Locally stored image imported from an external product source.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) product = models.ForeignKey( Product, on_delete=models.CASCADE, related_name="images", verbose_name="Товар", ) source = models.ForeignKey( "commerce.SourceSystem", on_delete=models.CASCADE, related_name="product_images", verbose_name="Источник", ) external_id = models.CharField("Внешний ID", max_length=255) file = models.ImageField( "Оригинал", upload_to=product_image_upload_to, max_length=500, ) thumbnail = models.ImageField( "Миниатюра", upload_to=product_thumbnail_upload_to, max_length=500, blank=True, ) filename = models.CharField("Имя файла", max_length=255, blank=True) title = models.CharField("Название", max_length=255, blank=True) source_size = models.PositiveBigIntegerField("Размер в источнике", default=0) source_updated_at = models.DateTimeField( "Изменено в источнике", null=True, blank=True, ) position = models.PositiveIntegerField("Позиция", default=0) 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 = "Изображения товаров" ordering = ["position", "created_at"] constraints = [ models.UniqueConstraint( fields=["product", "source", "external_id"], name="commerce_product_image_identity", ) ] indexes = [ models.Index( fields=["product", "is_primary", "position"], name="commerce_product_image_order", ) ] def __str__(self): return self.title or self.filename or self.external_id