""" Management команда для отмены всех резервов в указанной схеме Используется для очистки "зависших" резервов после ошибок """ from django.core.management.base import BaseCommand from django.db import connection, transaction from django.utils import timezone class Command(BaseCommand): help = 'Отменяет все резервы (меняет статус на released) в указанной схеме' def add_arguments(self, parser): parser.add_argument( '--schema', type=str, required=True, help='Схема базы данных (tenant) для работы' ) parser.add_argument( '--confirm', action='store_true', help='Подтверждение операции (без этого флага будет только показан список резервов)' ) def handle(self, *args, **options): schema_name = options['schema'] confirm = options.get('confirm', False) self.stdout.write('=' * 60) self.stdout.write(f'[INFO] Схема: {schema_name}') self.stdout.write('=' * 60) with connection.cursor() as cursor: # Устанавливаем схему cursor.execute(f'SET search_path TO {schema_name}') # Проверяем наличие резервов со статусом 'reserved' cursor.execute(f""" SELECT r.id, p.name as product_name, r.quantity, w.name as warehouse_name, r.reserved_at, r.order_item_id, r.showcase_id, r.product_kit_id FROM {schema_name}.inventory_reservation r JOIN {schema_name}.products_product p ON p.id = r.product_id JOIN {schema_name}.inventory_warehouse w ON w.id = r.warehouse_id WHERE r.status = 'reserved' ORDER BY r.reserved_at """) reservations = cursor.fetchall() if not reservations: self.stdout.write(self.style.SUCCESS('[OK] В схеме нет активных резервов со статусом "reserved"')) return self.stdout.write(f'\n[INFO] Найдено резервов со статусом "reserved": {len(reservations)}\n') # Группируем резервы по типу order_reservations = [] showcase_reservations = [] kit_reservations = [] other_reservations = [] for res in reservations: res_id, product_name, quantity, warehouse_name, reserved_at, order_item_id, showcase_id, product_kit_id = res if order_item_id: order_reservations.append(res) elif showcase_id: showcase_reservations.append(res) elif product_kit_id: kit_reservations.append(res) else: other_reservations.append(res) # Выводим статистику if order_reservations: self.stdout.write(f' - Резервы для заказов: {len(order_reservations)}') if showcase_reservations: self.stdout.write(f' - Резервы для витрин: {len(showcase_reservations)}') if kit_reservations: self.stdout.write(f' - Резервы для комплектов: {len(kit_reservations)}') if other_reservations: self.stdout.write(f' - Прочие резервы: {len(other_reservations)}') # Выводим детали первых 10 резервов self.stdout.write('\n[INFO] Первые 10 резервов:') for res in reservations[:10]: res_id, product_name, quantity, warehouse_name, reserved_at, order_item_id, showcase_id, product_kit_id = res context = [] if order_item_id: context.append(f'заказ #{order_item_id}') if showcase_id: context.append(f'витрина #{showcase_id}') if product_kit_id: context.append(f'комплект #{product_kit_id}') context_str = ', '.join(context) if context else 'без привязки' self.stdout.write( f' #{res_id}: {product_name} - {quantity} ед. на складе "{warehouse_name}" ' f'({reserved_at.strftime("%Y-%m-%d %H:%M:%S")}, {context_str})' ) if not confirm: self.stdout.write('\n' + '=' * 60) self.stdout.write(self.style.WARNING('[ВНИМАНИЕ] Это режим предварительного просмотра')) self.stdout.write(self.style.WARNING(f'Для отмены всех {len(reservations)} резервов запустите команду с флагом --confirm:')) self.stdout.write(f' python manage.py clear_reservations --schema={schema_name} --confirm') self.stdout.write('=' * 60) return # Подтверждение получено - отменяем все резервы self.stdout.write('\n' + '=' * 60) self.stdout.write(self.style.WARNING(f'[НАЧАЛО] Отмена {len(reservations)} резервов...')) self.stdout.write('=' * 60) with transaction.atomic(): released_at = timezone.now() # Обновляем все резервы одним запросом cursor.execute(f""" UPDATE {schema_name}.inventory_reservation SET status = 'released', released_at = %s WHERE status = 'reserved' """, [released_at]) updated_count = cursor.rowcount self.stdout.write(f'\n[OK] Обновлено резервов: {updated_count}') # Теперь пересчитываем quantity_reserved для всех затронутых Stock записей self.stdout.write('\n[INFO] Пересчет quantity_reserved для Stock записей...') # Получаем уникальные комбинации product_id + warehouse_id из освобожденных резервов cursor.execute(f""" SELECT DISTINCT product_id, warehouse_id FROM {schema_name}.inventory_reservation WHERE released_at = %s """, [released_at]) stock_updates = cursor.fetchall() for product_id, warehouse_id in stock_updates: # Пересчитываем quantity_reserved для этой комбинации cursor.execute(f""" SELECT COALESCE(SUM(quantity), 0) FROM {schema_name}.inventory_reservation WHERE product_id = %s AND warehouse_id = %s AND status = 'reserved' """, [product_id, warehouse_id]) total_reserved = cursor.fetchone()[0] # Обновляем Stock cursor.execute(f""" UPDATE {schema_name}.inventory_stock SET quantity_reserved = %s, updated_at = CURRENT_TIMESTAMP WHERE product_id = %s AND warehouse_id = %s """, [total_reserved, product_id, warehouse_id]) self.stdout.write(f'[OK] Обновлено Stock записей: {len(stock_updates)}') self.stdout.write('\n' + '=' * 60) self.stdout.write(self.style.SUCCESS('[ЗАВЕРШЕНО] Все резервы успешно отменены!')) self.stdout.write(f' Освобождено резервов: {updated_count}') self.stdout.write(f' Обновлено Stock записей: {len(stock_updates)}') self.stdout.write('=' * 60)