Files
octopus/myproject/test_order_status_default.py
Andrey Smakotin e0437cdb5a Исправлено двойное списание товаров при смене статуса заказа
Проблема:
- При изменении статуса заказа на 'Выполнен' товар списывался дважды
- Заказ на 10 шт создавал Sale на 10 шт, но со склада уходило 20 шт

Найдено ДВЕ причины:

1. Повторное обновление резервов через .save() (inventory/signals.py)
   - Резервы обновлялись через res.save() каждый раз при сохранении заказа
   - Это вызывало сигнал update_stock_on_reservation_change
   - При повторном сохранении заказа происходило двойное срабатывание

   Решение:
   - Проверка дубликатов ПЕРЕД обновлением резервов
   - Замена .save() на .update() для массового обновления без вызова сигналов
   - Ручное обновление Stock после .update()

2. Двойное FIFO-списание (inventory/services/sale_processor.py)
   - Sale создавалась с processed=False
   - Сигнал process_sale_fifo срабатывал и списывал товар (1-й раз)
   - Затем SaleProcessor.create_sale() тоже списывал товар (2-й раз)

   Решение:
   - Sale создаётся сразу с processed=True
   - Сигнал не срабатывает, списание только в сервисе

Дополнительно:
- Ограничен выбор статусов при создании заказа только промежуточными
- Статус 'Черновик' установлен по умолчанию
- Убран пустой выбор '-------' из поля статуса

Изменённые файлы:
- myproject/orders/forms.py - настройки статусов для формы заказа
- myproject/inventory/signals.py - исправление сигнала create_sale_on_order_completion
- myproject/inventory/services/sale_processor.py - исправление create_sale
- myproject/test_order_status_default.py - обновлён тест
- DOUBLE_SALE_FIX.md - документация по исправлению
2025-12-01 00:56:26 +03:00

126 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Тест для проверки настройки статусов при создании и редактировании заказа.
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django.setup()
from orders.models import OrderStatus, Order
from orders.forms import OrderForm
def test_default_status():
"""Проверяет настройки статусов для новых и существующих заказов"""
print("\n=== ТЕСТ: Настройки статусов заказов ===\n")
# Проверяем наличие статуса "Черновик"
try:
draft_status = OrderStatus.objects.get(code='draft', is_system=True)
print(f"✓ Статус 'Черновик' найден: {draft_status.name} (ID: {draft_status.pk})")
except OrderStatus.DoesNotExist:
print("✗ ОШИБКА: Статус 'draft' не найден в БД!")
print(" Создайте системные статусы командой:")
print(" python manage.py shell -c \"from orders.services.order_status_service import OrderStatusService; OrderStatusService.create_default_statuses()\"")
return False
# Проверяем финальные статусы
final_positive = OrderStatus.objects.filter(is_positive_end=True)
final_negative = OrderStatus.objects.filter(is_negative_end=True)
intermediate = OrderStatus.objects.filter(is_positive_end=False, is_negative_end=False)
print(f"\n📊 Статистика статусов:")
print(f" - Промежуточные (доступны при создании): {intermediate.count()}")
print(f" - Положительные финальные: {final_positive.count()}")
print(f" - Отрицательные финальные: {final_negative.count()}")
print(f"\n📋 Промежуточные статусы:")
for status in intermediate.order_by('order'):
print(f" - {status.name} ({status.code})")
print(f"\n🚫 Финальные статусы (недоступны при создании):")
for status in final_positive:
print(f" - {status.name} ({status.code}) [положительный]")
for status in final_negative:
print(f" - {status.name} ({status.code}) [отрицательный]")
# === ТЕСТ 1: Создание нового заказа ===
print("\n=== ТЕСТ 1: Создание нового заказа ===\n")
form_new = OrderForm()
status_field = form_new.fields['status']
# Проверяем начальное значение
initial_value = status_field.initial
print(f"✓ Начальное значение поля 'status': {initial_value}")
print(f"✓ Ожидаемое значение (ID статуса 'Черновик'): {draft_status.pk}")
if initial_value == draft_status.pk:
print("✓ УСПЕХ: Статус 'Черновик' установлен по умолчанию!")
else:
print(f"✗ ОШИБКА: Начальное значение {initial_value} != {draft_status.pk}")
return False
# Проверяем queryset (только промежуточные статусы)
available_statuses = list(status_field.queryset)
print(f"\n✓ Доступно статусов при создании: {len(available_statuses)}")
has_final_positive = any(s.is_positive_end for s in available_statuses)
has_final_negative = any(s.is_negative_end for s in available_statuses)
if not has_final_positive and not has_final_negative:
print("✓ УСПЕХ: Финальные статусы исключены из выбора!")
else:
print("✗ ОШИБКА: В списке есть финальные статусы!")
return False
# Проверяем, что поле обязательно
if status_field.required:
print("✓ Поле 'status' обязательно (required=True)")
else:
print("✗ ОШИБКА: Поле 'status' не обязательно!")
return False
# Проверяем, что пустой выбор убран
if status_field.empty_label is None:
print("✓ Пустой выбор '-------' убран (empty_label=None)")
else:
print(f"✗ ОШИБКА: Пустой выбор не убран! empty_label={status_field.empty_label}")
return False
# === ТЕСТ 2: Редактирование существующего заказа ===
print("\n=== ТЕСТ 2: Редактирование существующего заказа ===\n")
# Создаем мок-объект заказа с pk (чтобы форма считала его существующим)
class MockOrder:
pk = 999
status = draft_status
customer = None
delivery_address = None
form_edit = OrderForm(instance=MockOrder())
status_field_edit = form_edit.fields['status']
# Проверяем queryset (все статусы)
all_statuses = list(status_field_edit.queryset)
print(f"✓ Доступно статусов при редактировании: {len(all_statuses)}")
has_final_in_edit = any(s.is_positive_end or s.is_negative_end for s in all_statuses)
if has_final_in_edit:
print("✓ УСПЕХ: При редактировании доступны ВСЕ статусы (включая финальные)!")
else:
print("⚠ ВНИМАНИЕ: При редактировании финальные статусы отсутствуют")
print("\n=== ВСЕ ТЕСТЫ ПРОЙДЕНЫ! ===\n")
return True
if __name__ == '__main__':
success = test_default_status()
exit(0 if success else 1)