Исправлена критическая проблема с резервами при смене статуса заказа

Проблема:
- При смене статуса заказа на 'Выполнен' товар списывался со склада
- Резервы обновлялись на статус 'converted_to_sale'
- НО Stock.quantity_reserved не обновлялся автоматически
- В результате резервы продолжали 'держать' товар, хотя он уже продан

Решение:
1. Изменен сигнал create_sale_on_order_completion:
   - Используется .save(update_fields=[...]) вместо .update()
   - Это вызывает сигнал update_stock_on_reservation_change
   - Убран костыль с ручным вызовом refresh_from_batches()

2. Оптимизирован сигнал update_stock_on_reservation_change:
   - Stock обновляется ТОЛЬКО при изменении status или quantity
   - При изменении других полей (даты и т.д.) Stock НЕ пересчитывается
   - Предотвращены лишние пересчёты и улучшена производительность

3. Добавлены диагностические инструменты:
   - check_stock_103.py - для диагностики проблем с Stock
   - fix_stock_after_sale.py - команда для исправления старых заказов
   - diagnose_reservation_issue.py - универсальная диагностика

Результат:
- Элегантное решение без дублирования логики
- Stock автоматически обновляется при изменении резервов
- Работает везде, не только в заказах
- Оптимизировано для производительности
This commit is contained in:
2025-12-01 02:34:54 +03:00
parent 490e5d5401
commit e4cb175db2
5 changed files with 512 additions and 34 deletions

View File

@@ -0,0 +1,117 @@
"""
Команда для исправления Stock после обработки заказов.
Пересчитывает quantity_reserved для всех товаров.
"""
from django.core.management.base import BaseCommand
from inventory.models import Stock, Reservation
from django.db.models import Sum
from decimal import Decimal
class Command(BaseCommand):
help = 'Исправляет quantity_reserved в Stock после обработки заказов'
def add_arguments(self, parser):
parser.add_argument(
'--order',
type=int,
help='Номер заказа для исправления (опционально)'
)
def handle(self, *args, **options):
order_number = options.get('order')
if order_number:
self.stdout.write(f"Исправление Stock для заказа {order_number}...")
self.fix_for_order(order_number)
else:
self.stdout.write("Исправление всех Stock...")
self.fix_all_stock()
def fix_for_order(self, order_number):
"""Исправить Stock для конкретного заказа"""
from orders.models import Order
try:
order = Order.objects.get(order_number=order_number)
except Order.DoesNotExist:
self.stdout.write(
self.style.ERROR(f"Заказ {order_number} не найден!")
)
return
# Получаем все резервы для этого заказа
reservations = Reservation.objects.filter(
order_item__order=order
).values('product_id', 'warehouse_id').distinct()
fixed_count = 0
for res in reservations:
product_id = res['product_id']
warehouse_id = res['warehouse_id']
try:
stock = Stock.objects.get(
product_id=product_id,
warehouse_id=warehouse_id
)
old_reserved = stock.quantity_reserved
stock.refresh_from_batches()
new_reserved = stock.quantity_reserved
if old_reserved != new_reserved:
self.stdout.write(
self.style.SUCCESS(
f"✓ Обновлено: product_id={product_id}, "
f"warehouse_id={warehouse_id}, "
f"reserved: {old_reserved}{new_reserved}"
)
)
fixed_count += 1
else:
self.stdout.write(
f" Без изменений: product_id={product_id}"
)
except Stock.DoesNotExist:
self.stdout.write(
self.style.WARNING(
f"⚠ Stock не найден: product_id={product_id}, "
f"warehouse_id={warehouse_id}"
)
)
self.stdout.write(
self.style.SUCCESS(
f"\n✅ Готово! Обновлено {fixed_count} Stock для заказа {order_number}"
)
)
def fix_all_stock(self):
"""Пересчитать все Stock"""
stocks = Stock.objects.all()
total = stocks.count()
fixed_count = 0
self.stdout.write(f"Найдено {total} Stock записей...")
for stock in stocks:
old_reserved = stock.quantity_reserved
stock.refresh_from_batches()
new_reserved = stock.quantity_reserved
if old_reserved != new_reserved:
self.stdout.write(
self.style.SUCCESS(
f"{stock.product.name} на {stock.warehouse.name}: "
f"reserved {old_reserved}{new_reserved}"
)
)
fixed_count += 1
self.stdout.write(
self.style.SUCCESS(
f"\n✅ Готово! Обновлено {fixed_count} из {total} Stock"
)
)