from decimal import Decimal 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="Витрина, с которой был продан товар" ) # === ПОЛЯ ДЛЯ ЕДИНИЦ ПРОДАЖИ === sales_unit = models.ForeignKey( 'products.ProductSalesUnit', on_delete=models.SET_NULL, null=True, blank=True, related_name='order_items', verbose_name="Единица продажи" ) unit_name_snapshot = models.CharField( max_length=100, blank=True, default='', verbose_name="Название единицы (snapshot)", help_text="Название единицы продажи на момент заказа" ) conversion_factor_snapshot = models.DecimalField( max_digits=15, decimal_places=6, null=True, blank=True, verbose_name="Коэффициент конверсии (snapshot)", help_text="Коэффициент конверсии на момент заказа" ) quantity_in_base_units = models.DecimalField( max_digits=10, decimal_places=6, null=True, blank=True, 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.sales_unit: self.price = self.sales_unit.actual_price elif self.product: self.price = self.product.actual_price elif self.kit_snapshot: self.price = self.kit_snapshot.actual_price # Сохраняем snapshot единицы продажи if self.sales_unit: self.unit_name_snapshot = self.sales_unit.name self.conversion_factor_snapshot = self.sales_unit.conversion_factor self.quantity_in_base_units = self.sales_unit.convert_to_base( Decimal(self.quantity) ) 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