Удалена вся документация .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:
2025-12-02 15:05:46 +03:00
parent 387f5dfdb4
commit 384f3c22f8
31 changed files with 3 additions and 7947 deletions

View File

@@ -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

View File

@@ -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. 🔜 Реализуйте биллинг и тарифные планы

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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.

View File

@@ -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` установлен
---
**Система готова к использованию!**
Теперь вы можете:
- Принимать заявки на регистрацию
- Модерировать их через админку
- Управлять подписками
- Контролировать доступ к магазинам

View File

@@ -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 секунды

View File

@@ -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

View File

@@ -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 проекта.

View File

@@ -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. Создать отдельный модуль для миксинов и базовых классов