Files
octopus/myproject/cleanup_duplicate_reservations.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

193 lines
6.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.
"""
Скрипт для очистки дубликатов резервов в базе данных (для 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()