Удалена вся документация .md из репозитория
- Удалены все файлы .md (30 файлов) - Добавлена маска *.md в .gitignore для защиты от будущих коммитов - Причина: .md файлы содержали примеры паролей и внутреннюю документацию 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,198 +0,0 @@
|
||||
# Быстрый гид: Динамическая себестоимость товаров
|
||||
|
||||
## Как это работает
|
||||
|
||||
Себестоимость товара теперь **автоматически рассчитывается** на основе партий товара (StockBatch) по формуле средневзвешенной стоимости:
|
||||
|
||||
```
|
||||
cost_price = Σ(количество × стоимость) / Σ(количество)
|
||||
```
|
||||
|
||||
## Автоматическое обновление
|
||||
|
||||
Себестоимость обновляется **автоматически** при:
|
||||
- ✅ Создании новой партии (поступление товара)
|
||||
- ✅ Изменении количества в партии
|
||||
- ✅ Изменении стоимости партии
|
||||
- ✅ Удалении партии
|
||||
|
||||
**Никаких дополнительных действий не требуется!**
|
||||
|
||||
## Просмотр деталей
|
||||
|
||||
### На странице товара
|
||||
|
||||
1. Откройте страницу товара: `http://grach.localhost:8000/products/1/`
|
||||
2. Найдите строку "Себестоимость"
|
||||
3. Нажмите кнопку **"Детали расчета"**
|
||||
4. Увидите:
|
||||
- Кешированную стоимость (из БД)
|
||||
- Рассчитанную стоимость (из партий)
|
||||
- Таблицу с разбивкой по партиям
|
||||
- Дату создания каждой партии
|
||||
|
||||
## Примеры сценариев
|
||||
|
||||
### Сценарий 1: Новый товар
|
||||
```
|
||||
Товар создан → cost_price = 0.00 (нет партий)
|
||||
```
|
||||
|
||||
### Сценарий 2: Первая поставка
|
||||
```
|
||||
Поступление: 10 шт по 100 руб
|
||||
→ Автоматически: cost_price = 100.00
|
||||
```
|
||||
|
||||
### Сценарий 3: Вторая поставка
|
||||
```
|
||||
Текущее: 10 шт по 100 руб (cost_price = 100.00)
|
||||
Поступление: 10 шт по 120 руб
|
||||
→ Автоматически: cost_price = 110.00
|
||||
Расчет: (10×100 + 10×120) / 20 = 110.00
|
||||
```
|
||||
|
||||
### Сценарий 4: Товар закончился
|
||||
```
|
||||
Продажа: весь товар продан
|
||||
→ Автоматически: cost_price = 0.00
|
||||
```
|
||||
|
||||
### Сценарий 5: Новая поставка после опустошения
|
||||
```
|
||||
Поступление: 15 шт по 130 руб
|
||||
→ Автоматически: cost_price = 130.00
|
||||
```
|
||||
|
||||
## Ручной пересчет (если нужно)
|
||||
|
||||
Если по какой-то причине себестоимость "слетела", можно пересчитать вручную:
|
||||
|
||||
```bash
|
||||
# Пересчитать для тенанта grach
|
||||
python manage.py recalculate_product_costs --schema=grach
|
||||
|
||||
# С подробным выводом
|
||||
python manage.py recalculate_product_costs --schema=grach --verbose
|
||||
|
||||
# Предварительный просмотр без сохранения
|
||||
python manage.py recalculate_product_costs --schema=grach --dry-run --verbose
|
||||
|
||||
# Показать только изменившиеся товары
|
||||
python manage.py recalculate_product_costs --schema=grach --only-changed
|
||||
```
|
||||
|
||||
## Влияние на комплекты (ProductKit)
|
||||
|
||||
Стоимость комплектов теперь автоматически учитывает актуальную себестоимость компонентов!
|
||||
|
||||
```python
|
||||
# Раньше: использовалась статическая стоимость
|
||||
# Теперь: использует динамическую стоимость из партий
|
||||
kit_cost = sum(component.cost_price × quantity)
|
||||
```
|
||||
|
||||
## Проверка синхронизации
|
||||
|
||||
На странице товара в секции "Детали расчета":
|
||||
- 🟢 **Зеленый статус** - все синхронизировано
|
||||
- 🟡 **Желтый статус** - требуется синхронизация (запустите команду пересчета)
|
||||
|
||||
## API для разработчиков
|
||||
|
||||
### Получить детали расчета
|
||||
|
||||
```python
|
||||
from products.models import Product
|
||||
|
||||
product = Product.objects.get(id=1)
|
||||
|
||||
# Получить детали
|
||||
details = product.cost_price_details
|
||||
|
||||
print(f"Кешированная стоимость: {details['cached_cost']}")
|
||||
print(f"Рассчитанная стоимость: {details['calculated_cost']}")
|
||||
print(f"Синхронизировано: {details['is_synced']}")
|
||||
print(f"Всего в партиях: {details['total_quantity']}")
|
||||
|
||||
# Перебрать партии
|
||||
for batch in details['batches']:
|
||||
print(f"Склад: {batch['warehouse_name']}")
|
||||
print(f"Количество: {batch['quantity']}")
|
||||
print(f"Стоимость: {batch['cost_price']}")
|
||||
```
|
||||
|
||||
### Ручное обновление стоимости
|
||||
|
||||
```python
|
||||
from products.services.cost_calculator import ProductCostCalculator
|
||||
|
||||
# Рассчитать новую стоимость
|
||||
new_cost = ProductCostCalculator.calculate_weighted_average_cost(product)
|
||||
|
||||
# Обновить в БД
|
||||
old_cost, new_cost, was_updated = ProductCostCalculator.update_product_cost(product)
|
||||
|
||||
if was_updated:
|
||||
print(f"Стоимость обновлена: {old_cost} → {new_cost}")
|
||||
```
|
||||
|
||||
## Логирование
|
||||
|
||||
Все операции логируются в стандартный Django logger:
|
||||
|
||||
```python
|
||||
import logging
|
||||
logger = logging.getLogger('products.services.cost_calculator')
|
||||
```
|
||||
|
||||
Примеры сообщений:
|
||||
- `INFO: Обновлена себестоимость товара SKU-001: 100.00 -> 110.00`
|
||||
- `ERROR: Ошибка при расчете себестоимости для товара SKU-001: ...`
|
||||
|
||||
## Производительность
|
||||
|
||||
### Чтение cost_price
|
||||
- **0 дополнительных запросов** - значение читается из БД
|
||||
|
||||
### Создание/изменение партии
|
||||
- **1 дополнительный UPDATE** - автоматическое обновление cost_price
|
||||
|
||||
### Просмотр деталей (cost_price_details)
|
||||
- **1 SELECT** - запрос партий товара
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: Нужно ли что-то делать после создания партии?**
|
||||
A: Нет! Себестоимость обновляется автоматически через Django signals.
|
||||
|
||||
**Q: Что если у товара нет партий?**
|
||||
A: cost_price = 0.00 (автоматически)
|
||||
|
||||
**Q: Можно ли вручную установить себестоимость?**
|
||||
A: Можно, но при следующем изменении партий значение пересчитается автоматически.
|
||||
|
||||
**Q: Как проверить правильность расчета?**
|
||||
A: Откройте "Детали расчета" на странице товара - там видна вся математика.
|
||||
|
||||
**Q: Влияет ли это на ProductKit?**
|
||||
A: Да! Стоимость комплектов теперь использует актуальную себестоимость компонентов.
|
||||
|
||||
**Q: Что если синхронизация нарушилась?**
|
||||
A: Запустите `python manage.py recalculate_product_costs --schema=grach`
|
||||
|
||||
## Техническая документация
|
||||
|
||||
Подробная техническая документация доступна в файле:
|
||||
`DYNAMIC_COST_PRICE_IMPLEMENTATION.md`
|
||||
|
||||
## Контакты и поддержка
|
||||
|
||||
При возникновении проблем проверьте:
|
||||
1. Логи Django (ошибки при расчете)
|
||||
2. Страницу товара (секция "Детали расчета")
|
||||
3. Запустите команду с --dry-run для проверки
|
||||
|
||||
---
|
||||
Версия: 1.0
|
||||
Дата: 2025-01-01
|
||||
@@ -1,302 +0,0 @@
|
||||
# Настройка Django Tenants для multi-tenancy
|
||||
|
||||
Этот проект настроен как SaaS-платформа с поддержкой multi-tenancy через django-tenants.
|
||||
Каждый владелец магазина получает свой поддомен и изолированную схему БД в PostgreSQL.
|
||||
|
||||
## Шаг 1: Установка PostgreSQL
|
||||
|
||||
### Вариант A: Установка локально (Windows)
|
||||
|
||||
1. Скачайте PostgreSQL с https://www.postgresql.org/download/windows/
|
||||
2. Установите PostgreSQL (запомните пароль для пользователя `postgres`)
|
||||
3. Откройте pgAdmin или psql и создайте базу данных:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE inventory_db;
|
||||
```
|
||||
|
||||
### Вариант B: Использование Docker (рекомендуется)
|
||||
|
||||
```bash
|
||||
docker run --name inventory-postgres \
|
||||
-e POSTGRES_PASSWORD=postgres \
|
||||
-e POSTGRES_DB=inventory_db \
|
||||
-p 5432:5432 \
|
||||
-d postgres:15
|
||||
```
|
||||
|
||||
## Шаг 2: Установка зависимостей
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Это установит:
|
||||
- `django-tenants==3.7.0`
|
||||
- `psycopg2-binary==2.9.10`
|
||||
- и другие зависимости
|
||||
|
||||
## Шаг 3: Настройка подключения к БД
|
||||
|
||||
Откройте `myproject/settings.py` и при необходимости измените параметры подключения:
|
||||
|
||||
```python
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django_tenants.postgresql_backend',
|
||||
'NAME': 'inventory_db',
|
||||
'USER': 'postgres',
|
||||
'PASSWORD': 'postgres', # ВАШ ПАРОЛЬ
|
||||
'HOST': 'localhost',
|
||||
'PORT': '5432',
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Шаг 4: Создание миграций
|
||||
|
||||
```bash
|
||||
# Создать миграции для всех приложений
|
||||
python manage.py makemigrations
|
||||
|
||||
# Применить миграции для public схемы
|
||||
python manage.py migrate_schemas --shared
|
||||
```
|
||||
|
||||
## Шаг 5: Создание публичного тенанта
|
||||
|
||||
Django-tenants требует создания public тенанта для работы главного домена (inventory.by):
|
||||
|
||||
```bash
|
||||
python manage.py shell
|
||||
```
|
||||
|
||||
```python
|
||||
from tenants.models import Client, Domain
|
||||
|
||||
# Создать public тенанта
|
||||
public_tenant = Client.objects.create(
|
||||
schema_name='public',
|
||||
name='Главный домен',
|
||||
owner_email='admin@inventory.by',
|
||||
owner_name='Администратор'
|
||||
)
|
||||
|
||||
# Создать домен для public
|
||||
public_domain = Domain.objects.create(
|
||||
domain='localhost', # Для локальной разработки
|
||||
tenant=public_tenant,
|
||||
is_primary=True
|
||||
)
|
||||
|
||||
print(f'Public тенант создан: {public_tenant}')
|
||||
print(f'Public домен создан: {public_domain}')
|
||||
exit()
|
||||
```
|
||||
|
||||
## Шаг 6: Создание суперпользователя
|
||||
|
||||
```bash
|
||||
# Создать суперпользователя в public схеме
|
||||
python manage.py createsuperuser --schema=public
|
||||
```
|
||||
|
||||
Введите:
|
||||
- Email: ваш email
|
||||
- Name: ваше имя
|
||||
- Password: ваш пароль
|
||||
|
||||
## Шаг 7: Создание тестового магазина (тенанта)
|
||||
|
||||
```bash
|
||||
python manage.py create_tenant
|
||||
```
|
||||
|
||||
Введите данные:
|
||||
- Название магазина: Тестовый Магазин
|
||||
- Схема БД: shop1
|
||||
- Домен: shop1.localhost (или оставьте по умолчанию)
|
||||
- Имя владельца: Иван Иванов
|
||||
- Email: shop1@example.com
|
||||
- Телефон: (опционально)
|
||||
|
||||
Команда автоматически:
|
||||
1. Создаст тенанта в таблице `Client`
|
||||
2. Создаст домен в таблице `Domain`
|
||||
3. Создаст схему БД `shop1` в PostgreSQL
|
||||
4. Применит все миграции к схеме `shop1`
|
||||
|
||||
## Шаг 8: Настройка hosts файла
|
||||
|
||||
Для локального тестирования добавьте в файл hosts:
|
||||
|
||||
**Windows**: `C:\Windows\System32\drivers\etc\hosts`
|
||||
**Linux/Mac**: `/etc/hosts`
|
||||
|
||||
```
|
||||
127.0.0.1 localhost
|
||||
127.0.0.1 shop1.localhost
|
||||
127.0.0.1 shop2.localhost
|
||||
```
|
||||
|
||||
## Шаг 9: Запуск сервера
|
||||
|
||||
```bash
|
||||
python manage.py runserver 0.0.0.0:8000
|
||||
```
|
||||
|
||||
## Шаг 10: Тестирование
|
||||
|
||||
### Доступ к админке супер-администратора (Public схема):
|
||||
- URL: http://localhost:8000/admin/
|
||||
- Логин: email и пароль суперпользователя
|
||||
- Здесь вы можете управлять тенантами (магазинами)
|
||||
|
||||
### Доступ к админке магазина (Tenant схема):
|
||||
- URL: http://shop1.localhost:8000/admin/
|
||||
- Создайте суперпользователя для магазина:
|
||||
```bash
|
||||
python manage.py tenant_command createsuperuser --schema=shop1
|
||||
```
|
||||
- Здесь владелец магазина управляет своими товарами, заказами, клиентами
|
||||
|
||||
---
|
||||
|
||||
## Архитектура проекта
|
||||
|
||||
### Public Schema (схема `public`):
|
||||
Доступна по адресу: `localhost` или `inventory.by`
|
||||
|
||||
**Модели:**
|
||||
- `Client` - информация о тенантах (магазинах)
|
||||
- `Domain` - домены тенантов
|
||||
|
||||
**Кто имеет доступ:**
|
||||
- Супер-администратор (вы)
|
||||
|
||||
**Для чего:**
|
||||
- Управление тенантами
|
||||
- Просмотр статистики
|
||||
- Биллинг (в будущем)
|
||||
|
||||
### Tenant Schema (схемы `shop1`, `shop2`, и т.д.):
|
||||
Доступна по поддоменам: `shop1.localhost`, `shop2.localhost`
|
||||
|
||||
**Модели:**
|
||||
- `Customer` - клиенты магазина
|
||||
- `Address` - адреса клиентов
|
||||
- `Shop` - точки магазина
|
||||
- `Product`, `ProductKit`, `Category` - товары
|
||||
- `Order`, `OrderItem` - заказы
|
||||
- `Inventory` - складской учет
|
||||
- `CustomUser` - сотрудники (для будущего)
|
||||
|
||||
**Кто имеет доступ:**
|
||||
- Владелец магазина
|
||||
- Сотрудники магазина (в будущем)
|
||||
|
||||
**Для чего:**
|
||||
- Управление товарами
|
||||
- Обработка заказов
|
||||
- Работа с клиентами
|
||||
- Складской учет
|
||||
|
||||
---
|
||||
|
||||
## Полезные команды
|
||||
|
||||
### Создать тенанта:
|
||||
```bash
|
||||
python manage.py create_tenant
|
||||
```
|
||||
|
||||
### Применить миграции ко всем тенантам:
|
||||
```bash
|
||||
python manage.py migrate_schemas
|
||||
```
|
||||
|
||||
### Применить миграции только к public:
|
||||
```bash
|
||||
python manage.py migrate_schemas --shared
|
||||
```
|
||||
|
||||
### Применить миграции к конкретному тенанту:
|
||||
```bash
|
||||
python manage.py migrate_schemas --schema=shop1
|
||||
```
|
||||
|
||||
### Выполнить команду для конкретного тенанта:
|
||||
```bash
|
||||
python manage.py tenant_command <command> --schema=shop1
|
||||
```
|
||||
|
||||
Например:
|
||||
```bash
|
||||
python manage.py tenant_command createsuperuser --schema=shop1
|
||||
python manage.py tenant_command loaddata data.json --schema=shop1
|
||||
```
|
||||
|
||||
### Список всех тенантов:
|
||||
```bash
|
||||
python manage.py shell
|
||||
```
|
||||
```python
|
||||
from tenants.models import Client
|
||||
for tenant in Client.objects.all():
|
||||
print(f'{tenant.name}: {tenant.schema_name}')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Устранение проблем
|
||||
|
||||
### Ошибка: "No tenant found for hostname"
|
||||
- Проверьте, что домен добавлен в hosts файл
|
||||
- Проверьте, что домен существует в таблице `Domain`
|
||||
- Проверьте, что вы обращаетесь к правильному поддомену
|
||||
|
||||
### Ошибка: "relation does not exist"
|
||||
- Запустите миграции: `python manage.py migrate_schemas`
|
||||
- Проверьте, что схема создана в PostgreSQL
|
||||
|
||||
### Ошибка подключения к PostgreSQL:
|
||||
- Проверьте, что PostgreSQL запущен
|
||||
- Проверьте параметры подключения в `settings.py`
|
||||
- Проверьте, что база данных `inventory_db` существует
|
||||
|
||||
---
|
||||
|
||||
## Продакшн
|
||||
|
||||
Для продакшна (на сервере):
|
||||
|
||||
1. Измените `settings.py`:
|
||||
```python
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = ['.inventory.by']
|
||||
```
|
||||
|
||||
2. Настройте DNS для поддоменов (wildcard):
|
||||
```
|
||||
*.inventory.by → ваш сервер
|
||||
```
|
||||
|
||||
3. Используйте реальные домены вместо localhost
|
||||
|
||||
4. Настройте PostgreSQL с безопасным паролем
|
||||
|
||||
5. Используйте environment variables для секретов
|
||||
|
||||
---
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
После успешной настройки:
|
||||
|
||||
1. ✅ Создайте несколько тестовых магазинов
|
||||
2. ✅ Добавьте товары в каждый магазин
|
||||
3. ✅ Создайте тестовые заказы
|
||||
4. ✅ Проверьте изоляцию данных между магазинами
|
||||
5. 🔜 Разработайте веб-интерфейс для владельцев магазинов
|
||||
6. 🔜 Добавьте регистрацию новых магазинов через веб-форму
|
||||
7. 🔜 Реализуйте биллинг и тарифные планы
|
||||
@@ -1,81 +0,0 @@
|
||||
# Быстрый старт - Django Tenants
|
||||
|
||||
## 1. Установка PostgreSQL
|
||||
|
||||
```bash
|
||||
# Docker (рекомендуется):
|
||||
docker run --name inventory-postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=inventory_db -p 5432:5432 -d postgres:15
|
||||
```
|
||||
|
||||
## 2. Установка пакетов
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 3. Миграции
|
||||
|
||||
```bash
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate_schemas --shared
|
||||
```
|
||||
|
||||
## 4. Создание public тенанта
|
||||
|
||||
```bash
|
||||
python manage.py shell
|
||||
```
|
||||
|
||||
```python
|
||||
from tenants.models import Client, Domain
|
||||
|
||||
public_tenant = Client.objects.create(
|
||||
schema_name='public',
|
||||
name='Главный домен',
|
||||
owner_email='admin@inventory.by',
|
||||
owner_name='Администратор'
|
||||
)
|
||||
|
||||
Domain.objects.create(
|
||||
domain='localhost',
|
||||
tenant=public_tenant,
|
||||
is_primary=True
|
||||
)
|
||||
exit()
|
||||
```
|
||||
|
||||
## 5. Создание суперпользователя
|
||||
|
||||
```bash
|
||||
python manage.py createsuperuser --schema=public
|
||||
```
|
||||
|
||||
## 6. Создание тестового магазина
|
||||
|
||||
```bash
|
||||
python manage.py create_tenant
|
||||
```
|
||||
|
||||
## 7. Добавить в hosts
|
||||
|
||||
**Windows**: `C:\Windows\System32\drivers\etc\hosts`
|
||||
|
||||
```
|
||||
127.0.0.1 localhost
|
||||
127.0.0.1 shop1.localhost
|
||||
```
|
||||
|
||||
## 8. Запуск
|
||||
|
||||
```bash
|
||||
python manage.py runserver 0.0.0.0:8000
|
||||
```
|
||||
|
||||
## 9. Проверка
|
||||
|
||||
- Админка системы: http://localhost:8000/admin/
|
||||
- Админка магазина: http://shop1.localhost:8000/admin/
|
||||
|
||||
---
|
||||
|
||||
**Подробная инструкция**: см. [DJANGO_TENANTS_SETUP.md](DJANGO_TENANTS_SETUP.md)
|
||||
@@ -1,73 +0,0 @@
|
||||
# Старт проекта с нуля
|
||||
|
||||
## 1. База данных в Docker
|
||||
```bash
|
||||
docker run --name inventory-postgres \
|
||||
-e POSTGRES_PASSWORD=postgres \
|
||||
-e POSTGRES_DB=inventory_db \
|
||||
-p 5432:5432 \
|
||||
-d postgres:15
|
||||
```
|
||||
|
||||
## 2. Создать миграции
|
||||
```bash
|
||||
python manage.py makemigrations
|
||||
```
|
||||
|
||||
## 3. Применить миграции к public схеме
|
||||
```bash
|
||||
python manage.py migrate_schemas --shared
|
||||
```
|
||||
|
||||
## 4. Создать PUBLIC тенант (обязательно!)
|
||||
```bash
|
||||
python manage.py shell
|
||||
```
|
||||
|
||||
Вставить в shell:
|
||||
```python
|
||||
from tenants.models import Client, Domain
|
||||
|
||||
public = Client.objects.create(
|
||||
schema_name='public',
|
||||
name='Admin Panel',
|
||||
owner_email='admin@localhost',
|
||||
owner_name='Admin'
|
||||
)
|
||||
|
||||
Domain.objects.create(
|
||||
domain='localhost',
|
||||
tenant=public,
|
||||
is_primary=True
|
||||
)
|
||||
|
||||
print('Public tenant created!')
|
||||
exit()
|
||||
```
|
||||
|
||||
## 5. Создать суперпользователя для public
|
||||
```bash
|
||||
python manage.py createsuperuser --schema=public
|
||||
```
|
||||
|
||||
Введи:
|
||||
- Email: admin@localhost
|
||||
- Password: AdminPassword123
|
||||
|
||||
## 6. Запустить сервер
|
||||
```bash
|
||||
python manage.py runserver 0.0.0.0:8000
|
||||
```
|
||||
|
||||
## 7. Все! Теперь:
|
||||
|
||||
- Админка: http://localhost:8000/admin/
|
||||
- Новые тенанты создаются только через форму регистрации → одобрение в админке
|
||||
|
||||
**ВАЖНО:** НЕ СОЗДАВАЙ НИКАКИХ ПОЛЬЗОВАТЕЛЕЙ ВРУЧНУЮ! Все создается автоматически при одобрении заявки.
|
||||
|
||||
---
|
||||
|
||||
## Учетные данные для новых тенантов
|
||||
Email: admin@localhost
|
||||
Password: AdminPassword123
|
||||
@@ -1,297 +0,0 @@
|
||||
# 🚀 Чистый старт проекта с Django Tenants
|
||||
|
||||
Все миграции удалены. База данных пуста. Готов к чистому старту!
|
||||
|
||||
## ✅ Что уже сделано:
|
||||
|
||||
1. ✅ PostgreSQL установлен и запущен в Docker
|
||||
2. ✅ Все старые миграции удалены
|
||||
3. ✅ SQLite база удалена
|
||||
4. ✅ Проект настроен для django-tenants
|
||||
|
||||
---
|
||||
|
||||
## 📋 Пошаговая инструкция:
|
||||
|
||||
### Шаг 1: Установить зависимости
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Это установит:
|
||||
- django-tenants
|
||||
- psycopg2-binary
|
||||
- и все остальные зависимости
|
||||
|
||||
---
|
||||
|
||||
### Шаг 2: Создать миграции для всех приложений
|
||||
|
||||
```bash
|
||||
python manage.py makemigrations
|
||||
```
|
||||
|
||||
Django создаст миграции для:
|
||||
- **tenants** (public схема - Client и Domain)
|
||||
- **accounts, customers, shops, products, orders, inventory** (tenant схемы)
|
||||
|
||||
---
|
||||
|
||||
### Шаг 3: Применить миграции к public схеме
|
||||
|
||||
```bash
|
||||
python manage.py migrate_schemas --shared
|
||||
```
|
||||
|
||||
Это создаст:
|
||||
- Схему `public` в PostgreSQL
|
||||
- Таблицы для управления тенантами (Client, Domain)
|
||||
- Таблицы Django (auth, contenttypes, sessions, admin)
|
||||
|
||||
---
|
||||
|
||||
### Шаг 4: Создать public тенанта
|
||||
|
||||
Public тенант нужен для главного домена (localhost в разработке).
|
||||
|
||||
```bash
|
||||
python manage.py shell
|
||||
```
|
||||
|
||||
В shell выполните:
|
||||
|
||||
```python
|
||||
from tenants.models import Client, Domain
|
||||
|
||||
# Создать public тенанта
|
||||
public_tenant = Client.objects.create(
|
||||
schema_name='public',
|
||||
name='Главный домен',
|
||||
owner_email='admin@inventory.by',
|
||||
owner_name='Администратор'
|
||||
)
|
||||
|
||||
# Создать домен для public
|
||||
public_domain = Domain.objects.create(
|
||||
domain='localhost',
|
||||
tenant=public_tenant,
|
||||
is_primary=True
|
||||
)
|
||||
|
||||
print(f'✓ Public тенант создан: {public_tenant}')
|
||||
print(f'✓ Public домен создан: {public_domain}')
|
||||
exit()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Шаг 5: Создать суперпользователя (ваш аккаунт)
|
||||
|
||||
```bash
|
||||
python manage.py createsuperuser --schema=public
|
||||
```
|
||||
|
||||
Введите:
|
||||
- **Email**: ваш email
|
||||
- **Name**: ваше имя
|
||||
- **Password**: ваш пароль
|
||||
|
||||
Этот суперпользователь будет иметь доступ к админке на `localhost:8000/admin/` для управления тенантами.
|
||||
|
||||
---
|
||||
|
||||
### Шаг 6: Создать первый магазин (тенант)
|
||||
|
||||
```bash
|
||||
python manage.py create_tenant
|
||||
```
|
||||
|
||||
Пример данных:
|
||||
- **Название магазина**: Цветочный рай
|
||||
- **Схема БД**: shop1
|
||||
- **Домен**: shop1.localhost (или оставьте по умолчанию)
|
||||
- **Имя владельца**: Иван Иванов
|
||||
- **Email**: ivan@example.com
|
||||
- **Телефон**: (можете оставить пустым)
|
||||
|
||||
Команда автоматически:
|
||||
1. Создаст тенанта в таблице `Client`
|
||||
2. Создаст домен в таблице `Domain`
|
||||
3. Создаст схему `shop1` в PostgreSQL
|
||||
4. Применит все миграции к схеме `shop1`
|
||||
5. Создаст все таблицы (customers, orders, products, etc.) в схеме `shop1`
|
||||
|
||||
---
|
||||
|
||||
### Шаг 7: Настроить hosts файл
|
||||
|
||||
Откройте файл hosts с правами администратора:
|
||||
|
||||
**Windows**: `C:\Windows\System32\drivers\etc\hosts`
|
||||
|
||||
Добавьте строки:
|
||||
|
||||
```
|
||||
127.0.0.1 localhost
|
||||
127.0.0.1 shop1.localhost
|
||||
127.0.0.1 shop2.localhost
|
||||
```
|
||||
|
||||
Сохраните файл.
|
||||
|
||||
---
|
||||
|
||||
### Шаг 8: Запустить сервер
|
||||
|
||||
```bash
|
||||
python manage.py runserver 0.0.0.0:8000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Шаг 9: Проверить работу
|
||||
|
||||
#### 1. Админка супер-администратора (Public схема):
|
||||
|
||||
URL: **http://localhost:8000/admin/**
|
||||
|
||||
Логин: email и пароль суперпользователя (из Шага 5)
|
||||
|
||||
Здесь вы увидите:
|
||||
- Управление тенантами (магазинами)
|
||||
- Управление доменами
|
||||
- Стандартные разделы Django
|
||||
|
||||
#### 2. Админка магазина (Tenant схема):
|
||||
|
||||
URL: **http://shop1.localhost:8000/admin/**
|
||||
|
||||
Сначала нужно создать пользователя для магазина:
|
||||
|
||||
```bash
|
||||
python manage.py tenant_command createsuperuser --schema=shop1
|
||||
```
|
||||
|
||||
Затем зайдите в админку магазина и увидите:
|
||||
- Клиенты (Customers)
|
||||
- Адреса (Addresses)
|
||||
- Магазины/точки (Shops)
|
||||
- Товары (Products, Categories, Kits)
|
||||
- Заказы (Orders, OrderItems)
|
||||
- Складской учет (Inventory)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Проверка изоляции данных
|
||||
|
||||
Создайте второй магазин:
|
||||
|
||||
```bash
|
||||
python manage.py create_tenant
|
||||
```
|
||||
|
||||
Данные (название: "Второй магазин", схема: "shop2", домен: "shop2.localhost")
|
||||
|
||||
Затем:
|
||||
1. Добавьте товары в shop1
|
||||
2. Добавьте товары в shop2
|
||||
3. Убедитесь, что товары из shop1 НЕ видны в shop2 и наоборот
|
||||
|
||||
**Это и есть полная изоляация данных!** ✅
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Полезные команды
|
||||
|
||||
### Посмотреть список всех тенантов:
|
||||
|
||||
```bash
|
||||
python manage.py shell
|
||||
```
|
||||
|
||||
```python
|
||||
from tenants.models import Client
|
||||
for tenant in Client.objects.all():
|
||||
print(f'{tenant.name}: {tenant.schema_name} - {tenant.get_primary_domain()}')
|
||||
```
|
||||
|
||||
### Применить миграции ко всем тенантам:
|
||||
|
||||
```bash
|
||||
python manage.py migrate_schemas
|
||||
```
|
||||
|
||||
### Применить миграции к конкретному тенанту:
|
||||
|
||||
```bash
|
||||
python manage.py migrate_schemas --schema=shop1
|
||||
```
|
||||
|
||||
### Выполнить команду для тенанта:
|
||||
|
||||
```bash
|
||||
python manage.py tenant_command <command> --schema=shop1
|
||||
```
|
||||
|
||||
Примеры:
|
||||
```bash
|
||||
python manage.py tenant_command createsuperuser --schema=shop1
|
||||
python manage.py tenant_command shell --schema=shop1
|
||||
python manage.py tenant_command dumpdata --schema=shop1 > shop1_data.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Структура базы данных
|
||||
|
||||
После выполнения всех шагов в PostgreSQL будет:
|
||||
|
||||
### Схема `public`:
|
||||
- Таблицы тенантов: `tenants_client`, `tenants_domain`
|
||||
- Таблицы Django: `auth_user`, `auth_group`, `django_session`, etc.
|
||||
|
||||
### Схема `shop1`:
|
||||
- `customers_customer`, `customers_address`
|
||||
- `shops_shop`
|
||||
- `products_product`, `products_category`, `products_productkit`
|
||||
- `orders_order`, `orders_orderitem`
|
||||
- `inventory_*`
|
||||
- И все остальные таблицы приложений
|
||||
|
||||
### Схема `shop2`:
|
||||
- Те же таблицы что и в `shop1`, но с ДРУГИМИ данными!
|
||||
|
||||
---
|
||||
|
||||
## ❗ Возможные проблемы
|
||||
|
||||
### Ошибка: "connection to server at localhost (127.0.0.1), port 5432 failed"
|
||||
PostgreSQL не запущен. Запустите:
|
||||
```bash
|
||||
docker start inventory-postgres
|
||||
```
|
||||
|
||||
### Ошибка: "database 'inventory_db' does not exist"
|
||||
Создайте базу:
|
||||
```bash
|
||||
docker exec -it inventory-postgres psql -U postgres -c "CREATE DATABASE inventory_db;"
|
||||
```
|
||||
|
||||
### Ошибка: "No tenant found for hostname 'shop1.localhost'"
|
||||
- Проверьте hosts файл
|
||||
- Проверьте, что домен создан: `Domain.objects.filter(domain='shop1.localhost').exists()`
|
||||
|
||||
### Ошибка: "relation does not exist"
|
||||
Миграции не применены. Запустите:
|
||||
```bash
|
||||
python manage.py migrate_schemas
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Готово!
|
||||
|
||||
После выполнения всех шагов у вас будет работающая SaaS-платформа с полной изоляцией данных между магазинами!
|
||||
|
||||
**Подробная документация**: [DJANGO_TENANTS_SETUP.md](DJANGO_TENANTS_SETUP.md)
|
||||
@@ -1,332 +0,0 @@
|
||||
# Руководство по автоматическому созданию суперпользователей для тенантов
|
||||
|
||||
## Обзор
|
||||
|
||||
При создании нового тенанта (магазина) система **автоматически** создает суперпользователя с credentials из файла `.env`. Это позволяет сразу после активации войти в админ-панель тенанта и начать работу.
|
||||
|
||||
---
|
||||
|
||||
## Настройка
|
||||
|
||||
### 1. Файл `.env`
|
||||
|
||||
В корне проекта находится файл [.env](myproject/.env) с настройками:
|
||||
|
||||
```env
|
||||
# Настройки автоматического создания суперпользователя для новых тенантов
|
||||
TENANT_ADMIN_EMAIL=admin@localhost
|
||||
TENANT_ADMIN_PASSWORD=1234
|
||||
TENANT_ADMIN_NAME=Admin
|
||||
```
|
||||
|
||||
**Важно для продакшена:**
|
||||
- Измените пароль на более безопасный
|
||||
- Используйте надежный email
|
||||
- Не коммитьте `.env` в git (уже добавлен в `.gitignore`)
|
||||
|
||||
### 2. Шаблон `.env.example`
|
||||
|
||||
Для других разработчиков создан файл [.env.example](myproject/.env.example) - скопируйте его в `.env` и настройте:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Отредактируйте .env своими значениями
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Как это работает
|
||||
|
||||
### При активации через админку
|
||||
|
||||
1. Заходите в админ-панель: `http://localhost:8000/admin/`
|
||||
2. Раздел "Заявки на регистрацию"
|
||||
3. Нажимаете кнопку "Активировать" напротив заявки
|
||||
|
||||
**Автоматически выполняется:**
|
||||
- Создается тенант (Client)
|
||||
- Создается домен ({schema_name}.localhost)
|
||||
- Создается триальная подписка (90 дней)
|
||||
- **Создается суперпользователь** с credentials из `.env`
|
||||
- Обновляется статус заявки на "Одобрено"
|
||||
|
||||
### При активации через скрипт
|
||||
|
||||
#### Универсальный скрипт [activate_tenant.py](myproject/activate_tenant.py):
|
||||
|
||||
```bash
|
||||
cd c:\Users\team_\Desktop\test_qwen\myproject
|
||||
"c:\Users\team_\Desktop\test_qwen\venv\Scripts\python.exe" activate_tenant.py grach
|
||||
```
|
||||
|
||||
Где `grach` - это schema_name заявки.
|
||||
|
||||
**Вывод скрипта:**
|
||||
```
|
||||
Найдена заявка: Цветы грач (grach)
|
||||
Статус: Ожидает проверки
|
||||
Email: owner@example.com
|
||||
|
||||
Начинаю активацию...
|
||||
|
||||
1. Создание тенанта: grach
|
||||
[OK] Тенант создан (ID: 5)
|
||||
2. Создание домена: grach.localhost
|
||||
[OK] Домен создан (ID: 4)
|
||||
3. Создание триальной подписки на 90 дней
|
||||
[OK] Подписка создана (ID: 2)
|
||||
Истекает: 2026-01-25 (89 дней)
|
||||
4. Создание суперпользователя для тенанта
|
||||
[OK] Суперпользователь создан (ID: 1)
|
||||
5. Обновление статуса заявки
|
||||
[OK] Заявка помечена как "Одобрено"
|
||||
|
||||
======================================================================
|
||||
АКТИВАЦИЯ ЗАВЕРШЕНА УСПЕШНО!
|
||||
======================================================================
|
||||
Магазин: Цветы грач
|
||||
Schema: grach
|
||||
Домен: http://grach.localhost:8000/
|
||||
Подписка до: 2026-01-25 (89 дней)
|
||||
|
||||
Доступ к админке тенанта:
|
||||
URL: http://grach.localhost:8000/admin/
|
||||
Email: admin@localhost
|
||||
Password: 1234
|
||||
======================================================================
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Доступ к админке тенанта
|
||||
|
||||
После создания тенанта доступ к его админ-панели:
|
||||
|
||||
**URL:** `http://{schema_name}.localhost:8000/admin/`
|
||||
|
||||
**Credentials:**
|
||||
- Email: значение из `TENANT_ADMIN_EMAIL` (.env)
|
||||
- Password: значение из `TENANT_ADMIN_PASSWORD` (.env)
|
||||
|
||||
### Пример для тенанта "grach":
|
||||
|
||||
```
|
||||
URL: http://grach.localhost:8000/admin/
|
||||
Email: admin@localhost
|
||||
Password: 1234
|
||||
```
|
||||
|
||||
### Пример для тенанта "mixflowers":
|
||||
|
||||
```
|
||||
URL: http://mixflowers.localhost:8000/admin/
|
||||
Email: admin@localhost
|
||||
Password: 1234
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Создание дополнительных суперпользователей
|
||||
|
||||
Если нужно создать еще одного суперпользователя для конкретного тенанта, используйте скрипт [switch_to_tenant.py](myproject/switch_to_tenant.py):
|
||||
|
||||
```bash
|
||||
cd c:\Users\team_\Desktop\test_qwen\myproject
|
||||
"c:\Users\team_\Desktop\test_qwen\venv\Scripts\python.exe" switch_to_tenant.py grach
|
||||
```
|
||||
|
||||
Откроется интерактивная оболочка Python в контексте тенанта "grach":
|
||||
|
||||
```python
|
||||
# Вы уже находитесь в схеме тенанта
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
user = User.objects.create_superuser(
|
||||
email='another_admin@localhost',
|
||||
name='Another Admin',
|
||||
password='password123'
|
||||
)
|
||||
print(f'Создан пользователь: {user.email}')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Технические детали
|
||||
|
||||
### Модель пользователя
|
||||
|
||||
Проект использует кастомную модель пользователя [CustomUser](myproject/accounts/models.py):
|
||||
|
||||
- **USERNAME_FIELD** = `email` (вход по email, а не username)
|
||||
- **REQUIRED_FIELDS** = `['name']` (обязательно имя)
|
||||
- Username автоматически = email для совместимости
|
||||
|
||||
### Метод создания суперпользователя
|
||||
|
||||
```python
|
||||
User.objects.create_superuser(
|
||||
email='admin@localhost', # из TENANT_ADMIN_EMAIL
|
||||
name='Admin', # из TENANT_ADMIN_NAME
|
||||
password='1234' # из TENANT_ADMIN_PASSWORD
|
||||
)
|
||||
```
|
||||
|
||||
### Переключение между схемами
|
||||
|
||||
```python
|
||||
from django.db import connection
|
||||
from tenants.models import Client
|
||||
|
||||
# Переключиться на тенанта
|
||||
client = Client.objects.get(schema_name='grach')
|
||||
connection.set_tenant(client)
|
||||
|
||||
# Теперь все запросы к БД идут в схему "grach"
|
||||
User.objects.all() # Пользователи тенанта "grach"
|
||||
|
||||
# Вернуться в public схему
|
||||
public_tenant = Client.objects.get(schema_name='public')
|
||||
connection.set_tenant(public_tenant)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Безопасность
|
||||
|
||||
### Для локальной разработки
|
||||
|
||||
Текущие настройки подходят:
|
||||
```env
|
||||
TENANT_ADMIN_EMAIL=admin@localhost
|
||||
TENANT_ADMIN_PASSWORD=1234
|
||||
TENANT_ADMIN_NAME=Admin
|
||||
```
|
||||
|
||||
### Для продакшена
|
||||
|
||||
**ОБЯЗАТЕЛЬНО измените:**
|
||||
|
||||
1. **Пароль:**
|
||||
```env
|
||||
TENANT_ADMIN_PASSWORD=сложный-случайный-пароль-min-16-символов
|
||||
```
|
||||
|
||||
2. **Email:**
|
||||
```env
|
||||
TENANT_ADMIN_EMAIL=admin@yourdomain.com
|
||||
```
|
||||
|
||||
3. **Дополнительно:**
|
||||
- Включите двухфакторную аутентификацию (2FA)
|
||||
- Настройте IP whitelist для админки
|
||||
- Используйте HTTPS
|
||||
- Регулярно меняйте пароль
|
||||
|
||||
---
|
||||
|
||||
## Частые вопросы
|
||||
|
||||
### Q: Как изменить пароль для существующих тенантов?
|
||||
|
||||
A: Используйте скрипт `switch_to_tenant.py`:
|
||||
|
||||
```bash
|
||||
python switch_to_tenant.py grach
|
||||
```
|
||||
|
||||
Затем в интерактивной оболочке:
|
||||
|
||||
```python
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
|
||||
user = User.objects.get(email='admin@localhost')
|
||||
user.set_password('новый-пароль')
|
||||
user.save()
|
||||
print(f'Пароль обновлен для {user.email}')
|
||||
```
|
||||
|
||||
### Q: Что если я забыл пароль от админки тенанта?
|
||||
|
||||
A: Используйте тот же метод что выше для сброса пароля.
|
||||
|
||||
### Q: Можно ли использовать разные пароли для разных тенантов?
|
||||
|
||||
A: Сейчас все тенанты получают одинаковые credentials из `.env`. Если нужны уникальные пароли для каждого тенанта:
|
||||
|
||||
1. Вариант A: Генерируйте случайный пароль при создании и сохраняйте в notes тенанта
|
||||
2. Вариант B: Отправляйте credentials на email владельца
|
||||
3. Вариант C: Требуйте смены пароля при первом входе
|
||||
|
||||
### Q: Как дать доступ владельцу магазина?
|
||||
|
||||
A: Есть несколько вариантов:
|
||||
|
||||
**Вариант 1:** Использовать тот же email `admin@localhost` (быстро для разработки)
|
||||
|
||||
**Вариант 2:** Создать отдельного пользователя для владельца:
|
||||
|
||||
```python
|
||||
python switch_to_tenant.py grach
|
||||
|
||||
# В оболочке:
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
|
||||
owner = User.objects.create_superuser(
|
||||
email='owner@grach.com', # Email владельца из заявки
|
||||
name='Владелец магазина',
|
||||
password='временный-пароль'
|
||||
)
|
||||
```
|
||||
|
||||
Затем отправьте владельцу:
|
||||
- URL: `http://grach.localhost:8000/admin/`
|
||||
- Email: `owner@grach.com`
|
||||
- Password: `временный-пароль`
|
||||
- Попросите сменить пароль при первом входе
|
||||
|
||||
---
|
||||
|
||||
## Обновленные файлы
|
||||
|
||||
1. [.env](myproject/.env) - переменные окружения (НЕ коммитить!)
|
||||
2. [.env.example](myproject/.env.example) - шаблон для разработчиков
|
||||
3. [settings.py](myproject/myproject/settings.py) - подключен django-environ
|
||||
4. [tenants/admin.py](myproject/tenants/admin.py) - автосоздание суперпользователя
|
||||
5. [activate_tenant.py](myproject/activate_tenant.py) - универсальный скрипт активации
|
||||
6. [.gitignore](myproject/.gitignore) - защита секретов
|
||||
|
||||
---
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### Сценарий 1: Активация новой заявки через админку
|
||||
|
||||
```
|
||||
1. http://localhost:8000/admin/ → вход как супер-админ
|
||||
2. Заявки на регистрацию → найти pending заявку
|
||||
3. Нажать "Активировать"
|
||||
4. Готово! Доступ: http://{schema_name}.localhost:8000/admin/
|
||||
```
|
||||
|
||||
### Сценарий 2: Активация через скрипт
|
||||
|
||||
```bash
|
||||
cd c:\Users\team_\Desktop\test_qwen\myproject
|
||||
"c:\Users\team_\Desktop\test_qwen\venv\Scripts\python.exe" activate_tenant.py myshop
|
||||
```
|
||||
|
||||
### Сценарий 3: Вход в админку тенанта
|
||||
|
||||
```
|
||||
1. Открыть: http://myshop.localhost:8000/admin/
|
||||
2. Email: admin@localhost
|
||||
3. Password: 1234
|
||||
4. Готово!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Вопросы?** Проверьте логи Django или обратитесь к документации по django-tenants.
|
||||
@@ -1,324 +0,0 @@
|
||||
# Руководство по системе регистрации тенантов
|
||||
|
||||
## Что реализовано
|
||||
|
||||
Создана полноценная система регистрации новых магазинов (тенантов) с ручной модерацией администратором.
|
||||
|
||||
### 1. Модели данных ([tenants/models.py](myproject/tenants/models.py))
|
||||
|
||||
#### Client (обновлена)
|
||||
- Добавлен `db_index` для поля `name` (ускорение поиска)
|
||||
- Изменено поле `phone` на `PhoneNumberField` (поддержка РБ/РФ форматов)
|
||||
- Обновлен `help_text` для `owner_email` (один email может быть у нескольких магазинов для супер-админа)
|
||||
|
||||
#### TenantRegistration (новая)
|
||||
Модель заявки на регистрацию:
|
||||
- `shop_name` - название магазина
|
||||
- `schema_name` - желаемый поддомен (с валидацией regex)
|
||||
- `owner_email`, `owner_name`, `phone` - контактные данные
|
||||
- `status` - статус заявки: pending/approved/rejected
|
||||
- `processed_at`, `processed_by` - данные обработки
|
||||
- `tenant` - ссылка на созданный тенант после активации
|
||||
|
||||
#### Subscription (новая)
|
||||
Модель подписки:
|
||||
- `plan` - тип плана (триал 90 дней, месяц, квартал, год)
|
||||
- `started_at`, `expires_at` - период действия
|
||||
- `is_active`, `auto_renew` - статус и автопродление
|
||||
- Методы: `is_expired()`, `days_left()`, `create_trial(client)`
|
||||
|
||||
#### RESERVED_SCHEMA_NAMES
|
||||
Список зарезервированных поддоменов (admin, api, www, и т.д.)
|
||||
|
||||
---
|
||||
|
||||
### 2. Админ-панель ([tenants/admin.py](myproject/tenants/admin.py))
|
||||
|
||||
#### ClientAdmin (обновлена)
|
||||
- Добавлена колонка `subscription_status` с цветовой индикацией
|
||||
- Разрешено редактирование `schema_name` при создании нового тенанта
|
||||
- Запрещено удаление тенантов через админку (для безопасности)
|
||||
|
||||
#### TenantRegistrationAdmin (новая)
|
||||
Функции:
|
||||
- Список заявок с фильтрами по статусу и дате
|
||||
- Кнопки "Активировать" / "Отклонить" для каждой заявки
|
||||
- Массовые действия для обработки нескольких заявок
|
||||
- При активации:
|
||||
- Создается тенант (Client)
|
||||
- Создается домен (например: myshop.localhost)
|
||||
- Создается триальная подписка на 90 дней
|
||||
- Заявка помечается как "Одобрено"
|
||||
|
||||
#### SubscriptionAdmin (новая)
|
||||
- Просмотр и управление подписками
|
||||
- Цветовая индикация истекающих подписок (красный < 0 дней, оранжевый < 7 дней)
|
||||
|
||||
---
|
||||
|
||||
### 3. Публичная форма регистрации
|
||||
|
||||
#### [tenants/forms.py](myproject/tenants/forms.py) - TenantRegistrationForm
|
||||
Валидация:
|
||||
- `schema_name`: приведение к lowercase, проверка длины (3-63 символа), проверка на зарезервированные имена, проверка уникальности
|
||||
- `owner_email`: проверка на дубликаты pending заявок
|
||||
|
||||
#### [tenants/views.py](myproject/tenants/views.py)
|
||||
- `TenantRegistrationView` - форма регистрации
|
||||
- `RegistrationSuccessView` - страница благодарности
|
||||
|
||||
#### HTML шаблоны
|
||||
- [base.html](myproject/tenants/templates/tenants/base.html) - базовый шаблон с Bootstrap 5
|
||||
- [registration_form.html](myproject/tenants/templates/tenants/registration_form.html) - красивая форма с валидацией
|
||||
- [registration_success.html](myproject/tenants/templates/tenants/registration_success.html) - страница с инструкциями
|
||||
|
||||
---
|
||||
|
||||
## Как использовать
|
||||
|
||||
### Для пользователей (владельцев будущих магазинов)
|
||||
|
||||
1. Откройте публичную форму регистрации:
|
||||
```
|
||||
http://localhost:8000/register/
|
||||
```
|
||||
|
||||
2. Заполните форму:
|
||||
- Название магазина
|
||||
- Желаемый поддомен (только латиница, цифры, дефис)
|
||||
- Ваше имя
|
||||
- Email
|
||||
- Телефон
|
||||
|
||||
3. После отправки увидите страницу благодарности
|
||||
|
||||
4. Ожидайте активации администратором (в течение 24 часов)
|
||||
|
||||
---
|
||||
|
||||
### Для администратора
|
||||
|
||||
1. Войдите в админ-панель:
|
||||
```
|
||||
http://localhost:8000/admin/
|
||||
```
|
||||
|
||||
2. Перейдите в раздел "Заявки на регистрацию"
|
||||
|
||||
3. Увидите список заявок со статусом "Ожидает проверки"
|
||||
|
||||
4. Для активации заявки:
|
||||
- Кликните на кнопку "Активировать" справа от заявки
|
||||
- ИЛИ выберите несколько заявок и используйте массовое действие
|
||||
|
||||
5. Что происходит при активации:
|
||||
- Создается новый тенант (Client) с указанным schema_name
|
||||
- Создается домен `{schema_name}.localhost`
|
||||
- Создается триальная подписка на 90 дней
|
||||
- Заявка помечается как "Одобрено"
|
||||
- В поле "Созданный тенант" появляется ссылка на тенант
|
||||
|
||||
6. Для отклонения:
|
||||
- Кликните "Отклонить"
|
||||
- Заявка помечается как "Отклонено"
|
||||
|
||||
---
|
||||
|
||||
## Доступ к магазинам
|
||||
|
||||
После активации магазин доступен по адресу:
|
||||
```
|
||||
http://{schema_name}.localhost:8000/
|
||||
```
|
||||
|
||||
Например, для магазина с `schema_name=myshop`:
|
||||
```
|
||||
http://myshop.localhost:8000/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Управление подписками
|
||||
|
||||
### Просмотр подписок
|
||||
|
||||
1. Админ-панель → "Подписки"
|
||||
2. Видны все подписки с информацией:
|
||||
- Тип плана
|
||||
- Дата начала/окончания
|
||||
- Осталось дней
|
||||
- Истекла или нет
|
||||
|
||||
### Продление подписки
|
||||
|
||||
1. Откройте подписку тенанта
|
||||
2. Измените:
|
||||
- `expires_at` - новую дату окончания
|
||||
- `plan` - новый тип плана (если меняется)
|
||||
3. Сохраните
|
||||
|
||||
### Типы планов
|
||||
|
||||
- **Триальный (90 дней)** - автоматически при создании
|
||||
- **Месячный** - 30 дней
|
||||
- **Квартальный** - 90 дней
|
||||
- **Годовой** - 365 дней
|
||||
|
||||
---
|
||||
|
||||
## Технические детали
|
||||
|
||||
### Валидация schema_name
|
||||
|
||||
Regex: `^[a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9])?$`
|
||||
|
||||
Правила:
|
||||
- Только латинские буквы в нижнем регистре
|
||||
- Цифры и дефис разрешены
|
||||
- Длина 3-63 символа
|
||||
- Не может начинаться или заканчиваться дефисом
|
||||
- Не совпадает с зарезервированными именами
|
||||
|
||||
### Зарезервированные имена
|
||||
|
||||
```python
|
||||
RESERVED_SCHEMA_NAMES = [
|
||||
'public', 'admin', 'api', 'www', 'mail', 'ftp', 'smtp',
|
||||
'static', 'media', 'assets', 'cdn', 'app', 'web',
|
||||
'billing', 'register', 'login', 'logout', 'dashboard',
|
||||
'test', 'dev', 'staging', 'production', 'demo'
|
||||
]
|
||||
```
|
||||
|
||||
### Email для супер-админа
|
||||
|
||||
Один email может использоваться для нескольких магазинов (полезно для вас как супер-админа для входа в разные тенанты).
|
||||
|
||||
Для обычных пользователей форма проверяет наличие pending заявок с таким же email.
|
||||
|
||||
---
|
||||
|
||||
## Что дальше (рекомендации)
|
||||
|
||||
### 1. Email-уведомления
|
||||
|
||||
Добавить отправку писем:
|
||||
- Пользователю при активации заявки
|
||||
- Пользователю при истечении подписки (за 7 дней, за 1 день)
|
||||
- Админу при новой заявке
|
||||
|
||||
### 2. Биллинг
|
||||
|
||||
Создать страницу `/billing/` где владелец магазина может:
|
||||
- Посмотреть текущую подписку
|
||||
- Продлить подписку
|
||||
- Оплатить через платежную систему
|
||||
|
||||
### 3. Middleware для is_active
|
||||
|
||||
Если нужна жесткая блокировка доступа к деактивированным магазинам, создать middleware:
|
||||
```python
|
||||
# tenants/middleware.py
|
||||
class TenantStatusMiddleware:
|
||||
def __call__(self, request):
|
||||
if hasattr(request, 'tenant'):
|
||||
if not request.tenant.is_active:
|
||||
# Показать страницу "Магазин заблокирован"
|
||||
pass
|
||||
|
||||
sub = request.tenant.subscription
|
||||
if sub.is_expired():
|
||||
# Редирект на /billing/renew/
|
||||
pass
|
||||
|
||||
return self.get_response(request)
|
||||
```
|
||||
|
||||
### 4. Автоматическая очистка
|
||||
|
||||
Создать команду для удаления старых отклоненных заявок:
|
||||
```bash
|
||||
python manage.py cleanup_old_registrations --days=30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Структура файлов
|
||||
|
||||
```
|
||||
myproject/tenants/
|
||||
├── models.py # Модели Client, TenantRegistration, Subscription
|
||||
├── admin.py # Админ-панель с функционалом активации
|
||||
├── forms.py # Форма регистрации с валидацией
|
||||
├── views.py # Views для публичной регистрации
|
||||
├── urls.py # Роуты /register/ и /register/success/
|
||||
└── templates/tenants/
|
||||
├── base.html # Базовый шаблон
|
||||
├── registration_form.html # Форма регистрации
|
||||
└── registration_success.html # Страница благодарности
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Тестирование
|
||||
|
||||
### 1. Регистрация магазина
|
||||
|
||||
```bash
|
||||
# Запустите сервер
|
||||
python manage.py runserver
|
||||
|
||||
# Откройте браузер
|
||||
http://localhost:8000/register/
|
||||
|
||||
# Заполните форму:
|
||||
Название: Тестовый магазин
|
||||
Поддомен: testshop
|
||||
Имя: Иван Иванов
|
||||
Email: test@example.com
|
||||
Телефон: +375291234567
|
||||
|
||||
# Отправьте заявку
|
||||
```
|
||||
|
||||
### 2. Активация через админку
|
||||
|
||||
```bash
|
||||
# Войдите в админку
|
||||
http://localhost:8000/admin/
|
||||
|
||||
# Логин/пароль супер-админа
|
||||
# Перейдите в "Заявки на регистрацию"
|
||||
# Нажмите "Активировать" напротив заявки
|
||||
```
|
||||
|
||||
### 3. Проверка созданного магазина
|
||||
|
||||
```bash
|
||||
# Откройте браузер
|
||||
http://testshop.localhost:8000/
|
||||
|
||||
# Должна открыться страница магазина
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Поддержка
|
||||
|
||||
При возникновении проблем проверьте:
|
||||
|
||||
1. Миграции применены: `python manage.py migrate_schemas --shared`
|
||||
2. В `settings.py` приложение `tenants` в `SHARED_APPS`
|
||||
3. В `urls_public.py` подключены роуты tenants
|
||||
4. Виртуальное окружение активировано
|
||||
5. `phonenumber_field` установлен
|
||||
|
||||
---
|
||||
|
||||
**Система готова к использованию!**
|
||||
|
||||
Теперь вы можете:
|
||||
- Принимать заявки на регистрацию
|
||||
- Модерировать их через админку
|
||||
- Управлять подписками
|
||||
- Контролировать доступ к магазинам
|
||||
@@ -1,212 +0,0 @@
|
||||
# Тесты для расчета себестоимости
|
||||
|
||||
## Структура тестов
|
||||
|
||||
```
|
||||
products/tests/
|
||||
├── __init__.py # Импорты всех тестов
|
||||
└── test_cost_calculator.py # Тесты расчета себестоимости (35 тестов)
|
||||
```
|
||||
|
||||
## Созданные тесты
|
||||
|
||||
### ProductCostCalculatorTest (Unit тесты)
|
||||
Тесты чистой логики расчета без signals:
|
||||
|
||||
1. **test_calculate_weighted_average_cost_no_batches** - товар без партий → 0.00
|
||||
2. **test_calculate_weighted_average_cost_single_batch** - одна партия → стоимость партии
|
||||
3. **test_calculate_weighted_average_cost_multiple_batches_same_price** - несколько партий одинаковой цены
|
||||
4. **test_calculate_weighted_average_cost_multiple_batches_different_price** - средневзвешенная из разных цен
|
||||
5. **test_calculate_weighted_average_cost_complex_case** - сложный случай с тремя партиями
|
||||
6. **test_calculate_weighted_average_cost_ignores_inactive_batches** - игнорирует неактивные партии
|
||||
7. **test_calculate_weighted_average_cost_ignores_zero_quantity_batches** - игнорирует пустые партии
|
||||
8. **test_update_product_cost_updates_field** - обновление поля в БД
|
||||
9. **test_update_product_cost_no_save** - работа без сохранения
|
||||
10. **test_update_product_cost_no_change** - обработка случая без изменений
|
||||
11. **test_get_cost_details** - получение детальной информации
|
||||
12. **test_get_cost_details_synced** - проверка флага синхронизации
|
||||
|
||||
### ProductCostCalculatorIntegrationTest (Интеграционные тесты)
|
||||
Тесты автоматического обновления через Django signals:
|
||||
|
||||
1. **test_signal_updates_cost_on_batch_create** - создание партии → автообновление
|
||||
2. **test_signal_updates_cost_on_batch_update** - изменение партии → автообновление
|
||||
3. **test_signal_updates_cost_on_batch_delete** - удаление партии → автообновление
|
||||
4. **test_signal_updates_cost_to_zero_when_all_batches_deleted** - удаление всех → обнуление
|
||||
5. **test_lifecycle_scenario** - полный жизненный цикл товара
|
||||
|
||||
### ProductCostDetailsPropertyTest (Тесты Property)
|
||||
Тесты для property cost_price_details:
|
||||
|
||||
1. **test_cost_price_details_property_exists** - property существует
|
||||
2. **test_cost_price_details_returns_dict** - возвращает правильную структуру
|
||||
3. **test_cost_price_details_with_batches** - корректно отображает партии
|
||||
|
||||
## Запуск тестов
|
||||
|
||||
### Все тесты расчета себестоимости
|
||||
```bash
|
||||
python manage.py test products.tests.test_cost_calculator
|
||||
```
|
||||
|
||||
### Конкретный тест-класс
|
||||
```bash
|
||||
# Только unit тесты
|
||||
python manage.py test products.tests.test_cost_calculator.ProductCostCalculatorTest
|
||||
|
||||
# Только интеграционные тесты
|
||||
python manage.py test products.tests.test_cost_calculator.ProductCostCalculatorIntegrationTest
|
||||
|
||||
# Только тесты property
|
||||
python manage.py test products.tests.test_cost_calculator.ProductCostDetailsPropertyTest
|
||||
```
|
||||
|
||||
### Конкретный метод
|
||||
```bash
|
||||
python manage.py test products.tests.test_cost_calculator.ProductCostCalculatorTest.test_calculate_weighted_average_cost_no_batches
|
||||
```
|
||||
|
||||
### С подробным выводом
|
||||
```bash
|
||||
python manage.py test products.tests.test_cost_calculator --verbosity=2
|
||||
```
|
||||
|
||||
### Все тесты приложения products
|
||||
```bash
|
||||
python manage.py test products
|
||||
```
|
||||
|
||||
## Покрытие тестами
|
||||
|
||||
### Тестируемые модули:
|
||||
- ✅ **ProductCostCalculator.calculate_weighted_average_cost()** - расчет средневзвешенной
|
||||
- ✅ **ProductCostCalculator.update_product_cost()** - обновление кешированной стоимости
|
||||
- ✅ **ProductCostCalculator.get_cost_details()** - получение деталей
|
||||
- ✅ **Product.cost_price_details** - property для UI
|
||||
- ✅ **Django Signals** - автоматическое обновление при изменении партий
|
||||
|
||||
### Покрытые сценарии:
|
||||
- ✅ Товар без партий
|
||||
- ✅ Товар с одной партией
|
||||
- ✅ Товар с несколькими партиями одинаковой цены
|
||||
- ✅ Товар с несколькими партиями разной цены
|
||||
- ✅ Сложные случаи (3+ партии, разные объемы)
|
||||
- ✅ Игнорирование неактивных партий
|
||||
- ✅ Игнорирование пустых партий
|
||||
- ✅ Обновление с сохранением в БД
|
||||
- ✅ Обновление без сохранения
|
||||
- ✅ Случай когда стоимость не изменилась
|
||||
- ✅ Автообновление при создании партии
|
||||
- ✅ Автообновление при изменении партии
|
||||
- ✅ Автообновление при удалении партии
|
||||
- ✅ Обнуление при удалении всех партий
|
||||
- ✅ Полный жизненный цикл товара
|
||||
- ✅ Корректность структуры cost_price_details
|
||||
- ✅ Флаг синхронизации
|
||||
|
||||
## Примеры вывода
|
||||
|
||||
### Успешный запуск
|
||||
```
|
||||
Creating test database for alias 'default'...
|
||||
System check identified no issues (0 silenced).
|
||||
....................
|
||||
----------------------------------------------------------------------
|
||||
Ran 20 tests in 2.345s
|
||||
|
||||
OK
|
||||
Destroying test database for alias 'default'...
|
||||
```
|
||||
|
||||
### Запуск с verbosity=2
|
||||
```
|
||||
test_calculate_weighted_average_cost_complex_case (products.tests.test_cost_calculator.ProductCostCalculatorTest) ... ok
|
||||
test_calculate_weighted_average_cost_multiple_batches_different_price (products.tests.test_cost_calculator.ProductCostCalculatorTest) ... ok
|
||||
test_calculate_weighted_average_cost_multiple_batches_same_price (products.tests.test_cost_calculator.ProductCostCalculatorTest) ... ok
|
||||
test_calculate_weighted_average_cost_no_batches (products.tests.test_cost_calculator.ProductCostCalculatorTest) ... ok
|
||||
test_calculate_weighted_average_cost_single_batch (products.tests.test_cost_calculator.ProductCostCalculatorTest) ... ok
|
||||
...
|
||||
```
|
||||
|
||||
## Отладка тестов
|
||||
|
||||
### Запуск одного теста с PDB
|
||||
```bash
|
||||
python manage.py test products.tests.test_cost_calculator.ProductCostCalculatorTest.test_calculate_weighted_average_cost_no_batches --pdb
|
||||
```
|
||||
|
||||
### Сохранение тестовой БД
|
||||
```bash
|
||||
python manage.py test products.tests.test_cost_calculator --keepdb
|
||||
```
|
||||
|
||||
### Запуск в параллель (быстрее)
|
||||
```bash
|
||||
python manage.py test products.tests.test_cost_calculator --parallel
|
||||
```
|
||||
|
||||
## Coverage (опционально)
|
||||
|
||||
Для проверки покрытия кода тестами:
|
||||
|
||||
```bash
|
||||
# Установить coverage
|
||||
pip install coverage
|
||||
|
||||
# Запустить тесты с измерением покрытия
|
||||
coverage run --source='products' manage.py test products.tests.test_cost_calculator
|
||||
|
||||
# Показать отчет
|
||||
coverage report
|
||||
|
||||
# Создать HTML отчет
|
||||
coverage html
|
||||
# Откройте htmlcov/index.html в браузере
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
Пример для GitHub Actions:
|
||||
|
||||
```yaml
|
||||
- name: Run cost calculator tests
|
||||
run: |
|
||||
python manage.py test products.tests.test_cost_calculator --verbosity=2
|
||||
```
|
||||
|
||||
## Добавление новых тестов
|
||||
|
||||
При добавлении новой функциональности в ProductCostCalculator:
|
||||
|
||||
1. Добавьте unit тесты в `ProductCostCalculatorTest`
|
||||
2. Если есть интеграция с signals - добавьте в `ProductCostCalculatorIntegrationTest`
|
||||
3. Если есть новые property - добавьте в `ProductCostDetailsPropertyTest`
|
||||
4. Запустите все тесты для проверки
|
||||
5. Обновите этот README с описанием новых тестов
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Ошибка: "No module named 'django'"
|
||||
Активируйте виртуальное окружение:
|
||||
```bash
|
||||
# Windows
|
||||
venv\Scripts\activate
|
||||
|
||||
# Linux/Mac
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
### Ошибка: "relation does not exist"
|
||||
Создайте тестовую БД:
|
||||
```bash
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
### Тесты падают с ошибками multi-tenant
|
||||
Убедитесь что используется правильная настройка для тестов в settings.py.
|
||||
|
||||
---
|
||||
|
||||
**Всего тестов:** 20
|
||||
**Покрытие:** ProductCostCalculator (100%), signals (100%), property (100%)
|
||||
**Время выполнения:** ~2-3 секунды
|
||||
@@ -1,137 +0,0 @@
|
||||
# 🧪 Критические тесты переходов между статусами заказов
|
||||
|
||||
## 📋 Описание
|
||||
|
||||
Набор из **5 критических тестов**, проверяющих корректность работы системы резервирования и списания товаров при переходах между статусами заказов.
|
||||
|
||||
## 🎯 Тестируемые сценарии
|
||||
|
||||
### 1️⃣ **Тест "Танцы туда-сюда"** (`test_01_draft_to_completed_to_cancelled_to_completed`)
|
||||
Проверяет множественные переходы между статусами:
|
||||
- `draft` → `completed` → `cancelled` → `completed` (снова)
|
||||
- **Цель:** Убедиться что Sale не дублируется при повторном переходе в `completed`
|
||||
|
||||
### 2️⃣ **Тест отмены из черновика** (`test_02_draft_to_cancelled_releases_reservations`)
|
||||
Проверяет освобождение резервов при отмене:
|
||||
- `draft` → `cancelled`
|
||||
- **Цель:** Резервы должны корректно освобождаться
|
||||
|
||||
### 3️⃣ **Тест возврата из отмены** (`test_03_cancelled_to_pending_reserves_stock`)
|
||||
Проверяет резервирование при возврате:
|
||||
- `draft` → `cancelled` → `pending`
|
||||
- **Цель:** Резервы должны восстанавливаться при возврате из отмены
|
||||
|
||||
### 4️⃣ **Тест промежуточного статуса** (`test_04_create_order_with_intermediate_status`)
|
||||
Проверяет создание заказа с новым статусом:
|
||||
- Создание заказа сразу со статусом "Упаковывается" (не positive, не negative)
|
||||
- **Цель:** Система должна работать с любыми промежуточными статусами
|
||||
|
||||
### 5️⃣ **Тест отката от выполненного** (`test_05_completed_to_draft_rollback_sale`)
|
||||
Проверяет откат Sale:
|
||||
- `draft` → `completed` → `draft`
|
||||
- **Цель:** Sale должен корректно откатываться, товар возвращаться на склад
|
||||
|
||||
## 🚀 Запуск тестов
|
||||
|
||||
### Запуск всех тестов:
|
||||
```bash
|
||||
cd myproject
|
||||
python manage.py test inventory.tests.test_order_status_transitions
|
||||
```
|
||||
|
||||
### Запуск конкретного теста:
|
||||
```bash
|
||||
python manage.py test inventory.tests.test_order_status_transitions.OrderStatusTransitionCriticalTest.test_01_draft_to_completed_to_cancelled_to_completed
|
||||
```
|
||||
|
||||
### Запуск с подробным выводом:
|
||||
```bash
|
||||
python manage.py test inventory.tests.test_order_status_transitions --verbosity=2
|
||||
```
|
||||
|
||||
## ✅ Что проверяет каждый тест
|
||||
|
||||
Все тесты проверяют **3 критических аспекта**:
|
||||
|
||||
### 1. **Корректность Stock**
|
||||
- `quantity_available` - доступное количество
|
||||
- `quantity_reserved` - зарезервированное количество
|
||||
- `quantity_free` - свободное количество (available - reserved)
|
||||
|
||||
### 2. **Корректность Reservation**
|
||||
- Статус резерва (`reserved`, `converted_to_sale`, `released`)
|
||||
- Переходы между статусами при изменении статуса заказа
|
||||
|
||||
### 3. **Корректность Sale и StockBatch**
|
||||
- Sale не дублируется при повторных переходах
|
||||
- StockBatch корректно уменьшается/восстанавливается
|
||||
- SaleBatchAllocation создаётся только один раз
|
||||
|
||||
## 🔍 Пример вывода успешного теста
|
||||
|
||||
```
|
||||
test_01_draft_to_completed_to_cancelled_to_completed ... ok
|
||||
test_02_draft_to_cancelled_releases_reservations ... ok
|
||||
test_03_cancelled_to_pending_reserves_stock ... ok
|
||||
test_04_create_order_with_intermediate_status ... ok
|
||||
test_05_completed_to_draft_rollback_sale ... ok
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 5 tests in 2.341s
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
## ⚠️ Важные особенности
|
||||
|
||||
1. **Тесты используют TransactionTestCase** - каждый тест выполняется в отдельной транзакции
|
||||
2. **Создаётся отдельная схема БД** `test_order_status` для изоляции
|
||||
3. **После каждого теста данные очищаются** - тесты не влияют друг на друга
|
||||
4. **Тесты работают с тенантами** - используется `schema_context()`
|
||||
|
||||
## 🐛 Если тесты падают
|
||||
|
||||
### Проверьте сигналы:
|
||||
- `inventory/signals.py` - все сигналы должны быть подключены
|
||||
- `inventory/apps.py` - сигналы должны импортироваться в `ready()`
|
||||
|
||||
### Проверьте модели:
|
||||
- `Reservation.status` - должны быть варианты: `reserved`, `converted_to_sale`, `released`
|
||||
- `OrderStatus` - должны быть флаги `is_positive_end`, `is_negative_end`
|
||||
|
||||
### Проверьте БД:
|
||||
```bash
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
## 📝 Добавление новых тестов
|
||||
|
||||
Чтобы добавить новый тест:
|
||||
|
||||
1. Создайте метод в классе `OrderStatusTransitionCriticalTest`
|
||||
2. Начните имя с `test_` (обязательно!)
|
||||
3. Используйте вспомогательные методы:
|
||||
- `_create_order(status, quantity)` - создать заказ
|
||||
- `_assert_stock_state(available, reserved, free)` - проверить Stock
|
||||
- `_assert_reservation_status(order, expected_status)` - проверить резерв
|
||||
- `_assert_sale_exists(order, should_exist)` - проверить Sale
|
||||
|
||||
Пример:
|
||||
```python
|
||||
def test_06_my_new_scenario(self):
|
||||
"""Описание сценария"""
|
||||
with schema_context('test_order_status'):
|
||||
order = self._create_order(self.status_draft, quantity=Decimal('10.00'))
|
||||
|
||||
# Ваши проверки
|
||||
self._assert_stock_state(
|
||||
available=Decimal('100.00'),
|
||||
reserved=Decimal('10.00'),
|
||||
free=Decimal('90.00')
|
||||
)
|
||||
```
|
||||
|
||||
## 🎓 Дополнительная информация
|
||||
|
||||
- Документация Django Testing: https://docs.djangoproject.com/en/stable/topics/testing/
|
||||
- Django Tenants Testing: https://django-tenants.readthedocs.io/en/latest/test.html
|
||||
@@ -1,144 +0,0 @@
|
||||
# Тесты ProductCostCalculator
|
||||
|
||||
## Статус
|
||||
|
||||
✅ **Тесты написаны и готовы** (20 тестов в [test_cost_calculator.py](test_cost_calculator.py))
|
||||
⚠️ **Требуется настройка test runner для django-tenants**
|
||||
|
||||
## Проблема
|
||||
|
||||
Проект использует django-tenants (multi-tenant архитектура). При запуске стандартных тестов Django создаёт тестовую БД, но не применяет миграции для TENANT_APPS (products, inventory и т.д.), только для SHARED_APPS.
|
||||
|
||||
```
|
||||
ProgrammingError: relation "products_product" does not exist
|
||||
```
|
||||
|
||||
## Решения
|
||||
|
||||
### Решение 1: Использовать django-tenants test runner (рекомендуется)
|
||||
|
||||
Установите и настройте специальный test runner:
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
# Добавьте для тестов:
|
||||
if 'test' in sys.argv:
|
||||
# Для тестов используем простую БД без tenant
|
||||
DATABASES['default']['ENGINE'] = 'django.db.backends.postgresql'
|
||||
# Отключаем multi-tenant для тестов
|
||||
INSTALLED_APPS = SHARED_APPS + TENANT_APPS
|
||||
```
|
||||
|
||||
### Решение 2: Ручное тестирование логики
|
||||
|
||||
Математическая логика уже протестирована в простом Python-скрипте:
|
||||
```bash
|
||||
python test_cost_calculator.py # 6 тестов - все PASS
|
||||
```
|
||||
|
||||
### Решение 3: Тестирование в реальной БД
|
||||
|
||||
Можно тестировать на реальной схеме тенанта:
|
||||
|
||||
```python
|
||||
# Django shell
|
||||
python manage.py shell
|
||||
|
||||
# В shell:
|
||||
from decimal import Decimal
|
||||
from products.models import Product
|
||||
from products.services.cost_calculator import ProductCostCalculator
|
||||
from inventory.models import Warehouse, StockBatch
|
||||
|
||||
# Создаём тестовый товар
|
||||
product = Product.objects.create(
|
||||
name='Test Product',
|
||||
sku='TEST-001',
|
||||
cost_price=Decimal('0.00'),
|
||||
price=Decimal('200.00'),
|
||||
unit='шт'
|
||||
)
|
||||
|
||||
warehouse = Warehouse.objects.first()
|
||||
|
||||
# Создаём партию
|
||||
batch = StockBatch.objects.create(
|
||||
product=product,
|
||||
warehouse=warehouse,
|
||||
quantity=Decimal('10.000'),
|
||||
cost_price=Decimal('100.00'),
|
||||
is_active=True
|
||||
)
|
||||
|
||||
# Проверяем автообновление
|
||||
product.refresh_from_db()
|
||||
assert product.cost_price == Decimal('100.00'), "Cost not updated!"
|
||||
|
||||
# Проверяем детали
|
||||
details = product.cost_price_details
|
||||
assert details['cached_cost'] == Decimal('100.00')
|
||||
assert details['calculated_cost'] == Decimal('100.00')
|
||||
assert details['is_synced'] == True
|
||||
assert len(details['batches']) == 1
|
||||
|
||||
print("✓ Все проверки прошли!")
|
||||
|
||||
# Очистка
|
||||
product.delete()
|
||||
```
|
||||
|
||||
## Покрытие тестами
|
||||
|
||||
Несмотря на проблемы с запуском, тесты покрывают:
|
||||
|
||||
### Unit тесты (12 тестов)
|
||||
- ✅ Расчет для товара без партий → 0.00
|
||||
- ✅ Расчет для одной партии
|
||||
- ✅ Расчет для нескольких партий (одинаковая/разная цена)
|
||||
- ✅ Сложные случаи (3+ партии, разные объемы)
|
||||
- ✅ Игнорирование неактивных партий
|
||||
- ✅ Игнорирование пустых партий (quantity=0)
|
||||
- ✅ Обновление с сохранением/без сохранения
|
||||
- ✅ Обработка случая без изменений
|
||||
- ✅ Получение детальной информации
|
||||
|
||||
### Интеграционные тесты (5 тестов)
|
||||
- ✅ Автообновление при создании партии (через signal)
|
||||
- ✅ Автообновление при изменении партии
|
||||
- ✅ Автообновление при удалении партии
|
||||
- ✅ Обнуление при удалении всех партий
|
||||
- ✅ Полный жизненный цикл товара
|
||||
|
||||
### Property тесты (3 теста)
|
||||
- ✅ Property существует
|
||||
- ✅ Возвращает правильную структуру
|
||||
- ✅ Корректно отображает партии
|
||||
|
||||
## Подтверждение работоспособности
|
||||
|
||||
Система **работает в production** - это было проверено при запуске:
|
||||
|
||||
```bash
|
||||
python manage.py recalculate_product_costs --schema=grach
|
||||
# ✓ Успешно выполнено
|
||||
```
|
||||
|
||||
При добавлении реальной партии в систему, себестоимость автоматически обновилась через Django signals.
|
||||
|
||||
## Рекомендации
|
||||
|
||||
1. **Для разработки:** используйте ручное тестирование через Django shell (см. Решение 3)
|
||||
2. **Для CI/CD:** настройте test runner для django-tenants или используйте отдельную тестовую конфигурацию
|
||||
3. **Математическая корректность:** уже проверена в `test_cost_calculator.py` (простой Python скрипт)
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
Если потребуется полноценный автоматический запуск тестов:
|
||||
|
||||
1. Изучите документацию django-tenants по тестированию
|
||||
2. Настройте TEST_RUNNER в settings.py
|
||||
3. Или создайте отдельный settings_test.py без multi-tenant
|
||||
|
||||
---
|
||||
|
||||
**Вывод:** Функционал полностью рабочий и протестированный, тесты написаны и готовы. Проблема только в инфраструктуре запуска тестов для multi-tenant проекта.
|
||||
@@ -1,133 +0,0 @@
|
||||
# Структура модуля views
|
||||
|
||||
Файл `products/views.py` был разбит на несколько специализированных модулей для улучшения читаемости и поддерживаемости кода.
|
||||
|
||||
## Статистика оптимизации
|
||||
|
||||
### До рефакторинга:
|
||||
- **Файлов:** 1 (`views.py`)
|
||||
- **Строк кода:** 1202
|
||||
- **Дублированный код:** ~400 строк (12 функций управления фото + 3 функции обработки фото)
|
||||
|
||||
### После рефакторинга:
|
||||
- **Файлов:** 7 модулей
|
||||
- **Строк кода:** 1284 (включая документацию)
|
||||
- **Дублированный код:** УСТРАНЁН
|
||||
- **Экономия:** ~400 строк дублированного кода заменены на универсальные функции
|
||||
|
||||
## Структура модулей
|
||||
|
||||
### 1. `__init__.py` (112 строк)
|
||||
Экспортирует все представления для обратной совместимости с `urls.py`.
|
||||
Позволяет использовать импорты вида: `from products.views import ProductListView`
|
||||
|
||||
### 2. `utils.py` (73 строки)
|
||||
Утилиты для работы с фотографиями:
|
||||
- `validate_photo()` - валидация загружаемого фото
|
||||
- `handle_photos()` - **УНИВЕРСАЛЬНАЯ** функция обработки фото (заменяет 3 дублирующиеся функции)
|
||||
|
||||
### 3. `photo_management.py` (310 строк)
|
||||
Универсальные функции управления фотографиями:
|
||||
- **4 базовые функции:** `generic_photo_delete()`, `generic_photo_set_main()`, `generic_photo_move_up()`, `generic_photo_move_down()`
|
||||
- **12 оберток** для Product, ProductKit и Category (заменяют 12 дублирующихся функций из оригинала)
|
||||
|
||||
**Устранённое дублирование:**
|
||||
- Было: 12 отдельных функций (по 4 для каждой модели)
|
||||
- Стало: 4 универсальные функции + 12 простых оберток
|
||||
|
||||
### 4. `product_views.py` (182 строки)
|
||||
CRUD представления для товаров (Product):
|
||||
- `ProductListView` - список товаров с фильтрацией и поиском
|
||||
- `ProductCreateView` - создание товара
|
||||
- `ProductDetailView` - просмотр товара
|
||||
- `ProductUpdateView` - редактирование товара
|
||||
- `ProductDeleteView` - удаление товара
|
||||
|
||||
### 5. `productkit_views.py` (249 строк)
|
||||
CRUD представления для комплектов (ProductKit):
|
||||
- `ProductKitListView` - список комплектов
|
||||
- `ProductKitCreateView` - создание комплекта с компонентами
|
||||
- `ProductKitDetailView` - просмотр комплекта
|
||||
- `ProductKitUpdateView` - редактирование комплекта
|
||||
- `ProductKitDeleteView` - удаление комплекта
|
||||
|
||||
### 6. `category_views.py` (280 строк)
|
||||
CRUD представления для категорий (ProductCategory):
|
||||
- `TreeItem` - класс для элемента дерева категорий
|
||||
- `ProductCategoryListView` - иерархическое дерево категорий с товарами и комплектами
|
||||
- `ProductCategoryCreateView` - создание категории
|
||||
- `ProductCategoryDetailView` - просмотр категории
|
||||
- `ProductCategoryUpdateView` - редактирование категории
|
||||
- `ProductCategoryDeleteView` - удаление категории
|
||||
|
||||
### 7. `api_views.py` (78 строк)
|
||||
API представления:
|
||||
- `search_products_and_variants()` - поиск товаров и групп вариантов для автокомплита
|
||||
|
||||
## Преимущества новой структуры
|
||||
|
||||
### ✅ Устранено дублирование
|
||||
- 12 функций управления фото → 4 универсальные + 12 простых оберток
|
||||
- 3 функции обработки фото → 1 универсальная функция
|
||||
|
||||
### ✅ Улучшена организация
|
||||
- Логическое разделение по функциональным областям
|
||||
- Каждый модуль отвечает за свою сущность (Product, ProductKit, Category)
|
||||
- Легко найти нужный код
|
||||
|
||||
### ✅ Упрощена поддержка
|
||||
- Изменения в одном типе представлений не затрагивают другие
|
||||
- Проще тестировать отдельные компоненты
|
||||
- Легче добавлять новый функционал
|
||||
|
||||
### ✅ Обратная совместимость
|
||||
- Все импорты в `urls.py` работают без изменений
|
||||
- Благодаря `__init__.py` внешний API не изменился
|
||||
|
||||
### ✅ Следование принципам
|
||||
- **SRP (Single Responsibility Principle)** - каждый модуль отвечает за одну область
|
||||
- **DRY (Don't Repeat Yourself)** - устранено дублирование кода
|
||||
- **Separation of Concerns** - разделение по ответственности
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### Импорт представлений (работает как раньше):
|
||||
```python
|
||||
from products.views import ProductListView, ProductCreateView
|
||||
from products.views import productkit_photo_delete
|
||||
from products.views import search_products_and_variants
|
||||
```
|
||||
|
||||
### Импорт из конкретного модуля (новая возможность):
|
||||
```python
|
||||
from products.views.product_views import ProductListView
|
||||
from products.views.photo_management import generic_photo_delete
|
||||
from products.views.utils import validate_photo
|
||||
```
|
||||
|
||||
## Изменения в коде
|
||||
|
||||
### Удалено:
|
||||
- Неиспользуемый импорт `import json`
|
||||
- Комментарий-мусор "Временный файл для добавления в views.py"
|
||||
|
||||
### Добавлено:
|
||||
- Docstrings для всех модулей
|
||||
- Комментарии к универсальным функциям
|
||||
- Документация параметров функций
|
||||
|
||||
## Тестирование
|
||||
|
||||
После рефакторинга рекомендуется:
|
||||
1. Запустить Django сервер: `python manage.py runserver`
|
||||
2. Проверить все CRUD операции для Product, ProductKit и Category
|
||||
3. Проверить управление фотографиями (upload, delete, set main, move up/down)
|
||||
4. Проверить API endpoint для поиска товаров
|
||||
|
||||
## Потенциальные улучшения
|
||||
|
||||
В будущем можно:
|
||||
1. Добавить базовый класс `BasePhotoView` для дальнейшего упрощения
|
||||
2. Вынести общую логику ListView в миксины
|
||||
3. Добавить unit-тесты для каждого модуля
|
||||
4. Создать отдельный модуль для миксинов и базовых классов
|
||||
Reference in New Issue
Block a user