159 lines
5.5 KiB
Python
159 lines
5.5 KiB
Python
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
|