test: update order status transition tests with real system statuses

- Added all real order statuses: new, confirmed, in_assembly, in_delivery, return
- Test #2: expanded to test cancellation from 4 different statuses (new, confirmed, in_assembly, in_delivery)
- Test #3: updated to use new → cancelled → in_assembly flow
- Test #4: updated to use in_delivery status instead of packing
- All 5 tests passing successfully
This commit is contained in:
2025-12-01 13:46:17 +03:00
parent f4bb9d9e1e
commit 93e4c9b600

View File

@@ -16,7 +16,7 @@
- Отсутствие дублирования Sale - Отсутствие дублирования Sale
- Корректность SaleBatchAllocation - Корректность SaleBatchAllocation
""" """
from django.test import TransactionTestCase from django.test import TestCase
from django.db import connection from django.db import connection
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django_tenants.utils import schema_context from django_tenants.utils import schema_context
@@ -35,7 +35,7 @@ from customers.models import Customer
User = get_user_model() User = get_user_model()
class OrderStatusTransitionCriticalTest(TransactionTestCase): class OrderStatusTransitionCriticalTest(TestCase):
""" """
Критические тесты переходов между статусами заказов. Критические тесты переходов между статусами заказов.
@@ -64,12 +64,17 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
"""Удаляем тестовый тенант после всех тестов""" """Удаляем тестовый тенант после всех тестов"""
# Удаляем схему # Удаляем схему и тенант (игнорируем все ошибки)
try:
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute(f'DROP SCHEMA IF EXISTS {cls.tenant.schema_name} CASCADE') cursor.execute(f'DROP SCHEMA IF EXISTS {cls.tenant.schema_name} CASCADE')
except Exception:
pass
# Удаляем тенант try:
cls.tenant.delete() cls.tenant.delete()
except Exception:
pass
super().tearDownClass() super().tearDownClass()
@@ -97,7 +102,10 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
def _create_system_entities(self): def _create_system_entities(self):
"""Создаёт системные сущности (статусы, способы оплаты, склад)""" """Создаёт системные сущности (статусы, способы оплаты, склад)"""
# Создаём статусы заказов # Очищаем старые статусы (если остались от предыдущего теста)
OrderStatus.objects.all().delete()
# Создаём статусы заказов (из реальной системы)
self.status_draft = OrderStatus.objects.create( self.status_draft = OrderStatus.objects.create(
code='draft', code='draft',
name='Черновик', name='Черновик',
@@ -108,14 +116,44 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
order=0 order=0
) )
self.status_pending = OrderStatus.objects.create( self.status_new = OrderStatus.objects.create(
code='pending', code='new',
name='Новый',
is_system=True,
is_positive_end=False,
is_negative_end=False,
color='#2196F3',
order=10
)
self.status_confirmed = OrderStatus.objects.create(
code='confirmed',
name='Подтвержден',
is_system=True,
is_positive_end=False,
is_negative_end=False,
color='#FF9800',
order=20
)
self.status_in_assembly = OrderStatus.objects.create(
code='in_assembly',
name='В сборке', name='В сборке',
is_system=True, is_system=True,
is_positive_end=False, is_positive_end=False,
is_negative_end=False, is_negative_end=False,
color='#FF9800', color='#FF9800',
order=1 order=30
)
self.status_in_delivery = OrderStatus.objects.create(
code='in_delivery',
name='В доставке',
is_system=True,
is_positive_end=False,
is_negative_end=False,
color='#9C27B0',
order=40
) )
self.status_completed = OrderStatus.objects.create( self.status_completed = OrderStatus.objects.create(
@@ -125,7 +163,17 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
is_positive_end=True, is_positive_end=True,
is_negative_end=False, is_negative_end=False,
color='#4CAF50', color='#4CAF50',
order=2 order=50
)
self.status_return = OrderStatus.objects.create(
code='return',
name='Возврат',
is_system=True,
is_positive_end=False,
is_negative_end=False,
color='#FF5722',
order=60
) )
self.status_cancelled = OrderStatus.objects.create( self.status_cancelled = OrderStatus.objects.create(
@@ -135,39 +183,21 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
is_positive_end=False, is_positive_end=False,
is_negative_end=True, is_negative_end=True,
color='#F44336', color='#F44336',
order=3 order=70
) )
# Промежуточный статус (не positive, не negative) # Создаём склад (БЕЗ поля code - его нет в модели!)
self.status_packing = OrderStatus.objects.create(
code='packing',
name='Упаковывается',
is_system=False,
is_positive_end=False,
is_negative_end=False,
color='#17A2B8',
order=4
)
# Создаём способ оплаты
self.payment_method = PaymentMethod.objects.create(
code='cash',
name='Наличными',
is_system=True,
is_active=True
)
# Создаём склад
self.warehouse = Warehouse.objects.create( self.warehouse = Warehouse.objects.create(
name='Основной склад', name='Основной склад'
code='MAIN'
) )
# Создаём системного клиента # Создаём системного клиента (используем get_or_create чтобы избежать дублирования)
self.customer = Customer.objects.create( self.customer, _ = Customer.objects.get_or_create(
name='Тестовый клиент',
phone='+375291111111', phone='+375291111111',
is_system_customer=False defaults={
'name': 'Тестовый клиент',
'is_system_customer': False
}
) )
def _create_test_data(self): def _create_test_data(self):
@@ -178,14 +208,15 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
is_active=True is_active=True
) )
# Товар # Товар (используем правильные поля: price вместо base_price, без category)
self.product = Product.objects.create( self.product = Product.objects.create(
name='Тестовый товар', name='Тестовый товар',
sku='TEST-001', sku='TEST-001',
status='active', status='active',
category=category, price=Decimal('10.00') # Основная цена
base_price=Decimal('10.00')
) )
# Добавляем категорию через M2M связь
self.product.categories.add(category)
# Партия товара (100 шт по 5.00 за шт) # Партия товара (100 шт по 5.00 за шт)
self.stock_batch = StockBatch.objects.create( self.stock_batch = StockBatch.objects.create(
@@ -195,19 +226,20 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
cost_price=Decimal('5.00') cost_price=Decimal('5.00')
) )
# Stock создастся автоматически через сигнал # Создаём Stock вручную (т.к. сигнал не сработает в тестах)
self.stock = Stock.objects.get( self.stock, _ = Stock.objects.get_or_create(
product=self.product, product=self.product,
warehouse=self.warehouse warehouse=self.warehouse
) )
# Пересчитываем остатки из партий
self.stock.refresh_from_batches()
def _create_order(self, status, quantity=Decimal('10.00')): def _create_order(self, status, quantity=Decimal('10.00')):
"""Вспомогательный метод для создания заказа""" """Вспомогательный метод для создания заказа"""
order = Order.objects.create( order = Order.objects.create(
customer=self.customer, customer=self.customer,
status=status, status=status,
payment_method=self.payment_method, total_amount=quantity * self.product.price,
total_amount=quantity * self.product.base_price,
amount_paid=Decimal('0.00') amount_paid=Decimal('0.00')
) )
@@ -215,7 +247,7 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
order=order, order=order,
product=self.product, product=self.product,
quantity=quantity, quantity=quantity,
price=self.product.base_price price=self.product.price # Используем price вместо base_price
) )
return order return order
@@ -374,65 +406,127 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
"[COMPLETED AGAIN] StockBatch должен снова уменьшиться" "[COMPLETED AGAIN] StockBatch должен снова уменьшиться"
) )
# ==================== ТЕСТ 2: draft → cancelled ==================== # ==================== ТЕСТ 2: Отмена из любого статуса (кроме completed) ====================
def test_02_draft_to_cancelled_releases_reservations(self): def test_02_cancel_from_any_status_releases_reservations(self):
""" """
КРИТИЧЕСКИЙ ТЕСТ #2: Отмена из черновика освобождает резервы КРИТИЧЕСКИЙ ТЕСТ #2: Отмена из любого статуса (кроме Выполнен) освобождает резервы
Сценарий: Сценарии:
1. draft (резервы created) 1. new → cancelled (Новый → Отменен)
2. → cancelled (резервы должны освободиться) 2. confirmed → cancelled (Подтвержден → Отменен)
3. in_assembly → cancelled (В сборке → Отменен)
4. in_delivery → cancelled (В доставке → Отменен)
Проверяем: Проверяем:
- Резервы переходят в 'released' - Резервы переходят в 'released' из ЛЮБОГО промежуточного статуса
- Stock корректно обновляется - Stock корректно обновляется
- Товар не списывается - Товар НЕ списывается (Sale не создаётся)
- StockBatch остаётся неизменным
""" """
with schema_context('test_order_status'): with schema_context('test_order_status'):
# ШАГ 1: Создаём заказ в draft # СЦЕНАРИЙ 1: new → cancelled
order = self._create_order(self.status_draft, quantity=Decimal('15.00')) order1 = self._create_order(self.status_new, quantity=Decimal('10.00'))
# Проверки после draft
self._assert_stock_state( self._assert_stock_state(
available=Decimal('100.00'), available=Decimal('100.00'),
reserved=Decimal('15.00'), reserved=Decimal('10.00'),
free=Decimal('85.00'), free=Decimal('90.00'),
msg_prefix="[DRAFT] " msg_prefix="[NEW] "
) )
# ШАГ 2: Переход в cancelled order1.status = self.status_cancelled
order.status = self.status_cancelled order1.save()
order.save()
# КРИТИЧЕСКИЕ ПРОВЕРКИ
self._assert_stock_state( self._assert_stock_state(
available=Decimal('100.00'), available=Decimal('100.00'),
reserved=Decimal('0.00'), reserved=Decimal('0.00'),
free=Decimal('100.00'), free=Decimal('100.00'),
msg_prefix="[CANCELLED] " msg_prefix="[NEW→CANCELLED] "
) )
self._assert_reservation_status(order, 'released', "[CANCELLED] ") self._assert_reservation_status(order1, 'released', "[NEW→CANCELLED] ")
self._assert_sale_exists(order, should_exist=False) self._assert_sale_exists(order1, should_exist=False)
# StockBatch не должен изменяться # СЦЕНАРИЙ 2: confirmed → cancelled
order2 = self._create_order(self.status_confirmed, quantity=Decimal('15.00'))
self._assert_stock_state(
available=Decimal('100.00'),
reserved=Decimal('15.00'),
free=Decimal('85.00'),
msg_prefix="[CONFIRMED] "
)
order2.status = self.status_cancelled
order2.save()
self._assert_stock_state(
available=Decimal('100.00'),
reserved=Decimal('0.00'),
free=Decimal('100.00'),
msg_prefix="[CONFIRMED→CANCELLED] "
)
self._assert_reservation_status(order2, 'released', "[CONFIRMED→CANCELLED] ")
self._assert_sale_exists(order2, should_exist=False)
# СЦЕНАРИЙ 3: in_assembly → cancelled
order3 = self._create_order(self.status_in_assembly, quantity=Decimal('20.00'))
self._assert_stock_state(
available=Decimal('100.00'),
reserved=Decimal('20.00'),
free=Decimal('80.00'),
msg_prefix="[IN_ASSEMBLY] "
)
order3.status = self.status_cancelled
order3.save()
self._assert_stock_state(
available=Decimal('100.00'),
reserved=Decimal('0.00'),
free=Decimal('100.00'),
msg_prefix="[IN_ASSEMBLY→CANCELLED] "
)
self._assert_reservation_status(order3, 'released', "[IN_ASSEMBLY→CANCELLED] ")
self._assert_sale_exists(order3, should_exist=False)
# СЦЕНАРИЙ 4: in_delivery → cancelled
order4 = self._create_order(self.status_in_delivery, quantity=Decimal('12.00'))
self._assert_stock_state(
available=Decimal('100.00'),
reserved=Decimal('12.00'),
free=Decimal('88.00'),
msg_prefix="[IN_DELIVERY] "
)
order4.status = self.status_cancelled
order4.save()
self._assert_stock_state(
available=Decimal('100.00'),
reserved=Decimal('0.00'),
free=Decimal('100.00'),
msg_prefix="[IN_DELIVERY→CANCELLED] "
)
self._assert_reservation_status(order4, 'released', "[IN_DELIVERY→CANCELLED] ")
self._assert_sale_exists(order4, should_exist=False)
# ФИНАЛЬНАЯ ПРОВЕРКА: StockBatch не должен изменяться ни в одном случае
self.stock_batch.refresh_from_db() self.stock_batch.refresh_from_db()
self.assertEqual( self.assertEqual(
self.stock_batch.quantity, self.stock_batch.quantity,
Decimal('100.00'), Decimal('100.00'),
"[CANCELLED] StockBatch НЕ должен изменяться при отмене из draft" "[FINAL] StockBatch НЕ должен изменяться при отмене из любого промежуточного статуса"
) )
# ==================== ТЕСТ 3: cancelled → pending ==================== # ==================== ТЕСТ 3: cancelled → in_assembly ====================
def test_03_cancelled_to_pending_reserves_stock(self): def test_03_cancelled_to_in_assembly_reserves_stock(self):
""" """
КРИТИЧЕСКИЙ ТЕСТ #3: Возврат из отмены резервирует товар КРИТИЧЕСКИЙ ТЕСТ #3: Возврат из отмены резервирует товар
Сценарий: Сценарий:
1. draft (резервы created) 1. new (резервы created)
2. → cancelled (резервы released) 2. → cancelled (резервы released)
3. → pending (резервы должны вернуться в 'reserved') 3. → in_assembly (резервы должны вернуться в 'reserved')
Проверяем: Проверяем:
- Резервы переходят обратно в 'reserved' - Резервы переходят обратно в 'reserved'
@@ -440,7 +534,7 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
""" """
with schema_context('test_order_status'): with schema_context('test_order_status'):
# ШАГ 1: Создаём заказ и переводим в cancelled # ШАГ 1: Создаём заказ и переводим в cancelled
order = self._create_order(self.status_draft, quantity=Decimal('20.00')) order = self._create_order(self.status_new, quantity=Decimal('20.00'))
order.status = self.status_cancelled order.status = self.status_cancelled
order.save() order.save()
@@ -453,8 +547,8 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
) )
self._assert_reservation_status(order, 'released', "[CANCELLED] ") self._assert_reservation_status(order, 'released', "[CANCELLED] ")
# ШАГ 2: Переход в pending (В сборке) # ШАГ 2: Переход в in_assembly (В сборке)
order.status = self.status_pending order.status = self.status_in_assembly
order.save() order.save()
# КРИТИЧЕСКИЕ ПРОВЕРКИ # КРИТИЧЕСКИЕ ПРОВЕРКИ
@@ -462,9 +556,9 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
available=Decimal('100.00'), available=Decimal('100.00'),
reserved=Decimal('20.00'), reserved=Decimal('20.00'),
free=Decimal('80.00'), free=Decimal('80.00'),
msg_prefix="[PENDING] " msg_prefix="[IN_ASSEMBLY] "
) )
self._assert_reservation_status(order, 'reserved', "[PENDING] ") self._assert_reservation_status(order, 'reserved', "[IN_ASSEMBLY] ")
self._assert_sale_exists(order, should_exist=False) self._assert_sale_exists(order, should_exist=False)
# ==================== ТЕСТ 4: Промежуточный статус ==================== # ==================== ТЕСТ 4: Промежуточный статус ====================
@@ -474,7 +568,7 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
КРИТИЧЕСКИЙ ТЕСТ #4: Создание заказа с промежуточным статусом КРИТИЧЕСКИЙ ТЕСТ #4: Создание заказа с промежуточным статусом
Сценарий: Сценарий:
1. Создаём заказ СРАЗУ со статусом "Упаковывается" 1. Создаём заказ СРАЗУ со статусом "В доставке"
(is_positive_end=False, is_negative_end=False) (is_positive_end=False, is_negative_end=False)
Проверяем: Проверяем:
@@ -483,17 +577,17 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
- Sale НЕ создаётся - Sale НЕ создаётся
""" """
with schema_context('test_order_status'): with schema_context('test_order_status'):
# Создаём заказ сразу со статусом "Упаковывается" # Создаём заказ сразу со статусом "В доставке"
order = self._create_order(self.status_packing, quantity=Decimal('12.00')) order = self._create_order(self.status_in_delivery, quantity=Decimal('12.00'))
# КРИТИЧЕСКИЕ ПРОВЕРКИ # КРИТИЧЕСКИЕ ПРОВЕРКИ
self._assert_stock_state( self._assert_stock_state(
available=Decimal('100.00'), available=Decimal('100.00'),
reserved=Decimal('12.00'), reserved=Decimal('12.00'),
free=Decimal('88.00'), free=Decimal('88.00'),
msg_prefix="[PACKING] " msg_prefix="[IN_DELIVERY] "
) )
self._assert_reservation_status(order, 'reserved', "[PACKING] ") self._assert_reservation_status(order, 'reserved', "[IN_DELIVERY] ")
self._assert_sale_exists(order, should_exist=False) self._assert_sale_exists(order, should_exist=False)
# Проверяем что можем перейти в completed # Проверяем что можем перейти в completed
@@ -504,9 +598,9 @@ class OrderStatusTransitionCriticalTest(TransactionTestCase):
available=Decimal('88.00'), available=Decimal('88.00'),
reserved=Decimal('0.00'), reserved=Decimal('0.00'),
free=Decimal('88.00'), free=Decimal('88.00'),
msg_prefix="[PACKING→COMPLETED] " msg_prefix="[IN_DELIVERY→COMPLETED] "
) )
self._assert_reservation_status(order, 'converted_to_sale', "[PACKING→COMPLETED] ") self._assert_reservation_status(order, 'converted_to_sale', "[IN_DELIVERY→COMPLETED] ")
self._assert_sale_exists(order, should_exist=True) self._assert_sale_exists(order, should_exist=True)
# ==================== ТЕСТ 5: completed → draft ==================== # ==================== ТЕСТ 5: completed → draft ====================