Досвід автоматизації непростий листування (Частина 1. Вхідний)

Досвід автоматизації непростий листування (Частина 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. Описаний вище об'єкт «Вхідний документ» повністю реалізований в опублікованому для всіх процесі Листування. Запозичуючи процес не можна втрачати час на написання такого ж обсягу, нехай і нескладного, коду.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.