Проблема: - При изменении статуса заказа на 'Выполнен' товар списывался дважды - Заказ на 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 - документация по исправлению
193 lines
6.9 KiB
Python
193 lines
6.9 KiB
Python
"""
|
||
Скрипт для очистки дубликатов резервов в базе данных (для tenant: buba).
|
||
|
||
Проблема: У некоторых позиций заказов (OrderItem) существует несколько резервов
|
||
в статусе 'reserved', что вызывает ошибку MultipleObjectsReturned.
|
||
|
||
Решение: Оставляем только первый резерв, остальные удаляем.
|
||
"""
|
||
|
||
import os
|
||
import django
|
||
|
||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
|
||
django.setup()
|
||
|
||
from django.db import transaction, connection
|
||
from django.db.models import Count
|
||
from inventory.models import Reservation
|
||
from orders.models import OrderItem
|
||
|
||
|
||
# Устанавливаем tenant-схему
|
||
def set_tenant_schema(schema_name='buba'):
|
||
"""Переключаемся на нужную tenant-схему"""
|
||
connection.set_schema(schema_name)
|
||
print(f"✓ Переключились на схему: {schema_name}")
|
||
|
||
|
||
def find_duplicate_reservations():
|
||
"""Находит OrderItem с несколькими резервами в статусе 'reserved'"""
|
||
|
||
print("\n" + "="*80)
|
||
print("Поиск дубликатов резервов...")
|
||
print("="*80)
|
||
|
||
# Группируем резервы по order_item и считаем количество
|
||
duplicates = Reservation.objects.filter(
|
||
status='reserved'
|
||
).values('order_item').annotate(
|
||
count=Count('id')
|
||
).filter(count__gt=1).order_by('-count')
|
||
|
||
print(f"\nНайдено OrderItem с дубликатами: {duplicates.count()}")
|
||
|
||
if duplicates.count() == 0:
|
||
print("✅ Дубликатов не обнаружено!")
|
||
return []
|
||
|
||
# Выводим детали
|
||
problem_items = []
|
||
|
||
for dup in duplicates:
|
||
order_item_id = dup['order_item']
|
||
count = dup['count']
|
||
|
||
try:
|
||
order_item = OrderItem.objects.get(id=order_item_id)
|
||
|
||
product_name = (
|
||
order_item.product.sku if order_item.product
|
||
else order_item.product_kit.name if order_item.product_kit
|
||
else "Unknown"
|
||
)
|
||
|
||
print(f"\n OrderItem #{order_item_id}:")
|
||
print(f" Заказ: #{order_item.order.order_number}")
|
||
print(f" Товар: {product_name}")
|
||
print(f" Количество резервов: {count}")
|
||
|
||
# Показываем все резервы
|
||
reservations = Reservation.objects.filter(
|
||
order_item=order_item,
|
||
status='reserved'
|
||
).order_by('reserved_at') # Сортируем по дате создания
|
||
|
||
for idx, res in enumerate(reservations, 1):
|
||
marker = "✓ ОСТАВИТЬ" if idx == 1 else "✗ УДАЛИТЬ"
|
||
print(f" {marker} - Резерв #{res.id}: qty={res.quantity}, создан {res.reserved_at}")
|
||
|
||
problem_items.append({
|
||
'order_item': order_item,
|
||
'count': count,
|
||
'reservations': list(reservations)
|
||
})
|
||
|
||
except OrderItem.DoesNotExist:
|
||
print(f"\n ⚠ OrderItem #{order_item_id} не существует (удален)")
|
||
|
||
return problem_items
|
||
|
||
|
||
def clean_duplicate_reservations(problem_items, dry_run=True):
|
||
"""
|
||
Очищает дубликаты резервов.
|
||
|
||
Args:
|
||
problem_items: Список OrderItem с дубликатами
|
||
dry_run: Если True, только показывает что будет сделано, но не выполняет
|
||
"""
|
||
|
||
print("\n" + "="*80)
|
||
if dry_run:
|
||
print("РЕЖИМ ПРОВЕРКИ (dry_run=True) - изменения НЕ будут сохранены")
|
||
else:
|
||
print("⚠ РЕЖИМ ОЧИСТКИ (dry_run=False) - изменения БУДУТ сохранены!")
|
||
print("="*80)
|
||
|
||
if not problem_items:
|
||
print("\nНечего очищать!")
|
||
return
|
||
|
||
total_deleted = 0
|
||
|
||
for item_data in problem_items:
|
||
order_item = item_data['order_item']
|
||
reservations = item_data['reservations']
|
||
|
||
# Оставляем первый (самый старый) резерв
|
||
first_reservation = reservations[0]
|
||
duplicates = reservations[1:]
|
||
|
||
print(f"\nOrderItem #{order_item.id} (Заказ #{order_item.order.order_number}):")
|
||
print(f" Оставляем: Резерв #{first_reservation.id}")
|
||
print(f" Удаляем: {len(duplicates)} дубликатов")
|
||
|
||
if not dry_run:
|
||
with transaction.atomic():
|
||
for dup in duplicates:
|
||
print(f" ✗ Удаляем Резерв #{dup.id}")
|
||
dup.delete()
|
||
total_deleted += 1
|
||
else:
|
||
for dup in duplicates:
|
||
print(f" [DRY RUN] Будет удален Резерв #{dup.id}")
|
||
total_deleted += len(duplicates)
|
||
|
||
print("\n" + "="*80)
|
||
if dry_run:
|
||
print(f"[DRY RUN] Будет удалено резервов: {total_deleted}")
|
||
else:
|
||
print(f"✅ УДАЛЕНО резервов: {total_deleted}")
|
||
print("="*80)
|
||
|
||
|
||
def main():
|
||
"""Главная функция"""
|
||
|
||
print("\n" + "="*80)
|
||
print("ОЧИСТКА ДУБЛИКАТОВ РЕЗЕРВОВ")
|
||
print("="*80)
|
||
|
||
# Переключаемся на tenant buba
|
||
set_tenant_schema('buba')
|
||
|
||
# Шаг 1: Находим дубликаты
|
||
problem_items = find_duplicate_reservations()
|
||
|
||
if not problem_items:
|
||
return
|
||
|
||
# Шаг 2: Сначала делаем dry run
|
||
print("\n" + "="*80)
|
||
print("ШАГ 1: ПРОВЕРКА (без изменений)")
|
||
print("="*80)
|
||
clean_duplicate_reservations(problem_items, dry_run=True)
|
||
|
||
# Шаг 3: Спрашиваем подтверждение
|
||
print("\n" + "="*80)
|
||
response = input("\n⚠ Выполнить очистку? (yes/no): ")
|
||
|
||
if response.lower() in ['yes', 'y', 'да', 'д']:
|
||
print("\n" + "="*80)
|
||
print("ШАГ 2: ОЧИСТКА (с изменениями)")
|
||
print("="*80)
|
||
clean_duplicate_reservations(problem_items, dry_run=False)
|
||
|
||
# Проверяем что дубликатов больше нет
|
||
print("\n" + "="*80)
|
||
print("ПРОВЕРКА ПОСЛЕ ОЧИСТКИ")
|
||
print("="*80)
|
||
remaining = find_duplicate_reservations()
|
||
|
||
if not remaining:
|
||
print("\n✅ ВСЕ ДУБЛИКАТЫ УСПЕШНО УДАЛЕНЫ!")
|
||
else:
|
||
print(f"\n⚠ Еще остались дубликаты: {len(remaining)}")
|
||
else:
|
||
print("\n❌ Очистка отменена")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|