Проблема: - При изменении статуса заказа на 'Выполнен' товар списывался дважды - Заказ на 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 - документация по исправлению
23 KiB
Удаление Тенантов в Django-Tenants
⚠️ КРИТИЧЕСКИ ВАЖНО
В этом проекте auto_drop_schema = False, поэтому ВСЕ команды удаления (включая cleanup_tenant и delete_tenant) НЕ удаляют схему из PostgreSQL автоматически!
После удаления тенанта через любую команду ОБЯЗАТЕЛЬНО нужно вручную удалить схему из базы данных, иначе:
- ✅ Запись
Clientудалится - ❌ Схема со всеми таблицами и данными останется в PostgreSQL
- ❌ При повторной регистрации с тем же именем новый тенант увидит старые данные!
Как удалить схему вручную:
Вариант 1 — через Django shell:
from django.db import connection
with connection.cursor() as cursor:
cursor.execute('DROP SCHEMA IF EXISTS имя_схемы CASCADE;')
Вариант 2 — одной командой из консоли:
python manage.py shell -c "from django.db import connection; cur = connection.cursor(); cur.execute('DROP SCHEMA IF EXISTS имя_схемы CASCADE;')"
Вариант 3 — напрямую в PostgreSQL (psql):
DROP SCHEMA IF EXISTS имя_схемы CASCADE;
Быстрая справка
Рекомендуемый способ (с улучшенной командой):
# Базовое удаление (Client + файлы, заявка остается в истории)
python manage.py cleanup_tenant --schema=papa --noinput
# Полная очистка (Client + заявка + файлы)
python manage.py cleanup_tenant --schema=papa --noinput --purge-registration --delete-files
# ЗАТЕМ ВРУЧНУЮ удалить схему из PostgreSQL:
python manage.py shell -c "from django.db import connection; cur = connection.cursor(); cur.execute('DROP SCHEMA IF EXISTS papa CASCADE;')"
Альтернативный способ (встроенная команда django-tenants):
⚠️ ВНИМАНИЕ: В этом проекте auto_drop_schema = False, поэтому команда delete_tenant НЕ удаляет схему из PostgreSQL. Удаляется только запись Client, но все таблицы и данные в схеме остаются в базе. Для полного удаления используйте cleanup_tenant (см. выше) или удалите схему вручную.
# Удалить конкретного тенанта (только запись Client, схема БД остаётся!)
python manage.py delete_tenant --schema=papa --noinput
# Удалить все файлы тенанта (после удаления из БД)
Remove-Item -Path 'media/tenants' -Recurse -Force
Подробное руководство
⭐ Способ 0: Новая улучшенная команда cleanup_tenant (РЕКОМЕНДУЕТСЯ)
Эта команда решает проблему с TenantRegistration и управляет связанными данными
Что это за команда?
Это новая management команда, которая автоматически:
- Удаляет запись Client из таблицы тенантов
- Обрабатывает TenantRegistration (может оставить в истории или удалить)
- Опционально удаляет физические файлы
- Показывает красивый прогресс с подтверждением
⚠️ ВАЖНО: Команда НЕ удаляет схему PostgreSQL (т.к. auto_drop_schema = False). После выполнения команды нужно вручную удалить схему через SQL (см. раздел выше).
Параметры:
--schema=<имя> # Имя тенанта (обязательно)
--noinput # Не запрашивать подтверждение
--purge-registration # Удалить TenantRegistration (иначе оставляет с tenant=NULL)
--delete-files # Удалить физические файлы из /media/tenants/
Варианты использования:
1️⃣ Базовое удаление (рекомендуется):
python manage.py cleanup_tenant --schema=papa --noinput
# ЗАТЕМ вручную:
python manage.py shell -c "from django.db import connection; cur = connection.cursor(); cur.execute('DROP SCHEMA IF EXISTS papa CASCADE;')"
Удаляет: Client (заявка остается в истории с tenant=NULL) Требует: Ручное удаление схемы PostgreSQL
2️⃣ Полная очистка:
python manage.py cleanup_tenant --schema=papa --noinput --purge-registration --delete-files
# ЗАТЕМ вручную:
python manage.py shell -c "from django.db import connection; cur = connection.cursor(); cur.execute('DROP SCHEMA IF EXISTS papa CASCADE;')"
Удаляет: Client + заявка + файлы Требует: Ручное удаление схемы PostgreSQL
3️⃣ Удаление с заявкой:
python manage.py cleanup_tenant --schema=papa --noinput --purge-registration
# ЗАТЕМ вручную:
python manage.py shell -c "from django.db import connection; cur = connection.cursor(); cur.execute('DROP SCHEMA IF EXISTS papa CASCADE;')"
Удаляет: Client + заявка (файлы остаются) Требует: Ручное удаление схемы PostgreSQL
Пример вывода:
=== Удаление тенанта (магазина) ===
======================================================================
ВНИМАНИЕ! Будут удалены следующие данные:
======================================================================
📋 Тенант:
• Название: Papa Shop
• Schema: papa
• Владелец: admin@example.com
💾 База данных:
• Запись Client "papa" будет удалена
• ⚠️ Схема PostgreSQL останется и требует ручного удаления!
📝 TenantRegistration:
• Заявка от Papa Owner (papa@example.com)
• Статус: Одобрено
• Действие: оставить с tenant=NULL (сохранить историю)
======================================================================
▶ Начинаю удаление...
======================================================================
1️⃣ Обновляю TenantRegistration (сохраняю историю)...
✓ TenantRegistration обновлена (tenant=NULL)
2️⃣ Удаляю Client...
✓ Client "Papa Shop" удален
⚠️ Схема БД "papa" НЕ удалена - требует ручного удаления!
======================================================================
✓ Тенант успешно удален!
======================================================================
Способ 1: Удаление одного тенанта по schema
Команда:
cd myproject
python manage.py delete_tenant --schema=papa --noinput
Параметры:
--schema=papa- Удалить тенант с именем schemapapa--noinput- Не запрашивать подтверждение (автоматический режим)
Результат:
Deleting 'papa'
Deleted 'papa'
⚠️ ВНИМАНИЕ: В этом проекте auto_drop_schema = False, поэтому команда delete_tenant НЕ удаляет схему из PostgreSQL!
Эта команда удаляет:
- ✅ Запись Client из таблицы тенантов
- ❌ НЕ удаляет схему PostgreSQL (остаются все таблицы и данные!)
- ❌ НЕ удаляет файлы в
/media/tenants/{tenant_id}/
После выполнения нужно вручную удалить схему (см. раздел "КРИТИЧЕСКИ ВАЖНО" выше).
Способ 2: Интерактивное удаление (с выбором)
Команда:
python manage.py delete_tenant
Результат: Система запросит у вас:
Enter Tenant Schema ('?' to list schemas): papa
Are you sure you want to delete the tenant: papa? (yes/no): yes
Введите:
?- чтобы увидеть список всех тенантов- Имя schema - чтобы выбрать тенант
yes- чтобы подтвердить удаление
Способ 3: Удаление файлов после удаления из БД
После удаления тенанта из БД нужно удалить его файлы вручную.
На Windows (PowerShell):
Remove-Item -Path 'c:\Users\team_\Desktop\test_qwen\myproject\media\tenants' -Recurse -Force
Write-Host 'Removed tenants directory'
На Windows (CMD):
rmdir /s /q "c:\Users\team_\Desktop\test_qwen\myproject\media\tenants"
На Linux/Mac:
rm -rf ./media/tenants
Что удаляется?
БД (требует РУЧНОГО удаления):
⚠️ Схема PostgreSQL НЕ удаляется автоматически из-за auto_drop_schema = False!
Что остаётся в базе после cleanup_tenant или delete_tenant:
PostgreSQL schema: papa ← ОСТАЁТСЯ В БАЗЕ!
├── products_product
├── products_productphoto
├── products_productkit
├── products_productkitphoto
├── products_productcategory
├── products_productcategoryphoto
├── inventory_*
├── orders_*
└── ... (все другие таблицы в schema)
Для полного удаления выполните:
python manage.py shell -c "from django.db import connection; cur = connection.cursor(); cur.execute('DROP SCHEMA IF EXISTS papa CASCADE;')"
Файлы (нужно удалить вручную):
media/tenants/papa/
├── products/
│ ├── {product_id}/{photo_id}/original.jpg
│ ├── {product_id}/{photo_id}/large.webp
│ ├── {product_id}/{photo_id}/medium.webp
│ ├── {product_id}/{photo_id}/thumb.webp
│ └── temp/... (временные файлы)
├── kits/
├── categories/
└── ... (все файлы тенанта)
Полный цикл удаления тенанта
1️⃣ Удалить Client:
python manage.py cleanup_tenant --schema=papa --noinput --purge-registration --delete-files
2️⃣ Удалить схему PostgreSQL:
python manage.py shell -c "from django.db import connection; cur = connection.cursor(); cur.execute('DROP SCHEMA IF EXISTS papa CASCADE;')"
3️⃣ Проверить удаление схемы (опционально):
Test-Path 'c:\Users\team_\Desktop\test_qwen\myproject\media\tenants'
# Должно вернуть: False
Примеры
Пример 1: Удалить тенант "papa"
cd myproject
python manage.py delete_tenant --schema=papa --noinput
Remove-Item -Path 'media/tenants/papa' -Recurse -Force
Пример 2: Удалить все тенанты
# Удалить из БД (нужно сделать для каждого тенанта)
python manage.py delete_tenant --schema=papa --noinput
python manage.py delete_tenant --schema=customer1 --noinput
python manage.py delete_tenant --schema=test --noinput
# Удалить все файлы
Remove-Item -Path 'media/tenants' -Recurse -Force
Пример 3: Список доступных тенантов
# Интерактивный режим - введите ? для списка
python manage.py delete_tenant
# Введите: ?
# Получите: список всех доступных schemas
Важные замечания
⚠️ ВНИМАНИЕ!
- Удаление необратимо - нет возможности восстановления
- Сначала удаляйте из БД, потом удаляйте файлы
- Не забывайте удалять файлы - они занимают место на диске
- При удалении всех тенантов БД может остаться в некорректном состоянии
✅ РЕКОМЕНДАЦИИ:
- Делайте бэкап перед удалением на продакшене
- Используйте
--noinputдля автоматизации скриптами - Удаляйте файлы регулярно, чтобы освободить место
- Проверяйте результат удаления
Ошибки и решения
Ошибка: "EOFError: EOF when reading a line"
Enter Tenant Schema ('?' to list schemas):
EOFError: EOF when reading a line
Решение: Используйте флаг --schema=
python manage.py delete_tenant --schema=papa --noinput
Ошибка: "Tenant doesn't exist"
Error: Tenant doesn't exist
Решение: Проверьте точное имя schema
# Посмотрите список тенантов
python manage.py delete_tenant
# Введите: ?
Файлы не удаляются на Windows
# Если файл заблокирован, закройте все приложения и попробуйте:
Remove-Item -Path 'media/tenants' -Recurse -Force -ErrorAction SilentlyContinue
Команда для быстрого удаления (скрипт)
delete_all_tenants.sh (для Linux/Mac):
#!/bin/bash
python manage.py delete_tenant --schema=papa --noinput
python manage.py delete_tenant --schema=test --noinput
rm -rf ./media/tenants
echo "All tenants deleted"
delete_all_tenants.ps1 (для Windows):
# Удалить БД
python manage.py delete_tenant --schema=papa --noinput
python manage.py delete_tenant --schema=test --noinput
# Удалить файлы
Remove-Item -Path 'media/tenants' -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "All tenants deleted successfully"
Что делать с TenantRegistration?
Проблема
При удалении Client тенанта остается заявка TenantRegistration со статусом 'approved' и tenant=NULL.
Это создает проблему: если клиент захочет повторно зарегистрироваться с тем же поддоменом, система выдаст ошибку:
Error: duplicate key value violates unique constraint 'schema_name'
Потому что в таблице TenantRegistration уже есть запись с schema_name='papa'.
Решение
Вариант 1: Оставить заявку в истории (рекомендуется)
python manage.py cleanup_tenant --schema=papa --noinput
Заявка остается в админке с tenant=NULL. Это:
- ✅ Сохраняет историю регистраций
- ✅ Видна попытка создания магазина (для аналитики)
- ❌ Требует ручного удаления старой заявки перед новой регистрацией
Если клиент захочет зарегистрироваться снова:
- Вручную удалить старую TenantRegistration через админку
- Тогда он сможет создать новую заявку с тем же schema_name
Вариант 2: Удалить заявку автоматически
python manage.py cleanup_tenant --schema=papa --noinput --purge-registration
Удаляет:
- ✅ Client
- ✅ TenantRegistration (полная очистка)
- ❌ Теряется история регистраций
- ⚠️ Требует ручного удаления схемы PostgreSQL
Если клиент захочет зарегистрироваться снова:
- Просто заполняет форму регистрации
- Может использовать тот же schema_name
- Все работает как в первый раз
Мой совет
Для тестового проекта: используй --purge-registration (чище)
python manage.py cleanup_tenant --schema=papa --noinput --purge-registration
Для боевого проекта: оставляй заявку в истории (для аудита)
python manage.py cleanup_tenant --schema=papa --noinput
Вопросы и ответы
Q: Как посмотреть список тенантов?
A: Введите ? в интерактивном режиме:
python manage.py delete_tenant
# Введите: ?
Q: Могу ли я восстановить удаленного тенанта? A: Нет, удаление необратимо. Только из бэкапа БД.
Q: Что если тенант все еще используется? A: Появится ошибка. Закройте все приложения работающие с БД и попробуйте снова.
Q: Как удалить тенант если забыл его schema name?
A: Посмотрите в таблице django_tenants_tenant:
python manage.py shell
# Введите:
from django_tenants.models import Client
for tenant in Client.objects.all():
print(f"{tenant.name} -> {tenant.schema_name}")
Ручной контроль удаления схем (PostgreSQL + django-tenants 3.7.0)
Что важно знать про django-tenants
В django-tenants==3.7.0 удаление тенанта работает так:
- Тенант удаляется через обычный ORM:
tenant.delete() - На модели тенанта (
Client) есть флаг:auto_drop_schema = False # по умолчанию - Если
auto_drop_schema = False:- При
tenant.delete()удаляется только запись в таблице клиентов. - Схема в PostgreSQL (
schema_name) физически остаётся со всеми таблицами и данными.
- При
- Если
auto_drop_schema = True:- При
tenant.delete()будет выполненDROP SCHEMA <schema_name> CASCADE. - Это удобно, но ОЧЕНЬ опасно: любое удаление тенанта через ORM (например, через админку) без дополнительных проверок сразу дропает схему.
- При
Выбранная стратегия: полный ручной контроль
Для этого проекта принято решение:
- В модели
Clientоставить:auto_drop_schema = False - НЕ полагаться на автоматический
auto_drop_schema=True. - Всегда явно контролировать момент, когда схема в PostgreSQL удаляется.
Это даёт:
- ✅ Защиту от случайного дропа схемы через админку или произвольный
.delete(). - ✅ Прозрачный и предсказуемый процесс: схема дропается только явной SQL-командой.
- ✅ Возможность временно сохранить схему для отладки/анализа.
- ❌ Требуется дополнительный шаг — ручное удаление схемы после каждого удаления тенанта.
Рекомендуемый workflow удаления
# Шаг 1: Удалить Client и связанные данные
python manage.py cleanup_tenant --schema=papa --noinput --purge-registration --delete-files
# Шаг 2: Явно удалить схему PostgreSQL
python manage.py shell -c "from django.db import connection; cur = connection.cursor(); cur.execute('DROP SCHEMA IF EXISTS papa CASCADE;')"
# Или через psql напрямую:
# DROP SCHEMA IF EXISTS papa CASCADE;
Практические выводы
-
При удалении тенанта в этом проекте НЕЛЬЗЯ полагаться только на:
python manage.py delete_tenant --schema=... -
Рекомендуется ВСЕГДА использовать двухшаговый процесс:
cleanup_tenantдля удаления Client, TenantRegistration и файлов- Явное
DROP SCHEMA ... CASCADEдля удаления схемы PostgreSQL
-
Если потребуется временно оставить схему (например, для отладки), достаточно:
- Выполнить только шаг 1 (cleanup_tenant)
- Отложить шаг 2 (DROP SCHEMA) на потом
Дата создания: 2025-11-23 Дата обновления: 2025-12-01 Версия: 2.1 Статус: Production Ready ✅
Что нового в версии 2.1:
- 🔴 КРИТИЧЕСКОЕ: Добавлено предупреждение о необходимости ручного удаления схем PostgreSQL
- 📖 Добавлен раздел "Ручной контроль удаления схем" с объяснением стратегии
auto_drop_schema = False - 📖 Обновлены все примеры команд с указанием необходимости ручного удаления схемы
- 📖 Исправлены описания того, что именно удаляют команды
cleanup_tenantиdelete_tenant - ✨ Добавлены три способа удаления схемы вручную (Django shell, консоль, psql)
Что было в версии 2.0:
- ✨ Добавлена новая улучшенная команда
cleanup_tenant - ✨ Команда автоматически обрабатывает TenantRegistration
- ✨ Добавлена опция
--purge-registrationдля удаления заявок - ✨ Добавлена опция
--delete-filesдля удаления физических файлов - 📖 Расширена документация с объяснением проблемы TenantRegistration
- 📖 Добавлены примеры использования для разных сценариев