393 lines
21 KiB
Markdown
393 lines
21 KiB
Markdown
# 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` |
|