Исправлена критическая проблема с резервами при смене статуса заказа
Проблема: - При смене статуса заказа на 'Выполнен' товар списывался со склада - Резервы обновлялись на статус 'converted_to_sale' - НО Stock.quantity_reserved не обновлялся автоматически - В результате резервы продолжали 'держать' товар, хотя он уже продан Решение: 1. Изменен сигнал create_sale_on_order_completion: - Используется .save(update_fields=[...]) вместо .update() - Это вызывает сигнал update_stock_on_reservation_change - Убран костыль с ручным вызовом refresh_from_batches() 2. Оптимизирован сигнал update_stock_on_reservation_change: - Stock обновляется ТОЛЬКО при изменении status или quantity - При изменении других полей (даты и т.д.) Stock НЕ пересчитывается - Предотвращены лишние пересчёты и улучшена производительность 3. Добавлены диагностические инструменты: - check_stock_103.py - для диагностики проблем с Stock - fix_stock_after_sale.py - команда для исправления старых заказов - diagnose_reservation_issue.py - универсальная диагностика Результат: - Элегантное решение без дублирования логики - Stock автоматически обновляется при изменении резервов - Работает везде, не только в заказах - Оптимизировано для производительности
This commit is contained in:
95
myproject/check_stock_103.py
Normal file
95
myproject/check_stock_103.py
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Диагностика Stock для заказа 103 в схеме buba
|
||||
"""
|
||||
import os
|
||||
import django
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
|
||||
django.setup()
|
||||
|
||||
from django_tenants.utils import schema_context
|
||||
from orders.models import Order
|
||||
from inventory.models import Reservation, Stock, Sale
|
||||
from django.db.models import Sum
|
||||
|
||||
# Работаем в схеме buba
|
||||
with schema_context('buba'):
|
||||
# Получаем заказ 103
|
||||
order = Order.objects.get(order_number=103)
|
||||
|
||||
print("="*80)
|
||||
print(f"ДИАГНОСТИКА ЗАКАЗА #{order.order_number} (схема: buba)")
|
||||
print("="*80)
|
||||
|
||||
# Проверяем резервы
|
||||
print("\n📝 РЕЗЕРВЫ:")
|
||||
reservations = Reservation.objects.filter(order_item__order=order)
|
||||
for res in reservations:
|
||||
print(f" - {res.product.name}:")
|
||||
print(f" Количество: {res.quantity}")
|
||||
print(f" Статус: {res.status}")
|
||||
print(f" Склад: {res.warehouse.name}")
|
||||
print(f" Product ID: {res.product_id}")
|
||||
print(f" Warehouse ID: {res.warehouse_id}")
|
||||
|
||||
# Проверяем Sale
|
||||
print("\n💰 ПРОДАЖИ (Sale):")
|
||||
sales = Sale.objects.filter(order=order)
|
||||
for sale in sales:
|
||||
print(f" - {sale.product.name}: {sale.quantity} шт.")
|
||||
|
||||
# Проверяем Stock
|
||||
print("\n📊 STOCK:")
|
||||
for res in reservations:
|
||||
stock = Stock.objects.get(
|
||||
product_id=res.product_id,
|
||||
warehouse_id=res.warehouse_id
|
||||
)
|
||||
print(f" - {stock.product.name} на {stock.warehouse.name}:")
|
||||
print(f" quantity_available: {stock.quantity_available}")
|
||||
print(f" quantity_reserved: {stock.quantity_reserved}")
|
||||
print(f" quantity_free: {stock.quantity_free}")
|
||||
|
||||
# Проверяем: сколько РЕАЛЬНО резервов со статусом 'reserved'
|
||||
print("\n🔍 ПЕРЕСЧЁТ РЕЗЕРВОВ ВРУЧНУЮ:")
|
||||
for res in reservations.values('product_id', 'warehouse_id').distinct():
|
||||
product_id = res['product_id']
|
||||
warehouse_id = res['warehouse_id']
|
||||
|
||||
# Считаем резервы со статусом 'reserved'
|
||||
reserved_count = Reservation.objects.filter(
|
||||
product_id=product_id,
|
||||
warehouse_id=warehouse_id,
|
||||
status='reserved'
|
||||
).aggregate(Sum('quantity'))['quantity__sum'] or 0
|
||||
|
||||
# Считаем резервы со статусом 'converted_to_sale'
|
||||
converted_count = Reservation.objects.filter(
|
||||
product_id=product_id,
|
||||
warehouse_id=warehouse_id,
|
||||
status='converted_to_sale'
|
||||
).aggregate(Sum('quantity'))['quantity__sum'] or 0
|
||||
|
||||
print(f" Product ID {product_id}, Warehouse ID {warehouse_id}:")
|
||||
print(f" Резервов 'reserved': {reserved_count}")
|
||||
print(f" Резервов 'converted_to_sale': {converted_count}")
|
||||
|
||||
# Что должно быть в Stock
|
||||
stock = Stock.objects.get(product_id=product_id, warehouse_id=warehouse_id)
|
||||
print(f" Stock.quantity_reserved: {stock.quantity_reserved}")
|
||||
print(f" ❌ ПРОБЛЕМА!" if stock.quantity_reserved != reserved_count else " ✅ OK")
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("ВЫВОД:")
|
||||
print("="*80)
|
||||
if stock.quantity_reserved > 0 and converted_count > 0:
|
||||
print("❌ Stock НЕ обновился после конвертации резервов!")
|
||||
print(" quantity_reserved показывает старое значение")
|
||||
print("\n🔧 Попробуем обновить вручную...")
|
||||
stock.refresh_from_batches()
|
||||
print(f" После refresh_from_batches():")
|
||||
print(f" quantity_reserved: {stock.quantity_reserved}")
|
||||
print(f" ✅ ИСПРАВЛЕНО!" if stock.quantity_reserved == 0 else " ❌ НЕ ПОМОГЛО!")
|
||||
else:
|
||||
print("✅ Всё в порядке!")
|
||||
171
myproject/diagnose_reservation_issue.py
Normal file
171
myproject/diagnose_reservation_issue.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Скрипт для диагностики проблемы с резервами при смене статуса на 'completed'.
|
||||
Проверяет:
|
||||
1. Изменяется ли статус резервов на 'converted_to_sale'
|
||||
2. Создаются ли Sale
|
||||
3. Списывается ли товар со склада
|
||||
4. Освобождаются ли резервы
|
||||
"""
|
||||
import os
|
||||
import django
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
|
||||
django.setup()
|
||||
|
||||
from django.db import connection
|
||||
from orders.models import Order, OrderStatus
|
||||
from inventory.models import Reservation, Sale, Stock
|
||||
|
||||
|
||||
def diagnose_order(order_number):
|
||||
"""Диагностика конкретного заказа"""
|
||||
print(f"\n{'='*80}")
|
||||
print(f"ДИАГНОСТИКА ЗАКАЗА #{order_number}")
|
||||
print(f"{'='*80}\n")
|
||||
|
||||
# Проверяем тенант
|
||||
print(f"📌 Текущая схема БД: {connection.schema_name}")
|
||||
|
||||
try:
|
||||
order = Order.objects.get(order_number=order_number)
|
||||
except Order.DoesNotExist:
|
||||
print(f"❌ ОШИБКА: Заказ #{order_number} не найден!")
|
||||
return
|
||||
|
||||
print(f"✓ Заказ найден")
|
||||
print(f" - Клиент: {order.customer.name}")
|
||||
print(f" - Статус: {order.status.name if order.status else 'Нет статуса'} (code: {order.status.code if order.status else 'None'})")
|
||||
print(f" - Дата создания: {order.created_at}")
|
||||
print(f" - Дата обновления: {order.updated_at}")
|
||||
|
||||
# Проверяем позиции заказа
|
||||
print(f"\n📦 ПОЗИЦИИ ЗАКАЗА:")
|
||||
items = order.items.all()
|
||||
if not items.exists():
|
||||
print(" ⚠ Нет позиций в заказе")
|
||||
else:
|
||||
for item in items:
|
||||
product = item.product or item.product_kit
|
||||
print(f" - {product.name}: {item.quantity} шт. по {item.price} руб.")
|
||||
|
||||
# Проверяем резервы
|
||||
print(f"\n📝 РЕЗЕРВЫ:")
|
||||
reservations = Reservation.objects.filter(order_item__order=order)
|
||||
if not reservations.exists():
|
||||
print(" ⚠ НЕТ РЕЗЕРВОВ для этого заказа!")
|
||||
else:
|
||||
for res in reservations:
|
||||
print(f" - {res.product.name}:")
|
||||
print(f" • Количество: {res.quantity}")
|
||||
print(f" • Статус: {res.get_status_display()} ({res.status})")
|
||||
print(f" • Склад: {res.warehouse.name}")
|
||||
print(f" • Зарезервировано: {res.reserved_at}")
|
||||
if res.converted_at:
|
||||
print(f" • Конвертировано: {res.converted_at}")
|
||||
if res.released_at:
|
||||
print(f" • Освобождено: {res.released_at}")
|
||||
|
||||
# Проверяем Sale
|
||||
print(f"\n💰 ПРОДАЖИ (Sale):")
|
||||
sales = Sale.objects.filter(order=order)
|
||||
if not sales.exists():
|
||||
print(" ⚠ НЕТ ПРОДАЖ для этого заказа!")
|
||||
if order.status and order.status.code == 'completed':
|
||||
print(" ❌ ПРОБЛЕМА: Заказ в статусе 'completed', но Sale не созданы!")
|
||||
else:
|
||||
for sale in sales:
|
||||
print(f" - {sale.product.name}:")
|
||||
print(f" • Количество: {sale.quantity}")
|
||||
print(f" • Цена продажи: {sale.sale_price}")
|
||||
print(f" • Склад: {sale.warehouse.name}")
|
||||
print(f" • Дата: {sale.created_at}")
|
||||
|
||||
# Проверяем распределение по партиям
|
||||
from inventory.models import SaleBatchAllocation
|
||||
allocations = SaleBatchAllocation.objects.filter(sale=sale)
|
||||
if allocations.exists():
|
||||
print(f" • Распределение по партиям:")
|
||||
for alloc in allocations:
|
||||
print(f" - Партия #{alloc.batch.id}: {alloc.quantity} шт.")
|
||||
|
||||
# Проверяем Stock
|
||||
print(f"\n📊 ОСТАТКИ НА СКЛАДЕ (Stock):")
|
||||
for item in items:
|
||||
product = item.product or item.product_kit
|
||||
warehouse = order.pickup_warehouse or order.items.first().order.pickup_warehouse
|
||||
|
||||
if warehouse:
|
||||
try:
|
||||
stock = Stock.objects.get(product=product, warehouse=warehouse)
|
||||
print(f" - {product.name} на складе {warehouse.name}:")
|
||||
print(f" • Всего: {stock.quantity}")
|
||||
print(f" • Зарезервировано: {stock.reserved_quantity}")
|
||||
print(f" • Доступно: {stock.available_quantity}")
|
||||
except Stock.DoesNotExist:
|
||||
print(f" - {product.name}: ❌ Stock не найден")
|
||||
else:
|
||||
print(f" - {product.name}: ⚠ Склад не определён")
|
||||
|
||||
# Проверяем историю изменений
|
||||
print(f"\n📜 ИСТОРИЯ ИЗМЕНЕНИЙ СТАТУСА:")
|
||||
history = order.history.all()[:5] # Последние 5 записей
|
||||
for idx, record in enumerate(history, 1):
|
||||
status_name = record.status.name if record.status else "Нет статуса"
|
||||
print(f" {idx}. {status_name} - {record.history_date}")
|
||||
|
||||
# Выводим выводы
|
||||
print(f"\n{'='*80}")
|
||||
print("ДИАГНОСТИКА:")
|
||||
print(f"{'='*80}")
|
||||
|
||||
issues = []
|
||||
|
||||
# Проверка 1: Резервы существуют?
|
||||
if not reservations.exists():
|
||||
issues.append("❌ КРИТИЧНО: Нет резервов для заказа")
|
||||
|
||||
# Проверка 2: Статус 'completed' + нет Sale
|
||||
if order.status and order.status.code == 'completed':
|
||||
if not sales.exists():
|
||||
issues.append("❌ КРИТИЧНО: Заказ 'completed', но Sale не созданы")
|
||||
|
||||
# Проверка 3: Статус резервов
|
||||
reserved_count = reservations.filter(status='reserved').count()
|
||||
converted_count = reservations.filter(status='converted_to_sale').count()
|
||||
|
||||
if reserved_count > 0:
|
||||
issues.append(f"❌ ПРОБЛЕМА: {reserved_count} резервов ещё в статусе 'reserved' при completed заказе")
|
||||
|
||||
if converted_count == 0 and reservations.exists():
|
||||
issues.append("❌ ПРОБЛЕМА: Ни один резерв не конвертирован в продажу")
|
||||
|
||||
if issues:
|
||||
print("\n🔴 НАЙДЕНЫ ПРОБЛЕМЫ:")
|
||||
for issue in issues:
|
||||
print(f" {issue}")
|
||||
else:
|
||||
print("\n✅ Проблем не обнаружено")
|
||||
|
||||
print(f"\n{'='*80}\n")
|
||||
|
||||
|
||||
def main():
|
||||
print("\n" + "="*80)
|
||||
print("ДИАГНОСТИКА ПРОБЛЕМЫ С РЕЗЕРВАМИ")
|
||||
print("="*80)
|
||||
|
||||
# Спрашиваем номер заказа
|
||||
order_number_input = input("\nВведите номер заказа для диагностики: ").strip()
|
||||
|
||||
try:
|
||||
order_number = int(order_number_input)
|
||||
except ValueError:
|
||||
print("❌ Ошибка: Введите корректный номер заказа (число)")
|
||||
return
|
||||
|
||||
diagnose_order(order_number)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
117
myproject/inventory/management/commands/fix_stock_after_sale.py
Normal file
117
myproject/inventory/management/commands/fix_stock_after_sale.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Команда для исправления Stock после обработки заказов.
|
||||
Пересчитывает quantity_reserved для всех товаров.
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from inventory.models import Stock, Reservation
|
||||
from django.db.models import Sum
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Исправляет quantity_reserved в Stock после обработки заказов'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--order',
|
||||
type=int,
|
||||
help='Номер заказа для исправления (опционально)'
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
order_number = options.get('order')
|
||||
|
||||
if order_number:
|
||||
self.stdout.write(f"Исправление Stock для заказа {order_number}...")
|
||||
self.fix_for_order(order_number)
|
||||
else:
|
||||
self.stdout.write("Исправление всех Stock...")
|
||||
self.fix_all_stock()
|
||||
|
||||
def fix_for_order(self, order_number):
|
||||
"""Исправить Stock для конкретного заказа"""
|
||||
from orders.models import Order
|
||||
|
||||
try:
|
||||
order = Order.objects.get(order_number=order_number)
|
||||
except Order.DoesNotExist:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f"Заказ {order_number} не найден!")
|
||||
)
|
||||
return
|
||||
|
||||
# Получаем все резервы для этого заказа
|
||||
reservations = Reservation.objects.filter(
|
||||
order_item__order=order
|
||||
).values('product_id', 'warehouse_id').distinct()
|
||||
|
||||
fixed_count = 0
|
||||
for res in reservations:
|
||||
product_id = res['product_id']
|
||||
warehouse_id = res['warehouse_id']
|
||||
|
||||
try:
|
||||
stock = Stock.objects.get(
|
||||
product_id=product_id,
|
||||
warehouse_id=warehouse_id
|
||||
)
|
||||
|
||||
old_reserved = stock.quantity_reserved
|
||||
stock.refresh_from_batches()
|
||||
new_reserved = stock.quantity_reserved
|
||||
|
||||
if old_reserved != new_reserved:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"✓ Обновлено: product_id={product_id}, "
|
||||
f"warehouse_id={warehouse_id}, "
|
||||
f"reserved: {old_reserved} → {new_reserved}"
|
||||
)
|
||||
)
|
||||
fixed_count += 1
|
||||
else:
|
||||
self.stdout.write(
|
||||
f" Без изменений: product_id={product_id}"
|
||||
)
|
||||
|
||||
except Stock.DoesNotExist:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"⚠ Stock не найден: product_id={product_id}, "
|
||||
f"warehouse_id={warehouse_id}"
|
||||
)
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"\n✅ Готово! Обновлено {fixed_count} Stock для заказа {order_number}"
|
||||
)
|
||||
)
|
||||
|
||||
def fix_all_stock(self):
|
||||
"""Пересчитать все Stock"""
|
||||
stocks = Stock.objects.all()
|
||||
total = stocks.count()
|
||||
fixed_count = 0
|
||||
|
||||
self.stdout.write(f"Найдено {total} Stock записей...")
|
||||
|
||||
for stock in stocks:
|
||||
old_reserved = stock.quantity_reserved
|
||||
stock.refresh_from_batches()
|
||||
new_reserved = stock.quantity_reserved
|
||||
|
||||
if old_reserved != new_reserved:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"✓ {stock.product.name} на {stock.warehouse.name}: "
|
||||
f"reserved {old_reserved} → {new_reserved}"
|
||||
)
|
||||
)
|
||||
fixed_count += 1
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"\n✅ Готово! Обновлено {fixed_count} из {total} Stock"
|
||||
)
|
||||
)
|
||||
@@ -69,16 +69,22 @@ def reserve_stock_on_order_create(sender, instance, created, **kwargs):
|
||||
|
||||
|
||||
@receiver(post_save, sender=Order)
|
||||
@transaction.atomic
|
||||
def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Сигнал: Когда заказ переходит в статус 'completed' (доставлен),
|
||||
создается операция Sale и резервы преобразуются в продажу.
|
||||
|
||||
КРИТИЧНО: Резервы обновляются ТОЛЬКО ПОСЛЕ успешного создания Sale!
|
||||
|
||||
Процесс:
|
||||
1. Проверяем, изменился ли статус на 'completed'
|
||||
2. Для каждого товара создаем Sale (автоматический FIFO-список)
|
||||
3. Обновляем резерв на 'converted_to_sale'
|
||||
3. ТОЛЬКО после успешного создания Sale обновляем резервы на 'converted_to_sale'
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if created:
|
||||
return # Только для обновлений
|
||||
|
||||
@@ -90,41 +96,29 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
||||
if Sale.objects.filter(order=instance).exists():
|
||||
return # Продажи уже созданы, выходим БЕЗ обновления резервов
|
||||
|
||||
# Обновляем резервы ТОЛЬКО если Sale ещё не созданы
|
||||
# Используем update() вместо save() чтобы избежать повторного вызова сигналов
|
||||
# Проверяем наличие резервов ДО начала операции
|
||||
reservations_to_update = Reservation.objects.filter(
|
||||
order_item__order=instance,
|
||||
status='reserved'
|
||||
)
|
||||
|
||||
if reservations_to_update.exists():
|
||||
# Массовое обновление БЕЗ вызова сигналов
|
||||
reservations_to_update.update(
|
||||
status='converted_to_sale',
|
||||
converted_at=timezone.now()
|
||||
if not reservations_to_update.exists():
|
||||
logger.warning(
|
||||
f"⚠ Заказ {instance.order_number} переведён в 'completed', но нет резервов в статусе 'reserved'"
|
||||
)
|
||||
|
||||
# Обновляем Stock вручную, т.к. update() не вызывает сигналы
|
||||
# Группируем по product + warehouse для эффективности
|
||||
reservation_groups = reservations_to_update.values_list('product_id', 'warehouse_id').distinct()
|
||||
|
||||
for product_id, warehouse_id in reservation_groups:
|
||||
try:
|
||||
stock = Stock.objects.get(
|
||||
product_id=product_id,
|
||||
warehouse_id=warehouse_id
|
||||
)
|
||||
stock.refresh_from_batches()
|
||||
except Stock.DoesNotExist:
|
||||
pass # Stock не найден, пропускаем
|
||||
# Продолжаем выполнение - возможно, это повторный вызов или резервы уже обработаны
|
||||
|
||||
# Определяем склад (используем склад самовывоза из заказа или первый активный)
|
||||
warehouse = instance.pickup_warehouse or Warehouse.objects.filter(is_active=True).first()
|
||||
|
||||
if not warehouse:
|
||||
logger.error(f"❌ Не найден склад для заказа {instance.order_number}. Списание невозможно.")
|
||||
return
|
||||
|
||||
# Для каждого товара в заказе создаем Sale
|
||||
sales_created = []
|
||||
sale_creation_failed = False
|
||||
|
||||
for item in instance.items.all():
|
||||
# Определяем товар
|
||||
product = item.product if item.product else item.product_kit
|
||||
@@ -142,13 +136,49 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
||||
order=instance,
|
||||
document_number=instance.order_number
|
||||
)
|
||||
sales_created.append(sale)
|
||||
logger.info(f"✓ Sale создан для {product.name}: {item.quantity} шт.")
|
||||
|
||||
except ValueError as e:
|
||||
# Логируем ошибку, но не прерываем процесс
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
# Логируем ошибку и прерываем процесс
|
||||
logger.error(
|
||||
f"Ошибка при создании Sale для заказа {instance.order_number}: {e}"
|
||||
f"❌ ОШИБКА при создании Sale для заказа {instance.order_number}, товар {product.name}: {e}"
|
||||
)
|
||||
sale_creation_failed = True
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"❌ КРИТИЧЕСКАЯ ОШИБКА при создании Sale для заказа {instance.order_number}: {e}"
|
||||
)
|
||||
sale_creation_failed = True
|
||||
break
|
||||
|
||||
# КРИТИЧНО: Обновляем резервы ТОЛЬКО если ВСЕ Sale созданы успешно
|
||||
if sale_creation_failed:
|
||||
logger.error(
|
||||
f"❌ Не удалось создать Sale для заказа {instance.order_number}. "
|
||||
f"Резервы НЕ будут обновлены. Транзакция откатится."
|
||||
)
|
||||
# Поднимаем исключение чтобы откатить всю транзакцию
|
||||
raise ValueError(f"Не удалось создать Sale для заказа {instance.order_number}")
|
||||
|
||||
# Все Sale созданы успешно - теперь обновляем резервы
|
||||
if reservations_to_update.exists():
|
||||
# Обновляем резервы через .save() чтобы сработал сигнал обновления Stock
|
||||
# Сигнал update_stock_on_reservation_change автоматически обновит Stock
|
||||
for reservation in reservations_to_update:
|
||||
reservation.status = 'converted_to_sale'
|
||||
reservation.converted_at = timezone.now()
|
||||
reservation.save(update_fields=['status', 'converted_at'])
|
||||
|
||||
updated_count = reservations_to_update.count()
|
||||
logger.info(
|
||||
f"✓ Обновлено {updated_count} резервов для заказа {instance.order_number}: reserved → converted_to_sale"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"🎉 Заказ {instance.order_number} успешно обработан: создано {len(sales_created)} Sale, "
|
||||
f"обновлено {reservations_to_update.count() if reservations_to_update.exists() else 0} резервов"
|
||||
)
|
||||
|
||||
|
||||
@@ -700,21 +730,45 @@ def update_stock_on_reservation_change(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Сигнал: При создании или изменении резерва (Reservation) обновляем Stock.
|
||||
|
||||
ОПТИМИЗАЦИЯ: Обновляем Stock только если изменились поля, влияющие на резервы:
|
||||
- status (reserved/converted_to_sale/released)
|
||||
- quantity (количество резерва)
|
||||
- created (новый резерв)
|
||||
|
||||
Процесс:
|
||||
1. При создании или изменении резерва пересчитываем quantity_reserved
|
||||
2. Обновляем запись Stock для этого товара
|
||||
1. При создании резерва пересчитываем quantity_reserved
|
||||
2. При изменении статуса или количества пересчитываем quantity_reserved
|
||||
3. При других изменениях (например, дата) НЕ пересчитываем Stock (оптимизация)
|
||||
"""
|
||||
if not instance.product or not instance.warehouse:
|
||||
return
|
||||
|
||||
# Получаем или создаем Stock запись
|
||||
# Если это создание нового резерва - всегда обновляем Stock
|
||||
if created:
|
||||
stock, _ = Stock.objects.get_or_create(
|
||||
product=instance.product,
|
||||
warehouse=instance.warehouse
|
||||
)
|
||||
stock.refresh_from_batches()
|
||||
return
|
||||
|
||||
# Пересчитываем остатки из всех активных партий и резервов
|
||||
# refresh_from_batches() уже вызывает save()
|
||||
# Для обновления - проверяем, изменились ли поля, влияющие на Stock
|
||||
# Используем django-simple-history или проверяем через update_fields
|
||||
update_fields = kwargs.get('update_fields', None)
|
||||
|
||||
# Если update_fields указаны (вызов через save(update_fields=[...]))
|
||||
# проверяем, есть ли среди них 'status' или 'quantity'
|
||||
if update_fields is not None:
|
||||
fields_affecting_stock = {'status', 'quantity'}
|
||||
if not fields_affecting_stock.intersection(update_fields):
|
||||
# Изменились другие поля (например, дата) - не обновляем Stock
|
||||
return
|
||||
|
||||
# Если дошли сюда - нужно обновить Stock
|
||||
stock, _ = Stock.objects.get_or_create(
|
||||
product=instance.product,
|
||||
warehouse=instance.warehouse
|
||||
)
|
||||
stock.refresh_from_batches()
|
||||
|
||||
|
||||
|
||||
@@ -132,6 +132,8 @@ class TenantCreationIntegrationTest(TransactionTestCase):
|
||||
def test_new_tenant_gets_order_statuses(self):
|
||||
"""
|
||||
Тест: Новый тенант получает системные статусы заказов.
|
||||
|
||||
КРИТИЧЕСКИ ВАЖНО: Должен быть минимум ОДИН позитивный и ОДИН негативный финальный статус.
|
||||
"""
|
||||
# Создаём и активируем тенант
|
||||
registration = TenantRegistration.objects.create(
|
||||
@@ -165,9 +167,48 @@ class TenantCreationIntegrationTest(TransactionTestCase):
|
||||
self.assertIsNotNone(draft_status, "Статус 'draft' не создан")
|
||||
self.assertTrue(draft_status.is_system)
|
||||
|
||||
# КРИТИЧЕСКАЯ ПРОВЕРКА #1: Позитивный финальный статус
|
||||
positive_statuses = OrderStatus.objects.filter(is_positive_end=True)
|
||||
self.assertGreaterEqual(
|
||||
positive_statuses.count(),
|
||||
1,
|
||||
"Должен быть минимум ОДИН позитивный финальный статус (is_positive_end=True)"
|
||||
)
|
||||
|
||||
# Проверяем что есть 'completed' (основной позитивный статус)
|
||||
completed_status = OrderStatus.objects.filter(code='completed').first()
|
||||
self.assertIsNotNone(completed_status, "Статус 'completed' не создан")
|
||||
self.assertTrue(completed_status.is_system)
|
||||
self.assertIsNotNone(
|
||||
completed_status,
|
||||
"Статус 'completed' не создан (основной позитивный статус)"
|
||||
)
|
||||
self.assertTrue(
|
||||
completed_status.is_system,
|
||||
"'completed' должен быть системным"
|
||||
)
|
||||
self.assertTrue(
|
||||
completed_status.is_positive_end,
|
||||
"'completed' должен быть позитивным финальным статусом"
|
||||
)
|
||||
|
||||
# КРИТИЧЕСКАЯ ПРОВЕРКА #2: Негативный финальный статус
|
||||
negative_statuses = OrderStatus.objects.filter(is_negative_end=True)
|
||||
self.assertGreaterEqual(
|
||||
negative_statuses.count(),
|
||||
1,
|
||||
"Должен быть минимум ОДИН негативный финальный статус (is_negative_end=True)"
|
||||
)
|
||||
|
||||
# Проверяем что есть 'cancelled' или другой негативный статус
|
||||
# Проверяем первый найденный негативный статус
|
||||
first_negative_status = negative_statuses.first()
|
||||
self.assertTrue(
|
||||
first_negative_status.is_system,
|
||||
f"Негативный статус '{first_negative_status.code}' должен быть системным"
|
||||
)
|
||||
self.assertTrue(
|
||||
first_negative_status.is_negative_end,
|
||||
f"Статус '{first_negative_status.code}' должен быть негативным финальным"
|
||||
)
|
||||
|
||||
def test_new_tenant_gets_system_customer(self):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user