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

Бизнес процессы, описывающие производственную деятельность принято представлять в формате BPMN, а системы, автоматизирующие бизнес-процессы, часто создают с использованием ИТ-платформ типа Camunda. Camunda выступает в роли BPMN-движка.

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

Подход к автоматизации бизнес-процессов

Если предметная область имеет детерминированный характер, то можно применять парадигму автоматного программирование, в рамках которой автоматизируемая производственная деятельность рассматривается с точки зрения теории конечных автоматов (Finite state machines).

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

Детерминированный конечный автомат в результате входного воздействия может перейти из одного состояния в строго одно следующее состояний. Это состояние зависит от входного воздействия.

На практике документ после рассмотрения должностным лицом может быть дальше передан на рассмотрение более чем одному должностному лицу. Соответствующий автомат будет иметь несколько активных состояний и будет являться недетерминированным конечным автоматом.

Добавим в недетерминированный конечный автомат особый тип состояния «Точка сборки»: переход автомата из точки сборки в следующие состояния происходит не по команде, а тогда, когда осуществлён переход в точку сборки изо всех состояний, имеющих выход в эту точку сборки. Назовём такой автомат структурным автоматом.

Пример 1. Диаграмма переходов структурного автоматаЕсли в состоянии 2 выбрана команда 1, то одновременно активируются состояния 4 и 5. Если затем и в состоянии 4 и в состоянии 5 выбрана команда 1, то активируется точка сборки 6 и далее одновременно активируются состояния 7, 8 и 11
Пример 1. Диаграмма переходов структурного автомата
Если в состоянии 2 выбрана команда 1, то одновременно активируются состояния 4 и 5. Если затем и в состоянии 4 и в состоянии 5 выбрана команда 1, то активируется точка сборки 6 и далее одновременно активируются состояния 7, 8 и 11

Веб-приложение для реализация бизнес-процесса

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

Логикой прохождения документа по своему жизненному циклу управляет движок веб-приложения, реализующий структурный автомат. Логика приложения зашита в таблицу переходов автомата и отделена от кодового каркаса движка веб-приложения. Разработка логики автоматизируемых бизнес-процессов, создание диаграмм переходов автоматов является конфигурированием системы, осуществляется аналитиками и не требует программирования.

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

Если документ находится в определённом состоянии, то внести изменения в документ и перевести его в другие состояния может только роль, связанная с этим состоянием. Для перевода документа в другие состояния служат командные кнопки или ссылки на веб-странице.

Для каждого состояния документа для роли, связанной с этим состоянием, имеется определённый набор действий, который роль должна выполнить над элементами управления веб-страницы документа. Только после выполнения всего набора действий на веб-странице активируются командные кнопки или ссылки для перевода документа в другие состояния.

Пример 2. Диаграмма переходов структурного автомата реального бизнес-процессаЕсли электронный документ находится в состоянии 3 и роль 3 откроет веб-страницу документа, то она увидит командные кнопки или ссылки 1 и 2.  При нажатии ролью 3 на кнопку или ссылку 1 документ перейдёт в состояние 4 с которым связана роль 4.При нажатии ролью 3 на кнопку или ссылку 2 документ перейдёт в состояние 6 с которым связана роль 2.Если документ находится в состоянии 6 и роль 2  нажмёт на кнопку или ссылку  1,  то документ перейдёт сразу в два состояния: 7  и 8  с которыми связаны роли 2 и 3, соответственно. Если каждая из этих ролей нажмёт на веб-странице документа кнопку  или ссылку 1, то сработает точка сборки 18 и документ прейдёт в состояние 9 с которым связана роль 2.
Пример 2. Диаграмма переходов структурного автомата реального бизнес-процесса
Если электронный документ находится в состоянии 3 и роль 3 откроет веб-страницу документа, то она увидит командные кнопки или ссылки 1 и 2. 
При нажатии ролью 3 на кнопку или ссылку 1 документ перейдёт в состояние 4 с которым связана роль 4.
При нажатии ролью 3 на кнопку или ссылку 2 документ перейдёт в состояние 6 с которым связана роль 2.
Если документ находится в состоянии 6 и роль 2  нажмёт на кнопку или ссылку 1,  то документ перейдёт сразу в два состояния: 7  и 8  с которыми связаны роли 2 и 3, соответственно. Если каждая из этих ролей нажмёт на веб-странице документа кнопку или ссылку 1, то сработает точка сборки 18 и документ прейдёт в состояние 9 с которым связана роль 2.

Если роль открыла документ с неактивным состоянием, привязанным к этой роли, то роль имеет к элементам управления документа доступ только на чтение.

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

Документ может иметь несколько экземпляров. Каждый экземпляр документа находится в определённом активном состоянии.

Юридическая значимость электронного документа

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

Реализация движка структурного автомата

Рассматривается реализация движка структурного автомата  для одного экземпляра документа. Веб-версия движка реализована на Python и PHP. Логика реализована с помощью SQL-запросов.

Исходные коды, примеры структурных автоматов в виде файлов SQLite-баз и руководство по установке движка можно посмотреть на github.

Скрипт - движок структурного автомата.

Версия на Python:

#! C:/Users/p/anaconda3/python
print('Content-type: text/html; charset=utf-8\n\n')
print('<img src=fsmx1.png height="50%">')
print('<p><h2>Active states:</h2>')
import os
import sqlite3
import cgi

form = cgi.FieldStorage()
#Я
self=os.path.basename(__file__)
refresh = '<html><head><meta http-equiv=refresh content=0;url=http://localhost/%s></head></html>' % self
con = sqlite3.connect('fsmx1.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>State=%d for role=%d.</a>' % (self,r,ast,ast,r))

print('<p>Select to go!</p>')
#Показываем выбранное  активное состояние
if form.getfirst('role'):
	role = form.getfirst('role')
	ast =form.getfirst('ast')
	print('<p>-----------------------------------------------------------------------------------')
	print('<p><h2>This is the web page for state=%s and role=%s.</h2></p>' % (ast, role))
	print('<p><h3>Here will be many web elements for input and editing: data grids, charts, etc.</h3></p>')
	print('<br>')
	print('<p><h3>Commands for transition to other states:</h3>')
	#Из состояния команда идёт в одно следующее состояние
	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 LEFT JOIN roleStates ON fsmx.state=roleStates.state where 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))
		cur.execute('select role from roleStates where state=%s' % nextState)
		print (' next state = %s ' % nextState)
		nextRole = cur.fetchone()['role']
		#Показываем, какое будет после выполнения команды следующее состояние и связанная с ним роль
		print ('for role = %s.' % 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))
		#Показываем, какИЕ будУТ после выполнения команды следующИЕ состояниЯ и связаннЫЕ с нимИ ролИ
		cur.execute('select nextState, role from fsmx LEFT JOIN roleStates ON fsmx.state=roleStates.state where fsmx.state=%s and role=%s and command=%s' % (ast, role, cmd))
		rows1 = cur.fetchall()
		for row1 in rows1:
			nextState = row1['nextState']
			print (' next state = %s ' % nextState)
			cur.execute('select role from roleStates where state=%s' % nextState)
			nextRole = cur.fetchone()['role']
			print('for role = %s,' % 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()

Версия на PHP:

<html>
<img src=fsmx1.png height="60%">
<h2>Active states:</h2></html> 
<?php
$self=$_SERVER['PHP_SELF']; 
$db = new SQLite3('fsmx1.db');

//Показываем ссылки для перехода в активные состояния 
$result = $db->query('select activeState, role from activeStates, roleStates where activeStates.activeState=roleStates.state');
while ($row = $result->fetchArray(SQLITE3_ASSOC)){
	$r=$row['role']; $as=$row['activeState'];
	echo '<p><a href='.$self.'?role='.$r.'&as='.$as.'>State='.$as.' for role='.$r.'</a>';}
echo '<p>Select to go!</p>';
//Показываем выбранное  активное состояние
if(isset($_GET['role'])) {
	$role=$_GET['role']; $as=$_GET['as'];
	echo '<p>-----------------------------------------------------------------------------------';
	echo '<p><h2>This is the web page for state='.$as.' and role='.$role.'.</h2></p>';
	echo '<p><h3>There will be many web elements for input and editing: data grids, charts, etc.</h3>';
	//echo '<br>';
	echo '<p><h3>Commands for transition to other states:</h3>';
	//Из состояния команда идёт в одно следующее состояние
	$result = $db->query('select command, count(command) c from fsmx, roleStates where fsmx.state=roleStates.state and fsmx.state='.$as.' and role='.$role.' group by command having c=1');
	//Перебираем команды исходящие из состояния
	while ($row = $result->fetchArray(SQLITE3_ASSOC)){
		$cmd=$row['command'];
		//Формируем ссылку для перехода в другое состояние		
		//$nextState = $db->querySingle('select nextState from fsmx, roleStates where fsmx.state=roleStates.state and fsmx.state='.$as.' and role='.$role.' and command='.$cmd);
		$nextState = $db->querySingle('select nextState from fsmx LEFT JOIN roleStates ON fsmx.state=roleStates.state where fsmx.state='.$as.' and role='.$role.' and command='.$cmd);
		echo '<p><a href='.$self.'?command='.$cmd.'&as='.$as.'>Command = '.$cmd.'. </a>';
		$nextRole = $db->querySingle('select role from roleStates where state='.$nextState);
		//Показываем, какое будет после выполнения команды следующее состояние и связанная с ним роль
		echo ' next state = '.$nextState.' for role = '.$nextRole.',';}
	//Из состояния команда идёт в несколько следующих состояний
	$result = $db->query('select command, count(command) c from fsmx, roleStates where fsmx.state=roleStates.state and fsmx.state='.$as.' and role='.$role.' group by command having c>1 order by command');
	//Перебираем команды исходящие из состояния
	while ($row = $result->fetchArray(SQLITE3_ASSOC)){
		$cmd=$row['command'];
                //Формируем ссылку для прехода в другИЕ состояниЯ
		echo '<p><a href='.$self.'?commandX='.$cmd.'&as='.$as.'>Command = '.$cmd.'. </a>';
		//Показываем, какИЕ будУТ после выполнения команды следующИЕ состояниЯ и связаннЫЕ с нимИ ролИ
		//$result1 = $db->query('select nextState, role from fsmx, roleStates where fsmx.state=roleStates.state and fsmx.state='.$as.' and role='.$role.' and command='.$cmd);
		$result1 = $db->query('select nextState, role from fsmx LEFT JOIN roleStates ON fsmx.state=roleStates.state where fsmx.state='.$as.' and role='.$role.' and command='.$cmd);
		while ($row1 = $result1->fetchArray(SQLITE3_ASSOC)){
			$nextState = $row1['nextState'];
			$nextRole = $db->querySingle('select role from roleStates where state='.$nextState);
			echo ' next state = '.$nextState.' for role = '.$nextRole.',';}}}

//Отрабатываем команду перехода в следующее состояние 
if(isset($_GET['command'])) {
	$as=$_GET['as'];
	//Удаляем состояние из активных состояний
	$db->query('delete from activeStates where activeState='.$as);
	//Добавляем следующее состояние в активные состояния
	$nextState = $db->querySingle('select nextState from fsmx where state='.$as.' and command ='.$_GET['command']);
	$db->query('insert into activeStates(activeState) values ('.$nextState.')');
        //Проверяем, что следующее состояние - это шлюз у которого все входы активны
	$gate = $db->querySingle('select gate from gatecnt, currentgatecnt where gatecnt.cnt=currentgatecnt.cnt and gatecnt.gate='.$nextState);
	if(isset($gate)) {
		//Удаляем шлюз из активных состояний
		$db->query('delete from activeStates where activeState='.$gate);
		//Активируем все состояния, выходящие из шлюза
		$result = $db->query('select nextState from fsmx where state='.$gate);
		while ($row = $result->fetchArray(SQLITE3_ASSOC))
			$db->query('insert into activeStates(activeState) values ('.$row['nextState'].')');}
	header('Location: http://localhost/'.$self); }

//Отрабатываем команду перехода в следующИЕ состояниЯ 
if(isset($_GET['commandX'])) {
	$cmd=$_GET['commandX'];
	$as=$_GET['as'];
	//Удаляем состояние из активных состояний
	$db->query('delete from activeStates where activeState='.$as);
	//Перебираем следующие состояния
	$result = $db->query('select nextState from fsmx where state='.$as.' and command ='.$cmd);
        while ($row = $result->fetchArray(SQLITE3_ASSOC)){
		$nextState=$row['nextState'];
		$db->query('insert into activeStates(activeState) values ('.$nextState.')');
		//Проверяем, что следующее состояние - это шлюз у которого все входы активны
        	$gate = $db->querySingle('select gate from gatecnt, currentgatecnt where gatecnt.cnt=currentgatecnt.cnt and gatecnt.gate='.$nextState);
		if(isset($gate)) {
			//Удаляем шлюз из активных состояний
			$db->query('delete from activeStates where activeState='.$gate);
			//Активируем все состояния, выходящие из шлюза
			$result1 = $db->query('select nextState from fsmx where state='.$gate);
			while ($row1 = $result->fetchArray(SQLITE3_ASSOC))
				$db->query('insert into activeStates(activeState) values ('.$row1['nextState'].')');}}
	header('Location: http://localhost/'.$self);  }  ?>

SQLite-база fsmx.db содержит три таблицы:

  1. fsmx — таблица переходов структурного автомата с колонками state (номер состояния), command (номер команды) и nextState (номер следующего состояния);

  2. roleStates — таблица связки ролей с состояниями с колонками state (номер состояния) и role (номер роли); к точкам сборки и финальному состоянию роли не привязываются;

  3. 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 nextState

 2.      Число повторяющихся активных состояний. Используется для активации точки сборки, когда число переходов в точку сборки равно числу её входов.

CREATE VIEW currentgatecnt(state,cnt) as
select activeState, count(activeState) c from activeStates 
group by activeState having c > 1

Скрипт для задание номера начального активного состояния, например 20, для примера 2 выше.

Python:

#! 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()

PHP:

<?php
$db = new SQLite3('fsmx.db');
$db->query('delete from activeStates');
$db->query('insert into activeStates(activeState) values (20)'); ?>

Комментарии (2)


  1. risik
    28.08.2023 20:59
    +2

    Конечный автомат в результате входного воздействия может перейти из одного состояния в строго одно следующее состояний. Это состояние зависит от входного воздействия.

    Мне кажется, что следует уточнить, что Вы здесь описываете детерминированный конечный автомат (ДКА). Ведь речь про него?

    Соответствующий автомат будет иметь несколько активных состояний. Назовём конечный автомат с несколькими активными состояниями структурным автоматом.

    А чем Ваш «Структурный автомат» будет отличаться от обычного недетерминированного конечного автомата (НДКА)?

    Кстати, любой НДКА можно преобразовать в эквивалентный ему ДКА.


    1. vmg19571007 Автор
      28.08.2023 20:59

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

      Согласен, что недетерминированный конечный автомат может иметь несколько активных состояний. Однако у моего структурного автомата присутствует ещё и особое состояние "Точка сборки". Мой структурный автомат нельзя назвать НДКА.

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

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