Дата: 2026-05-06. Статус: дизайн-документ, согласован с пользователем (4 развилки + модель длительностей). Контекст: этот документ — первичная истина для модуля PLANNER в части автоматического планирования ППР (ЗН по расписанию). Сидер и реализация Phase 6/7/8 должны соответствовать ему.
Связанные документы:
planner.md— общий обзор модуля, фазы 1-4 (закрыты), фазы 6-8 (план).cadre/spec.md— модуль CADRE, точка интеграции черезonPlannedWorkAssigneeAdding.framework-vision.md— манифест «один концепт = справочник».Ключевая идея: PLANNER — самостоятельный модуль (не надстройка над VAULT). Он использует справочники VAULT как входные данные (Договоры, Пакеты, ТСО, Шаблоны ППР, Чек-листы), но логика генерации, оперативного планирования и публикации — собственная.
| Сущность | Где живёт | Тип |
|---|---|---|
| Договор | VAULT | document |
| Пакет обслуживания | VAULT | directory |
| Физобъект | VAULT | directory |
| Элемент ТСО | VAULT | directory |
| Тип ТСО | VAULT | directory (hierarchical) |
| Фактор обслуживания | VAULT | directory |
| SLA | VAULT | directory |
| Операция (чек-листа) | VAULT | directory |
| Шаблон чек-листа | VAULT | directory |
Шаблон ППР (MaintenanceTemplate) |
VAULT | directory |
| PlannedWork (ЗН-черновик) | PLANNER (модель) | модуль |
| PlannedWorkAssignee | PLANNER (модель) | модуль |
| planGenerator (cron) | PLANNER (сервис) | модуль |
| DnD-планировщик (UI) | PLANNER (frontend) | модуль |
| Заказ-Наряд (опубликованный) | VAULT | document (создаётся на Phase 6 publish) |
Принцип разделения: всё что описывает «как устроен мир заказчика» (объекты, оборудование, регламенты, операции) — справочники. Всё что про процесс генерации/планирования/публикации — модуль.
paket_tso содержит две колонки:
mode (auto / manual_include / manual_exclude) и tso_id.
По умолчанию авто-фильтр («все ТСО на физобъектах пакета с типами, заявленными в шаблонах ППР»),
но юзер может вручную добавлять/исключать конкретные ТСО.parent (Камера → Камера на высоте → Камера на мачте).
Применяется COMPUTED-логика: атрибут наследуется, если на уровне child не переопределён.При генерации ЗН для каждой операции на конкретном ТСО система резолвит длительность по приоритету:
1. Override (Операция × Тип ТСО):
operation.duration_by_type[tso.type_id].duration_minutes
2. Default операции:
operation.duration_minutes_default
Если ни override, ни default не задан — validation error при сохранении операции.
duration_minutes_per_tso(template, tso) =
Σ resolve_duration(item.operation, tso.type_id)
for item in template.checklist_template.items
if item.uslovie_aktivacii == null OR
item.uslovie_aktivacii ∈ tso.faktory
+ Σ resolve_duration(avto_op.operation, tso.type_id)
for faktor in tso.faktory
for avto_op in faktor.avto_operations
duration_minutes_total(template, tso_list) =
Σ duration_minutes_per_tso(template, tso) for tso in tso_list
Авто-операции считаются per-instance ТСО (без дедупликации). Это сознательное упрощение MVP: безопаснее переоценить, чем недооценить. Дедупликация по location_group — задача v2.
duration_hours на ППР-шаблонеВ скрине пользователя поле было «обязательное, число». В модели оставляем два поля:
| Поле | Источник | Назначение |
|---|---|---|
duration_hours_normative |
ручной ввод | то, что зафиксировано в договоре с заказчиком (биллинг, отчёты PRISM) |
duration_hours_calculated |
COMPUTED (на лету при чтении) | системный расчёт по операциям/факторам, для bin-packing 24ч-лимита |
UI шаблона ППР показывает оба + индикатор расхождения.
function generatePlannedWorks():
horizon = now() + 30 days
for template in PPR_TEMPLATE.where(active=true):
dates = recurrenceService.computeOccurrences(template.recurrence_rule, horizon)
for date in dates:
if PlannedWork.exists(template_id=template.id, scheduled_at=date):
continue # idempotent
tso_list = collectTSO(template)
groups = groupBy(tso_list, t => t.physobject_id)
for physobject_id, tso_group in groups:
total_minutes = computeTotalDuration(template, tso_group)
if total_minutes <= 24 * 60:
createPlannedWork(template, date, physobject_id, tso_group)
else:
sub_groups = binPack(tso_group, max_minutes=24*60)
for sub in sub_groups:
createPlannedWork(template, date, physobject_id, sub)
function collectTSO(template):
paket = template.paket
physobjects = paket.paket_physobjects.map(_.physobject_id)
# avto-filtr
auto = TSO.where(physobject_id IN physobjects, tso_type_id = template.tso_type_id)
# primenit ruchnye pravki iz paket_tso
excluded = paket.paket_tso.where(mode='manual_exclude').map(_.tso_id)
included = paket.paket_tso.where(mode='manual_include').map(_.tso_id)
return (auto - excluded) ∪ TSO.where(id IN included, tso_type_id = template.tso_type_id)
function computeTotalDuration(template, tso_list):
total = 0
for tso in tso_list:
# operacii iz check-lista
for item in template.checklist_template.items:
if item.uslovie_aktivacii is null OR item.uslovie_aktivacii in tso.faktory:
total += resolveDuration(item.operation, tso.tso_type_id)
# avto-operacii ot faktorov (per-instance, bez deduplikacii)
for faktor in tso.faktory:
for avto in faktor.avto_operations:
total += resolveDuration(avto.operation, tso.tso_type_id)
return total
function resolveDuration(operation, tso_type_id):
override = operation.duration_by_type.find(_.tso_type_id == tso_type_id)
if override: return override.duration_minutes
return operation.duration_minutes_default
template_id × scheduled_at × physobject_id).Порядок операций в ЗН:
Это управляется полем sort_order в TABLE_PART авто-операций фактора + смысловыми категориями операций.
tso_type = Камера, чек-лист = «Протереть»duration_minutes_default = 30Камера на высоте (если иерархия) или просто default| Камера | Операция «Протереть» | Авто от фактора | Σ на 1 ТСО |
|---|---|---|---|
| #1-#7 (обычные) | 30 мин | — | 30 мин × 7 = 3ч 30мин |
| #8-#10 (на высоте) | 30 мин | 60 + 60 + 30 = 2ч 30мин | 3ч × 3 = 9ч |
| Σ на физобъекте | 12ч 30мин |
12ч 30мин ≤ 24ч → один ЗН на физобъект.
PlannedWork:
physobject_id = "ТЭЦ-14, главный корпус"template_id = "Ежеквартальная очистка камер"scheduled_at = <вычислено recurrenceService>instances = [10 элементов TSO с инлайн-чек-листами]assignees = [default_employee] (CADRE проверила — никаких отсутствий)state = "draft" (до bulk-publish)location_group. Если 3 камеры на высоте на одной мачте — подъём один раз. Требует поля location_group на ТСО + умной группировки в computeTotalDuration.recurrence_rule фактора, не ТСО. Сейчас не предусмотрено.duration_hours_normative (договор) vs duration_hours_actual (по подписанным чек-листам в CHRONOS). Отчёт в PRISM.| Концепт документа | Текущая модель PLANNER | Изменения |
|---|---|---|
| Шаблон ППР | PlanTemplate |
Переименовать в MaintenanceTemplate? Или оставить PlanTemplate как универсальный, а ППР-шаблоны хранить как справочник VAULT (тип directory)? |
| PlannedWork | PlannedWork |
Добавить physobject_id, tso_instances (JSON snapshot), template_id на VAULT-справочник |
| PlannedWorkAssignee | PlannedWorkAssignee |
Добавить conflicted BOOLEAN, conflictReason STRING (для CADRE C5) |
| recurrenceRule | уже есть | переиспользуем для MaintenanceTemplate.recurrence_rule |
| planGenerator | уже есть cron | расширить логикой группировки по физобъекту + bin-packing 24ч |
Главный архитектурный выбор для реализации Phase 6+:
MaintenanceTemplate — это справочник VAULT (directory), а не модель PLANNER. PLANNER читает его через NEXUS-провайдер.
Текущий PlanTemplate (модель в backend/modules/planner/models/) остаётся как универсальный шаблон планируемых работ для случаев, когда автоматическая генерация по справочнику не применима (разовые работы, аварийные работы и т.д.). ППР как декларативная конфигурация — переезжает в VAULT.
Это требует:
Шаблоны ППР (через сидер).PLANNER.maintenanceTemplate → справочник.planGenerator чтобы он читал шаблоны через nexus.resolve('PLANNER.maintenanceTemplate').getEntries().Заказ-Наряды (Phase 6, через NEXUS).Договор, Пакет обслуживания, Физобъект, Тип ТСО, Элемент ТСО, Фактор, Операция, Шаблон чек-листа, Шаблон ППР, SLA.PLANNER.workOrderDocument → справочник Заказ-Наряды.PLANNER.maintenanceTemplate → справочник Шаблоны ППР.publishBulk создавать запись в Заказ-Наряды через NEXUS, заполнять workOrderId.conflicted / conflictReason на PlannedWorkAssignee (inline-миграция).planGeneratorPlanTemplate модель).onPlannedWorkAssigneeAdding — звонок в CADRE.conflictDetector.js, эндпоинты /cadre/conflicts.ConflictsFeedView — реализация (сейчас заглушка).onPlannedWorkPublished, onPlannedWorkOverdue, onPlannedWorkCompleted.PlannerRoleConfigView (модель уже есть).Сидер находится в
backend/seeds/data/fsm-service-company.gdd(зашифрованный) +.json(расшифрованный, может быть устаревшим). Перед правкой —scripts/decrypt-seed.js(актуализировать.json).
| Справочник | kind | Иерархия | Демо-записей минимум | Зависимости |
|---|---|---|---|---|
| Договор | document | нет | 2 (на двух разных заказчиков) | Заказчик, Регион |
| Пакет обслуживания | directory | нет | 3 (СКУД на ТЭЦ-14, Видео на ТЭЦ-14, СКУД на офисе) | Договор, SLA |
| Физобъект | directory | нет | 4 (ТЭЦ-14 главный корпус, ТЭЦ-14 склад, офис Лужники, офис Тверская) | Заказчик, Регион |
| Тип ТСО | directory | да (folders+items) | 8: Камера → Камера на высоте, Камера обычная; СКУД → Турникет, Контроллер; ОПС → Извещатель | — |
| Элемент ТСО | directory | нет | 14 (10 камер на ТЭЦ-14 — 3 на высоте + 7 обычных, 2 турникета на ТЭЦ-14, 2 турникета в офисе) | Физобъект, Тип ТСО, Фактор |
| Фактор обслуживания | directory | нет | 4 («на высоте», «во взрывоопасной зоне», «уличная установка», «доступ ограничен») | Операция |
| SLA | directory | нет | 3 («8x5 standard», «24x7 critical», «Working hours response 4h») | — |
| Операция | directory | нет | 10 («Протереть», «Проверить разъёмы», «Проверить угол», «Оформить наряд-допуск», «Подъём с подготовкой», «Спуск», «Очистка от пыли», «Проверка крепежа», «Проверка изоляции», «Тест работоспособности») | Тип ТСО (для override) |
| Шаблон чек-листа | directory | нет | 3 («ТО камеры — очистка», «ТО камеры — проверка», «ТО турникета — квартальное») | Операция, Фактор |
Шаблон ППР (MaintenanceTemplate) |
directory | нет | 4 («Ежеквартальная очистка камер ТЭЦ-14», «Ежемесячная проверка камер ТЭЦ-14», «Ежеквартальное ТО турникетов ТЭЦ-14», «Ежеквартальное ТО турникетов офис») | Пакет, Тип ТСО, Чек-лист |
| Группа | Значения |
|---|---|
| Тип работ ППР | ppr (ППР), repair (Ремонт), inspection (Обследование), emergency (Аварийный) |
| Периодичность (для UI-выбора) | weekly, monthly, quarterly, semiannual, yearly |
| Статус ТСО | active, decommissioned, under_repair, planned_install |
| Mode состава ТСО в Пакете | auto, manual_include, manual_exclude |
Mode состава), tso (REFERENCE → ТСО, nullable когда mode=auto)Статус ТСО)Тип работ ППР)PlanTemplate.recurrence)Заказчики:
[Z1] АО ТЭЦ-14
[Z2] ООО Лужники-офис
Регионы:
[R1] Москва (есть в сидере)
Договоры:
[D1] Договор обслуживания 2026/01 (заказчик Z1, c 2026-01-01)
Пакеты:
[P1] ТО видеонаблюдения на ТЭЦ-14 (договор D1, SLA «8x5 standard»)
physobjects: [PH1, PH2]
tso_composition: mode=auto
Физобъекты:
[PH1] ТЭЦ-14 главный корпус (заказчик Z1, регион R1)
[PH2] ТЭЦ-14 склад (заказчик Z1, регион R1)
Типы ТСО:
[TT1] Камера (parent=null)
[TT2] Камера на высоте (parent=TT1)
[TT3] Камера обычная (parent=TT1)
Факторы:
[F1] На высоте
avto_operations: [OP_DOPUSK, OP_PODYOM, OP_SPUSK]
Операции:
[OP_PROTERET] Протереть
duration_minutes_default = 30
duration_by_type: (TT2: 30) ← переопределение для камеры на высоте
[OP_PROVERIT] Проверить разъёмы
duration_minutes_default = 10
[OP_DOPUSK] Оформить наряд-допуск (60 мин)
[OP_PODYOM] Подъём с подготовкой (60 мин)
[OP_SPUSK] Спуск (30 мин)
Элементы ТСО (на PH1):
[TSO1..TSO7] камеры обычные (tso_type=TT3, факторов нет)
[TSO8..TSO10] камеры на высоте (tso_type=TT2, factors=[F1])
Шаблоны чек-листов:
[CL1] ТО камеры — очистка
items: [{operation: OP_PROTERET, sort_order: 1}]
Шаблоны ППР:
[MT1] Ежеквартальная очистка камер ТЭЦ-14
paket=P1, type=ppr, tso_type=TT1 (Камера — общий тип, иерархия покроет TT2 и TT3)
checklist_template=CL1
recurrence_rule={type:'quarterly', dayOfMonth:15, timeOfDay:'09:00'}
duration_hours_normative=12.5
duration_hours_calculated=COMPUTED
Расчёт duration_hours_calculated для MT1:
Все справочники добавляются через seed.directory resolver — идемпотентно по code (там где есть) или по name + parent (там где нет code). Перезапуск сидера не должен создавать дубликаты.
MaintenanceTemplate создавать как VAULT-справочник, но не подключать его к planGenerator пока не закрыт Plan-C.PLANNER.maintenanceTemplate, PLANNER.workOrderDocument) — отдельный шаг Plan-B.PlannedWork, в VAULT не уезжают (Plan-B).Конец документа.