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