diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py
index 30ffe12..23f0d68 100644
--- a/myproject/inventory/signals.py
+++ b/myproject/inventory/signals.py
@@ -59,10 +59,13 @@ def reserve_stock_on_order_create(sender, instance, created, **kwargs):
Процесс:
1. Проверяем, новый ли заказ (создан только что)
- 2. Для каждого товара в заказе создаем Reservation
- 3. Статус резерва = 'reserved'
- 4. Проверяем на существующие резервы (защита от дубликатов)
+ 2. Для обычных товаров - создаём резерв напрямую
+ 3. Для комплектов - резервируем компоненты (группируя одинаковые товары)
+ 4. Статус резерва = 'reserved'
+ 5. Проверяем на существующие резервы (защита от дубликатов)
"""
+ from collections import defaultdict
+
if not created:
return # Только для новых заказов
@@ -76,33 +79,63 @@ def reserve_stock_on_order_create(sender, instance, created, **kwargs):
# Для каждого товара в заказе
for item in instance.items.all():
- # Определяем товар (может быть product или product_kit)
- product = item.product if item.product else item.product_kit
+ if item.product:
+ # Обычный товар - резервируем как раньше
+ _create_or_update_reservation(item, item.product, warehouse, Decimal(str(item.quantity)))
- if product:
- # ЗАЩИТА ОТ ДУБЛИКАТОВ: Проверяем, нет ли уже резерва для этой позиции
- existing_reservation = Reservation.objects.filter(
- order_item=item,
- product=product,
- warehouse=warehouse
- ).first()
+ elif item.product_kit and item.kit_snapshot:
+ # Комплект - резервируем КОМПОНЕНТЫ из снимка
+ # Группируем одинаковые товары для создания одного резерва
+ product_quantities = defaultdict(Decimal)
- if existing_reservation:
- # Резерв уже существует - обновляем его вместо создания нового
- existing_reservation.quantity = Decimal(str(item.quantity))
- existing_reservation.status = 'reserved'
- existing_reservation.save()
- else:
- # Резерва нет - создаем новый
- Reservation.objects.create(
- order_item=item,
- product=product,
- warehouse=warehouse,
- quantity=Decimal(str(item.quantity)),
- status='reserved'
+ for kit_item in item.kit_snapshot.items.select_related('original_product'):
+ if kit_item.original_product:
+ # Суммируем количество: qty компонента * qty комплектов в заказе
+ product_quantities[kit_item.original_product_id] += (
+ kit_item.quantity * Decimal(str(item.quantity))
+ )
+
+ # Создаём по одному резерву на каждый уникальный товар
+ from products.models import Product
+ for product_id, total_qty in product_quantities.items():
+ product = Product.objects.get(pk=product_id)
+ _create_or_update_reservation(
+ item, product, warehouse, total_qty, product_kit=item.product_kit
)
+def _create_or_update_reservation(order_item, product, warehouse, quantity, product_kit=None):
+ """
+ Вспомогательная функция для создания или обновления резерва.
+ """
+ # Формируем фильтр для поиска существующего резерва
+ filter_kwargs = {
+ 'order_item': order_item,
+ 'product': product,
+ 'warehouse': warehouse,
+ }
+ if product_kit:
+ filter_kwargs['product_kit'] = product_kit
+
+ existing_reservation = Reservation.objects.filter(**filter_kwargs).first()
+
+ if existing_reservation:
+ # Резерв уже существует - обновляем его
+ existing_reservation.quantity = quantity
+ existing_reservation.status = 'reserved'
+ existing_reservation.save()
+ else:
+ # Резерва нет - создаём новый
+ Reservation.objects.create(
+ order_item=order_item,
+ product=product,
+ product_kit=product_kit,
+ warehouse=warehouse,
+ quantity=quantity,
+ status='reserved'
+ )
+
+
@receiver(post_save, sender=Order)
@transaction.atomic
def create_sale_on_order_completion(sender, instance, created, **kwargs):
@@ -907,47 +940,95 @@ def update_reservation_on_item_change(sender, instance, created, **kwargs):
return # Для витринных комплектов не создаем новые резервы
# Обычный товар или постоянный комплект
- # Ищем резерв для этой позиции в ЛЮБОМ статусе (не только 'reserved')
+ # Ищем резервы для этой позиции в ЛЮБОМ статусе (не только 'reserved')
# КРИТИЧНО: Убран фильтр status='reserved' для предотвращения дубликатов
- reservation = Reservation.objects.filter(
- order_item=instance
- ).first()
+ reservations = Reservation.objects.filter(order_item=instance)
+
+ if reservations.exists():
+ if instance.product:
+ # Обычный товар - один резерв, обновляем количество напрямую
+ reservation = reservations.first()
+ old_quantity = reservation.quantity
+ reservation.quantity = Decimal(str(instance.quantity))
+ reservation.save(update_fields=['quantity'])
- if reservation:
- # Резерв существует - обновляем ТОЛЬКО количество
- # НЕ меняем статус! (может быть 'converted_to_sale', 'reserved', 'released')
- old_quantity = reservation.quantity
- reservation.quantity = Decimal(str(instance.quantity))
- reservation.save(update_fields=['quantity'])
-
- logger.info(
- f"✓ Резерв #{reservation.id} обновлён: quantity {old_quantity} → {reservation.quantity} "
- f"(статус: {reservation.status}, OrderItem #{instance.id}, заказ {instance.order.order_number})"
- )
- else:
- # Резерва нет - создаем новый ТОЛЬКО для обычных товаров (не комплектов)
- if not instance.product:
- # Это обычный комплект (не витринный) - не создаем резерв на уровне комплекта
logger.info(
- f"ℹ Обычный комплект '{instance.product_kit.name}': резервы управляются на уровне компонентов"
+ f"✓ Резерв #{reservation.id} обновлён: quantity {old_quantity} → {reservation.quantity} "
+ f"(статус: {reservation.status}, OrderItem #{instance.id}, заказ {instance.order.order_number})"
)
- return
-
- # Создаем резерв для обычного товара
+
+ elif instance.product_kit and instance.kit_snapshot:
+ # Комплект - несколько резервов (по одному на компонент)
+ # Обновляем количество каждого резерва пропорционально изменению количества комплектов
+ from collections import defaultdict
+
+ # Собираем ожидаемые количества компонентов
+ product_quantities = defaultdict(Decimal)
+ for kit_item in instance.kit_snapshot.items.select_related('original_product'):
+ if kit_item.original_product:
+ product_quantities[kit_item.original_product_id] = (
+ kit_item.quantity * Decimal(str(instance.quantity))
+ )
+
+ # Обновляем каждый резерв
+ for reservation in reservations:
+ expected_qty = product_quantities.get(reservation.product_id, Decimal('0'))
+ if expected_qty > 0:
+ old_quantity = reservation.quantity
+ reservation.quantity = expected_qty
+ reservation.save(update_fields=['quantity'])
+
+ logger.info(
+ f"✓ Резерв #{reservation.id} ({reservation.product.name}) обновлён: "
+ f"quantity {old_quantity} → {reservation.quantity} "
+ f"(статус: {reservation.status}, OrderItem #{instance.id})"
+ )
+ else:
+ # Резерва нет - создаем новый
+ from collections import defaultdict
+
warehouse = instance.order.pickup_warehouse or Warehouse.objects.filter(is_active=True).first()
- if warehouse:
- product = instance.product
+ if not warehouse:
+ logger.warning(f"⚠ Не найден склад для резервирования (OrderItem #{instance.id})")
+ return
+
+ if instance.product:
+ # Обычный товар - создаем один резерв
reservation = Reservation.objects.create(
order_item=instance,
- product=product,
+ product=instance.product,
warehouse=warehouse,
quantity=Decimal(str(instance.quantity)),
status='reserved'
)
-
+
logger.info(
- f"✓ Создан новый резерв #{reservation.id}: {product.name}, quantity={reservation.quantity} "
+ f"✓ Создан новый резерв #{reservation.id}: {instance.product.name}, quantity={reservation.quantity} "
+ f"(OrderItem #{instance.id}, заказ {instance.order.order_number})"
+ )
+
+ elif instance.product_kit and instance.kit_snapshot:
+ # Обычный комплект - резервируем КОМПОНЕНТЫ из снимка
+ # Группируем одинаковые товары
+ product_quantities = defaultdict(Decimal)
+
+ for kit_item in instance.kit_snapshot.items.select_related('original_product'):
+ if kit_item.original_product:
+ product_quantities[kit_item.original_product_id] += (
+ kit_item.quantity * Decimal(str(instance.quantity))
+ )
+
+ # Создаём резервы для компонентов
+ from products.models import Product
+ for product_id, total_qty in product_quantities.items():
+ product = Product.objects.get(pk=product_id)
+ _create_or_update_reservation(
+ instance, product, warehouse, total_qty, product_kit=instance.product_kit
+ )
+
+ logger.info(
+ f"✓ Комплект '{instance.product_kit.name}': создано {len(product_quantities)} резервов компонентов "
f"(OrderItem #{instance.id}, заказ {instance.order.order_number})"
)
diff --git a/myproject/orders/migrations/0007_kit_snapshots.py b/myproject/orders/migrations/0007_kit_snapshots.py
new file mode 100644
index 0000000..0376352
--- /dev/null
+++ b/myproject/orders/migrations/0007_kit_snapshots.py
@@ -0,0 +1,76 @@
+# Generated by Django 5.0.10 on 2025-12-17 07:50
+
+import django.db.models.deletion
+from decimal import Decimal
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('orders', '0006_transaction_delete_payment_and_more'),
+ ('products', '0010_alter_product_cost_price'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='KitSnapshot',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=200, verbose_name='Название')),
+ ('sku', models.CharField(blank=True, max_length=100, verbose_name='Артикул')),
+ ('description', models.TextField(blank=True, verbose_name='Описание')),
+ ('base_price', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=10, verbose_name='Базовая цена')),
+ ('price', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=10, verbose_name='Итоговая цена')),
+ ('sale_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Цена со скидкой')),
+ ('price_adjustment_type', models.CharField(default='none', max_length=20, verbose_name='Тип корректировки')),
+ ('price_adjustment_value', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=10, verbose_name='Значение корректировки')),
+ ('is_temporary', models.BooleanField(default=False, verbose_name='Временный комплект')),
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
+ ('original_kit', models.ForeignKey(blank=True, help_text='Ссылка на комплект, с которого создан снимок', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='snapshots', to='products.productkit', verbose_name='Оригинальный комплект')),
+ ],
+ options={
+ 'verbose_name': 'Снимок комплекта',
+ 'verbose_name_plural': 'Снимки комплектов',
+ 'ordering': ['-created_at'],
+ },
+ ),
+ migrations.CreateModel(
+ name='KitItemSnapshot',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('product_name', models.CharField(blank=True, max_length=200, verbose_name='Название товара')),
+ ('product_sku', models.CharField(blank=True, max_length=100, verbose_name='Артикул товара')),
+ ('product_price', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=10, verbose_name='Цена товара')),
+ ('variant_group_name', models.CharField(blank=True, max_length=200, verbose_name='Группа вариантов')),
+ ('quantity', models.DecimalField(decimal_places=3, max_digits=10, verbose_name='Количество')),
+ ('kit_snapshot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='orders.kitsnapshot', verbose_name='Снимок комплекта')),
+ ],
+ options={
+ 'verbose_name': 'Снимок компонента',
+ 'verbose_name_plural': 'Снимки компонентов',
+ },
+ ),
+ migrations.AddField(
+ model_name='historicalorderitem',
+ name='kit_snapshot',
+ field=models.ForeignKey(blank=True, db_constraint=False, help_text='Хранит состав комплекта на момент оформления заказа', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='orders.kitsnapshot', verbose_name='Снимок комплекта'),
+ ),
+ migrations.AddField(
+ model_name='orderitem',
+ name='kit_snapshot',
+ field=models.ForeignKey(blank=True, help_text='Хранит состав комплекта на момент оформления заказа', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='order_items', to='orders.kitsnapshot', verbose_name='Снимок комплекта'),
+ ),
+ migrations.AddIndex(
+ model_name='kitsnapshot',
+ index=models.Index(fields=['original_kit'], name='orders_kits_origina_f8d311_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='kitsnapshot',
+ index=models.Index(fields=['created_at'], name='orders_kits_created_70de88_idx'),
+ ),
+ migrations.AddIndex(
+ model_name='kititemsnapshot',
+ index=models.Index(fields=['kit_snapshot'], name='orders_kiti_kit_sna_bf307e_idx'),
+ ),
+ ]
diff --git a/myproject/orders/migrations/0008_add_item_snapshots.py b/myproject/orders/migrations/0008_add_item_snapshots.py
new file mode 100644
index 0000000..1bf46f3
--- /dev/null
+++ b/myproject/orders/migrations/0008_add_item_snapshots.py
@@ -0,0 +1,39 @@
+# Generated by Django 5.0.10 on 2025-12-17 11:58
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('orders', '0007_kit_snapshots'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='historicalorderitem',
+ name='item_name_snapshot',
+ field=models.CharField(default='', max_length=200, verbose_name='Название на момент заказа'),
+ ),
+ migrations.AddField(
+ model_name='historicalorderitem',
+ name='item_sku_snapshot',
+ field=models.CharField(blank=True, max_length=100, verbose_name='Артикул на момент заказа'),
+ ),
+ migrations.AddField(
+ model_name='orderitem',
+ name='item_name_snapshot',
+ field=models.CharField(default='', max_length=200, verbose_name='Название на момент заказа'),
+ ),
+ migrations.AddField(
+ model_name='orderitem',
+ name='item_sku_snapshot',
+ field=models.CharField(blank=True, max_length=100, verbose_name='Артикул на момент заказа'),
+ ),
+ migrations.AlterField(
+ model_name='orderitem',
+ name='kit_snapshot',
+ field=models.ForeignKey(blank=True, help_text='Хранит состав комплекта на момент оформления заказа', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_items', to='orders.kitsnapshot', verbose_name='Снимок комплекта'),
+ ),
+ ]
diff --git a/myproject/orders/migrations/0009_add_original_product_to_kit_item_snapshot.py b/myproject/orders/migrations/0009_add_original_product_to_kit_item_snapshot.py
new file mode 100644
index 0000000..1f68f20
--- /dev/null
+++ b/myproject/orders/migrations/0009_add_original_product_to_kit_item_snapshot.py
@@ -0,0 +1,20 @@
+# Generated by Django 5.0.10 on 2025-12-17 18:37
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('orders', '0008_add_item_snapshots'),
+ ('products', '0010_alter_product_cost_price'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='kititemsnapshot',
+ name='original_product',
+ field=models.ForeignKey(blank=True, help_text='Ссылка на товар для резервирования на складе', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='kit_item_snapshots', to='products.product', verbose_name='Оригинальный товар'),
+ ),
+ ]
diff --git a/myproject/orders/models/__init__.py b/myproject/orders/models/__init__.py
index caf26b5..e93ed7c 100644
--- a/myproject/orders/models/__init__.py
+++ b/myproject/orders/models/__init__.py
@@ -22,6 +22,7 @@ from .address import Address
from .order import Order
# 4. Зависимые модели
+from .kit_snapshot import KitSnapshot, KitItemSnapshot
from .order_item import OrderItem
from .transaction import Transaction
@@ -32,4 +33,6 @@ __all__ = [
'OrderItem',
'PaymentMethod',
'Transaction',
+ 'KitSnapshot',
+ 'KitItemSnapshot',
]
diff --git a/myproject/orders/models/kit_snapshot.py b/myproject/orders/models/kit_snapshot.py
new file mode 100644
index 0000000..1957338
--- /dev/null
+++ b/myproject/orders/models/kit_snapshot.py
@@ -0,0 +1,163 @@
+"""
+Снапшоты комплектов для сохранения истории заказов.
+
+При добавлении комплекта (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="Группа вариантов"
+ )
+
+ 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
diff --git a/myproject/orders/models/order_item.py b/myproject/orders/models/order_item.py
index e2782d0..7b46d29 100644
--- a/myproject/orders/models/order_item.py
+++ b/myproject/orders/models/order_item.py
@@ -3,6 +3,7 @@ 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):
@@ -36,6 +37,30 @@ class OrderItem(models.Model):
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="Количество"
@@ -92,12 +117,7 @@ class OrderItem(models.Model):
]
def __str__(self):
- item_name = ""
- if self.product:
- item_name = self.product.name
- elif self.product_kit:
- item_name = self.product_kit.name
- return f"{item_name} x{self.quantity} в заказе #{self.order.order_number}"
+ return f"{self.item_name_snapshot} x{self.quantity} в заказе #{self.order.order_number}"
def clean(self):
"""Валидация модели"""
@@ -116,12 +136,34 @@ class OrderItem(models.Model):
)
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.price
- elif self.product_kit:
- self.price = self.product_kit.price
+ 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):
@@ -130,21 +172,15 @@ class OrderItem(models.Model):
@property
def item_name(self):
- """Название товара/комплекта"""
- if self.product:
- return self.product.name
- elif self.product_kit:
- return self.product_kit.name
- return "Не указано"
+ """Название из snapshot"""
+ return self.item_name_snapshot
@property
def original_price(self):
- """Оригинальная цена товара/комплекта из каталога"""
- if self.product:
- return self.product.actual_price
- elif self.product_kit:
- return self.product_kit.actual_price
- return None
+ """Оригинальная цена из snapshot"""
+ if self.kit_snapshot:
+ return self.kit_snapshot.actual_price
+ return self.price
@property
def price_difference(self):
diff --git a/myproject/products/models/kits.py b/myproject/products/models/kits.py
index 8070f3e..6a94fa7 100644
--- a/myproject/products/models/kits.py
+++ b/myproject/products/models/kits.py
@@ -257,6 +257,50 @@ class ProductKit(BaseProductEntity):
"""Полное удаление из БД (необратимо!)"""
super().delete()
+ def create_snapshot(self):
+ """
+ Создает снимок текущего состояния комплекта.
+ Используется при добавлении комплекта в заказ для сохранения истории.
+
+ Returns:
+ KitSnapshot: Созданный снимок с компонентами
+ """
+ from orders.models import KitSnapshot, KitItemSnapshot
+
+ # Создаем снимок комплекта
+ snapshot = KitSnapshot.objects.create(
+ original_kit=self,
+ name=self.name,
+ sku=self.sku or '',
+ description=self.description or '',
+ base_price=self.base_price,
+ price=self.price,
+ sale_price=self.sale_price,
+ price_adjustment_type=self.price_adjustment_type,
+ price_adjustment_value=self.price_adjustment_value,
+ is_temporary=self.is_temporary,
+ )
+
+ # Создаем снимки компонентов
+ for item in self.kit_items.select_related('product', 'variant_group'):
+ product_price = Decimal('0')
+ if item.product:
+ product_price = item.product.actual_price or Decimal('0')
+ elif item.variant_group:
+ product_price = item.variant_group.price or Decimal('0')
+
+ KitItemSnapshot.objects.create(
+ kit_snapshot=snapshot,
+ original_product=item.product, # Сохраняем ссылку для резервирования
+ product_name=item.product.name if item.product else '',
+ product_sku=item.product.sku if item.product else '',
+ product_price=product_price,
+ variant_group_name=item.variant_group.name if item.variant_group else '',
+ quantity=item.quantity or Decimal('1'),
+ )
+
+ return snapshot
+
class KitItem(models.Model):
"""