From 123f330a26b144f3af6d8fcf83d9b05de4723c54 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sun, 4 Jan 2026 02:29:49 +0300 Subject: [PATCH] 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 --- myproject/accounts/migrations/0001_initial.py | 2 +- .../customers/migrations/0001_initial.py | 2 +- .../customers/migrations/0002_initial.py | 2 +- .../management/commands/clear_reservations.py | 178 ++++++++++++++++++ .../inventory/migrations/0001_initial.py | 2 +- .../inventory/migrations/0002_initial.py | 2 +- myproject/orders/migrations/0001_initial.py | 2 +- myproject/orders/migrations/0002_initial.py | 2 +- myproject/products/migrations/0001_initial.py | 2 +- myproject/tenants/migrations/0001_initial.py | 2 +- .../user_roles/migrations/0001_initial.py | 2 +- 11 files changed, 188 insertions(+), 10 deletions(-) create mode 100644 myproject/inventory/management/commands/clear_reservations.py diff --git a/myproject/accounts/migrations/0001_initial.py b/myproject/accounts/migrations/0001_initial.py index b2185e4..297a6bf 100644 --- a/myproject/accounts/migrations/0001_initial.py +++ b/myproject/accounts/migrations/0001_initial.py @@ -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.contrib.auth.validators import django.utils.timezone diff --git a/myproject/customers/migrations/0001_initial.py b/myproject/customers/migrations/0001_initial.py index efbe34f..98c4848 100644 --- a/myproject/customers/migrations/0001_initial.py +++ b/myproject/customers/migrations/0001_initial.py @@ -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 diff --git a/myproject/customers/migrations/0002_initial.py b/myproject/customers/migrations/0002_initial.py index cad2828..1fa7c7d 100644 --- a/myproject/customers/migrations/0002_initial.py +++ b/myproject/customers/migrations/0002_initial.py @@ -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.db import migrations, models diff --git a/myproject/inventory/management/commands/clear_reservations.py b/myproject/inventory/management/commands/clear_reservations.py new file mode 100644 index 0000000..034ef50 --- /dev/null +++ b/myproject/inventory/management/commands/clear_reservations.py @@ -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) diff --git a/myproject/inventory/migrations/0001_initial.py b/myproject/inventory/migrations/0001_initial.py index aab5025..38de300 100644 --- a/myproject/inventory/migrations/0001_initial.py +++ b/myproject/inventory/migrations/0001_initial.py @@ -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 diff --git a/myproject/inventory/migrations/0002_initial.py b/myproject/inventory/migrations/0002_initial.py index d8815b9..d15329d 100644 --- a/myproject/inventory/migrations/0002_initial.py +++ b/myproject/inventory/migrations/0002_initial.py @@ -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 diff --git a/myproject/orders/migrations/0001_initial.py b/myproject/orders/migrations/0001_initial.py index 3d646d0..1890b72 100644 --- a/myproject/orders/migrations/0001_initial.py +++ b/myproject/orders/migrations/0001_initial.py @@ -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 diff --git a/myproject/orders/migrations/0002_initial.py b/myproject/orders/migrations/0002_initial.py index 87e465d..2952116 100644 --- a/myproject/orders/migrations/0002_initial.py +++ b/myproject/orders/migrations/0002_initial.py @@ -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 diff --git a/myproject/products/migrations/0001_initial.py b/myproject/products/migrations/0001_initial.py index bf96190..9dbaff2 100644 --- a/myproject/products/migrations/0001_initial.py +++ b/myproject/products/migrations/0001_initial.py @@ -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.core.validators import django.db.models.deletion diff --git a/myproject/tenants/migrations/0001_initial.py b/myproject/tenants/migrations/0001_initial.py index 2e4993d..ee3409f 100644 --- a/myproject/tenants/migrations/0001_initial.py +++ b/myproject/tenants/migrations/0001_initial.py @@ -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.core.validators import django.db.models.deletion diff --git a/myproject/user_roles/migrations/0001_initial.py b/myproject/user_roles/migrations/0001_initial.py index cfe633e..e914cfb 100644 --- a/myproject/user_roles/migrations/0001_initial.py +++ b/myproject/user_roles/migrations/0001_initial.py @@ -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