Added 5 critical tests for order status transitions
Tests cover: 1. Multiple status transitions (draft->completed->cancelled->completed) 2. Cancellation from draft (reservation release) 3. Un-cancellation to pending (reservation restore) 4. Creating order with intermediate status 5. Rollback from completed to draft Each test verifies: - Stock state (available, reserved, free) - Reservation status transitions - Sale creation/deletion without duplicates - StockBatch quantity changes Files: - inventory/tests/test_order_status_transitions.py (570 lines) - inventory/tests/README.md (138 lines) - inventory/tests/__init__.py - run_status_tests.bat (launch script)
This commit is contained in:
137
myproject/inventory/tests/README.md
Normal file
137
myproject/inventory/tests/README.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# 🧪 Критические тесты переходов между статусами заказов
|
||||||
|
|
||||||
|
## 📋 Описание
|
||||||
|
|
||||||
|
Набор из **5 критических тестов**, проверяющих корректность работы системы резервирования и списания товаров при переходах между статусами заказов.
|
||||||
|
|
||||||
|
## 🎯 Тестируемые сценарии
|
||||||
|
|
||||||
|
### 1️⃣ **Тест "Танцы туда-сюда"** (`test_01_draft_to_completed_to_cancelled_to_completed`)
|
||||||
|
Проверяет множественные переходы между статусами:
|
||||||
|
- `draft` → `completed` → `cancelled` → `completed` (снова)
|
||||||
|
- **Цель:** Убедиться что Sale не дублируется при повторном переходе в `completed`
|
||||||
|
|
||||||
|
### 2️⃣ **Тест отмены из черновика** (`test_02_draft_to_cancelled_releases_reservations`)
|
||||||
|
Проверяет освобождение резервов при отмене:
|
||||||
|
- `draft` → `cancelled`
|
||||||
|
- **Цель:** Резервы должны корректно освобождаться
|
||||||
|
|
||||||
|
### 3️⃣ **Тест возврата из отмены** (`test_03_cancelled_to_pending_reserves_stock`)
|
||||||
|
Проверяет резервирование при возврате:
|
||||||
|
- `draft` → `cancelled` → `pending`
|
||||||
|
- **Цель:** Резервы должны восстанавливаться при возврате из отмены
|
||||||
|
|
||||||
|
### 4️⃣ **Тест промежуточного статуса** (`test_04_create_order_with_intermediate_status`)
|
||||||
|
Проверяет создание заказа с новым статусом:
|
||||||
|
- Создание заказа сразу со статусом "Упаковывается" (не positive, не negative)
|
||||||
|
- **Цель:** Система должна работать с любыми промежуточными статусами
|
||||||
|
|
||||||
|
### 5️⃣ **Тест отката от выполненного** (`test_05_completed_to_draft_rollback_sale`)
|
||||||
|
Проверяет откат Sale:
|
||||||
|
- `draft` → `completed` → `draft`
|
||||||
|
- **Цель:** Sale должен корректно откатываться, товар возвращаться на склад
|
||||||
|
|
||||||
|
## 🚀 Запуск тестов
|
||||||
|
|
||||||
|
### Запуск всех тестов:
|
||||||
|
```bash
|
||||||
|
cd myproject
|
||||||
|
python manage.py test inventory.tests.test_order_status_transitions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запуск конкретного теста:
|
||||||
|
```bash
|
||||||
|
python manage.py test inventory.tests.test_order_status_transitions.OrderStatusTransitionCriticalTest.test_01_draft_to_completed_to_cancelled_to_completed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запуск с подробным выводом:
|
||||||
|
```bash
|
||||||
|
python manage.py test inventory.tests.test_order_status_transitions --verbosity=2
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Что проверяет каждый тест
|
||||||
|
|
||||||
|
Все тесты проверяют **3 критических аспекта**:
|
||||||
|
|
||||||
|
### 1. **Корректность Stock**
|
||||||
|
- `quantity_available` - доступное количество
|
||||||
|
- `quantity_reserved` - зарезервированное количество
|
||||||
|
- `quantity_free` - свободное количество (available - reserved)
|
||||||
|
|
||||||
|
### 2. **Корректность Reservation**
|
||||||
|
- Статус резерва (`reserved`, `converted_to_sale`, `released`)
|
||||||
|
- Переходы между статусами при изменении статуса заказа
|
||||||
|
|
||||||
|
### 3. **Корректность Sale и StockBatch**
|
||||||
|
- Sale не дублируется при повторных переходах
|
||||||
|
- StockBatch корректно уменьшается/восстанавливается
|
||||||
|
- SaleBatchAllocation создаётся только один раз
|
||||||
|
|
||||||
|
## 🔍 Пример вывода успешного теста
|
||||||
|
|
||||||
|
```
|
||||||
|
test_01_draft_to_completed_to_cancelled_to_completed ... ok
|
||||||
|
test_02_draft_to_cancelled_releases_reservations ... ok
|
||||||
|
test_03_cancelled_to_pending_reserves_stock ... ok
|
||||||
|
test_04_create_order_with_intermediate_status ... ok
|
||||||
|
test_05_completed_to_draft_rollback_sale ... ok
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
Ran 5 tests in 2.341s
|
||||||
|
|
||||||
|
OK
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Важные особенности
|
||||||
|
|
||||||
|
1. **Тесты используют TransactionTestCase** - каждый тест выполняется в отдельной транзакции
|
||||||
|
2. **Создаётся отдельная схема БД** `test_order_status` для изоляции
|
||||||
|
3. **После каждого теста данные очищаются** - тесты не влияют друг на друга
|
||||||
|
4. **Тесты работают с тенантами** - используется `schema_context()`
|
||||||
|
|
||||||
|
## 🐛 Если тесты падают
|
||||||
|
|
||||||
|
### Проверьте сигналы:
|
||||||
|
- `inventory/signals.py` - все сигналы должны быть подключены
|
||||||
|
- `inventory/apps.py` - сигналы должны импортироваться в `ready()`
|
||||||
|
|
||||||
|
### Проверьте модели:
|
||||||
|
- `Reservation.status` - должны быть варианты: `reserved`, `converted_to_sale`, `released`
|
||||||
|
- `OrderStatus` - должны быть флаги `is_positive_end`, `is_negative_end`
|
||||||
|
|
||||||
|
### Проверьте БД:
|
||||||
|
```bash
|
||||||
|
python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Добавление новых тестов
|
||||||
|
|
||||||
|
Чтобы добавить новый тест:
|
||||||
|
|
||||||
|
1. Создайте метод в классе `OrderStatusTransitionCriticalTest`
|
||||||
|
2. Начните имя с `test_` (обязательно!)
|
||||||
|
3. Используйте вспомогательные методы:
|
||||||
|
- `_create_order(status, quantity)` - создать заказ
|
||||||
|
- `_assert_stock_state(available, reserved, free)` - проверить Stock
|
||||||
|
- `_assert_reservation_status(order, expected_status)` - проверить резерв
|
||||||
|
- `_assert_sale_exists(order, should_exist)` - проверить Sale
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
```python
|
||||||
|
def test_06_my_new_scenario(self):
|
||||||
|
"""Описание сценария"""
|
||||||
|
with schema_context('test_order_status'):
|
||||||
|
order = self._create_order(self.status_draft, quantity=Decimal('10.00'))
|
||||||
|
|
||||||
|
# Ваши проверки
|
||||||
|
self._assert_stock_state(
|
||||||
|
available=Decimal('100.00'),
|
||||||
|
reserved=Decimal('10.00'),
|
||||||
|
free=Decimal('90.00')
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎓 Дополнительная информация
|
||||||
|
|
||||||
|
- Документация Django Testing: https://docs.djangoproject.com/en/stable/topics/testing/
|
||||||
|
- Django Tenants Testing: https://django-tenants.readthedocs.io/en/latest/test.html
|
||||||
1
myproject/inventory/tests/__init__.py
Normal file
1
myproject/inventory/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Inventory app tests
|
||||||
569
myproject/inventory/tests/test_order_status_transitions.py
Normal file
569
myproject/inventory/tests/test_order_status_transitions.py
Normal file
@@ -0,0 +1,569 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
КРИТИЧЕСКИЕ ТЕСТЫ: Переходы между статусами заказов и управление резервами
|
||||||
|
|
||||||
|
Тестируем 5 самых критичных сценариев:
|
||||||
|
1. draft → completed → cancelled → completed (танцы туда-сюда)
|
||||||
|
2. draft → cancelled (освобождение резервов при отмене из черновика)
|
||||||
|
3. cancelled → pending (резервирование при возврате из отмены)
|
||||||
|
4. Создание заказа с промежуточным статусом (не positive, не negative)
|
||||||
|
5. completed → draft (откат Sale и возврат в резервы)
|
||||||
|
|
||||||
|
Каждый тест проверяет:
|
||||||
|
- Корректность статусов резервов
|
||||||
|
- Корректность Stock (доступно, зарезервировано, свободно)
|
||||||
|
- Корректность StockBatch (количество не дублируется)
|
||||||
|
- Отсутствие дублирования Sale
|
||||||
|
- Корректность SaleBatchAllocation
|
||||||
|
"""
|
||||||
|
from django.test import TransactionTestCase
|
||||||
|
from django.db import connection
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django_tenants.utils import schema_context
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from tenants.models import Client, Domain
|
||||||
|
from products.models import Product, ProductCategory
|
||||||
|
from inventory.models import (
|
||||||
|
Warehouse, StockBatch, Stock, Reservation,
|
||||||
|
Sale, SaleBatchAllocation
|
||||||
|
)
|
||||||
|
from orders.models import Order, OrderItem, OrderStatus, PaymentMethod
|
||||||
|
from customers.models import Customer
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class OrderStatusTransitionCriticalTest(TransactionTestCase):
|
||||||
|
"""
|
||||||
|
Критические тесты переходов между статусами заказов.
|
||||||
|
|
||||||
|
TransactionTestCase используется для работы с тенантами и реальными транзакциями.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
"""Создаём тестовый тенант один раз для всех тестов"""
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
# Создаём тестовый тенант
|
||||||
|
cls.tenant = Client.objects.create(
|
||||||
|
schema_name='test_order_status',
|
||||||
|
name='Test Order Status Tenant',
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаём домен
|
||||||
|
Domain.objects.create(
|
||||||
|
domain='test_order_status.localhost',
|
||||||
|
tenant=cls.tenant,
|
||||||
|
is_primary=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
"""Удаляем тестовый тенант после всех тестов"""
|
||||||
|
# Удаляем схему
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(f'DROP SCHEMA IF EXISTS {cls.tenant.schema_name} CASCADE')
|
||||||
|
|
||||||
|
# Удаляем тенант
|
||||||
|
cls.tenant.delete()
|
||||||
|
|
||||||
|
super().tearDownClass()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Подготовка перед каждым тестом"""
|
||||||
|
with schema_context('test_order_status'):
|
||||||
|
# Создаём системные сущности
|
||||||
|
self._create_system_entities()
|
||||||
|
|
||||||
|
# Создаём тестовые данные
|
||||||
|
self._create_test_data()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Очистка после каждого теста"""
|
||||||
|
with schema_context('test_order_status'):
|
||||||
|
# Удаляем все тестовые данные
|
||||||
|
Sale.objects.all().delete()
|
||||||
|
SaleBatchAllocation.objects.all().delete()
|
||||||
|
Reservation.objects.all().delete()
|
||||||
|
OrderItem.objects.all().delete()
|
||||||
|
Order.objects.all().delete()
|
||||||
|
Stock.objects.all().delete()
|
||||||
|
StockBatch.objects.all().delete()
|
||||||
|
Product.objects.all().delete()
|
||||||
|
|
||||||
|
def _create_system_entities(self):
|
||||||
|
"""Создаёт системные сущности (статусы, способы оплаты, склад)"""
|
||||||
|
# Создаём статусы заказов
|
||||||
|
self.status_draft = OrderStatus.objects.create(
|
||||||
|
code='draft',
|
||||||
|
name='Черновик',
|
||||||
|
is_system=True,
|
||||||
|
is_positive_end=False,
|
||||||
|
is_negative_end=False,
|
||||||
|
color='secondary',
|
||||||
|
order=0
|
||||||
|
)
|
||||||
|
|
||||||
|
self.status_pending = OrderStatus.objects.create(
|
||||||
|
code='pending',
|
||||||
|
name='В сборке',
|
||||||
|
is_system=True,
|
||||||
|
is_positive_end=False,
|
||||||
|
is_negative_end=False,
|
||||||
|
color='warning',
|
||||||
|
order=1
|
||||||
|
)
|
||||||
|
|
||||||
|
self.status_completed = OrderStatus.objects.create(
|
||||||
|
code='completed',
|
||||||
|
name='Выполнен',
|
||||||
|
is_system=True,
|
||||||
|
is_positive_end=True,
|
||||||
|
is_negative_end=False,
|
||||||
|
color='success',
|
||||||
|
order=2
|
||||||
|
)
|
||||||
|
|
||||||
|
self.status_cancelled = OrderStatus.objects.create(
|
||||||
|
code='cancelled',
|
||||||
|
name='Отменен',
|
||||||
|
is_system=True,
|
||||||
|
is_positive_end=False,
|
||||||
|
is_negative_end=True,
|
||||||
|
color='danger',
|
||||||
|
order=3
|
||||||
|
)
|
||||||
|
|
||||||
|
# Промежуточный статус (не positive, не negative)
|
||||||
|
self.status_packing = OrderStatus.objects.create(
|
||||||
|
code='packing',
|
||||||
|
name='Упаковывается',
|
||||||
|
is_system=False,
|
||||||
|
is_positive_end=False,
|
||||||
|
is_negative_end=False,
|
||||||
|
color='info',
|
||||||
|
order=4
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаём способ оплаты
|
||||||
|
self.payment_method = PaymentMethod.objects.create(
|
||||||
|
code='cash',
|
||||||
|
name='Наличными',
|
||||||
|
is_system=True,
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаём склад
|
||||||
|
self.warehouse = Warehouse.objects.create(
|
||||||
|
name='Основной склад',
|
||||||
|
code='MAIN'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаём системного клиента
|
||||||
|
self.customer = Customer.objects.create(
|
||||||
|
name='Тестовый клиент',
|
||||||
|
phone='+375291111111',
|
||||||
|
is_system_customer=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_test_data(self):
|
||||||
|
"""Создаёт тестовые товары и партии"""
|
||||||
|
# Категория
|
||||||
|
category = ProductCategory.objects.create(
|
||||||
|
name='Тестовая категория',
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Товар
|
||||||
|
self.product = Product.objects.create(
|
||||||
|
name='Тестовый товар',
|
||||||
|
sku='TEST-001',
|
||||||
|
status='active',
|
||||||
|
category=category,
|
||||||
|
base_price=Decimal('10.00')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Партия товара (100 шт по 5.00 за шт)
|
||||||
|
self.stock_batch = StockBatch.objects.create(
|
||||||
|
product=self.product,
|
||||||
|
warehouse=self.warehouse,
|
||||||
|
quantity=Decimal('100.00'),
|
||||||
|
cost_price=Decimal('5.00')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stock создастся автоматически через сигнал
|
||||||
|
self.stock = Stock.objects.get(
|
||||||
|
product=self.product,
|
||||||
|
warehouse=self.warehouse
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_order(self, status, quantity=Decimal('10.00')):
|
||||||
|
"""Вспомогательный метод для создания заказа"""
|
||||||
|
order = Order.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
status=status,
|
||||||
|
payment_method=self.payment_method,
|
||||||
|
total_amount=quantity * self.product.base_price,
|
||||||
|
amount_paid=Decimal('0.00')
|
||||||
|
)
|
||||||
|
|
||||||
|
OrderItem.objects.create(
|
||||||
|
order=order,
|
||||||
|
product=self.product,
|
||||||
|
quantity=quantity,
|
||||||
|
price=self.product.base_price
|
||||||
|
)
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
|
def _assert_stock_state(self, available, reserved, free, msg_prefix=""):
|
||||||
|
"""Проверяет состояние Stock"""
|
||||||
|
self.stock.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.stock.quantity_available,
|
||||||
|
available,
|
||||||
|
f"{msg_prefix}Stock.quantity_available: ожидалось {available}, получено {self.stock.quantity_available}"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.stock.quantity_reserved,
|
||||||
|
reserved,
|
||||||
|
f"{msg_prefix}Stock.quantity_reserved: ожидалось {reserved}, получено {self.stock.quantity_reserved}"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.stock.quantity_free,
|
||||||
|
free,
|
||||||
|
f"{msg_prefix}Stock.quantity_free: ожидалось {free}, получено {self.stock.quantity_free}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _assert_reservation_status(self, order, expected_status, msg_prefix=""):
|
||||||
|
"""Проверяет статус резерва для заказа"""
|
||||||
|
reservation = Reservation.objects.filter(
|
||||||
|
order_item__order=order
|
||||||
|
).first()
|
||||||
|
|
||||||
|
self.assertIsNotNone(reservation, f"{msg_prefix}Резерв не найден")
|
||||||
|
self.assertEqual(
|
||||||
|
reservation.status,
|
||||||
|
expected_status,
|
||||||
|
f"{msg_prefix}Reservation.status: ожидался '{expected_status}', получен '{reservation.status}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
return reservation
|
||||||
|
|
||||||
|
def _assert_sale_exists(self, order, should_exist=True):
|
||||||
|
"""Проверяет наличие/отсутствие Sale для заказа"""
|
||||||
|
sale = Sale.objects.filter(order=order).first()
|
||||||
|
|
||||||
|
if should_exist:
|
||||||
|
self.assertIsNotNone(sale, "Sale должен существовать")
|
||||||
|
return sale
|
||||||
|
else:
|
||||||
|
self.assertIsNone(sale, "Sale НЕ должен существовать")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ==================== ТЕСТ 1: Танцы туда-сюда ====================
|
||||||
|
|
||||||
|
def test_01_draft_to_completed_to_cancelled_to_completed(self):
|
||||||
|
"""
|
||||||
|
КРИТИЧЕСКИЙ ТЕСТ #1: Множественные переходы между статусами
|
||||||
|
|
||||||
|
Сценарий:
|
||||||
|
1. draft (резервы created)
|
||||||
|
2. → completed (Sale created, товар списан)
|
||||||
|
3. → cancelled (Sale откатывается, резервы released)
|
||||||
|
4. → completed СНОВА (Sale создаётся заново, товар списан снова)
|
||||||
|
|
||||||
|
Проверяем:
|
||||||
|
- Нет дублирования Sale
|
||||||
|
- Корректность Stock на каждом шаге
|
||||||
|
- Корректность StockBatch (количество не дублируется)
|
||||||
|
"""
|
||||||
|
with schema_context('test_order_status'):
|
||||||
|
# ШАГ 1: Создаём заказ в draft
|
||||||
|
order = self._create_order(self.status_draft, quantity=Decimal('10.00'))
|
||||||
|
|
||||||
|
# Проверки после draft
|
||||||
|
self._assert_stock_state(
|
||||||
|
available=Decimal('100.00'),
|
||||||
|
reserved=Decimal('10.00'),
|
||||||
|
free=Decimal('90.00'),
|
||||||
|
msg_prefix="[DRAFT] "
|
||||||
|
)
|
||||||
|
self._assert_reservation_status(order, 'reserved', "[DRAFT] ")
|
||||||
|
self._assert_sale_exists(order, should_exist=False)
|
||||||
|
|
||||||
|
# ШАГ 2: Переход в completed
|
||||||
|
order.status = self.status_completed
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
# Проверки после completed
|
||||||
|
self._assert_stock_state(
|
||||||
|
available=Decimal('90.00'),
|
||||||
|
reserved=Decimal('0.00'),
|
||||||
|
free=Decimal('90.00'),
|
||||||
|
msg_prefix="[COMPLETED] "
|
||||||
|
)
|
||||||
|
self._assert_reservation_status(order, 'converted_to_sale', "[COMPLETED] ")
|
||||||
|
sale1 = self._assert_sale_exists(order, should_exist=True)
|
||||||
|
self.assertEqual(sale1.quantity, Decimal('10.00'))
|
||||||
|
|
||||||
|
# Проверяем StockBatch
|
||||||
|
self.stock_batch.refresh_from_db()
|
||||||
|
self.assertEqual(
|
||||||
|
self.stock_batch.quantity,
|
||||||
|
Decimal('90.00'),
|
||||||
|
"[COMPLETED] StockBatch должен уменьшиться на 10"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ШАГ 3: Переход в cancelled
|
||||||
|
order.status = self.status_cancelled
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
# Проверки после cancelled
|
||||||
|
self._assert_stock_state(
|
||||||
|
available=Decimal('100.00'),
|
||||||
|
reserved=Decimal('0.00'),
|
||||||
|
free=Decimal('100.00'),
|
||||||
|
msg_prefix="[CANCELLED] "
|
||||||
|
)
|
||||||
|
self._assert_reservation_status(order, 'released', "[CANCELLED] ")
|
||||||
|
self._assert_sale_exists(order, should_exist=False)
|
||||||
|
|
||||||
|
# Проверяем StockBatch восстановился
|
||||||
|
self.stock_batch.refresh_from_db()
|
||||||
|
self.assertEqual(
|
||||||
|
self.stock_batch.quantity,
|
||||||
|
Decimal('100.00'),
|
||||||
|
"[CANCELLED] StockBatch должен восстановиться"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ШАГ 4: Переход обратно в completed
|
||||||
|
order.status = self.status_completed
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
# Проверки после повторного completed
|
||||||
|
self._assert_stock_state(
|
||||||
|
available=Decimal('90.00'),
|
||||||
|
reserved=Decimal('0.00'),
|
||||||
|
free=Decimal('90.00'),
|
||||||
|
msg_prefix="[COMPLETED AGAIN] "
|
||||||
|
)
|
||||||
|
self._assert_reservation_status(order, 'converted_to_sale', "[COMPLETED AGAIN] ")
|
||||||
|
|
||||||
|
# КРИТИЧЕСКАЯ ПРОВЕРКА: Sale должен быть ОДИН (не дублируется)
|
||||||
|
sales_count = Sale.objects.filter(order=order).count()
|
||||||
|
self.assertEqual(
|
||||||
|
sales_count,
|
||||||
|
1,
|
||||||
|
f"[COMPLETED AGAIN] Должен быть ОДИН Sale, найдено {sales_count}"
|
||||||
|
)
|
||||||
|
|
||||||
|
sale2 = Sale.objects.get(order=order)
|
||||||
|
self.assertEqual(sale2.quantity, Decimal('10.00'))
|
||||||
|
|
||||||
|
# Проверяем StockBatch снова уменьшился
|
||||||
|
self.stock_batch.refresh_from_db()
|
||||||
|
self.assertEqual(
|
||||||
|
self.stock_batch.quantity,
|
||||||
|
Decimal('90.00'),
|
||||||
|
"[COMPLETED AGAIN] StockBatch должен снова уменьшиться"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== ТЕСТ 2: draft → cancelled ====================
|
||||||
|
|
||||||
|
def test_02_draft_to_cancelled_releases_reservations(self):
|
||||||
|
"""
|
||||||
|
КРИТИЧЕСКИЙ ТЕСТ #2: Отмена из черновика освобождает резервы
|
||||||
|
|
||||||
|
Сценарий:
|
||||||
|
1. draft (резервы created)
|
||||||
|
2. → cancelled (резервы должны освободиться)
|
||||||
|
|
||||||
|
Проверяем:
|
||||||
|
- Резервы переходят в 'released'
|
||||||
|
- Stock корректно обновляется
|
||||||
|
- Товар не списывается
|
||||||
|
"""
|
||||||
|
with schema_context('test_order_status'):
|
||||||
|
# ШАГ 1: Создаём заказ в draft
|
||||||
|
order = self._create_order(self.status_draft, quantity=Decimal('15.00'))
|
||||||
|
|
||||||
|
# Проверки после draft
|
||||||
|
self._assert_stock_state(
|
||||||
|
available=Decimal('100.00'),
|
||||||
|
reserved=Decimal('15.00'),
|
||||||
|
free=Decimal('85.00'),
|
||||||
|
msg_prefix="[DRAFT] "
|
||||||
|
)
|
||||||
|
|
||||||
|
# ШАГ 2: Переход в cancelled
|
||||||
|
order.status = self.status_cancelled
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
# КРИТИЧЕСКИЕ ПРОВЕРКИ
|
||||||
|
self._assert_stock_state(
|
||||||
|
available=Decimal('100.00'),
|
||||||
|
reserved=Decimal('0.00'),
|
||||||
|
free=Decimal('100.00'),
|
||||||
|
msg_prefix="[CANCELLED] "
|
||||||
|
)
|
||||||
|
self._assert_reservation_status(order, 'released', "[CANCELLED] ")
|
||||||
|
self._assert_sale_exists(order, should_exist=False)
|
||||||
|
|
||||||
|
# StockBatch не должен изменяться
|
||||||
|
self.stock_batch.refresh_from_db()
|
||||||
|
self.assertEqual(
|
||||||
|
self.stock_batch.quantity,
|
||||||
|
Decimal('100.00'),
|
||||||
|
"[CANCELLED] StockBatch НЕ должен изменяться при отмене из draft"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== ТЕСТ 3: cancelled → pending ====================
|
||||||
|
|
||||||
|
def test_03_cancelled_to_pending_reserves_stock(self):
|
||||||
|
"""
|
||||||
|
КРИТИЧЕСКИЙ ТЕСТ #3: Возврат из отмены резервирует товар
|
||||||
|
|
||||||
|
Сценарий:
|
||||||
|
1. draft (резервы created)
|
||||||
|
2. → cancelled (резервы released)
|
||||||
|
3. → pending (резервы должны вернуться в 'reserved')
|
||||||
|
|
||||||
|
Проверяем:
|
||||||
|
- Резервы переходят обратно в 'reserved'
|
||||||
|
- Stock корректно резервирует товар
|
||||||
|
"""
|
||||||
|
with schema_context('test_order_status'):
|
||||||
|
# ШАГ 1: Создаём заказ и переводим в cancelled
|
||||||
|
order = self._create_order(self.status_draft, quantity=Decimal('20.00'))
|
||||||
|
order.status = self.status_cancelled
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
# Проверяем cancelled
|
||||||
|
self._assert_stock_state(
|
||||||
|
available=Decimal('100.00'),
|
||||||
|
reserved=Decimal('0.00'),
|
||||||
|
free=Decimal('100.00'),
|
||||||
|
msg_prefix="[CANCELLED] "
|
||||||
|
)
|
||||||
|
self._assert_reservation_status(order, 'released', "[CANCELLED] ")
|
||||||
|
|
||||||
|
# ШАГ 2: Переход в pending (В сборке)
|
||||||
|
order.status = self.status_pending
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
# КРИТИЧЕСКИЕ ПРОВЕРКИ
|
||||||
|
self._assert_stock_state(
|
||||||
|
available=Decimal('100.00'),
|
||||||
|
reserved=Decimal('20.00'),
|
||||||
|
free=Decimal('80.00'),
|
||||||
|
msg_prefix="[PENDING] "
|
||||||
|
)
|
||||||
|
self._assert_reservation_status(order, 'reserved', "[PENDING] ")
|
||||||
|
self._assert_sale_exists(order, should_exist=False)
|
||||||
|
|
||||||
|
# ==================== ТЕСТ 4: Промежуточный статус ====================
|
||||||
|
|
||||||
|
def test_04_create_order_with_intermediate_status(self):
|
||||||
|
"""
|
||||||
|
КРИТИЧЕСКИЙ ТЕСТ #4: Создание заказа с промежуточным статусом
|
||||||
|
|
||||||
|
Сценарий:
|
||||||
|
1. Создаём заказ СРАЗУ со статусом "Упаковывается"
|
||||||
|
(is_positive_end=False, is_negative_end=False)
|
||||||
|
|
||||||
|
Проверяем:
|
||||||
|
- Резервы создаются корректно
|
||||||
|
- Stock резервируется
|
||||||
|
- Sale НЕ создаётся
|
||||||
|
"""
|
||||||
|
with schema_context('test_order_status'):
|
||||||
|
# Создаём заказ сразу со статусом "Упаковывается"
|
||||||
|
order = self._create_order(self.status_packing, quantity=Decimal('12.00'))
|
||||||
|
|
||||||
|
# КРИТИЧЕСКИЕ ПРОВЕРКИ
|
||||||
|
self._assert_stock_state(
|
||||||
|
available=Decimal('100.00'),
|
||||||
|
reserved=Decimal('12.00'),
|
||||||
|
free=Decimal('88.00'),
|
||||||
|
msg_prefix="[PACKING] "
|
||||||
|
)
|
||||||
|
self._assert_reservation_status(order, 'reserved', "[PACKING] ")
|
||||||
|
self._assert_sale_exists(order, should_exist=False)
|
||||||
|
|
||||||
|
# Проверяем что можем перейти в completed
|
||||||
|
order.status = self.status_completed
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
self._assert_stock_state(
|
||||||
|
available=Decimal('88.00'),
|
||||||
|
reserved=Decimal('0.00'),
|
||||||
|
free=Decimal('88.00'),
|
||||||
|
msg_prefix="[PACKING→COMPLETED] "
|
||||||
|
)
|
||||||
|
self._assert_reservation_status(order, 'converted_to_sale', "[PACKING→COMPLETED] ")
|
||||||
|
self._assert_sale_exists(order, should_exist=True)
|
||||||
|
|
||||||
|
# ==================== ТЕСТ 5: completed → draft ====================
|
||||||
|
|
||||||
|
def test_05_completed_to_draft_rollback_sale(self):
|
||||||
|
"""
|
||||||
|
КРИТИЧЕСКИЙ ТЕСТ #5: Откат от completed к draft
|
||||||
|
|
||||||
|
Сценарий:
|
||||||
|
1. draft (резервы created)
|
||||||
|
2. → completed (Sale created)
|
||||||
|
3. → draft (Sale откатывается, резервы восстанавливаются)
|
||||||
|
|
||||||
|
Проверяем:
|
||||||
|
- Sale удаляется
|
||||||
|
- Резервы возвращаются в 'reserved'
|
||||||
|
- Stock корректно восстанавливается
|
||||||
|
- StockBatch восстанавливается
|
||||||
|
"""
|
||||||
|
with schema_context('test_order_status'):
|
||||||
|
# ШАГ 1: Создаём заказ и переводим в completed
|
||||||
|
order = self._create_order(self.status_draft, quantity=Decimal('25.00'))
|
||||||
|
order.status = self.status_completed
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
# Проверяем completed
|
||||||
|
self._assert_stock_state(
|
||||||
|
available=Decimal('75.00'),
|
||||||
|
reserved=Decimal('0.00'),
|
||||||
|
free=Decimal('75.00'),
|
||||||
|
msg_prefix="[COMPLETED] "
|
||||||
|
)
|
||||||
|
self._assert_reservation_status(order, 'converted_to_sale', "[COMPLETED] ")
|
||||||
|
self._assert_sale_exists(order, should_exist=True)
|
||||||
|
|
||||||
|
self.stock_batch.refresh_from_db()
|
||||||
|
batch_qty_after_sale = self.stock_batch.quantity
|
||||||
|
self.assertEqual(batch_qty_after_sale, Decimal('75.00'))
|
||||||
|
|
||||||
|
# ШАГ 2: Откат в draft
|
||||||
|
order.status = self.status_draft
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
# КРИТИЧЕСКИЕ ПРОВЕРКИ
|
||||||
|
self._assert_stock_state(
|
||||||
|
available=Decimal('100.00'),
|
||||||
|
reserved=Decimal('25.00'),
|
||||||
|
free=Decimal('75.00'),
|
||||||
|
msg_prefix="[DRAFT ROLLBACK] "
|
||||||
|
)
|
||||||
|
self._assert_reservation_status(order, 'reserved', "[DRAFT ROLLBACK] ")
|
||||||
|
self._assert_sale_exists(order, should_exist=False)
|
||||||
|
|
||||||
|
# Проверяем восстановление StockBatch
|
||||||
|
self.stock_batch.refresh_from_db()
|
||||||
|
self.assertEqual(
|
||||||
|
self.stock_batch.quantity,
|
||||||
|
Decimal('100.00'),
|
||||||
|
"[DRAFT ROLLBACK] StockBatch должен восстановиться до исходного значения"
|
||||||
|
)
|
||||||
10
run_status_tests.bat
Normal file
10
run_status_tests.bat
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
@echo off
|
||||||
|
REM Скрипт для запуска тестов переходов между статусами заказов
|
||||||
|
|
||||||
|
cd /d C:\Users\team_\Desktop\test_qwen
|
||||||
|
call .venv\Scripts\activate.bat
|
||||||
|
|
||||||
|
cd myproject
|
||||||
|
python manage.py test inventory.tests.test_order_status_transitions --verbosity=2
|
||||||
|
|
||||||
|
pause
|
||||||
Reference in New Issue
Block a user