Предисловие

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

Целевая аудитория

Данное руководство предназначено для разработчиков приложений на платформе CUBA. Предполагается, что читатель ознакомлен с Руководством по разработке приложений.

Дополнительные материалы

Настоящее Руководство, а также другая документация по платформе CUBA доступны по адресу https://www.cuba-platform.ru/documentation.

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

Обратная связь

Если у Вас имеются предложения по улучшению данного руководства, мы будем рады принять ваши pull request’ы и issues в исходниках документации на GitHub. Если вы увидели ошибку или несоответствие в документе, пожалуйста, форкните репозиторий и исправьте проблему. Заранее спасибо!

1. Установка

Для добавления аддона в проект выполните следующие действия:

  1. Кликните дважды Add-ons в дереве проекта CUBA.

    addons
  2. Перейдите на вкладку Marketplace и найдите аддон Business Process Management.

    bpm addon
  3. Кликните Install и затем Apply & Close.

    addon install
  4. Кликните Continue в появившемся диалоговом окне.

    addon continue

Аддон, соответствующий используемой версии платформы, будет добавлен в проект.

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

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

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

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

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

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

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

  1. Создайте новый проект в CUBA Studio, как это описано в разделе Создание нового проекта в Руководстве пользователя CUBA Studio:

    • Project namespace: bpmdemo

    • Root package: com.company.bpmdemo

  1. Добавьте в проект аддон, как это описано в разделе Установка.

  2. Создайте базу данных на локальном сервере HyperSQL. Выберите пункт меню CUBA > Create database.

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

Создадим сущность Договор (Contract).

  1. Перейдите в секцию Data Model дерева CUBA и нажмите New > Entity в её контекстном меню. Появится диалоговое окно New CUBA Entity.

  2. В поле Entity name введите название класса сущности – Contract. Нажмите OK. В рабочей области откроется страница дизайнера сущности.

  3. Добавьте атрибуты с помощью дизайнера сущности:

    • number типа String;

    • date типа Date;

    • state типа String.

На этом создание сущности Contract завершено.

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

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

  1. Выделите сущность Contract в секции Data Model и в контекстном меню сущности нажмите New > Screen. После этого на экране отобразится страница создания стандартных экранов сущности.

  2. Для создания стандартных экранов просмотра и редактирования в списке доступных шаблонов выберите Entity browser and editor screens и нажмите Next.

  3. Оставьте значения по умолчанию, нажмите Next и в следующем окне - Finish.

В разделе Screens секции Generic UI появятся файлы созданных экранов:

contract-browse.xml – дескриптор экрана просмотра,

ContractBrowse – контроллер экрана просмотра,

contract-edit.xml – дескриптор экрана редактирования,

ContractEdit – контроллер экрана редактирования.

2.4. Бин ApprovalHelper

Создайте бин ApprovalHelper, как это описано в разделе Создание бинов. Замените содержимое файла на следующий код:

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(ApprovalHelper.NAME)
public class ApprovalHelper {
    public static final String NAME = "demo_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();
        }
    }
}

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

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

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

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

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

  1. Для создания таблиц базы данных нажмите Generate Database Scripts в главном меню CUBA. После этого откроется страница Database Scripts.

  2. Для сохранения сгенерированных скриптов нажмите на кнопку Save and close.

  3. Для запуска скриптов обновления выполните CUBA > Update database.

  4. Посмотрим, как созданные нами экраны выглядят в работающем приложении. Для этого нажмите на кнопку run_button в главной панели инструментов.

  5. Откройте приложение в браузере по адресу http://localhost:8080/app. Вы также можете открыть запущенное приложение в браузере, используя элемент дерева CUBA Runs At…​

  6. Авторизуйтесь, используя логин и пароль пользователя − admin / admin. Пользователь Администратор имеет все права доступа. При создании другого пользователя, который будет моделировать процессы, необходимо назначить ему роль bpm-process-admin.

Запущенное приложение содержит пункты меню BPM, Application, Administration и Help, функциональность подсистемы безопасности и администрирования системы.

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

В этом разделе мы создадим и развернём процесс.

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

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

ProcessFull
Рисунок 1. Модель процесса

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

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

Tip

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

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

ProcessRolesProperty
Рисунок 2. Панель свойств модели

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

ProcessRolesEditor
Рисунок 3. Окно редактирования процессных ролей

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

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

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

StartForm
Рисунок 4. Окно выбора формы

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

ModelValidationNode
Рисунок 5. Модель процесса. Узел Validation

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

SelectProcRoleForValidation
Рисунок 6. Выбор процессной роли для задачи

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

OutcomesForValidation
Рисунок 7. Окно редактирования выходов из задачи

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

ModelValidationExclGateway
Рисунок 8. Узел Exclusive gateway на модели процесса

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

NotValidFlowOutcome
Рисунок 9. Панель свойств перехода Not valid

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

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

Warning

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

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

ValidationFlowOrder
Рисунок 10. Порядок обработки переходов для текущего узла

Перейдем к узлу 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
Рисунок 11. Панель свойств задачи Approval

Создайте для задачи два выхода: 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
Рисунок 12. Модель процесса утверждения

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

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

ApprovalFlowOrder
Рисунок 13. Порядок обработки переходов для текущего узла

Вернемся к 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
Рисунок 14. Модель процесса

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

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

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

  • Развёртывание процесса во внутренние таблицы Activiti Engine.

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

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

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

DeployModelScreen
Рисунок 15. Экран развёртывания модели

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

ProcDefinitionEdit
Рисунок 16. Окно описания процесса

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

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

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

Откройте файл contract-edit.xml в студии и полностью замените его содержимое на следующий код:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        caption="msg://editorCaption"
        focusComponent="form"
        messagesPack="com.company.bpmdemo.web.screens.contract">
    <data>
        <instance id="contractDc"
                  class="com.company.bpmdemo.entity.Contract"
                  view="_local">
            <loader id="contractDl"/>
        </instance>
        <collection id="procAttachmentsDc"
                    class="com.haulmont.bpm.entity.ProcAttachment"
                    view="procAttachment-browse">
            <loader id="procAttachmentsDl">
                <query><![CDATA[select e from bpm$ProcAttachment e
                                where e.procInstance.entity.entityId  = :entityId
                                order by e.createTs]]>
                </query>
            </loader>
        </collection>
    </data>
    <dialogMode height="600"
                width="800"/>
    <layout expand="editActions"
            spacing="true">
        <form id="form"
              dataContainer="contractDc">
            <column width="250px">
                <textField id="numberField"
                           property="number"/>
                <dateField id="dateField"
                           property="date"/>
                <textField id="stateField"
                           property="state"/>
            </column>
        </form>
        <groupBox id="procActionsBox"
                  caption="msg://process"
                  spacing="true"
                  width="AUTO"
                  orientation="vertical">
            <fragment id="procActionsFragment"
                      screen="bpm_ProcActionsFragment"/>
        </groupBox>
        <groupBox caption="msg://attachments"
                  height="300px"
                  spacing="true"
                  width="700px">
            <table id="attachmentsTable"
                   dataContainer="procAttachmentsDc"
                   height="100%"
                   width="100%">
                <columns>
                    <column id="file.name"/>
                    <column id="author"/>
                    <column id="type"/>
                    <column id="comment"
                            maxTextLength="50"/>
                </columns>
            </table>
        </groupBox>
        <hbox id="editActions"
              spacing="true">
            <button action="windowCommitAndClose"/>
            <button action="windowCommit"/>
            <button action="windowClose"/>
        </hbox>
    </layout>
</window>

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

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

Откройте экран ContractEdit и замените его содержимое на следующий код:

package com.company.bpmdemo.web.screens.contract;

import com.haulmont.bpm.entity.ProcAttachment;
import com.haulmont.bpm.gui.procactionsfragment.ProcActionsFragment;
import com.haulmont.cuba.gui.app.core.file.FileDownloadHelper;
import com.haulmont.cuba.gui.components.Table;
import com.haulmont.cuba.gui.model.CollectionLoader;
import com.haulmont.cuba.gui.model.InstanceContainer;
import com.haulmont.cuba.gui.model.InstanceLoader;
import com.haulmont.cuba.gui.screen.*;
import com.company.bpmdemo.entity.Contract;

import javax.inject.Inject;

@UiController("bpmdemo_Contract.edit")
@UiDescriptor("contract-edit.xml")
@EditedEntityContainer("contractDc")
public class ContractEdit extends StandardEditor<Contract> {
    private static final String PROCESS_CODE = "contractApproval";

    @Inject
    private CollectionLoader<ProcAttachment> procAttachmentsDl;

    @Inject
    private InstanceContainer<Contract> contractDc;

    @Inject
    protected ProcActionsFragment procActionsFragment;

    @Inject
    private Table<ProcAttachment> attachmentsTable;

    @Inject
    private InstanceLoader<Contract> contractDl;

    @Subscribe
    private void onBeforeShow(BeforeShowEvent event) {
        contractDl.load();
        procAttachmentsDl.setParameter("entityId",contractDc.getItem().getId());
        procAttachmentsDl.load();
        procActionsFragment.initializer()
                .standard()
                .init(PROCESS_CODE, getEditedEntity());

        FileDownloadHelper.initGeneratedColumn(attachmentsTable, "file");
    }
}

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

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

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

Стандартная инициализация фрагмента (standard()) делает следующее:

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

  • Инициализирует стандартные слушатели, вызываемые после завершения процессного действия. Данные слушатели отображают уведомления ("Процесс запущен", "Действие выполнено", и т.п.), а также обновляют procActionsFragment.

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

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

2.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

Назначьте роль bpm-process-actor, чтобы предоставить доступ к сущностям BPM.

Создайте роль quick-start и предоставьте ей необходимые для работы с проектом разрешения:

  1. Перейдите на экран Administration → Roles и создайте новую роль с именем quick-start.

  2. На вкладке Screens разрешите доступ к экрану BPM → Process Tasks. Этот экран не доступен в роли bpm-process-actor, так как может понадобиться не всем проектам.

  3. Разрешите доступ к экранам:

    • Application →Contracts

    • bpm$ProcTask.edit

    • bpmdemo_Contract.edit

    • bpm$ProcInstance.edit

  4. Разрешите bpmdemo_Contract во вкладках Entities и Attributes.

  5. Сохраните роль.

Назначьте роль quick-start созданным ранее пользователям.

2.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
    Рисунок 17. Форма запуска процесса
  5. Нажмите ОК − процесс запущен.

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

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

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

ProcTaskBrowse
Рисунок 18. Экран для отображения списка невыполненных задач для текущего пользователя

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

ContractEditValidation
Рисунок 19. Окно редактирования договора

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

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

ValidationCompleteForm
Рисунок 20. Окно добавления комментария

Нажмите OK.

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

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

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

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

TaskListApproval
Рисунок 21. Список текущих задач

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

ProcInstanceEditApproval
Рисунок 22. Системный экран для работы с экземпляром ProcInstance

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

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

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

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

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

CompleteApprovalForm
Рисунок 23. Форма завершения действия

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

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

ContractEditNotApproved
Рисунок 24. Редактор договора. Состояние Not approved

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

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

DataModel
Рисунок 25. Модель данных
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 (см. Переходы в зависимости от выхода задачи).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4.1. BpmActivitiListener

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

4.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>

4.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>

4.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.

Задать участника возможно и не указывая его заранее в объекте ProcActor, а задав в свойстве assignee элемента User Task. Свойство может содержать строку с идентификатором пользователя CUBA: da8159cc-757f-7f59-6579-7f629ac02f72, переменную процесса, которая содержит строку с идентификатором пользователя: ${varialbeName}, или выражение вызова сервиса, который возвращает строку с идентификатором пользователя: ${someService.getSomeUserId()}. При этом свойство procRole элемента UserTask должно быть заполнено. Когда процесс достигает подобной задачи, сначала ищется объект ProcActor с указанным пользователем и процессной ролью. Если ProcActor не найден, будет создан новый экземпляр ProcActor. Для задания свойства assignee в редакторе моделей выделите элемент User Task, кликните на ссылку Show advanced properties, откройте редактор свойства Assignments и в появившемся диалоге заполните поле Assignee.

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

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

4.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>

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

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

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

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

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

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

TaskOutcomesExample
Рисунок 26. Пример переходов

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4.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>

4.11. Подмодели

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

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

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

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

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

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

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

StencilSetAddGroup
Рисунок 28. Добавление группы

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

StencilSetAddStencil
Рисунок 29. Добавление элемента

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

  • Title: Contract discount

  • Stencil 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
Рисунок 30. Панель свойств элемента Contract discount
Tip

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

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

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

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

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

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

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

5.1. ProcessRepositoryService

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

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

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

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

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

5.2. ProcessRuntimeService

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

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

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

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

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

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

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

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

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

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

5.3. ProcessFormService

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

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

5.4. ProcessMessagesService

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

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

5.5. ModelService

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

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

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

6.1. ProcActionsFragment

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

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

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

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

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

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

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

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

procActionsFragment.initializer()
        .setBeforeStartProcessPredicate(() -> {
            getScreenData().getDataContext().commit();
            return true;
        })
        .setAfterStartProcessListener(() -> {
            notifications.create()
                    .withCaption(messageBundle.getMessage("processStarted"))
                    .withType(Notifications.NotificationType.HUMANIZED)
                    .show();
        })
        .init(PROCESS_CODE, getEditedEntity());
  • Метод initializer() возвращает объект, используемый для инициализации фрагмента.

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

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

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

Самый простой способ проинициализировать ProcActionsFragment – использовать метод standard():

procActionsFragment.initializer()
        .standard()
        .init(PROCESS_CODE, getEditedEntity());

Стандартная инициализация делает следующее:

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

  • создает слушатели, показывающие сообщения "Процесс запущен", "Задача завершена" после процессного действия, а также вызывающие повторную инициализацию ProcActionsFragment.

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

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

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

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

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

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

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

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

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

  • addActionButton() – позволяет добавить к фрагменту дополнительную кнопку, помимо создаваемых автоматически.

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

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

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

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

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

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

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

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

Поставщики переменных и параметров
  • setStartProcessActionProcessVariablesSupplier() – задаёт поставщик переменных процесса. Поставщик возвращает мэп переменных процесса, которые должны быть добавлены к экземпляру процесса Activiti при запуске процесса.

  • setCompleteTaskActionProcessVariablesSupplier() – задаёт поставщик переменных процесса. Поставщик возвращает мэп переменных процесса, которые должны быть добавлены к экземпляру процесса Activiti при завершении задачи.

  • setStartProcessActionScreenParametersSupplier() – задаёт поставщик параметров экрана процессной формы. Поставщик возвращает мэп параметров экрана, которые должны быть переданы в процессную форму, отображаемую с помощью StartProcessAction.

  • setCompleteTaskActionScreenParametersSupplier() – задаёт поставщик параметров экрана процессной формы. Поставщик возвращает мэп параметров экрана, которые должны быть переданы в процессную форму, отображаемую с помощью CompleteTaskAction.



6.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 = com/haulmont/bpm/forms.xml app-bpm-forms.xml


7. Примеры

7.1. Пример Task Execution

Пример показывает:

  • Как программно задать участников процесса на старте, используя ProcActionsFragment;

  • Как передать процессные переменные на старте процесса, используя ProcActionsFragment;

  • Как получить и изменить стандартные процессные действия, автоматически сгенерированные фрагментом ProcActionsFragment (например, изменить заголовок кнопки "Запустить процесс");

  • Как программно запустить процесс без использования ProcActionsFragment;

  • Как с помощью ActivitiEventListener автоматически обновлять поле сущности (в данном примере processState) при движении по процессу.

Пример использует модель Task execution – 1:

TaskExecution1Model
Рисунок 31. Модель процесса Task execution

В текущем примере не используется StandardProcForm. Для назначения участников процесса мы воспользуемся before start process predicate фрагмента ProcActionsFragment. См. метод setBeforeStartProcessPredicate().

TaskEdit.java
@UiController("bpmsamples$Task.edit")
@UiDescriptor("task-edit.xml")
@EditedEntityContainer("taskDc")
@LoadDataBeforeShow
public class TaskEdit extends StandardEditor<Task> {

    public static final String PROCESS_CODE = "taskExecution-1";

    @Inject
    protected ProcActionsFragment procActionsFragment;

    @Inject
    protected BpmEntitiesService bpmEntitiesService;

    @Inject
    protected ProcessRuntimeService processRuntimeService;

    @Inject
    private MessageBundle messageBundle;

    @Inject
    private Notifications notifications;

    @Inject
    private Metadata metadata;

    @Inject
    private Messages messages;

    @Inject
    private InstanceLoader<Task> taskDl;

        ...

    /** * Method starts the process without {@link ProcActionsFragment} */
    @Subscribe("startProcessProgrammaticallyBtn")
    private void onStartProcessProgrammaticallyBtnClick(Button.ClickEvent event) {

        commitChanges()
                .then(() -> {
            /*The ProcInstanceDetails object is used for describing a ProcInstance to be created with its proc actors*/
            BpmEntitiesService.ProcInstanceDetails procInstanceDetails = new BpmEntitiesService.ProcInstanceDetails(PROCESS_CODE)
                    .addProcActor("initiator", getEditedEntity().getInitiator())
                    .addProcActor("executor", getEditedEntity().getExecutor())
                    .setEntity(getEditedEntity());

            /*The created ProcInstance will have two proc actors. None of the entities is persisted yet.*/
            ProcInstance procInstance = bpmEntitiesService.createProcInstance(procInstanceDetails);

            /*A map with process variables that must be passed to the Activiti process instance when it is started. This variable is used in the model to make a decision for one of gateways.*/
            HashMap<String, Object> processVariables = new HashMap<>();
            processVariables.put("acceptanceRequired", getEditedEntity().getAcceptanceRequired());

            /*Starts the process. The "startProcess" method automatically persists the passed procInstance with its actors*/
            processRuntimeService.startProcess(procInstance, "Process started programmatically", processVariables);
            notifications.create()
                    .withCaption(messageBundle.getMessage("processStarted"))
                    .withType(Notifications.NotificationType.HUMANIZED)
                    .show();

            /*refresh the procActionsFragment to display complete tasks buttons (if a process task appears for the current user after the process is started)*/
            initProcActionsFragment();
        });
    }

    private void initProcActionsFragment() {
        procActionsFragment.initializer()
                .standard()
                .setBeforeStartProcessPredicate(() -> {
                    /*the predicate creates process actors and sets them to the process instance created by the ProcActionsFragment*/
                    if (commitChanges().getStatus() == OperationResult.Status.SUCCESS) {
                        ProcInstance procInstance = procActionsFragment.getProcInstance();
                        ProcActor initiatorProcActor = createProcActor("initiator", procInstance, getEditedEntity().getInitiator());
                        ProcActor executorProcActor = createProcActor("executor", procInstance, getEditedEntity().getExecutor());
                        Set<ProcActor> procActors = new HashSet<>();
                        procActors.add(initiatorProcActor);
                        procActors.add(executorProcActor);
                        procInstance.setProcActors(procActors);
                        return true;
                    }
                    return false;
                })
                .setStartProcessActionProcessVariablesSupplier(() -> {
                    /*the supplier returns a map with process variables that will be used by the Activiti process*/
                    Map<String, Object> processVariables = new HashMap<>();
                    processVariables.put("acceptanceRequired", getEditedEntity().getAcceptanceRequired());
                    return processVariables;
                })
                .setAfterStartProcessListener(() -> {
                    /*custom listener in addition to the standard behavior refreshes the "taskDs", because the process automatically updates the "processState" field of the "Task" entity.*/
                    notifications.create()
                            .withCaption(messages.getMessage(ProcActionsFragment.class,"processStarted"))
                            .withType(Notifications.NotificationType.HUMANIZED)
                            .show();
                    initProcActionsFragment();
                    taskDl.setEntityId(getEditedEntity().getId());
                    taskDl.load();
                })
                .setAfterCompleteTaskListener(() -> {
                    notifications.create()
                            .withCaption(messages.getMessage(ProcActionsFragment.class,"taskCompleted"))
                            .withType(Notifications.NotificationType.HUMANIZED)
                            .show();
                    initProcActionsFragment();
                    taskDl.setEntityId(getEditedEntity().getId());
                    taskDl.load();
                })
                .init(PROCESS_CODE, getEditedEntity());
    }

    private ProcActor createProcActor(String procRoleCode, ProcInstance procInstance, User user) {
        ProcActor initiatorProcActor = metadata.create(ProcActor.class);
        initiatorProcActor.setUser(user);
        ProcRole initiatorProcRole = bpmEntitiesService.findProcRole(PROCESS_CODE, procRoleCode, View.MINIMAL);
        initiatorProcActor.setProcRole(initiatorProcRole);
        initiatorProcActor.setProcInstance(procInstance);
        return initiatorProcActor;
    }

    /** * Method demonstrates how to get and modify process actions automatically created by the ProcActionsFragment */
    private void changeStartProcessBtnCaption() {
        StartProcessAction startProcessAction = procActionsFragment.getStartProcessAction();
        if (startProcessAction != null) {
            startProcessAction.setCaption("Start process using ProcActionsFragment");
        }
    }
}

Метод setStartProcessActionProcessVariablesSupplier() демонстрирует, как передать процессные переменные на старте процесса с помощью ProcActionsFragment. Процессная переменная acceptanceRequired будет использоваться одним из gateway в процессе для принятия решения, должен ли процесс выполнения задачи завершиться, либо же задача должна уйти инициатору для утверждения.

Метод changeStartProcessBtnCaption() показывает, как можно получить действие запуска процесса, которое было автоматически создано фрагментом ProcActionsFragment, и поменять заголовок кнопки "Start process" на произвольный.

Метод onStartProcessProgrammaticallyBtnClick() демонстрирует, как программно запустить процесс без использования фрагмента ProcActionsFragment.

UpdateProcessStateListener.java – это реализация интерфейса org.activiti.engine.delegate.event.ActivitiEventListener. Данный слушатель зарегистрирован на уровне процесса. Он выполняет следующее: каждый раз, когда процесс входит в новое состояние, в поле processState связанной с процессом сущности com.company.bpmsamples.entity.Task проставляется имя текущего шага процесса.

UpdateProcessStateListener.java
/** * The listener updates the "processState" field of the {@link HasProcessState} with the name of current BPM process * node. This listener is used in the "taskExecution-1" BPM process */
public class UpdateProcessStateListener implements ActivitiEventListener {

    private static final Logger log = LoggerFactory.getLogger(UpdateProcessStateListener.class);

    private Metadata metadata;

    public UpdateProcessStateListener() {
        metadata = AppBeans.get(Metadata.class);
    }

    @Override
    public void onEvent(ActivitiEvent event) {
        RuntimeService runtimeService = event.getEngineServices().getRuntimeService();
        String executionId = event.getExecutionId();
        UUID entityId = (UUID) runtimeService.getVariable(executionId, "entityId");
        String entityName = (String) runtimeService.getVariable(executionId, "entityName");
        if (entityId == null) {
            log.error("Cannot update process state. entityId variable is null");
            return;
        }
        if (Strings.isNullOrEmpty(entityName)) {
            log.error("Cannot update process state. entityName variable is null");
            return;
        }
        MetaClass metaClass = metadata.getClass(entityName);
        if (metaClass == null) {
            log.error("Cannot update process state. MetaClass {} not found", entityName);
            return;
        }

        if (!HasProcessState.class.isAssignableFrom(metaClass.getJavaClass())) {
            log.error("{} doesn't implement the HasProcessState");
            return;
        }

        switch (event.getType()) {
            case ACTIVITY_STARTED:
                //activityName is the name of the current element taken from the process model
                String activityName = ((ActivitiActivityEvent) event).getActivityName();
                if (!Strings.isNullOrEmpty(activityName)) {
                    updateProcessState(metaClass, entityId, activityName);
                }
                break;
        }
    }

    /** * Method updates the process state of the entity linked with the process instance */
    private void updateProcessState(MetaClass metaClass, UUID entityId, String processState) {
        Persistence persistence = AppBeans.get(Persistence.class);
        try (Transaction tx = persistence.getTransaction()) {
            EntityManager em = persistence.getEntityManager();
            Entity entity = em.find(metaClass.getJavaClass(), entityId);
            if (entity != null) {
                ((HasProcessState) entity).setProcessState(processState);
            } else {
                log.error("Entity {} with id {} not found", metaClass.getName(), entityId);
            }
            tx.commit();
        }
    }

    @Override
    public boolean isFailOnException() {
        return false;
    }
}

Конфигурация данного слушателя в модели процесса:

TaskExecution1UpdateProcessStateListener
Рисунок 32. Конфигурация слушателя в модели процесса

Для открытия данного экрана щёлкните в пустое место в моделере, затем кликните на ссылку Show advanced properties и откройте редактор свойства Event listeners.

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

bpm.activiti.asyncExecutorEnabled

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

. . .