Продолжаем учиться правильно кастомизировать Битрикс24
В этой статье расскажем как реализовать бизнес-процессы по собственной сущности в коробочном Битрикс24.
Слово коллегам из
В изучении этой темы вам поможет
Введение
Часто с разными сущностями системы необходимо связать какую-либо логику. В Б24 есть два способа это сделать:
- написать код;
- создать бизнес-процесс.
В данной статье расскажем как подключить бизнес-процессы к своей сущности.
В качестве «своей сущности» будем использовать торговые точки CRM, о добавлении которых в Б24 мы рассказывали
[spoiler]
Общие сведения
Для подключения бизнес-процессов к своей сущности нужно выполнить следующие задачи:
- определить типы документов и документы;
- подключить конструктор шаблонов бизнес-процессов с помощью штатных компонентов;
- подключить стандартные интерфейсы управления бизнес-процессами: список, запуск, журнал, форма задания (с помощью штатных компонентов);
- реализовать автоматический запуск бизнес-процессов при наступлении событий создания/изменения документа;
- реализовать остановку и удаление БП при удалении документа.
Модуль БП спроектирован таким образом, чтобы была возможность запускать бизнес-процессы по разным сущностям системы, даже тем, которые появятся в будущем. Чтобы достичь этой универсальности, были введены абстракции «тип документа» и «документ».
Бизнес-процесс не может выполняться без документа. Однако в самом модуле БП, «тип документа» и «документ» — абстрактные понятия, физический смысл которых не определен. Конкретные реализации предоставляют модули, которые интегрированы с БП. Например, в модуле инфоблоков, типом документа является инфоблок, а документом — элемент инфоблока; в универсальных списках — аналогично; в CRM типы документов — лид, сделка, контакт…, документы — конкретные лиды, сделки, контакты...
Благодаря тому, что все документы имеют единый интерфейс, модулю БП не нужно знать API конкретной сущности. Таким образом, эти понятия являются связующим звеном между БП и вашей сущностью.
Поскольку в разработанном ранее модуле торговых точек всего одна сущность, у нас будет один тип документа — «торговая точка».
Физически оба эти понятия в каждом модуле представлены одним классом, реализующим интерфейс IBPWorkflowDocument.
Комплексные идентификаторы
Поскольку модуль БП может работать с разными сущностями, ему необходимо отличать «определения» документа и типа документа для разных модулей. Поэтому модуль БП использует комплексные идентификаторы.
Идентификатор типа документа — это кортеж из трех элементов <M, E, D>, где:
M — код модуля, которому принадлежит тип документа,
E — класс, описывающий тип документа,
D — код типа документа.
Представление на языке PHP — последовательный массив из трех элементов, например, так представлен стандартный универсальный список «Клиенты»:
array('lists', 'Bitrix\Lists\BizprocDocumentLists', 'iblock_12') |
M — код модуля, которому принадлежит тип документа,
E — класс, описывающий тип документа,
I — непосредственный идентификатор документа.
Представление на языке PHP — последовательный массив из трех элементов, например, так представлен элемент стандартного универсального списка «Клиенты»:
array('lists', 'Bitrix\Lists\BizprocDocumentLists', 144) |
Методы модуля бизнес-процессов чаще всего принимают на вход именно такие идентификаторы. Компоненты обычно принимают три элемента идентификатора как отдельные параметры.
Интеграция с бизнес-процессами
На уровне API
Приступим к интеграции. Начнем с класса документа, так как без него не будет работать все остальное.
Класс документа будет называться \Academy\CrmStores\BizProc\StoreDocument. Он должен реализовывать интерфейс \IBPWorkflowDocument. Этот интерфейс потребует реализовать немало методов, но сначала несколько оговорок.
Во-первых, забегая вперед, конструктор бизнес-процессов — это отдельный модуль. Существуют ситуации, когда конструктор обращается к классу документа без подключения модуля бизнес-процессов, это приводит к ошибке. Поэтому перед объявлением класса необходимо убедиться, что модуль БП подключен.
Во-вторых, в интерфейсе IBPWorkflowDocument отсутствуют некоторые методы, необходимые для корректной работы БП. Один из таких методов — GetDocumentType.
namespace Academy\CrmStores\BizProc; if (! Loader::includeModule('bizproc')) { return; } class StoreDocument implements \IBPWorkflowDocument { ... } |
Теперь приступим к реализации методов.
GetDocumentType
Метод должен возвращать код типа документа для документа с указанным идентификатором. Идентификатор документа не комплексный.
Поскольку для торговых точек мы не предусматриваем разделение на «типы», возвращаем константу store.
static public function GetDocumentType($storeId) { return 'store'; } |
Метод должен возвращать описание полей документа указанного типа в виде массива, где ключ — код поля, значение — массив с параметрами:
- Name — название поля в конструкторе БП;
- Type — тип данных поля, константа класса FieldType (конструктор фильтрует поля по типу в зависимости от контекста);
- Editable — разрешено ли изменение значения поля (если нет, то поле не появится в действии «Изменение документа»);
- Required — обязательность заполнения поля (для действия «Создание документа»).
static public function GetDocumentFields ($documentType) { return array( 'ID' => array( 'Name' => Loc::getMessage('CRMSTORES_FIELD_ID'), 'Type' => FieldType::INT, 'Editable' => false, 'Required' => false ), 'NAME' => array( 'Name' => Loc::getMessage('CRMSTORES_FIELD_NAME'), 'Type' => FieldType::STRING, 'Editable' => true, 'Required' => true ), 'ADDRESS' => array( 'Name' => Loc::getMessage('CRMSTORES_FIELD_ADDRESS'), 'Type' => FieldType::STRING, 'Editable' => true, 'Required' => true ), 'ASSIGNED_BY_ID' => array( 'Name' => Loc::getMessage('CRMSTORES_FIELD_ASSIGNED_BY_ID'), 'Type' => FieldType::USER, 'Editable' => true, 'Required' => false ) ); } |
CanUserOperateDocument
С помощью этого метода БП проверяет права пользователя на выполнение операций с документом.
Аргументы:
Название | Тип данных | Описание |
$operation | int | Операция, право на выполнение которой проверяется. Могут быть переданы следующие константы: CBPCanUserOperateOperation::ViewWorkflow CBPCanUserOperateOperation::StartWorkflow CBPCanUserOperateOperation::WriteDocument CBPCanUserOperateOperation::ReadDocument |
$userId | int | Идентификатор пользователя, от имени которого предполагается выполнить операцию. |
$documentId | int | Идентификатор документа (не комплексный). |
$arParameters | array | Вспомогательные параметры, например: DocumentStates - массив состояний БП данного документа; WorkflowId - код бизнес-процесса. |
Поскольку для торговых точек мы не реализовали систему прав, будем разрешать выполнение всех операций всем пользователям.
static public function CanUserOperateDocument ($operation, $userId, $documentId, $arParameters = array()) { return true; } |
CanUserOperateDocumentType
С помощью этого метода БП проверяет права пользователя на выполнение операций с типом документа.
Аргументы те же, что и у метода CanUserOperateDocument, кроме:
Название | Тип данных | Описание |
$operation | int | Операция, право на выполнение которой проверяется. Могут быть переданы следующие константы: CBPCanUserOperateOperation::WriteDocument CBPCanUserOperateOperation::CreateWorkflow |
Поскольку для торговых точек мы не реализовали систему прав, будем разрешать выполнение всех операций всем пользователям.
static public function CanUserOperateDocumentType ($operation, $userId, $documentType, $arParameters = array()) { return true; } |
Реализации описанных выше методов достаточно для работы конструктора шаблонов бизнес-процессов. Вы уже можете перейти к следующему разделу, а затем вернуться к классу документа и закончить его реализацию.
Все следующие методы необходимы для выполнения самих бизнес-процессов. Нам понадобится несколько вспомогательных методов. Следующие 4 метода не заявлены в интерфейсе.
getComplexDocumentType
Возвращает комплексный идентификатор типа документа. Последний элемент массива должен быть взят из возможных возвращаемых значений метода GetDocumentType.
static public function getComplexDocumentType() { return array('academy.crmstores', self::class, 'store'); } |
getComplexDocumentId
Генерирует комплексный идентификатор документа.
static public function getComplexDocumentId($storeId) { return array('academy.crmstores', self::class, $storeId); } |
convertStoreToBp
Модуль БП требует несколько иного представления полей документа. Например, в БД мы храним идентификатор ответственного за торговую точку в виде числа. Модуль БП требует префикс user_ перед идентификатором пользователя.
Данный метод выполняет преобразование данных, полученных из БД в формат, требуемый модулем БП.
static private function convertStoreToBp($store) { if (isset($store['ASSIGNED_BY_ID'])) { $store['ASSIGNED_BY_ID'] = 'user_' . $store['ASSIGNED_BY_ID']; } return $store; } |
convertStoreFromBp
Данный метод выполняет преобразование, обратное тому, что делает convertStoreToBp.
static private function convertStoreFromBp ($store) { if (isset($store['ASSIGNED_BY_ID'])) { $store['ASSIGNED_BY_ID'] = str_replace('user_', '', $store['ASSIGNED_BY_ID']); } return $store; } |
Вернемся к реализации методов интерфейса IBPWorkflowDocument.
GetDocument
Метод должен возвращать значения тех полей, которые заявлены в методе GetDocumentFields. Идентификатор документа — не комплексный.
static public function GetDocument ($documentId) { $dbStore = StoreTable::getById($documentId); $store = $dbStore->fetch(); return self::convertStoreToBp($store); } |
CreateDocument
Создает новый документ. Используется действием «Создание документа». $parentDocumentId — идентификатор документа, по которому выполняется бизнес-процесс, вызвавший создание нового документа.
static public function CreateDocument ($parentDocumentId, $arFields) { $result = StoreTable::add(self::convertStoreFromBp($arFields)); return $result->getId(); } |
UpdateDocument
Изменяет значений полей документа. Используется действием «Изменение документа». Идентификатор — не комплексный.
static public function UpdateDocument ($documentId, $arFields) { StoreTable::update($documentId, self::convertStoreFromBp($arFields)); } |
DeleteDocument
Удаляет документ с указанным идентификатором. Идентификатор — не комплексный.
static public function DeleteDocument ($documentId) { StoreTable::delete($documentId); } |
GetDocumentAdminPage
Метод должен возвращать URL страницы управления документом.
Примеры:
- для элемента инфоблока генерируется ссылка на форму редактирования элемента в административной панели;
- для сущности CRM генерируется ссылка на карточку сущности в режиме просмотра.
В нашем случае вернем ссылку на карточку торговой точки, аналогично CRM. URL генерируется из шаблона, который задан в настройках модуля.
static public function GetDocumentAdminPage ($documentId) { return \CComponentEngine::makePathFromTemplate( Option::get('academy.crmstores', 'STORE_DETAIL_TEMPLATE'), array( 'STORE_ID' => $documentId )); } |
GetAllowableUserGroups
Метод возвращает логические группы пользователей, имеющие смысл только в рамках бизнес-процесса и/или документа.
Отстраненный синтетический пример. Если бы мы запускали бизнес-процесс по задаче, то мы могли бы определить группу «Все участники задачи», в которую входили бы постановщик, ответственные, наблюдатели, а также все, кто оставлял комментарии.
Для торговых точек (в качестве примера) определим две группы:
- Ответственный — включает одного пользователя, указанного в соответствующем поле;
- Администраторы — все участники группы «Администраторы».
static public function GetAllowableUserGroups ($documentType) { $dbAdminGroup = GroupTable::getById(1); $adminGroup = $dbAdminGroup->fetch(); return array( 'Author' => Loc::getMessage('CRMSTORES_GROUP_AUTHOR'), 'group_1' => $adminGroup['NAME'] ); } |
GetUsersFromUserGroup
Метод должен возвращать массив идентификаторов пользователей, входящих в указанную группу. На вход методу подаются группы, которые возвращает метод GetAllowableUserGroups. Идентификаторы пользователей — числовые, без префикса user_.
static public function GetUsersFromUserGroup ($group, $documentId) { $group = strtolower($group); if ($group == 'author') { if (intval($documentId) > 0) { $dbStore = StoreTable::getById($documentId); $store = $dbStore->fetch(); return array( $store['ASSIGNED_BY_ID'] ); } else { return array(); } } $groupId = intval(str_replace('group_', '', $group)); if ($groupId <= 0) { return array(); } return \CGroup::GetGroupUser($groupId); } |
Следующие методы необходимы для работы специфической функциональности. Реализовывать их не обязательно.
GetDocumentForHistory
RecoverDocumentFromHistory
Эти методы используются действием «Сохранение истории», которое позволяет сохранить все данные документа в некоторое хранилище истории, из которого впоследствии можно восстановить документ.
Это действие доступно только для модуля инфоблоков, поэтому реализовывать его необязательно.
PublishDocument
UnpublishDocument
Эти методы используются действиями «Публикация документа» и «Снятие документа с публикации». Если понятие публикации не применимо к вашему документу, можно не реализовывать эти методы.
Например, для элементов инфоблоков это понятие определено. Считается, что в публичной части появляются только те элементы, которые опубликованы.
Другой пример: в CRM нет разделения на публичный и административный интерфейс для работы с лидами, сделками и т. д. Элементы CRM не могут быть опубликованы, поэтому данные методы не реализованы.
Для торговых точек мы так же не предусматриваем публикацию. Поэтому методы будут возвращать false.
LockDocument
UnlockDocument
IsDocumentLocked
Данные методы используются действиями «Блокировка документа» и «Разблокировка документа». Эти действия нужны в том случае, если предполагается одновременное выполнение нескольких бизнес-процессов над одним документом и стоит задача сделать так, чтобы в некоторые моменты времени доступ к документу был только у одного бизнес-процесса.
На практике такая ситуация встречается редко, поэтому эти методы можно не реализовывать. Например, для CRM блокировки не поддерживаются.
Если вы решили оставить эти методы без реализации, то IsDocumentLocked должен возвращать false (в противном случае, БП «зависнет»), а остальные — true.
static public function LockDocument ($documentId, $workflowId) { return true; } static public function UnlockDocument ($documentId, $workflowId) { return true; } static public function IsDocumentLocked ($documentId, $workflowId) { return false; } |
GetAllowableOperations
Этот метод должен возвращать список операций, которые можно совершать с документом. В шаблоне БП со статусами для каждого статуса можно задать пользователей, которые имеют право выполнять эти операции. Это нужно, чтобы изменять права доступа к документу по мере перехода по статусам.
Права БП и их отображение на права доступа к документу — непростая тема, которую в данной статье затрагивать не будем.
Для торговых точек оставим метод без реализации, возвращать будем пустой массив.
static public function GetAllowableOperations ($documentType) { return array(); } |
В целом, на уровне API интеграция завершена, хотя некоторые небольшие правки мы внесем позже. Техническая возможность запустить БП уже есть, однако отсутствуют интерфейсы для разработки шаблонов и управления бизнес-процессами.
Конструктор шаблонов БП
Теперь необходимо дать пользователям возможность разрабатывать шаблоны бизнес-процессов. Понадобится создать два интерфейса: список шаблонов БП и конструктор БП. Будем использовать для этого штатные компоненты.
Технически сделаем эти интерфейсы страницами комплексного компонента торговых точек. Добавим две страницы в шаблон и зададим шаблоны адресов по-умолчанию.
В class.php компонента stores.
const SEF_DEFAULT_TEMPLATES = array( 'details' => '#STORE_ID#/', 'edit' => '#STORE_ID#/edit/', 'bizproc_workflow_admin' => 'bp_list/', // Список шаблонов БП. 'bizproc_workflow_edit' => 'bp_edit/#ID#/' // Конструктор. ); |
Добавим переход на страницу списка шаблонов со страницы списка. Для этого добавим следующие параметры в компонент crm.interface.toolbar в файле list.php (страница шаблона компонента stores).
$APPLICATION->IncludeComponent('bitrix:crm.interface.toolbar', 'title', array( 'TOOLBAR_ID' => 'CRMSTORES_TOOLBAR', 'BUTTONS' => array( array( 'TEXT' => Loc::getMessage('CRMSTORES_ADD'), 'TITLE' => Loc::getMessage('CRMSTORES_ADD'), 'LINK' => CComponentEngine::makePathFromTemplate( $urlTemplates['EDIT'], array( 'STORE_ID' => 0 )), 'ICON' => 'btn-add' ), array( 'NEWBAR' => true ), array( 'TEXT' => Loc::getMessage('CRMSTORES_CONFIGURE_WORKFLOWS'), 'TITLE' => Loc::getMessage('CRMSTORES_CONFIGURE_WORKFLOWS'), 'LINK' => $urlTemplates['BP_LIST'] ) ) ), $this->getComponent(), array( 'HIDE_ICONS' => 'Y' ) ); |
Все кнопки, объявленные после array('NEWBAR' => true), будут выведены как одна кнопка «шестеренка» с выпадающим меню. URL строится на основе параметров ЧПУ.
Страница списка шаблонов БП
Рассмотрим страницу списка шаблонов БП (bizproc_workflow_admin.php). В самом начале вызывается компонент верхнего меню CRM (crm.control_panel) и добавляется ссылка для возврата к списку. Мы уже рассматривали их в предыдущих статьях.
Далее мы вызываем два компонента — панель инструментов с кнопками создания нового шаблона (со статусами и последовательного) и непосредственно список шаблонов.
$APPLICATION->IncludeComponent('bitrix:main.interface.toolbar', '', array( 'BUTTONS' => array( array( 'TEXT' => Loc::getMessage('CRMSTORES_NEW_BP_STATEMACHINE'), 'TITLE' => Loc::getMessage('CRMSTORES_NEW_BP_STATEMACHINE'), 'LINK' => CComponentEngine::makePathFromTemplate( $urlTemplates['BP_EDIT_STATEMACHINE'], array( 'ID' => 0 )), 'ICON' => 'btn-new' ), array( 'TEXT' => Loc::getMessage('CRMSTORES_NEW_BP_SEQUENTAL'), 'TITLE' => Loc::getMessage('CRMSTORES_NEW_BP_SEQUENTAL'), 'LINK' => CComponentEngine::makePathFromTemplate( $urlTemplates['BP_EDIT'], array( 'ID' => 0 )), 'ICON' => 'btn-new' ) ) ) ); |
Ссылки строятся на основе настроек ЧПУ (обе ведут на страницу конструктора). В ссылку на создание БП со статусами добавлен GET-параметр init=statemachine, который используется компонентом конструктора и указывает тип шаблона.
Далее следует вызов компонента списка.
$APPLICATION->IncludeComponent('bitrix:bizproc.workflow.list', '.default', array( 'MODULE_ID' => 'academy.crmstores', 'ENTITY' => StoreDocument::class, 'DOCUMENT_ID' => 'store', 'CREATE_DEFAULT_TEMPLATE' => 'N', 'EDIT_URL' => $editUrlTemplate, 'SET_TITLE' => 'N', 'TARGET_MODULE_ID' => 'academy.crmstores' ), $this->getComponent() ); |
Первые три параметра — комплексный идентификатор типа документа. EDIT_URL — шаблон ссылки на редактирование шаблона БП (должен содержать подстановку #ID#, которую компонент списка заменит на настоящий идентификатор).
Теперь, если перейти на эту страницу, можно увидеть следующее.
После создания шаблонов, они будут появляться в этом списке.
Страница конструктора шаблонов БП
На странице конструктора вызовем компонент bizproc.workflow.edit.
$urlTemplates = array( 'BP_EDIT' => $arResult['SEF_FOLDER'] . $arResult['SEF_URL_TEMPLATES']['bizproc_workflow_edit'], 'BP_LIST' => $arResult['SEF_FOLDER'] . $arResult['SEF_URL_TEMPLATES']['bizproc_workflow_admin'] ); $APPLICATION->IncludeComponent('bitrix:bizproc.workflow.edit', '', array( 'MODULE_ID' => 'academy.crmstores', 'ENTITY' => StoreDocument::class, 'DOCUMENT_TYPE' => 'store', 'ID' => (int) $arResult['VARIABLES']['ID'], 'EDIT_PAGE_TEMPLATE' => $urlTemplates['BP_EDIT'], 'LIST_PAGE_URL' => $urlTemplates['BP_LIST'], 'SHOW_TOOLBAR' => 'Y', 'SET_TITLE' => 'Y' ), $this->getComponent() ); |
Первые три параметра компонента — комплексный идентификатор типа документа. ID — идентификатор редактируемого шаблона, если 0 — создание нового. Шаблоны ссылок взяты из настроек ЧПУ.
Однако вызова этого компонента для работы конструктора недостаточно. Потребуется создать три служебных скрипта, используемых для отображения окон настроек шаблона, настроек действия и т. п. Скрипты должны располагаться в /bitrix/admin и подключать скрипты модуля бизнес-процессов. Названия скриптов образуются следующим образом:
- <код модуля>_bizproc_activity_settings.php;
- <код модуля>_bizproc_selector.php;
- <код модуля>_bizproc_wf_settings.php.
В начале каждого файла необходимо определить две константы — код модуля и класс документа.
academy.crmstores_bizproc_activity_settings.php
define('MODULE_ID', 'academy.crmstores'); define('ENTITY', '\Academy\CrmStores\BizProc\StoreDocument'); $fp = $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/bizprocdesigner/admin/bizproc_activity_settings.php'; if (is_file($fp)) { require ($fp); } |
bitrix/admin/academy.crmstores_bizproc_selector.php
define('MODULE_ID', 'academy.crmstores'); define('ENTITY', '\Academy\CrmStores\BizProc\StoreDocument'); $fp = $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/bizproc/admin/bizproc_selector.php'; if (is_file($fp)) { require ($fp); } |
bitrix/admin/academy.crmstores_bizproc_wf_settings.php
define('MODULE_ID', 'academy.crmstores'); define('ENTITY', '\Academy\CrmStores\BizProc\StoreDocument'); $fp = $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/bizprocdesigner/admin/bizproc_wf_settings.php'; if (is_file($fp)) { require($fp); } |
На этом подключение конструктора БП завершено.
Создадим тестовый БП, который будем использовать далее для отладки запуска. БП состоит из одного действия — «Уведомление пользователя», которое отправляет уведомление с названием торговой точки (если поля документа описаны правильно в методе GetDocumentFields, вы будете видеть их в окне вставки значения). У этого шаблона БП установлен флаг «Автоматически запускать при добавлении».
Автоматический запуск БП
В модуле БП есть функция автоматического запуска бизнес-процессов при наступлении некоторых событий в результате операций с документом. Поддерживается запуск при:
- создании нового документа;
- изменении существующего документа.
Устоявшаяся практика — запускать БП только если причиной добавления или изменения документа являются соответствующие действия пользователя. То есть, например, при создании документа через API (StoreTable::add) запуск БП происходить не должен. Вы можете это проверить на любой сущности системы.
Таким образом, код запуска бизнес-процессов разместим в компоненте редактирования торговой точки (store.edit). Модифицируем метод processSave: добавим выходной параметр $eventType, который будет сообщать о выполненном действии — добавление или изменение. Для этого заведены две константы в классе компонента: EVENT_UPDATED и EVENT_CREATED.
private function processSave($initialStore, &$eventType = false) { ... if (! empty($store['ID'])) { $result = StoreTable::update($store['ID'], $store); $eventType = self::EVENT_UPDATED; } else { $result = StoreTable::add($store); $eventType = self::EVENT_CREATED; } ... } |
Добавим метод, который запускает бизнес-процессы. Мы не можем запускать бизнес-процессы из метода processSave, т. к. эта задача находится за рамками его зоны ответственности.
private static function startBp($event, $id) { if (! Loader::includeModule('bizproc')) { return array(); } static $eventMap = array( self::EVENT_CREATED => CBPDocumentEventType::Create, self::EVENT_UPDATED => CBPDocumentEventType::Edit ); CBPDocument::AutoStartWorkflows(StoreDocument::getComplexDocumentType(), $eventMap[$event], StoreDocument::getComplexDocumentId($id), array(), $errors); return $errors; } |
Обратите внимание, что модуль БП может быть не установлен. В этом случае нужно просто не запускать БП, а не останавливать сохранение данных. По этой причине мы не можем использовать константы класса CBPDocumentEventType, т. к. не уверены в доступности всего модуля.
Метод CBPDocument::AutoStartWorkflows самостоятельно выбирает шаблоны БП, которые нужно запустить и запускает БП. Параметры метода:
Название | Тип данных | Описание |
$documentType | array | Комплексный идентификатор типа документа. |
$autoExecute | int | Операция, совершенная с документом. Одна из констант: - CBPDocumentEventType::Create - CBPDocumentEventType::Edit |
$documentId | array | Комплексный идентификатор документа. |
$arParameters | array | Значения параметров шаблонов БП. |
$arErrors | [out] array | Выходной параметр. Массив ошибок. |
Наконец, модифицируем метод executeComponent, в котором вызовем startBp.
public function executeComponent() { ... if (self::isFormSubmitted()) { $savedStoreId = $this->processSave($store, $saveEventType); if ($savedStoreId > 0) { self::startBp($saveEventType, $savedStoreId); LocalRedirect($this->getRedirectUrl($savedStoreId)); } $submittedStore = $this->getSubmittedStore(); $store = array_merge($store, $submittedStore); } ... } |
Создадим новую торговую точку. Должен запуститься тестовый БП, который отправляет уведомление с названием торговой точки.
В результате интеграции с модулем БП мы получаем еще один способ создания торговой точки — действие «Создание документа» и изменения — действие «Изменение документа». Считается, что при выполнении этих действий также должен происходить запуск бизнес-процессов.
Необходимо добавить запуск БП в два метода документа — CreateDocument и UpdateDocument.
static public function CreateDocument ($parentDocumentId, $arFields) { $result = StoreTable::add(self::convertStoreFromBp($arFields)); if ($result->isSuccess()) { \CBPDocument::AutoStartWorkflows(self::getComplexDocumentType(), \CBPDocumentEventType::Create, self::getComplexDocumentId($result->getId()), array(), $errors); } return $result->getId(); } static public function UpdateDocument ($documentId, $arFields) { $result = StoreTable::update($documentId, self::convertStoreFromBp($arFields)); if ($result->isSuccess()) { \CBPDocument::AutoStartWorkflows(self::getComplexDocumentType(), \CBPDocumentEventType::Edit, self::getComplexDocumentId($documentId), array(), $errors); } } |
Управление бизнес-процессами в интерфейсах сущности
Пользователям необходимо предоставить возможность мониторинга и запуска новых бизнес-процессов. Для этого потребуется создать несколько стандартных интерфейсов.
Компонент | Интерфейс/Функция |
bizproc.document |
|
bizproc.workflow.start |
|
bizproc.log |
|
bizproc.task |
|
Компоненты БП будем вызывать в компоненте карточки торговой точки (store.show), а полученный HTML-код передавать в шаблон.
Начнем с добавления вкладки в шаблоне карточки торговой точки.
if (! empty($arResult['BIZPROC_TAB'])) { $tabs[] = array( 'id' => $arResult['BIZPROC_TAB']['ID'], 'name' => Loc::getMessage('CRMSTORES_TAB_BP_NAME'), 'title' => Loc::getMessage('CRMSTORES_TAB_BP_TITLE'), 'fields' => array( array( 'id' => 'WORKFLOW_VIEW', 'colspan' => true, 'type' => 'custom', 'value' => $arResult['BIZPROC_TAB']['HTML'] ) ) ); } |
В классе компонента, в методе executeComponent значение BIZPROC_TAB формируется следующим образом:
$this->arResult = array( ... 'BIZPROC_TAB' => array( 'ID' => self::BIZPROC_TAB_ID, 'HTML' => $this->getBizprocTabHtml($store['ID']) ) ); |
private function getBizprocTabHtml ($storeId) { if (! Loader::includeModule('bizproc')) { return null; } $context = Context::getCurrent(); $request = $context->getRequest(); $workflowIdLog = $request->get('bizproc_log'); $workflowIdTask = $request->get('bizproc_task'); $workflowStart = $request->get('bizproc_start'); if (! empty($workflowIdLog)) { return $this->getWorkflowLogHtml($storeId, $workflowIdLog); } elseif (! empty($workflowIdTask)) { return $this->getWorkflowTaskHtml($workflowIdTask); } elseif ($workflowStart == 'Y') { return $this->getWorkflowStartHtml($storeId); } else { return $this->getWorkflowListHtml($storeId); } } |
Когда GET-параметров нет, запускается метод getWorkflowListHtml. Он вызывает компонент bizproc.document, который выводит список запущенных БП, а также несколько ссылок.
private function getWorkflowListHtml($storeId) { global $APPLICATION; ... ob_start(); $APPLICATION->IncludeComponent('bitrix:bizproc.document', '', array( 'MODULE_ID' => 'academy.crmstores', 'ENTITY' => StoreDocument::class, 'DOCUMENT_TYPE' => 'store', 'DOCUMENT_ID' => $storeId, 'TASK_EDIT_URL' => $taskUrlTemplate, 'WORKFLOW_LOG_URL' => $logUrlTemplate, 'WORKFLOW_START_URL' => $startUrl, 'POST_FORM_URI' => '', 'back_url' => $requestUri->getUri(), 'SET_TITLE' => 'N' ), $this, array( 'HIDE_ICONS' => 'Y' )); return ob_get_clean(); } |
Первые 4 параметра образуют комплексные идентификаторы документа и типа документа.
Следующие параметры — шаблоны ссылок, которые выводит компонент:
- TASK_EDIT_URL — на форму текущего задания;
- WORKFLOW_LOG_URL — на журнал бизнес-процесса;
- WORKFLOW_START_URL — на форму запуска БП;
- back_url — ссылка для редиректа после удаления БП.
private function getWorkflowListHtml ($storeId) { ... $context = Context::getCurrent(); $request = $context->getRequest(); $requestUri = new Uri($request->getRequestUri()); $requestUri->addParams( array( self::FORM_ID . '_active_tab' => self::BIZPROC_TAB_ID )); $requestUri->deleteParams( array( 'action', 'back_url', 'bizproc_log', 'bizproc_task', 'bizproc_start' )); $logUrlTemplate = CHTTP::urlAddParams($requestUri->getUri(), array( 'bizproc_log' => '#ID#' )); $taskUrlTemplate = CHTTP::urlAddParams($requestUri->getUri(), array( 'bizproc_task' => '#ID#' )); $startUrl = CHTTP::urlAddParams($requestUri->getUri(), array( 'bizproc_start' => 'Y' )); } |
Параметр #ID# используется компонентом bizproc.document, он будет заменен на настоящий идентификатор.
При вызове этого компонента вы должны увидеть следующее.
При переходе по ссылке «Запустить новый бизнес-процесс», GET-параметр bizproc_start будет равен Y, в этом случае запустим другой компонент.
private function getWorkflowStartHtml($storeId) { global $APPLICATION; $context = Context::getCurrent(); $request = $context->getRequest(); ob_start(); $APPLICATION->IncludeComponent( 'bitrix:bizproc.workflow.start', '', array( 'MODULE_ID' => 'academy.crmstores', 'ENTITY' => StoreDocument::class, 'DOCUMENT_TYPE' => 'store', 'DOCUMENT_ID' => $storeId, 'TEMPLATE_ID' => $request->get('workflow_template_id'), 'SET_TITLE' => 'N' ), $this, array('HIDE_ICONS' => 'Y') ); return ob_get_clean(); } |
Теперь при переходе по ссылке «Запустить новый бизнес-процесс» можно увидеть список. При клике по названию шаблона БП запускается.
Если задано значение GET-параметра bizproc_log, запустим компонент bizproc.log, который выводит журнал выполнения БП.
private function getWorkflowLogHtml($storeId, $workflowId) { global $APPLICATION; ob_start(); $APPLICATION->IncludeComponent( 'bitrix:bizproc.log', '', array( 'MODULE_ID' => 'academy.crmstores', 'ENTITY' => StoreDocument::class, 'DOCUMENT_TYPE' => 'store', 'COMPONENT_VERSION' => 2, 'DOCUMENT_ID' => $storeId, 'ID' => $workflowId, 'SET_TITLE' => 'N', 'INLINE_MODE' => 'Y', 'AJAX_MODE' => 'N', 'NAME_TEMPLATE' => CSite::GetNameFormat() ), $this, array('HIDE_ICONS' => 'Y') ); return ob_get_clean(); } |
Теперь при переходе по ссылке «журнал», открывается список событий, произошедших во время выполнения БП. Для нашего простого БП журнал пуст, так как действие «Уведомление пользователя» не добавляет записей в журнал.
Когда задано значение GET-параметра bizproc_task, запустим компонент bizproc.task, который выводит форму задания БП.
private function getWorkflowTaskHtml($taskId) { global $APPLICATION; $context = Context::getCurrent(); $request = $context->getRequest(); ob_start(); $APPLICATION->IncludeComponent( 'bitrix:bizproc.task', '', array( 'TASK_ID' => $taskId, 'USER_ID' => 0, 'WORKFLOW_ID' => '', 'DOCUMENT_URL' => $request->getRequestUri(), 'SET_TITLE' => 'N', 'SET_NAV_CHAIN' => 'N' ), $this, array('HIDE_ICONS' => 'Y') ); return ob_get_clean(); } |
Бизнес-процесс «Утверждение названия». Простой БП, который ставит задание на ввод утвержденного названия торговой точки, а затем сохраняет его. Задание выполняет пользователь, указанный в параметре шаблона «Ответственный».
Запустим БП из карточки CRM.
Компонент bizproc.workflow.start перед непосредственно запуском просит задать значения параметров.
В списке БП видно, что поставлено задание.
При переходе по ссылке с названием задания должна появиться форма для его выполнения.
Обратите внимание, что задания также появляются в разделе «Бизнес-процессы» (см. меню слева).
Запуск БП из списка
В интерфейсе CRM также предусмотрен запуск БП из списка (контекстное меню строки). Например:
Для реализации этого способа запуска будем использовать библиотеку bp_starter. Для запуска БП достаточно вызвать один метод: BX.Bizproc.Starter.singleStart.
Пункты меню формируются в шаблоне компонента списка (stores.list). Воспользуемся методом CBPDocument::GetWorkflowTemplatesForDocumentType, чтобы получить список шаблонов БП и передадим этот список в шаблон. На вход метод принимает комплексный идентификатор типа документа.
public function executeComponent () { $this->arResult = array( 'WORKFLOW_TEMPLATES' => $this->getWorkflowTemplates() ); $this->includeComponentTemplate(); } private function getWorkflowTemplates () { if (! Loader::includeModule('bizproc')) { return null; } return CBPDocument::GetWorkflowTemplatesForDocumentType(StoreDocument::getComplexDocumentType()); } |
Подключим библиотеку bp_starter с помощью класса CJSCore.
CJSCore::Init('bp_starter');
Сформируем пункты меню для запуска БП.
if (! empty($arResult['WORKFLOW_TEMPLATES'])) { $startWorkflowActions = array(); foreach ($arResult['WORKFLOW_TEMPLATES'] as $workflowTemplate) { $starterParams = array( 'moduleId' => 'academy.crmstores', 'entity' => StoreDocument::class, 'documentType' => 'store', 'documentId' => $store['ID'], 'templateId' => $workflowTemplate['ID'], 'templateName' => $workflowTemplate['NAME'], 'hasParameters' => $workflowTemplate['HAS_PARAMETERS'] ); $callback = sprintf('function () { BX.Main.gridManager.reload(%s); }', Json::encode($arResult['GRID_ID'])); $startWorkflowActions[] = array( 'TITLE' => $workflowTemplate['DESCRIPTION'], 'TEXT' => $workflowTemplate['NAME'], 'ONCLICK' => sprintf('BX.Bizproc.Starter.singleStart(%s, %s)', Json::encode($starterParams), $callback) ); } $rowActions[] = array( 'SEPARATOR' => true ); $rowActions[] = array( 'TITLE' => Loc::getMessage('CRMSTORES_ACTION_START_WORKFLOW_TITLE'), 'TEXT' => Loc::getMessage('CRMSTORES_ACTION_START_WORKFLOW_TEXT'), 'MENU' => $startWorkflowActions ); } |
Метод BX.Bizproc.Starter.singleStart принимает на вход два аргумента — параметры БП и callback-функцию, которая будет вызвана после запуска БП.
Параметры запуска — JS-объект, включающий комплексные идентификаторы документа и типа документа, а также некоторые параметры шаблона БП, которые были получены ранее.
В качестве callback-функции мы вызываем BX.Main.gridManager.reload, которая обновляет грид без перезагрузки страницы. Обновление необходимо, т. к. БП мог внести изменения в существующие торговые точки, создать или удалить новые и т. п. Вам также это может понадобиться, если вы выводите состояние БП и ссылки на задания в списке (как это уже сделано в сделках).
Чтобы сделать пункт меню с вложенным меню, вместо ONCLICK передаем массив дочерних пунктов меню с ключем MENU.
Бизнес-процессы без параметров (см. $workflowTemplate['HAS_PARAMETERS']) запускаются сразу. Для БП с параметрами выводится всплывающее окно для задания значений параметров. Например, для БП «Утверждение названия»:
Об удалении документа
Как уже говорилось ранее, модуль БП самостоятельно не отслеживает события сущности. В случае удаления документа, все бизнес-процессы, выполняющиеся по нему, должны быть остановлены и удалены. Причем важно это сделать при удалении документа любым из доступных способов, т. к. оставшиеся после удаления документы БП могут привести к ошибкам.
Самый удобный способ это сделать — определить метод onAfterDelete в классе сущности (StoreTable).
public static function onAfterDelete (Event $event) { $primary = $event->getParameter('primary'); $id = $primary['ID']; if (Loader::includeModule('bizproc')) { \CBPDocument::OnDocumentDelete(StoreDocument::getComplexDocumentId($id), $bpErrors); } } |
Чтобы убедиться, что БП действительно удаляются, проверьте таблицу b_bp_workflow_state. Столбцы MODULE_ID, ENTITY, DOCUMENT_ID образуют комплексный идентификатор документа.
Установщик модуля
Процесс установки модуля почти не изменился. Мы лишь добавили три служебных скрипта для конструктора, которые устанавливаются в каталог /bitrix/admin.
Если у вас возникнет необходимость установить шаблоны бизнес-процессов во время установки модуля, вы можете воспользоваться функцией импорта шаблонов. Пример для торговы точек:
CBPWorkflowTemplateLoader::ImportTemplate( 0, StoreDocument::getComplexDocumentType(), CBPDocumentEventType::Create, 'Предустановленный шаблон', 'Описание предустановленного шаблона', file_get_contents('template.bpt'), null, true ); |
Аргументы метода:
Название | Тип данных | Описание |
$id | int | Идентификатор шаблона. Если 0 — импортируется как новый, если задан — заменяет существующий. |
$documentType | array | Комплексный идентификатор типа документа. |
$autoExecute | int | Тип автозапуска шаблона. 0 — не запускать автоматически. Используйте константы класса CBPDocumentEventType. |
$name | string | Название шаблона. |
$description | string | Описание шаблона. |
$datum | string | Содержимое файла экспортированного шаблона (как правило, строка с двоичными данными). |
$systemCode | string | Символьный код шаблона. |
$systemImport | boolean | Отключает некоторые проверки, необязательные во время установки. В частности, если true, шаблоны будут установлены от имени администратора системы. |
При необходимости сделать локализацию в шаблонах, потребуется разработать несколько версий одного шаблона (для каждого языка).
Заключение
Модуль БП — мощный инструмент для автоматизации бизнес-процессов как с точки зрения пользователя, так и программиста. Возможность подключить бизнес-процессы к своим сущностям позволяет автоматизировать обработку документов без привлечения программиста, оперативно изменять эту логику, а также разделить ответственность между разработчиками.
Мы ранее рассматривали разработку собственных действий для
-----
Спасибо за внимание
Материалы:
Автор статьи:
Фото:
По умолчанию в БП доступны переменные типов которые описаны в CBPHelper::GetDocumentFieldTypes(); Но для поля типа список нельзя установить варианты значений . Оказалось проблема в отсутствии в описании этого типа параметра "Complex" => true. Следующий код нужно добавить в класс реализующий интерфейс IBPWorkflowDocument
Так же можно добавить другие типа полей, пример можно посмотреть в методе CCrmDocument::GetDocumentFieldTypes($documentType)
...
В итоге куча реализаций одних и тех же типов в разных модулях кто во что горазд и невозможность полноценного использования в других местах.
Те же захардкоженные в модуле БП типы данных и "Complex", который вообще позволяет хранить и использовать настройки типов в шаблоне процесса в "Options", отличный тому пример. "Комплекс" для штатных полей - только для enum и привязки к элементам, всё остальное - никаких настроек, кроме значения по умолчанию, которое и то не всегда работает.
Скажу даже больше - дизайнер бизнес-процессов и шаблоны БП вполне позволяют сделать так:
И да, успешно хранить и использовать такие настройки типа в его Options в шаблоне для типов Complex. Поля с полноценными настройками в БП возможны, для этого не требуется даже изменений ни в дизайнер бп, ни в структуру шаблонов!
Только вот штатно доступен только куцый дефолт...
В CRM есть прекрасный пример - вызов компонента: bitrix:bizproc.document, который сам предоставит возможности:
1) Списка запущенных БП, статусов, истории (журнала)
2) Возможность запуска БП, удаления
Все что нужно - вызов с "магическими" параметрами:
Но возник вопрос. А как быть с файлами
[*]<код модуля>_bizproc_activity_settings.php;
[*]<код модуля>_bizproc_selector.php;
[*]<код модуля>_bizproc_wf_settings.php.
/bitrix/admin/disk_bizproc_activity_settings.php
/bitrix/admin/disk_bizproc_selector.php
bitrix/admin/disk_bizproc_wf_settings.php