Corp/docs/in-progress/PROCUREMENT_REFACTORING_PLAN.md

24 KiB
Raw Blame History

План рефакторинга модуля Procurement для pip-публикации

Цель: Сделать модуль procurement независимым и готовым к публикации на PyPI.

Статус: 🚧 В разработке
Дата начала: 27 февраля 2026
Оценка времени: 2-3 недели
Риск: ⚠️ Средний (модуль не в проде, но есть тесты)


📊 Анализ текущего состояния

Проблемы tight coupling:

19 жестких связей:

# ❌ Текущее состояние
from employees.models import Department  # Жесткая зависимость
User = get_user_model()                  # Предполагает employees.Employee

ProcurementRequest:
  - department: ForeignKey(Department)           # 1
  - requestor: ForeignKey(User)                  # 2
  - executor: ForeignKey(User)                   # 3
  
ProcurementItem:
  - request: ForeignKey(ProcurementRequest)      # OK (внутренняя)
  
Approval:
  - request: ForeignKey(ProcurementRequest)      # OK
  - approver: ForeignKey(User)                   # 4
  
EquipmentCategory:
  - parent: ForeignKey('self')                   # OK
  
Equipment:
  - category: ForeignKey(EquipmentCategory)      # OK
  - department: ForeignKey(Department)           # 5
  - responsible_person: ForeignKey(User)         # 6
  - procurement_request: ForeignKey(...)         # OK
  
MaintenanceRecord:
  - equipment: ForeignKey(Equipment)             # OK
  - performed_by: ForeignKey(User)               # 7
  
EquipmentTransfer:
  - equipment: ForeignKey(Equipment)             # OK
  - from_department: ForeignKey(Department)      # 8
  - to_department: ForeignKey(Department)        # 9
  - from_person: ForeignKey(User)                # 10
  - to_person: ForeignKey(User)                  # 11
  - created_by: ForeignKey(User)                 # 12
  
Budget:
  - department: ForeignKey(Department)           # 13

Итого:

  • 13 зависимостей от внешних моделей
  • 9 ссылок на User (Employee)
  • 4 ссылки на Department

🎯 Целевая архитектура

Принципы pip-ready модуля:

  1. Swappable models - модели настраиваются через settings
  2. Abstract базы - переиспользуемая логика
  3. Минимальные зависимости - только Django core
  4. Type hints - полная типизация
  5. Документация - docstrings everywhere

Целевая структура:

# procurement/settings.py (новый файл)
from django.conf import settings

PROCUREMENT_USER_MODEL = getattr(
    settings, 
    'PROCUREMENT_USER_MODEL', 
    settings.AUTH_USER_MODEL
)

PROCUREMENT_DEPARTMENT_MODEL = getattr(
    settings,
    'PROCUREMENT_DEPARTMENT_MODEL',
    'employees.Department'  # default для EUSRR
)

# procurement/models.py
from django.contrib.auth import get_user_model
from .conf import PROCUREMENT_USER_MODEL, PROCUREMENT_DEPARTMENT_MODEL

class ProcurementRequest(models.Model):
    # ✅ Настраиваемые модели
    department = models.ForeignKey(
        PROCUREMENT_DEPARTMENT_MODEL,
        on_delete=models.PROTECT,
        ...
    )
    requestor = models.ForeignKey(
        PROCUREMENT_USER_MODEL,
        on_delete=models.PROTECT,
        ...
    )

📋 План рефакторинга (10 шагов)

Этап 1: Подготовка (1 день)

✅ Шаг 1.1: Создать бэкап

# Бэкап БД
python manage.py dumpdata procurement > backup_procurement_$(date +%Y%m%d).json

# Бэкап кода
git checkout -b refactor/procurement-pip-ready
git commit -am "Checkpoint: Before procurement refactoring"

✅ Шаг 1.2: Запустить существующие тесты

pytest backend/tests/api/v1/procurement/ -v --tb=short
# Записать результаты как baseline

✅ Шаг 1.3: Создать файлы конфигурации

procurement/
  conf.py          # Настройки модуля (новый)
  setup.py         # pip package metadata (новый)
  pyproject.toml   # Modern Python packaging (новый)
  MANIFEST.in      # Файлы для включения в пакет (новый)

Этап 2: Создание конфигурации (2 дня)

✅ Шаг 2.1: Создать procurement/conf.py

"""
Настройки модуля procurement.

Можно переопределить в settings.py проекта:

PROCUREMENT_USER_MODEL = 'auth.User'
PROCUREMENT_DEPARTMENT_MODEL = 'myapp.Department'
"""
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

# User model (usually Employee or auth.User)
PROCUREMENT_USER_MODEL = getattr(
    settings,
    'PROCUREMENT_USER_MODEL',
    settings.AUTH_USER_MODEL
)

# Department model (must have: name, head, active_employees)
PROCUREMENT_DEPARTMENT_MODEL = getattr(
    settings,
    'PROCUREMENT_DEPARTMENT_MODEL',
    'employees.Department'
)

# Approval thresholds (₽)
APPROVAL_THRESHOLD_LOW = getattr(
    settings,
    'PROCUREMENT_APPROVAL_THRESHOLD_LOW',
    10000
)

APPROVAL_THRESHOLD_HIGH = getattr(
    settings,
    'PROCUREMENT_APPROVAL_THRESHOLD_HIGH',
    100000
)

# Validation функция
def validate_department_model():
    """Проверяет что DEPARTMENT_MODEL имеет нужные поля"""
    from django.apps import apps
    
    try:
        model = apps.get_model(PROCUREMENT_DEPARTMENT_MODEL)
    except LookupError:
        raise ImproperlyConfigured(
            f"PROCUREMENT_DEPARTMENT_MODEL '{PROCUREMENT_DEPARTMENT_MODEL}' "
            f"not found. Make sure the app is in INSTALLED_APPS."
        )
    
    # Проверяем обязательные поля
    required_fields = ['name']
    for field in required_fields:
        if not hasattr(model, field):
            raise ImproperlyConfigured(
                f"PROCUREMENT_DEPARTMENT_MODEL must have '{field}' field"
            )
    
    return model

✅ Шаг 2.2: Обновить procurement/apps.py

from django.apps import AppConfig

class ProcurementConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'procurement'
    verbose_name = 'Закупки и инвентарь'
    
    def ready(self):
        # Импорт signals
        from . import signals
        
        # Валидация настроек
        from .conf import validate_department_model
        validate_department_model()

Этап 3: Рефакторинг моделей (3 дня)

✅ Шаг 3.1: Обновить imports в models.py

# ❌ Старое
from employees.models import Department
User = get_user_model()

# ✅ Новое
from django.apps import apps
from .conf import PROCUREMENT_USER_MODEL, PROCUREMENT_DEPARTMENT_MODEL

def get_user_model():
    return apps.get_model(PROCUREMENT_USER_MODEL)

def get_department_model():
    return apps.get_model(PROCUREMENT_DEPARTMENT_MODEL)

✅ Шаг 3.2: Изменить ForeignKey на строковые ссылки

# ❌ Старое
department = models.ForeignKey(
    Department,
    on_delete=models.PROTECT,
    ...
)

# ✅ Новое
department = models.ForeignKey(
    PROCUREMENT_DEPARTMENT_MODEL,  # Строка!
    on_delete=models.PROTECT,
    ...
)

# ❌ Старое
requestor = models.ForeignKey(
    User,
    on_delete=models.PROTECT,
    ...
)

# ✅ Новое
requestor = models.ForeignKey(
    PROCUREMENT_USER_MODEL,  # Строка!
    on_delete=models.PROTECT,
    ...
)

✅ Шаг 3.3: Добавить type hints

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from django.contrib.auth.models import AbstractUser
    from django.db.models import Model
    
    UserModel = AbstractUser
    DepartmentModel = Model
else:
    UserModel = None
    DepartmentModel = None

class ProcurementRequest(models.Model):
    # Type hints для IDE
    requestor: UserModel
    department: DepartmentModel
    
    def approve(self, by_user: UserModel) -> None:
        """Одобрить заявку"""
        ...

Этап 4: Миграции (1 день)

✅ Шаг 4.1: Создать новую миграцию

python manage.py makemigrations procurement

Важно: Миграция должна использовать swappable_dependency:

# migrations/XXXX_refactor_swappable.py
from django.conf import settings
from django.db import migrations, models
import procurement.conf

class Migration(migrations.Migration):
    
    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        migrations.swappable_dependency(procurement.conf.PROCUREMENT_DEPARTMENT_MODEL),
        ('procurement', '0001_initial'),
    ]
    
    operations = [
        # Django автоматически создаст AlterField для ForeignKey
        ...
    ]

✅ Шаг 4.2: Применить миграции

python manage.py migrate procurement

✅ Шаг 4.3: Проверить БД

python manage.py dbshell
\d+ procurement_procurementrequest
# Проверить что FK ссылаются на правильные таблицы

Этап 5: Обновление кода (2 дня)

✅ Шаг 5.1: Обновить методы моделей

# Везде где используется User/Department, заменить на get_*_model()

class ProcurementRequest(models.Model):
    def check_budget_available(self):
        Department = get_department_model()  # ← Добавить
        
        try:
            budget = Budget.objects.get(
                department=self.department,
                ...
            )

✅ Шаг 5.2: Обновить permissions.py

# api/v1/procurement/permissions.py

from procurement.conf import get_department_model

class IsDepartmentHead(permissions.BasePermission):
    def has_permission(self, request, view):
        Department = get_department_model()  # ← Добавить
        
        # Проверяем через свойство head
        # Предполагается что у Department есть поле head
        return Department.objects.filter(head=request.user).exists()

✅ Шаг 5.3: Обновить serializers.py

# api/v1/procurement/serializers.py

from procurement.conf import PROCUREMENT_USER_MODEL

class ProcurementRequestSerializer(serializers.ModelSerializer):
    requestor = serializers.PrimaryKeyRelatedField(
        queryset=get_user_model().objects.all()  # ← Использовать get_user_model()
    )

Этап 6: Обновление тестов (2 дня)

✅ Шаг 6.1: Обновить conftest.py

# tests/api/v1/procurement/conftest.py

from django.apps import apps
from procurement.conf import PROCUREMENT_USER_MODEL, PROCUREMENT_DEPARTMENT_MODEL

@pytest.fixture
def user_factory(db):
    """Фабрика пользователей - работает с любой моделью"""
    User = apps.get_model(PROCUREMENT_USER_MODEL)
    
    def _create(**kwargs):
        return User.objects.create_user(**kwargs)
    
    return _create

@pytest.fixture
def department_factory(db):
    """Фабрика отделов - работает с любой моделью"""
    Department = apps.get_model(PROCUREMENT_DEPARTMENT_MODEL)
    
    def _create(**kwargs):
        defaults = {'name': 'Test Department'}
        defaults.update(kwargs)
        return Department.objects.create(**defaults)
    
    return _create

✅ Шаг 6.2: Запустить тесты

pytest backend/tests/api/v1/procurement/ -v
# Все 69 тестов должны пройти!

Этап 7: Создание pip пакета (1 день)

✅ Шаг 7.1: Создать setup.py

# procurement/setup.py
from setuptools import setup, find_packages

setup(
    name='django-procurement',
    version='0.1.0',
    description='Django app for procurement and inventory management',
    long_description=open('README.md').read(),
    long_description_content_type='text/markdown',
    author='Your Name',
    author_email='your@email.com',
    url='https://github.com/yourorg/django-procurement',
    packages=find_packages(),
    include_package_data=True,
    install_requires=[
        'Django>=4.2',
        'djangorestframework>=3.14',
        'Pillow>=10.0',  # Для QR кодов
    ],
    extras_require={
        'dev': [
            'pytest>=7.0',
            'pytest-django>=4.5',
            'black>=23.0',
            'isort>=5.12',
        ]
    },
    classifiers=[
        'Development Status :: 3 - Alpha',
        'Framework :: Django',
        'Framework :: Django :: 4.2',
        'Framework :: Django :: 5.0',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: MIT License',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.10',
        'Programming Language :: Python :: 3.11',
        'Programming Language :: Python :: 3.12',
    ],
    python_requires='>=3.10',
)

✅ Шаг 7.2: Создать pyproject.toml

# procurement/pyproject.toml
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "django-procurement"
version = "0.1.0"
description = "Django app for procurement and inventory management"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
authors = [
    {name = "Your Name", email = "your@email.com"}
]
keywords = ["django", "procurement", "inventory", "equipment"]
classifiers = [
    "Development Status :: 3 - Alpha",
    "Framework :: Django",
    "Programming Language :: Python :: 3",
]
dependencies = [
    "Django>=4.2",
    "djangorestframework>=3.14",
    "Pillow>=10.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "pytest-django>=4.5",
    "black>=23.0",
    "isort>=5.12",
]

[project.urls]
Homepage = "https://github.com/yourorg/django-procurement"
Documentation = "https://django-procurement.readthedocs.io"
Repository = "https://github.com/yourorg/django-procurement"

✅ Шаг 7.3: Создать MANIFEST.in

# procurement/MANIFEST.in
include LICENSE
include README.md
include pyproject.toml
recursive-include procurement/templates *
recursive-include procurement/static *
recursive-include procurement/migrations *.py
recursive-exclude * __pycache__
recursive-exclude * *.pyc
recursive-exclude * *.pyo

Этап 8: Документация (2 дня)

✅ Шаг 8.1: Создать README.md

# Django Procurement

Django приложение для управления закупками и инвентарем.

## Особенности

- ✅ Swappable User/Department models
- ✅ Workflow для заявок на закупку
- ✅ Управление оборудованием
- ✅ Система согласований
- ✅ REST API (DRF)
- ✅ Полные type hints
- ✅ 80%+ test coverage

## Установка

```bash
pip install django-procurement

Быстрый старт

  1. Добавьте в INSTALLED_APPS:
INSTALLED_APPS = [
    ...
    'rest_framework',
    'procurement',
]
  1. Настройте модели (опционально):
# settings.py
PROCUREMENT_USER_MODEL = 'auth.User'  # default: AUTH_USER_MODEL
PROCUREMENT_DEPARTMENT_MODEL = 'myapp.Department'  # default: 'employees.Department'

# Пороги согласования (₽)
PROCUREMENT_APPROVAL_THRESHOLD_LOW = 10000
PROCUREMENT_APPROVAL_THRESHOLD_HIGH = 100000
  1. Примените миграции:
python manage.py migrate procurement
  1. Подключите URLs:
urlpatterns = [
    path('api/v1/procurement/', include('procurement.api.urls')),
]

Требования к Department model

Если используется кастомная модель отдела, она должна иметь:

  • name (CharField) - название отдела
  • head (ForeignKey to User, optional) - руководитель

Пример:

class Department(models.Model):
    name = models.CharField(max_length=255)
    head = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True
    )

API

Endpoints

  • GET /api/v1/procurement/requests/ - список заявок
  • POST /api/v1/procurement/requests/ - создать заявку
  • GET /api/v1/procurement/equipment/ - список оборудования
  • ... (see docs/)

Тестирование

pytest tests/

Лицензия

MIT License


#### ✅ Шаг 8.2: Обновить docstrings
Добавить полные docstrings во все классы и методы с примерами использования.

---

### **Этап 9: Интеграция в EUSRR (1 день)**

#### ✅ Шаг 9.1: Обновить settings.py проекта
```python
# backend/eusrr_backend/settings.py

# Настройки procurement модуля
PROCUREMENT_USER_MODEL = 'employees.Employee'  # Явно указываем
PROCUREMENT_DEPARTMENT_MODEL = 'employees.Department'
PROCUREMENT_APPROVAL_THRESHOLD_LOW = 10000
PROCUREMENT_APPROVAL_THRESHOLD_HIGH = 100000

✅ Шаг 9.2: Проверить работу

# Запустить сервер
python manage.py runserver

# Проверить API
curl http://localhost:9000/api/v1/procurement/requests/

# Проверить админку
# Зайти в /admin/procurement/

Этап 10: Финализация (1 день)

✅ Шаг 10.1: Запустить полный тест-suite

# Все тесты procurement
pytest backend/tests/api/v1/procurement/ -v --cov=procurement

# Интеграционные тесты
pytest backend/tests/integration/ -k procurement

# Проверить покрытие
pytest --cov=procurement --cov-report=html
# Цель: 80%+

✅ Шаг 10.2: Code quality checks

# Форматирование
black backend/procurement/
isort backend/procurement/

# Линтинг
flake8 backend/procurement/
mypy backend/procurement/

# Проверка безопасности
bandit -r backend/procurement/

✅ Шаг 10.3: Создать git tag

git add .
git commit -m "refactor: Make procurement module pip-ready

- Add swappable USER/DEPARTMENT models
- Add comprehensive type hints
- Extract configuration to conf.py
- Add setup.py for pip distribution
- Update tests to work with any models
- 100% backward compatible with EUSRR

BREAKING CHANGES: None (backward compatible)
"

git tag -a v0.1.0 -m "Release 0.1.0: pip-ready procurement module"
git push origin refactor/procurement-pip-ready
git push --tags

🧪 Тестирование в изоляции

Создать тестовый Django проект:

# Создать чистый проект для проверки
mkdir test-procurement
cd test-procurement
python -m venv venv
source venv/bin/activate

# Установить procurement как pip пакет
pip install -e ../EUSRR/backend/procurement/

# Создать minimal Django проект
django-admin startproject testproject
cd testproject

# Добавить в INSTALLED_APPS
# settings.py
INSTALLED_APPS = [
    ...
    'procurement',
]

# Использовать стандартный auth.User
PROCUREMENT_USER_MODEL = 'auth.User'

# Создать минимальную Department модель
# testproject/models.py
class Department(models.Model):
    name = models.CharField(max_length=255)
    head = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True
    )

PROCUREMENT_DEPARTMENT_MODEL = 'testproject.Department'

# Запустить миграции
python manage.py migrate

# Проверить что всё работает!
python manage.py shell
>>> from procurement.models import ProcurementRequest
>>> ProcurementRequest.objects.create(...)

✅ Критерии успеха

Функциональные:

  • Все 69 тестов проходят
  • Модуль работает с auth.User вместо employees.Employee
  • Модуль работает с custom Department моделью
  • API endpoints работают
  • Админка работает
  • Миграции применяются без ошибок

Код quality:

  • 80%+ test coverage
  • Type hints везде
  • Docstrings для всех public API
  • Black + isort пройдены
  • Flake8 без ошибок
  • Mypy пройден

Packaging:

  • pip install -e . работает
  • setup.py корректен
  • pyproject.toml корректен
  • MANIFEST.in включает все нужные файлы
  • README.md полный

Backward compatibility:

  • EUSRR проект работает БЕЗ изменений (кроме settings)
  • Существующие данные в БД не затронуты
  • Frontend не требует изменений

📊 Метрики прогресса

До рефакторинга:

Зависимости: 19 жестких связей (13 внешних)
Покрытие тестами: 81% (56/69 passed)
Документация: Minimal
Type hints: Частичные
pip-ready: ❌

После рефакторинга:

Зависимости: 0 жестких связей (все swappable)
Покрытие тестами: 80%+ (все проходят)
Документация: Полная (README, docstrings)
Type hints: 100%
pip-ready: ✅

🚨 Риски и митигация

Риск 1: Миграции сломают БД

Митигация:

  • Полный бэкап перед началом
  • Тестировать на копии БД
  • Rollback plan готов

Риск 2: Тесты не проходят

Митигация:

  • Запустить baseline до изменений
  • Фиксить по одному тесту
  • Не коммитить если есть failed tests

Риск 3: Breaking changes для EUSRR

Митигация:

  • Backward compatible подход
  • Только добавление, не удаление
  • Settings опциональные

Риск 4: Performance деградация

Митигация:

  • Бенчмарки до/после
  • EXPLAIN ANALYZE запросов
  • Индексы на месте

📝 Чеклист перед коммитом

  • Бэкап БД создан
  • Все тесты проходят
  • Black + isort применены
  • Type hints добавлены
  • Docstrings обновлены
  • README.md актуален
  • Миграции протестированы
  • EUSRR проект работает
  • Coverage 80%+
  • Нет TODO/FIXME в коде

🎉 После завершения

Опубликовать на TestPyPI:

cd backend/procurement
python -m build
twine upload --repository testpypi dist/*

Опубликовать на PyPI (когда готово):

twine upload dist/*

Документация:

  • Создать docs/ сайт (MkDocs/Sphinx)
  • Опубликовать на ReadTheDocs

Время начала: 27.02.2026
Ожидаемое завершение: 15.03.2026 (2-3 недели)
Ответственный: [Ваше имя]
Статус: 📝 План готов, ожидает утверждения