From abcb264e38e69202c73d9e3e98f1b39140f05094 Mon Sep 17 00:00:00 2001 From: lizerk Date: Tue, 10 Mar 2026 12:21:22 +0300 Subject: [PATCH] =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D0=B8=D0=BB=20=D0=BB?= =?UTF-8?q?=D0=B8=D1=88=D0=BD=D0=B5=D0=B3=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BUDGET_SYSTEM.md | 246 ---- CHAT_PAGE_RENDERING_ARCHITECTURE.md | 827 ------------- DYNAMIC_MEDIA_URLS.md | 130 -- FEED_AJAX_REFACTORING.md | 249 ---- IP_REGISTRATION_RESTRICTIONS.md | 290 ----- IP_RESTRICTIONS_README.md | 96 -- MESSAGE_NEWLINES_FIX.md | 198 ---- MESSAGE_RENDERING_REFACTORING.md | 272 ----- PROCUREMENT_APPROVERS.md | 160 --- PROCUREMENT_IMPLEMENTATION_PLAN.md | 1568 ------------------------- PROCUREMENT_TEST_COMPLETION_REPORT.md | 229 ---- REQUESTS_APP_STRUCTURE.md | 0 12 files changed, 4265 deletions(-) delete mode 100644 BUDGET_SYSTEM.md delete mode 100644 CHAT_PAGE_RENDERING_ARCHITECTURE.md delete mode 100644 DYNAMIC_MEDIA_URLS.md delete mode 100644 FEED_AJAX_REFACTORING.md delete mode 100644 IP_REGISTRATION_RESTRICTIONS.md delete mode 100644 IP_RESTRICTIONS_README.md delete mode 100644 MESSAGE_NEWLINES_FIX.md delete mode 100644 MESSAGE_RENDERING_REFACTORING.md delete mode 100644 PROCUREMENT_APPROVERS.md delete mode 100644 PROCUREMENT_IMPLEMENTATION_PLAN.md delete mode 100644 PROCUREMENT_TEST_COMPLETION_REPORT.md delete mode 100644 REQUESTS_APP_STRUCTURE.md diff --git a/BUDGET_SYSTEM.md b/BUDGET_SYSTEM.md deleted file mode 100644 index 2adcb0fd..00000000 --- a/BUDGET_SYSTEM.md +++ /dev/null @@ -1,246 +0,0 @@ -# Система бюджетов отделов в модуле закупок - -## Где выставляется бюджет? - -### 1️⃣ **Django Admin (Основное место)** - -``` -http://localhost:9000/admin/procurement/budget/ -``` - -**Процесс:** -1. Перейти в Django Admin → Procurement → Budget -2. Нажать "Add budget" -3. Заполнить форму: - - **Department** — выбрать отдел (IT, HR, финансы и т.д.) - - **Year** — год (2025, 2026) - - **Quarter** — квартал (1, 2, 3, 4) - - **Allocated amount** — сумма выделенного бюджета в рублях - - **Spent amount** — (заполняется автоматически, если оставить 0) - -### 2️⃣ **REST API (для приложений)** - -**Создание бюджета:** -```bash -POST /api/procurement/budgets/ -Content-Type: application/json - -{ - "department": 5, # ID отдела - "year": 2025, - "quarter": 4, - "allocated_amount": "500000.00" -} -``` - -**Редактирование:** -```bash -PATCH /api/procurement/budgets/1/ -{ - "allocated_amount": "600000.00" -} -``` - -**Получение текущего квартала:** -```bash -GET /api/procurement/budgets/current_quarter/ -``` - -**Получить бюджет своего отдела:** -```bash -GET /api/procurement/budgets/my-department/ -``` - -## Структура модели Budget - -```python -class Budget(models.Model): - department # ForeignKey на Department - year # 2025, 2026, ... - quarter # 1, 2, 3, 4 - allocated_amount # Выделено денег (основное число!) - spent_amount # Потрачено (меняется при одобрении заявок) - created_at # Когда создан - updated_at # Когда обновлён -``` - -**Ключевое свойство:** `(department, year, quarter)` — уникальный набор! -``` -Нельзя создать два бюджета для одного отдела на один период! -``` - -## Примеры бюджетов - -### Компания с 3 отделами на Q4 2025: - -``` -┌────────────────┬──────┬─────────┬──────────────┐ -│ Отдел │ 2025 │ Q4 │ Бюджет │ -├────────────────┼──────┼─────────┼──────────────┤ -│ IT │ 2025 │ 4 │ 1,000,000₽ │ -│ HR │ 2025 │ 4 │ 300,000₽ │ -│ Финансы │ 2025 │ 4 │ 500,000₽ │ -│ Маркетинг │ 2025 │ 4 │ 250,000₽ │ -└────────────────┴──────┴─────────┴──────────────┘ -``` - -## Как работает проверка бюджета? - -Когда пользователь отправляет заявку на согласование: - -```python -# 1. Система получает текущий квартал -now = timezone.now() -quarter = (now.month - 1) // 3 + 1 - -# 2. Ищет бюджет отдела на этот период -budget = Budget.objects.get( - department=procurement_request.department, - year=now.year, - quarter=quarter -) - -# 3. Проверяет остаток -remaining = budget.remaining_amount # allocated - spent -if remaining >= procurement_request.estimated_cost: - # ✅ Можно отправить на согласование -else: - # ❌ Недостаточно бюджета - return error -``` - -## Ключевые свойства Budget - -```python -# Остаток бюджета -remaining_amount = allocated_amount - spent_amount - -# Зарезервировано (pending заявки) -reserved_amount = sum(pending_requests.estimated_cost) - -# Доступно с учётом резерва -available_amount = remaining_amount - reserved_amount - -# Процент использования -utilization_percentage = (spent_amount / allocated_amount) * 100 - -# Можно ли потратить X? -can_spend(amount: Decimal) → bool -``` - -## Пример в коде - -```python -# Проверка в views.py при отправке заявки -available, remaining = procurement_request.check_budget_available() - -if not available: - return Response({ - 'error': 'Недостаточно бюджета', - 'required': float(procurement_request.estimated_cost), - 'remaining': float(remaining), - }, status=400) -``` - -## Алерты при низком бюджете - -Система автоматически отправляет уведомления руководителю отдела: - -```python -# budget_low (остаток < 20%) -"Бюджет отдела снижается. Остаток: 45,000₽ (18%)" - -# budget_critical (остаток < 10%) -"⚠️ Критически низкий бюджет! Остаток: 8,000₽ (3%)" -``` - -**Когда:** При одобрении заявки если бюджет падает ниже порогов - -## Как менять бюджет? - -### ✅ Правильно (сумма выделенного): -``` -Отдел IT на Q4 2025: 1,000,000₽ -Потом изменили: 1,500,000₽ (увеличили бюджет) -``` - -### ❌ Неправильно (not spent_amount): -``` -❌ Не меняйте spent_amount вручную - это меняется автоматически -``` - -## Права доступа - -| Action | Кто может | -|--------|-----------| -| Создать бюджет | `CanManageBudget` (админ) | -| Редактировать | `CanManageBudget` (админ) | -| Удалить | `CanManageBudget` (админ) | -| Просмотреть свой | Руководитель отдела | -| Просмотреть все | Суперпользователь/Админ | - -## Типичный цикл - -``` -Q1 2025: Выделили IT-отделу 1,000,000₽ - ↓ -Заявка #1: 50,000₽ → одобрена -Заявка #2: 100,000₽ → одобрена -Zaявка #3: 30,000₽ → одобрена - ↓ -Потрачено: 180,000₽ -Остаток: 820,000₽ -Использовано: 18% - ↓ -Q2 2025: Новый квартал, новый бюджет -``` - -## SQL для статистики - -```sql --- Сколько потрачено каждый отдел на Q4 2025? -SELECT - d.name, - b.allocated_amount, - b.spent_amount, - (b.allocated_amount - b.spent_amount) as remaining -FROM procurement_budget b -JOIN employees_department d ON b.department_id = d.id -WHERE b.year = 2025 AND b.quarter = 4; - --- Самый загруженный отдел по бюджету -SELECT - d.name, - b.allocated_amount, - (b.spent_amount::float / b.allocated_amount * 100) as utilization -FROM procurement_budget b -JOIN employees_department d ON b.department_id = d.id -WHERE b.year = 2025 -ORDER BY utilization DESC; -``` - -## Интеграция с заявками - -``` -ProcurementRequest (заявка) - ├─ department_id - ├─ estimated_cost - └─ check_budget_available() - ↓ ищет - Budget (бюджет) - ├─ department - ├─ allocated_amount - └─ spent_amount -``` - -## Автоматические обновления spent_amount - -```python -# На данный момент spent_amount меняется вручную в: -# - Django Admin -# - API PATCH запрос - -# В будущем можно автоматизировать при: -# - Завершении заявки (status=COMPLETED) -# - Поступлении счёта (через другую модель Invoice) -``` diff --git a/CHAT_PAGE_RENDERING_ARCHITECTURE.md b/CHAT_PAGE_RENDERING_ARCHITECTURE.md deleted file mode 100644 index 0e3357b9..00000000 --- a/CHAT_PAGE_RENDERING_ARCHITECTURE.md +++ /dev/null @@ -1,827 +0,0 @@ -# Архитектура рендеринга и наполнения страницы чата - -## 📋 Оглавление -1. [Общая схема](#общая-схема) -2. [Серверная часть (Backend)](#серверная-часть-backend) -3. [Шаблон страницы (Template)](#шаблон-страницы-template) -4. [JavaScript инициализация](#javascript-инициализация) -5. [WebSocket соединение](#websocket-соединение) -6. [Рендеринг сообщений](#рендеринг-сообщений) -7. [Поток данных](#поток-данных) - ---- - -## 🎯 Общая схема - -Страница чата работает по следующей архитектуре: - -``` -HTTP Request → Django View → HTML Template → DOM Ready → WebSocket Connect → Load Messages - ↓ ↓ ↓ ↓ ↓ - Context Data Initial HTML Init JS Real-time Render to DOM -``` - ---- - -## 🔧 Серверная часть (Backend) - -### 1. `ChatDetailView` (communications/views.py) - -**Класс:** `ChatDetailView(LoginRequiredMixin, DetailView, FormView)` - -**Ответственность:** -- Проверка прав доступа пользователя к чату -- Подготовка контекста для шаблона -- Установка заголовков для отключения кэширования - -#### Ключевые методы: - -```python -def get_object(self): - """Получает объект чата с проверкой прав доступа""" - # Фильтрует по типу чата: global/department/private/group - # Проверяет membership для приватных чатов -``` - -```python -def get_context_data(self, **kwargs): - """Подготавливает контекст для шаблона""" - # 1. Проверка доступа через _user_has_access() - # 2. Загрузка сообщений (последние 50, в обратном порядке) - # 3. Определение has_more (есть ли еще история) - # 4. Вычисление last_read_at и first_unread_id - # 5. Проверка прав на отправку сообщений -``` - -**Контекст, передаваемый в шаблон:** -```python -{ - 'chat': Chat, # Объект чата - 'messages': [...], # Последние 50 сообщений (УСТАРЕЛО - не рендерятся) - 'messages_has_more': bool, # Есть ли еще история - 'messages_oldest_id': int, # ID самого старого сообщения - 'messages_oldest_ts': int, # Timestamp самого старого сообщения - 'messages_page_size': 50, # Размер страницы - 'participants': QuerySet, # Участники чата - 'last_read_at': datetime, # Время последнего прочтения - 'first_unread_id': int, # ID первого непрочитанного - 'can_send_messages': bool, # Может ли отправлять сообщения - 'is_announcement_creator': bool # Создатель объявления -} -``` - -#### Отключение кэширования: -```python -def dispatch(self, request, *args, **kwargs): - response = super().dispatch(request, *args, **kwargs) - response['Cache-Control'] = 'no-cache, no-store, must-revalidate, max-age=0' - response['Pragma'] = 'no-cache' - response['Expires'] = '0' - return response -``` - ---- - -## 🖼️ Шаблон страницы (Template) - -### `templates/communications/chat_detail.html` - -#### Структура DOM: - -```html -
-
-
- - -
- -
- - -
- - -
...
- - - - - -
- - -
...
- - -
-
...
-

Загружаем сообщения...

-
- - -
- - -
...
- - -
-
- - - -
-
- -
-
-
-
-``` - -#### Data-атрибуты контейнера: - -```html -
-``` - -**Важно:** Сообщения из контекста `messages` больше НЕ рендерятся в шаблоне! -Вместо этого используется **WebSocket для загрузки initial_messages**. - ---- - -## ⚙️ JavaScript инициализация - -### `static/js/pages/chatDetail.js` - -**Главный файл инициализации страницы чата.** - -#### Этапы инициализации: - -```javascript -// 1. Загрузка модулей -const modules = await Promise.all([ - import('../components/chatMarkRead.js'), - import('../components/chatComposer.js'), - import('../components/chatHistoryLoader.js'), - import('../components/chatFormManager.js'), - import('../components/messageRenderer.js'), - import('../components/initialMessagesLoader.js') // Не используется -]); - -// 2. Ожидание DOM Ready -document.addEventListener('DOMContentLoaded', initWhenReady); - -// 3. Чтение конфигурации из data-атрибутов -const config = { - chatId: Number(appContainer.dataset.chatId), - userId: Number(appContainer.dataset.userId), - uploadUrl: appContainer.dataset.uploadUrl, - messagesUrl: appContainer.dataset.messagesUrl, - // ... и другие -}; - -// 4. Ожидание готовности WebSocket -waitForWebSocket().then((userWs) => { - initializeComponents(config, userWs); -}); -``` - -#### Инициализация компонентов: - -```javascript -function initializeComponents(config, userWs) { - // 1. FormManager - управление формой - const formManager = initChatFormManager({ ... }); - - // 2. MarkRead - отметка прочитанных - const markReadApi = initChatMarkRead({ ... }); - - // 3. Composer - композер сообщений - const composer = initChatComposer({ ... }); - - // 4. HistoryLoader - загрузка старых сообщений - const historyLoader = initChatHistoryLoader({ ... }); - - // 5. MessageRenderer - рендеринг сообщений - const messageRenderer = new MessageRenderer({ ... }); - - // 6. Конфигурация WebSocket - userWs.configure({ - scrollContainerId: 'chatScroll', - messageRenderer: messageRenderer, - markReadApi: markReadApi, - // ... - }); - - // 7. Открытие чата в WebSocket - userWs.openChat(config.chatId, true); // true = загрузить историю -} -``` - ---- - -## 🌐 WebSocket соединение - -### `static/js/components/userWebSocket.js` - -**Единое WebSocket соединение для всех real-time обновлений.** - -#### Инициализация в base.html: - -```javascript -// templates/base.html -const userWs = initUserWebSocket({ - userId: {{ user.id }}, - badgeManager: badgeManager, - onListUpdate: (data) => { /* ... */ } -}); - -window.userWebSocket = userWs; - -// Уведомляем модули о готовности -window.dispatchEvent(new CustomEvent('user:ws-ready', { - detail: { api: userWs } -})); -``` - -#### Подключение: - -```javascript -const proto = location.protocol === 'https:' ? 'wss' : 'ws'; -const wsUrl = `${proto}://${location.host}/ws/`; -const ws = new WebSocket(wsUrl); -``` - -#### Открытие чата: - -```javascript -function openChat(chatId, loadHistory = false) { - send({ - action: 'open_chat', - chat_id: chatId, - load_history: loadHistory - }); -} -``` - ---- - -## 📨 Загрузка сообщений через WebSocket - -### Backend: `communications/consumers.py` - -#### `ChatConsumer.connect()`: - -```python -async def connect(self): - # 1. Проверка аутентификации - user = self.scope.get("user") - - # 2. Проверка прав доступа к чату - chat = await self._get_chat(self.chat_id) - if not await self._user_can_access(chat, user): - await self.close(code=4403) - return - - # 3. Подключение к группе - await self.channel_layer.group_add(self.group_name, self.channel_name) - await self.accept() - - # 4. Отправка начальной истории - await self._send_initial_messages() - - # 5. Отметка как прочитанное - await self._mark_read(self.chat, user) - - # 6. Запуск ping цикла - self.ping_task = asyncio.create_task(self._ping_loop()) -``` - -#### `_send_initial_messages()`: - -```python -async def _send_initial_messages(self): - """Отправить начальную историю сообщений при подключении""" - messages = await self._get_initial_messages(self.chat_id, limit=50) - - await self.send_json({ - "type": "initial_messages", - "messages": messages - }) - -@database_sync_to_async -def _get_initial_messages(self, chat_id: int, limit: int = 50): - """Получить последние N сообщений из чата""" - messages = Message.objects.filter( - chat_id=chat_id - ).select_related( - 'author' - ).prefetch_related( - 'attachments', - 'reactions__user' - ).order_by('-created_at')[:limit] - - # Возвращаем в прямом порядке (старые -> новые) - return [serialize_message(msg) for msg in reversed(list(messages))] -``` - -#### Формат сообщения: - -```python -def serialize_message(m: Message) -> dict: - return { - "id": m.id, - "chat_id": m.chat_id, - "author_id": m.author_id, - "author_name": m.author.get_full_name() or m.author.username, - "avatar": m.author.avatar.url if hasattr(m.author, 'avatar') and m.author.avatar else None, - "content": m.content, - "created_ts": int(m.created_at.timestamp() * 1000), - "is_edited": m.is_edited, - "edited_at": m.edited_at.isoformat() if m.edited_at else None, - "attachments": [ - { - "id": att.id, - "file_url": att.file.url if att.file else None, - "file_name": att.file.name.split('/')[-1] if att.file else "file", - "file_type": att.file_type or "unknown" - } - for att in m.attachments.all() - ], - "reactions_summary": m.get_reactions_summary(), - "is_forwarded": m.is_forwarded, - "forwarded_from": { ... } if m.is_forwarded else None, - "reply_to": { ... } if m.reply_to else None, - "poll": { ... } if hasattr(m, 'poll') else None, - } -``` - ---- - -## 🎨 Рендеринг сообщений - -### `static/js/components/messageRenderer.js` - -#### Класс `MessageRenderer`: - -```javascript -class MessageRenderer { - constructor(config) { - this.containerId = config.containerId; - this.currentUserId = config.currentUserId; - this.currentUserAvatar = config.currentUserAvatar; - this.profileUrl = config.profileUrl; - this.detailUrlTemplate = config.detailUrlTemplate; - } - - /** - * Рендерит массив сообщений (с разделителями дней) - */ - renderMessages(messages) { - const container = document.getElementById(this.containerId); - - let lastDay = null; - messages.forEach(msg => { - // Добавляем разделитель дня если нужно - const msgDate = new Date(msg.created_ts); - const msgDay = this.formatDay(msgDate); - - if (msgDay !== lastDay) { - this.addDayDivider(msgDay, container); - lastDay = msgDay; - } - - this.renderMessage(msg, container); - }); - } - - /** - * Рендерит одно сообщение - */ - renderMessage(msg, container) { - // Проверка на дубликаты - const existingMessage = container.querySelector(`[data-message-id="${msg.id}"]`); - if (existingMessage) { - console.log('Message already exists, skipping:', msg.id); - return; - } - - const isOwn = msg.author_id === this.currentUserId; - const messageHtml = this.buildMessageHtml(msg, isOwn); - - // Добавляем в конец контейнера - container.insertAdjacentHTML('beforeend', messageHtml); - } -} -``` - -#### Структура HTML сообщения: - -```html -
- - - - - - - - -
- -
- Иван Иванов · -
- - -
- - -
...
- - -
...
- - - Привет, как дела? - - - (изменено) - - -
...
- - -
...
-
- - -
...
-
- - - ... -
-``` - ---- - -## 🔄 Поток данных - -### 1. Начальная загрузка страницы - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ HTTP GET /communications/chats/123/ │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ ChatDetailView.get() │ -│ ├─ get_queryset() → Проверка доступа │ -│ ├─ get_context_data() │ -│ │ ├─ messages = последние 50 (НЕ ИСПОЛЬЗУЕТСЯ) │ -│ │ ├─ has_more = True/False │ -│ │ ├─ last_read_at │ -│ │ └─ first_unread_id │ -│ └─ render(chat_detail.html) │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ HTML с пустым контейнером #chatScroll │ -│ + индикатор загрузки #initialLoader │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ JS: DOMContentLoaded │ -│ ├─ chatDetail.js → initWhenReady() │ -│ ├─ Чтение config из data-атрибутов │ -│ └─ waitForWebSocket() │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ base.html → initUserWebSocket() │ -│ ├─ ws = new WebSocket('ws://host/ws/') │ -│ └─ dispatchEvent('user:ws-ready') │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ chatDetail.js → initializeComponents() │ -│ ├─ FormManager │ -│ ├─ MarkRead │ -│ ├─ Composer │ -│ ├─ HistoryLoader │ -│ ├─ MessageRenderer │ -│ └─ userWs.openChat(chatId, true) │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ WebSocket → { action: 'open_chat', chat_id: 123 } │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ Backend: ChatConsumer.connect() │ -│ ├─ _send_initial_messages() │ -│ │ └─ _get_initial_messages(limit=50) │ -│ └─ send_json({ type: 'initial_messages', messages: [...] }) │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ userWebSocket.js → handleInitialMessages() │ -│ └─ messageRenderer.renderMessage() для каждого сообщения │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ messageRenderer.js → renderMessage() │ -│ ├─ Проверка дубликатов │ -│ ├─ Добавление разделителей дней │ -│ ├─ buildMessageHtml() │ -│ └─ container.insertAdjacentHTML('beforeend', html) │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ DOM обновлён → сообщения видны пользователю │ -│ ├─ Скролл вниз │ -│ ├─ Удаление #initialLoader │ -│ ├─ dispatchEvent('chat:initial-messages-loaded') │ -│ └─ Инициализация poll виджетов │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 2. Получение нового сообщения - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Другой пользователь отправляет сообщение │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ Backend: ChatConsumer → channel_layer.group_send() │ -│ → broadcast в chat_123 │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ WebSocket → { type: 'new_message', message: {...} } │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ userWebSocket.js → handleNewMessage() │ -│ └─ messageRenderer.renderMessage(message) │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ DOM обновлён → новое сообщение появилось в чате │ -│ ├─ Автоскролл (если внизу) │ -│ └─ Отметка как прочитанное (если в viewport) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 3. Загрузка истории (скролл вверх) - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Пользователь скроллит вверх │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ chatHistoryLoader.js → IntersectionObserver срабатывает │ -│ └─ loadMoreMessages() │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ API Request: GET /api/v1/chats/123/messages/ │ -│ ?before_id=456&limit=50 │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ Backend: ChatMessagesAPIView.get() │ -│ └─ Возвращает { messages: [...], has_more: true } │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ chatHistoryLoader.js → прикрепляет сообщения в начало │ -│ └─ messageRenderer.prependMessage() для каждого │ -└───────────────────────────────┬─────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ DOM обновлён → старые сообщения видны │ -│ └─ Сохранение позиции скролла │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 📊 Компоненты и их взаимодействие - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ chatDetail.js │ -│ (Оркестратор компонентов) │ -└────────────────┬────────────────────────────────┬───────────────┘ - │ │ - ┌───────────▼──────────┐ ┌──────────▼──────────┐ - │ userWebSocket.js │◄────────┤ messageRenderer.js │ - │ (WebSocket соединение)│ │ (Рендеринг HTML) │ - └───────────┬──────────┘ └─────────────────────┘ - │ - ┌───────────▼──────────┐ - │ chatComposer.js │ - │ (Форма отправки) │ - └──────────────────────┘ - │ - ┌───────────▼──────────┐ - │ chatMarkRead.js │ - │ (Отметка прочитанных)│ - └──────────────────────┘ - │ - ┌───────────▼──────────┐ - │chatHistoryLoader.js │ - │ (Загрузка истории) │ - └──────────────────────┘ -``` - ---- - -## 🔑 Ключевые особенности - -### 1. ✅ Отключен серверный рендеринг сообщений -- Раньше: Django рендерил `messages` через `{% for message in messages %}` -- Сейчас: WebSocket отправляет `initial_messages` после подключения -- Преимущество: Единый механизм рендеринга (JS), меньше дублирования кода - -### 2. ✅ Предотвращение дубликатов -```javascript -const existingMessage = container.querySelector(`[data-message-id="${msg.id}"]`); -if (existingMessage) { - console.log('Message already exists, skipping:', msg.id); - return; -} -``` - -### 3. ✅ Автоматические разделители дней -```javascript -let lastDay = null; -messages.forEach(msg => { - const msgDay = this.formatDay(msgDate); - if (msgDay !== lastDay) { - this.addDayDivider(msgDay, container); - lastDay = msgDay; - } - this.renderMessage(msg, container); -}); -``` - -### 4. ✅ Ленивая загрузка истории -- IntersectionObserver отслеживает элемент `.history-loader` -- При видимости → загружает старые сообщения -- Сохраняет позицию скролла - -### 5. ✅ Real-time обновления -- Новые сообщения → `new_message` -- Редактирование → `message_updated` -- Удаление → `message_deleted` -- Реакции → `reaction_added/removed` -- Голосования → `poll_update` -- Индикатор печати → `typing_start/stop` - ---- - -## 🛠️ Порядок инициализации - -``` -1. HTTP Request → ChatDetailView -2. Render HTML Template -3. DOM Ready -4. Load JS Modules (chatDetail.js, components/*) -5. Read Config from data-attributes -6. Wait for WebSocket Ready (user:ws-ready event) -7. Initialize Components: - ├─ FormManager - ├─ MarkRead - ├─ Composer - ├─ HistoryLoader - └─ MessageRenderer -8. Configure WebSocket with components -9. Open Chat: userWs.openChat(chatId, true) -10. WebSocket → Backend: { action: 'open_chat' } -11. Backend → WebSocket: { type: 'initial_messages' } -12. Render Messages → DOM -13. Remove #initialLoader -14. Scroll to Bottom -15. Initialize Polls -16. Ready! ✓ -``` - ---- - -## 📝 Итоговая схема - -``` -┌──────────────────────────────────────────────────────────────────────┐ -│ СТРАНИЦА ЧАТА │ -├──────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ 1. DJANGO VIEW (Backend) │ │ -│ │ - Проверка доступа │ │ -│ │ - Подготовка контекста (без messages) │ │ -│ │ - Рендеринг HTML шаблона │ │ -│ └────────────────────────┬────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ 2. HTML TEMPLATE │ │ -│ │ - Пустой контейнер #chatScroll │ │ -│ │ - Индикатор загрузки #initialLoader │ │ -│ │ - Data-атрибуты с конфигом │ │ -│ │ - Форма композера #chatForm │ │ -│ └────────────────────────┬────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ 3. JAVASCRIPT INIT (chatDetail.js) │ │ -│ │ - Загрузка модулей │ │ -│ │ - Ожидание WebSocket │ │ -│ │ - Инициализация компонентов │ │ -│ └────────────────────────┬────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ 4. WEBSOCKET CONNECTION (userWebSocket.js) │ │ -│ │ - openChat(chatId, loadHistory=true) │ │ -│ │ - Backend → initial_messages │ │ -│ └────────────────────────┬────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ 5. MESSAGE RENDERING (messageRenderer.js) │ │ -│ │ - renderMessages(messages) │ │ -│ │ - Разделители дней │ │ -│ │ - Проверка дубликатов │ │ -│ │ - Вставка в DOM │ │ -│ └─────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ 6. REAL-TIME UPDATES │ │ -│ │ - new_message → добавить │ │ -│ │ - message_updated → обновить │ │ -│ │ - message_deleted → удалить │ │ -│ │ - reaction_added/removed → обновить реакции │ │ -│ │ - poll_update → обновить голосование │ │ -│ └─────────────────────────────────────────────────────────────┘ │ -│ │ -└──────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 📚 Связанные документы - -- `CHAT_RENDERING_ANALYSIS.md` - Подробный анализ архитектуры рендеринга -- `WEBSOCKET_UNIFIED_MIGRATION.md` - Миграция на единое WebSocket соединение -- `CHAT_CACHING_FIX.md` - Отключение кэширования страницы чата -- `MESSAGE_EDITING_ANALYSIS.md` - Механизм редактирования сообщений -- `POLLS_IMPLEMENTATION.md` - Реализация голосований - ---- - -**Документ обновлён:** 4 декабря 2025 г. diff --git a/DYNAMIC_MEDIA_URLS.md b/DYNAMIC_MEDIA_URLS.md deleted file mode 100644 index 2f671d5f..00000000 --- a/DYNAMIC_MEDIA_URLS.md +++ /dev/null @@ -1,130 +0,0 @@ -# Динамическая генерация ссылок на медиа - -## Проблема -При работе с несколькими доменами (локальный и глобальный) статические ссылки на медиа-файлы работали только для одного домена. - -## Решение -Реализовали динамическую генерацию ссылок на медиа на основе текущего домена запроса. - -## Что изменилось - -### 1. Context Processor -**Файл:** `backend/core/context_processors.py` - -Добавлен context processor `media_domain()`, который: -- Определяет текущий домен из запроса (`request.get_host()`) -- Формирует динамический `MEDIA_URL_DYNAMIC` с учётом протокола и домена -- Добавляет переменные в контекст всех шаблонов: - - `MEDIA_URL_DYNAMIC` — полный URL для медиа (например: `http://localhost:9000/media/` или `https://example.com/media/`) - - `CURRENT_DOMAIN` — текущий домен - - `CURRENT_SCHEME` — протокол (http/https) - -### 2. Настройки Django -**Файл:** `backend/eusrr_backend/settings.py` - -Добавлен context processor в `TEMPLATES`: -```python -"context_processors": [ - # ... - "core.context_processors.media_domain", -], -``` - -### 3. Обновлённые шаблоны -Все шаблоны обновлены для использования динамических ссылок: - -#### `backend/templates/feed/_feed_cards.html` -- Изображения постов -- Аватары авторов -- Добавлен `loading="lazy"` и `decoding="async"` для оптимизации - -#### `backend/templates/feed/feed_list.html` -- Аватары новых сотрудников - -#### `backend/templates/feed/post_detail.html` -- Изображения постов в детальном виде -- Аватары авторов - -## Логика обработки URL - -Шаблоны проверяют формат URL и обрабатывают его соответственно: - -```django -{% if img_url|slice:":7" == "/media/" %} - {# Относительный путь /media/... → добавляем домен #} - ... -{% elif img_url|slice:":1" == "/" %} - {# Абсолютный путь /... → добавляем схему и домен #} - ... -{% else %} - {# Уже полный URL или внешняя ссылка → используем как есть #} - ... -{% endif %} -``` - -## Примеры работы - -### Локальный домен (localhost:9000) -``` -Запрос: http://localhost:9000/feed/ -MEDIA_URL_DYNAMIC: http://localhost:9000/media/ - -Файл: /media/feed/images/photo.jpg -Результат: http://localhost:9000/media/feed/images/photo.jpg -``` - -### Глобальный домен (example.com) -``` -Запрос: https://example.com/feed/ -MEDIA_URL_DYNAMIC: https://example.com/media/ - -Файл: /media/feed/images/photo.jpg -Результат: https://example.com/media/feed/images/photo.jpg -``` - -## Преимущества - -✅ **Автоматическая адаптация** — один код работает на всех доменах -✅ **Без перезапуска** — не нужно менять настройки при переключении доменов -✅ **SEO-дружественные URL** — правильные абсолютные ссылки -✅ **Поддержка HTTPS/HTTP** — автоматически определяется из запроса -✅ **Оптимизация загрузки** — добавлены атрибуты `loading="lazy"` и `decoding="async"` - -## Дальнейшие улучшения - -### Возможные оптимизации: -1. **CDN интеграция** — добавить отдельные CDN домены для статики -2. **Оптимизация изображений** — генерация thumbnails разных размеров -3. **WebP/AVIF форматы** — современные форматы для уменьшения размера -4. **Кэширование** — добавить Cache-Control заголовки -5. **Версионирование** — добавить версии к URL для инвалидации кэша - -### Пример с CDN: -```python -def media_domain(request): - host = request.get_host().split(':')[0] - - # Определяем CDN на основе домена - if host in ['localhost', '127.0.0.1']: - cdn_url = f"{request.scheme}://{request.get_host()}/media/" - elif 'dev' in host: - cdn_url = "https://cdn-dev.example.com/media/" - else: - cdn_url = "https://cdn.example.com/media/" - - return {'MEDIA_URL_DYNAMIC': cdn_url} -``` - -## Тестирование - -Проверьте работу на обоих доменах: -1. Откройте ленту на `http://localhost:9000/feed/` -2. Откройте ленту на `https://your-domain.com/feed/` -3. Убедитесь, что изображения загружаются корректно в обоих случаях -4. Проверьте в DevTools, что URLs формируются правильно - -## Откат изменений - -Если нужно вернуться к старому поведению: -1. Удалить `core.context_processors.media_domain` из `settings.py` -2. В шаблонах заменить `{{ MEDIA_URL_DYNAMIC }}{{ img_url|slice:"7:" }}` на `{{ img_url }}` diff --git a/FEED_AJAX_REFACTORING.md b/FEED_AJAX_REFACTORING.md deleted file mode 100644 index 88d4f25f..00000000 --- a/FEED_AJAX_REFACTORING.md +++ /dev/null @@ -1,249 +0,0 @@ -# Рефакторинг ленты новостей на AJAX - -## 📋 Что было сделано - -Лента новостей переделана с серверного рендеринга на клиентскую загрузку через API, по аналогии с модалом просмотра публикации. - -## 🎯 Преимущества - -### 1. **Правильные URL медиа-файлов** -- ✅ Изображения постов отображаются с правильным доменом -- ✅ Аватары авторов загружаются корректно -- ✅ Работает одинаково на localhost и production - -### 2. **Unified архитектура** -``` -Модал публикации → API через fetch (из браузера) -Лента новостей → API через fetch (из браузера) - ↓ - Одинаковый механизм! -``` - -### 3. **Бесконечная прокрутка (Infinite Scroll)** -- Автоматическая подгрузка постов при прокрутке -- Без перезагрузки страницы -- Плавный UX - -### 4. **Производительность** -- Первая загрузка быстрее (нет серверного рендеринга) -- Меньше нагрузки на backend -- Кэширование на уровне API - -## 🏗️ Архитектура - -### До: -``` -Browser → Django View → ApiClient → API (127.0.0.1) - ↓ - Проблема с доменом! - медиа URL: http://127.0.0.1/media/... -``` - -### После: -``` -Browser → API напрямую (corp.robotail.local) - ↓ - request.get_host() = 'corp.robotail.local' - ↓ - медиа URL: https://corp.robotail.local/media/... -``` - -## 📁 Файлы - -### Новые: -- `backend/static/js/components/feedList.js` - модуль загрузки ленты - -### Изменены: -- `backend/templates/feed/feed_list.html` - использует feedList.js -- `backend/feed/views_front.py` - упрощен, только новые сотрудники - -### Удалено: -- Серверный рендеринг постов через `_feed_cards.html` -- Пагинация на стороне сервера -- Сложная логика преобразования URL - -## 🔧 Использование - -### Базовая инициализация: -```javascript -import { FeedList } from '/static/js/components/feedList.js'; - -const feed = new FeedList({ - containerId: 'feedList', - apiUrl: '/api/v1/posts/', - params: { type: 'company' } -}); -``` - -### С кастомными параметрами: -```javascript -// Лента отдела -const deptFeed = new FeedList({ - containerId: 'deptFeedList', - apiUrl: '/api/v1/posts/', - params: { - type: 'department', - department: 5 - } -}); -``` - -### Методы: -```javascript -feed.refresh(); // Перезагрузить ленту -feed.loadPosts(1); // Загрузить страницу -feed.currentPage; // Текущая страница -feed.hasMore; // Есть ли еще посты -``` - -## 🚀 Функционал - -### Загрузка постов: -- ✅ Первая страница при инициализации -- ✅ Автоматическая подгрузка при прокрутке (500px до конца) -- ✅ Индикатор загрузки -- ✅ Обработка ошибок - -### Рендеринг: -- ✅ Карточка поста с заголовком, телом, изображением -- ✅ Аватар и имя автора (кликабельные) -- ✅ Дата публикации -- ✅ Значок закрепления -- ✅ Лайки и комментарии -- ✅ Открытие модала при клике - -### Поиск: -- ✅ Фильтрация по заголовку -- ✅ Фильтрация по автору -- ✅ Фильтрация по содержанию -- ✅ Работает через data-атрибуты - -## 🔍 Техническая информация - -### API Response: -```json -{ - "count": 42, - "next": "https://corp.robotail.local/api/v1/posts/?page=2", - "previous": null, - "results": [ - { - "id": 1, - "title": "Заголовок", - "body": "Содержание", - "image": "https://corp.robotail.local/media/posts/img.jpg", - "author": { - "id": 5, - "full_name": "Иван Иванов", - "avatar": "https://corp.robotail.local/media/avatars/5.jpg" - }, - "created_at_display": "05.12.2025 10:30", - "likes_count": 10, - "comments_count": 3, - "is_liked": false, - "pinned": false - } - ] -} -``` - -### Структура карточки: -```html -
-
- -
-
- -
-
- -
-
- -
-
-``` - -## 🐛 Debugging - -### Включить отладку: -```javascript -// В feedList.js временно добавить: -console.log('Loading posts:', params); -console.log('Response:', data); -``` - -### Проверить запросы: -``` -DevTools → Network → XHR -Ищем: /api/v1/posts/?type=company&page=1 -``` - -### Проверить медиа URL: -```javascript -// В консоли: -document.querySelectorAll('.feed-post img').forEach(img => { - console.log(img.src); -}); - -// Должны быть: https://corp.robotail.local/media/... -// НЕ должны быть: http://127.0.0.1/media/... -``` - -## 📊 Производительность - -### Метрики: -- **Первая загрузка**: ~200-300ms (без серверного рендеринга) -- **Подгрузка страницы**: ~100-150ms (только JSON) -- **Рендеринг 20 постов**: ~50ms (native DOM) - -### Оптимизации: -- ✅ Ленивая загрузка изображений (`loading="lazy"`) -- ✅ RequestAnimationFrame для скролла -- ✅ Debounce на проверку позиции -- ✅ Переиспользование API cache - -## 🔄 Миграция на production - -### 1. Деплой кода: -```bash -git pull origin master -``` - -### 2. Проверка: -- Открыть `/feed/` -- Проверить DevTools → Network -- Убедиться что запросы идут на правильный домен - -### 3. Откат (если что-то не так): -```bash -git revert HEAD -``` - -## ✅ Чеклист тестирования - -- [ ] Лента загружается при открытии страницы -- [ ] Infinite scroll работает (прокрутка вниз подгружает) -- [ ] Медиа отображаются корректно -- [ ] Клик по посту открывает модал -- [ ] Лайки работают -- [ ] Поиск фильтрует посты -- [ ] Создание поста обновляет ленту -- [ ] Работает на мобильных устройствах - -## 📚 Связанные файлы - -- `postDetailModal.js` - модал просмотра (использует ту же схему) -- `createPostModal.js` - модал создания -- `feedLikes.js` - обработка лайков -- `feedComments.js` - обработка комментариев -- `listFilter.js` - поиск и фильтрация - -## 🎓 Выводы - -Переход на AJAX загрузку решил проблему с медиа URL и улучшил UX: -1. Медиа отдаются с правильным доменом автоматически -2. Нет нужды в сложной логике на сервере -3. Unified подход для модалов и ленты -4. Бесконечная прокрутка из коробки diff --git a/IP_REGISTRATION_RESTRICTIONS.md b/IP_REGISTRATION_RESTRICTIONS.md deleted file mode 100644 index 10592bf7..00000000 --- a/IP_REGISTRATION_RESTRICTIONS.md +++ /dev/null @@ -1,290 +0,0 @@ -# Ограничение доступа к регистрации по IP - -## Описание - -Реализована система ограничения доступа к регистрации новых пользователей по IP-адресу через middleware. Это позволяет разрешить регистрацию только из локальной сети или конкретных IP-адресов. - -## Файлы - -### 1. `backend/common/ip_restrictions.py` - -Утилиты для проверки IP-адресов: - -- **`get_client_ip(request)`** - получает реальный IP клиента (учитывает прокси) -- **`is_local_ip(ip_address)`** - проверяет, является ли IP локальным -- **`is_ip_allowed(ip_address, allowed_networks)`** - проверяет доступ для IP - -### 2. `backend/eusrr_backend/middleware.py` - -Добавлен класс `RegistrationIPRestrictionMiddleware` для проверки IP при регистрации. - -### 3. Настройка в `backend/eusrr_backend/settings.py` - -В `MIDDLEWARE` добавлена строка: -```python -"eusrr_backend.middleware.RegistrationIPRestrictionMiddleware", -``` - -Добавлена настройка `REGISTRATION_ALLOWED_IPS`: -```python -REGISTRATION_ALLOWED_IPS = None # По умолчанию только локальные IP -``` - -## Конфигурация - -### По умолчанию (только локальные IP) - -```python -REGISTRATION_ALLOWED_IPS = None -``` - -Разрешены только: -- `127.0.0.0/8` (localhost) -- `10.0.0.0/8` (частная сеть класса A) -- `172.16.0.0/12` (частная сеть класса B) -- `192.168.0.0/16` (частная сеть класса C) -- `::1` (IPv6 localhost) -- `fe80::/10` (IPv6 link-local) - -### Разрешить все IP - -```python -REGISTRATION_ALLOWED_IPS = ['*'] -``` - -### Разрешить конкретные сети (CIDR) - -```python -REGISTRATION_ALLOWED_IPS = [ - '192.168.1.0/24', # Локальная сеть офиса - '10.0.0.0/8', # Корпоративная сеть - '172.16.100.0/24', # VPN сеть -] -``` - -### Разрешить конкретные IP-адреса - -```python -REGISTRATION_ALLOWED_IPS = [ - '192.168.1.100', - '192.168.1.101', - '203.0.113.42', -] -``` - -### Комбинированный подход - -```python -REGISTRATION_ALLOWED_IPS = [ - '192.168.1.0/24', # Вся локальная сеть - '203.0.113.42', # Конкретный внешний IP - '198.51.100.0/28', # Диапазон внешних IP -] -``` - -## Поведение при блокировке - -### Для обычных view (HTML) - -При попытке доступа с неразрешенного IP пользователь увидит: - -```html -

403 Forbidden

-

Регистрация доступна только из локальной сети.

-

Ваш IP: 203.0.113.50

-``` - -### Для API endpoints (JSON) - -При попытке доступа с неразрешенного IP API вернет: - -```json -{ - "detail": "Регистрация доступна только из локальной сети.", - "client_ip": "203.0.113.50" -} -``` - -HTTP статус: `403 Forbidden` - -## Работа с прокси - -Функция `get_client_ip()` корректно определяет реальный IP клиента даже при использовании прокси-серверов: - -1. Проверяет заголовок `X-Forwarded-For` -2. Берет первый IP из списка (клиентский IP) -3. Если заголовка нет, использует `REMOTE_ADDR` - -### Настройка для работы за Nginx - -Добавьте в конфигурацию Nginx: - -```nginx -location / { - proxy_pass http://127.0.0.1:9000; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; -} -``` - -## Тестирование - -### Проверка локального доступа - -```bash -curl http://localhost:9000/api/v1/auth/register/ \ - -H "Content-Type: application/json" \ - -d '{"email":"test@example.com","password":"test123",...}' -``` - -Должно работать с `127.0.0.1`. - -### Проверка блокировки (эмуляция внешнего IP) - -```bash -curl http://localhost:9000/api/v1/auth/register/ \ - -H "Content-Type: application/json" \ - -H "X-Forwarded-For: 203.0.113.50" \ - -d '{"email":"test@example.com","password":"test123",...}' -``` - -Должно вернуть `403 Forbidden` если `REGISTRATION_ALLOWED_IPS` не содержит этот IP. - -### Тестирование в коде - -```python -from common.ip_restrictions import is_local_ip, is_ip_allowed - -# Проверка локальных IP -assert is_local_ip('127.0.0.1') == True -assert is_local_ip('192.168.1.100') == True -assert is_local_ip('10.0.0.5') == True -assert is_local_ip('203.0.113.50') == False - -# Проверка с настройками -from django.conf import settings -settings.REGISTRATION_ALLOWED_IPS = ['192.168.1.0/24'] - -assert is_ip_allowed('192.168.1.50') == True -assert is_ip_allowed('192.168.2.50') == False -``` - -## Безопасность - -### Рекомендации - -1. **Для production**: используйте конкретные сети, не `['*']` -2. **За прокси**: убедитесь, что прокси правильно передает заголовки -3. **IPv6**: модуль поддерживает IPv6, учтите это в конфигурации -4. **Логирование**: при необходимости добавьте логирование заблокированных попыток - -### Обход защиты - -Потенциальные проблемы: - -1. **Подмена заголовков**: если прокси настроен неправильно, злоумышленник может подменить `X-Forwarded-For` -2. **VPN/Proxy клиента**: пользователь может использовать VPN с локальным IP - -### Дополнительная защита - -Рекомендуется комбинировать с: -- Rate limiting (throttling в DRF) -- CAPTCHA для регистрации -- Email верификация (уже реализована) -- Логирование попыток регистрации - -## Примеры использования в других view - -Если нужно ограничить доступ к другим URL по IP, можно создать дополнительные middleware или расширить существующий: - -```python -class CustomIPRestrictionMiddleware: - """Ограничение по IP для произвольных URL""" - - RESTRICTED_PATHS = ( - "/admin/", # Например, админка только из локальной сети - "/api/v1/sensitive/", # Чувствительные API - ) - - def __call__(self, request): - from common.ip_restrictions import get_client_ip, is_ip_allowed - - path = request.path_info - - if any(path.startswith(p) for p in self.RESTRICTED_PATHS): - client_ip = get_client_ip(request) - if not is_ip_allowed(client_ip): - # Блокируем доступ - ... - - return self.get_response(request) -``` - -## Отключение ограничений - -### Временное отключение (для разработки) - -В `settings.py`: - -```python -REGISTRATION_ALLOWED_IPS = ['*'] # Разрешить все IP -``` - -### Полное отключение - -В `settings.py` закомментируйте middleware: - -```python -MIDDLEWARE = [ - # ... другие middleware ... - # "eusrr_backend.middleware.RegistrationIPRestrictionMiddleware", # Отключено - "eusrr_backend.middleware.CacheControlMiddleware", -] -``` - -## Мониторинг - -### Добавление логирования - -Можно расширить `backend/eusrr_backend/middleware.py`: - -```python -def __call__(self, request): - import logging - logger = logging.getLogger(__name__) - - path = request.path_info - - if not any(path.startswith(p) for p in self.RESTRICTED_PATHS): - return self.get_response(request) - - client_ip = get_client_ip(request) - - if not is_ip_allowed(client_ip): - logger.warning( - f'Registration blocked for IP {client_ip} ' - f'(path: {path}, user-agent: {request.META.get("HTTP_USER_AGENT")})' - ) - # ... блокировка ... - - logger.info(f'Registration allowed for IP {client_ip}') - return self.get_response(request) -``` - -### Метрики - -Можно отслеживать: -- Количество заблокированных попыток регистрации -- IP-адреса, которые часто блокируются -- Географию заблокированных запросов - -## Вопросы и поддержка - -При возникновении проблем проверьте: - -1. Правильность настройки `REGISTRATION_ALLOWED_IPS` в settings.py -2. Корректность заголовков прокси (если используется) -3. Middleware включен в `MIDDLEWARE` в settings.py -4. Формат IP-адресов в конфигурации (должны быть валидными CIDR или IP) -5. Логи Django на наличие ошибок в модуле `eusrr_backend.middleware` diff --git a/IP_RESTRICTIONS_README.md b/IP_RESTRICTIONS_README.md deleted file mode 100644 index 3b31d071..00000000 --- a/IP_RESTRICTIONS_README.md +++ /dev/null @@ -1,96 +0,0 @@ -# 🔒 IP-ограничения для регистрации - Краткая инструкция - -## ✅ Что сделано - -Добавлен **middleware** для ограничения доступа к регистрации по IP-адресу. - -## 📁 Изменённые файлы - -1. **`backend/eusrr_backend/middleware.py`** - добавлен `RegistrationIPRestrictionMiddleware` -2. **`backend/eusrr_backend/settings.py`** - middleware включен в `MIDDLEWARE` -3. **`backend/common/ip_restrictions.py`** - утилиты для проверки IP - -## ⚙️ Настройка - -В `backend/eusrr_backend/settings.py` найдите: - -```python -# IP ОГРАНИЧЕНИЯ ДЛЯ РЕГИСТРАЦИИ -REGISTRATION_ALLOWED_IPS = None # По умолчанию только локальные IP -``` - -### Варианты настройки: - -```python -# 1. Только локальные IP (по умолчанию) -REGISTRATION_ALLOWED_IPS = None - -# 2. Разрешить все IP (для разработки) -REGISTRATION_ALLOWED_IPS = ['*'] - -# 3. Разрешить конкретные сети -REGISTRATION_ALLOWED_IPS = [ - '192.168.1.0/24', # Локальная сеть офиса - '10.0.0.0/8', # Корпоративная сеть -] - -# 4. Разрешить конкретные IP -REGISTRATION_ALLOWED_IPS = ['192.168.1.100', '203.0.113.42'] -``` - -## 🎯 Как работает - -**По умолчанию (None):** -- ✅ Разрешены: `127.0.0.1`, `192.168.x.x`, `10.x.x.x`, `172.16-31.x.x` -- ❌ Заблокированы: все внешние IP - -**Блокируемые URL:** -- `/auth/register/` (веб-форма) -- `/api/v1/auth/register/` (API) - -**Ответ при блокировке:** -- API: `{"detail": "Регистрация доступна только из локальной сети.", "client_ip": "..."}` -- Веб: Красивая страница 403 с информацией об IP - -## 🔧 Отключение - -### Временно (для разработки): -```python -REGISTRATION_ALLOWED_IPS = ['*'] -``` - -### Полностью: -В `settings.py` закомментируйте строку в `MIDDLEWARE`: -```python -# "eusrr_backend.middleware.RegistrationIPRestrictionMiddleware", -``` - -## 🧪 Тестирование - -```bash -# Из локальной сети - работает -curl http://localhost:9000/api/v1/auth/register/ \ - -H "Content-Type: application/json" \ - -d '{"email":"test@example.com",...}' - -# С внешнего IP - блокируется -curl http://localhost:9000/api/v1/auth/register/ \ - -H "X-Forwarded-For: 203.0.113.50" \ - -d '{"email":"test@example.com",...}' -``` - -## 🌐 Работа за прокси - -Middleware автоматически определяет реальный IP через заголовки `X-Forwarded-For` и `X-Real-IP`. - -Настройте Nginx: -```nginx -location / { - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Real-IP $remote_addr; -} -``` - -## 📚 Полная документация - -См. `IP_REGISTRATION_RESTRICTIONS.md` diff --git a/MESSAGE_NEWLINES_FIX.md b/MESSAGE_NEWLINES_FIX.md deleted file mode 100644 index 8322789c..00000000 --- a/MESSAGE_NEWLINES_FIX.md +++ /dev/null @@ -1,198 +0,0 @@ -# Исправление: Поддержка переносов строк в сообщениях - -**Дата:** 4 декабря 2025 г. -**Проблема:** Сообщения не отображали переносы строк (многострочный текст показывался одной строкой) - ---- - -## 🔴 Проблема - -При отправке многострочных сообщений в чате (с нажатием Enter для переноса строки), текст отображался одной строкой, игнорируя все переносы. - -### Пример: - -**Ввод пользователя:** -``` -Строка 1 -Строка 2 -Строка 3 -``` - -**Отображалось как:** -``` -Строка 1 Строка 2 Строка 3 -``` - ---- - -## 🔍 Анализ - -### 1. Рендеринг сообщений (`messageRenderer.js`) - -```javascript -// Строка 210 -${msg.content ? `
${this.escapeHtml(msg.content)}
` : ''} - -// Метод escapeHtml (строка 318) -escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} -``` - -Метод `escapeHtml()` корректно экранирует HTML, но переносы строк (`\n`) остаются в виде символов, которые браузер игнорирует в обычном режиме отображения. - -### 2. CSS стили bubble (`chat-detail.css`) - -```css -.bubble { - border-radius: 16px; - padding: var(--space-1) var(--space-2); - word-break: break-word; - /* white-space не указан - по умолчанию: normal */ - box-shadow: 0 1px 2px rgba(0, 0, 0, .06); -} -``` - -По умолчанию `white-space: normal` схлопывает последовательные пробелы и переносы строк в один пробел. - ---- - -## ✅ Решение - -Добавлено CSS свойство `white-space: pre-wrap` для класса `.bubble`: - -```css -.bubble { - border-radius: 16px; - padding: var(--space-1) var(--space-2); - word-break: break-word; - white-space: pre-wrap; /* ← ДОБАВЛЕНО */ - box-shadow: 0 1px 2px rgba(0, 0, 0, .06); -} -``` - -### Что делает `white-space: pre-wrap`: - -| Свойство | Переносы строк | Пробелы | Перенос текста | -|----------|---------------|---------|----------------| -| `normal` (по умолчанию) | ❌ Игнорируются | ❌ Схлопываются | ✅ Есть | -| `pre` | ✅ Сохраняются | ✅ Сохраняются | ❌ Нет | -| **`pre-wrap`** | **✅ Сохраняются** | **✅ Сохраняются** | **✅ Есть** | -| `pre-line` | ✅ Сохраняются | ❌ Схлопываются | ✅ Есть | - -`pre-wrap` — идеальный вариант для чатов: -- ✅ Сохраняет переносы строк -- ✅ Сохраняет множественные пробелы (для форматирования) -- ✅ Автоматически переносит длинные строки (не вылезает за пределы) - ---- - -## 🎨 Результат - -### До исправления: -``` -Привет! Как дела? Давно не виделись! -``` - -### После исправления: -``` -Привет! -Как дела? -Давно не виделись! -``` - ---- - -## 🧪 Тестирование - -### Примеры для тестирования: - -1. **Простой перенос:** - ``` - Строка 1 - Строка 2 - ``` - -2. **Множественные переносы:** - ``` - Строка 1 - - - Строка 2 (после двух пустых строк) - ``` - -3. **Длинная строка с переносом:** - ``` - Очень длинная строка текста, которая должна автоматически перенестись на новую строку если не помещается в ширину bubble - ``` - -4. **Форматированный текст:** - ``` - Список покупок: - - Молоко - - Хлеб - - Масло - ``` - -5. **Код или табуляция:** - ``` - function hello() { - console.log("Hello!"); - } - ``` - ---- - -## 📁 Изменённые файлы - -- ✅ `backend/static/css/components/chat-detail.css` - добавлен `white-space: pre-wrap` для `.bubble` - ---- - -## 🔄 Альтернативные решения (не использовались) - -### Вариант 2: Конвертация `\n` в `
` в JavaScript - -```javascript -// В messageRenderer.js -formatContent(text) { - return this.escapeHtml(text).replace(/\n/g, '
'); -} - -// Использование: -${msg.content ? `
${this.formatContent(msg.content)}
` : ''} -``` - -**Недостатки:** -- ❌ Не сохраняет множественные пробелы -- ❌ Требует изменения JS кода -- ❌ Менее гибко для будущих доработок - -### Вариант 3: CSS `white-space: pre-line` - -```css -.bubble { - white-space: pre-line; -} -``` - -**Недостатки:** -- ❌ Не сохраняет множественные пробелы (схлопывает в один) -- ❌ Менее удобно для форматированного текста - ---- - -## 🎯 Выводы - -1. ✅ **CSS решение** (`white-space: pre-wrap`) — наиболее правильное и простое -2. ✅ Не требует изменений в JavaScript -3. ✅ Поддерживает все виды форматирования текста -4. ✅ Совместимо с существующей логикой экранирования HTML -5. ✅ Работает для всех типов сообщений (свои и чужие) - ---- - -**Статус:** ✅ Исправлено -**Тестирование:** Требуется проверка в браузере diff --git a/MESSAGE_RENDERING_REFACTORING.md b/MESSAGE_RENDERING_REFACTORING.md deleted file mode 100644 index 39b1f22b..00000000 --- a/MESSAGE_RENDERING_REFACTORING.md +++ /dev/null @@ -1,272 +0,0 @@ -# Рефакторинг системы рендеринга сообщений - -## Дата: 4 декабря 2025 г. - -## Проблема - -Система рендеринга сообщений имела дублирование кода: -- `messageRenderer.js` - рендерил новые сообщения через WebSocket -- `chatMessageTemplates.js` - рендерил старые сообщения при скролле вверх - -Оба файла содержали: -1. **Многострочные HTML-шаблоны** с переносами строк и отступами -2. **Дублирующуюся логику** для формирования HTML -3. **Разные подходы** к обработке данных - -### Симптомы -- Пилюли сообщений растягивались по высоте из-за whitespace в HTML -- `white-space: pre-line` сохранял переносы из исходного кода JavaScript -- Различное отображение сообщений в зависимости от способа загрузки - -## Решение - -### 1. Единый источник правды -**messageRenderer.js** стал единственным местом генерации HTML: - -```javascript -export class MessageRenderer { - // Полный HTML с обёрткой .msg для WebSocket - buildMessageHtml(msg, isOwn) { - return `
${this.buildMessageInnerHtml(msg, isOwn)}
`; - } - - // Внутренний HTML без обёртки для DOM-элементов - buildMessageInnerHtml(msg, isOwn) { - // Все шаблоны в одну строку без переносов - return `${avatarHtml}
...
${rightAvatarHtml}`; - } -} -``` - -### 2. Обёртка в chatMessageTemplates.js -**chatMessageTemplates.js** теперь использует MessageRenderer: - -```javascript -import { MessageRenderer } from './messageRenderer.js'; - -export function createMessageElement(msg, options = {}) { - const renderer = new MessageRenderer({ - meId: options.meId, - profileUrl: options.profileUrl, - detailUrlTemplate: options.detailUrlTemplate, - currentUserAvatar: options.avatarMap[options.meId] - }); - - const wrap = document.createElement('div'); - // Настройка атрибутов wrap... - - // Используем единый рендерер - wrap.innerHTML = renderer.buildMessageInnerHtml(enrichedMsg, mine); - - return wrap; -} -``` - -### 3. Устранение многострочных шаблонов - -**ДО:** -```javascript -const forwardedHtml = msg.is_forwarded ? ` -
- -
-
Переслано от ${author}
-
-
` : ''; -``` - -**ПОСЛЕ:** -```javascript -const forwardedHtml = msg.is_forwarded ? `
Переслано от ${msg.forwarded_from.author_name || 'Пользователя'}${msg.forwarded_from.chat_name ? ` из «${msg.forwarded_from.chat_name}»` : ''}
${msg.forwarded_from.created_at ? `
${msg.forwarded_from.created_at}
` : ''}
` : ''; -``` - -### 4. Рефакторинг всех компонентов - -Применено ко всем HTML-генераторам: -- ✅ `forwardedHtml` - информация о пересылке -- ✅ `replyHtml` - цитата ответа -- ✅ `avatarHtml` - аватары пользователей -- ✅ `rightAvatarHtml` - аватар справа (свои сообщения) -- ✅ `attachmentsHtml` - вложения (изображения, файлы) -- ✅ `pollHtml` - голосования/опросы -- ✅ `bubble` - основной контейнер сообщения - -## Архитектура после рефакторинга - -``` -┌──────────────────────────────────────────────────┐ -│ messageRenderer.js │ -│ │ -│ ┌─────────────────────────────────────────┐ │ -│ │ buildMessageHtml(msg, isOwn) │ │ -│ │ → Полный HTML с обёрткой .msg │ │ -│ │ → Используется для WebSocket │ │ -│ └─────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────┐ │ -│ │ buildMessageInnerHtml(msg, isOwn) │◄───┼─── Единый источник -│ │ → HTML без обёртки .msg │ │ правды для всех -│ │ → Все шаблоны в одну строку │ │ HTML-шаблонов -│ │ → Нет whitespace из исходного кода │ │ -│ └─────────────────────────────────────────┘ │ -│ │ │ │ │ -│ ▼ ▼ ▼ │ -│ buildAttachmentHtml buildPollHtml ... │ -└──────────────────────────────────────────────────┘ - ▲ - │ - │ import & использование - │ -┌──────────────────────────────────────────────────┐ -│ chatMessageTemplates.js │ -│ │ -│ createMessageElement(msg, options) │ -│ → Создаёт DOM элемент │ -│ → Использует MessageRenderer.buildMessageInnerHtml │ -│ → Добавляет pending статус если нужно │ -└──────────────────────────────────────────────────┘ - ▲ - │ - │ использование - │ -┌──────────────────────────────────────────────────┐ -│ chatHistoryLoader.js │ -│ │ -│ prependMessages() → createMessageElement() │ -│ → Загрузка старых сообщений при скролле вверх │ -└──────────────────────────────────────────────────┘ -``` - -## Поток данных - -### WebSocket (новые сообщения) -``` -WebSocket event - ↓ -chatDetail.js - ↓ -messageRenderer.buildMessageHtml() - ↓ -insertAdjacentHTML() - ↓ -DOM -``` - -### HTTP + Скролл (старые сообщения) -``` -Scroll вверх - ↓ -chatHistoryLoader.loadMore() - ↓ -fetch(/api/messages?before_id=...) - ↓ -chatMessageTemplates.createMessageElement() - ↓ - → MessageRenderer.buildMessageInnerHtml() - ↓ -DOM element - ↓ -insertBefore() -``` - -## Преимущества рефакторинга - -### ✅ Единообразие -- Одинаковый HTML независимо от способа загрузки -- Нет расхождений между WebSocket и HTTP -- Единая логика обработки всех компонентов (ответы, вложения, опросы) - -### ✅ Производительность -- Нет лишних whitespace в HTML -- Компактные пилюли сообщений -- Правильная работа `white-space: pre-line` (сохраняет только `\n` из контента) - -### ✅ Поддерживаемость -- DRY (Don't Repeat Yourself) -- Одно место для изменений -- Легче тестировать и дебажить - -### ✅ Расширяемость -- Новые типы вложений добавляются в одном месте -- Новые поля сообщений обрабатываются единообразно - -## CSS изменения - -### _chat-detail.scss -```scss -.bubble { - border-radius: 16px; - padding: var(--space-1) var(--space-2); - word-break: break-word; - white-space: pre-line; /* Сохраняет \n из контента, схлопывает пробелы из HTML */ - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); - - > div { - white-space: inherit; /* Наследование для вложенных элементов */ - } -} -``` - -## Тестирование - -После рефакторинга проверить: -1. ✅ Переносы строк (`\n`) отображаются корректно -2. ✅ Высота пилюль соответствует контенту (без растягивания) -3. ✅ Сообщения с 1 символом компактные -4. ✅ Старые сообщения (скролл вверх) выглядят так же как новые (WebSocket) -5. ✅ Сообщения с вложениями, опросами, ответами не имеют лишних отступов -6. ✅ Аватары отображаются корректно -7. ✅ Реакции работают -8. ✅ Редактирование сообщений работает - -## Совместимость - -### Обратная совместимость -- ✅ Все существующие функции сохранены -- ✅ API не изменился -- ✅ DOM-структура осталась прежней -- ✅ CSS-классы не изменились - -### Зависимости -```javascript -// messageRenderer.js -export class MessageRenderer { ... } -export function createMessageRenderer(config) { ... } - -// chatMessageTemplates.js -import { MessageRenderer } from './messageRenderer.js'; -import { esc } from '../utils/stringUtils.js'; - -// chatHistoryLoader.js -import { createMessageElement } from './chatMessageTemplates.js'; -``` - -## Следующие шаги - -1. **Тестирование** - проверить все сценарии использования -2. **Мониторинг** - отследить производительность в production -3. **Документация** - обновить комментарии в коде -4. **Оптимизация** - возможно кеширование MessageRenderer -5. **Расширение** - добавить поддержку новых типов контента - -## Файлы изменены - -- ✅ `backend/static/js/components/messageRenderer.js` - - Добавлен метод `buildMessageInnerHtml()` - - Все шаблоны переведены в одну строку - - Добавлена поддержка параметра `meId` - -- ✅ `backend/static/js/components/chatMessageTemplates.js` - - Переписан `createMessageElement()` для использования MessageRenderer - - Удалён дублирующийся код генерации HTML - - Сохранена обратная совместимость API - -- ✅ `backend/static/scss/components/_chat-detail.scss` - - Изменён `white-space: pre-wrap` → `pre-line` - - Добавлено правило наследования для вложенных `div` - -## Авторы - -- Рефакторинг: GitHub Copilot + Igor -- Дата: 4 декабря 2025 г. diff --git a/PROCUREMENT_APPROVERS.md b/PROCUREMENT_APPROVERS.md deleted file mode 100644 index cb03d27b..00000000 --- a/PROCUREMENT_APPROVERS.md +++ /dev/null @@ -1,160 +0,0 @@ -# Механизм назначения согласующих в заявках на закупку - -## Как это работает - -### 1. Определение требуемых ролей (модель `ProcurementRequest`) - -Когда пользователь отправляет заявку на согласование (`submit` action), система определяет какие роли нужны: - -```python -# в procurement/models.py - метод get_required_approvals() - -cost = estimated_cost # Сумма заявки - -if cost < 10,000₽: # НИЗКАЯ - → [DEPARTMENT_HEAD] # Только руководитель отдела - -elif cost < 100,000₽: # СРЕДНЯЯ - → [DEPARTMENT_HEAD, # Руководитель отдела + - FINANCE_MANAGER] # Финансовый менеджер - -else: # ВЫСОКАЯ (> 100K) - → [DEPARTMENT_HEAD, # Руководитель отдела + - FINANCE_MANAGER, # Финансовый менеджер + - DIRECTOR] # Генеральный директор -``` - -### 2. Поиск конкретных людей (вьюсет `ProcurementRequestViewSet.submit()`) - -После определения ролей система ищет людей, занимающих эти должности: - -```python -for role in required_approvals: - approver = None - - if role == DEPARTMENT_HEAD: - # ✅ Берёт руководителя из department.head - approver = procurement_request.department.head - - elif role == FINANCE_MANAGER: - # ✅ Ищет пользователя с правом change_budget - approver = Employee.objects.filter( - is_active=True, - groups__permissions__codename='change_budget' - ).first() - - elif role == DIRECTOR: - # ✅ Берёт первого суперпользователя - approver = Employee.objects.filter( - is_superuser=True - ).first() - - # Создаём запись согласования - Approval.objects.create( - request=procurement_request, - approver=approver, - role=role, - status=ApprovalStatus.PENDING - ) -``` - -## Пример - -### Заявка на 50,000₽ от отдела IT - -``` -Сумма: 50,000₽ -Отдел: IT (руководитель = Петр Петров) - -┌─────────────────────────────────────────┐ -│ Определение ролей (get_required_approvals) -│ 10,000 < 50,000 < 100,000 -│ → [DEPARTMENT_HEAD, FINANCE_MANAGER] -└─────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────┐ -│ Поиск согласующих (submit action) │ -├─────────────────────────────────────────┤ -│ DEPARTMENT_HEAD │ -│ → IT.head = Петр Петров ✓ │ -│ │ -│ FINANCE_MANAGER │ -│ → Permission(change_budget) │ -│ → Мария Сидорова ✓ │ -└─────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────┐ -│ Создание записей согласования │ -├─────────────────────────────────────────┤ -│ Approval #1: Петр Петров (PENDING) │ -│ Approval #2: Мария Сидорова (PENDING) │ -└─────────────────────────────────────────┘ - ↓ - Уведомления - ↙ ↖ - Петр Мария -``` - -## Где это конфигурируется? - -| Компонент | Где конфигурируется | Как | -|-----------|-------------------|-----| -| **Пороги сумм** | `procurement/constants.py` | APPROVAL_THRESHOLD_LOW, APPROVAL_THRESHOLD_HIGH | -| **Руководитель отдела** | Django Admin → Departments | Выбрать `head` в модели Department | -| **Финансовый менеджер** | Django Admin → Permissions | Дать permission `change_budget` пользователю или группе | -| **Директор** | Django Admin → Users | Сделать пользователя суперпользователем (`is_superuser=True`) | - -## Потенциальные проблемы и решения - -### 1. "Финансовый менеджер не найдена" -```python -# Причина: нет пользователя с правом change_budget -# Решение: -Django Admin → Users → выбрать пользователя → добавить permission change_budget -``` - -### 2. "Директор не найден" -```python -# Причина: нет суперпользователей в системе -# Решение: -Django Admin → Users → выбрать пользователя → поставить галочку is_superuser -``` - -### 3. Разные согласующие для разных отделов -```python -# Текущая реализация: один финменеджер и один директор на всю компанию -# Чтобы сделать разных: -# 1. Создать модель ApprovalChain или DepartmentApprovalPolicy -# 2. Связать со своими финменеджерами и директорами -# 3. Изменить логику поиска в submit action -``` - -## API для отладки - -### Проверить какие роли требуются для заявки -```bash -GET /api/procurement/requests/{id}/ -# Ответ содержит поле "required_approvals" если его добавить в сериализатор -``` - -### Узнать кто назначен согласующим -```bash -GET /api/procurement/requests/{id}/ -# approvals: [ -# { id, approver, role, status, comment } -# ] -``` - -## Параметры для настройки - -```python -# backend/procurement/constants.py - -APPROVAL_THRESHOLD_LOW = 10_000 # ← Можно менять -APPROVAL_THRESHOLD_HIGH = 50_000 # ← Можно менять - -# Порог | Рол требующиеся -# < 10K | Руководитель отдела -# 10K - 50K | Руководитель + Финансы -# > 50K | Руководитель + Финансы + Директор -``` diff --git a/PROCUREMENT_IMPLEMENTATION_PLAN.md b/PROCUREMENT_IMPLEMENTATION_PLAN.md deleted file mode 100644 index 1f4f0e84..00000000 --- a/PROCUREMENT_IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,1568 +0,0 @@ -# План Разработки Модуля "Закупки и Инвентарь" (Procurement) - -**Дата начала:** 8 декабря 2025 -**Статус:** 🚀 В разработке -**Версия:** 1.0 - ---- - -## 📋 Содержание - -1. [Обзор Проекта](#обзор-проекта) -2. [Архитектурные Решения](#архитектурные-решения) -3. [Архитектура и Модели](#архитектура-и-модели) -4. [Права Доступа](#права-доступа) -5. [Workflow Согласования](#workflow-согласования) -6. [API Endpoints](#api-endpoints) -7. [UI/UX Компоненты](#uiux-компоненты) -8. [Этапы Реализации](#этапы-реализации) -9. [Технологический Стек](#технологический-стек) -10. [Чеклист Реализации](#чеклист-реализации) - ---- - -## Архитектурные Решения - -### 🔔 Система Уведомлений: NotificationService вместо Celery - -**Решение:** Использовать встроенную систему уведомлений (`notifications.services.NotificationService`) вместо внедрения Celery. - -**Обоснование:** - -**За использование NotificationService:** - -1. ✅ **Уже существует и протестирована** - используется во всех модулях (communications, documents, feed, calendar_app, requests_app) -2. ✅ **Полнофункциональная** - поддерживает Web (WebSocket), Email (HTML), Telegram, настройки пользователя -3. ✅ **Простота** - не требует дополнительных процессов и сервисов -4. ✅ **Масштаб проекта** - 50-200 сотрудников, <10 заявок/день - синхронная отправка приемлема -5. ✅ **Меньше точек отказа** - нет дополнительных worker'ов для мониторинга -6. ✅ **Легче debugging** - ошибки в том же процессе и логах - -**Против Celery:** - -1. ❌ **Усложнение** - +2 процесса в production (worker + beat) -2. ❌ **Дублирование** - NotificationService уже отправляет email -3. ❌ **Overkill** - избыточно для текущего масштаба проекта -4. ❌ **DevOps overhead** - настройка supervisor/systemd, мониторинг workers - -**Производительность:** - -- SMTP отправка: ~0.5-2 секунды -- Согласование заявки: редкое действие (1-5 раз/день на отдел) -- Задержка 2 секунды приемлема для редких critical операций - -**Когда пересмотреть:** - -- > 500 сотрудников и >50 заявок/день - > -- Генерация тяжелых отчётов (PDF >10MB) -- Нестабильный email-провайдер требует retry логики - -**Периодические задачи:** Используется **APScheduler** (уже в requirements.txt) для: - -- Дайджесты уведомлений -- Напоминания о просроченных заявках -- Очистка старых данных - ---- - -## Обзор Проекта - -### Цель - -Создать систему управления внутренними закупками и инвентарем с автоматизированным workflow согласования заявок и контролем бюджетов отделов. - -### Основные Возможности - -**Для сотрудников:** - -- ✅ Создание заявок на закупку -- ✅ Отслеживание статуса своих заявок -- ✅ Просмотр доступного оборудования - -**Для руководителей отделов:** - -- ✅ Согласование/отклонение заявок отдела -- ✅ Просмотр бюджета отдела -- ✅ Управление инвентарем отдела - -**Для финансового отдела:** - -- ✅ Контроль бюджетов всех отделов -- ✅ Финансовое согласование заявок -- ✅ Формирование отчетов - -**Для менеджера закупок:** - -- ✅ Исполнение одобренных заявок -- ✅ Работа с поставщиками -- ✅ Оприходование закупленного оборудования - -**Для завхоза:** - -- ✅ Учет всего оборудования -- ✅ Назначение ответственных -- ✅ Контроль технического обслуживания -- ✅ Списание оборудования - -### Метрики Успеха - -| Метрика | Целевое Значение | -| ------------------------------------------------------------- | ------------------------------- | -| Среднее время согласования | < 3 рабочих дня | -| Процент заявок без доработок | > 90% | -| Точность бюджетирования | 95-105% | -| Удовлетворенность пользователей | > 4.0/5.0 | -| Критических багов в production | 0 | - ---- - -## Архитектура и Модели - -### Диаграмма Связей - -``` -┌─────────────────┐ ┌──────────────────┐ -│ User │ │ Department │ -│ (встроенный) │◄──────┤ (employees) │ -└────────┬────────┘ └────────┬─────────┘ - │ │ - │ requestor │ department - │ │ - ▼ ▼ -┌─────────────────────────────────────────────┐ -│ ProcurementRequest │ -│ ─────────────────────────────────────────── │ -│ - title: str │ -│ - description: text │ -│ - status: FSMField (DRAFT→PENDING→APPROVED) │ -│ - urgency: choice │ -│ - estimated_cost: decimal │ -│ - actual_cost: decimal (nullable) │ -│ - created_at, updated_at, completed_at │ -└────────┬────────────────────┬───────────────┘ - │ │ - │ request │ request - ▼ ▼ -┌──────────────────┐ ┌─────────────────┐ -│ ProcurementItem │ │ Approval │ -│ ──────────────── │ │ ─────────────── │ -│ - name │ │ - approver │ -│ - quantity │ │ - role │ -│ - unit │ │ - status │ -│ - unit_price │ │ - comment │ -│ - supplier_info │ │ - created_at │ -└────────┬─────────┘ └─────────────────┘ - │ - │ procurement_item (nullable) - ▼ -┌─────────────────────────────────────────────┐ -│ Equipment │ -│ ─────────────────────────────────────────── │ -│ - name: str │ -│ - inventory_number: str (unique) │ -│ - serial_number: str │ -│ - category: FK │ -│ - status: choice (Available/In_Use/...) │ -│ - department: FK │ -│ - responsible_person: FK (nullable) │ -│ - purchase_date: date │ -│ - warranty_until: date (nullable) │ -│ - purchase_cost: decimal │ -└────────┬────────────────────────────────────┘ - │ - │ equipment - ▼ -┌──────────────────────┐ -│ MaintenanceRecord │ -│ ──────────────────── │ -│ - date: date │ -│ - type: choice │ -│ - description: text │ -│ - cost: decimal │ -│ - performed_by: FK │ -└──────────────────────┘ - -┌──────────────────────┐ ┌─────────────────┐ -│ EquipmentCategory │ │ Budget │ -│ ──────────────────── │ │ ─────────────── │ -│ - name: str │ │ - department │ -│ - parent: FK (self) │ │ - year: int │ -│ - description: text │ │ - quarter: int │ -└──────────────────────┘ │ - allocated: $ │ - │ - spent: $ │ - └─────────────────┘ - -┌──────────────────────┐ -│ Supplier │ -│ ──────────────────── │ -│ - name: str │ -│ - contact_person │ -│ - phone, email │ -│ - address: text │ -│ - rating: decimal │ -└──────────────────────┘ -``` - -### Модель 1: ProcurementRequest (Заявка на закупку) - -**Поля:** - -```python -title = CharField(max_length=255) # Название заявки -description = TextField() # Описание/обоснование -department = ForeignKey(Department) # Отдел-заявитель -requestor = ForeignKey(User) # Автор заявки -status = FSMField(default='draft') # Статус (state machine) -urgency = CharField(choices=UrgencyLevel) # Срочность (low/medium/high/critical) -estimated_cost = DecimalField() # Предполагаемая стоимость -actual_cost = DecimalField(null=True) # Фактическая (после закупки) -created_at = DateTimeField(auto_now_add=True) -updated_at = DateTimeField(auto_now=True) -completed_at = DateTimeField(null=True) # Когда завершена -``` - -**Статусы:** - -- `DRAFT` - Черновик (редактируется) -- `PENDING` - На согласовании -- `APPROVED` - Одобрено (все согласования пройдены) -- `REJECTED` - Отклонено -- `IN_PROGRESS` - В работе (закупка выполняется) -- `COMPLETED` - Завершено (товар получен) -- `CANCELLED` - Отменено заявителем - -**Методы:** - -- `submit()` - отправить на согласование (DRAFT → PENDING) -- `approve(approver)` - согласовать (PENDING → APPROVED если все ОК) -- `reject(approver, reason)` - отклонить (PENDING → REJECTED) -- `start_execution()` - начать закупку (APPROVED → IN_PROGRESS) -- `complete(actual_cost)` - завершить (IN_PROGRESS → COMPLETED) -- `cancel()` - отменить (любой → CANCELLED) -- `get_required_approvals()` - кто должен согласовать (зависит от суммы) -- `check_budget_available()` - есть ли бюджет в отделе - -**Permissions:** - -- `procurement.add_procurementrequest` - создавать заявки -- `procurement.change_procurementrequest` - редактировать свои -- `procurement.view_procurementrequest` - просматривать -- `procurement.approve_procurementrequest` - согласовывать (custom) -- `procurement.view_all_requests` - видеть заявки всех отделов (custom) - -### Модель 2: ProcurementItem (Позиция заявки) - -**Поля:** - -```python -request = ForeignKey(ProcurementRequest, related_name='items') -name = CharField(max_length=255) # Название товара/услуги -description = TextField(blank=True) # Подробное описание -quantity = PositiveIntegerField(default=1) # Количество -unit = CharField(max_length=50) # Единица измерения (шт, кг, л) -estimated_unit_price = DecimalField() # Цена за единицу -supplier_info = TextField(blank=True) # Инфо о поставщике/ссылки -equipment = ForeignKey(Equipment, null=True) # Связь с оборудованием после закупки -created_at = DateTimeField(auto_now_add=True) -``` - -**Свойства:** - -- `total_price` - quantity × estimated_unit_price - -### Модель 3: Approval (Согласование) - -**Поля:** - -```python -request = ForeignKey(ProcurementRequest, related_name='approvals') -approver = ForeignKey(User) # Кто согласовывает -role = CharField(choices=ApprovalRole) # Роль согласующего -status = CharField(choices=ApprovalStatus) # approved/rejected/pending -comment = TextField(blank=True) # Комментарий -created_at = DateTimeField(auto_now_add=True) -updated_at = DateTimeField(auto_now=True) -``` - -**Роли согласующих:** - -- `DEPARTMENT_HEAD` - Руководитель отдела -- `FINANCE_MANAGER` - Финансовый менеджер -- `DIRECTOR` - Генеральный директор - -**Статусы:** - -- `PENDING` - Ожидает решения -- `APPROVED` - Одобрено -- `REJECTED` - Отклонено - -**Логика согласования:** - -```python -if estimated_cost < 10_000: - # Только руководитель отдела - required = [DEPARTMENT_HEAD] -elif estimated_cost < 100_000: - # Руководитель + финансовый менеджер - required = [DEPARTMENT_HEAD, FINANCE_MANAGER] -else: - # Все три уровня - required = [DEPARTMENT_HEAD, FINANCE_MANAGER, DIRECTOR] -``` - -### Модель 4: Equipment (Оборудование) - -**Поля:** - -```python -name = CharField(max_length=255) # Название -inventory_number = CharField(unique=True) # Инвентарный номер (INV-2024-0001) -serial_number = CharField(blank=True) # Серийный номер производителя -category = ForeignKey(EquipmentCategory) # Категория -status = CharField(choices=EquipmentStatus) # Статус -department = ForeignKey(Department) # Отдел -responsible_person = ForeignKey(User, null=True) # Ответственный -location = CharField(max_length=255) # Расположение (кабинет, стеллаж) -purchase_date = DateField() # Дата покупки -warranty_until = DateField(null=True) # Гарантия до -purchase_cost = DecimalField() # Стоимость покупки -procurement_item = ForeignKey(ProcurementItem, null=True) # Из какой заявки -notes = TextField(blank=True) # Примечания -created_at = DateTimeField(auto_now_add=True) -updated_at = DateTimeField(auto_now=True) -``` - -**Статусы:** - -- `AVAILABLE` - Доступно (на складе) -- `IN_USE` - В использовании -- `MAINTENANCE` - На обслуживании -- `REPAIR` - В ремонте -- `RETIRED` - Списано -- `LOST` - Утеряно - -**Методы:** - -- `assign_to(user, location)` - назначить ответственного -- `send_to_maintenance(description)` - отправить на ТО -- `retire(reason)` - списать -- `generate_qr_code()` - генерировать QR-код с инв. номером - -### Модель 5: EquipmentCategory (Категория оборудования) - -**Поля:** - -```python -name = CharField(max_length=100, unique=True) -parent = ForeignKey('self', null=True) # Родительская категория -description = TextField(blank=True) -icon = CharField(max_length=50, blank=True) # Bootstrap icon класс -created_at = DateTimeField(auto_now_add=True) -``` - -**Примеры иерархии:** - -``` -- Компьютерная техника - ├── Ноутбуки - ├── Настольные ПК - ├── Мониторы - └── Периферия - ├── Клавиатуры - ├── Мыши - └── Веб-камеры -- Офисная мебель - ├── Столы - ├── Кресла - └── Шкафы -- Оргтехника - ├── Принтеры - ├── Сканеры - └── МФУ -``` - -### Модель 6: MaintenanceRecord (Запись обслуживания) - -**Поля:** - -```python -equipment = ForeignKey(Equipment, related_name='maintenance_history') -date = DateField() # Дата обслуживания -type = CharField(choices=MaintenanceType) # Тип обслуживания -description = TextField() # Что сделано -cost = DecimalField(default=0) # Стоимость (если платно) -performed_by = ForeignKey(User) # Кто выполнил -next_maintenance_date = DateField(null=True) # Следующее ТО -created_at = DateTimeField(auto_now_add=True) -``` - -**Типы обслуживания:** - -- `INSPECTION` - Осмотр/проверка -- `MAINTENANCE` - Плановое ТО -- `REPAIR` - Ремонт -- `UPGRADE` - Модернизация -- `CLEANING` - Чистка - -### Модель 7: Budget (Бюджет отдела) - -**Поля:** - -```python -department = ForeignKey(Department) -year = PositiveIntegerField() # Год -quarter = PositiveIntegerField(choices=[(1,1),(2,2),(3,3),(4,4)]) -allocated_amount = DecimalField() # Выделено -spent_amount = DecimalField(default=0) # Потрачено -created_at = DateTimeField(auto_now_add=True) -updated_at = DateTimeField(auto_now=True) - -class Meta: - unique_together = [('department', 'year', 'quarter')] -``` - -**Методы:** - -- `remaining_amount` - остаток бюджета -- `utilization_percentage` - процент использования -- `can_spend(amount)` - можно ли потратить сумму -- `reserve_amount(amount, request)` - зарезервировать под заявку -- `commit_spending(amount, request)` - зафиксировать расход - -### Модель 8: Supplier (Поставщик) - -**Поля:** - -```python -name = CharField(max_length=255) -contact_person = CharField(max_length=255, blank=True) -phone = CharField(max_length=50, blank=True) -email = EmailField(blank=True) -address = TextField(blank=True) -website = URLField(blank=True) -inn = CharField(max_length=12, blank=True) # ИНН -rating = DecimalField(default=0) # Оценка (0-5) -is_active = BooleanField(default=True) -notes = TextField(blank=True) -created_at = DateTimeField(auto_now_add=True) -``` - ---- - -## Права Доступа - -### Django Model Permissions - -**Автоматические (создаются Django):** - -- `procurement.add_procurementrequest` -- `procurement.change_procurementrequest` -- `procurement.delete_procurementrequest` -- `procurement.view_procurementrequest` -- `procurement.add_equipment` -- `procurement.change_equipment` -- `procurement.delete_equipment` -- `procurement.view_equipment` - -**Кастомные (добавляем вручную):** - -```python -class ProcurementRequest(models.Model): - class Meta: - permissions = [ - ('approve_procurementrequest', 'Can approve procurement requests'), - ('view_all_requests', 'Can view all department requests'), - ('manage_budget', 'Can manage department budgets'), - ('execute_procurement', 'Can execute approved requests'), - ] -``` - -### Department Permissions (добавить в employees.constants.DeptPerm) - -```python -class DeptPerm: - # ... существующие права ... - - # Новые права для Procurement - CREATE_PROCUREMENT = "create_procurement_request" - APPROVE_PROCUREMENT = "approve_procurement_request" - MANAGE_EQUIPMENT = "manage_department_equipment" - VIEW_BUDGET = "view_department_budget" -``` - -### Матрица Прав Доступа - -| Действие | Сотрудник | Рук. Отдела | Финансы | Закупки | Завхоз | Admin | -| ------------------------------------------------------ | ------------------ | -------------------- | -------------- | -------------- | ------------ | ----- | -| Создать заявку | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Просмотреть свои заявки | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Просмотреть заявки отдела | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | -| Просмотреть все заявки | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | -| Согласовать (1 уровень) | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | -| Согласовать (финансы) | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | -| Согласовать (директор) | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | -| Исполнить заявку | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | -| Просмотр оборудования | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Добавить оборудование | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | -| Управление инвентарем отдела | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | -| Управление всем инвентарем | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | -| Списание оборудования | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | -| Просмотр бюджета отдела | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | -| Управление бюджетами | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | - -### Permission Classes для API - -```python -# procurement/permissions.py - -class IsRequestorOrDeptHead(BasePermission): - """Автор заявки или руководитель его отдела""" - def has_object_permission(self, request, view, obj): - if request.user == obj.requestor: - return True - return user_is_dept_head(request.user, obj.department) - -class CanApproveProcurement(BasePermission): - """Может согласовывать заявки (зависит от суммы и роли)""" - def has_object_permission(self, request, view, obj): - if request.user.is_staff: - return True - # Логика проверки роли и суммы - required_approvals = obj.get_required_approvals() - # ... проверка текущей роли пользователя - -class CanManageEquipment(BasePermission): - """Может управлять оборудованием""" - def has_permission(self, request, view): - return ( - request.user.is_staff or - request.user.has_perm('procurement.manage_equipment') - ) -``` - ---- - -## Workflow Согласования - -### Схема Переходов Статусов - -```mermaid -stateDiagram-v2 - [*] --> DRAFT - DRAFT --> PENDING: submit() - DRAFT --> CANCELLED: cancel() - - PENDING --> APPROVED: approve() - PENDING --> REJECTED: reject() - PENDING --> CANCELLED: cancel() - - APPROVED --> IN_PROGRESS: start_execution() - APPROVED --> CANCELLED: cancel() - - IN_PROGRESS --> COMPLETED: complete() - IN_PROGRESS --> CANCELLED: cancel() - - COMPLETED --> [*] - REJECTED --> [*] - CANCELLED --> [*] -``` - -### Правила Согласования по Сумме - -| Сумма заявки | Необходимые согласования | Примерный срок | -| ----------------------- | ------------------------------------------------------------------------- | --------------------------- | -| < 10,000₽ | Руководитель отдела | 1-2 дня | -| 10,000 - 100,000₽ | Руководитель отдела + Финансы | 2-3 дня | -| > 100,000₽ | Руководитель отдела + Финансы + Директор | 3-5 дней | - -### Уведомления - -**При создании заявки:** - -- ✉️ Email руководителю отдела: "Новая заявка требует вашего согласования" - -**При согласовании:** - -- ✉️ Email следующему согласующему (если требуется) -- ✉️ Email автору заявки: "Ваша заявка согласована [Роль]" - -**При полном согласовании:** - -- ✉️ Email менеджеру закупок: "Заявка одобрена, можно приступать к исполнению" -- ✉️ Email автору: "Ваша заявка полностью одобрена и передана в работу" - -**При отклонении:** - -- ✉️ Email автору: "Ваша заявка отклонена. Причина: ..." - -**При завершении:** - -- ✉️ Email автору: "Ваша заявка выполнена" -- ✉️ Email руководителю отдела: "Заявка выполнена, оборудование оприходовано" - -### Автоматические Проверки - -**Перед отправкой на согласование (submit):** - -- ✅ Есть хотя бы одна позиция в заявке -- ✅ Указана предполагаемая стоимость -- ✅ Заполнены обязательные поля - -**Перед финансовым согласованием:** - -- ✅ В бюджете отдела достаточно средств -- ✅ Если бюджета нет → алерт финансовому директору - -**Перед завершением:** - -- ✅ Указана фактическая стоимость -- ✅ Все позиции оприходованы (если оборудование) - ---- - -## API Endpoints - -### Базовый URL - -``` -/api/v1/procurement/ -``` - -### Заявки (Requests) - -#### Список и создание - -```http -GET /api/v1/procurement/requests/ -``` - -**Query Parameters:** - -- `status` - фильтр по статусу (draft, pending, approved, etc.) -- `department` - ID отдела -- `requestor` - ID заявителя -- `urgency` - уровень срочности -- `created_at__gte` - с даты (ISO format) -- `created_at__lte` - до даты -- `min_cost` - минимальная стоимость -- `max_cost` - максимальная стоимость -- `search` - полнотекстовый поиск (title, description) -- `ordering` - сортировка (created_at, estimated_cost, -created_at) - -**Response:** - -```json -{ - "count": 42, - "next": "http://api/v1/procurement/requests/?page=2", - "previous": null, - "results": [ - { - "id": 1, - "title": "Ноутбуки для разработчиков", - "description": "Требуется 3 ноутбука MacBook Pro", - "department": { - "id": 5, - "name": "IT отдел" - }, - "requestor": { - "id": 10, - "full_name": "Иванов Иван Иванович" - }, - "status": "pending", - "urgency": "medium", - "estimated_cost": "750000.00", - "actual_cost": null, - "items_count": 3, - "created_at": "2025-12-01T10:00:00Z", - "updated_at": "2025-12-03T15:30:00Z", - "completed_at": null, - "pending_approvals": [ - { - "role": "finance_manager", - "approver": null - } - ] - } - ] -} -``` - -```http -POST /api/v1/procurement/requests/ -``` - -**Request Body:** - -```json -{ - "title": "Новые мониторы", - "description": "Необходимо заменить устаревшие мониторы", - "department": 5, - "urgency": "medium", - "items": [ - { - "name": "Монитор Dell UltraSharp 27\"", - "description": "4K, IPS, USB-C", - "quantity": 5, - "unit": "шт", - "estimated_unit_price": "45000.00", - "supplier_info": "https://dell.com/..." - } - ] -} -``` - -#### Детали, обновление, удаление - -```http -GET /api/v1/procurement/requests/{id}/ -PATCH /api/v1/procurement/requests/{id}/ -DELETE /api/v1/procurement/requests/{id}/ -``` - -#### Actions (Действия над заявкой) - -**Отправить на согласование:** - -```http -POST /api/v1/procurement/requests/{id}/submit/ -``` - -**Согласовать:** - -```http -POST /api/v1/procurement/requests/{id}/approve/ -``` - -```json -{ - "comment": "Одобрено. Необходимо для работы." -} -``` - -**Отклонить:** - -```http -POST /api/v1/procurement/requests/{id}/reject/ -``` - -```json -{ - "reason": "Недостаточно бюджета в этом квартале", - "comment": "Предлагаю перенести на следующий квартал" -} -``` - -**Начать исполнение:** - -```http -POST /api/v1/procurement/requests/{id}/start_execution/ -``` - -**Завершить:** - -```http -POST /api/v1/procurement/requests/{id}/complete/ -``` - -```json -{ - "actual_cost": "740000.00", - "completion_notes": "Все товары получены и оприходованы" -} -``` - -**Отменить:** - -```http -POST /api/v1/procurement/requests/{id}/cancel/ -``` - -```json -{ - "reason": "Больше не актуально" -} -``` - -**Добавить позицию:** - -```http -POST /api/v1/procurement/requests/{id}/add_item/ -``` - -```json -{ - "name": "Клавиатура механическая", - "quantity": 3, - "unit": "шт", - "estimated_unit_price": "8000.00" -} -``` - -### Оборудование (Equipment) - -#### Список и создание - -```http -GET /api/v1/procurement/equipment/ -POST /api/v1/procurement/equipment/ -``` - -**Query Parameters:** - -- `status` - статус оборудования -- `category` - ID категории -- `department` - ID отдела -- `responsible_person` - ID ответственного -- `search` - поиск по названию, инв. номеру, серийному номеру -- `warranty_expires_soon` - булев, гарантия истекает < 30 дней -- `ordering` - сортировка - -**Response:** - -```json -{ - "count": 156, - "results": [ - { - "id": 1, - "name": "MacBook Pro M3 16\"", - "inventory_number": "INV-2024-0142", - "serial_number": "C02XK0AHJG5H", - "category": { - "id": 1, - "name": "Ноутбуки", - "parent_name": "Компьютерная техника" - }, - "status": "in_use", - "department": { - "id": 5, - "name": "IT отдел" - }, - "responsible_person": { - "id": 10, - "full_name": "Иванов Иван" - }, - "location": "Офис 3.14, Рабочее место №5", - "purchase_date": "2024-03-15", - "warranty_until": "2027-03-15", - "purchase_cost": "250000.00", - "qr_code_url": "/media/qr_codes/INV-2024-0142.png" - } - ] -} -``` - -#### Actions - -**Назначить ответственного:** - -```http -POST /api/v1/procurement/equipment/{id}/assign/ -``` - -```json -{ - "responsible_person": 15, - "location": "Офис 2.10" -} -``` - -**Добавить запись обслуживания:** - -```http -POST /api/v1/procurement/equipment/{id}/maintenance/ -``` - -```json -{ - "date": "2025-12-08", - "type": "inspection", - "description": "Плановая проверка, все в норме", - "cost": "0.00" -} -``` - -**Списать:** - -```http -POST /api/v1/procurement/equipment/{id}/retire/ -``` - -```json -{ - "reason": "Устарело морально и физически", - "date": "2025-12-08" -} -``` - -**История:** - -```http -GET /api/v1/procurement/equipment/{id}/history/ -``` - -```json -{ - "equipment": {...}, - "maintenance_records": [...], - "assignments": [ - { - "date": "2024-03-15", - "person": "Иванов И.И.", - "location": "Офис 3.14" - } - ] -} -``` - -### Бюджеты (Budgets) - -```http -GET /api/v1/procurement/budgets/ -GET /api/v1/procurement/budgets/my_department/ -POST /api/v1/procurement/budgets/ -PATCH /api/v1/procurement/budgets/{id}/ -``` - -**Response (my_department):** - -```json -{ - "department": { - "id": 5, - "name": "IT отдел" - }, - "year": 2025, - "quarter": 4, - "allocated_amount": "2000000.00", - "spent_amount": "845000.00", - "remaining_amount": "1155000.00", - "utilization_percentage": 42.25, - "reserved_amount": "150000.00", - "pending_requests": [ - { - "id": 15, - "title": "Серверное оборудование", - "estimated_cost": "150000.00", - "status": "pending" - } - ] -} -``` - -### Статистика (Stats) - -```http -GET /api/v1/procurement/stats/overview/ -``` - -```json -{ - "total_requests": 124, - "pending_requests": 15, - "approved_this_month": 8, - "completed_this_month": 12, - "total_spent_this_year": "12500000.00", - "by_status": { - "draft": 5, - "pending": 15, - "approved": 3, - "in_progress": 8, - "completed": 89, - "rejected": 4 - }, - "by_urgency": { - "low": 30, - "medium": 70, - "high": 20, - "critical": 4 - } -} -``` - -```http -GET /api/v1/procurement/stats/by_department/ -``` - -```json -[ - { - "department": {"id": 5, "name": "IT отдел"}, - "total_requests": 42, - "total_spent": "3200000.00", - "budget_utilization": 85.5 - } -] -``` - ---- - -## UI/UX Компоненты - -### 1. Дашборд (`/procurement/`) - -**Элементы:** - -- 📊 Карточки со статистикой (всего заявок, ожидают согласования, одобрено, потрачено) -- 📈 График расходов по месяцам (Chart.js) -- 🔍 Быстрые фильтры (статус, отдел, период) -- 📋 Список последних заявок -- 🎯 Кнопка "Создать заявку" (если есть права) - -**Макет:** - -``` -┌─────────────────────────────────────────────────────────┐ -│ 📊 Дашборд Закупок [+ Создать] │ -├─────────────────────────────────────────────────────────┤ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │ -│ │ Всего │ │ Ожидают │ │ Одобрено │ │ Потрачено │ │ -│ │ 24 │ │ 8 │ │ 5 │ │ 450,000₽ │ │ -│ └──────────┘ └──────────┘ └──────────┘ └─────────────┘ │ -│ │ -│ 📈 [График расходов - Chart.js line chart] │ -│ │ -│ 🔍 [Все статусы ▼] [Мой отдел ▼] [Этот месяц ▼] │ -│ │ -│ 📋 Последние заявки │ -│ ┌──────────────────────────────────────────────────────┐│ -│ │ #142 Ноутбуки для разработки | 85,000₽ | Ожидает ││ -│ │ [Детали] [Согласовать] [Отклонить] ││ -│ └──────────────────────────────────────────────────────┘│ -└─────────────────────────────────────────────────────────┘ -``` - -### 2. Форма Создания Заявки (`/procurement/requests/create/`) - -**Шаги:** - -1. Основная информация (название, описание, срочность) -2. Добавление позиций (таблица с возможностью добавления строк) -3. Проверка и отправка - -**JavaScript функционал:** - -- Динамическое добавление/удаление позиций -- Автоматический расчет общей суммы -- Проверка остатка бюджета (AJAX запрос) -- Валидация перед отправкой - -### 3. Детальная Страница Заявки (`/procurement/requests/{id}/`) - -**Секции:** - -- Заголовок с бейджем статуса -- Основная информация (автор, отдел, даты) -- Таблица позиций с итоговой суммой -- Timeline согласований (кто, когда, решение) -- Кнопки действий (согласовать/отклонить/отменить) - зависит от прав -- Комментарии/история изменений - -### 4. Каталог Оборудования (`/procurement/equipment/`) - -**Элементы:** - -- Карточки оборудования с фото (если есть) -- Фильтры: статус, категория, отдел, ответственный -- Поиск по названию/инв. номеру -- Сортировка (дата покупки, стоимость, название) -- Массовые действия (экспорт, печать QR-кодов) - -### 5. Карточка Оборудования (`/procurement/equipment/{id}/`) - -**Секции:** - -- QR-код (для сканирования) -- Основная информация -- Текущий статус и местоположение -- Ответственный -- История обслуживания (таблица) -- История перемещений -- Кнопки действий - -### 6. Управление Бюджетом (`/procurement/budgets/`) - -**Элементы:** - -- Таблица бюджетов по отделам -- Визуализация использования (progress bars) -- Алерты при превышении 90% -- Форма добавления/редактирования бюджета -- График использования по кварталам - ---- - -## Этапы Реализации - -### ✅ Этап 1: Базовая Структура (1-2 недели) - -**Цель:** Создать приложение и определить все модели без сложной логики - -**Задачи:** - -- [X] Создать Django app `procurement` -- [ ] Определить все модели в `models.py` - - [ ] ProcurementRequest - - [ ] ProcurementItem - - [ ] Approval - - [ ] Equipment - - [ ] EquipmentCategory - - [ ] MaintenanceRecord - - [ ] Budget - - [ ] Supplier -- [ ] Создать файл `constants.py` с choices -- [ ] Создать миграции и применить -- [ ] Настроить `admin.py` для всех моделей -- [ ] Добавить `__str__` методы -- [ ] Написать базовые тесты моделей - -**Результат:** Все модели созданы, можно добавлять данные через админку - -### 🔄 Этап 2: API и Права (1 неделя) - -**Цель:** Создать REST API со всеми CRUD операциями и правами доступа - -**Задачи:** - -- [ ] Создать `api/v1/procurement/serializers.py` - - [ ] ProcurementRequestSerializer (read/write versions) - - [ ] EquipmentSerializer - - [ ] BudgetSerializer - - [ ] Остальные сериализаторы -- [ ] Создать `api/v1/procurement/views.py` - - [ ] ProcurementRequestViewSet (базовый CRUD) - - [ ] EquipmentViewSet - - [ ] BudgetViewSet - - [ ] Остальные ViewSets -- [ ] Создать `api/v1/procurement/permissions.py` - - [ ] IsRequestorOrDeptHead - - [ ] CanApproveProcurement - - [ ] CanManageEquipment -- [ ] Добавить департаментные права в `employees.constants.DeptPerm` -- [ ] Настроить URL routing -- [ ] Написать API тесты (pytest) - -**Результат:** API работает, можно создавать/редактировать через Postman - -### 🔄 Этап 3: Workflow Согласования (1-2 недели) - -**Цель:** Реализовать state machine и автоматическую логику согласования - -**Задачи:** - -- [ ] Установить `django-fsm` -- [ ] Добавить FSM transitions в ProcurementRequest: - - [ ] `submit()` - отправка на согласование - - [ ] `approve()` - согласование - - [ ] `reject()` - отклонение - - [ ] `complete()` - завершение - - [ ] `cancel()` - отмена -- [ ] Реализовать `get_required_approvals()` - логика по сумме -- [ ] Создать actions в ViewSet: - - [ ] `/requests/{id}/submit/` - - [ ] `/requests/{id}/approve/` - - [ ] `/requests/{id}/reject/` - - [ ] `/requests/{id}/complete/` - - [ ] `/requests/{id}/cancel/` -- [ ] Добавить проверки budget при согласовании -- [X] Интеграция с NotificationService: - - [X] Создание типов уведомлений (procurement_*) - - [X] Signals для автоматических уведомлений - - [X] Web + Email + Telegram каналы -- [ ] Написать тесты workflow - -**Результат:** Заявки проходят полный цикл согласования с автоматическими проверками и уведомлениями - -### 🔄 Этап 4: Бюджеты и Контроль (1 неделя) - -**Цель:** Контроль бюджетов и автоматические алерты - -**Задачи:** - -- [ ] Реализовать методы модели Budget: - - [ ] `remaining_amount` - - [ ] `utilization_percentage` - - [ ] `can_spend(amount)` - - [ ] `reserve_amount(request)` - - [ ] `commit_spending(request)` -- [ ] Добавить проверку бюджета перед финансовым согласованием -- [ ] Создать алерты при превышении 90% бюджета -- [ ] Реализовать quarterly/yearly отчеты -- [ ] Добавить endpoint для статистики по бюджетам -- [ ] Написать тесты для бюджетной логики - -**Результат:** Система контролирует бюджеты и не дает перерасход - -### 🔄 Этап 5: Инвентарь (1 неделя) - -**Цель:** Полноценный учет оборудования с QR-кодами - -**Задачи:** - -- [ ] Реализовать генерацию inventory_number (INV-YYYY-NNNN) -- [ ] Добавить методы Equipment: - - [ ] `assign_to(user, location)` - - [ ] `send_to_maintenance()` - - [ ] `retire(reason)` - - [ ] `generate_qr_code()` - python-barcode/qrcode -- [ ] Создать actions в EquipmentViewSet: - - [ ] `/equipment/{id}/assign/` - - [ ] `/equipment/{id}/maintenance/` - - [ ] `/equipment/{id}/retire/` - - [ ] `/equipment/{id}/history/` -- [ ] Реализовать иерархию категорий (recursive) -- [ ] Добавить фильтры (warranty_expires_soon, etc.) -- [ ] Написать тесты - -**Результат:** Полный учет оборудования с историей и QR-кодами - -### 🔄 Этап 6: Frontend (2-3 недели) - -**Цель:** Создать все UI компоненты - -**Подэтап 6.1: Дашборд и Списки** - -- [ ] Создать `templates/procurement/dashboard.html` -- [ ] Создать `static/js/procurement/dashboard.js` (загрузка stats) -- [ ] Интегрировать Chart.js для графиков -- [ ] Создать `templates/procurement/request_list.html` -- [ ] Создать `static/js/procurement/requestList.js` (фильтры, пагинация) - -**Подэтап 6.2: Форма Создания Заявки** - -- [ ] Создать `templates/procurement/request_create.html` -- [ ] Создать `static/js/procurement/requestForm.js`: - - [ ] Динамическое добавление позиций - - [ ] Расчет общей суммы - - [ ] AJAX проверка бюджета - - [ ] Валидация перед отправкой - -**Подэтап 6.3: Детальная Страница Заявки** - -- [ ] Создать `templates/procurement/request_detail.html` -- [ ] Создать `static/js/procurement/approvalFlow.js`: - - [ ] Кнопки approve/reject с модальными окнами - - [ ] Timeline согласований - - [ ] Real-time обновления (WebSocket) - -**Подэтап 6.4: Инвентарь** - -- [ ] Создать `templates/procurement/equipment_list.html` -- [ ] Создать `templates/procurement/equipment_detail.html` -- [ ] Создать `static/js/procurement/equipmentList.js` -- [ ] Добавить QR code scanner (jsQR или quagga.js) - -**Подэтап 6.5: Мобильная Адаптация** - -- [ ] Адаптировать все страницы под мобильные -- [ ] Тестировать на разных разрешениях - -**Результат:** Полноценный UI со всеми функциями - -### 🔄 Этап 7: Интеграции (1 неделя) - -**Цель:** Интеграция с другими модулями и внешними системами - -**Задачи:** - -- [ ] Email уведомления (django.core.mail): - - [ ] При создании заявки - - [ ] При согласовании/отклонении - - [ ] При завершении -- [ ] WebSocket для real-time обновлений (channels): - - [ ] Обновление статуса заявки - - [ ] Новые уведомления -- [ ] Экспорт отчетов: - - [ ] PDF (reportlab/WeasyPrint) - - [ ] Excel (openpyxl/xlsxwriter) -- [ ] Интеграция с модулем сотрудников: - - [ ] Использование Position для прав - - [ ] Department head автоопределение -- [X] Интеграция с системой уведомлений (NotificationService): - - [X] Web уведомления через WebSocket - - [X] Email уведомления с HTML шаблонами - - [X] Telegram уведомления через Bot API - - [X] Настройки пользователя для каналов доставки -- [ ] Периодические задачи (APScheduler): - - [ ] Напоминания о просроченных заявках - - [ ] Дайджесты уведомлений - - [ ] Очистка старых данных - -**Результат:** Полная интеграция со всеми системами - -### 🔄 Этап 8: Тестирование и Документация (1 неделя) - -**Цель:** Достичь высокого покрытия тестами и создать документацию - -**Задачи:** - -- [ ] Unit тесты (coverage > 80%): - - [ ] Модели - - [ ] Сериализаторы - - [ ] ViewSets - - [ ] Permissions - - [ ] Workflow -- [ ] Integration тесты: - - [ ] Полный цикл создания и согласования заявки - - [ ] Работа с бюджетами - - [ ] Управление оборудованием -- [ ] E2E тесты (Playwright/Selenium): - - [ ] Создание заявки через UI - - [ ] Согласование заявки - - [ ] Добавление оборудования -- [ ] API документация (drf-spectacular): - - [ ] OpenAPI schema - - [ ] Swagger UI -- [ ] Пользовательская документация: - - [ ] Инструкция для сотрудников - - [ ] Инструкция для руководителей - - [ ] Инструкция для завхоза - -**Результат:** Стабильное приложение с полной документацией - ---- - -## Технологический Стек - -### Backend - -| Компонент | Технология | Версия | Назначение | -| ------------------ | --------------------- | ------------ | ---------------------------------------------------------- | -| Framework | Django | 5.2+ | Основной фреймворк | -| API | Django REST Framework | 3.14+ | REST API | -| Database | PostgreSQL | 14+ | Основная БД | -| State Machine | django-fsm | 2.8+ | Workflow статусов | -| Notifications | NotificationService | - | Встроенная система уведомлений | -| Scheduler | APScheduler | 3.11+ | Периодические задачи | -| Message Broker | Redis | 7+ | Cache + Channels | -| WebSocket | Django Channels | 4+ | Real-time updates | -| Export | reportlab | 4+ | PDF генерация | -| Barcodes | python-barcode | 0.15+ | Штрихкоды | -| QR Codes | qrcode | 7.4+ | QR-коды | - -### Frontend - -| Компонент | Технология | Версия | Назначение | -| ------------------ | -------------------- | ------------ | --------------------------- | -| Framework | Vanilla JS | ES6+ | Логика | -| CSS Framework | Bootstrap | 5.3+ | Стили | -| Charts | Chart.js | 4+ | Графики | -| QR Scanner | jsQR | 1.4+ | Сканирование QR | -| Icons | Bootstrap Icons | 1.11+ | Иконки | -| HTTP Client | Fetch API | - | AJAX запросы | - -### DevOps - -| Компонент | Технология | Назначение | -| ------------------ | ---------------------- | -------------------------------------------- | -| Testing | pytest + pytest-django | Unit/Integration тесты | -| E2E Testing | Playwright | End-to-end тесты | -| Coverage | coverage.py | Покрытие кода | -| Code Quality | flake8, black | Линтинг, форматирование | -| CI/CD | GitHub Actions | Автоматизация | - ---- - -## Чеклист Реализации - -### Подготовка - -- [X] Создать ветку `feature/procurement-module` -- [X] Настроить окружение (установить зависимости) -- [X] Создать документ с требованиями - -### Этап 1: Модели ✅ ЗАВЕРШЕН - -- [X] Создать app: `python manage.py startapp procurement` -- [X] Добавить в `INSTALLED_APPS` -- [X] Создать `procurement/constants.py` -- [X] Создать модели: - - [X] ProcurementRequest (с FSM) - - [X] ProcurementItem - - [X] Approval - - [X] Equipment - - [X] EquipmentCategory - - [X] MaintenanceRecord - - [X] Budget - - [X] Supplier -- [X] Создать миграции: `python manage.py makemigrations` -- [X] Применить: `python manage.py migrate` -- [X] Настроить admin.py для всех моделей -- [X] Создать тестовые данные через админку -- [X] Написать тесты моделей (20/20 PASSED) -- [X] Коммит: "feat: add procurement app with basic models" - -### Этап 2: API ✅ ЗАВЕРШЕН (95%) - -- [X] Создать `procurement/` структуру (без api/v1) -- [X] Написать сериализаторы (9 сериализаторов) -- [X] Создать ViewSets (7 ViewSets) -- [X] Настроить URL routing -- [X] Создать permission classes (9 классов) -- [X] Добавить департаментные права -- [X] Написать API тесты (12/12 PASSED) -- [X] Протестировать endpoints (50+ endpoints) -- [X] Коммит: "feat: add procurement API endpoints" -- [ ] ⚠️ Исправить: баг items_count (property vs annotate) - ИСПРАВЛЕН - -### Этап 3: Workflow ✅ ЗАВЕРШЕН (100%) - -- [X] Установить django-fsm: `pip install django-fsm==3.0.1` -- [X] Добавить FSM в ProcurementRequest -- [X] Реализовать transitions (submit, approve, reject) -- [X] Создать actions в API (@action decorators) -- [X] Добавить проверки бюджета (check_budget_available) -- [X] Настроить signals (post_save для уведомлений) -- [X] Интеграция с NotificationService (вместо Celery) -- [X] Написать workflow тесты (9/9 PASSED) -- [X] Коммит: "feat: add procurement approval workflow" -- [X] Исправить queryset для approvers (41/41 тестов PASSED) - -### Этап 4: Бюджеты ✅ ЗАВЕРШЕН (100%) - -- [X] Реализовать методы Budget (reserved_amount, available_amount) -- [X] Добавить проверки (is_staff/headed_departments) -- [X] Создать алерты (_check_budget_alert при < 20% и < 10%) -- [X] Добавить endpoints статистики (/stats/overview/, /stats/by-department/) -- [X] Добавить my-department endpoint -- [X] Написать тесты (11/11 PASSED) -- [X] Коммит: "feat: add budget control and alerts" - -### Этап 5: Инвентарь - -- [X] Установить barcode библиотеки (qrcode[pil]) -- [X] Реализовать генерацию номеров (InventoryNumberGenerator) -- [X] Добавить EquipmentTransferLog модель -- [X] Создать actions в API (transfer, write_off, add_maintenance, qr_code, transfer_history) -- [X] Реализовать QR-коды (QRCodeGenerator) -- [X] Написать тесты (14/14 PASSED) -- [ ] Коммит: "feat: add equipment management with QR codes" - -### Этап 6: Frontend - -- [ ] Создать templates структуру -- [ ] Создать static файлы структуру -- [ ] Дашборд -- [ ] Форма создания -- [ ] Детальная страница -- [ ] Каталог оборудования -- [ ] Мобильная адаптация -- [ ] Коммит: "feat: add procurement UI components" - -### Этап 7: Интеграции - -- [X] Интеграция с NotificationService (система уведомлений) -- [ ] WebSocket обновления для real-time статусов -- [ ] Экспорт отчетов (PDF/Excel) -- [ ] APScheduler для периодических задач -- [ ] Коммит: "feat: add integrations and notifications" - -### Этап 8: Тесты и Документация - -- [ ] Довести coverage до 80%+ -- [ ] E2E тесты -- [ ] API документация -- [ ] Пользовательская документация -- [ ] Коммит: "docs: add comprehensive documentation" - -### Финализация - -- [ ] Code review -- [ ] Исправить замечания -- [ ] Merge в master -- [ ] Deploy на production -- [ ] Провести обучение пользователей - ---- - -## Риски и Митигация - -| Риск | Вероятность | Влияние | Митигация | -| --------------------------------------------------------------------------- | ---------------------- | -------------- | ------------------------------------------------------------------------------------------ | -| Сложность workflow | Высокая | Высокое | Начать с простого (2 уровня), расширять постепенно | -| Производительность при большой нагрузке | Средняя | Среднее | Индексы БД, кэширование, пагинация | -| Конфликты бюджетов | Средняя | Высокое | Pessimistic locking, транзакции | -| Миграция данных | Низкая | Среднее | Хорошо протестировать скрипты миграции | -| Недостаточное покрытие тестами | Средняя | Высокое | CI/CD с проверкой coverage, code review | -| Сопротивление пользователей | Средняя | Среднее | Обучение, простой UI, постепенное внедрение | - ---- - -## 🎯 Результаты Тестирования - -**Последнее тестирование:** 9 декабря 2025 -**Статус:** ✅ ВСЕ ТЕСТЫ ПРОХОДЯТ - -### Итоговая статистика: - -``` -✅ 66/66 PASSED (100%) - ├─ 20/20 Model tests ✅ - ├─ 12/12 API tests ✅ - ├─ 9/9 Workflow tests ✅ - ├─ 11/11 Budget tests ✅ - └─ 14/14 Inventory tests ✅ (NEW) -``` - -### Основные исправления: - -1. ✅ Исправлен `led_departments` → `headed_departments` -2. ✅ Удален конфликт `items_count` (annotate vs @property) -3. ✅ Создан `settings_test.py` с InMemoryChannelLayer -4. ✅ Добавлены поля в сериализаторы (purchase_cost, requestor) -5. ✅ Исправлен Finance Manager lookup (groups + permissions) -6. ✅ Обновлены фикстуры бюджета (2025 Q4) -7. ✅ **Расширен queryset для approvers** - ключевое исправление! - -### Техническая инфраструктура: - -- **Git cleanup:** localcerts_backup.zip удален из истории (279 коммитов) -- **Test settings:** InMemoryChannelLayer вместо Redis -- **Notifications:** NotificationService интегрирован (вместо Celery) - ---- - -## Следующие Шаги - -**Завершено:** - -1. ✅ Создать данную документацию -2. ✅ Создать ветку `feature/procurement-module` -3. ✅ Завершить **Этап 1: Базовая Структура** (100%) -4. ✅ Завершить **Этап 2: API и Права** (100%) -5. ✅ Завершить **Этап 3: Workflow** (100%) -6. ✅ Завершить **Этап 4: Бюджеты** (100%) - -**Следующий шаг:** - -- ⏭️ Начать **Этап 5: Инвентарь** - управление оборудованием с QR-кодами - -**Через 1 месяц:** - -- Завершить Этапы 5-6 (Инвентарь, Frontend) -- Иметь полнофункциональный модуль с UI - -**Через 2 месяца:** - -- Завершить все 8 этапов -- Готовый модуль к production deploy - ---- - -## Контакты и Вопросы - -**Ответственный за разработку:** [Ваше имя] -**Дата последнего обновления:** 9 декабря 2025 -**Версия документа:** 1.3 - -**Вопросы и предложения:** - -- Создавайте issues в репозитории -- Обсуждайте в команде - ---- - -**Статус:** 🎉 Этапы 1-5 завершены! Готовы к Этапу 6 (Frontend)! diff --git a/PROCUREMENT_TEST_COMPLETION_REPORT.md b/PROCUREMENT_TEST_COMPLETION_REPORT.md deleted file mode 100644 index 4a5da690..00000000 --- a/PROCUREMENT_TEST_COMPLETION_REPORT.md +++ /dev/null @@ -1,229 +0,0 @@ -# 🎉 Отчёт о Завершении Тестирования Модуля Procurement - -**Дата:** 9 декабря 2025 -**Сессия:** Исправление тестовых ошибок и завершение Этапа 3 -**Статус:** ✅ **УСПЕШНО ЗАВЕРШЕНО** - ---- - -## 📊 Итоговая Статистика - -### Результаты тестирования: -``` -✅ 41/41 PASSED (100%) - ├─ 20/20 Model tests ✅ - ├─ 12/12 API tests ✅ - └─ 9/9 Workflow tests ✅ -``` - -### Прогресс по этапам: -- ✅ **Этап 1: Базовая Структура** - 100% (20/20 тестов) -- ✅ **Этап 2: API и Права** - 100% (12/12 тестов) -- ✅ **Этап 3: Workflow** - 100% (9/9 тестов) - ---- - -## 🔧 Исправленные Проблемы - -### 1. **AttributeError: led_departments** -- **Проблема:** Неправильный related_name в `permissions.py` -- **Решение:** `led_departments` → `headed_departments` -- **Файл:** `backend/procurement/permissions.py:175` - -### 2. **Конфликт items_count** -- **Проблема:** Конфликт между `.annotate()` и `@property` -- **Решение:** Удалён `.annotate(items_count=Count('items'))` -- **Файл:** `backend/procurement/views.py:48-50` - -### 3. **Redis ConnectionError** -- **Проблема:** Тесты пытались подключиться к Redis:6379 -- **Решение:** Создан `settings_test.py` с `InMemoryChannelLayer` -- **Файлы:** - - `backend/eusrr_backend/settings_test.py` (новый) - - `backend/tests/procurement/conftest.py` (новый) - - `backend/pytest.ini` (обновлён) - -### 4. **IntegrityError: purchase_cost** -- **Проблема:** Отсутствие поля в сериализаторе -- **Решение:** Добавлено `purchase_cost` в `EquipmentListSerializer.Meta.fields` -- **Файл:** `backend/procurement/serializers.py:334` - -### 5. **KeyError: requestor** -- **Проблема:** Отсутствие поля в ответе API -- **Решение:** Добавлены поля в `ProcurementRequestCreateSerializer`: id, requestor, status, created_at -- **Файл:** `backend/procurement/serializers.py` - -### 6. **Неправильный статус код 200 vs 403** -- **Проблема:** Тест ожидал 200, но должен быть 403 для обычного пользователя -- **Решение:** Изменено ожидание на 403 в `test_list_budgets_regular_user` -- **Файл:** `backend/tests/procurement/test_api.py` - -### 7. **Некорректный год бюджета** -- **Проблема:** Budget создавался на 2024 Q1, но текущая дата 2025 Q4 -- **Решение:** Обновлены фикстуры бюджета на `year=2025, quarter=4` -- **Файл:** `backend/tests/procurement/test_workflow.py:107` - -### 8. **Finance Manager not found** -- **Проблема:** Поиск только по группам, не учитывались user_permissions -- **Решение:** Добавлен Q-фильтр для проверки как групп, так и прав пользователя -- **Файл:** `backend/procurement/views.py:145-154` - -### 9. **IntegrityError: estimated_cost** -- **Проблема:** Отсутствие обязательного поля в тестовых фикстурах -- **Решение:** Добавлено `estimated_cost` во все 8 вызовов `ProcurementRequest.objects.create()` -- **Файл:** `backend/tests/procurement/test_workflow.py` - -### 10. **404 на /approve/ endpoint** ⭐ КРИТИЧЕСКАЯ ПРОБЛЕМА -- **Проблема:** Finance user не мог видеть заявки, где является approver -- **Причина:** Queryset фильтровал только по requestor и department, но не по approvals -- **Решение:** Расширен `get_queryset()` с добавлением `Q(approvals__approver=user)` и `.distinct()` -- **Файлы:** - - `backend/procurement/views.py:81-94` (get_queryset) - - `backend/tests/procurement/test_workflow.py:420-425` (ожидание 404 как валидного кода) - ---- - -## 🛠️ Техническая Инфраструктура - -### Git History Cleanup -```bash -Команда: git filter-branch --tree-filter 'rm -f localcerts_backup.zip' -Результат: 279 commits обработано -Размер репозитория: ~54MB после cleanup -Очистка: git reflog expire + git gc --prune=now --aggressive -``` - -### Тестовое Окружение -- **settings_test.py:** Специальные настройки для тестов -- **InMemoryChannelLayer:** Замена Redis для тестов (без внешних зависимостей) -- **conftest.py:** Pytest fixtures с mock для channel_layer - -### NotificationService Integration -- **Решение:** Использование встроенного NotificationService вместо Celery -- **Обоснование:** Уже протестирован, поддерживает 3 канала, достаточен для масштаба 50-200 сотрудников -- **Преимущества:** Проще развертывание, меньше точек отказа, легче debugging - ---- - -## 📝 Созданные Коммиты - -``` -83115b3 docs: Update implementation plan - Stages 1-3 complete (100%) -466ca71 fix: Extend ProcurementRequest queryset to include approvers -7c6087c docs(procurement): update implementation plan with Stage 1-3 completion -52dbb05 fix(procurement): fix workflow tests - add estimated_cost and budget year -1afd103 fix(procurement): resolve test failures -``` - ---- - -## 🎯 Ключевые Уроки - -### 1. **Queryset Filtering vs Permissions** -- 404 = объект не в queryset (до проверки permissions) -- 403 = объект есть, но доступ запрещён (после permissions) -- Важно учитывать ВСЕ сценарии доступа при фильтрации queryset - -### 2. **Отладка Multi-Step Tests** -- Debug output критичен для понимания последовательных операций -- Важно логировать промежуточные состояния объектов -- Использование `print()` помогает выявить точку отказа - -### 3. **Test Infrastructure** -- Mock внешних зависимостей (Redis → InMemoryChannelLayer) -- Изолированные настройки для тестов (settings_test.py) -- Fixtures для общих моков (conftest.py) - -### 4. **@property vs .annotate()** -- Конфликт возникает при одинаковых именах -- Решение: использовать либо property, либо annotate, не оба -- Для queryset: annotate; для instance: property - ---- - -## 🚀 Следующие Шаги - -### Этап 4: Бюджеты (следующий) -- [ ] Реализовать методы Budget (remaining_amount, utilization_percentage, can_spend) -- [ ] Добавить проверки превышения бюджета -- [ ] Создать алерты для руководителей -- [ ] Добавить endpoints статистики бюджета -- [ ] Написать тесты для бюджетной логики -- [ ] Коммит: "feat: add budget control and alerts" - -### Этап 5: Инвентарь -- [ ] Установить barcode библиотеки -- [ ] Реализовать генерацию инвентарных номеров -- [ ] Добавить методы Equipment (is_under_warranty) -- [ ] Создать actions в API (transfer, maintenance, write_off) -- [ ] Реализовать QR-коды для оборудования -- [ ] Написать тесты инвентаризации - -### Этап 6: Frontend -- [ ] Создать templates структуру -- [ ] Создать static файлы (CSS/JS) -- [ ] Реализовать формы создания заявок -- [ ] Добавить HTMX интерактивность -- [ ] Интегрировать с WebSocket для real-time updates - ---- - -## 📈 Метрики Разработки - -### Время выполнения: -- **Начало сессии:** Анализ 15 failing tests -- **Конец сессии:** 41/41 PASSED (100%) -- **Длительность:** ~3-4 часа систематической отладки - -### Изменённые файлы: -- `backend/procurement/views.py` (queryset, finance manager lookup) -- `backend/procurement/permissions.py` (related_name fix) -- `backend/procurement/serializers.py` (поля в сериализаторах) -- `backend/tests/procurement/test_api.py` (исправление ожиданий) -- `backend/tests/procurement/test_workflow.py` (фикстуры, debug, ожидания) -- `backend/eusrr_backend/settings_test.py` (новый файл) -- `backend/tests/procurement/conftest.py` (новый файл) -- `backend/pytest.ini` (DJANGO_SETTINGS_MODULE) -- `PROCUREMENT_IMPLEMENTATION_PLAN.md` (обновление статуса) - -### Строки кода: -- **Добавлено:** ~200 строк (test infrastructure + fixes) -- **Изменено:** ~50 строк (bug fixes) -- **Удалено:** ~30 строк (debug output, конфликты) - ---- - -## ✅ Чек-лист Завершения - -- [x] Все 41 тест проходят -- [x] Нет pending/skipped тестов -- [x] Debug код удалён -- [x] Документация обновлена -- [x] Коммиты созданы с понятными сообщениями -- [x] Git история очищена (localcerts_backup.zip) -- [x] Test infrastructure настроен (InMemoryChannelLayer) -- [x] NotificationService интегрирован -- [x] Queryset security проверен (approvers filter) -- [x] Итоговый отчёт создан - ---- - -## 🎉 Заключение - -**Этапы 1-3 модуля Procurement успешно завершены!** - -Модуль имеет: -- ✅ 8 моделей с полными связями -- ✅ 50+ API endpoints с правами доступа -- ✅ FSM workflow с многоуровневым согласованием -- ✅ Интеграция с NotificationService (Web/Email/Telegram) -- ✅ 41 тест с 100% покрытием основного функционала -- ✅ Продуманная безопасность (queryset filtering) - -**Готов к продолжению разработки (Этап 4: Бюджеты).** - ---- - -**Автор отчёта:** GitHub Copilot -**Дата создания:** 9 декабря 2025 -**Версия:** 1.0 diff --git a/REQUESTS_APP_STRUCTURE.md b/REQUESTS_APP_STRUCTURE.md deleted file mode 100644 index e69de29b..00000000