Перейти к содержимому

Согласование по цепочке - маршрут, замещение, сроки

Собираем многоэтапное согласование документа: маршрут по ролям, параллельные и последовательные этапы, замещение отсутствующих сотрудников и запись итога в документ.

Что нужно знать заранее

Маршрут описывают ролями, а не фамилиями конкретных сотрудников. Роль хранят полем документа или группой пользователей, иначе шаблон приходится править после каждого кадрового изменения.

Этап согласования бывает последовательным или параллельным по составу и порядку его участников. В первом случае документ идёт от одного согласующего к другому, во втором его видят все участники этапа сразу.

Отклонение на любом этапе останавливает маршрут целиком и возвращает документ автору. Дальше цепочка либо завершается отказом, либо уходит на доработку автору и стартует заново уже с исправленным документом.

Шаги

  1. Описать этапы маршрута и решить, какие из них идут параллельно, а какие подряд.
  2. Завести поля документа под согласующих каждого этапа, под итог и под историю подписей.
  3. Собрать в шаблоне цепочку утверждений с ветвлением по результату каждого отдельного этапа.
  4. Добавить срок на каждый этап и описать поведение процесса при истечении этого срока.
  5. Записать итог и историю согласования в поля документа сразу после завершения маршрута.

Решение

Запускаем маршрут с составом согласующих:

\Bitrix\Main\Loader::includeModule('bizproc');
CBPDocument::StartWorkflow($templateId, ['iblock', 'CIBlockDocument', $elementId], [
'Stage1' => ['user_' . $chiefId], // первый этап: руководитель
'Stage2' => array_map(static fn(int $id): string => 'user_' . $id, $financeIds),
], $errors);
// привязка к пользователю хранится строкой с префиксом, а не голым числом

Состав этапов передают параметрами шаблона, а не зашивают в схему. Тогда один шаблон обслуживает и заявку на отпуск, и договор, различая маршруты только переданными списками участников.

Определяем замещающего внутри процесса:

// действие «PHP-код» на входе в этап
CModule::IncludeModule('iblock');
$approver = (int) str_replace('user_', '', (string) $this->GetVariable('Approver'));
$deputy = (int) (CUser::GetByID($approver)->Fetch()['UF_DEPUTY'] ?? 0);
$this->SetVariable('Approver', 'user_' . ($deputy && isAbsent($approver) ? $deputy : $approver));
// заместителя хранят пользовательским полем сотрудника, а не таблицей в коде

Замещение решают до создания задачи, а не после. Задача, уже назначенная отсутствующему сотруднику, остаётся висеть на нём до ручного вмешательства администратора портала.

Считаем результат параллельного этапа:

// после действия утверждения: сколько участников ответили и как
$votes = (array) $this->GetVariable('Stage2Votes');
$approved = count(array_filter($votes));
$this->SetVariable('Stage2Result', $approved === count($votes) ? 'Y' : 'N');
// режим «нужны все» и режим «достаточно одного» различаются только этой проверкой

Продолжаем просроченный этап из агента:

CBPDocument::SendExternalEvent($workflowId, $activityName,
['USER_ID' => $approverId, 'APPROVE' => false, 'COMMENT' => 'срок истёк']);
// отказ по сроку лучше молчаливого зависания: маршрут доходит до автора документа

Автоматическое решение по сроку описывают в регламенте заранее. Тихое отклонение без уведомления автора порождает вопрос «где мой договор» ровно через неделю после запуска маршрута.

Записываем итог согласования в документ:

CIBlockElement::SetPropertyValuesEx($elementId, $iblockId, [
'APPROVE_RESULT' => $this->GetVariable('Stage2Result'),
'APPROVE_LOG' => date('d.m.Y H:i') . ': ' . $this->GetVariable('Approver'),
]);
// переменные процесса исчезнут после завершения, а свойства документа останутся

Смотрим, кто задерживает согласование:

$res = CBPTaskService::GetList(['MODIFIED' => 'ASC'], ['USER_ID' => $userId, 'STATUS' => 0],
false, false, ['ID', 'NAME', 'MODIFIED', 'WORKFLOW_ID']);
while ($row = $res->Fetch()) { printf("%s ждёт с %s\n", $row['NAME'], $row['MODIFIED']); }
// отчёт по незакрытым задачам показывает узкое место маршрута лучше жалоб в чате

Типичные проблемы

Маршрут встал, потому что согласующий в отпуске.

Роль этапа указана конкретным сотрудником, а замещения в схеме нет. Заместителя определяют в начале этапа и подменяют участника ещё до создания его задачи.

Документ согласован, хотя ответил только один из троих.

У параллельного этапа выбран режим «достаточно одного ответа» вместо режима общего согласия. Режим этапа сверяют с регламентом: для финансовых документов обычно нужны все подписи.

После правки шаблона старые заявки идут по прежнему маршруту.

Запущенные экземпляры доигрывают маршрут по копии схемы, снятой в момент старта процесса. Новый маршрут увидят только те заявки, которые запущены после сохранения шаблона в дизайнере.

История согласования пропала после завершения процесса.

Она хранилась в переменных процесса, а они живут только во время работы экземпляра. Итог и подписи записывают в свойства документа прямо по ходу движения маршрута.

Отклонённая заявка ушла на следующий этап.

Ветвление по результату этапа не проверяет отказ и продолжает цепочку дальше. После каждого утверждения ставят условие с явной и отдельно описанной веткой отклонения.

Частые вопросы

Параллельный этап или последовательный?

Последовательный, когда следующий согласующий смотрит на решение предыдущего. Параллельный экономит дни, но требует решить, нужны все ответы или достаточно одного.

Как учесть отпуска согласующих?

Хранить заместителя пользовательским полем сотрудника и подменять участника на входе в этап. Подмена после создания задачи уже не работает.

Что делать с просроченным этапом?

Задать срок и поведение по его истечении: автоматическое согласование, отказ или напоминание. Молчаливое ожидание останавливает маршрут навсегда.

Где хранить историю подписей?

В свойствах документа или отдельной таблице, а не в переменных процесса. Переменные исчезают вместе с экземпляром, и восстановить их нельзя.

Как перезапустить маршрут после доработки?

Запустить процесс над тем же документом заново, предварительно очистив поля итога. Старый экземпляр при этом должен быть завершён или остановлен.

Смежное

Первоисточник