Добавлены поля original_sales_unit и conversion_factor в KitItemSnapshot для хранения единиц продажи и коэффициентов конверсии на момент создания снимка. Обновлена логика резервирования запасов для корректного расчета количества в базовых единицах. Изменения в шаблоне редактирования комплектов для сохранения выбранных единиц продажи при обновлении списка опций. BREAKING CHANGE: Изменена структура данных в KitItemSnapshot, требуется миграция базы данных.
183 lines
6.2 KiB
Python
183 lines
6.2 KiB
Python
"""
|
||
Снапшоты комплектов для сохранения истории заказов.
|
||
|
||
При добавлении комплекта (ProductKit) в заказ создается снимок его состояния,
|
||
чтобы изменения в комплекте не влияли на историю заказов.
|
||
"""
|
||
from django.db import models
|
||
from decimal import Decimal
|
||
|
||
|
||
class KitSnapshot(models.Model):
|
||
"""
|
||
Снимок комплекта на момент заказа.
|
||
Сохраняет название, цены и состав комплекта.
|
||
"""
|
||
|
||
# Связь с оригинальным комплектом (для аналитики)
|
||
original_kit = models.ForeignKey(
|
||
'products.ProductKit',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='snapshots',
|
||
verbose_name="Оригинальный комплект",
|
||
help_text="Ссылка на комплект, с которого создан снимок"
|
||
)
|
||
|
||
# Копия основных данных комплекта
|
||
name = models.CharField(max_length=200, verbose_name="Название")
|
||
sku = models.CharField(max_length=100, blank=True, verbose_name="Артикул")
|
||
description = models.TextField(blank=True, verbose_name="Описание")
|
||
|
||
# Цены на момент заказа
|
||
base_price = models.DecimalField(
|
||
max_digits=10,
|
||
decimal_places=2,
|
||
default=Decimal('0'),
|
||
verbose_name="Базовая цена"
|
||
)
|
||
price = models.DecimalField(
|
||
max_digits=10,
|
||
decimal_places=2,
|
||
default=Decimal('0'),
|
||
verbose_name="Итоговая цена"
|
||
)
|
||
sale_price = models.DecimalField(
|
||
max_digits=10,
|
||
decimal_places=2,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Цена со скидкой"
|
||
)
|
||
|
||
# Корректировки цены
|
||
price_adjustment_type = models.CharField(
|
||
max_length=20,
|
||
default='none',
|
||
verbose_name="Тип корректировки"
|
||
)
|
||
price_adjustment_value = models.DecimalField(
|
||
max_digits=10,
|
||
decimal_places=2,
|
||
default=Decimal('0'),
|
||
verbose_name="Значение корректировки"
|
||
)
|
||
|
||
is_temporary = models.BooleanField(default=False, verbose_name="Временный комплект")
|
||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||
|
||
class Meta:
|
||
verbose_name = "Снимок комплекта"
|
||
verbose_name_plural = "Снимки комплектов"
|
||
ordering = ['-created_at']
|
||
indexes = [
|
||
models.Index(fields=['original_kit']),
|
||
models.Index(fields=['created_at']),
|
||
]
|
||
|
||
def __str__(self):
|
||
date_str = self.created_at.strftime('%d.%m.%Y %H:%M') if self.created_at else ''
|
||
return f"Снимок: {self.name} ({date_str})"
|
||
|
||
@property
|
||
def actual_price(self):
|
||
"""Финальная цена (sale_price или price)"""
|
||
if self.sale_price:
|
||
return self.sale_price
|
||
return self.price
|
||
|
||
def get_total_components_count(self):
|
||
"""Количество компонентов в комплекте"""
|
||
return self.items.count()
|
||
|
||
|
||
class KitItemSnapshot(models.Model):
|
||
"""
|
||
Снимок компонента комплекта.
|
||
Сохраняет информацию о товаре и его количестве в комплекте.
|
||
"""
|
||
kit_snapshot = models.ForeignKey(
|
||
KitSnapshot,
|
||
on_delete=models.CASCADE,
|
||
related_name='items',
|
||
verbose_name="Снимок комплекта"
|
||
)
|
||
|
||
# Ссылка на оригинальный товар (для резервирования)
|
||
original_product = models.ForeignKey(
|
||
'products.Product',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='kit_item_snapshots',
|
||
verbose_name="Оригинальный товар",
|
||
help_text="Ссылка на товар для резервирования на складе"
|
||
)
|
||
|
||
# Данные о товаре
|
||
product_name = models.CharField(
|
||
max_length=200,
|
||
blank=True,
|
||
verbose_name="Название товара"
|
||
)
|
||
product_sku = models.CharField(
|
||
max_length=100,
|
||
blank=True,
|
||
verbose_name="Артикул товара"
|
||
)
|
||
product_price = models.DecimalField(
|
||
max_digits=10,
|
||
decimal_places=2,
|
||
default=Decimal('0'),
|
||
verbose_name="Цена товара"
|
||
)
|
||
|
||
# Если был выбран из группы вариантов
|
||
variant_group_name = models.CharField(
|
||
max_length=200,
|
||
blank=True,
|
||
verbose_name="Группа вариантов"
|
||
)
|
||
|
||
original_sales_unit = models.ForeignKey(
|
||
'products.ProductSalesUnit',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='kit_item_snapshots',
|
||
verbose_name="Единица продажи",
|
||
help_text="Единица продажи на момент создания снимка"
|
||
)
|
||
|
||
conversion_factor = models.DecimalField(
|
||
max_digits=15,
|
||
decimal_places=6,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Коэффициент конверсии",
|
||
help_text="Сколько единиц продажи в 1 базовой единице товара"
|
||
)
|
||
|
||
quantity = models.DecimalField(
|
||
max_digits=10,
|
||
decimal_places=3,
|
||
verbose_name="Количество"
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = "Снимок компонента"
|
||
verbose_name_plural = "Снимки компонентов"
|
||
indexes = [
|
||
models.Index(fields=['kit_snapshot']),
|
||
]
|
||
|
||
def __str__(self):
|
||
name = self.product_name or self.variant_group_name or "Неизвестный товар"
|
||
return f"{name} x{self.quantity}"
|
||
|
||
@property
|
||
def total_price(self):
|
||
"""Стоимость компонента (цена * количество)"""
|
||
return self.product_price * self.quantity
|