Files
octopus/myproject/orders/models/kit_snapshot.py
Andrey Smakotin ffc5f4cfc1 feat(inventory): учитывать коэффициент конверсии при резервировании компонентов комплектов
Добавлены поля original_sales_unit и conversion_factor в KitItemSnapshot для хранения
единиц продажи и коэффициентов конверсии на момент создания снимка. Обновлена логика
резервирования запасов для корректного расчета количества в базовых единицах.

Изменения в шаблоне редактирования комплектов для сохранения выбранных единиц продажи
при обновлении списка опций.

BREAKING CHANGE: Изменена структура данных в KitItemSnapshot, требуется миграция базы данных.
2026-01-21 11:05:00 +03:00

183 lines
6.2 KiB
Python
Raw Permalink 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.
"""
Снапшоты комплектов для сохранения истории заказов.
При добавлении комплекта (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