Обновления и новые функции: изменение шаблона клиента, обновление сигналов инвентаря, добавление снимков наборов и элементов заказа, обновление моделей заказов и продуктов

This commit is contained in:
2025-12-18 00:14:24 +03:00
parent 56725e8092
commit 7b32cdcebf
9 changed files with 547 additions and 87 deletions

View File

@@ -260,24 +260,22 @@
<!-- История заказов --> <!-- История заказов -->
<div class="col-md-12"> <div class="col-md-12">
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header d-flex justify-content-between align-items-center">
<button class="btn btn-link w-100 text-start text-decoration-none p-0 d-flex justify-content-between align-items-center" <button class="btn btn-link text-start text-decoration-none p-0 d-flex align-items-center flex-grow-1"
type="button" type="button"
data-bs-toggle="collapse" data-bs-toggle="collapse"
data-bs-target="#ordersHistoryCollapse" data-bs-target="#ordersHistoryCollapse"
aria-expanded="false" aria-expanded="false"
aria-controls="ordersHistoryCollapse"> aria-controls="ordersHistoryCollapse"
<h5 class="mb-0">История заказов</h5> style="border: none; background: none;">
<div> <h5 class="mb-0 me-3">История заказов</h5>
<span class="badge bg-primary me-2">{{ orders_page.paginator.count }}</span> <span class="badge bg-primary">{{ orders_page.paginator.count }}</span>
<a href="{% url 'orders:order-create' %}?customer={{ customer.pk }}" <i class="bi bi-chevron-down ms-auto"></i>
class="btn btn-sm btn-success me-2"
onclick="event.stopPropagation();">
<i class="bi bi-plus-circle"></i> Новый заказ
</a>
<i class="bi bi-chevron-down"></i>
</div>
</button> </button>
<a href="{% url 'orders:order-create' %}?customer={{ customer.pk }}"
class="btn btn-sm btn-success ms-2">
<i class="bi bi-plus-circle"></i> Новый заказ
</a>
</div> </div>
<div class="collapse" id="ordersHistoryCollapse"> <div class="collapse" id="ordersHistoryCollapse">
<div class="card-body"> <div class="card-body">

View File

@@ -59,10 +59,13 @@ def reserve_stock_on_order_create(sender, instance, created, **kwargs):
Процесс: Процесс:
1. Проверяем, новый ли заказ (создан только что) 1. Проверяем, новый ли заказ (создан только что)
2. Для каждого товара в заказе создаем Reservation 2. Для обычных товаров - создаём резерв напрямую
3. Статус резерва = 'reserved' 3. Для комплектов - резервируем компоненты (группируя одинаковые товары)
4. Проверяем на существующие резервы (защита от дубликатов) 4. Статус резерва = 'reserved'
5. Проверяем на существующие резервы (защита от дубликатов)
""" """
from collections import defaultdict
if not created: if not created:
return # Только для новых заказов return # Только для новых заказов
@@ -76,33 +79,63 @@ def reserve_stock_on_order_create(sender, instance, created, **kwargs):
# Для каждого товара в заказе # Для каждого товара в заказе
for item in instance.items.all(): for item in instance.items.all():
# Определяем товар (может быть product или product_kit) if item.product:
product = item.product if item.product else item.product_kit # Обычный товар - резервируем как раньше
_create_or_update_reservation(item, item.product, warehouse, Decimal(str(item.quantity)))
if product: elif item.product_kit and item.kit_snapshot:
# ЗАЩИТА ОТ ДУБЛИКАТОВ: Проверяем, нет ли уже резерва для этой позиции # Комплект - резервируем КОМПОНЕНТЫ из снимка
existing_reservation = Reservation.objects.filter( # Группируем одинаковые товары для создания одного резерва
order_item=item, product_quantities = defaultdict(Decimal)
product=product,
warehouse=warehouse
).first()
if existing_reservation: for kit_item in item.kit_snapshot.items.select_related('original_product'):
# Резерв уже существует - обновляем его вместо создания нового if kit_item.original_product:
existing_reservation.quantity = Decimal(str(item.quantity)) # Суммируем количество: qty компонента * qty комплектов в заказе
existing_reservation.status = 'reserved' product_quantities[kit_item.original_product_id] += (
existing_reservation.save() kit_item.quantity * Decimal(str(item.quantity))
else: )
# Резерва нет - создаем новый
Reservation.objects.create( # Создаём по одному резерву на каждый уникальный товар
order_item=item, from products.models import Product
product=product, for product_id, total_qty in product_quantities.items():
warehouse=warehouse, product = Product.objects.get(pk=product_id)
quantity=Decimal(str(item.quantity)), _create_or_update_reservation(
status='reserved' 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) @receiver(post_save, sender=Order)
@transaction.atomic @transaction.atomic
def create_sale_on_order_completion(sender, instance, created, **kwargs): 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 # Для витринных комплектов не создаем новые резервы return # Для витринных комплектов не создаем новые резервы
# Обычный товар или постоянный комплект # Обычный товар или постоянный комплект
# Ищем резерв для этой позиции в ЛЮБОМ статусе (не только 'reserved') # Ищем резервы для этой позиции в ЛЮБОМ статусе (не только 'reserved')
# КРИТИЧНО: Убран фильтр status='reserved' для предотвращения дубликатов # КРИТИЧНО: Убран фильтр status='reserved' для предотвращения дубликатов
reservation = Reservation.objects.filter( reservations = Reservation.objects.filter(order_item=instance)
order_item=instance
).first() 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( 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() warehouse = instance.order.pickup_warehouse or Warehouse.objects.filter(is_active=True).first()
if warehouse: if not warehouse:
product = instance.product logger.warning(f"Не найден склад для резервирования (OrderItem #{instance.id})")
return
if instance.product:
# Обычный товар - создаем один резерв
reservation = Reservation.objects.create( reservation = Reservation.objects.create(
order_item=instance, order_item=instance,
product=product, product=instance.product,
warehouse=warehouse, warehouse=warehouse,
quantity=Decimal(str(instance.quantity)), quantity=Decimal(str(instance.quantity)),
status='reserved' status='reserved'
) )
logger.info( 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})" f"(OrderItem #{instance.id}, заказ {instance.order.order_number})"
) )

View File

@@ -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'),
),
]

View File

@@ -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='Снимок комплекта'),
),
]

View File

@@ -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='Оригинальный товар'),
),
]

View File

@@ -22,6 +22,7 @@ from .address import Address
from .order import Order from .order import Order
# 4. Зависимые модели # 4. Зависимые модели
from .kit_snapshot import KitSnapshot, KitItemSnapshot
from .order_item import OrderItem from .order_item import OrderItem
from .transaction import Transaction from .transaction import Transaction
@@ -32,4 +33,6 @@ __all__ = [
'OrderItem', 'OrderItem',
'PaymentMethod', 'PaymentMethod',
'Transaction', 'Transaction',
'KitSnapshot',
'KitItemSnapshot',
] ]

View File

@@ -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

View File

@@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError
from products.models import Product, ProductKit from products.models import Product, ProductKit
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
from .order import Order from .order import Order
from .kit_snapshot import KitSnapshot
class OrderItem(models.Model): class OrderItem(models.Model):
@@ -36,6 +37,30 @@ class OrderItem(models.Model):
verbose_name="Комплект товаров" 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( quantity = models.PositiveIntegerField(
default=1, default=1,
verbose_name="Количество" verbose_name="Количество"
@@ -92,12 +117,7 @@ class OrderItem(models.Model):
] ]
def __str__(self): def __str__(self):
item_name = "" return f"{self.item_name_snapshot} x{self.quantity} в заказе #{self.order.order_number}"
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}"
def clean(self): def clean(self):
"""Валидация модели""" """Валидация модели"""
@@ -116,12 +136,34 @@ class OrderItem(models.Model):
) )
def save(self, *args, **kwargs): 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 not self.price:
if self.product: if self.product:
self.price = self.product.price self.price = self.product.actual_price
elif self.product_kit: elif self.kit_snapshot:
self.price = self.product_kit.price self.price = self.kit_snapshot.actual_price
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_total_price(self): def get_total_price(self):
@@ -130,21 +172,15 @@ class OrderItem(models.Model):
@property @property
def item_name(self): def item_name(self):
"""Название товара/комплекта""" """Название из snapshot"""
if self.product: return self.item_name_snapshot
return self.product.name
elif self.product_kit:
return self.product_kit.name
return "Не указано"
@property @property
def original_price(self): def original_price(self):
"""Оригинальная цена товара/комплекта из каталога""" """Оригинальная цена из snapshot"""
if self.product: if self.kit_snapshot:
return self.product.actual_price return self.kit_snapshot.actual_price
elif self.product_kit: return self.price
return self.product_kit.actual_price
return None
@property @property
def price_difference(self): def price_difference(self):

View File

@@ -257,6 +257,50 @@ class ProductKit(BaseProductEntity):
"""Полное удаление из БД (необратимо!)""" """Полное удаление из БД (необратимо!)"""
super().delete() 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): class KitItem(models.Model):
""" """