Files
octopus/myproject/orders/models/order_item.py

191 lines
6.8 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.
from django.db import models
from django.core.exceptions import ValidationError
from products.models import Product, ProductKit
from simple_history.models import HistoricalRecords
from .order import Order
from .kit_snapshot import KitSnapshot
class OrderItem(models.Model):
"""
Позиция (товар) в заказе.
Хранит информацию о товаре или комплекте, количестве и цене на момент заказа.
"""
order = models.ForeignKey(
Order,
on_delete=models.CASCADE,
related_name='items',
verbose_name="Заказ"
)
# Товар или комплект (один из двух должен быть заполнен)
product = models.ForeignKey(
Product,
on_delete=models.PROTECT,
null=True,
blank=True,
related_name='order_items',
verbose_name="Товар"
)
product_kit = models.ForeignKey(
ProductKit,
on_delete=models.PROTECT,
null=True,
blank=True,
related_name='order_items',
verbose_name="Комплект товаров"
)
# Снимок комплекта (для сохранения истории состава комплекта на момент заказа)
kit_snapshot = models.ForeignKey(
KitSnapshot,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='order_items',
verbose_name="Снимок комплекта",
help_text="Хранит состав комплекта на момент оформления заказа"
)
# Снапшот названия и артикула (для Product и Kit)
item_name_snapshot = models.CharField(
max_length=200,
default='',
verbose_name="Название на момент заказа"
)
item_sku_snapshot = models.CharField(
max_length=100,
blank=True,
verbose_name="Артикул на момент заказа"
)
quantity = models.PositiveIntegerField(
default=1,
verbose_name="Количество"
)
price = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name="Цена за единицу",
help_text="Цена на момент создания заказа (фиксируется)"
)
is_custom_price = models.BooleanField(
default=False,
verbose_name="Цена изменена вручную",
help_text="True если цена была изменена вручную при создании заказа"
)
# Витринные продажи
is_from_showcase = models.BooleanField(
default=False,
verbose_name="С витрины",
help_text="True если товар продан с витрины"
)
showcase = models.ForeignKey(
'inventory.Showcase',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='order_items',
verbose_name="Витрина",
help_text="Витрина, с которой был продан товар"
)
# Временные метки
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name="Дата добавления"
)
# История изменений
history = HistoricalRecords()
class Meta:
verbose_name = "Позиция заказа"
verbose_name_plural = "Позиции заказа"
indexes = [
models.Index(fields=['order']),
models.Index(fields=['product']),
models.Index(fields=['product_kit']),
models.Index(fields=['is_from_showcase']),
models.Index(fields=['showcase']),
]
def __str__(self):
return f"{self.item_name_snapshot} x{self.quantity} в заказе #{self.order.order_number}"
def clean(self):
"""Валидация модели"""
super().clean()
# Проверка: должен быть заполнен либо product, либо product_kit
if not self.product and not self.product_kit:
raise ValidationError(
'Необходимо указать либо товар, либо комплект товаров'
)
# Проверка: не должны быть заполнены оба поля одновременно
if self.product and self.product_kit:
raise ValidationError(
'Нельзя указать одновременно и товар, и комплект'
)
def save(self, *args, **kwargs):
is_new = not self.pk
# Определяем, изменился ли комплект (для существующих записей)
kit_changed = False
if self.pk and self.product_kit:
old = OrderItem.objects.filter(pk=self.pk).values('product_kit_id').first()
if old and old['product_kit_id'] != self.product_kit_id:
kit_changed = True
# Создаем snapshot для комплекта (новый или при замене)
if self.product_kit and (is_new or kit_changed):
self.kit_snapshot = self.product_kit.create_snapshot()
# Сохраняем название и артикул
if self.product:
self.item_name_snapshot = self.product.name
self.item_sku_snapshot = self.product.sku or ''
elif self.kit_snapshot:
self.item_name_snapshot = self.kit_snapshot.name
self.item_sku_snapshot = self.kit_snapshot.sku or ''
# Автоматически фиксируем цену при создании, если она не указана
if not self.price:
if self.product:
self.price = self.product.actual_price
elif self.kit_snapshot:
self.price = self.kit_snapshot.actual_price
super().save(*args, **kwargs)
def get_total_price(self):
"""Возвращает общую стоимость позиции"""
return self.price * self.quantity
@property
def item_name(self):
"""Название из snapshot"""
return self.item_name_snapshot
@property
def original_price(self):
"""Оригинальная цена из snapshot"""
if self.kit_snapshot:
return self.kit_snapshot.actual_price
return self.price
@property
def price_difference(self):
"""Разница между установленной ценой и оригинальной"""
if self.is_custom_price and self.original_price:
return self.price - self.original_price
return None