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

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

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

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 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):