Более новая версия доступна в разделе документации.

Предисловие

Данный документ является руководством по применению подсистемы исполнения бизнес-процессов (business process management, BPM) платформы CUBA.

Предполагается, что читатель ознакомлен с Руководством по разработке приложений, доступным по адресу https://www.cuba-platform.ru/manual.

Подсистема исполнения бизнес-процессов CUBA основана на фреймворке Activiti, поэтому знакомство с его устройством будет полезным. См. http://www.activiti.org. Исполняемый процесс описывается в нотации BPMN 2.0, поэтому разработчик должен быть знаком с данной нотацией. См. http://www.bpmn.org.

Если у Вас имеются предложения по улучшению данного руководства, обратитесь пожалуйста в службу поддержки по адресу http://www.cuba-platform.ru/support. При обнаружении ошибки в документации укажите, пожалуйста, номер главы и приведите небольшой участок окружающего текста для облегчения поиска.

1. Быстрый старт

В данной главе рассмотрим создание небольшого проекта, демонстрирующего работу с бизнес-процессами. Задача - реализовать процесс согласования договора. Процесс согласования выглядит следующим образом:

  • Пользователь создает объект Contract, назначает участников процесса и запускает процесс согласования.

  • Участник с ролью Controller получает задачу проверить приложенный договор на корректность заполнения.

  • Если проверка пройдена, то договор попадает к нескольким пользователям с ролью Manager, если нет, то процесс завершается, а договору проставляется статус Not valid.

  • После утверждения или отклонения договора менеджерами договор принимает состояния Approved или Not approved.

1.1. Создание проекта

  1. Создайте новый проект в Cuba Studio:

    • Project name: bpm-demo

    • Project namespace: demo

    • Root package: com.company.bpmdemo

StudioNewProject
  1. Откройте окно редактирования свойств проекта (секция Project properties, кнопка Edit).

  2. В группе App components подключите компонент bpm.

StudioSelectBpmModule
  1. Нажмите кнопку OK в окне редактирования свойств проекта. Система запросит подтверждение перезаписи скриптов сборки. Соглашаемся.

1.2. Создание модели данных

Перейдите на вкладку Data model и нажмите New entity. Имя класса: Contract.

CreateContractEntity

Создайте следующие атрибуты сущности:

  • number (тип String)

  • date (тип Date)

  • state (тип String)

ContractEntityAttributes

Перейдите на вкладку Instance name и добавьте атрибут number в Name pattern attributes.

ContractEntityNamePattern

Сохраните сущность, нажав кнопку OK.

1.3. Создание стандартных экранов

В секции Data model панели навигатора выделите сущность Contract и нажмите кнопку New > Generic UI screen. В списке шаблонов выделите Entity browser and editor screens и нажмите Create. Закройте список шаблонов.

CreateContractScreens

1.4. Бин ApprovalHelper

Метод updateState() бина ApprovalHelper будет вызываться из процесса согласования для установки состояния договора.

Параметры метода:

  • entityId - идентификатор сущности договора

  • state - состояние договора

Откройте проект в IDE. Простой способ сделать это - воспользоваться кнопкой IDE из какой-либо секции навигатора Studio, например, Project properties.

Создайте класс ApprovalHelper в пакете com.company.bpmdemo.core.

ApprovalHelperProjectTree

ApprovalHelper.java:

package com.company.bpmdemo.core;

import org.springframework.stereotype.Component;
import com.company.bpmdemo.entity.Contract;
import com.haulmont.cuba.core.Persistence;
import com.haulmont.cuba.core.Transaction;

import javax.inject.Inject;
import java.util.UUID;

@Component("demo_ApprovalHelper")
public class ApprovalHelper {

    @Inject
    private Persistence persistence;

    public void updateState(UUID entityId, String state) {
        try (Transaction tx = persistence.getTransaction()) {
            Contract contract = persistence.getEntityManager().find(Contract.class, entityId);
            if (contract != null) {
                contract.setState(state);
            }
            tx.commit();
        }
    }
}

1.5. Создание базы данных и запуск приложения

В Studio в секции Data model навигатора нажмите на Generate DB scripts. В открывшемся окне Database scripts нажмите кнопку Create database.

StudioCreateDatabase

Запустите сервер, выполнив команду Run → Start application server.

Откройте приложение в браузере по адресу http://localhost:8080/app или кликните на ссылку в панели статуса Studio.

1.6. Создание процесса

1.6.1. Создание модели процесса

Конечная версия модели процесса будет выглядеть следующим образом:

ProcessFull

Рассмотрим последовательность шагов для создания модели.

В веб-интерфейсе запущенного приложения откройте экран BPM → Process models и нажмите Create. Введите имя модели Contract approval и нажмите OK. Откроется новая закладка браузера Model editor.

Tip

Если закладка не появилась, убедитесь, что браузер не заблокировал вспылывающее окно.

В панели свойств модели выберите свойство Process roles - откроется окно редактирования процессных ролей.

ProcessRolesProperty

В процессе должно быть два типа участников: контролер и менеджер. Создайте 2 роли: Controller и Manager.

ProcessRolesEditor

Перетащите в рабочую область узел Start event из группы Start events. При старте процесса нам необходимо отображать форму выбора участников процесса. Для этого выделите узел Start event. В панели свойств выберите Start form - откроется окно выбора формы. В списке Form name выберите Standard form. После этого добавьте два параметра формы:

  • procActorsVisible со значением true говорит о том, что на форме будет показана таблица для выбора участников процесса;

  • attachmentsVisible со значение true говорит о том, что на форме будет показана таблица для добавления вложений к процессу.

StartForm

Добавьте в модель узел User task из группы Activities. Назовите его Validation.

ModelValidationNode

Выделите этот узел, и на панели свойств задайте свойству Process role значение controller. Так мы указали, что задача будет назначена на участника процесса с ролью controller.

SelectProcRoleForValidation

Далее выберите свойство Task outcomes. Откроется окно редактирования выходов из задачи. Выходы определяют возможные действия пользователя при получении задачи. Создайте два выхода: Valid и Not valid. Для каждого из них укажите форму Standard form. Для выхода Not valid добавьте параметр формы commentRequired = true. Это нужно, чтобы в случае некорректного договора пользователь обязательно добавил свой комментарий.

OutcomesForValidation

В зависимости от решения контролера нам необходимо либо отправить договор далее на утверждение группе менеджеров, либо завершить процесс, предварительно установив договору состояние Not valid. Для контроля над маршрутом процесса используется узел Exclusive gateway из группы Gateways. Добавьте его на рабочую область, а затем добавьте еще два элемента: Script task с именем Set 'Not valid' state и User task с именем Approval. Переход к Script task назовите Not valid, переход к узлу Approval назовите Valid.

ModelValidationExclGateway

Выделите переход Not valid. В панели свойств разверните выпадающий список Flow outcome. В нем представлены выходы из предыдущей задачи. Выберите Not valid.

NotValidFlowOutcome

Теперь в случае выбора пользователем решения Not valid будет осуществлен переход именно по этой ветке.

Переход Valid сделаем переходом по умолчанию (если не выполнилось никакое из условий на других переходах узла). Для этого выделите переход Valid и поставьте галочку в его свойстве Default flow.

Warning

Для перехода, помеченного как Default flow, значение в выпающем списке Flow outcome должно быть пустым.

Далее выделите Exclusive gateway и откройте редактор свойства Flow order. Убедитесь, что переход Not valid стоит первым в списке. Если это не так, измените порядок обработки переходов.

ValidationFlowOrder

Перейдем к узлу Set 'Not valid' state. Нам необходимо установить значение свойства state сущности Contract в Not valid. Выделите узел. В поле свойства Script format введите groovy, т.к. мы будем писать groovy-скрипт. Нажмите на поле свойства Script узла. Откроется окно редактирования скрипта. Скопируйте и вставьте туда следующий текст:

import com.company.bpmdemo.entity.Contract

def em = persistence.getEntityManager()
def contract = em.find(Contract.class, entityId)
contract.setState('Not valid')

В скрипте можно использовать процессные переменные, а также объекты платформы persistence и metadata (см. Руководство по разработке приложений). Переменная entityId создается при запуске процесса и хранит идентификатор связанной сущности.

После того, как состояние договора изменено, процесс должен быть завершен - добавляем узел End event из группы End Events и соединяем его с узлом Set 'Not valid' state.

Вернемся к задаче Approval. Как и в случае с первой задачей, укажите для нее процессную роль - в данном случае это будет роль manager. Так как предполагается, что эта задача должна быть назначена одновременно нескольким менеджерам, то установим её свойство Multi-instance type в значение Parallel.

ApprovalMutlInstanceType

Создайте для задачи два выхода: Approve и Reject (свойство Task outcomes). Задайте для обоих выходов форму Standard form, для перехода Reject установите параметр commentRequired в true.

После того, как согласование завершится, договору должно установиться состояние Approved или Not approved в зависимости от результата согласования. Добавьте узел Exclusive gateway после задачи Approval. После Exclusive gateway добавьте две Service task: Set 'Approved' state и Set 'Not approved' state. Они будут делать то же самое, что и Script task, созданная ранее, но другим способом - вызывая метод Spring-бина. Переход к Set 'Approved' state назовите Approved, переход к Set 'Not approved' state назовите Not approved.

ModelWithApproval

Выделите переход Not approved и в списке Flow outcome выберите значение Reject. Теперь если хотя бы один из менеджеров выполнит действие Reject, то будет инициирован этот переход. Выделите переход Approved и установите флажок Default flow - если остальные переходы не сработали (не было выбора Reject), то будет инициирован переход Approved.

По аналогии с предыдущим Exclusive gateway установите порядок обработки переходов для текущего. Выделите Exclusive gateway и откройте редактор свойства Flow order. Первым должен обрабатываться переход Not approved.

ApprovalFlowOrder

Вернемся к Service task. Выделите узел Set 'Approved' state и задайте свойству Expression значение:

${demo_ApprovalHelper.updateState(entityId, 'Approved')}

Для Set 'Not approved' state:

${demo_ApprovalHelper.updateState(entityId, 'Not approved')}

Activiti Engine интегрирован со Spring Framework, поэтому мы можем обращаться к объектам, управляемым Spring, по их имени. entityId - процессная переменная, хранящая идентификатор сущности связанного с процессом договора. Ее значение будет записано при старте процесса.

Соедините с End event последние созданные задачи, нажмите кнопку сохранения модели - модель готова. Переходим к её развертыванию.

ProcessFull

1.6.2. Развертывание модели процесса

Процесс развертывания модели состоит из следующих этапов:

  • Формирование XML процесса в нотации BPMN из модели.

  • Деплой процесса во внутренние таблицы Activiti Engine.

  • Создание объекта ProcDefinition, связанного с загруженным в Activiti Engine процессом.

  • Создание объектов ProcRole для процессных ролей, объявленных в модели.

Выделите модель в списке на экране Process models. Нажмите кнопку Deploy. Откроется окно развертывания модели. Модель разворачивается первый раз, поэтому выбрана опция Create new process. При последующих изменениях модели можно будет разворачивать модель в уже существующий процесс. Нажмите OK. Процесс создан.

DeployModelScreen

Откройте экран BPM → Process definitions. Откройте строку с Contract approval для редактирования. Поле Code имеет значение contractApproval. Запомните его. В дальнейшем мы используем это значение чтобы идентифицировать процесс.

ProcDefinitionEdit

1.7. Адаптация экранов к процессу

В данном разделе мы добавим в экран редактирования договора возможность работы с процессом согласования.

1.7.1. Компоновка экрана редактирования договора

Найдите в секции Screens на панели навигатора Studio экран contract-edit.xml и откройте его на редактирование. Перейдите на вкладку XML и полностью замените ее содержимое на следующий код:

contract-edit.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
        caption="msg://editorCaption"
        class="com.company.bpmdemo.web.contract.ContractEdit"
        datasource="contractDs"
        focusComponent="fieldGroup"
        messagesPack="com.company.bpmdemo.web.contract">
    <dsContext>
        <datasource id="contractDs"
                    class="com.company.bpmdemo.entity.Contract"
                    view="_local"/>
        <collectionDatasource id="procAttachmentsDs"
                              class="com.haulmont.bpm.entity.ProcAttachment"
                              view="procAttachment-browse">
            <query>
                <![CDATA[select a from bpm$ProcAttachment a
                            where a.procInstance.entityId = :ds$contractDs
                            order by a.createTs]]>
            </query>
        </collectionDatasource>
    </dsContext>
    <dialogMode height="600"
                width="800"/>
    <layout expand="windowActions"
            spacing="true">
        <fieldGroup id="fieldGroup"
                    datasource="contractDs">
            <column width="250px">
                <field property="number"/>
                <field property="date"/>
                <field editable="false"
                       property="state"/>
            </column>
        </fieldGroup>
        <groupBox id="procActionsBox"
                  caption="msg://process"
                  orientation="vertical"
                  spacing="true"
                  width="AUTO">
            <frame id="procActionsFrame"
                   screen="procActionsFrame"/>
        </groupBox>
        <groupBox caption="msg://attachments"
                  height="300px"
                  width="700px">
            <table id="attachmentsTable"
                   height="100%"
                   width="100%">
                <columns>
                    <column id="file.name"/>
                    <column id="author"/>
                    <column id="type"/>
                    <column id="comment"
                            maxTextLength="50"/>
                </columns>
                <rows datasource="procAttachmentsDs"/>
            </table>
        </groupBox>
        <frame id="windowActions"
               screen="extendedEditWindowActions"/>
    </layout>
</window>

Перейдите на вкладку Layout. Компоновка экрана станет следующей:

ContractEditStudioLayout

Экран содержит группу полей для редактирования самого договора, фрейм для отображения действий по процессу и таблицу с вложениями, созданными во время выполнения процесса.

1.7.2. Контроллер экрана редактирования договора

Перейдите на вкладку Controller и замените ее содержимое на следующий код:

ContractEdit.java
package com.company.bpmdemo.web.contract;

import com.haulmont.cuba.gui.components.AbstractEditor;
import com.company.bpmdemo.entity.Contract;
import com.haulmont.bpm.entity.ProcAttachment;
import com.haulmont.bpm.gui.procactions.ProcActionsFrame;
import com.haulmont.cuba.gui.app.core.file.FileDownloadHelper;
import com.haulmont.cuba.gui.components.Table;

import javax.inject.Inject;

public class ContractEdit extends AbstractEditor<Contract> {
    private static final String PROCESS_CODE = "contractApproval";

    @Inject
    private ProcActionsFrame procActionsFrame;

    @Inject
    private Table<ProcAttachment> attachmentsTable;

    @Override
    protected void postInit() {
        FileDownloadHelper.initGeneratedColumn(attachmentsTable, "file");
        initProcActionsFrame();
    }

    private void initProcActionsFrame() {
        procActionsFrame.initializer()
                .setBeforeStartProcessPredicate(this::commit)
                .setAfterStartProcessListener(() -> {
                    showNotification(getMessage("processStarted"), NotificationType.HUMANIZED);
                    close(COMMIT_ACTION_ID);
                })
                .setBeforeCompleteTaskPredicate(this::commit)
                .setAfterCompleteTaskListener(() -> {
                    showNotification(getMessage("taskCompleted"), NotificationType.HUMANIZED);
                    close(COMMIT_ACTION_ID);
                })
                .init(PROCESS_CODE, getItem());
    }
}

Сохраните изменения, нажав кнопку OK.

Рассмотрим код контроллера более подробно.

ProcActionsFrame - это стандартный фрейм для отображения кнопок доступных в данный момент процессных действий. Во время инициализации фрейма по двум параметрам (код процесса и сущность) ищется связанный экезмпляр ProcInstance. Если процесс не находится, то создается новый ProcInstance, и фрейм отображает кнопку запуска процесса. Если связанный с сущностью процесс найден, то проверяется, имеется ли незавершенная процессная задача для текущего пользователя, и если да, то отображаются кнопки завершения данной задачи. Подробнее о ProcActionsFrame см. ProcActionsFrame.

В контроллере редактора договора инициализация фрейма процессных действий происходит в методе initProcActionsFrame(). Самая важная часть метода - вызов init(PROCESS_CODE, getItem()). Константа PROCESS_CODE хранит код процесса (contractApproval - это значение мы видели при равертывании процесса, см. Развертывание модели процесса). Второй аргумент getItem() - текущий договор.

Во время инициализации фрейма мы также объявили дополнительную логику, связанную с процессными действиями:

  • setBeforeStartProcessPredicate добавляет проверку, выполняемую перед запуском процесса. Выполняется коммит экрана, если коммит не проходит успешно, процесс не запускается

  • setAfterStartProcessListener отображает уведомление и закрывает редактор договора после запуска процесса

  • setBeforeCompleteTaskPredicate вызывает коммит редактора договора перед выполнением процессного действия. В случае неуспешного коммита, процессное действие не выполняется

  • setAfterCompleteTaskListener отображает уведомление и закрывает редактор договора после завершения процессного действия.

1.7.3. Файл локализованных сообщений

В Studio откройте файл messages.properties, расположенный в пакете с экранами для договора. Замените его содержимое следующим текстом:

browseCaption = Contract browser
editorCaption = Contract editor
attachments = Attachments
process = Contract approval
processStarted = Process started
taskCompleted = Task completed

1.8. Работа с приложением

По умолчанию в Cuba Studio включен механизм Hot Deploy, и изменения в экране редактирования договора уже должны быть отправлены на сервер. Если Hot Deploy у вас был отключен, то перезапустите сервер, выполнив в Studio команду Run → Restart application server.

1.8.1. Создание пользователей

Для демонстрации работы процесса необходимо создать несколько тестовых пользователей. Откройте экран Administration → Users и создайте трех пользователей:

  • login: norman, First name: Tommy, Last name: Norman, Full name: Tommy Norman

  • login: roberts, First name: Casey, Last name: Roberts, Full name: Casey Roberts

  • login: pierce, First name: Walter, Last name: Pierce, Full name: Walter Pierce

1.8.2. Создание договора и запуск процесса

  1. Откройте список договоров Application → Contracts и создайте новый договор. Заполните поля Number и Date и нажмите кнопку Save.

  2. Нажмите на кнопку Start process - перед вами появится форма запуска процесса. При создании модели для узла Start event мы указали форму Standard form с атрибутами procActorsVisible=true и attachmentsVisible=true, поэтому сейчас перед нами форма с компонентами для указания участников процесса и добавления вложений.

  3. Введите комментарий для процесса, добавьте участников: контролер norman и два менеджера: pierce и roberts.

  4. Загрузите вложение к договору, нажав на кнопку Upload таблицы Attachments.

StartProcessForm
  1. Нажмите ОК - процесс запущен.

1.8.3. Этап проверки контролером

Зайдите в систему под пользователем norman.

При достижении процессом узла User task создается объект ProcTask, связанный с определенным участником процесса. В подсистеме BPM есть экран для отображения списка невыполненных задач для текущего пользователя. Откройте его: BPM → Process tasks.

ProcTaskBrowse

Видим, что для пользователя norman есть одна задача Validation по процессу Contract approval. Выделите ее и нажмите кнопку Open entity editor - откроется экран редактирования договора.

ContractEditValidation

Так как для текущего пользователя (norman) имеется незавершенная задача (ProcTask), то procActionsFrame отображает доступные действия. Когда мы описывали узел UserTask с именем Validation, то мы указали для него два возможных выхода Valid и Not valid. На основании этой информации в фрейм и добавлено две кнопки.

Нажмите на Valid. В открывшемся окне введите комментарий:

ValidationCompleteForm

Нажмите OK.

После успешной валидации договор должен уйти к менеджерам на параллельное согласование.

1.8.4. Этап утверждения менеджерами

Войдите в систему под пользователем pierce.

Откройте список текущих задач BPM → Process tasks. Имеется одна задача Approval.

TaskListApproval

Выделите ее и на этот раз нажмите кнопку Open process instance - откроется системный экран для работы с экземпляром ProcInstance.

ProcInstanceEditApproval

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

Обратите внимание на таблицу Tasks. Предыдущая задача Validation завершена с результатом Valid, и после успешной валидации контролером создались две новые задачи Approval на менеджеров pierce и roberts.

Утвердите договор, воспользовавшись кнопкой Approve.

Далее войдите в систему под пользователем roberts. Откройте договор из списка Application → Contracts.

Пользователь roberts имеет незавершенную задачу по договору, следовательно фрейм procActionsFrame отображает для него действия Approve и Reject. Нажмите кнопку Reject.

CompleteApprovalForm

Так как при описании выхода Reject в дизайнере мы указали параметр формы commentRequired=true, то комментарий в форме завершения данного действия обязателен. Введите комментарий и нажмите ОК.

Один из менеджеров отклонил договор, поэтому ему должно установиться состояние Not approved. Проверим это, открыв договор.

ContractEditNotApproved

Процесс согласования завершен.

2. Модель данных

DataModel
Tip

Атрибуты, имена которых начинаются с префикса act* являются ссылками на идентификаторы из Activiti.

  • ProcModel - модель процесса. Атрибуты модели:

    • name - имя модели.

    • description - описание модели.

    • actModelId - ID модели Activiti engine в таблице ACT_RE_MODEL.

  • ProcDefinition - описание процесса. Может быть получен из модели, либо загружен напрямую из XML файла. Атрибуты сущности:

    • name - имя процесса.

    • code - код процесса. Может использоваться для поиска экземпляра сущности из кода приложения.

    • actId - ID объекта процесса из Activiti. Необходим для доступа к модели BPMN (из нее читаются extensionElements).

    • active - определяет, возможен ли запуск новых процессов для текущего ProcDefinition.

    • procRoles - коллекция объектов, определяющих участников процесса.

    • model - ссылка на модель, из которой получено описание процесса

  • ProcRole - роль в процессе. Объекты данного типа создаются автоматически при развертывании процесса на основе информации из XML файла с процессом. Можно сказать, что роли определяют типы участников процесса. Атрибуты сущности:

    • name - имя роли.

    • code - код роли. Может использоваться кодом приложения для идентификации роли.

    • order - порядковый номер. Может использоваться приложением для определения порядка отрисовки ролей.

    • procDefinition - ссылка на описание процесса.

  • ProcInstance - экземпляр процесса. ProcInstance может быть запущен как с привязкой к сущности проекта (например, процесс согласования договора может быть привязан к экземпляру сущности "Договор"), так и без нее. Атрибуты сущности:

    • description - описание экземпляра процесса.

    • startDate - дата запуска процесса.

    • endDate - дата завершения процесса.

    • startedBy - пользователь, запустивший процесс.

    • active - признак, что процесс запущен и еще не завершен.

    • cancelled - признак, что процесс был принудительно отменен.

    • actProcessInstanceId - идентификатор соответствующего ProcessInstance из Activiti.

    • startComment - комментарий, заданный при старте процесса.

    • cancelComment - комментарий, заданный при отмене процесса.

    • entityName - имя сущности, с которой связан процесс.

    • entityId - ID сущности, с которой связан процесс.

    • entityEditorName - имя экрана, который будет использоваться для открытия связанной сущности, если он отличается от стандартного.

    • procTasks - коллекция задач процесса.

    • procActors - коллекция участников процесса.

    • procAttachments - коллекция вложений процесса.

  • ProcActor - участник процесса. Сущность определяет исполнителей для ролей процесса по конкретному экземпляру процесса. Атрибуты сущности:

    • user - ссылка на пользователя.

    • procInstance - ссылка на экземпляр процесса.

    • procRole - ссылка на процессную роль.

    • order - порядковый номер. Используется при определении порядка участников для последовательной задачи на многих пользователей.

  • ProcTask - задача по процессу. Объекты данного типа автоматически создаются при достижении процессом узла User task. Атрибуты сущности:

    • name - имя задачи.

    • startDate - дата начала выполнения задачи.

    • claimDate - дата принятия задачи пользователем в случае задачи без явного участника.

    • endDate - дата завершения задачи.

    • outcome - результат выполнения задачи (выход, по которому пользователь завершил задачу).

    • comment - комментарий при завершении задачи.

    • procActor - исполнитель.

    • actTaskId - Activiti task ID. Используется при сигнале Activiti engine о завершения задачи.

    • `actExecutionId `- Activiti execution ID. Используется для записи/чтения процессных переменных.

    • actTaskDefinitionKey - в XML процесса это поле id у UserTask. Используется при формировании имени переменной, хранящей результат задачи [taskId]_result (см. Переходы в зависимости от выхода (outcome) задачи).

    • cancelled - признак, что задача была завершена при отмене процесса.

    • candidateUsers - список возможных участников для групповой задачи.

    • procInstance - ссылка на экземпляр процесса.

  • ProcAttachment - процессное вложение. Атрибуты сущности:

    • file - ссылка на FileDescriptor.

    • type - тип вложения (ProcAttachmentType).

    • comment - комментарий.

    • author - автор вложения, ссылка на пользователя.

    • procInstance - ссылка на экземпляр процесса.

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

  • ProcAttachmentType - тип вложения. Атрибуты сущности:

    • code - код типа вложения.

    • name - имя типа вложения.

3. Функциональность

Для исполнения бизнес-процессов подсистема BPM использует Activiti Engine, для моделирования процессов в нотации BPMN используется доработанный редактор из веб-приложения Activiti Explorer. В дополнение к возможностям фреймворка Activiti подсистема BPM предоставляет дополнительную функциональность, которая описана далее в этом разделе. Перечисление возможностей фреймворка Activiti не входит в задачу данного руководства. Вы можете ознакомиться с ними на сайте Activiti: https://www.activiti.org/userguide/.

3.1. BpmActivitiListener

При создании модели процесса, в процесс автоматически добавляется слушатель событий BpmActivitiListener. BpmActivitiListener является реализацией интерфейса ActivitiEventListener (см. http://www.activiti.org/userguide/#eventDispatcher). Слушатель отвечает за создание и изменение сущностей подсистемы BPM при наступлении определенных событий процесса (вход в пользовательскую задачу, отмена процесса, завершение задачи, и т.д.). Именно он создает объекты ProcTask и проставляет значение endDate для ProcInstance.

3.2. Процессные роли (Process Roles)

Процессные роли определяют типы участников процесса, например, оператор или менеджер. Чтобы открыть экран редактирования процессных ролей в дизайнере процессов, выберите свойство Process roles в панели свойств процесса. При развертывании модели информация о ролях будет записана в XML процесса в секцию extensionElements элемента process, а затем будут созданы соответствующие объекты ProcRole.

Описание процессных ролей
<process id="testProcess" name="Test process">
    <extensionElements>
         <cuba:procRoles>
            <cuba:procRole name="Manager" code="manager"/>
            <cuba:procRole name="Operator" code="operator"/>
        </cuba:procRoles>
    </extensionElements>
</process>

3.3. Форма запуска процесса (Process Form)

Для задания формы, которая будет отображаться при запуске процесса, используется свойство Start form элемента Start event. Подробнее о формах см. Процессные формы (ProcForm).

Форма запуска процесса
<startEvent id="startEvent">
  <extensionElements>
    <cuba:form name="standardProcForm">
      <cuba:param name="procActorsVisible" value="true"></cuba:param>
    </cuba:form>
  </extensionElements>
</startEvent>

3.4. Пользовательская задача (User Task)

Для определения пользователя, на которого будет назначена задача, необходимо в свойстве Process role элемента User task выбрать одну из процессных ролей, определенных в модели. При достижении процессом задачи среди участников процесса (ProcActor) будут найдены участники с указанной процессной ролью, и задача будет назначена на них.

Задание процессной роли для задачи
<userTask id="managerApproval" name="Manager approval">
    <extensionElements>
        <cuba:procRole>manager</cuba:procRole>
    </extensionElements>
</process>

Если необходимо, чтобы задача была назначена одновременно нескольким пользователям, то в свойстве Multi-instance type элемента User task необходимо выбрать значение Parallel или Sequential.

Также возможен вариант, когда задача не должна быть сразу назначена на пользователя, а должна появиться в списке доступных, и один из пользователей должен забрать задачу себе. Для определения такой задачи необходимо установить фдажок Claim allowed. В этом случае задача появится в списке доступных у всех участников процесса с ролью, заданной в свойстве Process role.

Задача без конкретного участника
<userTask id="managerApproval" name="Manager approval">
    <extensionElements>
        <cuba:claimAllowed>true</cuba:claimAllowed>
    </extensionElements>
</process>

3.5. Выходы задачи (Outcomes)

Обычно задача требует от пользователя принятия решения (например, согласовать или отклонить). Дальнейший маршрут по процессу зависит от принятого решения. Для задания списка выходов из задачи используется свойство Task outcomes узла User Task. Для каждого выхода задается имя и выбирается форма, которая должна отображаться при выборе этого выхода, а также список параметров формы (подробнее о формах см. Процессные формы (ProcForm)).

Описание выходов из задачи
<userTask id="managerApproval" name="Manager approval">
    <extensionElements>
        <cuba:outcomes>
            <cuba:outcome name="approve">
                <cuba:form name="standardProcessForm">
                    <cuba:param name="commentRequired">true</cuba:param>
                    <cuba:param name="attachmentsVisible">true</cuba:param>
                </cuba:form>
            </cuba:outcome>
            <cuba:outcome name="reject">
                <cuba:form name="someOtherProcessForm">
                </cuba:form>
            </cuba:outcome>
        </cuba:outcomes>
    </extensionElements>
</process>

3.6. Переходы в зависимости от выхода задачи

В отличие от jBPM, в нотации BPMN отсутствует возможность указать несколько выходов из одной задачи. Чтобы направить процесс по нужной ветке в зависимости от результата используется узел Exclusive Gateway, переходы из которого имеют условия, оперирующие результатом выполнения задачи, расположенной перед этим Gateway. При завершении пользователем задачи, результат его действия записывается в процессную переменную с именем [taskId]_result. Тип этой переменной - ProcTaskResult.

Методы класса ProcTaskResult:

  • int count(String outcomeName) - возвращает количество пользователей, завершивших задачу с данным выходом

  • boolean exists(String outcomeName) - возвращает true, если есть хотя бы один пользователь, завершивший задачу с указанным выходом.

Далее объект с результатом используется в выражении Flow condition для переходов, выходящих из Gateway.

Пример:

TaskOutcomesExample

Предположим, что задача approval была параллельно назначена нескольким пользователям. Для задачи были определены два возможных выхода: approve и reject. После того, как все пользователи завершат задачу процесс перейдет к Exclusive gateway. Нам нужно следующее поведение: если хоть кто-либо выбрал вариант reject, то переходим по переходу Rejected, если все согласились (approve), то по Approved.

Задание условия в поле Flow outcome

Самым удобным вариантом задания условия, который подойдет для большинства случаев, является выбор имени outcome предыдущей задачи в свойстве Flow outcome стрелки перехода. Данный переход сработает, если было хотя бы одно завершение задачи с указанным outcome.

Задание сложных условий для перехода

Если необходимо иметь более сложные условия для перехода, то их можно задать в поле Flow condition. Например условие "Более 5 пользователей выбрали вариант Reject " будет выглядеть следующим образом:

${approval_result.count('reject') > 5}

3.6.1. Порядок обработки переходов

Обратите внимание, что необходимо задать порядок обработки переходов. Иначе Activiti может, например, обработать переход по умолчанию до переходов с явно заданными условиями. Для задания порядка вычисления условий установите свойство Flow order у узла Exclusive gateway.

3.7. Вызов скрипта

Для выполнения скрипта используется элемент Script task. При достижении элемента, система анализирует содержимое поля Script. Если содержимое является путем к файлу и данный файл существует, то система исполнит указанный файл. Если файла по указанному пути нет, то содержимое поля Script будет исполнено.

Внутри скрипта можно использовать объекты persistence и metadata.

3.8. Вызов методов бинов среднего слоя

Для вызова метода сервиса используется элемент Service task. Activiti Engine интегрирован со Spring Framework, т.е. возможно обращение к бинам среднего слоя по имени. Для вызова метода управляемого бина в поле Expression пишется выражение вида:

${beanName.methodName(processVarName, 'someStringParam')}

В качестве параметров вызова метода можно использовать процессные переменные, в том числе автоматически созданные при старте процесса (entityId, bpmProcInstanceId и т.д., как описано в ProcessRuntimeService).

3.9. Завершение задачи по таймеру

Для того, чтобы завершить задачу после истечения периода времени необходимо:

  • Добавить к элементу задачи элемент Boundary timer event.

  • От элемента таймера нарисовать переход к нужному этапу процесса.

  • В свойстве таймера Time duration написать выражение для периода времени. Например, PT15M (15 минут).

  • Установить флажок Cancel activity, чтобы по срабатыванию таймера текущая задача завершилась.

  • В свойстве Timer outcome указать имя выхода задачи, которое должно быть использовано при завершении по таймеру.

TimerEdit
Задание выхода для таймера
<boundaryEvent id="managerApprovalTimer" cancelActivity="true" attachedToRef="managerApproval">
    <extensionElements>
        <cuba:outcome>approve</cuba:outcome>
    </extensionElements>
</boundaryEvent>
Tip

По умолчанию Job executor для обработки заданий таймеров отключен. Для его включения установите свойство приложения bpm.activiti.asyncExecutorEnabled = true.

3.10. Локализация

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

Для открытия экрана задания локализованных значений выберите свойство Localization модели.

Для локализации имени задачи необходимо создать запись, ключом которой является id задачи.

Для локализации имени выхода из задачи необходимо создать запись, ключом которой является выражение вида TASK_ID.OUTCOME_NAME.

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

Локализованные сообщения
<process id="testProcess" name="Test process">
    <extensionElements>
        <cuba:localizations>
            <cuba:localization lang="en">
                <cuba:msg key="key1" value="value1"/>
                <cuba:msg key="key2" value="value2"/>
            </cuba:localization>
            <cuba:localization lang="ru">
                <cuba:msg key="key1" value="value1"/>
                <cuba:msg key="key2" value="value2"/>
            </cuba:localization>
      </cuba:localizations>
    </extensionElements>
</process>

3.11. Подмодели

Узел Sub model группы Structural позволяет использовать существующую модель в качестве части новой модели. При развертывании процесса из модели элементы подмодели вставляются в текущую модель, и из результата этой операции формируется XML с процессом.

3.12. Создание элементов для дизайнера модели

Подсистема BPM позволяет создавать собственные элементы для дизайнера моделей процесса. Новый элемент - это по сути ServiceTask, избавляющий разработчика модели от необходимости вводить длинные выражения для вызова метода, такие как ${app_MyBean.someMethod(argument1, 'argument2')}. Ниже приведен пример создания элемента.

Предположим, в системе имеется бин среднего слоя с именем app_DiscountManager. В бине имеется метод makeDiscount(BigDecimal discountPercent, UUID entityId). Метод обновляет стоимость договора, вычитая из нее указанную скидку.

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

Откройте редактор элементов с помощью пункта меню BPM → Model Elements Editor.

Нажмите на кнопку Add group. Введите имя группы - Discounts.

StencilSetAddGroup

Выделите созданную группу Discounts и нажмите кнопку Add element.

StencilSetAddStencil

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

  • Title: Contract discount

  • Element ID: contractDiscount

  • Icon: нажмите на кнопку Upload и выберите файл с иконкой (опционально)

  • Bean name: выберите app_DiscountManager

  • Method name: выберите makeDiscount

Warning

Выпадающий список Bean name содержит только бины, реализующие какой-либо интерфейс. В списке Method name отображаются методы реализуемых интерфейсов.

В таблице Method arguments отображаются аргументы метода. Вы можете изменить заголовок и значение по умолчанию для каждого из аргументов.

Сохраните набор элементов, нажав на кнопку Save.

Откройте редактор модели (BPM → Process Models). В списке элементов появилась группа Discounts и элемент Contract discount. Перетащите новый элемент на экран и выделите его. Видим, что в панели свойств появились поля для ввода значений процента скидки и имени процессной переменной с идентификатором сущности.

StencilSetModel
Tip

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

При развертывании процесса, кастомный элемент будет преобразован в serviceTask:

<serviceTask id="sid-5C184F22-6071-45CD-AEA9-1792512BBDCE" name="Make discount" activiti:expression="${app_DiscountManager.makeDiscount(10,entityId)}"></serviceTask>

Набор элементов модели может быть экспортирован в ZIP-архив и, соответственно, восстановлен из архива. Это полезно при разработке, когда элементы создаются на машине разработчика, а затем импортируются на продакшн-сервер. Импорт и экспорт осуществляются с помощью соответствующих кнопок в редакторе элементов модели.

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

4. Основные сервисы

Раздел содержит общее описание сервисов. Описание методов сервисов находится в Javadoc в исходном коде классов.

4.1. ProcessRepositoryService

Служит для работы с описаниями процесса (ProcDefinition). Сервис использутся для:

  • загрузки описания процесса из XML;

  • удаления процесса из Activiti engine;

  • преобразования JSON модели в BPMN XML.

Для доступа к функциональности сервиса в коде middleware используйте ProcessRepositoryManager.

4.2. ProcessRuntimeService

Служит для работы с экземпляром процесса (ProcInstance). Методы сервиса позволяют:

  • запускать процесс;

  • отменять процесс;

  • завершать задачу;

  • назначать задачу на пользователя.

При запуске процесса автоматически создаются следующие процессные переменные:

  • bpmProcInstanceId - ID экземпляра ProcInstance

  • entityName - имя связанной сущности

  • entityId - идентификатор связанной сущности

Для доступа к функциональности сервиса в коде middleware используйте ProcessRuntimeManager.

4.3. ProcessFormService

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

Для доступа к функциональности сервиса в коде middleware используйте ProcessFormManager.

4.4. ProcessMessagesService

Служит для доступа к локализованным сообщениям, определенным в процессе.

Для доступа к функциональности сервиса в коде middleware используйте ProcessMessagesManager.

4.5. ModelService

Используется для создания, обновления моделей во внутренних таблицах Activiti, а также для получения JSON-модели.

5. Компоненты UI

В этом разделе описаны компоненты пользовательского интерфейса подсистемы BPM.

5.1. ProcActionsFrame

ProcActionsFrame - фрейм для работы с процессными действиями. После инициализации во фрейме автоматически отобразятся:

  • кнопка запуска процесса, если процесс не запущен;

  • кнопки, соответствующие выходам из задачи, если процесс запущен и текущий пользователь имеет активную задачу;

  • кнопка отмены процесса;

  • информация о задаче (имя и дата создания).

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

ProcActionsFrame должен быть связан с экземпляром ProcInstance. Связывание происходит во время инициализации фрейма.

Пример инициализации фрейма:

procActionsFrame.initializer()
        .setBeforeStartProcessPredicate(this::commit)
        .setAfterStartProcessListener(showNotification(getMessage("processStarted"), NotificationType.HUMANIZED))
        .init(PROCESS_CODE, entity);
  • Метод initializer() возвращает объект, используемый для инициализации фрейма.

  • Метод setBeforeStartProcessPredicate устанавливает предикат, который будет вычислен перед выполнением запуска процесса. Если предикат вернет false, запуск процесса будет прерван.

  • Метод setAfterStartProcessListener задает слушатель, который будет вызыван после завершения действия запуска процесса.

  • Самое главное - метод init принимает два параметра: код процесса и экземпляр сущности. При вызове этого метода происходит поиск объекта ProcInstance, связанного с указанным экземпляром сущности и ссылающимся на ProcDefinition с указанным кодом. Если ProcInstance существует, фрейм связывается с ним, если нет, то создается новый объект ProcInstance.

Полный список методов, которые можно использовать для работы с фреймом:

Жизненный цикл процесса
  • initializer() - возвращает экземпляр инициализатора фрейма.

  • init() - пробует найти экземпляр процесса по переданному коду и ссылке на сущность. Если существующий процесс не найден, создаётся новый. Затем происходит инициализация UI, отображающий действия, доступные для этого процесса и для текущего пользователя.

Конфигурация процесса
  • setStartProcessEnabled() - устанавливает разрешение или запрет на запуск процесса.

  • setCancelProcessEnabled() - устанавливает разрешение или запрет на отмену процесса.

  • setCompleteTaskEnabled() - устанавливает разрешение или запрет на завершение задачи.

  • setClaimTaskEnabled() - устанавливает разрешение или запрет на выбор задачи самим пользователем.

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

  • setButtonWidth() - устанавливает ширину кнопок управления процессом. По умолчанию кнопки имеют ширину 150 px.

Предикаты
  • setBeforeStartProcessPredicate() - устанавливает предикат, который должен быть вычислен перед запуском процесса. Если предикат вернёт false, запуск процесса будет прерван.

  • setBeforeCompleteTaskPredicate() - устанавливает предикат, который должен быть вычислен перед завершением задачи. Если предикат вернёт false, завершение задачи будет прервано.

  • setBeforeClaimTaskPredicate() - устанавливает предикат, который должен быть вычислен перед назначением задачи на пользователя. Если предикат вернёт false, назначение задачи будет прервано.

  • setBeforeCancelProcessPredicate() - устанавливает предикат, который должен быть вычислен перед отменой задачи. Если предикат вернёт false, задача не будет отменена.

Слушатели процесса и задач
  • setAfterStartProcessListener() - задаёт слушатель, который будет вызван после запуска процесса.

  • setAfterCompleteTaskListener() - задаёт слушатель, который будет вызван после завершения задачи.

  • setAfterClaimTaskListener() - задаёт слушатель, который будет вызван после выбора задачи пользователем.

  • setAfterCancelProcessListener() - задаёт слушатель, который будет вызван после отмены выполнения процесса.



5.2. Процессные формы (ProcForm)

Интерфейс ProcForm

При определении выхода (outcome) для задачи и в узле начала процесса возможно указать форму, которая будет показана пользователю. Форма должна реализовывать интерфейс ProcForm. Методы интерфейса ProcForm:

  • getComment(): String - значение, возвращаемое этим методом, будет записано в поле comment объекта ProcTask при завершении задачи или в поле startComment у ProcInstance если форма отображается на старте процесса;

  • getFormResult(): Map<String, Object> - возвращает список объектов, которые будут добавлены к переменным процесса после коммита формы.

Задание списка форм для дизайнера моделей процесса

Список форм, доступный в дизайнере моделей процесса, формируется на основе конфигурационных файлов, определенных свойством приложения bpm.formsConfig. Для добавления новой процессной формы выполните следующие шаги:

  1. Создайте и зарегистрируйте экран для новой формы. Контроллер экрана должен реализовывать интерфейс ProcForm.

  2. Создайте файл, например app-bpm-forms.xml, для конфигурации новых процессных форм. Положите его в директорию src модуля web или gui.

    <?xml version="1.0" encoding="UTF-8"?>
    <forms xmlns="http://schemas.haulmont.com/cuba/bpm-forms.xsd">
        <form name="myCustomForm" default="true">
            <param name="someParam" value="hello"/>
            <param name="otherParam"/>
        </form>
    </forms>

    myCustomForm - идентификатор экрана.

    Помимо имени форм в файле определены имена возможных параметров формы и их значения по умолчанию.

    Форма с атрибутом default="true" будет использоваться в дизайнере как форма по умолчанию.

  3. Переопределите свойство bpm.formsConfig в файле web-app.properties.

    bpm.formsConfig = bpm-forms.xml app-bpm-forms.xml


6. Примеры

6.1. Программный запуск процесса

Ниже приведён пример программного запуска отчёта с заданными ролями из контроллера экрана редактирования сущности с помощью сервиса ProcessRuntimeService. Полный код данного тестового приложения можно посмотреть на GitHub.

  1. Для начала загрузите экземпляр описания процесса (process definition) по его коду:

    ProcDefinition procDefinition = dataManager.load(
            LoadContext.create(ProcDefinition.class)
                    .setQuery(new LoadContext.Query("select pd from bpm$ProcDefinition pd where pd.code = :code")
                            .setParameter("code", PROCESS_CODE))
                    .setView("procDefinition-procInstanceEdit")
    );
  2. Затем создайте экземпляр процесса (process instance) и свяжите его с описанием процесса и сущностью проекта:

    ProcInstance procInstance = metadata.create(ProcInstance.class);
    procInstance.setProcDefinition(procDefinition);
    procInstance.setEntityId(getItem().getUuid());
    procInstance.setEntityName(getItem().getMetaClass().getName());
  3. Создайте участников процесса (process actors), назначив их роли:

    ProcActor manager = metadata.create(ProcActor.class);
    manager.setUser(userSession.getUser());
    manager.setOrder(0);
    manager.setProcInstance(procInstance);
    manager.setProcRole(procDefinition.getProcRoles().get(0));
    
    ProcActor mechanic = metadata.create(ProcActor.class);
    mechanic.setUser(getItem().getMechanic().getUser());
    mechanic.setOrder(1);
    mechanic.setProcInstance(procInstance);
    mechanic.setProcRole(procDefinition.getProcRoles().get(1));
  4. Свяжите набор участников с экземпляром процесса:

    Set<ProcActor> procActors = new HashSet<>();
    procActors.add(manager);
    procActors.add(mechanic);
    
    procInstance.setProcActors(procActors);
  5. Сохраните созданные сущности участников и процесса в базе данных:

    Set<Entity> committed = dataManager.commit(new CommitContext()
            .addInstanceToCommit(manager)
            .addInstanceToCommit(mechanic)
            .addInstanceToCommit(procInstance));
  6. Запустите сохранённый экземпляр процесса:

    ProcInstance committedProcInstance = (ProcInstance) committed.stream()
            .filter(p -> p.equals(procInstance))
            .findFirst()
            .orElseThrow(() -> new RuntimeException("Error"));
    
    processRuntimeService.startProcess(committedProcInstance, "Started!", new HashMap<>());

В данном примере метод startProcess() вызывается в контроллере экрана по нажатию кнопки. Код метода полностью:

@Inject
private ProcessRuntimeService processRuntimeService;

private static String PROCESS_CODE = "orderProcessing";

public class OrderEdit extends AbstractEditor<Order> {

    public void startProcess() {
        commit();

        ProcDefinition procDefinition = dataManager.load(
                LoadContext.create(ProcDefinition.class)
                .setQuery(new LoadContext.Query("select pd from bpm$ProcDefinition pd where pd.code = :code")
                        .setParameter("code", PROCESS_CODE))
                .setView("procDefinition-procInstanceEdit")
        );

        ProcInstance procInstance = metadata.create(ProcInstance.class);
        procInstance.setProcDefinition(procDefinition);
        procInstance.setEntityId(getItem().getUuid());
        procInstance.setEntityName(getItem().getMetaClass().getName());

        Set<ProcActor> procActors = new HashSet<>();

        ProcActor manager = metadata.create(ProcActor.class);
        manager.setUser(userSession.getUser());
        manager.setOrder(0);
        manager.setProcInstance(procInstance);
        manager.setProcRole(procDefinition.getProcRoles().get(0));

        ProcActor mechanic = metadata.create(ProcActor.class);
        mechanic.setUser(getItem().getMechanic().getUser());
        mechanic.setOrder(1);
        mechanic.setProcInstance(procInstance);
        mechanic.setProcRole(procDefinition.getProcRoles().get(1));

        procActors.add(manager);
        procActors.add(mechanic);

        procInstance.setProcActors(procActors);

        Set<Entity> committed = dataManager.commit(new CommitContext()
                .addInstanceToCommit(manager)
                .addInstanceToCommit(mechanic)
                .addInstanceToCommit(procInstance));

        ProcInstance committedProcInstance = (ProcInstance) committed.stream()
                .filter(p -> p.equals(procInstance))
                .findFirst()
                .orElseThrow(() -> new RuntimeException("Error"));

        processRuntimeService.startProcess(committedProcInstance, "Started!", new HashMap<>());

        close(COMMIT_ACTION_ID);
    }
}

Приложение A: Свойства приложения

bpm.activiti.asyncExecutorEnabled

Возможные значение: true или false. Определяет, включен ли Job executor для выполнения задач таймеров и асинхронных задач. По умолчанию значение равно false.

. . .