Параллельное выполнение одинаковых блоков-заданий по визированию, если заранее не известно их количество и число ролей-исполнителей

8 9

Введение

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

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

Наиболее подходящим вариантом решения оказался - сделать программную отправку подзадач по другому ТМ (одна роль - 1 задача) и после завершения собрать результаты согласования.

Практически необходимо сделать пять шагов:

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

Итак, приступим.

Шаг 1. Создание правил получения ролей при помощи пользовательского справочника

Управлять ролями в типовом маршруте было решено при помощи пользовательского справочника "Матрица ролей".

Записи в нём - это правила назначения ролей. В каждой записи я указал следующие реквизиты:

  • Типовой маршрут (обязательный параметр)
  • Реквизит 1
  • Реквизит 2
  • Реквизит 3
  • ...
  • Реквизит N
  • Таблица ролей

Получилось примерно следующее:

Пользовательские реквизиты я закрасил, так как у вас они могут быть своими.

Шаг 2. Создание типового маршрута для подзадач

Создаем типовой маршрут с одним блоком задание. У вас может быть свой блок-задания - под ваши требования. Я назвал маршрут - "Визирование договорного документа".

Шаг 3. Отправка подзадач визирующим по другому типовому маршруту и запись подзадач в параметр типового маршрута

Располагаем на маршруте блок-сценарий, который будет отправлять подзадачи визирующим по другому типовому маршруту (в маршруте будет один блок визирования документа) для каждой роли.

В вычисления добавляем:

  
  // Список параметров типового маршрута
  Params = Object.WorkFlowParams
  
  // Очистка коллекци отправленных имен ролей
  SightingPerfs = Params.ValueByName('SightingPerfs')
  
  IndexRole = 0
  While IndexRole < SightingPerfs.Count
    // Очистка элемента коллекци отправленных наименований визирующих ролей
    SightingPerfs.Values(IndexRole) = Null    
    // Перебор индексов роли
    IndexRole = IndexRole + 1
  EndWhile
  
  // Согласуемый документ
  DInfo = Params.ValueByName('DocForApproval').Value

  // Документ
  Document = DInfo.Document

  // Инициатор
  Initiator = Params.ValueByName('Initiator').Value
  
  // Хотя бы одна роль есть в параметре ТМ?
  IndexSightingRole = Params.IndexOfName('SightingRoles')
  If IndexSightingRole > -1
  
    // Перебор указанных ролей
    IndexSightingRole = 0
    SightingRoles = Params.ValueByName('SightingRoles')  
    
    While IndexSightingRole < SightingRoles.Count
            
      // Роль IUser
      Role = SightingRoles.Values(IndexSightingRole)

      // Имя роли
      RoleRefName = Role.Name
      
      // Код роли
      RoleRefCode = Role.Code

      // Добавление ролей в коллекцию
      SightingPerfs = Params.ValueByName('SightingPerfs')
      If SightingPerfs.IsCollection
        SightingPerfs.Values(SightingPerfs.Count) = RoleRefName
      EndIf
      
      // Для каждой роли создаём подзадачу по ТМ
      // роль передаём в параметр маршрута с 1 блоком-заданием для этой роли
      
      // Создать подзадачу к задаче
      Task = Tasks.CreateSubTaskToTask(Object)
          
      // Код типового маршрута из справочника Типовые маршруты.
      RouteCode = 'ВДД'
      
      // Загрузить типовой маршрут
      Task.LoadStandardRoute(RouteCode)
      
      // Заполнение параметров типового маршрута    
      // Инициатор
      // Task.Author = Initiator.Code
      
      // Визирующая роль
      Role = ServiceFactory.GetRoleByCode(RoleRefCode)       
      indexRole = Task.WorkflowParams.IndexOfName('SightingArbitraryRole')
      Task.WorkflowParams.Values(indexRole).Value = Role
       
      // Согласуемый документ
      indexDoc = Task.WorkflowParams.IndexOfName('DocForApproval')
      Task.WorkflowParams.Values(indexDoc).Value = DInfo
                      
      // Установить параметры типового маршрута
      Task.SetupStandardRoute(True)
      
      // Стартовать задачу
      Task.Start
      
      // Добавление подзадач в коллекцию для мониторинга
      SentSubTasksByRoles = Params.ValueByName('SentSubTasksByRoles')
      If SentSubTasksByRoles.IsCollection
        SentSubTasksByRoles.Values(SentSubTasksByRoles.Count) = Task.Info
      EndIf

      // Перебор индексов роли
      IndexSightingRole = IndexSightingRole + 1 
      
    EndWhile
     
    // Запоминаем число ролей, если не запомнили раньше
    If Params.ValueByName('CountSightingRoles').Value == 0
      Params.ValueByName('CountSightingRoles').Value = SightingPerfs.Count
    EndIf
       
  EndIf
  

Здесь

  • SightingPerfs - Список наименований визирующих ролей - Коллекция строк
  • DocForApproval - Согласуемый документ - Электронный документ
  • Initiator - Инициатор - Пользователь
  • SightingRoles - Визирующие роли - Коллекция ролей
  • SentSubTasksByRoles - Отправленные подзадачи для визирующих ролей - Коллекция задач
  • CountSightingRoles - CountSightingRoles - Число визирующих ролей - Целое число - 0

Шаг 4. Размещение и настройка блока Мониторинг

Обязательно добавляем на схему блок Мониторинг и блоки-условия по вашему усмотрению:

Проверить есть ли визирующие роли можно, например, так:


  // Список параметров типового маршрута
  Params = Object.WorkFlowParams
    
  // Визирующие роли
  SightingRoles = Params.ValueByName('SightingRoles').Value
   
  // Проверяет, есть ли визирующие пользователи
  Res = Assigned(SightingRoles)
  
  // Запоминаем результат в параметрах ТМ
  Params.ValueByName('SightingRolesPresent').Value = Res
  
  // Результат выполнения блока-условия
  Result = Res
  

В блоке-мониторинг настраиваем так, чтобы он ожидал завершения созданных подзадач (Тип мониторинга => Список зависимостей):

В событиях блока ничего прописывать дополнительно не надо.

Шаг 5. Обработка результатов выполнения

Тут зависит от вашей задачи. В моем случае была проверка завизировали ли роли электронный документ. Я решил проверять в вычислениях блока-условия:


  // Список параметров типового маршрута
  Params = Object.WorkFlowParams

  // Присутствие визирующих пользователей
  SightingUsersPresent = Params.ValueByName('SightingRolesPresent').Value
  
  // Если визирующие пользователи не присутствуют, то идет дольше по ТМ
  If SightingUsersPresent
  
    // Созданные подзадачи
    SentSubTasks = Params.ValueByName('SentSubTasksByRoles')
    
    // Ожидаемый ответ визирующих
    ExpAnswer = Params.ValueByName('ExpAnswerSighting').Value
    
    // Ожидаемое число согласований
    CountSighting = Params.ValueByName('CountSightingRoles').Value
   
    // Определяет все ли визирующие роли дали положительный ответ
    Res = CheckAnswersSightingsFromTasks(SentSubTasks; ExpAnswer; CountSighting)
    
  Else
    
    // Визирующих нет, поэтому проходим дальше по ТМ
    Res = True
           
  EndIf

  // Определяем результат согласования
  Params.ValueByName('RolesAgreed').Value = Res
  
  // Результат выполнения блока-условия
  Result = Res
  

, где

  • SightingRolesPresent - Визирующие роли присутствуют - Логическое значение
  • SentSubTasksByRoles - Отправленные подзадачи для визирующих ролей - Коллекция задач
  • ExpAnswerSighting - Ожидаемый ответ визирующих - Строка
  • CountSightingRoles - Число визирующих ролей - Целое число - 0
  • RolesAgreed - Роли согласовали - Логическое значение

Шаг 6. Обработка подзадач в случае прекращения ведущей задачи

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

В событие "Прекращение" ведущей задачи добавляем следующее:

 
  // Список параметров типового маршрута
  Params = Object.WorkFlowParams
  
  // Созданная коллекция подзадач
  ListSubTasks = Params.ValueByName('SentSubTasksByRoles')

  // Индекс текущей подзадачи
  Index = 0 
  
  // Общее число подзадач
  ListSubTasksCount = ListSubTasks.Count
  
  // Перебор подзадач
  While Index < ListSubTasksCount
    
    // Информация о подзадаче
    TaskInfo = ListSubTasks.Values(Index)
   
    // Текущая подзадача
    Task = TaskInfo.Task
        
    // Прекращение задачи, если необходимо
    If Not (Task.TaskState == 'Выполнена' Or Task.TaskState == 'Прекращена')
      Task.Abort
    EndIf 
    
    // Перебор индексов подзадач
    Index = Index + 1
    
  EndWhile
  

Здесь:

  • SentSubTasksByRoles - Отправленные подзадачи для визирующих ролей - Коллекция задач

Подводя итоги

Цели задачи достигнуты, хотя и не быстрыми решениями "из коробки".

Отдельное спасибо Юлии Литвинюк за подсказку и общий принцип решения моего вопроса.

Остальным - спасибо за прочтение этой статьи.

Александр Манохин

Хотелось бы добавить, что для снижения нагрузки на Workflow (если такая потребность есть) можно вместо блока мониторинга использовать блок Пауза, появившийся в DIRECTUM 5.5. Однако, в таком случае необходимо добавлять в ТМ для подзадач проверку, что нет других подзадач от ведущей задачи по этому ТМ, и снятие с паузы ведущей задачи.

Юлия Литвинюк

Александр, тип мониторинга = "Список зависимостей" не нагружает Workflow, т.к. в этом случае нет периодического выполнения с заданным интервалом. Тип мониторинга = "Список зависимостей" работает по другому:

  1. При обработке службой Workflow блока мониторинга в таблицу SBWorkflowDependencies добавляется список зависимых друг от друга задач по блоку мониторинг.
  2. Записи удаляются из таблицы при завершении, прекращении, удалении задачи с блоком мониторинг и задач, от которых она зависит.

Подробнее можно почитать в этой статье.

Юлия Литвинюк

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

Александр Манохин

Юлия Литвинюк, странно, насколько я помню, при решении одной задачи было недостаточно использовать Мониторинг со списком зависимостей, помог тогда только блок Пауза, поэтому и написал. Возможно, были еще факторы, которые в совокупности улучшили ситуацию, а у меня в голове отложилась только замена блока. ¯\_(ツ)_/¯

Артем Сунцов

Фёдор, для пущей стройности добавьте ещё события проверки наличия активных подзадач при попытке прекращения главной задачи и защитите сами подзадачи от случайного или злонамеренного прекращения.

Юлия Литвинюк

Федор, еще одно небольшое улучшение кода (уж больно мне такое глазки режет ). Вместо

  // Определяем результат согласования
  If Res
    Params.ValueByName('RolesAgreed').Value = True
  Else
    Params.ValueByName('RolesAgreed').Value = False
  EndIf

проще писать

  // Определяем результат согласования
  Params.ValueByName('RolesAgreed').Value = Res

Такая же конструкция встречается в проверке есть ли визирующие роли 

Фёдор Сироткин

Юлия, согласен ) Поправил 

Фёдор Сироткин

Артем, спасибо за совет.

Сначала не совсем его понял, т.к. если инициатор для ведущей задачи и подзадач один и тот же то система сама "из коробки" предлагает прекратить подзадачи. Но тут есть минус - у него в исходящих накапливаются отправленные задачи и подзадачи, что может быть не совсем логично для пользователя и вводить его в заблуждение. В случае, если инициатором подзадач выступает служебный пользователь, то диалоговое окно не появляется и прекращается только ведущая задача. Вот тут то и потребуется эта проверка и программное прекращение подзадач при необходимости.

UPD: Обновил статью.

Фёдор Сироткин: обновлено 04.09.2019 в 14:18
Юлия Шурунова

Не поняла, зачем такие невероятные сложности - дополнительный ТМ, подзадачи... Есть блок задание, у него можно поставить "Параллельное выполнение=Да". Поскольку исполнителя типа "Список ролей" не существует (а когда я обратилась в техподдержку, меня попытались убедить, что это не нужно и можно обойтись, например, специально создаваемой для ТМ группой пользователей, что, ИМХО, совершенно неприемлемо), я решила проблему так: ставлю в блоке "Задание" исполнителя "Список пользователей", создаю в задаче параметр этого типа, и перед "Заданием" ставлю блок "Сценарий", в вычислении которого сначала  в явном виде задаю список ролей, часть из которых добавляется в зависимости от каких-то условий, а потом в цикле по этим ролям вычисляю исполнителей и добавляю их в тот самый параметр типа "Список пользователей". Допустимые результаты задания - "Завизировано" и еще какой-нибудь, например "На доработку". И всё!  Блок "Задание" будет выполнен, когда свои задания выполнят ВСЕ исполнители из списка, а исходящих соединений - всего два: "Завизировано" и "Иначе".

Params = Work.WorkFlowParams
AgreeList  = Params.ValueByName("СписокВизирующих").Value // параметр - список пользователей-исполнителей задания
// причем, при этом присвоении в переменную заносится ССЫЛКА на параметр-список, а не копия, поэтому в конце кода обратного присвоения делать не нужно
AgreeList.Clear()  // на всякий случай - а вдруг это уже не первый круг согласования

RoleNamesList  = CreateStringList() // список наименований ролей
RoleNamesList.Add("ИмяРоли1")
RoleNamesList.Add("ИмяРоли2")
if Params.ValueByName("ЛогическийПараметр1").Value или Params.ValueByName("ЛогическийПараметр2").Value
   RoleNamesList.Add("ИмяРоли3")
endif
if <любое условие>
   RoleNamesList.Add("ИмяРоли4")
else
   RoleNamesList.Add("ИмяРоли5")
   RoleNamesList.Add("ИмяРоли6")
endif

foreach Role in RoleNamesList
  UserNames = ТМПолучитьИсполнителейРоли(Role;";")
  // Добавить пользователей в список исполнителей задания
  foreach UserName in CSubString(UserNames; ';')
    CurrUser = ServiceFactory.GetUserByName(UserName)
    AgreeList.Add(CurrUser)
  endforeach
endforeach

Если вот такое - явное, в тексте сценария - заполнение списка наименований ролей почему-то неприемлемо, то можно заполнить как раз через справочник, который предложил Федор. Хотя я не вижу смысла, ибо, если бы был в ТМ доступен исполнитель "Список ролей", то роли тоже задавались бы при настройке ТМ, а не извне.

Юлия Шурунова: обновлено 12.04.2022 в 15:42
Юлия Шурунова: обновлено 12.04.2022 в 15:43
Юлия Шурунова: обновлено 12.04.2022 в 15:46

Авторизуйтесь, чтобы написать комментарий