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

Проблема:
- При смене статуса заказа на 'Выполнен' товар списывался со склада
- Резервы обновлялись на статус '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,171 @@
#!/usr/bin/env python
"""
Скрипт для диагностики проблемы с резервами при смене статуса на 'completed'.
Проверяет:
1. Изменяется ли статус резервов на 'converted_to_sale'
2. Создаются ли Sale
3. Списывается ли товар со склада
4. Освобождаются ли резервы
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django.setup()
from django.db import connection
from orders.models import Order, OrderStatus
from inventory.models import Reservation, Sale, Stock
def diagnose_order(order_number):
"""Диагностика конкретного заказа"""
print(f"\n{'='*80}")
print(f"ДИАГНОСТИКА ЗАКАЗА #{order_number}")
print(f"{'='*80}\n")
# Проверяем тенант
print(f"📌 Текущая схема БД: {connection.schema_name}")
try:
order = Order.objects.get(order_number=order_number)
except Order.DoesNotExist:
print(f"❌ ОШИБКА: Заказ #{order_number} не найден!")
return
print(f"✓ Заказ найден")
print(f" - Клиент: {order.customer.name}")
print(f" - Статус: {order.status.name if order.status else 'Нет статуса'} (code: {order.status.code if order.status else 'None'})")
print(f" - Дата создания: {order.created_at}")
print(f" - Дата обновления: {order.updated_at}")
# Проверяем позиции заказа
print(f"\n📦 ПОЗИЦИИ ЗАКАЗА:")
items = order.items.all()
if not items.exists():
print(" ⚠ Нет позиций в заказе")
else:
for item in items:
product = item.product or item.product_kit
print(f" - {product.name}: {item.quantity} шт. по {item.price} руб.")
# Проверяем резервы
print(f"\n📝 РЕЗЕРВЫ:")
reservations = Reservation.objects.filter(order_item__order=order)
if not reservations.exists():
print("НЕТ РЕЗЕРВОВ для этого заказа!")
else:
for res in reservations:
print(f" - {res.product.name}:")
print(f" • Количество: {res.quantity}")
print(f" • Статус: {res.get_status_display()} ({res.status})")
print(f" • Склад: {res.warehouse.name}")
print(f" • Зарезервировано: {res.reserved_at}")
if res.converted_at:
print(f" • Конвертировано: {res.converted_at}")
if res.released_at:
print(f" • Освобождено: {res.released_at}")
# Проверяем Sale
print(f"\n💰 ПРОДАЖИ (Sale):")
sales = Sale.objects.filter(order=order)
if not sales.exists():
print("НЕТ ПРОДАЖ для этого заказа!")
if order.status and order.status.code == 'completed':
print(" ❌ ПРОБЛЕМА: Заказ в статусе 'completed', но Sale не созданы!")
else:
for sale in sales:
print(f" - {sale.product.name}:")
print(f" • Количество: {sale.quantity}")
print(f" • Цена продажи: {sale.sale_price}")
print(f" • Склад: {sale.warehouse.name}")
print(f" • Дата: {sale.created_at}")
# Проверяем распределение по партиям
from inventory.models import SaleBatchAllocation
allocations = SaleBatchAllocation.objects.filter(sale=sale)
if allocations.exists():
print(f" • Распределение по партиям:")
for alloc in allocations:
print(f" - Партия #{alloc.batch.id}: {alloc.quantity} шт.")
# Проверяем Stock
print(f"\n📊 ОСТАТКИ НА СКЛАДЕ (Stock):")
for item in items:
product = item.product or item.product_kit
warehouse = order.pickup_warehouse or order.items.first().order.pickup_warehouse
if warehouse:
try:
stock = Stock.objects.get(product=product, warehouse=warehouse)
print(f" - {product.name} на складе {warehouse.name}:")
print(f"Всего: {stock.quantity}")
print(f" • Зарезервировано: {stock.reserved_quantity}")
print(f" • Доступно: {stock.available_quantity}")
except Stock.DoesNotExist:
print(f" - {product.name}: ❌ Stock не найден")
else:
print(f" - {product.name}: ⚠ Склад не определён")
# Проверяем историю изменений
print(f"\n📜 ИСТОРИЯ ИЗМЕНЕНИЙ СТАТУСА:")
history = order.history.all()[:5] # Последние 5 записей
for idx, record in enumerate(history, 1):
status_name = record.status.name if record.status else "Нет статуса"
print(f" {idx}. {status_name} - {record.history_date}")
# Выводим выводы
print(f"\n{'='*80}")
print("ДИАГНОСТИКА:")
print(f"{'='*80}")
issues = []
# Проверка 1: Резервы существуют?
if not reservations.exists():
issues.append("❌ КРИТИЧНО: Нет резервов для заказа")
# Проверка 2: Статус 'completed' + нет Sale
if order.status and order.status.code == 'completed':
if not sales.exists():
issues.append("❌ КРИТИЧНО: Заказ 'completed', но Sale не созданы")
# Проверка 3: Статус резервов
reserved_count = reservations.filter(status='reserved').count()
converted_count = reservations.filter(status='converted_to_sale').count()
if reserved_count > 0:
issues.append(f"❌ ПРОБЛЕМА: {reserved_count} резервов ещё в статусе 'reserved' при completed заказе")
if converted_count == 0 and reservations.exists():
issues.append("❌ ПРОБЛЕМА: Ни один резерв не конвертирован в продажу")
if issues:
print("\n🔴 НАЙДЕНЫ ПРОБЛЕМЫ:")
for issue in issues:
print(f" {issue}")
else:
print("\n✅ Проблем не обнаружено")
print(f"\n{'='*80}\n")
def main():
print("\n" + "="*80)
print("ДИАГНОСТИКА ПРОБЛЕМЫ С РЕЗЕРВАМИ")
print("="*80)
# Спрашиваем номер заказа
order_number_input = input("\nВведите номер заказа для диагностики: ").strip()
try:
order_number = int(order_number_input)
except ValueError:
print("❌ Ошибка: Введите корректный номер заказа (число)")
return
diagnose_order(order_number)
if __name__ == "__main__":
main()