Опыт автоматизации непростой переписки (Часть 1. Входящий)
Работаю руководителем отдела информационных технологий в проектной организации г. Тюмени. Чуть больше года назад мне была поставлена задача, усовершенствовать процесс управления официальной корреспонденцией, работающий в то время на elma.

Взамен elma была выбрана система easla.com. Под катом много текста и кода.

Прежде всего, несколько общих фраз о самом процессе. Непосвященному он кажется простым и даже недостойным автоматизации. Собственно, изначально я думал точно также. Однако, когда выслушал стоны специалиста по делопроизводству — главного участника процесса, послушал требования главного инженера — основного потребителя процесса, понял, что все не так просто, как кажется. Оказалось, что своевременно отправленное письмо может принести организации шести, а иногда и семизначную прибыль!
В свое время процесс был автоматизирован с помощью системы elma, но в течение одного года эксплуатации стало понятно, что систему надо менять на что-то более гибкое и отзывчивое.
Итак, требования к новому процессу были следующие (короткий список):
  • Регистрация входящих и исходящих официальных писем
  • Отправка исходящих писем по эл. почте
  • Уведомление заинтересованных лиц об отправке исходящих писем
  • Возможность сохранения версий писем и совместная разработка
  • Отслеживание процесса подготовки ответа на входящее письмо

При обдумывании процесса стало понятно, что сам по себе он существовать не сможет. Понадобится несколько смежных процессов, которые станут поставщиками дополнительной информации:
Заказчики – процесс управления контрагентами и контактами. Все письма привязываются к соответствующим контрагентам и контактам.
Договоры – процесс управления договорами. Все письма в большинстве случаев привязываются к конкретному договору или даже нескольким.
Задачи – процесс управления поручениями (задачами), позволяет отслеживать исполнение поручений по каждому документу. Очень серьезный процесс, о нем лучше рассказать отдельно. Он взял на себя отслеживание подготовки ответа на входящее письмо.
В то время я уже был зарегистрирован в системе easla.com и успешно ее использовал для управления другими, менее значительными процессами организации. Поэтому, не выдумывая ничего особенного, создал в системе новый процесс. Назвал его «Переписка», следом в процессе создал объект «Входящий документ» и перешел к наполнению его атрибутами.

Атрибуты


Объект «Входящий документ» обладает целой кучей атрибутов, о каждом нужно рассказать отдельно.

Регистрационный номер получателя

Обычный порядковый номер входящего документа. Нумерация начинается заново каждый год. Простым нумератором для присвоения порядкового номера воспользоваться не удалось, он не умеет сбрасывать значение каждый год, поэтому в скрипте «До инициализации объекта» была объявлена функция:
function updateDocumentRegNum()
{
    $year = date_format(currentDateTime(), 'Y');
    $num = selectAggregateAll(
        'max',
        'crs_management',
        'crs_management_incoming',
        'crs_management_incoming_receive_regnum',
        array('crs_management_incoming_receive_date'=>array('between', $year.'-01-01', $year.'-12-31'),)
    );
    return $num + 1;
}

Функция updateDocumentRegNum вызывается из скрипта атрибута «При инициализации»:
cattributeref()->value = cobjectref()->updateDocumentRegNum();

«При изменении» атрибута происходит вызов функции updateDocumentFileName, которая отвечает за обновление имени файла сопроводительного письма (не приложений), но о ней попозже.
cobjectref()->updateDocumentFileName();


Тип отправления

Классификатор, определяющий способ отправки входящего документа в наш адрес. Может принимать следующие значения:
  • Эл. письмо
  • Бумажное письмо
  • Факсимиле
  • Передан лично

В easla.com есть возможность хранить константы в иерархически упорядоченных классификаторах. Очень удобно, позволяет избежать необходимости хранить массивы или константы в коде описания объекта или атрибута.
Скрипт атрибута «При инициализации» возвращает список вложенных классификаторов, обрабатывает их и присваивает списку допустимых значений атрибута:
$src_classificators = classificatorChilds('crs_method');
$end_classificators = array();
foreach($src_classificators as $c)
  $end_classificators += array($c['id']=>$c['name']);
if (count($end_classificators) > 0)
{
  cobjectref()->attributeref('crs_management_incoming_method')->values=$end_classificators;
  cobjectref()->attributeref('crs_management_incoming_method')->value = key($end_classificators);
}


Лично мне понравилось, что список допустимых значений может быть сформирован так просто. Скажем, в elma, подобное было куда сложнее.

Контрагент

Ссылка на объект «Контрагент» из процесса «Заказчики». С целью получения списка доступных контрагентов в скрипте объекта «До инициализации объекта» была объявлена вспомогательная функция prepareIncomingContragents:
function prepareIncomingContragents()
{
    $src_contragents = selectAll(
      'crm_management',
      'crm_management_contragent'
    );
    $end_contragents = array();
    foreach ($src_contragents as $s)
      $end_contragents += array($s['id'] => $s['description']);
    asort($end_contragents);
    return $end_contragents;
}

Функция selectAll выбирает из процесса «crm_management» все объекты «crm_management_contragent» без каких-либо условий, обрабатывает и возвращает как результат функции.
Инициализация атрибута происходит в скрипте объекта «После инициализации объекта»:
cobjectref()->attributeref('crs_management_incoming_contragent')->values = prepareIncomingContragents();

Обращаю внимание, что не в самом атрибуте, а в объекте. Так тоже можно. Иногда именно такое решение может иметь большое значение.
Выбор контрагента пользователем должен приводить к изменению списка доступных контактов и договоров. Список доступных контактов и договоров формируется в скрипте атрибута «При изменении»:
if (!empty(cattributeref()->value))
{
    $contacts = cobjectref()->prepareIncomingContacts(cattributeref()->value);
    cobjectref()->attributeref('crs_management_incoming_contact')->values = $contacts;
    cobjectref()->attributeref('crs_management_incoming_performers')->values = $contacts;
    
    $contracts = cobjectref()->prepareContracts(cattributeref()->value);
    if (empty($contracts))
        $contracts = cobjectref()->prepareContracts();
    cobjectref()->attributeref('crs_management_incoming_contract')->values = $contracts;
}

Используются вспомогательные функции объявленные в скрипте объекта «При инициализации»:
function prepareIncomingContacts($contragent = null)
{
    if (empty($contragent))
        $src_contacts = selectAll(
          'crm_management',
          'crm_management_contact',
          array('crm_management_contact_contragent.description'),
          null
        //   array('with'=>'title')
        );
    else
        $src_contacts = selectAll(
          'crm_management',
          'crm_management_contact',
          array('crm_management_contact_contragent.description'),
          array('crm_management_contact_contragent'=>$contragent)
        );
        
    $end_contacts = array();
    $processed_contact = array(); 
    $processed_description = array();
    foreach ($src_contacts as $s)
    {
        $e = array_search($s['description'], $processed_description);
        if($e === false) {
            $processed_contact += array($s['id'] => $s);
            $processed_description += array($s['id'] => $s['description']);
            $end_contacts += array($s['id'] => $s['description']);
        } else {
            $end_contacts[$e] = $processed_contact[$e]['description'].' ['.trim($processed_contact[$e]['crm_management_contact_contragent.description']).']';
            $end_contacts += array($s['id'] => $s['description'].' ['.trim($s['crm_management_contact_contragent.description']).']');
        }
    }
    unset($processed_contact,$processed_description);
    asort($end_contacts);
    return $end_contacts;
}

Функция является наглядным примером того, почему скриптовое описание поведения лучше, чем «накликиваемое» мышкой. В данном случае хитрость в том, что в списке контактов могут встречать полные тезки, т.е. фамилия, имя и отчество могут полностью совпасть. Отличить одного от другого в сформированном списке будет нереально. Поэтому, функция просматривает весь список контактов, находит одинаковые и приписывает к ним названия организаций, в которых они работают. Конечно, чисто теоретически, в одной организации также могут работать полные тезки, но пока такой ситуации ни разу не возникало.
Кроме этого используется вспомогательная функция для формирования списка договоров:
function prepareContracts($contragent = null)
{
    if (empty($contragent))
        $src_contracts = selectAll(
          'agr_management',
          'agr_management_contract'
        );
    else
        $src_contracts = selectAll(
          'agr_management',
          'agr_management_contract',
          array(),
          array('agr_management_contract_contragent'=>$contragent)
        );
        
    $end_contracts = array();
    foreach ($src_contracts as $s)
      $end_contracts += array($s['id'] => $s['description']);
    asort($end_contracts);
    return $end_contracts;
}

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

Контакт

Ссылка на объект «Контакт» в процессе «Заказчики». Все вспомогательные функции необходимые для инициализации значений атрибута описаны выше, поэтому остается только добавить, что инициализация атрибута происходит в скрипте объекта «После инициализации объекта» (следом за инициализацией списка контрагентов):
$contacts = prepareIncomingContacts();
cobjectref()->attributeref('crs_management_incoming_contact')->values = $contacts;

Кстати, пользователь, при заполнении формы входящего письма может не указывать контрагента, а сразу указать нужный контакт. Система сама определит его контрагента и подставит значение. Реализовать такую хитрость удалось в скрипте атрибута «При изменении»:
if (empty(cobjectref()->attributeref('crs_management_incoming_contact')->value))
    return;
    
if (empty(cobjectref()->attributeref('crs_management_incoming_contragent')->value))
{
    $contact = select(cobjectref()->attributeref('crs_management_incoming_contact')->value);
    
    if (empty($contact))
        return;
    
    $contragent_id = $contact->attributeref('crm_management_contact_contragent')->value;
    cobjectref()->attributeref('crs_management_incoming_contragent')->value = $contragent_id;
    cobjectref()->attributeref('crs_management_incoming_performers')->values = cobjectref()->prepareIncomingContacts($contragent_id);
    
    $contracts = cobjectref()->prepareContracts($contragent_id);
    if (empty($contracts))
        $contracts = cobjectref()->prepareContracts();
    cobjectref()->attributeref('crs_management_incoming_contract')->values = $contracts;    
}

Помимо определения контрагента, обновляется и список доступных договоров.

Исполнители

Ссылка на объект «Контакт» в процессе «Заказчики». Предназначен для хранения информации о том, кто именно разработал письмо на стороне заказчика. Это такая короткая приписка в конце письма, дескать исполнил такой-то. Кстати, исполнителей бывает несколько, поэтому и атрибут множественный. Позволяет сохранить не одного, а целый список исполнителей. Инициализация атрибута происходит также в скрипте объекта «После инициализации объекта»:
cobjectref()->attributeref('crs_management_incoming_performers')->values = $contacts;


Дата отправления

Собственно, хранит дату отправления входящего письма, которую специалист по делопроизводству берет из документа. Важно знать, когда письмо было отправлено и когда получено. Частенько эти даты совпадают в нашем быстром XXI веке, поэтому для простоты заполнения атрибуту присваивается текущая дата и время в скрипте «При инициализации»:
cobjectref()->attributeref('crs_management_incoming_contragent_date')->value = currentDateTime();


Регистрационный номер отправителя

Как следует из названия атрибута, он предназначен для хранения регистрационного номера вх. письма присвоенного отправителем. Важный атрибут, т.к. обе стороны зачастую ориентируются при поиске письма именно по этому регистрационному номеру, а не по регистрационному номеру получателя, описанному выше. Однако, бывает и так, что рег. номер отправителя отсутствует вовсе, поэтому изначально атрибуту присваивается «б/н» в скрипте «При инициализации»:
cattributeref()->value = 'б/н';


Дата получения

Очевидно, хранит дату получения входящего письма. Также, как и дате отправления присваивается текущая дата и время в скрипте «При инициализации»:
cobjectref()->attributeref('crs_management_incoming_receive_date')->value = currentDateTime();

Но «При изменении» значения атрибута осуществляется обновление имени файла с помощью функции updateDocumentFileName:
cobjectref()->updateDocumentFileName();


Дата получения оригинала

Очередная дата, зачастую остается пустой. Предназначен атрибут для хранения даты получения оригинала письма, которое наперед было получено другим образом.

Оповестить о получении оригинала

При первоначальной разработке объекта этого атрибута не было. Он появился через несколько недель после начала эксплуатации. Смысл его существования в том, чтобы знать, надо уведомлять заинтересованных лиц о приходе оригинала входящего письма или нет. Кроме того, если оповещение было отправлено, атрибут меняет значение на «Оповещен».
В easla.com список предопределенных значений атрибута можно сформировать даже простым массивом. Вот так просто добавил список с тремя возможными значениями в скрипте атрибута «При инициализации»:
cattributeref()->values = array('Нет','Да','Оповещен');
if (empty(cattributeref()->value))
    cattributeref()->value = 0;


Тема

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

Тип содержания

Классификатор, позволяющий определить тип содержания письма. Перечень возможных значений хранится также, как и тип отправления. Всего у нас используется порядка 25 типов содержания, вот некоторые из них:
  • Ответы на замечания
  • Запрос исходных данных
  • Выдача исходных данных
  • Постановление
  • Подтверждение технической возможности
  • Согласование в рамках авторского надзора и т.п.

Список допустимых значений формируется аналогично атрибуту «Тип отправления»:
$src_classificators = classificatorChilds('crs_content');
$end_classificators = array();
foreach($src_classificators as $c) {
  $end_classificators += array($c['id']=>$c['name']);
  if ($c['code'] == 'crs_content_other')
    $default = $c['id'];
}
if (count($end_classificators) > 0)
{
  cobjectref()->attributeref('crs_management_incoming_content')->values=$end_classificators;
  cobjectref()->attributeref('crs_management_incoming_content')->value = isset($default) ? $default : $c['id'];
}


Кому

Пользователь (сотрудник) организации, которому направлено официальное письмо. В нашем случае письма приходят только высшему руководству и главным инженерам проектов (ГИПам). Таким образом, мне понадобилось ограничить список доступных пользователей. Инициализация значений осуществляется опять в скрипте атрибута «При инициализации»:
$src_users = corganization()->allUsersByGroups(array(
    'group_general_manager',
    'group_general_engineer',
    'group_general_manager_operations',
    'group_gip',
    'group_general_manager_economics'
    ), null);
    
$end_users = array();
foreach($src_users as $u) 
    if ($u['islocked'] == 0)
        $end_users += array($u['id']=>$u['description']);
asort($end_users);
cobjectref()->attributeref('crs_management_incoming_to')->values = $end_users;

Хитрая функция allUsersByGroups умеет возвращать массив пользователей входящих в указанные группы. Она же умеет исключать пользователей из массива. В общем, в моем случае, без сложных фильтров получил всех нужных пользователей, обработал и присвоил значениям атрибута.
Письмо может быть переадресовано другому сотруднику, но на всякий случай в скрипте «При изменении» изначально устанавливается тот же сотрудник:
if (empty(cobjectref()->attributeref('crs_management_incoming_forwardto')->value))
{
    cobjectref()->attributeref('crs_management_incoming_forwardto')->value = cobjectref()->attributeref('crs_management_incoming_to')->value;
}


Переадресовать

Наверное, в половине случаев, письмо, адресованное генеральному директору, на самом деле должно быть отправлено на рассмотрение не ему, а его заместителю или главному инженеру, т.к. несете в себе сугубо техническое содержание. Поэтому входящее письмо при регистрации направляется сотруднику, ответственному за решение вопросов описанных в письме.
Список доступных пользователей формируется в скрипте атрибута «При инициализации»:
$src_users = corganization()->allUsers();
$end_users = array();
foreach($src_users as $u) 
    if ($u['islocked'] == 0)
        $end_users += array($u['id']=>$u['description']);
asort($end_users);
cobjectref()->attributeref('crs_management_incoming_forwardto')->values = $end_users;


В ответ на исходящее

Ссылка на «Исходящий документ» этого же процесса. Очень важный атрибут, т.к. позволяет связать в цепочку все входящие и исходящие письма и отследить ее в случае необходимости. В скрипте «До инициализации объекта» написана вспомогательная функция prepareOutgoings, которая формирует список всех исходящих писем:
function prepareOutgoings()
{
    $src_documents = selectAll(
      'crs_management',
      'crs_management_outgoing'
    );
        
    $end_documents = array();
    foreach ($src_documents as $d)
      $end_documents += array($d['id'] => $d['description']);
    
    return $end_documents;
}

Она вызывается «При инициализации» атрибута:
$outgoings = cobjectref()->prepareOutgoings();
$outgoings = array_reverse($outgoings, true);
asort($outgoings);
cattributeref()->values = $outgoings;

Атрибут является множественным, таким образом входящее письмо может быть зарегистрировано как ответное сразу на несколько наших писем.

Договор

Ссылка на объект «Договор» в процессе «Договоры». Еще один важный атрибут, позволяющий классифицировать письма по договорам, что упрощает поиск и формирование отчетов в будущем. Атрибут множественный, что позволяет отнести входящее письмо к нескольким договорам сразу. Такое бывает довольно часто.
Вспомогательные функции для инициализации значений атрибута были описаны выше, поэтому только укажу, что его значения формируются в скрипте «После инициализации объекта» следом за инициализацией исполнителей:
cobjectref()->attributeref('crs_management_incoming_contract')->values = prepareContracts();


Документ

Вот в этом атрибуте и хранится файл входящего документа. Атрибут не множественный, что заставляет хранить в нем только один файл, и ни в коем случае, не несколько.
После загрузки файла на форму, происходит его автоматическое переименование с помощью скрипта атрибута «При изменении»:
cobjectref()->updateDocumentFileName();
В переименовании используется вспомогательная функция updateDocumentFileName описанная в скрипте «До инициализации объекта»:
function updateDocumentFileName()
{
    $desc = calcIncomingDesc();
    if (empty($desc))
        return;
        
    $files = cobjectref()->attributeref('crs_management_incoming_document')->availableFiles();
    foreach ($files as $f)
    {
        $nowname = sprintf(
            '%s.%s', 
            $desc,
            pathinfo($f->nowname, PATHINFO_EXTENSION)
        );
        if (strcmp($nowname, $f->nowname) == 0)
            continue;
            
        $f->nowname = $nowname;
        $f->save();
    }
}

Новое имя файла формируется из описания объекта, которое, в свою очередь, формируется с помощью функции calcIncomingDesc (о ней позже).

Приложения

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

Отправитель оповещен о передаче письма адресату

Интересный атрибут, отображает факт отправки уведомления отправителю о том, что его письмо было передано на рассмотрение. Был добавлен по просьбе специалиста по делопроизводству, которая жаловалась, что ей регулярно приходится отвечать на звонки со стороны заказчика с просьбой сообщить, достигло ли их ис письмо адресата или нет. В итоге, появился атрибут и автоматическая отправка соответствующего уведомления отправителю.
Атрибут может принимать одно из трех значений и изначально оно, разумеется, равно «Нет»:
cattributeref()->values = arraY('Нет','Невозможно','Да');
cattributeref()->value = 0;

В том случае, если отправить уведомление не получилось, скажем, не указан эл. адрес контакта, то после передачи вх. письма на рассмотрение атрибут примет значение «Невозможно». Но это редкость. В большинстве случаев уведомление успешно отправляется и атрибут принимает значение «Да».
Мне запомнилась реакция некоторых заказчиков на появление такого уведомления. Они были просто в восторге. Даже не думал, что такая мелочь может вызвать такую бурную положительную реакцию!

Объект


Одно из интересных требований было «раскрасить» статусы входящих писем для увеличения наглядности.


В easla.com достаточно дял этого добавить в скрипт «После инициализации объекта» вот такой код:
switch (cobjectref()->status->code) 
{
    case 'crs_management_incoming_created':
        cobjectref()->status->state = 1;
        break;
    case 'crs_management_incoming_handed':
        cobjectref()->status->state = 2;
        break;
    case 'crs_management_incoming_exec':
        cobjectref()->status->state = 3;
        break;
    case 'crs_management_incoming_ok':
        cobjectref()->status->state = 4;
        break;
}

Всего можно присвоить до 9 цветов. Все цвета радуги от 1 до 7. Белый — 8. Черный – 9.
Кстати, чтобы объект получал описание (изначально оно пустое), была написана вспомогательная функция в скрипте «До инициализации объекта»:
function calcIncomingDesc()
{
    if (empty(cobjectref()->attributeref('crs_management_incoming_receive_date')->value))
        return;
        
    if (empty(cobjectref()->attributeref('crs_management_incoming_receive_regnum')->value))
        return;
        
    $d = date_create(cobjectref()->attributeref('crs_management_incoming_receive_date')->value);
    return sprintf(
        '%s от %s',
        cobjectref()->attributeref('crs_management_incoming_receive_regnum')->value,
        localeFormatDate($d)
    );
}

Которая была вызвана в скрипте «Перед сохранением объекта»:
cobjectref()->description = calcIncomingDesc();

Кроме этого, перед сохранением объекта происходит смена статуса и проверка присвоенного в момент создания регистрационного номера. Этой возможности мне очень не хватало в elma, она сперва сохраняет объект, а потом… потом уже поздно!
if (cobjectref()->status->code == 'crs_management_incoming_create')
{
  cobjectref()->status = 'crs_management_incoming_created';
  cobjectref()->flags = 1;
}
else
  cobjectref()->flags = 0;
  
if (cobjectref()->isNewRecord &&
    !empty(cobjectref()->attributeref('crs_management_incoming_receive_regnum')->value) &&
    !empty(cobjectref()->attributeref('crs_management_incoming_contragent')->value))
{
    $year = date_format(date_create(cobjectref()->attributeref('crs_management_incoming_receive_date')->value), 'Y');
    $conditions = array(
        'crs_management_incoming_receive_regnum'=>cobjectref()->attributeref('crs_management_incoming_receive_regnum')->value,
        'crs_management_incoming_contragent'=>array('id',cobjectref()->attributeref('crs_management_incoming_contragent')->value),
        'crs_management_incoming_receive_date'=>array('between', $year.'-01-01', $year.'-12-31'),
    );
        
    $exist = selectCountAll('crs_management','crs_management_incoming',$conditions);
    if ($exist)
        throw new Exception('Невозможно сохранить документ, т.к. существует другой документ с рег. номером '.cobjectref()->attributeref('crs_management_incoming_receive_regnum')->value.' созданный в '.$year.' году!');
}
if (cobjectref()->isNewRecord &&
    !empty(cobjectref()->attributeref('crs_management_incoming_contragent_regnum')->value) &&
    !empty(cobjectref()->attributeref('crs_management_incoming_contragent_date')->value) &&
    cobjectref()->attributeref('crs_management_incoming_contragent_regnum')->value != 'б/н')
{
    $year = date_format(date_create(cobjectref()->attributeref('crs_management_incoming_contragent_date')->value), 'Y');
    $conditions = array(
        'crs_management_incoming_contragent_regnum'=>array('strict',cobjectref()->attributeref('crs_management_incoming_contragent_regnum')->value),
        'crs_management_incoming_contragent'=>array('id',cobjectref()->attributeref('crs_management_incoming_contragent')->value),
        'crs_management_incoming_contragent_date'=>array('between', $year.'-01-01', $year.'-12-31'),
    );
        
    $exist = selectCountAll('crs_management','crs_management_incoming',$conditions);
    if ($exist)
        throw new Exception('Невозможно сохранить документ, т.к. существует другой документ с рег. номером отправителя '.cobjectref()->attributeref('crs_management_incoming_contragent_regnum')->value.' зарегистрированный в '.$year.' году!');
}

В конечном счете получилась вот такая форма входящего документа:


Статусы


Разобравшись с объектом и его атрибутами, озадачился статусами. В easla.com существуют три типа статусов: начальный, обычный и конечный. Начальный присваивается объекту в момент создания, т.е. когда объект еще не сохранен и открыта форма создания объекта. Конечный блокирует любые попытки изменить объект, хотя, с помощью действий это можно обойти.
После коротких обсуждений сошелся на следующих статусах входящего документа:
Регистрация – начальный статус
Зарегистрирован – присваивается сразу после регистрации
Передан адресату – присваивается после отправки «переадресату» письма с требованием рассмотреть документ
Рассмотрен – присваивается после назначения задач для подготовки ответа (задачи исполняются в смежном процессе «Задачи»)
Аннулирован – присваивается, если письмо нужно аннулировать
На этом этапе уже можно было вздохнуть полегче. Надо было показать форму специалисту по делопроизводству, т.к. именно ей предстояло работать с ней большую часть времени. Создали с ней один объект. Поглядели, что получилось. В общем, получил от нее положительный отклик и пучок пожеланий.

Действия


В easla.com перевести объект из одного статуса в другой можно только действием. Действие – это кнопка под формой объекта, которая выполняет скрипт в контексте объекта.

На рассмотрение

Переводит объект из статуса «Зарегистрирован» в «Передан адресату» и отправляет письмо «переадресату» с просьбой рассмотреть входящий документ. Скрипт действия получился несложный:
if (empty(cobjectref()->attributeref('crs_management_incoming_forwardto')->value))
    throw new Exception("Не указан переадресат!");
$forwardto = corganization()->user(cobjectref()->attributeref('crs_management_incoming_forwardto')->value);
$groups = $forwardto->groups();
$isgip = false;
foreach($groups as $group)
    if (strncmp($group['data_one'],'09.',3) == 0) {
        $isgip = true;
        break;
    }
if ($isgip) 
    $to = $group->users();
else
    $to = $forwardto;
cobjectref()->description = cobjectref()->calcIncomingDesc();
cobjectref()->status = 'crs_management_incoming_handed';
sendEmail(array(
    'from'=>cuser(),
    'to'=>$to,
    'subj'=>cobjectref()->description.' на рассмотрение',
    'body'=>'Добрый день! Требуется рассмотрение входящего документа.',
    'objects'=>cobjectref(),
    'roles'=>'crs_management_all',
));

Особенностью действия является отправка не одного, а нескольких писем в том случае, если входящий документ переадресован ГИПу. ГИПы сами попросили отправлять копию письма в адрес их помощника, т.к. могут быть заняты другой работой или находится в командировке и письмо «зависнет» на неопределенный срок. Конечно, было понятно, что ГИПы лукавят, они вполне могли заниматься рассмотрением письма даже удаленно, но вступать с ними в спор бессмысленно. Поэтому, после согласования решения с высшим руководством, реализовал отправку копии письма помощнику ГИПа (мы их называем «гипятами»).

Добавить задачу

Сервисная команда, позволяет создать задачу с входящим письмом в основании для открытия.
$subj = '';
if (!empty(cobjectref()->attributeref('crs_management_incoming_subj')->value))
    $subj = $subj.(strlen($subj) > 0 ? ' ' : '').cobjectref()->attributeref('crs_management_incoming_subj')->value;
$new_task = new Objectref();
$new_task->prepare(objectDef('tsk_management','tsk_task'));
if (!empty(cobjectref()->attributeref('crs_management_incoming_contract')->value))
    $new_task->attributeref('tsk_task_contract')->value = cobjectref()->attributeref('crs_management_incoming_contract')->value[0];
    
$new_task->attributeref('tsk_task_subj')->value = $subj;
$category = classificator('task_category_answer');
if (!empty($category))
    $new_task->attributeref('tsk_task_category')->value = $category->id;
$new_task->attributeref('tsk_task_base_open')->value = cobjectref()->id;
caction()->redirect = urlNewObjectref($new_task);

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

Рассмотрен

Используется в том случае, когда входящий документ не требует создания задач и должен только сменить статус. Проще не придумаешь:
cobjectref()->status = 'crs_management_incoming_ok';


Добавить контакт...

В easla.com существует возможность привязки действий к атрибутам объекта. Такие действия отличаются от обычных тем, что отображаются не внизу формы объекта, а рядом с указанным атрибутом. Причем я, как администратор, могу выбирать с какой стороны разместить кнопку: сверху, снизу, слева, справа.
Точкой входа корреспонденции является специалист по делопроизводству, поэтому она регулярно сталкивается с необходимостью регистрации новых контрагентов и контактов. Порой обнаруживается, что недостающего контакта нет в списке, а форма с входящим документом почти заполнена.
В общем я и создал для нее сервисную функцию, которая открывает новую вкладку в браузере и позволяет добавить новый контакт всего четырьмя строчками кода:
$new_contact = new Objectref();
$new_contact->prepare(objectDef('crm_management','crm_management_contact'));
caction()->target = '_blank';
caction()->redirect = urlNewObjectref($new_contact);

Разумеется, действие привязано к атрибуту «Контакт».

Добавить контрагента...

По аналогии создал действие для создания нового контрагента и привязал его к атрибуту «Контрагент».
$new_contact = new Objectref();
$new_contact->prepare(objectDef('crm_management','crm_management_contragent'));
caction()->target = '_blank';
caction()->redirect = urlNewObjectref($new_contact);


Ответить

Честно признаюсь, как-то не сразу додумался до этого действия, хотя потребность в нем ощущалась сразу. Ведь в каждом почтовике есть кнопочка «Ответить», которая создает исходящее письмо и заполняет два поля: кому и тема. Но хорошо когда только 2 поля надо заполнить, можно и исходящее создавать без сервисной команды, а когда у нас 19 атрибутов и почти все надо заполнить, чтобы отправить исходящее письмо, действие просто незаменимо!
Мне казалось, что столкнусь со сложностями, однако все оказалось проще, чем думал:
$new_outgoing = new Objectref();
$new_outgoing->prepare(objectDef('crs_management','crs_management_outgoing'));
$new_outgoing->attributeref('crs_management_outgoing_contragent')->value = cobjectref()->crs_management_incoming_contragent;
$new_outgoing->attributeref('crs_management_outgoing_contact')->value = cobjectref()->crs_management_incoming_contact;
$new_outgoing->attributeref('crs_management_outgoing_recipients')->value = cobjectref()->crs_management_incoming_performers;
$new_outgoing->attributeref('crs_management_outgoing_responsibleuser')->value = cobjectref()->crs_management_incoming_forwardto;
$new_outgoing->attributeref('crs_management_outgoing_incomingdocs')->value = cobjectref()->id;
$new_outgoing->attributeref('crs_management_outgoing_contract')->value = cobjectref()->crs_management_incoming_contract;
caction()->redirect = urlNewObjectref($new_outgoing);


Уведомить о получении оригинала

Помните атрибут «Оповестить о получении оригинала»? Вот именно для этого действия он и был добавлен. Когда специалист по делопроизводству получает оригинал документа, то вызывает это действие и все!
if (empty(cobjectref()->attributeref('crs_management_incoming_receive_original_date')->value))
    throw new Exception('Не указана дата получения оригинала!');
    
if (cobjectref()->attributeref('crs_management_incoming_receive_original_flag')->value != 1)
    throw new Exception('Оповещение не требуется или сотрудник уже оповещен');
    
$forwardto = cobjectref()->attributeref('crs_management_incoming_forwardto')->value;
sendEmail(array(
    'from'=>cuser(),
    'to'=>corganization()->user($forwardto),
    'subj'=>cobjectref()->description.' получен оригинал',
    'body'=>'Добрый день! Получен оригинал входящего документ '.cobjectref()->viewLink().' ['.cobjectref()->attributeref('crs_management_incoming_contragent_regnum')->value.']'
));
cobjectref()->attributeref('crs_management_incoming_receive_original_flag')->value = 2;


Роли

Кстати, именно на этом этапе я опомнился, что нужно определиться с ролями и правами доступа! Есть во мне такая плохая черта, сперва все сделать, а потом только думать, кому нельзя давать то, что сделано.
Состоялся весьма бурный диалог с генеральным директором, а потом с главным инженером, из которого я вынес необходимость создания следующих ролей:
  • Все сотрудники
  • Высшее руководство
  • Бухгалтерия
  • Главные инженеры проектов
  • Делопроизводитель
  • Проектный офис

Вот таким образом распределились права доступа к входящему документу.

Но это не все. Нашим входящим документам нужно очень избирательно назначать права доступа в соответствии со значением атрибута «Тип содержания». В общем, в скрипте «После сохранения объекта» добавил назначение прав на только что сохраненный объект:
if (!empty(cobjectref()->attributeref('crs_management_incoming_content')->value))
{
    $c = classificator(cobjectref()->attributeref('crs_management_incoming_content')->value);
    //претензия
    if ($c->code == 'crs_content_claim')
    {
        cobjectref()->addRolesPermissions(array(
            'crs_management_tops',
            'crs_management_buh',
            'crs_management_gip',
            'crs_management_dp',
            'crs_management_pmo',
        ));
    }
    // //постановление, отзыв, определение
    elseif (in_array($c->code, array('crs_content_ruling','crs_content_review','crs_content_decision')))
    {
        cobjectref()->addRolesPermissions(array(
            'crs_management_tops',
            'crs_management_buh',
            'crs_management_dp',
            'crs_management_gip',
        ));
    }
}

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

Пока все


Про настройку объекта «Исходящий объект» в следующей части. Спасибо всем, кто дочитал до этого места. Надеюсь каждый нашел для себя что-то полезное.
P.S. Описанный выше объект «Входящий документ» полностью реализован в опубликованном для всех процессе Переписка. Заимствуя процесс можно не терять время на написание такого же объема, пусть и несложного, кода.

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


  1. ecocor
    18.04.2016 17:16

    Очень интересно)


    1. Alxdhere
      18.04.2016 17:17

      Спасибо. Дальше будет еще интереснее. Исходящий документ — это целая эпопея.


  1. infom
    18.04.2016 18:33

    Зашел на сайт easla.com… вообще не понял про что они и на каком основании можно было их выбрать для автоматизации процессов?


    1. Alxdhere
      19.04.2016 06:17

      Дело в том, что в момент принятия решения о выборе easla.com, у меня уже был опыт ее использования, на что я указал в статье. Система молодая и неизвестная, но очень гибкая, благодаря своей «скриптовости».
      Кстати, за свои 15-16 лет работы с разными системами документооборота (AutoManager, Lotsia PDM Plus, TDMS, Elma, Sharepoint и др.) мне удалось выработать у себя нулевую реакцию на внешний вид как самой системы, так и ее рекламных материалов, в том числе и сайтов. Предпочитаю изучать систему изнутри. Не на примитивных примерах, которые порой демонстрирует продавец/интегратор, а на своих собственных. Вот так было и с easla.com. Я развернул на ней простой, с точки зрения «айтишных» возможностей, процесс управления инцидентами, в котором реализовал и прием инцидентов от пользователей, и уведомление сотрудников отдела ИТ о поступлении/переназначении инцидентов, и классификацию, и приоритезацию, и назначение плановых сроков устранения в рабочих часах в соответствии с категорией и срочностью, и извещение пользователей о процессе устранения инцидентов. Этого оказалось достаточно, чтобы поверить в возможности easla.com.
      P.S. Взгляните, скажем, на сайт elma. Он отлично рекламирует свой продукт. И про BPMN 2.0 написано, и про блок-схемы, с помощью которых можно построить бизнес-процессы. Все так здорово, что в 2013 году я тоже на это купился. Но так от нее устал за год эксплуатации…


    1. GarbageIntegrator
      20.04.2016 14:53

      Я думаю, интерес к анализу таких систем возникает тогда, когда нужно не просто автоматизировать, а автоматизировать для увеличения эффективности. В нашем предприятии, например, использовалась система «Дело». Да, тоже можно складывать, да, тоже можно указывать «в ответ на...» и т.д… Но. когда вопрос встает о том, чтобы связать «Дело» с другой системой — поневоле начинаешь анализировать возможности «Дела» в этом отношении. Да и вообще возможности «Дела».

      Любая система обладает некоторыми возможностями выражения логики. Эта система имеет очень широкие возможности. Например, есть модуль для SQL-сервера, который позволяет делать выборку информации из системы. Возможности такие появились не на пустом месте, а результате многолетнего опыта работы с другими системами. В статье описано, как они использованы, на примере «вх/исх». На самом деле, автоматизировано больше. Что позволяет очень хорошо экономить время. Возможно, Александр опишет и другие решения.