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

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

Все требования участников процесса к представлению информации мне пришлось разделить на две неравные группы:
  • Требования специалиста по делопроизводству
  • Требования всех остальных сотрудников.

Оказалось, требования специалиста по делопроизводству несколько отличаются от требований всех остальных. Можно сказать так, что участники каждой группы смотрят на процесс под разными углами.
Если специалисту по делопроизводству важно, каким образом документ поступил в организацию или, напротив, каким образом должен быть отправлен адресату, то всем остальным это совершенно неинтересно.
Исходя из этого, стало понятно, что хоть пользователи и должны видеть схожие реестры входящих и исходящих писем, список атрибутов, отображаемых в реестрах, будет разным.
Изначально, мне казалось, что придется создать для каждой группы отдельный набор видов (так в easla.com называются выборки) и «разрулить» доступ к ним с помощью прав, но разобравшись, стало понятно, что в большинстве случаев можно обойтись несколькими строчками кода в самом виде.
Да-да, в easla.com, все виды описываются кодом, что позволяет достичь необыкновенной гибкости. Например, ничего не стоит построить код так, чтобы отдельному пользователю или группе отображались объекты соответствующие важному именно им критерию. Или, например, добавить в вид категории, каждой из которых соответствует отдельный фильтр.
Все виды настраиваются с помощью параметров переданных в функцию exec(). Разумеется, параметры могут быть сформированы программно в коде.
Кстати, в easla.com существует так называемый «Быстрый мастер», который позволяет создавать виды «накликиваниями». Очень помогает при создании нового вида и удобен для новичков.
Итак, опишу все виды, останавливаясь на особенностях каждого.

Входящие документы


Первый из двух самых важных реестров документов в процессе. Отражает полный перечень всех входящих документов в порядке убывания даты. Таким образом, в начале списка всегда находятся самые новые входящие письма.
$attributes = array(
    'crs_management_incoming_receive_regnum'=>array('link'=>'object', 'actions'=>array(), 'header'=>'Рег. номер получателя'),
);
if (cuser()->inGroup('group_dp') == true)
    $attributes += array('crs_management_incoming_content'=>array('link'=>'value'));
$attributes += array(
    'crs_management_incoming_receive_date',
    'crs_management_incoming_contragent_regnum'=>array('header'=>'Рег. номер отправителя'),
    'crs_management_incoming_contragent'=>array('link'=>'value'),
    'crs_management_incoming_contact'=>array('link'=>'value'),
    'crs_management_incoming_subj',
    'crs_management_incoming_outgoing'=>array('link'=>'value'),
    'crs_management_incoming_contract'=>array('link'=>'value','max'=>3),
    'crs_management_incoming_document'=>array('link'=>'value'),
    'crs_management_incoming_forwardto'=>array('link'=>'value'),
    'status'
);    
cviewpub()->exec(array(
    'object'=>objectdef('crs_management', 'crs_management_incoming'),
    'attributes'=>$attributes,
    'conditions'=>array(
        'permission'=>'2',
    ),
    'sort'=>array(
        'crs_management_incoming_receive_date'=>array('default'=>'desc', 'enable'=>true),
        'crs_management_incoming_contragent'=>array('enable'=>true)
    ),
    'sorting'=>true,
    'pagination'=>array('pagesize'=>10),
    'showcreate'=>true
));

В этом виде мне пришлось формировать список атрибутов в переменной $attributes только из-за требования специалиста по делопроизводству. Только ей нужно видеть тип содержания каждого входящего письма.
Первая колонка вида настроена так, чтобы рядом со значением атрибута crs_management_incoming_receive_regnum отображать контекстное меню объекта. Оно содержит не только стандартные действия, но и действия объявленные администратором.

Кроме этого, интересный нюанс всплыл с атрибутом crs_management_incoming_contract. Он множественный и хранит ссылки на договоры, к которым относится входящий документ. В подавляющем большинстве случаев атрибут обладал одним, максимум двумя значениями и с ним не было никаких проблем. Однако с одним из заказчиков был заключен большой контракт с более чем двадцатью доп. соглашениями. Он стал присылать нам письма, ссылаясь то на одно доп. соглашение, то на несколько, то почти на все. Специалисту по делопроизводству ничего не оставалось, как старательно указывать все доп. соглашения, на которые ссылается автор входящего письма. В итоге, число значений атрибута стало неприлично большим, а easla.com старательно отображала их все, растягивая ячейку почти на весь экран. В общем, все решилось просто, опция max позволяет ограничить число отображаемых значений, скрыв их за многоточием.

Статус входящего документа раскрашиваются самим объектом (см. в другой части), вид в этом плане настраивать не нужно. В конечном счете он выглядит вот так:


Исходящие документы


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

Конечно, если постараться, можно было все разграничить кодом, но я принял именно такое решение. В тот момент оно мне показалось наиболее верным.
Приведу код только одно вида, который предназначен для всех:
cviewpub()->categories = array('Все','Не отправлены');
cviewpub()->category = is_null(cviewpub()->category) ? 0 : cviewpub()->category;
$attributes = array(
    'crs_management_outgoing_regnum'=>array(
        'link'=>'object', 
        'actions'=>array(), 
        'export'=>array(
            'id',
            'crs_management_outgoing_regnum',
            'crs_management_outgoing_sentdate'
        )
    ),
    'crs_management_outgoing_sentdate',
    'crs_management_outgoing_performers'=>array('link'=>'value'),
    'crs_management_outgoing_contragent'=>array('link'=>'value'),
    'crs_management_outgoing_contact'=>array('link'=>'value'),
    'crs_management_outgoing_subj',
    'crs_management_outgoing_incomingdocs'=>array(
        'link'=>'value',
        'export'=>array(
            'id',
            'crs_management_incoming_contragent_regnum',
            'crs_management_incoming_contragent_date'
        )
    ),
    'crs_management_outgoing_contract'=>array('link'=>'value','max'=>3),
    'crs_management_outgoing_content'=>array('link'=>'value'),
);
$conditions = array();
switch(cviewpub()->category)
{
    case 0:
        $attributes[] = 'status';
        break;
    case 1:
        $conditions += array('status'=>array('crs_management_outgoing_create','crs_management_outgoing_created'));
        break;
}
cviewpub()->exec(array(
    'object'=>objectdef('crs_management', 'crs_management_outgoing'),
    'attributes'=>$attributes,
    'conditions'=>$conditions,
    'sort'=>array(
        'crs_management_outgoing_regdate'=>array('default'=>'desc', 'enable'=>true),
        'crs_management_outgoing_contragent'=>array('enable'=>true)
    ),
    'sorting'=>true,
    'pagination'=>array('pagesize'=>10),
    'showcreate'=>true
));

В этом виде использовал несколько крайне полезных для нас возможностей easla.com.
Прежде всего, обращу внимание на категории. С их помощью easla.com позволяет отображать вид в разных ракурсах, а проще говоря, несколько видов в одном. Хоть фильтровать по статусу можно прямо в виде, некоторые активные пользователи очень настоятельно просили создать для них отдельный вид с неотправленными исходящими письмами, на что я предложил выделить их в отдельную категорию. На этом и сошлись. Пара строчек кода:
cviewpub()->categories = array('Все','Не отправлены');
cviewpub()->category = is_null(cviewpub()->category) ? 0 : cviewpub()->category;

Они создают две категории и выбирают первую, если не указано иное. Активное значение категории используется в switch и формирует условие выбора объектов $conditions. Получилось просто и удобно.

Кроме этого, интересно остановиться на опции export, которую использовал в колонках crs_management_outgoing_regnum и crs_management_outgoing_incomingdocs. Опция позволяет при экспорте данных (об экспорте ниже), вместо, скажем, одного значений атрибута crs_management_outgoing_regnum выгружать значения атрибутов:
  • id
  • crs_management_outgoing_regnum (он же)
  • crs_management_outgoing_sentdate.

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

Исходящие и входящие для контрагента


В easla.com есть уникальная возможность, создавать виды не в контексте процесса, а в контексте объекта. Такие виды не отображаются в главном меню, а отображаются в закладках объекта, который будет их использовать. Умная система сама найдет связи между активным объектом и объектами выборки!
Мне требовалось отобразить в форме контрагента две закладки (виды): Исходящие для контрагента и Входящие от контрагента. Они позволяли наглядно представить всю переписку с выбранным контрагентом. Очень полезная функция для главного инженера проекта. Опять же, можно подобные выборки сделать в выше описанных видах, но так удобнее.
В объекте «Контрагент» мне понадобилось добавить следующие строчки:
$objectTabs = array('crm_management_contact');
if (corganization()->hasViewpub('crs_management_all_incoming_by_contragent'))
    $objectTabs[] = 'crs_management_all_incoming_by_contragent';
if (corganization()->hasViewpub('crs_management_all_outgoing_by_contragent'))
    $objectTabs[] = 'crs_management_all_outgoing_by_contragent';    
cobjectref()->objectTabs = $objectTabs;

Свойство объекта objectTabs должно содержать коды или наименования видов, чьи закладки должны появиться у объекта. Код видов для объекта получился следующий:
Исходящие для контрагента
$attributes = array(
    'crs_management_outgoing_regnum'=>array('link'=>'object'),
    'crs_management_outgoing_regdate',
    'crs_management_outgoing_contact'=>array('link'=>'value'),
    'crs_management_outgoing_subj',
    'crs_management_outgoing_incomingdocs'=>array('link'=>'value'),
    'crs_management_outgoing_contract'=>array('link'=>'value','max'=>3),
    'crs_management_outgoing_document'=>array('link'=>'value'),
    'status'=>array('actions'=>array())
);
cviewpub()->exec(array(
    'object'=>objectdef('crs_management', 'crs_management_outgoing'),
    'attributes'=>$attributes,
    'sort'=>array(
        'crs_management_outgoing_regdate'=>array('default'=>'desc', 'enable'=>true)
    ),
    'pagination'=>array('pagesize'=>10),
));


Входящие от контрагента
cviewpub()->exec(array(
    'object'=>objectdef('crs_management', 'crs_management_incoming'),
    'attributes'=>array(
        'crs_management_incoming_receive_regnum'=>array('link'=>'object'),
        'crs_management_incoming_receive_date',
        'crs_management_incoming_contragent_regnum',
        'crs_management_incoming_contact'=>array('link'=>'value'),
        'crs_management_incoming_subj',
        'crs_management_incoming_content',
        'crs_management_incoming_contract'=>array('link'=>'value','max'=>3),
        'crs_management_incoming_document'=>array('link'=>'value'),
        'status'=>array('actions'=>array())
    ),
    'conditions'=>array(
        'permission'=>'2',
    ),
    'sort'=>array(
        'crs_management_incoming_receive_date'=>array('default'=>'desc', 'enable'=>true)
    ),
    'sorting'=>true,
    'pagination'=>array('pagesize'=>10),
));


В результате, форма контрагента стала выглядеть вот так:

Помнится, в elma мне так и не удалось добиться такого результата.

Мои входящие


Вспомогательный вид, который отображает перечень всех входящих документов адресованных активному пользователю. Особенно удобен главному инженеру и ГИПам.
cviewpub()->categories = array('Все','Не рассмотрены','На исполнении','Рассмотрены');
cviewpub()->category = is_null(cviewpub()->category) ? 0 : cviewpub()->category;
$attributes = array(
    'crs_management_incoming_receive_regnum'=>array('link'=>'object', 'actions'=>array()),
    'crs_management_incoming_receive_date',
    'crs_management_incoming_contragent'=>array('link'=>'value'),
    'crs_management_incoming_contact'=>array('link'=>'value'),
    'crs_management_incoming_subj',
    'crs_management_incoming_contract'=>array('link'=>'value','max'=>3),
    'crs_management_incoming_document'=>array('link'=>'value'),
);
$conditions = array('crs_management_incoming_forwardto'=>cuser()->id, 'permission'=>'2');
switch(cviewpub()->category)
{
    case 0:
        $attributes[] = 'status';
        break;
    case 1:
        $conditions += array('status'=>array('crs_management_incoming_create','crs_management_incoming_created','crs_management_incoming_handed'));
        break;
    case 2:
        $conditions += array('status'=>array('crs_management_incoming_exec'));
        break;
    case 3:
        $conditions += array('status'=>array('crs_management_incoming_ok'));
        break;
}
cviewpub()->exec(array(
    'object'=>objectdef('crs_management', 'crs_management_incoming'),
    'attributes'=>$attributes,
    'conditions'=>$conditions,
    'sort'=>array(
        'crs_management_incoming_receive_date'=>array('default'=>'desc', 'enable'=>true)
    ),
    'sorting'=>true,
    'pagination'=>array('pagesize'=>10),
    'showcreate'=>true
));

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


Мои исходящие


Вспомогательный вид, который отображает полный перечень писем, в разработке которых участвовал активный пользователь.
$attributes = array(
    'crs_management_outgoing_regnum'=>array('link'=>'object', 'actions'=>array()),
    'crs_management_outgoing_regdate',
    'crs_management_outgoing_performers'=>array('link'=>'value'),
    'crs_management_outgoing_contragent'=>array('link'=>'value'),
    'crs_management_outgoing_contact'=>array('link'=>'value'),
    'crs_management_outgoing_subj',
    'crs_management_outgoing_incomingdocs'=>array('link'=>'value'),
    'crs_management_outgoing_contract'=>array('link'=>'value','max'=>3),
    'crs_management_outgoing_document'=>array('link'=>'value'),
    'status'
);
$conditions = array('crs_management_outgoing_performers'=>cuser()->id);
cviewpub()->exec(array(
    'object'=>objectdef('crs_management', 'crs_management_outgoing'),
    'attributes'=>$attributes,
    'conditions'=>$conditions,
    'sort'=>array(
        'crs_management_outgoing_regdate'=>array('default'=>'desc', 'enable'=>true),
        'crs_management_outgoing_contragent'=>array('enable'=>true)
    ),
    'sorting'=>true,
    'pagination'=>array('pagesize'=>10),
    'showcreate'=>true
));

Многие пользователи именно с него начинают поиск своих писем.

Экспорт


В easla.com реализован экспорт в формате Microsoft Excel (xlsx) и Microsoft Excel XML (xml), что позволяет выгружать реестры документов и обрабатывать их, приводя в нужную форму.

При экспорте выгружаются значения атрибутов, указанные в настройках вида. Иными словами, вид выгружается так же, как отображается.
Но, нам понадобилось выгружать вид, который содержал все входящие и ответные исходящие для передачи заказчику, своего рода реестр, подтверждающий факт ответа на каждое письмо обеими сторонами, и в нем надо было отдельными колонками выгрузить обозначение письма, дату его отправки и превратить в гиперссылку. Добавлять в вид соответствующие атрибуты, моветон. Пользователю надо только описание объекта, зачем ему еще два лишних атрибута. Можно создать отдельный вид с нужными атрибутами, но он будет виден в главном меню постоянно, а обращаться к нему будут только изредка. Опять некрасиво.
Оказалось, что в параметрах колонки можно указать параметр export (см. пример выше), и в нем указать список атрибутов, которые надо будет выгружать при экспорте вместо отображаемого атрибута. Безумно удобно!
В общем, с помощью параметра export дополнил все виды нужными для экспорта атрибутами и получил то, что нужно.

Выгрузка писем по реестру


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

«Натравливаем» на него:
Add-on выгрузки файлов
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Excel = Microsoft.Office.Interop.Excel;
using Office = Microsoft.Office.Core;
using TNGP_Office_Helper;
using System.Windows.Forms;
using System.IO;
using EaslaHelper;

namespace TNGP_ExcelAddIn_Specification
{
    public class Letters
    {
        const string SHEET_NAME = "Export";
        const int COLUMNS_COUN = 12;
        Excel.Workbook wb = null;

        public void RunForestRun()
        {
            ReadData();
        }

        public void ReadData()
        {
            wb = Exl.ActiveWb;
            if (wb == null)
                return;

            var exportSheet = wb.GetSheet(SHEET_NAME);

            if (exportSheet == null)
                return;

            string path = string.Empty;
            var fbd = new FolderBrowserDialog();
            fbd.Description = "Укажите путь для сохранения.";
            if (fbd.ShowDialog() == DialogResult.OK)
                path = fbd.SelectedPath;
            else
                return;

            int index = 1;
            if (wb.Path != path)
                wb.SaveAs(Path.Combine(path, wb.Name));

            var rowsCount = ExcelHelper.CountDataRows(exportSheet, 1, 1, COLUMNS_COUN);

            for (int iRow = rowsCount; iRow >= 2; iRow--)
            {
                string idOut = exportSheet.Range["A" + iRow].Text;
                string numberOut = exportSheet.Range["B" + iRow].Text;

                string pathOut = Path.Combine(path, Helper.ReplaceChars(numberOut));

                if (!Directory.Exists(pathOut))
                    Directory.CreateDirectory(pathOut);

                EaslaHelperClass.DLDocument("Out", "OutDocAttach", idOut, pathOut);
                index++;
                exportSheet.Hyperlinks.Add(Anchor: exportSheet.Range["B" + iRow], Address: Helper.ReplaceChars(numberOut), TextToDisplay: numberOut);

                string idIn = exportSheet.Range["H" + iRow].Text;
                if (!string.IsNullOrEmpty(idIn))
                {
                    string numberIn = exportSheet.Range["I" + iRow].Text;
                    string dateIn = exportSheet.Range["J" + iRow].Text;

                    string[] arrId = idIn.Split(';');
                    string[] arrNumber = numberIn.Split(';');
                    string[] arrDate = dateIn.Split(';');

                    if (arrId.Length > 1)
                    {
                        var RngToCopy = exportSheet.Rows[string.Format("{0}:{0}", iRow)];
                        for (int j = 0; j < arrId.Length - 1; j++)
                        {
                            var RngToInsert = exportSheet.Range["A" + iRow].EntireRow;
                            RngToInsert.Insert(Excel.XlInsertShiftDirection.xlShiftDown, RngToCopy.Copy(Type.Missing));
                        }
                    }

                    for (int i = 0; i < arrId.Length; i++)
                    {
                        exportSheet.Range["H" + (iRow + i)].Value2 = arrId[i];
                        exportSheet.Range["J" + (iRow + i)].Value2 = arrDate[i];

                        var numberInNew = Helper.ReplaceChars(arrNumber[i]);
                        exportSheet.Hyperlinks.Add(Anchor: exportSheet.Range["I" + (iRow + i)], Address: numberInNew, TextToDisplay: arrNumber[i]);

                        string pathIn = Path.Combine(path, numberInNew);

                        if (!Directory.Exists(pathIn))
                            Directory.CreateDirectory(pathIn);

                        EaslaHelperClass.DLDocument("In", "InDocAttach", arrId[i], pathIn);
                        index++;
                    }

                }

                double percent = index * 100 / (rowsCount * 2);
                ExcelHelper.SetStatusBarPercent(Exl.ExlApp, Helper.PercentProgressBar(percent), "Выгрузка писем");
            }

            exportSheet.Range["A:A"].EntireColumn.Hidden = true;
            exportSheet.Range["H:H"].EntireColumn.Hidden = true;

            wb.Save();
            MessageBox.Show("Выгрузка завершена!");
        }
    }
}


Вспомогательные функции
        public static DWReturnType DLDocument(string objdef, string attrs, string id, string path, string filename = "")
        {
            string objectdef = string.Empty;
            string[] attrrefs = null;

            switch (objdef)
            {
                case "Out":
                    objectdef = "crs_management_outgoing";
                    break;
                case "In":
                    objectdef = "crs_management_incoming";
                    break;
            }

            switch (attrs)
            {
                case "InDocAttach":
                    attrrefs = new string[] { "crs_management_incoming_document", "crs_management_incoming_attachments" };
                    break;
                case "InDoc":
                    attrrefs = new string[] { "crs_management_incoming_document" };
                    break;
                case "OutDocAttach":
                    attrrefs = new string[] { "crs_management_outgoing_document", "crs_management_outgoing_attachments" };
                    break;
                case "OutDoc":
                    attrrefs = new string[] { "crs_management_outgoing_document" };
                    break;
            }

            var process = EaslaApp.getProcess("crs_management");
            var object_def = EaslaApp.getObjectdef(process, objectdef, 0);

            var objectrefs = GetObjectRefs(id, object_def, attrrefs);
            if (objectrefs == null)
                return DWReturnType.Error;

            if (objectrefs.Length == 0)
                return DWReturnType.LetterNotFound;

            if (objectrefs.Length > 1)
                return DWReturnType.FoundManyLetters;

            return DownloadFile(filename, path, objectrefs[0].attributerefs);
        }

        public static DWReturnType DownloadFile(string filename, string path, Easla.AttributerefSimpleSoap[] attrs)
        {
            DWReturnType result = DWReturnType.FileNotFound;

            foreach (var attr in attrs)
            {
                if (attr.values != null)
                {
                    var files = attr.values.OfType<Easla.FileSimpleSoap>();
                    var selectfiles = files.Where(f => f.isdeleted == "0" & f.vid == "0");

                    foreach (var fss in selectfiles)
                    {
                        string filePath = Path.Combine(path, fss.nowname);

                        if (!string.IsNullOrEmpty(filename))
                        {
                            string ext = Path.GetExtension(fss.nowname);
                            filePath = Path.Combine(path, filename + ext);
                        }

                        EaslaApp.DownloadFile(fss.id, filePath);
                        result = DWReturnType.Ok;
                    }
                }
            }

            return result;
        }


Результат выглядит примерно вот так:

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

Итоги


Вопреки тому, что для описания процесса мне понадобилось написать три объемные статьи, могу смело утверждать, что создать процесс в easla.com просто и легко. На самом деле, на весь процесс управления корреспонденцией ушло 2-3 неполных рабочих дня, ведь меня еще отвлекали по текущим делам. Серьезного и внимательного подхода потребовало только действие «Отправить по эл. почте» в объекте «Исходящий документ» (см. предыдущую статью). В остальном, код объектов и атрибутов простой, повторяющийся и легко читаемый.
Убежден, что любой другой администратор процесса в easla.com легко сможет прочитать код процесса и разберется в нем в два счета.
С полной уверенностью могу настаивать, что easla.com отличная система электронного документооборота, готовая дать отпор многим аналогичным дорогим коммерческим продуктам на российском рынке!
P.S. Обращу внимание, что именно описанный процесс не доступен для заимствования только по причине наличия в нем конфиденциальных данных. Полный его аналог Переписка опубликован на сайте для общего использования без каких-либо ограничений! Будут вопросы – пишите на support@easla.com – вам обязательно ответят!

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