Files
public/directive_layer/TECHNICAL.md

393 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Directive Layer (Слой директив)
**name**: directive_layer
**version**: 19.0.1.0.0
**author**: Mk.Lab
**depends:** `base`, `web`, `base_user_role`, `project`
## Архитектура
```
directive_layer
├── directive.directive — основная сущность: директива
├── directive.stage — справочник стадий (7 стадий)
├── directive.work.type — справочник видов работ
├── directive.template — шаблон директивы
├── directive.template.group — группа шаблонов (пакет директив)
├── directive.template.group.line — строки группы
├── directive.origin — связка «модель + ID»
├── directive.policy — правило автогенерации директив
├── directive.mixin — AbstractModel для подключения к любой модели
├── res.users (расширение) — авто-установка домашней страницы исполнителя
└── ExecutorBoard (OWL) — рабочий стол исполнителя (fullscreen)
```
---
## Модели
### `directive.directive` — Директива
**Файл:** `models/directive.py`
| Поле | Тип | Описание |
|---|---|---|
| `name` | Char (required) | Название директивы |
| `active` | Boolean | Признак активности |
| `is_simulation` | Boolean | Флаг тестовой/демо-директивы |
| `deadline_at` | Datetime (required) | Дедлайн исполнения |
| `planned_hours` | Float (required) | Трудоёмкость в часах |
| `load_percent` | Integer (default: 10) | Процент загрузки ресурса |
| `executor_type` | Selection | `human` / `machine` / `software` |
| `success_probability` | Float (default: 98.0) | Вероятность успешного выполнения, % |
| `resource_cost` | Float | Стоимость ресурса |
| `retry_policy` | Selection | `none` / `retry_3` / `backoff` |
| `priority` | Selection | `0` — низкий / `1` — нормальный / `2` — высокий |
| `work_type_id` | Many2one → `directive.work.type` (required) | Тип работ |
| `executor_role_id` | Many2one → `res.users.role` | Роль исполнителя |
| `requester_role_id` | Many2one → `res.users.role` | Роль заявителя |
| `stage_id` | Many2one → `directive.stage` | Стадия (вид диспетчера) |
| `stage_code` | Char (related, stored) | Технический код текущей стадии |
| `executor_stage_id` | Many2one → `directive.stage` | Стадия (вид исполнителя) |
| `requester_stage_id` | Many2one → `directive.stage` | Стадия (вид заявителя) |
| `executor_traffic_light` | Selection | Светофор исполнителя: `white/green/yellow/red/black` |
| `resource_traffic_light` | Selection | Светофор ресурса: `white/green/yellow/red/black` |
| `executor_user_id` | Many2one → `res.users` | Назначенный исполнитель |
| `origin_id` | Many2one → `directive.origin` (required, cascade) | Объект-источник |
| `template_id` | Many2one → `directive.template` | Шаблон директивы |
| `policy_id` | Many2one → `directive.policy` | Политика создания |
| `generator_key` | Char (index) | Ключ дедупликации для режима `create_once` |
| `predecessor_ids` | Many2many → `directive.directive` | Директивы-предшественники |
| `sequence` | Integer | Порядок в планировщике |
**Вычисляемые поля:**
| Поле | Описание |
|---|---|
| `deadline_display` | Дедлайн в формате `дд.мм.гг Ч:ММ` с учётом часового пояса пользователя |
| `time_left_display` | Оставшееся время в формате `Xд Yч Zм` (отрицательное, если просрочено) |
| `is_overdue` | Boolean: дедлайн прошёл |
| `priority_stars` | Строка ★ / ★★ / ★★★ |
**Порядок сортировки по умолчанию:** `deadline_at asc, priority desc, id desc`
#### Статусы
Допустимые переходы между стадиями:
| Текущая стадия | Допустимые следующие |
|---|---|
| `draft` | `confirmed`, `planned`, `cancelled` |
| `confirmed` | `planned`, `in_progress`, `cancelled` |
| `planned` | `in_progress`, `cancelled` |
| `in_progress` | `paused`, `done` |
| `paused` | `in_progress`, `cancelled` |
| `done` | — (финальная) |
| `cancelled` | — (финальная) |
Переходы контролируются методом `_check_stage_transition()`. Недопустимый переход вызывает `UserError`.
#### Методы
| Метод | Описание |
|---|---|
| `action_assign_to_me()` | Назначить на текущего пользователя (из стадий `confirmed` / `planned`) |
| `action_start()` | Перевести в `in_progress` |
| `action_pause()` | Перевести в `paused` (только из `in_progress`) |
| `action_done()` | Завершить директиву и вызвать callback в записи-источнике |
| `action_cancel()` | Отменить (обновляет все три stage-поля одновременно) |
| `action_open_delete_wizard()` | Открыть визард удаления |
| `_set_stage(code)` | Установить стадию по коду (обновляет `stage_id`, `executor_stage_id`, `requester_stage_id`) |
| `_get_origin_record()` | Получить запись-источник через `origin_id.res_model` + `origin_id.res_id` |
| `_build_payload(event)` | Сформировать словарь данных для передачи в callback |
| `action_quick_change_stage(id, code)` | `@api.model` — смена стадии по вызову из JS |
#### Механизм callback при завершении
При вызове `action_done()` выполняется следующая последовательность:
1. Стадия устанавливается в `done` через `_set_stage("done")`
2. Из шаблона директивы читается `handler_key` — имя метода в модели-источнике
3. Через `_get_origin_record()` получается запись-источник
4. Метод `handler_key` вызывается на записи-источнике с payload
**Состав payload:**
`event`, `directive_id`, `directive_name`, `template_id`, `template_code`, `work_type_id`, `executor_user_id`, `planned_start_at`, `planned_end_at`, `stage_code`, `priority`, `load_percent`
---
### `directive.stage` — Стадии
**Файл:** `models/directive_stage.py`
| Поле | Тип | Описание |
|---|---|---|
| `name` | Char (required) | Название стадии |
| `code` | Char (required, unique) | Технический код |
| `is_hidden` | Boolean | Скрыть колонку в канбане |
**Предустановленные стадии** (`data/directive_stage_data.xml`, `noupdate="1"`):
| Код | Название | Скрыта в канбане |
|---|---|---|
| `draft` | Черновик | Нет |
| `confirmed` | Подтверждена | Нет |
| `planned` | Запланирована | Нет |
| `in_progress` | В работе | Нет |
| `paused` | На паузе | Нет |
| `done` | Выполнена | Да |
| `cancelled` | Отменена | Да |
---
### `directive.template` — Шаблон директивы
**Файл:** `models/directive_template.py`
| Поле | Тип | Описание |
|---|---|---|
| `name` | Char (required) | Название шаблона |
| `code` | Char (required, unique) | Технический код |
| `work_type_id` | Many2one → `directive.work.type` | Тип работ |
| `planned_hours` | Float (required) | Трудоёмкость в часах |
| `load_percent` | Integer | Процент загрузки ресурса |
| `priority` | Selection 0/1/2 | Приоритет директив по этому шаблону |
| `executor_role_id` | Many2one → `res.users.role` | Роль исполнителя по умолчанию |
| `requester_role_id` | Many2one → `res.users.role` | Роль заявителя по умолчанию |
| `default_stage_id` | Many2one → `directive.stage` | Стартовая стадия (по умолчанию: `draft`) |
| `handler_key` | Char | Имя метода в модели-источнике, вызываемого при завершении директивы |
| `description` | Text | Описание и инструкции для исполнителя |
---
### `directive.template.group` — Группа шаблонов
**Файл:** `models/directive_template_group.py`
| Поле | Тип | Описание |
|---|---|---|
| `name` | Char (required) | Название группы |
| `code` | Char | Технический код |
| `applicable_model` | Char | Техническое имя модели, к которой применяется группа |
| `template_line_ids` | One2many → `directive.template.group.line` | Строки группы |
**`directive.template.group.line`:**
| Поле | Тип | Описание |
|---|---|---|
| `group_id` | Many2one → `directive.template.group` (cascade) | Родительская группа |
| `sequence` | Integer | Порядок создания директив из этой строки |
| `template_id` | Many2one → `directive.template` | Шаблон директивы |
---
### `directive.origin` — Источник директив
**Файл:** `models/directive_origin.py`
Хранит хранит пару `(res_model, res_id)` и агрегирует все директивы для данного объекта. На каждую запись создаётся ровно один `directive.origin`.
| Поле | Тип | Описание |
|---|---|---|
| `res_model` | Char (required, index) | Техническое имя модели Odoo |
| `res_id` | Integer (required, index) | ID записи |
| `res_ref` | Reference (computed, stored) | Прямая ссылка на запись |
| `directive_ids` | One2many → `directive.directive` | Все директивы источника |
| `display_name` | Char (computed, stored) | Отображаемое имя в формате `res_model,res_id` |
---
### `directive.policy` — Политика автогенерации
**Файл:** `models/directive_policy.py`
| Поле | Тип | Описание |
|---|---|---|
| `name` | Char (required) | Название политики |
| `active` | Boolean | Политика активна |
| `sequence` | Integer | Порядок проверки политик |
| `origin_model` | Char (required) | Модель, к которой применяется политика |
| `event` | Selection | Тип события: `manual` / `on_create` / `on_write` / `on_stage` / `on_cron` / `on_callback` |
| `mode` | Selection | `create_once` — однократно; `create_each_time` — при каждом срабатывании |
| `use_record_template_group` | Boolean | Брать группу шаблонов из поля записи, а не из фиксированной |
| `template_group_id` | Many2one → `directive.template.group` | Фиксированная группа шаблонов |
| `trigger_field` | Char | Имя поля (для `on_write`) или метода (для `on_callback`) |
| `trigger_value_int` | Integer | Ожидаемое значение поля (ID записи для Many2one-полей) |
#### Логика `_policy_should_fire()`
Для события `on_callback` — всегда возвращает `True`. Для остальных событий: читает значение `trigger_field` у записи и сравнивает с `trigger_value_int`. Для Many2one-полей сравнивается `.id`.
#### Логика `apply_to_records()`
Для каждой записи: в режиме `create_once` проверяет, нет ли уже директив от этой политики для данного `origin_id`. Если нет — определяет группу шаблонов и вызывает `_directive_create_from_group()`.
---
### `directive.mixin` — Миксин для моделей
**Файл:** `models/directive_mixin.py`
**Тип:** `models.AbstractModel`
Подключается к модели через `_inherit`:
```python
class ProjectTask(models.Model):
_name = 'project.task'
_inherit = ['project.task', 'directive.mixin']
```
#### Поля, добавляемые в модель
| Поле | Тип | Описание |
|---|---|---|
| `directive_origin_id` | Many2one → `directive.origin` (readonly) | Origin-запись данного объекта |
| `directive_ids` | One2many (related) | Все директивы объекта |
| `directive_execution_status` | Char (computed, stored) | Текстовый статус исполнения |
| `directive_template_group_id` | Many2one → `directive.template.group` | Группа шаблонов объекта |
#### Методы
**`_ensure_directive_origin()`** — находит или создаёт `directive.origin` для записи. Вызывается при `create` и `write`.
**`write()`** — перехватывает изменения полей. Фиксирует значения до изменения, выполняет `super().write()`, затем проверяет политики с `event='on_write'` и вызывает `apply_to_records()` при совпадении.
**`_directive_register_callback_wrappers()`** — вызывается при старте сервера (`_register_hook`). Оборачивает методы модели через `functools.wraps` для обработки политик с `event='on_callback'`. Флаг `_directive_wrapped` предотвращает повторное оборачивание.
**`_compute_directive_execution_status()`** — формирует строку статуса на основе наиболее приоритетной активной директивы. Приоритет стадий: `paused > in_progress > planned/confirmed > draft`. Результат: `"{stage_name}, до {nearest_deadline}, ожидается завершение к {expected_end}"`.
**`action_choose_directive_group()`** — открывает визард `directive.choose.group.wizard`.
**`action_open_directives()`** — открывает список директив, отфильтрованных по `origin_id` текущей записи.
**`_directive_create_from_group(template_group, generator_key, policy)`** — создаёт директивы по строкам группы в порядке `sequence`. Каждая директива получает параметры из шаблона, ссылку на `origin` и `policy`.
#### Методы для переопределения в модели-источнике
| Метод | Вызывается |
|---|---|
| `_directive_on_assign(self, payload)` | При назначении исполнителя |
| `_directive_on_start(self, payload)` | При переходе в `in_progress` |
| `_directive_on_pause(self, payload)` | При переходе в `paused` |
| `_directive_on_done(self, payload)` | При завершении директивы |
| `_directive_generation_spec(self)` | Возвращает список спецификаций для автогенерации (альтернатива Policy UI) |
---
### `res.users` — Расширение
**Файл:** `models/res_users.py`
При создании или изменении пользователя проверяет вхождение в группу `role_directive_executor_res_groups`. При совпадении устанавливает `action_id = action_executor_dashboard_client` — рабочий стол исполнителя становится домашней страницей пользователя.
---
## Визарды
### `directive.choose.group.wizard`
**Файл:** `wizard/directive_choose_group_wizard.py`
| Поле | Тип | Описание |
|---|---|---|
| `template_group_id` | Many2one → `directive.template.group` (required) | Выбранная группа шаблонов |
Метод `action_confirm()` берёт `active_model` и `active_id` из контекста и записывает выбранную группу в поле `directive_template_group_id` исходной записи. Сами директивы не создаются — только сохраняется группа.
### `directive.delete.wizard`
**Файл:** `wizard/directive_delete_wizard.py`
| Режим | Действие |
|---|---|
| `archive` | Мягкое удаление: `active=False` |
| `delete` | Жёсткое удаление: `unlink()` |
---
## Представления и меню
**Файл:** `views/directive_views.xml`, `views/directive_policy_views.xml`
### Структура меню
```
Директивы (menu_directive_root)
├── Все директивы (menu_directive_all)
│ → action_directive_all: directive.directive (kanban/list/form)
├── Политики (menu_directive_policy_action)
│ → action_directive_policy: directive.policy (list/form)
└── Настройки (menu_directive_settings_root)
├── Источники директив → directive.origin (list/form)
├── Статусы директив → directive.stage (list/form)
├── Виды работ → directive.work.type (list/form)
├── Шаблоны директив → directive.template (list/form)
└── Группы шаблонов директив → directive.template.group (list/form)
```
### Рабочий стол исполнителя
Зарегистрирован как `ir.actions.client` (`action_executor_dashboard_client`) с тегом `directive_layer.executor_dashboard_action`. Открывается в режиме `fullscreen`. Не является пунктом меню — устанавливается как `action_id` (домашняя страница) пользователям с ролью исполнителя через `res.users`.
---
## `ExecutorBoard` (OWL-компонент)
**Файлы:** `static/src/components/executor_board/executor_board.{js,xml,scss}`
Зарегистрирован как `ir.actions.client` с тегом `directive_layer.executor_dashboard_action`, открывается в режиме `fullscreen`.
**Логика загрузки данных:**
1. Определяет `userId` из `session.user_id` или `storeData`
2. Загружает все стадии (`directive.stage`)
3. Загружает директивы с фильтром `executor_user_id = userId`
4. Строит колонки канбана: стадия и директивы с совпадающим `executor_stage_id`
**Быстрые действия на карточке директивы:**
| Действие | Переход |
|---|---|
| Начать | `action_quick_change_stage(id, 'in_progress')` |
| Пауза | `action_quick_change_stage(id, 'paused')` |
| Завершить | `action_quick_change_stage(id, 'done')` |
После каждого действия данные перезагружаются.
---
## Безопасность
### Роли (`security/roles.xml`)
Каждая роль создаётся одновременно в двух системах:
- `res.users.role` (модуль `base_user_role`) — для назначения через интерфейс ролей
- `res.groups` — для использования в record rules и `ir.model.access`
| Роль | Назначение |
|---|---|
| `role_directive_executor` | Исполнитель: видит свои директивы, берёт в работу, завершает |
| `role_directive_requester` | Заявитель: создаёт и отслеживает директивы |
| `role_directive_dispatcher` | Диспетчер: назначает исполнителей, управляет очередью |
### Record Rules (`security/directives_rules.xml`)
Ограничивают видимость директив по ролям пользователя.
### Права доступа (`security/ir.model.access.csv`)
CRUD-права для всех моделей модуля, распределённые по ролям.
---
## Тесты
**Файл:** `tests/test_directive_layer.py`
| Что покрывает |
|---|
| Создание директив через шаблоны и группы |
| Статусная машина: допустимые и недопустимые переходы |
| Политики: срабатывание `on_write` |
| Миксин на модели `project.task` |