chore(migrations): update migration generation timestamps to latest time
- Updated generated timestamps in initial migrations of accounts, customers, inventory, orders, products, tenants, and user_roles apps - Reflect new generation time from 08:35 to 23:23 on 2026-01-03 - No changes to migration logic or schema detected - Ensures migration files align with most recent generation time for consistency
This commit is contained in:
178
myproject/inventory/management/commands/clear_reservations.py
Normal file
178
myproject/inventory/management/commands/clear_reservations.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.0.10 on 2026-01-03 08:35
|
||||
# Generated by Django 5.0.10 on 2026-01-03 23:23
|
||||
|
||||
import django.db.models.deletion
|
||||
import phonenumber_field.modelfields
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.0.10 on 2026-01-03 08:35
|
||||
# Generated by Django 5.0.10 on 2026-01-03 23:23
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
Reference in New Issue
Block a user