Исправлено двойное списание товаров при смене статуса заказа

Проблема:
- При изменении статуса заказа на 'Выполнен' товар списывался дважды
- Заказ на 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 - документация по исправлению
This commit is contained in:
2025-12-01 00:56:26 +03:00
parent 4e66f03957
commit e0437cdb5a
8 changed files with 1284 additions and 70 deletions

View File

@@ -0,0 +1,125 @@
#!/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)