import uuid from decimal import Decimal from django.core.validators import MinValueValidator from django.db import models class SaleEventType(models.TextChoices): SALE = "sale", "Продажа" RETURN = "return", "Возврат" class Sale(models.Model): """Normalized sale/order event supplied by an external or internal system.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) project = models.ForeignKey( "commerce.ExternalProject", null=True, blank=True, on_delete=models.PROTECT ) source = models.ForeignKey( "commerce.SourceSystem", on_delete=models.PROTECT, related_name="sales", verbose_name="Источник", ) source_record = models.ForeignKey( "commerce.SourceRecord", on_delete=models.SET_NULL, null=True, blank=True, related_name="sales", verbose_name="Исходная запись", ) external_id = models.CharField("Внешний ID", max_length=255) event_type = models.CharField( "Тип операции", max_length=16, choices=SaleEventType.choices, default=SaleEventType.SALE, ) number = models.CharField("Номер", max_length=128, blank=True) channel = models.CharField("Канал продаж", max_length=64, blank=True, db_index=True) customer_external_id = models.CharField( "ID клиента в источнике", max_length=255, blank=True ) customer_name = models.CharField("Клиент", max_length=255, blank=True) status = models.CharField("Статус источника", max_length=128, blank=True) counts_as_sale = models.BooleanField( "Учитывать в продажах", default=True, help_text="Отключается для отменённых или тестовых заказов.", ) currency = models.CharField("Валюта", max_length=3, default="RUB") occurred_at = models.DateTimeField("Дата операции", db_index=True) source_updated_at = models.DateTimeField( "Изменено в источнике", null=True, 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 = ["-occurred_at", "-created_at"] constraints = [ models.UniqueConstraint( fields=["source", "external_id"], name="commerce_sale_source_external", ) ] indexes = [ models.Index( fields=["counts_as_sale", "occurred_at"], name="commerce_sale_counted_date", ) ] def __str__(self): return self.number or f"{self.source.code}:{self.external_id}" class SaleLine(models.Model): """Product-level monetary fact within a sale.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) sale = models.ForeignKey( Sale, on_delete=models.CASCADE, related_name="lines", verbose_name="Продажа", ) product = models.ForeignKey( "commerce.Product", on_delete=models.PROTECT, null=True, blank=True, related_name="sale_lines", verbose_name="Товар", ) external_id = models.CharField("Внешний ID позиции", max_length=255) external_product_id = models.CharField( "Внешний ID товара", max_length=255, blank=True ) external_sku = models.CharField("Внешний артикул", max_length=128, blank=True) name = models.CharField("Название в источнике", max_length=255, blank=True) quantity = models.DecimalField( "Количество", max_digits=18, decimal_places=4, default=Decimal("1"), validators=[MinValueValidator(Decimal("0"))], ) gross_amount = models.DecimalField( "Сумма до скидки", max_digits=18, decimal_places=2, default=Decimal("0"), validators=[MinValueValidator(Decimal("0"))], ) discount_amount = models.DecimalField( "Скидка", max_digits=18, decimal_places=2, default=Decimal("0"), validators=[MinValueValidator(Decimal("0"))], ) cost_amount = models.DecimalField( "Себестоимость", max_digits=18, decimal_places=2, default=Decimal("0"), validators=[MinValueValidator(Decimal("0"))], ) metadata = models.JSONField("Дополнительные данные", default=dict, blank=True) class Meta: verbose_name = "Позиция продажи" verbose_name_plural = "Позиции продаж" constraints = [ models.UniqueConstraint( fields=["sale", "external_id"], name="commerce_sale_line_identity", ), models.CheckConstraint( condition=models.Q(discount_amount__lte=models.F("gross_amount")), name="commerce_sale_line_discount_lte_gross", ), ] indexes = [ models.Index(fields=["product", "sale"], name="commerce_line_product_sale") ] @property def net_amount(self): return self.gross_amount - self.discount_amount def __str__(self): return self.name or self.external_sku or self.external_id