""" Тест для проверки исправления двойного возврата товара и резервов. Проблема: При смене статуса с 'completed' на нейтральный возвращается двойное количество товара и резервов. Решение: Использовать update() вместо save() для резервов, чтобы избежать повторного вызова сигнала update_stock_on_reservation_change. """ import os import django os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') django.setup() from decimal import Decimal from django.db import transaction from orders.models import Order, OrderStatus from inventory.models import Sale, Reservation, Stock, StockBatch def print_state(order, title): """Выводит текущее состояние заказа, резервов, товара и Stock""" print(f"\n{'='*60}") print(f"{title}") print(f"{'='*60}") print(f"Заказ #{order.order_number}: status={order.status.code if order.status else None}") # Резервы print("\nРезервы:") reservations = Reservation.objects.filter(order_item__order=order).select_related('product') for res in reservations: print(f" {res.product.sku}: qty={res.quantity}, status={res.status}") # Sale print("\nSale:") sales = Sale.objects.filter(order=order).select_related('product') for sale in sales: print(f" {sale.product.sku}: qty={sale.quantity}") if not sales.exists(): print(" (нет Sale)") # Stock и Batches print("\nStock и StockBatch:") for item in order.items.all(): product = item.product if item.product else item.product_kit warehouse = order.pickup_warehouse if not product or not warehouse: continue # Stock try: stock = Stock.objects.get(product=product, warehouse=warehouse) print(f" {product.sku}:") print(f" Stock: available={stock.quantity_available}, reserved={stock.quantity_reserved}") except Stock.DoesNotExist: print(f" {product.sku}: Stock не найден") continue # Batches batches = StockBatch.objects.filter( product=product, warehouse=warehouse, is_active=True ).order_by('created_at') total_batch_qty = sum(b.quantity for b in batches) print(f" Batches (всего {batches.count()}): total_qty={total_batch_qty}") for batch in batches: print(f" Batch #{batch.id}: qty={batch.quantity}, cost={batch.cost_price}") def test_status_change_rollback(): """ Тест: Проверка отката при смене статуса completed → draft Шаги: 1. Найти заказ в статусе 'draft' с товарами 2. Записать начальное состояние Stock/Batches 3. Перевести в 'completed' (создаются Sale, списывается товар) 4. Вернуть в 'draft' (откат Sale, восстановление товара) 5. Проверить, что количество вернулось к исходному (без дублирования) """ print("\n" + "="*80) print("ТЕСТ: Проверка отката при смене статуса completed → draft") print("="*80) # Найти заказ для теста draft_status = OrderStatus.objects.get(code='draft') completed_status = OrderStatus.objects.get(code='completed') order = Order.objects.filter(status=draft_status).exclude(items__isnull=True).first() if not order: print("❌ Не найден заказ в статусе 'draft' для теста") return print(f"Тестовый заказ: #{order.order_number}") # Получаем товар и склад для проверки item = order.items.first() product = item.product if item.product else item.product_kit warehouse = order.pickup_warehouse if not product or not warehouse: print("❌ У заказа нет товара или склада") return # === ШАГ 1: Записываем начальное состояние === print_state(order, "ШАГ 1: Начальное состояние (draft)") try: stock_initial = Stock.objects.get(product=product, warehouse=warehouse) initial_available = stock_initial.quantity_available initial_reserved = stock_initial.quantity_reserved except Stock.DoesNotExist: print("❌ Stock не найден для товара") return batches_initial = list( StockBatch.objects.filter( product=product, warehouse=warehouse, is_active=True ).values('id', 'quantity') ) print(f"\n📊 Записано начальное состояние:") print(f" Stock: available={initial_available}, reserved={initial_reserved}") print(f" Batches: {len(batches_initial)} партий") # === ШАГ 2: Переводим в 'completed' === print(f"\n{'='*60}") print("ШАГ 2: Переводим заказ в 'completed'") print(f"{'='*60}") with transaction.atomic(): order.status = completed_status order.save() print_state(order, "Состояние после перехода в 'completed'") # === ШАГ 3: Возвращаем в 'draft' === print(f"\n{'='*60}") print("ШАГ 3: Возвращаем заказ в 'draft' (ОТКАТ)") print(f"{'='*60}") with transaction.atomic(): order.status = draft_status order.save() print_state(order, "Состояние после возврата в 'draft'") # === ШАГ 4: Проверка результатов === print(f"\n{'='*60}") print("ШАГ 4: Проверка результатов") print(f"{'='*60}") stock_final = Stock.objects.get(product=product, warehouse=warehouse) final_available = stock_final.quantity_available final_reserved = stock_final.quantity_reserved batches_final = list( StockBatch.objects.filter( product=product, warehouse=warehouse, is_active=True ).values('id', 'quantity') ) print(f"\n📊 Сравнение начального и конечного состояния:") print(f" Stock available: {initial_available} → {final_available}") print(f" Stock reserved: {initial_reserved} → {final_reserved}") print(f" Batches count: {len(batches_initial)} → {len(batches_final)}") # Проверки errors = [] if final_available != initial_available: errors.append( f"❌ Stock.quantity_available не совпадает! " f"Ожидалось {initial_available}, получено {final_available}" ) else: print(f"✅ Stock.quantity_available вернулся к исходному: {final_available}") if final_reserved != initial_reserved: errors.append( f"❌ Stock.quantity_reserved не совпадает! " f"Ожидалось {initial_reserved}, получено {final_reserved}" ) else: print(f"✅ Stock.quantity_reserved вернулся к исходному: {final_reserved}") # Проверяем количество в партиях for batch_init in batches_initial: batch_final = next((b for b in batches_final if b['id'] == batch_init['id']), None) if not batch_final: errors.append(f"❌ Партия #{batch_init['id']} исчезла после отката!") elif batch_final['quantity'] != batch_init['quantity']: errors.append( f"❌ Партия #{batch_init['id']}: количество не совпадает! " f"Ожидалось {batch_init['quantity']}, получено {batch_final['quantity']}" ) if not errors: print("\n✅ ТЕСТ ПРОЙДЕН: Все данные вернулись к исходному состоянию!") else: print("\n❌ ТЕСТ ПРОВАЛЕН:") for error in errors: print(f" {error}") # === ШАГ 5: Откатываем изменения (возвращаем заказ в исходное состояние) === print(f"\n{'='*60}") print("Откатываем тестовые изменения...") print(f"{'='*60}") with transaction.atomic(): # Заказ уже в draft, ничего не делаем pass print("Тест завершен.") if __name__ == '__main__': test_status_change_rollback()