Будем учиться кастомизировать Битрикс24
В этой большой и полезной статье расскажем как создать свое действие (активити) для коробочной версии продукта. Материалы подготовили ребята из
Исходный код решения доступен по
По этой теме мы так же провели вебинар:
На вебинаре показали разработку "в живую" , полезное дополнение к статье.
Разработку своего действия можно рассмотреть как несколько этапов:
- Подготовка объекта
- Реализация формы настройки для визуального конструктора БП
- Реализация логики действия (у нас будет задание на звонок):
- Создание задания БП и подписка на события
- Обработка исключительных ситуаций во время выполнения БП
- Вывод формы задания исполнителю
- Обработка формы и событий задания
- Создание задания БП и подписка на события
Итак, слово коллегам из Интерволги:
[spoiler]
Введение
Модуль бизнес-процессов позволяет администраторам корпоративных порталов автоматизировать деятельность организации без привлечения программиста. Для этого модуль БП предоставляет визуальный конструктор шаблонов БП, в котором разрабатывается алгоритм или конечный автомат. Алгоритм состоит из действий (например, утверждение документа, запрос доп. информации, постановка задачи, изменение полей документа и т. п.), с помощью которых строится взаимодействие с сотрудниками и автоматизируются некоторые операции над документами.
Иногда случается так, что штатных возможностей модуля БП недостаточно для реализации того или иного БП. Разработчиками модуля предусмотрены различные механизмы расширения возможностей БП. Один из них — возможность разрабатывать собственные действия для конструктора шаблонов БП. Эта возможность доступна как для облачных, так и для коробочных версий КП, но принципы разработки для облака и коробки сильно отличаются.
В данной статье мы рассмотрим разработку собственного действия для коробочной версии КП на примере одной из задач кредитной организации. Некоторая информация о разработке своих действий имеется в курсе по бизнес-процессам:
Постановка задачи
Общая задача: автоматизировать процесс обработки заявки для кредитной организации.
В процессе обработки заявки на кредит сотрудники совершают звонки заемщикам, поручителям, родным, знакомым, работодателю и т. п. Общий бизнес-процесс ставит задания сотрудникам типа «позвонить заемщику». Однако для совершения звонка через стандартную телефонию, сотрудник должен перейти на детальную страницу документа БП (в нашем случае — сделки CRM), затем позвонить клиенту, задать вопросы и заполнить анкету (которая еще должна быть перед глазами) ответами на эти вопросы.
Частная задача: уменьшить затраты времени сотрудников на обзвон и анкетирование клиентов и поручителей.
Для решения этой задачи разрабатываем действие БП «Звонок клиенту с анкетой». Данное действие должно позволять поставить задание сотруднику на звонок кому-либо с заполнением анкеты (похож на запрос доп. информации). Сотрудник должен видеть кнопку звонка, по нажатии на которую начинается вызов абонента через телефонию Б24, а также выводятся поля анкеты для заполнения.
Исходя из прочих требований, действие должно позволять:
- указать в качестве абонента как контакт CRM, так и произвольный номер телефона;
- добавлять в анкету однострочные, многострочные, числовые поля, выбор значения из списка, а также примечания (можно использовать для группировки полей анкеты);
- требовать обязательного заполнения некоторых полей анкеты;
- управлять порядком полей анкеты по вертикали;
- форматировать текст примечаниях с помощью BB-кодов;
- использовать ответы на вопросы в последующих шагах БП.
Разработка действия БП «Звонок клиенту с анкетой»
1. Подготовка объекта
Нестандартные действия БП должны располагаться в каталоге /local/activities.
Для начала выберем название для нашего действия: IvSipCallActivity.
Теперь создадим каталог нашего действия. Его название должно быть в нижнем регистре: /local/activities/ivsipcallactivity.
В этом каталоге создадим следующие файлы:
Файл | Комментарий |
.description.php | Файл описания действия (название, категория в конструкторе и пр.). |
ivsipcallactivity.php | Файл с классом действия, в котором будем описывать логику действия. Должен называться как каталог действия. |
icon.gif | Не обязательно. Иконка действия размером 24x24 для конструктора. |
lang/ | Каталог файлов локализации для Loc::getMessage. |
В этом файле необходимо определить массив $arActivityDescription с описанием действия следующим образом.
<?php defined('B_PROLOG_INCLUDED') || die(); use Bitrix\Bizproc\FieldType; use Bitrix\Main\Localization\Loc; $arActivityDescription = array( // Название действия для конструтора. 'NAME' => Loc::getMessage('SIPCALL_NAME'), // Описание действия для конструктора. 'DESCRIPTION' => Loc::getMessage('SIPCALL_DESCRIPTION'), // Тип: “activity” - действие, “condition” - ветка составного действия. 'TYPE' => 'activity', // Название класса действия без префикса “CBP”. 'CLASS' => 'IvSipCallActivity', // Название JS-класса для управления внешним видом и поведением в конструкторе. // Если нужно только стандартное поведение, указывайте “BizProcActivity”. 'JSCLASS' => 'BizProcActivity', // Категория действия в конструкторе. // Данный шаг размещен в категории “Уведомления” по историческим причинам. 'CATEGORY' => array( 'ID' => 'interaction', ) // Названия свойств действия, из которых будут взяты возвращаемые значения. 'ADDITIONAL_RESULT' => array('QuestionnaireResults') ); ?> |
На возврате значений остановимся подробнее. Существует два способа описать возвращаемые значения: с помощью ключей RETURN и ADDITIONAL_RESULT.
Используйте RETURN, если вам заранее известен состав возвращаемых значений. Например, если вы разрабатываете действие «Сумма чисел», то вы заранее знаете, что выход у действия будет один — значение суммы.
RETURN должен содержать массив, описывающий каждое возвращаемое значение следующим образом:
$arActivityDescription = array( 'RETURN' => array( // Одно из возвращаемых значений. 'Sum' => array( 'NAME' => Loc::getMessage('SUM_RESULT'), 'TYPE' => FieldType::DOUBLE ) ) ); |
ADDITIONAL_RESULT должен содержать последовательный массив названий свойств действия, из которых будут взяты возвращаемые значения. А значения этих свойств будут заданы уже в конструкторе и/или во время выполнения БП. В нашем примере возвращаемые значения будут взяты из свойства QuestionnaireResults (результаты заполнения анкеты).
Использовать одновременно RETURN и ADDITIONAL_RESULT нельзя: будут использованы значения только из RETURN.
ivsipcallactivily.php
В этом файле определим класс нашего действия, который будет описывать его логику. Название класса строится из двух частей: префикса CBP и значения $arActivityDescription[‘CLASS’]. В нашем случае название класса будет CBPIvSipCallActivity.
Поскольку мы разрабатываем «обычное действие» (которое не будет содержать дочерних действий), наш класс должен быть унаследован от CBPActivity. Если же вам понадобится разработать действие, которое должно содержать дочерние действия (как, например, альтернативы и циклы), наследуйте класс своего действия от CBPCompositeActivity.
Создадим класс и определим в нем самые необходимые методы. Реализацию рассмотрим далее.
<?php defined('B_PROLOG_INCLUDED') || die(); use ... /** * Действие "Звонок клиенту с анкетой". */ class CBPIvSipCallActivity extends CBPActivity { /** * Инициализирует действие. * * @param * $name */ public function __construct ($name) { parent::__construct($name); } /** * Начинает выполнение действия. * * @return int Константа CBPActivityExecutionStatus::*. * @throws Exception */ public function Execute () {} /** * Обработчик ошибки выполнения БП * (вызывается, если ошибка произошла во время выполнения данного действия). * * @param Exception $exception * @return int Константа CBPActivityExecutionStatus::*. * @throws Exception */ public function HandleFault (Exception $exception) {} /** * Обработчик остановки БП (если остановка произошла во время выполнения * данного действия). * * @return int Константа CBPActivityExecutionStatus::*. * @throws Exception */ public function Cancel () {} } ?> |
Реализация формы настройки действия для визуального конструктора БП
Для начала определим, какие настройки нужны пользователю:
- название действия в шаблоне, обязательный;
- тип контакта (контакт CRM или произвольный номер телефона), обязательный;
- ID контакта CRM (для соответствующего типа контакта), обязательный;
- номер телефона (для соответствующего типа контакта), обязательный;
- ответственные за задание на звонок (исполнители), обязательный;
- название задания, обязательный;
- описание задания, необязательный;
- поля анкеты, обязательный (должно быть минимум одно поле), — каждое поле имеет следующие настройки:
- тип поля (строка, число, текст, список, заметка — не поле, остальные настройки ему не нужны);
- код поля анкеты для идентификации возвращаемых значений, обязательный;
- название поля для исполнителя, обязательный;
- обязательность заполнения;
- значение по-умолчанию, не обязательный;
- позиция в анкете по вертикали.
- тип поля (строка, число, текст, список, заметка — не поле, остальные настройки ему не нужны);
public function __construct ($name) { // Зададим начальные значения свойств действия при добавлении в шаблон // в конструкторе. В целом, заранее свойства декларировать не требуется. // По сути — это просто ключи массива. $this->arProperties = array( 'ContactType' => 'phone', 'Phone' => '', 'CrmContactId' => '', 'Responsible' => '', 'AssignmentName' => '', 'AssignmentDescription' => '', // Анкета: 'QuestionnaireResults' => array() ); $this->SetPropertiesTypes( array( 'ContactType' => array( 'Type' => FieldType::SELECT ), 'Phone' => array( 'Type' => FieldType::STRING ), 'CrmContactId' => array( 'Type' => FieldType::INT ), 'Responsible' => array( 'Type' => FieldType::USER ), 'AssignmentName' => array( 'Type' => FieldType::STRING ), 'AssignmentDescription' => array( 'Type' => FieldType::TEXT ) )); } |
Чтобы у действия появилась форма настроек, необходимо создать два метода: GetPropertiesDialog и GetPropertiesDialogValues. Первый отвечает за формирование HTML формы настроек, второй — за обработку отправки формы пользователем.
Начнем с GetPropertiesDialog. Этот метод похож на код компонента: сначала формируется массив значений для полей формы, а затем вызывается шаблон самой формы. HTML формы можно сформировать прямо в этом методе, однако, “каша” из HTML и PHP снизит сопровождаемость вашего кода, поэтому такой подход применять не рекомендуется.
Логика подготовки данных следующая:
если форма была отправлена ранее, использовать имеющиеся данные, в противном случае:
инициализировать массив данных формы по-умолчанию,
загрузить свойства действия и дополнить этот массив значениями, которые могли быть сохранены ранее.
public static function GetPropertiesDialog(...) { if (! is_array($arCurrentValues)) { $arCurrentValues = array( 'ContactType' => 'phone', 'Phone' => '', 'CrmContactId' => '', 'Responsible' => '', 'AssignmentName' => '', 'AssignmentDescription' => '', 'SetStatus' => 'Y', 'QuestionnaireResults' => array() ); $arCurrentActivity = &CBPWorkflowTemplateLoader::FindActivityByName( $arWorkflowTemplate, $activityName); if (is_array($arCurrentActivity['Properties'])) { $arCurrentValues = array_merge($arCurrentValues, $arCurrentActivity['Properties']); $arCurrentValues['Responsible'] = CBPHelper::UsersArrayToString( $arCurrentValues['Responsible'], $arWorkflowTemplate, $documentType); } } $runtime = CBPRuntime::GetRuntime(); return $runtime->ExecuteResourceFile(__FILE__, "properties_dialog.php", array( "arCurrentValues" => $arCurrentValues, "formName" => $formName )); } |
Метод имеет много формальных аргументов, поэтому здесь они не приводятся. В исходном коде класса действия вы можете найти их все, а также там есть документирующие комментарии с описанием каждого аргумента.
$arCurrentValues — аргумент метода GetPropertiesDialog. Он будет равен false, когда пользователь нажал на иконку шестеренки в конструкторе. Если же при сохранении настроек данные не прошли валидацию, конструктор снова вызовет метод GetPropertiesDialog, но $arCurrentValues будет являться массивом с теми значениями, которые были отправлены. Поэтому мы проверяем, является ли он массивом, и, если нет — заполняем значениями по-умолчанию и сохраненными ранее значениями.
В нашем случае данные для формы и свойства действия для хранения настроек совпадают, поэтому код инициализации можно скопировать из конструктора.
GetPropertiesDialog — статический метод. Это означает, что из него обращаться к сохраненным ранее свойствам действия через оператор «->» нельзя. Однако весь шаблон БП в виде дерева (массив) и имя действия, настройки которого в данный момент редактируются, являются входными данными метода GetPropertiesDialog. Поэтому воспользуемся утилитным методом FindActivityByName класса CBPWorkflowTemplateLoader, чтобы найти наше действие в шаблоне и получить доступ к его свойствам (они хранятся в поле Properties). После этого просто воспользуемся функцией array_merge, чтобы заменить значения по-умолчанию в массиве $arCurrentValues на сохраненные ранее значения.
Важно! Списки пользователей (поле «Ответственный») должны храниться в специальной форме в виде массива. В конструкторе это же самое поле представлено строкой. Поэтому мы используем метод CBPHelper::UsersArrayToString на значении свойства Responsible, перед тем, как передать его в форму. При сохранении настроек требуется конвертация в обратную сторону, пример будет далее.
Код формы настроек — преимущественно HTML. Конструктор шаблонов БП ожидает, что метод GetPropertiesDialog вернет фрагмент HTML-таблицы, поэтому каждое поле необходимо описывать тегом tr.
Шаблон формы настройки будем хранить отдельно от логики действия. Для этого создадим файл properties_dialog.php в каталоге действия. Чтобы выполнить этот шаблон воспользуемся методом ExecuteResourceFile исполняющей среды, который запускает PHP-файл в режиме буферизации и возвращает весь вывод скрипта в виде строки. В третьем аргументе укажем, какие переменные будут доступны в области видимости скрипта: ключ — название переменной в скрипте.
Код формы довольно объемный, просмотрите его самостоятельно и прочтите комментарии. Здесь лишь опишем общие соображения о создании шаблонов форм настройки.
Для вывода элементов управления используйте метод CBPDocument::ShowParameterField, который поддерживает следующие типы полей: многострочный текст, пользователь, да/нет, дата и время, текстовое поле ввода. К каждому полю этот метод добавляет кнопку «...», которая позволяет подставлять значения вида {=...} из переменных, констант и пр. Если же вам необходимо создать свой элемент управления, то кнопку «...» можно добавить с помощью метода CBPHelper::renderControlSelectorButton.
HTML в файле properties_dialog.php должен быть следующего вида:
<tr> <td align="right" width="40%"><span class="adm-required-field"><?= Loc::getMessage('RESPONSIBLE') ?>:</span> </td> <td width="60%"> <?= CBPDocument::ShowParameterField( 'user', 'Responsible', $arCurrentValues['Responsible']) ?> </td> </tr> |
Редактор полей анкеты реализуем на стороне клиента с помощью JS.
На текущий момент вы можете добавить действие в шаблон и вызвать его настройки.
Теперь добавим метод GetPropertiesDialogValues в класс действия. Как и в первом случае, список аргументов и документирующие комментарии вы можете найти в исходном коде действия.
Задача этого метода — проверить введенные данные на корректность и сохранить их в свойствах действия, либо сообщить об ошибках.
Аргумент $arCurrentValues содержит введенные пользователем данные (считайте это чем-то вроде $_REQUEST). В зависимости от ограничений, которые должны быть наложены на данные, реализуйте проверку, в случае если данные не проходят критерий валидации, добавьте в массив $arErrors описание ошибки. $arErrors — аргумент метода GetPropertiesDialogValues, который передается по ссылке. Например, поле «Название задания» является обязательным, а, значит, не должно быть пустым. Его проверку мы можем реализовать следующим образом:
if (empty($arCurrentValues['AssignmentName'])) { $arErrors[] = array( 'code' => 'Empty', 'message' => Loc::getMessage('ERROR_NO_ASSIGN_NAME') ); } |
После проверки всех полей, если есть ошибки, метод GetPropertiesDialogValues должен вернуть false. Когда этот метод возвращает false, конструктор шаблонов не закроет окно с настройками, а выведет эти ошибки. Форма будет заполнена данными, которые были на момент сохранения, если метод GetPropertiesDialog учитывает, что $arCurrentValues может быть чем-либо заполнен.
В случае успешного прохождения валидации, мы формируем новый массив значений свойств. Как и в прошлый раз, воспользуемся методом CBPWorkflowTemplateLoader::FindActivityByName, чтобы найти наше действие в дереве шаблона. Однако в этот раз, дерево передано нам по ссылке, чем мы и воспользуемся для сохранения настроек.
$arProperties = array( // ... 'CrmContactId' => $arCurrentValues['CrmContactId'], 'Responsible' => CBPHelper::UsersStringToArray( $arCurrentValues['Responsible'], $documentType, $arErrors ), 'AssignmentName' => $arCurrentValues['AssignmentName'], // ... ); $arCurrentActivity = &CBPWorkflowTemplateLoader::FindActivityByName( $arWorkflowTemplate, $activityName ); $arCurrentActivity['Properties'] = $arProperties; return true; |
Обратите внимание, что список ответственных за задание передается нам из конструктора в виде строки. Перед сохранением мы преобразуем его в массив с помощью метода CBPHelper::UsersStringToArray.
Если ошибок нет, метод должен вернуть true, в этом случае конструктор закроет окно настройки действия.
На этом поведение действия для конструктора можно считать законченным.
Реализация логики шага: задание на звонок
Теперь приступим к реализации логики действия, которая будет выполняться во время работы бизнес-процесса.
Если действие включает постановку задания пользователям, то общий принцип решения этой задачи следующий:
при запуске действия исполняющей средой происходит создание задания (задание — отдельная сущность) и регистрация действия как обработчика события задания;
действие сообщает исполняющей среде, что шаг еще не завершил выполнение, но на текущий момент делать больше нечего (будем ожидать действий пользователя), БП приостанавливает свое выполнение;
обработка отправленной формы задания: валидация данных и отправка события о попытке завершения задания;
обработка события нашим действием: если все необходимые условия выполнены, завершение задания и самого действия (исполняющая среда поймет, что действие завершилось и запустит следующее).
Для такой «событийной» деятельности разработчиками модуля БП предусмотрены определенные механизмы. Со стороны разработчика действия потребуется реализовать два интерфейса в классе действия.
Интерфейс | Назначение |
IBPEventActivity | Информирует исполняющую среду и пользователей действия (с программной точки зрения) о возможности действия отправлять внешние по отношению к БП события. Включает два метода:
|
IBPActivityExternalEventListener | Информирует о возможности действия обрабатывать внешние по отношению к БП события. Включает один метод:
|
class CBPIvSipCallActivity extends CBPActivity implements IBPEventActivity, IBPActivityExternalEventListener { ... |
Таким образом, наше действие — само себе обработчик события.
Создание задания БП и подписка на события
Для начала реализуем метод Subscribe. Его задача — создать задание для пользователей и зарегистрировать обработчик события.
Прежде всего запишем в журнал выполнения БП, что его исполнение дошло до задания на звонок. В общем случае, чтобы добавить запись в этот журнал, необходимо использовать службу журналирования бизнес-процессов (Tracking Service), однако для этой задачи есть метод-обертка WriteToTrackingService.
// $MESS['STARTED_EXEC'] = 'Поставлено задание: #ACTIVITY#. Ответственный: // #USERS#.'; $arUsersTmp = $this->Responsible; if (! is_array($arUsersTmp)) { $arUsersTmp = array( $arUsersTmp ); } $this->WriteToTrackingService( Loc::getMessage('STARTED_EXEC', array( '#ACTIVITY#' => $this->AssignmentName, '#USERS#' => '{=user:' . implode('}, {=user:', $arUsersTmp) . '}' ))); |
При записи в журнал можно использовать некоторые выражения как при создании шаблона БП.
Создание задания выполняется с помощью службы заданий бизнес-процессов. Заданию потребуются некоторые данные из документа БП, которые, аналогично, можно получить с помощью службы документов. Получить экземпляры служб можно следующим образом:
$rootActivity = $this->GetRootActivity(); $documentId = $rootActivity->GetDocumentId(); $runtime = CBPRuntime::GetRuntime(); $documentService = $runtime->GetService('DocumentService'); $taskService = $this->workflow->GetService('TaskService'); |
Создание задания выполняется с помощью вызова метода $taskService->CreateTask(...). О поле класса действия taskId — далее.
// Получить ID пользователей из поля «Ответственные». $arUsers = CBPHelper::ExtractUsers($arUsersTmp, $documentId, false); $arParameters = array( 'CONTACT_TYPE' => $this->ContactType, 'PHONE' => $this->Phone, 'CRM_CONTACT_ID' => $this->CrmContactId ); /** @var CBPTaskService $taskService */ $taskService = $this->workflow->GetService('TaskService'); $this->taskId = $taskService->CreateTask( array( 'USERS' => $arUsers, 'WORKFLOW_ID' => $this->GetWorkflowInstanceId(), 'ACTIVITY' => 'IvSipCallActivity', 'ACTIVITY_NAME' => $this->name, 'NAME' => $this->AssignmentName, 'DESCRIPTION' => $this->AssignmentDescription, 'PARAMETERS' => $arParameters, 'IS_INLINE' => 'N', 'DOCUMENT_NAME' => $documentService->GetDocumentName($documentId) ) ); |
Параметры задания ($arParameters) вы выбираете самостоятельно. Передайте все, что нужно для отображения формы задания. Если какие-либо данные в форме должны меняться в зависимости от значений свойств действия, передавать их в качестве параметров задания не имеет смысла. Для примера, в качестве параметров не передаются поля анкеты и значения, введенные пользователем — к ним доступ будет осуществлен иначе.
Наконец, нужно сообщить исполняющей среде о регистрации обработчика события.
$this->workflow->AddEventHandler($this->name, $eventHandler); |
Первый аргумент — название события. Его можно выбрать произвольно, однако общепринятая практика — использовать название действия в качестве названия события. Второй аргумент — сам обработчик события. Это объект, реализующий интерфейс IBPActivityExternalEventListener, он передается в метод Subscribe.
Полную реализацию метода со всеми проверками на исключительные ситуации вы можете посмотреть в исходном коде действия.
Задача метода Unsubscribe — удалить зарегистрированный обработчик событий и удалить или закрыть задание БП в зависимости от ситуации. В классе действия заведем поля taskId и taskStatus, которые будут содержать ID созданного задания и его конечный статус на случай непредвиденного вызова Unsubscribe (об этом далее).
if ($this->taskStatus === false) { $taskService->DeleteTask($this->taskId); } else { $taskService->Update($this->taskId, array( 'STATUS' => $this->taskStatus ) ); } $this->workflow->RemoveEventHandler($this->name, $eventHandler); $this->taskId = 0; $this->taskStatus = false; |
Наконец, реализуем метод Execute.
Чтобы задание создалось, достаточно вызвать метод Subscribe. Событие завершения задания будет обрабатывать наше действие, поэтому в качестве обработчика укажем $this (наше действие как раз реализует интерфейс IBPActivityExternalEventListener).
$this->Subscribe($this); |
return CBPActivityExecutionStatus::Executing; |
Обработка исключительных ситуаций во время выполнения БП
При разработке своих действий всегда надо помнить о двух ситуациях, когда действие должно быть корректно завершено раньше, чем должно было. К таким ситуациям относятся:
- остановка БП пользователем или удаление документа;
- ошибка при выполнении БП, из-за которой исполняющая среда принимает решение об остановке БП.
Реализуем метод Cancel. Поскольку в методе Unsubscribe уже реализована логика удаления или завершения поставленного ранее задания, вызовем его. Также вернем исполняющей среде состояние действия — “завершено”.
/** * Обработчик остановки БП (если остановка произошла во время выполнения данного действия). * @return int Константа CBPActivityExecutionStatus::*. * @throws Exception */ public function Cancel () { if ($this->taskId > 0) { $this->Unsubscribe($this); } return CBPActivityExecutionStatus::Closed; } |
В методе HandleFault вызовем Cancel для корректного завершения действия в случае ошибки.
/** * Обработчик ошибки выполнения БП (вызывается, если ошибка произошла * во время выполнения данного действия). * @param Exception $exception * @return int Константа CBPActivityExecutionStatus::*. * @throws Exception */ public function HandleFault (Exception $exception) { $status = $this->Cancel(); if ($status == CBPActivityExecutionStatus::Canceling) { return CBPActivityExecutionStatus::Faulting; } return $status; } |
Некоторые несущественные проверки опущены в приведенном выше коде. Полные реализации методов вы можете увидеть в исходном коде действия.
Вывод формы задания исполнителю
Теперь реализуем форму задания — то, что увидит пользователь, которому оно будет поставлено.
За формирование HTML формы отвечает метод ShowTaskForm, который нужно добавить в класс действия. Метод, в числе прочих, принимает аргумент $arTask, в котором можно найти параметры, указанные при создании задания. Для получения значений свойств действия БП сделаем следующее.
$runtime = CBPRuntime::GetRuntime(); $workflow = $runtime->GetWorkflow($arTask['WORKFLOW_ID'], true); /** @var CBPIvSipCallActivity $activity */ $activity = $workflow->GetActivityByName($arTask['ACTIVITY_NAME']); // Например, получим анкету: $questions = $activity->QuestionnaireResults; |
Для формирования HTML виджета телефонии в классе действия созданы два метода: getSipPhoneHtml и getContactHtml, возвращающие код виджета для произвольного номера телефона и контакта CRM соответственно.
Рассмотрим метод getSipPhoneHtml. Он использует класс CCrmViewHelper модуля CRM для генерации виджета телефонии.
/** * Генерирует HTML-код виджета для совершения звонка через телефонию * для произвольного номера телефона. * * @param array[] $phones * array(array('VALUE' => '+12345678901', 'VALUE_TYPE' => '<phone * type>'), ...), * Смотрите CCrmFieldMulti::GetEntityTypes(), чтобы узнать доступные * типы телефонов. * @param int $crmContactId * @return string HTML. */ private static function getSipPhoneHtml ($phones, $crmContactId = null) { Loader::includeModule('crm'); $asset = Asset::getInstance(); $asset->addCss('/bitrix/js/crm/css/crm.css'); $asset->addJs('/bitrix/js/crm/common.js'); $typeID = 'PHONE'; $arEntityFields = array( 'FM' => array( $typeID => $phones ) ); $arOptions = array( 'ENABLE_SIP' => true, 'SIP_PARAMS' => array() ); if (intval($crmContactId) > 0) { $arOptions['SIP_PARAMS']['ENTITY_TYPE'] = 'CRM_CONTACT'; $arOptions['SIP_PARAMS']['ENTITY_ID'] = intval($crmContactId); } return CCrmViewHelper::PrepareFormMultiField($arEntityFields, $typeID, '', null, $arOptions); } |
Первым аргументом передается список полей и их значений. У нас поле одно — телефон, значений может быть несколько. Сами значения являются входными данными метода getSipPhoneHtml. Пример $arEntityFields:
$arEntityFields = array( 'FM' => array( 'PHONE' => array( array( 'VALUE' => '+71234567890', 'VALUE_TYPE' => 'WORK' ) ) ) ); |
Второй аргумент — $typeID — поле, значение которого нужно вывести. Поэтому ожидать, что метод сгенерирует HTML для всех полей не стоит, только для того, которое укажем.
Третий аргумент — префикс атрибута id для создаваемого HTML. Аргумент необязательный, значение по умолчанию — пустая строка. Четвертый аргумент — описания полей (PHONE, EMAIL...), необязательный, если передать null, метод сам возьмет описания всех доступных полей.
Последний аргумент — $arOptions — настройки генератора кода. Чтобы виджет телефонии сгенерировался (а не просто номер телефона текстом), нужно указать следующие опции:
$arOptions = array( 'ENABLE_SIP' => true, 'SIP_PARAMS' => array() ); |
Также важно указать, с какой сущностью CRM будет связан звонок: это нужно, чтобы появилось сообщение в ленте контакта с записью разговора (если хранение записей включено в настройках модуля телефонии). Сделать это можно так:
$arOptions['SIP_PARAMS']['ENTITY_TYPE'] = 'CRM_CONTACT'; $arOptions['SIP_PARAMS']['ENTITY_ID'] = $crmContactId; |
Виджет телефонии выглядит как ссылка на номер телефона. Однако при ее нажатии запускается звонок.
Этого достаточно, если необходимо вывести виджет для произвольного номера телефона. Однако, если в настройках действия указан контакт CRM, мы выведем дополнительную информацию и ссылку на страницу контакта, чтобы сотруднику было удобнее.
Для этого реализуем метод getContactHtml. В нем, помимо телефона, мы выведем фотографию, полное имя и должность. Рассмотрите код этого метода самостоятельно, он достаточно прост. Для генерации виджета телефонии используем ранее созданный метод getSipPhoneHtml. В результате получим следующее:
Окно звонка, которое открывается при нажатии по номеру телефона, можно сворачивать. Поэтому сотрудник, которому поставлено задание, сможет во время разговора заполнять анкету.
Как и в случае с формой настройки действия, HTML-код всей формы вынесем в отдельный файл tasktemplate.php (который нужно создать в каталоге действия). Этот файл, аналогично, представляет собой фрагмент таблицы и должен состоять из полей, описываемых с помощью тега tr.
<tr> <td valign="top" width="40%" align="right" class="bizproc-field-name"> <?= Loc::getMessage('CONTACT')?> </td> <td valign="top" width="60%" class="bizproc-field-value"> <?= $arResult['CONTACT_HTML']?> </td> </tr> |
Вызов этого шаблона, как и ранее, выполним с помощью метода ExecuteResourceFile экземпляра исполняющей среды. В качестве переменных укажем $arResult, включающий:
- CONTACT_HTML — HTML код для звонка, полученный с помощью getSipPhoneHtml или getContactHtml;
- QUESTIONS — вопросы анкеты и последние сохраненные сотрудником значения (или значения по-умолчанию).
$buttons = '<input type="submit" name="finish" value="' . Loc::getMessage('FINISH') . '">'; |
В новых версиях модуля БП можно вывести красивые кнопки в стиле кор. портала. Для этого нужно создать еще один метод — getTaskControls.
/** * * @param array $arTask * Поля задания. * @return array */ public static function getTaskControls ($arTask) { return array( 'BUTTONS' => array( array( 'TYPE' => 'submit', 'TARGET_USER_STATUS' => CBPTaskUserStatus::Ok, 'NAME' => 'finish', 'VALUE' => 'Y', 'TEXT' => Loc::getMessage('FINISH') ) ) ); } |
Обработка формы и событий задания
Осталось лишь реализовать поведение нашего действия при нажатии кнопки «Готово» в форме задания.
Когда пользователь на нее нажимает, запускается метод PostTaskForm. Реализация данного метода похожа на GetPropertiesDialogValues: нужно проверить корректность данных и собрать массив ошибок, затем вернуть true или false в зависимости от успеха. Однако, в случае, если данные успешно прошли проверку, нужно еще отправить событие о попытке завершения задания.
Последнее сделаем следующим образом:
$arEventParameters = array( 'USER_ID' => $userId, 'REAL_USER_ID' => $realUserId, 'USER_NAME' => $userName, 'QUESTIONS' => $questions ); CBPRuntime::SendExternalEvent($arTask['WORKFLOW_ID'],$arTask['ACTIVITY_NAME'], $arEventParameters); |
$arTask, $userId, $userName, $realUserId — аргументы метода PostTaskForm. Все остальное аналогично методу GetPropertiesDialogValues, посмотрите исходный код этого метода самостоятельно.
Наконец, осталось реализовать последний метод — OnExternalEvent, который занимается обработкой событий.
В этом методе мы проверим, все ли требования выполнены для завершения задания и, если да, то завершим его и само действие.
Здесь сделаем небольшое отступление. Вы могли заметить в исходном коде действия понятие ValueStorage. Рассмотрим его немного подробнее.
В общем случае, существует два способа получить выходные данные действия:
- возвращаемые значения (как сделано в данном действии),
- побочный эффект, например, изменение переменных БП (как сделано в действии «Запрос доп. информации»).
В процессе использования этого действия «в бою» оказалось удобным вызывать его в цикле с разными контактами. Например, сотрудник не смог выяснить всю необходимую информацию по контакту, тогда БП предлагает позвонить другому контакту. Когда, позднее, сотрудник вернется к первому, все введенные значения в поля анкеты, соответствующие данному контакту, должны быть восстановлены. Поэтому значения хранятся отдельно от анкеты и сразу для всех контактов, с которыми было запущено данное действие. ValueStorage — свойство действия, массив, ключами которого являются ID контакта, либо номера телефонов, значения — массивы значений для полей анкеты. Метод getValueStorageName возвращает ключ массива ValueStorage, соответствующий выбранному контакту.
В методе OnExternalEvent мы сохраняем введенные значения в ValueStorage и проверяем, все ли обязательные поля заполнены.
Критерием завершения задания является заполнение всех обязательных полей. Если это так ($allRequiredFilled), то:
- завершаем задание;
- записываем в журнал выполнения БП факт завершения действия;
- удаляем обработчики событий;
- сообщаем исполняющей среде о том, что действие завершилось, можно начинать следующее.
if ($allRequiredFilled) { $taskService = $this->workflow->GetService('TaskService'); $taskService->MarkCompleted($this->taskId, $arEventParameters['REAL_USER_ID'], CBPTaskUserStatus::Ok); $this->WriteToTrackingService( Loc::getMessage('FINISHED', array( '#ACTIVITY#' => $this->AssignmentName ))); $this->taskStatus = CBPTaskStatus::CompleteOk; $this->Unsubscribe($this); $this->workflow->CloseActivity($this); } |
$arEventParameters — аргумент метода OnExternalEvent. Код сохранения значений полей анкеты изучите самостоятельно.
Теперь при запуске БП можно видеть как создается задание и выполнить его.
Тестовый пример
Проверим работу нашего действия на примере небольшого БП в ленте.
Документ имеет следующие поля:
- Клиент — контакт CRM, которому будем звонить,
- Возраст — то, что нужно выяснить при звонке,
- Образование — то, что нужно выяснить при звонке.
- Responsible — ответственный за задание, тип — привязка к сотруднику.
- Phoned — «дозвонились».
Дозвонились или нет — поле анкеты (результат). БП будет ставить задание на звонок с интервалом в 1 час до тех пор, пока ответственный не укажет в анкете, что звонок был успешным.
Действие имеет следующие настройки.
Анкета, в данном случае, соответствует полям документа.
Запустим БП. У нашего действия выполнится метод Execute, затем Subscribe, будет создано задание БП.
Созданное задание мы увидим в ленте.
При нажатии кнопки «Приступить» выполнится метод ShowTaskForm, который выводит анкету.
Нажатие на номер телефона запускает звонок через телефонию Битрикс24.
После того как связь с абонентом будет успешно установлена, в правом верхнем углу появится кнопка «Свернуть». Окно звонка будет минимизировано и появится возможность заполнять анкету прямо во время разговора. В левом нижнем углу экрана вы увидите следующее:
Стрелка справа позволяет развернуть окно звонка, как было изначально.
Сразу после завершения звонка в ленте контакта будет создано сообщение, как при обычном звонке из карточки контакта:
Вернемся к форме задания. Если ввести что-либо, кроме результата, и нажать «Готово», то выполнится метод PostTaskForm, затем OnExternalEvent. Как мы помним, если хотя бы одно обязательное поле не заполнено, задание не завершится. В нашем случае обязательно только одно поле — результат, которое мы специально не заполнили. В результате задание останется в ленте и к нему можно будет снова «Приступить». При этом введенные значения будут сохранены.
Теперь выберем результат «Не дозвонились». Задание стало завершенным, но это временно. Мы видим, что сам БП «Выполняется».
Как только закончится пауза, оно будет поставлено вновь, при этом введенные ранее значения будут восстановлены. При этом поля документа не меняются.
Теперь введем корректные значения и выберем результат «Дозвонились». БП завершился.
Введенная информация сохранилась в поля документа (см. последний шаг БП).
В журнале БП можем видеть результат вызовов WriteToTrackingService.
Для тестирования, мы, конечно, заменили 1 час на одну минуту
Заключение
Модуль БП — мощный инструмент для автоматизации бизнес-процессов как с точки зрения пользователя, так и программиста. Возможность разрабатывать собственные действия позволяет реализовать бизнес-процессы, потенциально, любой сложности.
В данном уроке мы рассмотрели лишь создание «обычного» действия. Также существуют составные, с помощью которых можно реализовать самые разные способы управления «потоками» исполнения БП.
Также возможно подключение собственных сущностей в качестве документов БП, но это уже отдельная тема.
-----
Пишите в комментариях ваш опыт разработки своих действий.
Какие еще статьи по кастомизации коробки Б24 вы бы хотели видеть?
Материалы:
Автор статьи:
Пишите в комментариях ваш опыт разработки своих действий, что интересного делали, какие грабли ловили?
Какие еще статьи по кастомизации коробки Б24 вы бы хотели видеть?
1. Комментарии на русском? Серьезно? Я понимаю, что это конечно удобно прямо здесь и сейчас, но не более. Даете вы своему коллеге по цеху и он видит вот такое
2. Почистите ivsipcallactivity.php
Из самого последнего, но не интересного делали
* Запись в crm timeline
* Перевод в другой crm pipeline с определенным статусом
Грабли (основные для меня)
* Отсутствие динамических список (параметры)
* Практическая невозможность работы со множественными значениями переменной\параметра итп (в интерфейсе)
Неужели коллеги по цеху затрудняться конвертнуть в нужный им формат.
пока все очень ограничено
- не возможно получить контекст БП в активити
- не возможно использовать динамический набор параметров для ввода данных пользователю
- нужны нормальнокастомизируемые формы для заданий БП (формы на запрос ввода данных) - тот же динамический набор для ввода полей а не строго фиксированный
- не возможно использовать динамический набор значений параметров на входе и на выходе активити
- непонятно как установить шаблон БП автоматически - скорее всего нельзя вообще (хотя б создать из файла который предварительно экспортировали и установить заданные параметры) через rest api (т.е. например при установке внешнего приложения)
- нужно больше источников для запуска БП (с изменения добавления файла диска, изменения добавления товара CRM и т.п, изменения добавления дела CRM сущности)
- а лучше добавить rest api методы для запуска БП из кода приложения
- адекватные блоки для проверки условий и циклов и т.п. - чтоб не приходилось переносить логику БП в активити а описывать ее в шаблоне БП
БП практически не развиваются с момента создания.
А это одна из самых сильных и конкурентных возможностей в Б24
Вот бы такое в облаке повторить.
Может кто описать как корректно подготовить ADDITIONAL_RESULT чтобы можно было вернуть значения.
Добавил простой Activity где пользователь выбирает задание и какие значения должна вернуть по выбранному заданию.
Не пойму как правильно формировать массив для того чтобы все работало корректно.