Обновления и новые функции: изменение шаблона клиента, обновление сигналов инвентаря, добавление снимков наборов и элементов заказа, обновление моделей заказов и продуктов
This commit is contained in:
76
myproject/orders/migrations/0007_kit_snapshots.py
Normal file
76
myproject/orders/migrations/0007_kit_snapshots.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
39
myproject/orders/migrations/0008_add_item_snapshots.py
Normal file
39
myproject/orders/migrations/0008_add_item_snapshots.py
Normal 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='Снимок комплекта'),
|
||||
),
|
||||
]
|
||||
@@ -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='Оригинальный товар'),
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
163
myproject/orders/models/kit_snapshot.py
Normal file
163
myproject/orders/models/kit_snapshot.py
Normal 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
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user