Производственная деятельность предприятия связана с работой ответственных должностных лиц над одним или несколькими документами, включая электронные. Порядок прохождения документов определён нормативными актами. Каждое должностное лицо заполняет ту или иную часть документа, согласовывает, утверждает документ, возвращает его на доработку, участвует в выполнении работ по документу.
Бизнес процессы, описывающие производственную деятельность принято представлять в формате BPMN, а системы, автоматизирующие бизнес-процессы, часто создают с использованием ИТ-платформ типа Camunda. Camunda выступает в роли BPMN-движка.
Camunda довольно специфический продукт, требующий привлечения специалистов соответствующей квалификации. Можно предложить подход к автоматизации бизнес-процессов, не требующий специальной подготовки аналитиков и разработчиков.
Подход к автоматизации бизнес-процессов
Детерминированный характер предметной области позволяет применять парадигму автоматного программирование, в рамках которой автоматизируемая производственная деятельность рассматривается с точки зрения теории конечных автоматов (Finite state machines (FSM)) и структурных автоматов.
Прохождение документа по своему жизненному циклу можно представить в виде конечного или структурного автомата: этапы прохождения документа определяют состояния автомата, а действия должностных лиц над документом определяют переходы между состояниями. Конечный и структурный автомат характеризуются набором состояний и правилами переходов между этими состояниями.
Конечный автомат в результате входного воздействия может перейти из одного состояния в строго одно следующее состояний. Это состояние зависит от входного воздействия.
На практике документ после рассмотрения должностным лицом может быть дальше передан на рассмотрение более чем одному должностному лицу. Соответствующий автомат будет иметь несколько активных состояний. Строго говоря такому процессу не может соответствовать конечный автомат. Назовём конечный автомат с несколькими активными состояниями структурным автоматом. Структурный автомат в результате входного воздействия может перейти из одного состояния сразу в несколько следующих состояний.
Непременным элементом структурного автомата являются особые состояния «Точка сборки»: переход автомата из точки сборки в следующие состояния происходит не по команде, а тогда, когда активны состояния на всех входах точки сборки, являющейся выходом этих активных состояний.

Пример диаграммы переходов структурного автомата
Веб-страницы документа содержит набор элементов управления, включая дата-гриды, диаграммы, командные кнопки и т.д. Каждое состояние документа может иметь свой набор элементов управления на веб-странице.
В предлагаемом подходе каждое состоянием документа связано с определённой ролью или ролями пользователя. Роль может иметь несколько связанных с ней состояний документа.
Если документ находится в определённом состоянии, то внести изменения в документ и перевести его в другие состояния может только роль, связанная с этим состоянием. Для перевода документа в другие состояния служат командные кнопки на веб-странице.
Для каждого состояния документа для роли, связанной с этим состоянием, имеется определённый набор действий, который роль должна выполнить над элементами управления веб-страницы документа. Перечень этих действий роль видит при открытии веб-страницы документа. Только после выполнения всего набора действий на веб-странице активируются командные кнопки для перевода документа в другие состояния. Перед переводом документа в другое состояния роль даёт комментарии по выполненным действиям. Комментарии видят все роли, привязанные к следующим состояниям документа.
Если роль открыла документ с неактивным состоянием, привязанным к этой роли, то роль имеет к элементам управления документа доступ только на чтение.
Если роль открыла документ с состоянием, не привязанным к этой роли, то роль имеет к элементам управления документа доступ только на чтение или вообще их не видит.
Документ может иметь несколько экземпляров. Каждый экземпляр документа находится в определённом активном состоянии. При открытии главной веб-страницы роль видит список тех экземпляров документов, у которых активное состояние привязано к этой роли. При активации ролью элемента списка открывается экземпляр документа, над которым роль может совершить действия и перевести экземпляр в другие состояния. Роль информируется о наличии у неё активных незакрытых состояний с помощью электронной почты или СМС.
Юридическая значимость электронного документа
Переход электронного документа в новое состояние подтверждается простой электронной подписью. В качестве простой электронной подписи рассматривается имя и пароль учётной записи пользователя, используемые для входа в веб-приложение. Между пользователями и администрацией составляется соглашение, устанавливающее признание электронного документа, подписанного простой электронной подписью, равнозначным документу на бумажном носителе, подписанным собственноручной подписью.
Разработка логики автоматизируемых бизнес-процессов, создание диаграмм переходов автоматов является конфигурированием системы, осуществляется аналитиками, не требует программирования и отделено от кодового каркаса веб-приложения.
Реализация
Рассматривается реализация структурного автомата для одного экземпляра документа. Веб-версия движка реализована на Python и PHP. Логика реализована с помощью SQL-запросов.
Исходные коды и руководство по установке движка можно посмотреть здесь.
Версия на Python
Скрипт fsmx.py - движок структурного автомата
#! C:/Users/P/anaconda3/python
print('Content-type: text/html; charset=utf-8\n\n')
print('Active states:')
import os
import sqlite3
import cgi
form = cgi.FieldStorage()
#Показываем ссылки для перехода в активные состояния
self=os.path.basename(__file__)
#Очистка GET-параметров
refresh = '<html><head><meta http-equiv=refresh content=0;url=http://localhost/%s></head></html>' % self
con = sqlite3.connect('fsmx.db')
con.row_factory = sqlite3.Row
cur = con.cursor()
cur.execute('select activeState, role from activeStates, roleStates where activeStates.activeState=roleStates.state')
rows = cur.fetchall()
for row in rows:
r=row['role']
ast=row['activeState']
print('<p><a href=%s?role=%d&ast=%d>ActiveState=%d. Role=%d.</a>' % (self,r,ast,ast,r))
#Показываем выбранное активное состояние
if form.getfirst('role'):
role = form.getfirst('role')
ast =form.getfirst('ast')
print('<p>-----------------------------------------------------------------------------------')
print('<p>Web page for activeState=%s, role=%s.<p>' % (ast, role))
print('<p><h2>There will be many web elements for input and editing: data grids, charts, etc.</h2>')
#Из состояния команда идёт в одно следующее состояние
cur.execute('select command, count(command) c from fsmx, roleStates where fsmx.state=roleStates.state and fsmx.state=%s and role=%s group by command having c=1' % (ast, role))
rows = cur.fetchall()
#Перебираем команды исходящие из состояния
for row in rows:
cmd=row['command']
#Формируем ссылку для перехода в другое состояние
cur.execute('select nextState from fsmx, roleStates where fsmx.state=roleStates.state and fsmx.state=%s and role=%s and command=%s' % (ast, role, cmd))
nextState = cur.fetchone()['nextState']
print('<p><a href=%s?command=%s&ast=%s>Command = %s</a>' % (self,cmd, ast, cmd))
sql = 'select role from roleStates where state=%s' % nextState
cur.execute(sql)
nextRole = cur.fetchone()['role']
#//Показываем, какое будет после выполнения команды следующее состояние и связанная с ним роль
print (' Next State = %s for role = %s' % (nextState,nextRole ))
cur.execute('select command, count(command) c from fsmx, roleStates where fsmx.state=roleStates.state and fsmx.state=%s and role=%s group by command having c>1 order by command' % (ast, role))
rows = cur.fetchall()
#//Перебираем команды исходящие из состояния
for row in rows:
cmd=row['command']
#Формируем ссылку для прехода в другИЕ состояниЯ
print( '<p><a href=%s?commandX=%s&ast=%s>Command =%s</a>' % (self, cmd, ast, cmd))
#Показываем, какИЕ будУТ после выполнения команды следующИЕ состояниЯ и связаннЫЕ с нимИ ролИ
print('In parallel: ')
cur.execute('select nextState, role from fsmx, roleStates where fsmx.state=roleStates.state and fsmx.state=%s and role=%s and command=%s' % (ast, role, cmd))
rows1 = cur.fetchall()
for row1 in rows1:
nextState = row1['nextState']
cur.execute('select role from roleStates where state=%s' % nextState)
nextRole = cur.fetchone()['role']
print('Next State = %s for role = %s' % (nextState, nextRole))
#Отрабатываем команду перехода в следующее состояние
if form.getfirst('command'):
command=form.getfirst('command')
ast=form.getfirst('ast')
#Удаляем состояние из активных состояний
cur.execute('delete from activeStates where activeState=%s' % ast)
con.commit()
#Добавляем следующее состояние в активные состояния
cur.execute('select nextState from fsmx where state=%s and command = %s' % (ast, command))
nextState = cur.fetchone()['nextState']
cur.execute('insert into activeStates(activeState) values (%s)' % nextState)
con.commit()
#Проверяем, что следующее состояние - эта точка сборки у которого все входы активны
cur.execute('select gate from gatecnt, currentgatecnt where gatecnt.cnt=currentgatecnt.cnt and gatecnt.gate=%s' % nextState)
gate = cur.fetchone()
if gate is not None:
#Удаляем точку сборки из активных состояний
gate=gate['gate']
cur.execute('delete from activeStates where activeState=%s' % gate)
con.commit()
#Активируем все состояния, выходящие из точки сборки
cur.execute('select nextState from fsmx where state=%s' % gate)
rows = cur.fetchall()
for row in rows:
cur.execute('insert into activeStates(activeState) values (%s)' % row['nextState'])
con.commit()
print(refresh)
#Отрабатываем команду перехода в следующИЕ состояниЯ
if form.getfirst('commandX'):
command=form.getfirst('commandX')
ast=form.getfirst('ast')
#Удаляем состояние из активных состояний
cur.execute('delete from activeStates where activeState=%s' % ast)
con.commit()
#Перебираем следующие состояния
cur.execute('select nextState from fsmx where state=%s and command = %s' % (ast, command))
rows = cur.fetchall()
for row in rows:
nextState=row['nextState']
cur.execute('insert into activeStates(activeState) values (%s)' % nextState )
con.commit()
#Проверяем, что следующее состояние - это точка сборки у которого все входы активны
cur.execute('select gate from gatecnt, currentgatecnt where gatecnt.cnt=currentgatecnt.cnt and gatecnt.gate=%s' % nextState)
gate = cur.fetchone()
if gate is not None:
gate=gate['gate']
#Удаляем точку сборки из активных состояний
cur.execute('delete from activeStates where activeState=%s' % gate)
con.commit()
#Активируем все состояния, выходящие из точки сборки
cur.execute('select nextState from fsmx where state=%s' % gate)
rows1 = cur.fetchall()
for row1 in rows1:
cur.execute('insert into activeStates(activeState) values (%s)' % row1['nextState'])
con.commit()
print(refresh)
cur.close()
con.close()Здесь SQLite-база fsmx.db содержит три таблицы:
fsmx — таблица переходов структурного автомата с колонками state (номер состояния), command (номер команды) и nextState (номер следующего состояния);
roleStates — таблица связки ролей с состояниями с колонками state (номер состояния) и role (номер роли);
activeState — таблица с колонкой, содержащая перечень номеров активных состояний структурного автомата.
В базу добавлены sql-представления
1. Число входов у точки сборки. Признак точка сборки: command равна -1.
CREATE VIEW gatecnt(gate, cnt) as
select nextstate gate, count(nextstate) cnt from fsmx
where nextState in (select f2.state from fsmx f2 where command = -1)
group by nextState2. Число повторяющихся активных состояний (используется для активации точки сборки)
CREATE VIEW currentgatecnt(state,cnt) as
select activeState, count(activeState) c from activeStates
group by activeState having c > 1Скрипт newActiveStates.py - задание номера начального активного состояния, например 20, для примера на github.
#! C:/Users/P/anaconda3/python
print('Content-type: text/html; charset=utf-8\n\n')
import sqlite3
con = sqlite3.connect('fsmx.db')
cur = con.cursor()
cur.execute('delete from activeStates')
cur.execute('insert into activeStates(activeState) values (20)')
con.commit()В репозитории можно видеть пример SQLite-базы, содержащей таблицу переходов структурного автомата и привязку ролей к состояниям автомата.

Использование конечных (не структурных) автоматов для автоматизации бизнес-процессов представлено в свидетельствах о государственной регистрации программы для ЭВМ №№ 2016661880, №2017661726, 2019619743.
Я не автор этих разработок.
