Улучшения в тестах переходов статусов заказов

- Исправлены комментарии и форматирование в signals.py
- Улучшена читаемость кода в models.py
- Обновлены шаблоны форм статусов
- Доработаны тесты переходов статусов
This commit is contained in:
2026-01-05 21:30:25 +03:00
parent 70f0e4fb4c
commit 2aba3d2404
5 changed files with 400 additions and 121 deletions

View File

@@ -902,3 +902,238 @@ class OrderStatusTransitionCriticalTest(TestCase):
# Проверяем, что is_returned False (есть Sale)
order.refresh_from_db()
self.assertFalse(order.is_returned, "[COMPLETED AGAIN] is_returned должен быть False при наличии Sale")
# ==================== ТЕСТ 9: Хаотичная смена статусов (стресс-тест) ====================
def test_09_random_status_changes_stress_test(self):
"""
ТЕСТ #9: Хаотичная смена статусов заказа в разные стороны
Сценарий (симулирует реальное поведение пользователя):
1. draft → completed (продажа)
2. completed → draft (откат)
3. draft → cancelled (отмена)
4. cancelled → in_assembly (возобновление)
5. in_assembly → completed (продажа снова)
6. completed → cancelled (отмена выполненного)
7. cancelled → completed (автоматический промежуточный переход через draft)
8. completed → draft → cancelled (откат и финальная отмена)
Проверяем на каждом шаге:
- Консистентность резервов
- Консистентность Stock (quantity_available, quantity_reserved, quantity_free)
- Отсутствие дублирования Sale
- Корректность восстановления партий
- Корректность флага is_returned
"""
with schema_context('test_order_status'):
# Создаём заказ в draft
order = self._create_order(self.status_draft, quantity=Decimal('10.00'))
# ========== ШАГ 1: draft → completed ==========
order.status = self.status_completed
order.save()
order.refresh_from_db()
# Проверки после первой продажи
self._assert_stock_state(
available=Decimal('90.00'),
reserved=Decimal('0.00'),
free=Decimal('90.00'),
msg_prefix="[STEP 1: draft→completed] "
)
self._assert_reservation_status(order, 'converted_to_sale', "[STEP 1] ")
self._assert_sale_exists(order, should_exist=True)
self.assertFalse(order.is_returned, "[STEP 1] is_returned должен быть False после продажи")
# Проверяем партии
self.stock_batch.refresh_from_db()
self.assertEqual(
self.stock_batch.quantity,
Decimal('90.00'),
"[STEP 1] Партия должна уменьшиться на 10"
)
# ========== ШАГ 2: completed → draft ==========
order.status = self.status_draft
order.save()
order.refresh_from_db()
# Проверки после отката
self._assert_stock_state(
available=Decimal('100.00'),
reserved=Decimal('10.00'),
free=Decimal('90.00'),
msg_prefix="[STEP 2: completed→draft] "
)
self._assert_reservation_status(order, 'reserved', "[STEP 2] ")
self._assert_sale_exists(order, should_exist=False)
self.assertTrue(order.is_returned, "[STEP 2] is_returned должен быть True (был completed, но Sale удалены)")
# Партии должны восстановиться
self.stock_batch.refresh_from_db()
self.assertEqual(
self.stock_batch.quantity,
Decimal('100.00'),
"[STEP 2] Партия должна восстановиться"
)
# ========== ШАГ 3: draft → cancelled ==========
order.status = self.status_cancelled
order.save()
order.refresh_from_db()
# Проверки после отмены
self._assert_stock_state(
available=Decimal('100.00'),
reserved=Decimal('0.00'),
free=Decimal('100.00'),
msg_prefix="[STEP 3: draft→cancelled] "
)
self._assert_reservation_status(order, 'released', "[STEP 3] ")
self._assert_sale_exists(order, should_exist=False)
self.assertTrue(order.is_returned, "[STEP 3] is_returned остается True")
# ========== ШАГ 4: cancelled → in_assembly ==========
order.status = self.status_in_assembly
order.save()
order.refresh_from_db()
# Проверки после возобновления
self._assert_stock_state(
available=Decimal('100.00'),
reserved=Decimal('10.00'),
free=Decimal('90.00'),
msg_prefix="[STEP 4: cancelled→in_assembly] "
)
self._assert_reservation_status(order, 'reserved', "[STEP 4] ")
self._assert_sale_exists(order, should_exist=False)
self.assertTrue(order.is_returned, "[STEP 4] is_returned остается True (Sale еще нет)")
# ========== ШАГ 5: in_assembly → completed ==========
order.status = self.status_completed
order.save()
order.refresh_from_db()
# Проверки после второй продажи
self._assert_stock_state(
available=Decimal('90.00'),
reserved=Decimal('0.00'),
free=Decimal('90.00'),
msg_prefix="[STEP 5: in_assembly→completed] "
)
self._assert_reservation_status(order, 'converted_to_sale', "[STEP 5] ")
self._assert_sale_exists(order, should_exist=True)
self.assertFalse(order.is_returned, "[STEP 5] is_returned должен стать False (Sale созданы)")
# Проверяем, что НЕТ дублей Sale
sales_count = Sale.objects.filter(order=order).count()
self.assertEqual(sales_count, 1, "[STEP 5] Должна быть ровно 1 Sale (нет дублей)")
self.stock_batch.refresh_from_db()
self.assertEqual(
self.stock_batch.quantity,
Decimal('90.00'),
"[STEP 5] Партия должна снова уменьшиться на 10"
)
# ========== ШАГ 6: completed → cancelled ==========
order.status = self.status_cancelled
order.save()
order.refresh_from_db()
# Проверки после отмены выполненного
self._assert_stock_state(
available=Decimal('100.00'),
reserved=Decimal('0.00'),
free=Decimal('100.00'),
msg_prefix="[STEP 6: completed→cancelled] "
)
self._assert_reservation_status(order, 'released', "[STEP 6] ")
self._assert_sale_exists(order, should_exist=False)
self.assertTrue(order.is_returned, "[STEP 6] is_returned должен быть True (был completed, Sale удалены)")
self.stock_batch.refresh_from_db()
self.assertEqual(
self.stock_batch.quantity,
Decimal('100.00'),
"[STEP 6] Партия должна восстановиться после отмены"
)
# ========== ШАГ 7: cancelled → completed (автоматический промежуточный переход) ==========
# Должен произойти: cancelled → draft → completed
order.status = self.status_completed
order.save()
order.refresh_from_db()
# Проверяем, что прошли через draft (автоматический промежуточный переход)
history = order.history.all()
self.assertGreaterEqual(history.count(), 2, "[STEP 7] Должна быть история переходов")
# Проверки после автоматического перехода
self._assert_stock_state(
available=Decimal('90.00'),
reserved=Decimal('0.00'),
free=Decimal('90.00'),
msg_prefix="[STEP 7: cancelled→completed auto] "
)
self._assert_reservation_status(order, 'converted_to_sale', "[STEP 7] ")
self._assert_sale_exists(order, should_exist=True)
self.assertFalse(order.is_returned, "[STEP 7] is_returned должен быть False")
# Проверяем, что НЕТ дублей Sale после автоматического перехода
sales_count = Sale.objects.filter(order=order).count()
self.assertEqual(sales_count, 1, "[STEP 7] Должна быть ровно 1 Sale (нет дублей после auto-transition)")
# ========== ШАГ 8: completed → draft → cancelled ==========
order.status = self.status_draft
order.save()
order.refresh_from_db()
# Промежуточная проверка в draft
self._assert_stock_state(
available=Decimal('100.00'),
reserved=Decimal('10.00'),
free=Decimal('90.00'),
msg_prefix="[STEP 8a: completed→draft] "
)
self._assert_reservation_status(order, 'reserved', "[STEP 8a] ")
self._assert_sale_exists(order, should_exist=False)
# Финальная отмена
order.status = self.status_cancelled
order.save()
order.refresh_from_db()
# ========== ФИНАЛЬНЫЕ ПРОВЕРКИ ==========
self._assert_stock_state(
available=Decimal('100.00'),
reserved=Decimal('0.00'),
free=Decimal('100.00'),
msg_prefix="[FINAL: cancelled] "
)
self._assert_reservation_status(order, 'released', "[FINAL] ")
self._assert_sale_exists(order, should_exist=False)
self.assertTrue(order.is_returned, "[FINAL] is_returned должен быть True в финале")
# Проверяем, что все партии восстановлены до исходного состояния
self.stock_batch.refresh_from_db()
self.assertEqual(
self.stock_batch.quantity,
Decimal('100.00'),
"[FINAL] Партия должна полностью восстановиться до начального значения"
)
# Проверяем, что НЕТ активных Sale
sales = Sale.objects.filter(order=order)
self.assertEqual(sales.count(), 0, "[FINAL] Не должно быть активных Sale после финальной отмены")
# Проверяем общую консистентность данных
# Все резервы должны быть в released
reservations = Reservation.objects.filter(order_item__order=order)
for res in reservations:
self.assertEqual(
res.status,
'released',
f"[FINAL] Все резервы должны быть в 'released', но нашли {res.status}"
)