Files
octopus/УДАЛЕНИЕ_ТЕНАНТОВ.md
Andrey Smakotin e0437cdb5a Исправлено двойное списание товаров при смене статуса заказа
Проблема:
- При изменении статуса заказа на 'Выполнен' товар списывался дважды
- Заказ на 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 - документация по исправлению
2025-12-01 00:56:26 +03:00

588 lines
23 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Удаление Тенантов в Django-Tenants
## ⚠️ КРИТИЧЕСКИ ВАЖНО
**В этом проекте `auto_drop_schema = False`, поэтому ВСЕ команды удаления (включая `cleanup_tenant` и `delete_tenant`) НЕ удаляют схему из PostgreSQL автоматически!**
После удаления тенанта через любую команду **ОБЯЗАТЕЛЬНО** нужно вручную удалить схему из базы данных, иначе:
- ✅ Запись `Client` удалится
- ❌ Схема со всеми таблицами и данными останется в PostgreSQL
- ❌ При повторной регистрации с тем же именем новый тенант увидит старые данные!
### Как удалить схему вручную:
**Вариант 1 — через Django shell:**
```python
from django.db import connection
with connection.cursor() as cursor:
cursor.execute('DROP SCHEMA IF EXISTS имя_схемы CASCADE;')
```
**Вариант 2 — одной командой из консоли:**
```bash
python manage.py shell -c "from django.db import connection; cur = connection.cursor(); cur.execute('DROP SCHEMA IF EXISTS имя_схемы CASCADE;')"
```
**Вариант 3 — напрямую в PostgreSQL (psql):**
```sql
DROP SCHEMA IF EXISTS имя_схемы CASCADE;
```
---
## Быстрая справка
### Рекомендуемый способ (с улучшенной командой):
```bash
# Базовое удаление (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` (см. выше) или удалите схему вручную.
```bash
# Удалить конкретного тенанта (только запись 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 (см. раздел выше).
#### Параметры:
```bash
--schema=<имя> # Имя тенанта (обязательно)
--noinput # Не запрашивать подтверждение
--purge-registration # Удалить TenantRegistration (иначе оставляет с tenant=NULL)
--delete-files # Удалить физические файлы из /media/tenants/
```
#### Варианты использования:
**1⃣ Базовое удаление (рекомендуется):**
```bash
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⃣ Полная очистка:**
```bash
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⃣ Удаление с заявкой:**
```bash
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
**Команда:**
```bash
cd myproject
python manage.py delete_tenant --schema=papa --noinput
```
**Параметры:**
- `--schema=papa` - Удалить тенант с именем schema `papa`
- `--noinput` - Не запрашивать подтверждение (автоматический режим)
**Результат:**
```
Deleting 'papa'
Deleted 'papa'
```
⚠️ **ВНИМАНИЕ:** В этом проекте `auto_drop_schema = False`, поэтому команда `delete_tenant` **НЕ удаляет схему из PostgreSQL**!
Эта команда удаляет:
- ✅ Запись Client из таблицы тенантов
-НЕ удаляет схему PostgreSQL (остаются все таблицы и данные!)
-НЕ удаляет файлы в `/media/tenants/{tenant_id}/`
После выполнения нужно вручную удалить схему (см. раздел "КРИТИЧЕСКИ ВАЖНО" выше).
---
### Способ 2: Интерактивное удаление (с выбором)
**Команда:**
```bash
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):**
```powershell
Remove-Item -Path 'c:\Users\team_\Desktop\test_qwen\myproject\media\tenants' -Recurse -Force
Write-Host 'Removed tenants directory'
```
**На Windows (CMD):**
```cmd
rmdir /s /q "c:\Users\team_\Desktop\test_qwen\myproject\media\tenants"
```
**На Linux/Mac:**
```bash
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)
```
Для полного удаления выполните:
```bash
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:
```bash
python manage.py cleanup_tenant --schema=papa --noinput --purge-registration --delete-files
```
### 2⃣ Удалить схему PostgreSQL:
```bash
python manage.py shell -c "from django.db import connection; cur = connection.cursor(); cur.execute('DROP SCHEMA IF EXISTS papa CASCADE;')"
```
### 3⃣ Проверить удаление схемы (опционально):
```powershell
Test-Path 'c:\Users\team_\Desktop\test_qwen\myproject\media\tenants'
# Должно вернуть: False
```
---
## Примеры
### Пример 1: Удалить тенант "papa"
```bash
cd myproject
python manage.py delete_tenant --schema=papa --noinput
```
```powershell
Remove-Item -Path 'media/tenants/papa' -Recurse -Force
```
### Пример 2: Удалить все тенанты
```bash
# Удалить из БД (нужно сделать для каждого тенанта)
python manage.py delete_tenant --schema=papa --noinput
python manage.py delete_tenant --schema=customer1 --noinput
python manage.py delete_tenant --schema=test --noinput
```
```powershell
# Удалить все файлы
Remove-Item -Path 'media/tenants' -Recurse -Force
```
### Пример 3: Список доступных тенантов
```bash
# Интерактивный режим - введите ? для списка
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=`
```bash
python manage.py delete_tenant --schema=papa --noinput
```
### Ошибка: "Tenant doesn't exist"
```
Error: Tenant doesn't exist
```
**Решение:** Проверьте точное имя schema
```bash
# Посмотрите список тенантов
python manage.py delete_tenant
# Введите: ?
```
### Файлы не удаляются на Windows
```powershell
# Если файл заблокирован, закройте все приложения и попробуйте:
Remove-Item -Path 'media/tenants' -Recurse -Force -ErrorAction SilentlyContinue
```
---
## Команда для быстрого удаления (скрипт)
**delete_all_tenants.sh (для Linux/Mac):**
```bash
#!/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):**
```powershell
# Удалить БД
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: Оставить заявку в истории (рекомендуется)**
```bash
python manage.py cleanup_tenant --schema=papa --noinput
```
Заявка остается в админке с `tenant=NULL`. Это:
- ✅ Сохраняет историю регистраций
- ✅ Видна попытка создания магазина (для аналитики)
- ❌ Требует ручного удаления старой заявки перед новой регистрацией
**Если клиент захочет зарегистрироваться снова:**
1. Вручную удалить старую TenantRegistration через админку
2. Тогда он сможет создать новую заявку с тем же schema_name
**Вариант 2: Удалить заявку автоматически**
```bash
python manage.py cleanup_tenant --schema=papa --noinput --purge-registration
```
Удаляет:
- ✅ Client
- ✅ TenantRegistration (полная очистка)
- ❌ Теряется история регистраций
- ⚠️ Требует ручного удаления схемы PostgreSQL
**Если клиент захочет зарегистрироваться снова:**
1. Просто заполняет форму регистрации
2. Может использовать тот же schema_name
3. Все работает как в первый раз
### Мой совет
Для **тестового проекта**: используй `--purge-registration` (чище)
```bash
python manage.py cleanup_tenant --schema=papa --noinput --purge-registration
```
Для **боевого проекта**: оставляй заявку в истории (для аудита)
```bash
python manage.py cleanup_tenant --schema=papa --noinput
```
---
## Вопросы и ответы
**Q: Как посмотреть список тенантов?**
A: Введите `?` в интерактивном режиме:
```bash
python manage.py delete_tenant
# Введите: ?
```
**Q: Могу ли я восстановить удаленного тенанта?**
A: Нет, удаление необратимо. Только из бэкапа БД.
**Q: Что если тенант все еще используется?**
A: Появится ошибка. Закройте все приложения работающие с БД и попробуйте снова.
**Q: Как удалить тенант если забыл его schema name?**
A: Посмотрите в таблице `django_tenants_tenant`:
```bash
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:
```python
tenant.delete()
```
- На модели тенанта (`Client`) есть флаг:
```python
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` оставить:
```python
auto_drop_schema = False
```
- НЕ полагаться на автоматический `auto_drop_schema=True`.
- Всегда явно контролировать момент, когда схема в PostgreSQL удаляется.
Это даёт:
- ✅ Защиту от случайного дропа схемы через админку или произвольный `.delete()`.
- ✅ Прозрачный и предсказуемый процесс: схема дропается только явной SQL-командой.
- ✅ Возможность временно сохранить схему для отладки/анализа.
- ❌ Требуется дополнительный шаг — ручное удаление схемы после каждого удаления тенанта.
### Рекомендуемый workflow удаления
```bash
# Шаг 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;
```
### Практические выводы
- При удалении тенанта в этом проекте НЕЛЬЗЯ полагаться только на:
```bash
python manage.py delete_tenant --schema=...
```
- Рекомендуется ВСЕГДА использовать двухшаговый процесс:
1. `cleanup_tenant` для удаления Client, TenantRegistration и файлов
2. Явное `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
- 📖 Добавлены примеры использования для разных сценариев