# 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` |