Fix product reservation system for demo orders

PROBLEM ANALYSIS:
- SQL script created orders bypassing Django ORM
- Django signals (post_save) didn't trigger
- No reservations were created automatically
- Found 51 orders with 102 items and 0 reservations

SOLUTION IMPLEMENTED:

1. Updated create_demo_orders command:
   - Added clear documentation about ORM usage
   - Already uses ORM (.save()) which triggers signals
   - Added informative messages about automatic reservations

2. Created fix_missing_reservations command:
   - Finds OrderItems without reservations
   - Creates missing Reservation records
   - Supports --dry-run mode for safety
   - Handles missing warehouses gracefully

3. Created SQL fix script:
   - Direct SQL approach for existing data
   - Creates reservations for all 102 items
   - Status: 'reserved'
   - Verified: All items now have reservations

4. Added verification scripts:
   - check_orders.py: Shows orders/items/reservations count
   - run_fix_reservations.py: Executes SQL fix

RESULTS:
- ✓ 102 reservations created for existing orders
- ✓ Future orders will use ORM and create reservations automatically
- ✓ System now works correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-08 00:04:55 +03:00
parent e3bab2252e
commit fcc7f2263d
5 changed files with 291 additions and 5 deletions

53
myproject/check_orders.py Normal file
View File

@@ -0,0 +1,53 @@
"""
Проверка созданных заказов и резервов
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django.setup()
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("SET search_path TO grach")
# Считаем заказы
cursor.execute("SELECT COUNT(*) FROM grach.orders_order")
orders_count = cursor.fetchone()[0]
print(f"Заказов: {orders_count}")
# Считаем позиции заказов
cursor.execute("SELECT COUNT(*) FROM grach.orders_orderitem")
items_count = cursor.fetchone()[0]
print(f"Позиций в заказах: {items_count}")
# Считаем резервы
cursor.execute("SELECT COUNT(*) FROM grach.inventory_reservation")
reservations_count = cursor.fetchone()[0]
print(f"Резервов: {reservations_count}")
# Детали по заказам без резервов
print("\nПервые 10 позиций без резервов:")
cursor.execute("""
SELECT
o.order_number,
oi.id as item_id,
p.name as product_name,
oi.quantity,
COUNT(r.id) as reservations_count
FROM grach.orders_order o
JOIN grach.orders_orderitem oi ON oi.order_id = o.id
LEFT JOIN grach.products_product p ON p.id = oi.product_id
LEFT JOIN grach.inventory_reservation r ON r.order_item_id = oi.id
GROUP BY o.order_number, oi.id, p.name, oi.quantity
HAVING COUNT(r.id) = 0
ORDER BY o.order_number
LIMIT 10
""")
rows = cursor.fetchall()
if rows:
for row in rows:
print(f" Заказ {row[0]}: ItemID={row[1]}, Товар=\"{row[2]}\", Кол-во={row[3]}, Резервов={row[4]}")
else:
print(" Все позиции имеют резервы!")

View File

@@ -0,0 +1,59 @@
-- Создание резервов для всех позиций заказов без резервов
SET search_path TO grach;
-- Проверяем наличие активного склада
DO $$
DECLARE
warehouse_id_val INT;
created_count INT := 0;
BEGIN
-- Получаем ID активного склада (если есть)
SELECT id INTO warehouse_id_val
FROM grach.inventory_warehouse
WHERE is_active = true
LIMIT 1;
IF warehouse_id_val IS NULL THEN
RAISE NOTICE 'WARNING: Нет активного склада, резервы будут созданы без склада';
ELSE
RAISE NOTICE 'Используем склад ID: %', warehouse_id_val;
END IF;
-- Создаем резервы для всех позиций без резервов
INSERT INTO grach.inventory_reservation (
order_item_id,
product_id,
warehouse_id,
quantity,
status,
reserved_at,
released_at,
converted_at
)
SELECT
oi.id,
COALESCE(oi.product_id, oi.product_kit_id),
warehouse_id_val,
oi.quantity,
'reserved',
CURRENT_TIMESTAMP,
NULL,
NULL
FROM grach.orders_orderitem oi
LEFT JOIN grach.inventory_reservation r ON r.order_item_id = oi.id
WHERE r.id IS NULL -- Только позиции без резервов
AND (oi.product_id IS NOT NULL OR oi.product_kit_id IS NOT NULL) -- Есть товар
ON CONFLICT DO NOTHING;
GET DIAGNOSTICS created_count = ROW_COUNT;
RAISE NOTICE 'Создано резервов: %', created_count;
END $$;
-- Проверяем результат
SELECT
COUNT(*) as total_items,
COUNT(r.id) as items_with_reservations,
COUNT(*) - COUNT(r.id) as items_without_reservations
FROM grach.orders_orderitem oi
LEFT JOIN grach.inventory_reservation r ON r.order_item_id = oi.id;

View File

@@ -1,5 +1,7 @@
""" """
Management команда для создания демо-заказов на разные даты Management команда для создания демо-заказов на разные даты
ВАЖНО: Создает заказы через Django ORM, что автоматически активирует
сигналы резервирования товаров!
""" """
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import timezone from django.utils import timezone
@@ -15,7 +17,7 @@ from products.models import Product
class Command(BaseCommand): class Command(BaseCommand):
help = 'Создает 20-30 демо-заказов на разные даты' help = 'Создает демо-заказы через ORM (с автоматическим резервированием товаров)'
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
@@ -39,7 +41,8 @@ class Command(BaseCommand):
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute(f'SET search_path TO {schema_name}') cursor.execute(f'SET search_path TO {schema_name}')
self.stdout.write(f'Начинаем создание демо-заказов в схеме {schema_name}...') self.stdout.write(f'[НАЧАЛО] Создание {count} демо-заказов в схеме {schema_name}...')
self.stdout.write('[INFO] Заказы создаются через ORM - резервы товаров будут созданы автоматически!')
# Проверяем наличие необходимых данных # Проверяем наличие необходимых данных
customers = list(Customer.objects.all()) customers = list(Customer.objects.all())
@@ -193,10 +196,11 @@ class Command(BaseCommand):
order.save() order.save()
created_count += 1 created_count += 1
self.stdout.write(f' Создан заказ #{order.order_number} на {delivery_date}') self.stdout.write(f' [OK] Заказ #{order.order_number} на {delivery_date} (товаров: {len(order_products)})')
except Exception as e: except Exception as e:
self.stdout.write(self.style.ERROR(f'Ошибка при создании заказа {i+1}: {str(e)}')) self.stdout.write(self.style.ERROR(f'[ОШИБКА] Заказ {i+1}: {str(e)}'))
self.stdout.write(self.style.SUCCESS(f'\nУспешно создано {created_count} заказов!')) self.stdout.write(self.style.SUCCESS(f'\n[ЗАВЕРШЕНО] Успешно создано {created_count} заказов!'))
self.stdout.write(f'Даты доставки: от {today - timedelta(days=15)} до {today + timedelta(days=15)}') self.stdout.write(f'Даты доставки: от {today - timedelta(days=15)} до {today + timedelta(days=15)}')
self.stdout.write(self.style.SUCCESS('\n[ВАЖНО] Резервы товаров созданы автоматически через Django сигналы!'))

View File

@@ -0,0 +1,138 @@
"""
Management команда для восстановления недостающих резервов товаров
Используется для исправления заказов, созданных напрямую через SQL
"""
from django.core.management.base import BaseCommand
from django.db import connection, transaction
from datetime import datetime
from orders.models import OrderItem
from inventory.models import Reservation, Warehouse
class Command(BaseCommand):
help = 'Создает недостающие резервы для существующих заказов'
def add_arguments(self, parser):
parser.add_argument(
'--schema',
type=str,
default='grach',
help='Схема базы данных (tenant) для работы'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Показать что будет сделано, но не применять изменения'
)
def handle(self, *args, **options):
schema_name = options['schema']
dry_run = options['dry_run']
# Устанавливаем схему для работы с tenant
with connection.cursor() as cursor:
cursor.execute(f'SET search_path TO {schema_name}')
self.stdout.write(f'[НАЧАЛО] Поиск заказов без резервов в схеме {schema_name}...')
if dry_run:
self.stdout.write(self.style.WARNING('[DRY RUN] Режим просмотра - изменения НЕ будут применены'))
# Получаем активный склад (если есть)
try:
warehouse = Warehouse.objects.filter(is_active=True).first()
if warehouse:
self.stdout.write(f'[INFO] Используем склад: {warehouse.name}')
else:
self.stdout.write(self.style.WARNING('[ВНИМАНИЕ] Нет активного склада, резервы будут созданы без привязки к складу'))
except Exception as e:
self.stdout.write(self.style.WARNING(f'[ВНИМАНИЕ] Не удалось получить склад: {e}'))
warehouse = None
# Находим все OrderItem без резервов
items_without_reservations = OrderItem.objects.filter(
reservations__isnull=True
).select_related('order', 'product', 'product_kit')
total_items = items_without_reservations.count()
if total_items == 0:
self.stdout.write(self.style.SUCCESS('[OK] Все заказы имеют резервы!'))
return
self.stdout.write(f'[НАЙДЕНО] {total_items} позиций заказов без резервов')
created_count = 0
errors_count = 0
with transaction.atomic():
for item in items_without_reservations:
try:
# Определяем товар (может быть product или product_kit)
product = item.product if item.product else item.product_kit
if not product:
self.stdout.write(
self.style.WARNING(
f'[ПРОПУСК] OrderItem #{item.id}: нет товара'
)
)
errors_count += 1
continue
# Создаем резерв
if not dry_run:
reservation = Reservation.objects.create(
order_item=item,
product=product,
warehouse=warehouse,
quantity=item.quantity,
status='reserved'
)
self.stdout.write(
f' [OK] Резерв #{reservation.id}: '
f'Заказ {item.order.order_number}, '
f'Товар "{product.name}", '
f'Кол-во: {item.quantity}'
)
else:
self.stdout.write(
f' [DRY RUN] Будет создан резерв: '
f'Заказ {item.order.order_number}, '
f'Товар "{product.name}", '
f'Кол-во: {item.quantity}'
)
created_count += 1
except Exception as e:
self.stdout.write(
self.style.ERROR(
f'[ОШИБКА] OrderItem #{item.id}: {str(e)}'
)
)
errors_count += 1
if dry_run:
# В режиме dry-run откатываем транзакцию
transaction.set_rollback(True)
self.stdout.write(
self.style.WARNING(
f'\n[DRY RUN] Изменения НЕ применены (транзакция откачена)'
)
)
# Итоги
self.stdout.write('\n' + '=' * 60)
if dry_run:
self.stdout.write(self.style.WARNING('[DRY RUN] Результаты (БЕЗ изменений):'))
self.stdout.write(f' Будет создано резервов: {created_count}')
self.stdout.write(f' Ошибок/пропусков: {errors_count}')
self.stdout.write('\nДля применения изменений запустите без флага --dry-run')
else:
self.stdout.write(self.style.SUCCESS('[ЗАВЕРШЕНО] Результаты:'))
self.stdout.write(f' Создано резервов: {created_count}')
self.stdout.write(f' Ошибок/пропусков: {errors_count}')
self.stdout.write(self.style.SUCCESS('\n✓ Резервы успешно восстановлены!'))

View File

@@ -0,0 +1,32 @@
"""
Скрипт для создания резервов для существующих заказов
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django.setup()
from django.db import connection
# Читаем SQL скрипт
with open('fix_reservations.sql', 'r', encoding='utf-8') as f:
sql = f.read()
# Выполняем SQL
with connection.cursor() as cursor:
try:
cursor.execute(sql)
print("[OK] SQL script executed successfully!")
print("\nResults:")
# Получаем результат последнего SELECT
rows = cursor.fetchall()
if rows:
row = rows[0]
print(f" Total items: {row[0]}")
print(f" Items with reservations: {row[1]}")
print(f" Items without reservations: {row[2]}")
print("\n[OK] Reservations created!")
except Exception as e:
print(f"[ERROR] {e}")
raise