Управление задачами в проектной организации

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

    Нетривиальные задачи потребовали нетривиального подхода. Подробное описание с картинками и исходным кодом под катом.

    Проектная организация, численностью порядка 200 человек. В высшем руководстве 4 человека, в прямом подчинении главного инженера 4 главных инженера проекта (ГИП) и столько же их помощников, плюс все начальники отделов.

    Главный инженер в день может поручать от 5 до 15 задач своим прямым подчиненным, которые, в свою очередь, могут делегировать выполнение задач нескольким начальникам отделов, а те своим подчиненным. Классическая иерархическая схема. Таким образом, количество активных задач в единицу времени может достигать 600-800! Удержать их все в голове просто нереально, а в условиях хромающей исполнительской дисциплины вопрос контроля становится жизненно важным.

    В организации в тот момент уже несколько лет использовался MS Project, правда, для управления проектами в целом, а не краткосрочными поручениями. Идею использовать его для управления краткосрочными поручениями отмели после краткого обсуждения.
    Учитывая наличие опыта использования системы easla.com для управления корреспонденцией решили попробовать использовать ее же для управления задачами. Тем более, задачи предполагали тесную интеграцию с перепиской.

    Задача


    В смысле, не задача, а постановка задачи. Изначально все-таки планировалось сделать задачи простыми: тема, описание, автор, исполнитель, плановые и фактические даты. Поэтому и требования были простыми:
    • Регистрировать задачу в системе
    • Автоматически определять автора и позволять выбирать любого исполнителя
    • Автоматически вычислять плановые даты
    • При смене статусов фиксировать фактические даты.

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

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

    Решение


    Прежде всего, вопросы вызвали задачи нескольким исполнителям. Если создать одну задачу для всех, т.е. не персональную, то вероятность ее исполнения уменьшится почти до нуля. Каждый исполнитель будет надеяться на другого. Не знаю, как у других, но у нас именно так. Поэтому все задачи должны быть индивидуальными, так что, пришлось реализовать механизм клонирования задачи каждому исполнителю.
    Затем надо было определиться с важностью задачи. Ввели три типа важности и каждому назначили максимальный срок исполнения:
    • Высокая (8 рабочих часов, т.е. рабочий день)
    • Обычная (40 рабочих часов, т.е. рабочая неделя)
    • Низкая (80 рабочих часов).

    Трудозатраты тоже интересная тема. Некоторые сотрудники затрудняются с указанием потраченного времени. Даже не знаю, как такое пояснить, но то ли боятся поставить слишком мало, то ли боятся указать слишком много, поэтому ввели приблизительную шкалу с достаточной для анализа точностью:
    • Нисколько
    • Секунды
    • Несколько минут
    • 15 минут
    • Полчаса
    • 45 минут
    • Целый час
    • Больше часа и т.д.

    Определившись с инструментом и принципами работы процесса, приступил к реализации.

    Реализация


    В easla.com создал новый процесс «Задачи». В нем создал объект «Задача». Объект наделил следующими атрибутами.

    Атрибуты


    Номер

    Обычный счетчик для последовательной нумерации.

    Обозначение

    Строковый атрибут. Значение вычисляется после создания задачи и не может быть изменено пользователем. В режим «только для чтения» атрибут переводится в скрипте «При инициализации»:
    cobjectref()->attributeref('tsk_task_code')->readonly = true;
    


    Автор

    Пользователь, т.е. сотрудник организации. Автором может быть любой сотрудник организации. Полный список сотрудников формируется в скрипте «При инициализации»:
    $src_users = corganization()->users();
    $end_users = array();
    foreach ($src_users as $u)
      $end_users += array($u->id => $u->description);
    cobjectref()->attributeref('tsk_task_author')->values = $end_users; 
    cobjectref()->attributeref('tsk_task_author')->value = cuser()->id;
    cobjectref()->attributeref('tsk_task_author')->readonly = true;
    

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

    Исполнитель

    Сотрудник организации, которому поручено выполнение задачи. Множественный атрибут, т.к. одна и та же задача может выполняться разными специалистами, а, скажем, ГИП соберет все вместе в одно решение. Список сотрудников формируется в скрипте «При инициализации»:
    $src_users = corganization()->group('group_all')->users();
    $end_users = array();
    foreach ($src_users as $u)
        if ($u['islocked'] == 0)
            $end_users += array($u->id => $u->description);
    asort($end_users);
    cobjectref()->attributeref('tsk_task_executor')->values = $end_users; 
    cattributeref()->size = 6;
    


    Договор

    Ссылка на объект «Договор». Инициализация атрибута происходит скрипте объекта «Задача».

    Тема

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

    Описание

    Многострочный строковый атрибут для подробного описания поставленной задачи. Описание может менять только автор задачи, поэтому в скриптах «При инициализации» и «При изменении» прописано:
    cattributeref()->readonly = cuser()->id != cobjectref()->attributeref('tsk_task_author')->value;
    


    Уведомить об исполнении

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


    Категория задачи

    Классификатор, определяющий категорию задачи. Сейчас их всего три:
    • Подготовка ответа на письмо
    • Решение планерки
    • Прочее.

    Список допустимых значений формируется в скрипте «При инициализации»:
    $src_classificators = classificatorChilds('task_category');
    $end_classificators = array();
    foreach($src_classificators as $c)
      $end_classificators += array($c['id']=>$c['name']);
    if (count($end_classificators) > 0)
    {
      cobjectref()->attributeref('tsk_task_category')->values = $end_classificators;
      cobjectref()->attributeref('tsk_task_category')->value = key($end_classificators);
    }
    

    При изменении категории меняется «необходимость» атрибута "Основание для закрытия".
    $src_classificators = classificatorChilds('task_category');
    foreach($src_classificators as $c)
        if ($c['id'] == cattributeref()->value)
            break;
            
    if (empty($c))
        return;
        
    if ($c['code'] == 'task_category_answer') {
        cobjectref()->attributeref('tsk_task_base_open')->isRequired = true;
    }
    


    Важность

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

    «При изменении» происходит пересчет плановой даты закрытия задачи:
    if (empty(cattributeref()->value))
        return;
    cobjectref()->calcPlanEndDate(cattributeref()->value);
    

    Функция calcPlanEndDate описана в самом объекте.

    Основание для открытия

    Ссылка на объект, в частности, входящий или исходящий документ, который и стал основанием для появления задачи. Список входящих и исходящих документов формируется в скрипте «При инициализации»:
    cattributeref()->readonly = cobjectref()->attributeref('tsk_task_description')->readonly;
    $base = cobjectref()->prepareIncomings();
    $base += cobjectref()->prepareOutgoings();
    $base = array_reverse($base, true);
    cattributeref()->values = $base;
    


    Приложения

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

    Дата начала (план)

    Плановые дата и время начала выполнения задачи. Договорились о том, что она будет назначаться со смещением +1 час к текущему времени, что и прописано в скрипте «При инициализации»:
    cattributeref()->readonly = cobjectref()->attributeref('tsk_task_description')->readonly;
    cattributeref()->value = calendarDateAdd(currentDateTime(), 3600);
    

    Изменить может только автор. Отдельное внимание на функцию calendarDateAdd, она вычисляет плановую дату начала в соответствии с производственным календарем!

    Дата окончания (план)

    Плановые дата и время окончания выполнения задачи. Зависит от важности и плановой даты начала. Начальное значение вычисляется «При инициализации»:
    cattributeref()->readonly = cobjectref()->attributeref('tsk_task_description')->readonly;
    if (empty(cattributeref()->value) && !empty(cobjectref()->tsk_task_importance)) {
        cobjectref()->calcPlanEndDate(cobjectref()->tsk_task_importance);
    }
    


    Дата начала (факт) и Дата окончания (факт)

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

    Основание для закрытия

    Ссылка на объект, а именно, исходящий документ, который стал основанием для закрытия задачи. Более того, если категория задачи «Подготовка ответа на входящее», то задача не может быть закрыта, пока не будет заполнено основание для закрытия. Список доступных исходящих документов формируется в скрипте «При инициализации»:
    $base = cobjectref()->prepareIncomings();
    $base += cobjectref()->prepareOutgoings();
    $base = array_reverse($base, true);
    cattributeref()->values = $base;
    


    Трудозатраты

    Целочисленный атрибут, содержащий количество секунд потраченных на выполнение задачи исполнителем. Так как задачи предполагаются краткосрочные, списка допустимых значений предостаточно. Он формируется при инициализации атрибута:
    cattributeref()->values = array(
    0=>'Нисколько',
    1=>'Секунды',
    5=>'Несколько минут',
    15=>'15 минут',
    30=>'Полчаса',
    45=>'45 минут',
    60=>'Целый час',
    75=>'Больше часа',
    90=>'Полтора часа',
    105=>'Почти два часа',
    120=>'2 часа',
    150=>'2 часа 30 минут',
    360=>'3 часа',
    240=>'Полдня',
    480=>'Целый день',
    960=>'2 дня',
    1440=>'3 дня',
    1920=>'4 дня',
    2400=>'Рабочая неделя',
    4800=>'Две недели',
    7200=>'Три недели',
    9600=>'Целый месяц',
    19200=>'Два месяца',
    19200=>'Два месяца',
    28800=>'Три месяца',
    );
    cattributeref()->value = 0;
    


    Комментарии

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

    Объект


    Объект «Задача» обладает непростым поведением и валидацией, которые описаны в его скриптах. Вспомогательные функции для инициализация атрибутов и вычисления даты и времени описаны в скрипте:
    До инициализации объекта
    function calcTskCode($num)
    {
      return 'ЗАДАЧА-'.sprintf('%06d', $num);
    }
    function prepareContracts()
    {
        $src_contracts = selectAll(
          'agr_management',
          'agr_management_contract'
        );
            
        $end_contracts = array();
        foreach ($src_contracts as $s)
          $end_contracts += array($s['id'] => $s['description']);
        asort($end_contracts);
        return $end_contracts;
    }
    function prepareIncomings()
    {
        $src_documents = selectAll(
          'crs_management',
          'crs_management_incoming',
          array('crs_management_incoming_contragent_regnum')
        );
            
        $end_documents = array();
        foreach ($src_documents as $d)
          $end_documents += array($d['id'] => $d['description'].' ['.$d['crs_management_incoming_contragent_regnum'].']');
        //asort($end_documents);
        return $end_documents;
    }
    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']);
        //asort($end_documents);
        return $end_documents;
    }
    function calcPlanEndDate($importance)
    {
        if (empty($importance))
            return;
            
        $c = classificator($importance);
        if (empty($c))
            return;
        
        $delta = 0;
        switch ($c['code']) {
            case 'tsk_importance_01':
                $delta = 28800;
                break;
            case 'tsk_importance_02':
                $delta = 144000;
                break;
            case 'tsk_importance_03':
                $delta = 288000;
                break;
        }
        cobjectref()->attributeref('tsk_task_plan_enddate')->value = calendarDateAdd(currentDateTime(), $delta);
    }
    if (cobjectref()->hasAttributeref('tsk_task_contract'))
        cobjectref()->attributeref('tsk_task_contract')->values = prepareContracts();
    cobjectref()->childTabs = array('tsk_task_sub');
    cobjectref()->childAll = false;
    


    Отдельно обращу внимание на функцию calcPlanEndDate, которая для вычисления плановой даты и времени использует функцию calendarDateAdd. С ее помощью удается рассчитать время именно в рабочих часах с учетом производственного календаря организации.

    Дополнительная инициализация атрибутов и вычисление состояния плановых дат начала и окончания осуществляется в скрипте:
    После инициализации объекта
    if (cobjectref()->hasAttributeref('tsk_task_base_open') && empty(cobjectref()->attributeref('tsk_task_base_open')->value) && !empty(cobjectref()->parentrefId))
    {
        $parent = select(cobjectref()->parentrefId);
        if (!empty($parent))
            cobjectref()->attributeref('tsk_task_base_open')->value = $parent->attributeref('tsk_task_base_open')->value;
    }
    cobjectref()->attributeref('tsk_task_code')->value = calcTskCode(cobjectref()->attributeref('tsk_task_num')->value);
    if (!cobjectref()->inFinalStatus())
    {
        if (!empty(cobjectref()->tsk_task_plan_startdate) && (cobjectref()->status->code == 'tsk_task_initiated' || cobjectref()->status->code == 'tsk_task_created'))
        {
            if (cobjectref()->tsk_task_plan_startdate instanceof DateTime)
                $dts_plan = date_timestamp_get(cobjectref()->tsk_task_plan_startdate);
            else
                $dts_plan = date_timestamp_get(date_create(cobjectref()->tsk_task_plan_startdate));
                
            $dts_now = date_timestamp_get(date_create());
            if ($dts_plan < $dts_now)
                cobjectref()->attributeref('tsk_task_plan_startdate')->state = 1;
            elseif ($dts_plan - $dts_now < 3600)
                cobjectref()->attributeref('tsk_task_plan_startdate')->state = 2;
            elseif ($dts_plan - $dts_now < 28800)
                cobjectref()->attributeref('tsk_task_plan_startdate')->state = 3;
            else
                cobjectref()->attributeref('tsk_task_plan_startdate')->state = 4;
        }
        
        if (!empty(cobjectref()->tsk_task_plan_enddate) && (cobjectref()->status->code == 'tsk_task_initiated' || cobjectref()->status->code == 'tsk_task_created' || cobjectref()->status->code == 'tsk_task_processed'))
        {
            if (cobjectref()->tsk_task_plan_enddate instanceof DateTime)
                $dts_plan = date_timestamp_get(cobjectref()->tsk_task_plan_enddate);
            else
                $dts_plan = date_timestamp_get(date_create(cobjectref()->tsk_task_plan_enddate));
                
            $dts_now = date_timestamp_get(date_create());
            if ($dts_plan < $dts_now)
                cobjectref()->attributeref('tsk_task_plan_enddate')->state = 1;
            elseif ($dts_plan - $dts_now < 3600)
                cobjectref()->attributeref('tsk_task_plan_enddate')->state = 2;
            elseif ($dts_plan - $dts_now < 28800)
                cobjectref()->attributeref('tsk_task_plan_enddate')->state = 3;
            else
                cobjectref()->attributeref('tsk_task_plan_enddate')->state = 4;
        }
    }
    if (!empty(cobjectref()->tsk_task_author))
    {
        $pgroup = array('group_pdg');
        $agroups = corganization()->user(cobjectref()->tsk_task_author)->groups();
        foreach ($agroups as $ag)
            if (in_array($ag['code'], $pgroup))
            {
                cobjectref()->attributeref('tsk_task_author')->readonly = false;
            }
        
    }
    if (cobjectref()->hasAttributeref('tsk_task_notice_of_execute')) {
        if (empty(cobjectref()->attributeref('tsk_task_notice_of_execute')->value)) {
            cobjectref()->attributeref('tsk_task_notice_of_execute')->value = 0;
        }
        cobjectref()->attributeref('tsk_task_notice_of_execute')->readonly = $cuser_id != cobjectref()->attributeref('tsk_task_author')->value;
    }
    if (cobjectref()->hasAttributeref('tsk_task_category')) {
        if (empty(cobjectref()->attributeref('tsk_task_category')->value)) {
            $values = cobjectref()->attributeref('tsk_task_category')->values;
            cobjectref()->attributeref('tsk_task_category')->value = key($values);
        }
        $category_id = cobjectref()->attributeref('tsk_task_category')->value;
        $category_classificator = classificator($category_id);
        
        if ($category_classificator->code == 'task_category_plan') {
            $ro = cuser()->id != cobjectref()->attributeref('tsk_task_author')->value;
            cobjectref()->attributeref('tsk_task_category')->readonly = $ro;
            cobjectref()->attributeref('tsk_task_plan_startdate')->readonly = $ro;
            cobjectref()->attributeref('tsk_task_plan_enddate')->readonly = $ro;
        }
    }
    cobjectref()->attributeref('tsk_task_plan_enddate')->readonly = $cuser_id != cobjectref()->attributeref('tsk_task_author')->value;
    


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

    Перед сохранением объекта важно выполнить валидацию всех введенных значений и отказать в его сохранении, если что-то не так.
    Помимо выявления ошибок, происходит проверка на существование похожей задачи по трем признакам: теме, основанию для открытия и исполнителю. Если найдена точно такая же задача, то в назначении новой отказано. Очень и очень полезная фишка!
    Кроме этого, как было упомянуто выше, задача должна быть назначена только одному исполнителю, а все остальные в списке должны получить ее копии, поэтому задача сохраняется только с одним исполнителем, а остальные сохраняются в аргументах объекта.
    Кстати, список исполнителей анализируется на наличие в нем ГИПа. И если он найден, то задача ГИПа становится основной, а все остальные создаются как подзадачи к ней. Такое упорядочивание задач очень удобно для ГИПа.
    До сохранения объекта
    $executors = cobjectref()->attributeref('tsk_task_executor')->value;
    if (count($executors) > 1)
    {
        $gips = corganization()->group('group_gip_only')->users();
        $fgip = false;
        foreach ($gips as $gip)
            if (in_array($gip['id'], $executors)) {
                $fgip = true;
                break;
            }
            
        $hgips = corganization()->group('group_gip_helper_only')->users();
        $fhgip = false;
        foreach ($hgips as $hgip)
            if (in_array($hgip['id'], $executors)) {
                $fhgip = true;
                break;
            }
        
        if ($fgip) {
            cobjectref()->attributeref('tsk_task_executor')->value = $gip->id;
            $this->arguments['executor'] = array_diff($executors, array($gip->id));
            $this->arguments['executorIsChild'] = true;
        } elseif ($fhgip) {
            cobjectref()->attributeref('tsk_task_executor')->value = $hgip->id;
            $this->arguments['executor'] = array_diff($executors, array($hgip->id));
            $this->arguments['executorIsChild'] = true;
        } else {
            cobjectref()->attributeref('tsk_task_executor')->value = $executors[0];
            $this->arguments['executor'] = array_slice($executors, 1);
        }
    }
    if (!empty($executors) &&
        cobjectref()->attributeref('tsk_task_executor')->existValue != cobjectref()->attributeref('tsk_task_executor')->value)
    {
        $conditions = array(
            'tsk_task_subj'=>cobjectref()->tsk_task_subj,
            'tsk_task_base_open'=>cobjectref()->tsk_task_base_open,
            'tsk_task_executor'=>$executors[0],
        );
        
        if (!cobjectref()->isNewRecord)
            $conditions['id'] = '<>'.cobjectref()->id;
            
        $exist = selectAll('tsk_management', 'tsk_task', array(), $conditions);
        if (count($exist) > 0) {
            $exs_task_links = array();
            $executor = corganization()->user($executors[0]);
            foreach ($exist as $x) {
                $exs_task = select($x['id']);
                $exs_task_links[] = $exs_task->viewLink().' для '.$executor->viewLink().' Статус: '.$exs_task->status->viewLink();
            }
            throw new Exception('Невозможно назначить задачу, т.к. найдены подобные задачи:'.implode('',$exs_task_links));      
        }
    }
    if (cobjectref()->status->code == 'tsk_task_initiated')
    {
      cobjectref()->status = 'tsk_task_created';
      cobjectref()->flags = 1;
    }
    elseif (!cobjectref()->isNewRecord && (cobjectref()->status->code == 'tsk_task_created' || cobjectref()->status->code == 'tsk_task_processed'))
    {
        $src_user_id = cobjectref()->attributeref('tsk_task_executor')->existValue;
        $trg_user_id = cobjectref()->attributeref('tsk_task_executor')->value;
        $src_user_id = $src_user_id[0];
        $trg_user_id = $trg_user_id[0];
        if ($src_user_id != $trg_user_id) 
        {
            $conditions = array(
                'tsk_task_subj'=>cobjectref()->tsk_task_subj,
                'tsk_task_base_open'=>cobjectref()->tsk_task_base_open,
                'tsk_task_executor'=>$trg_user_id,
            );
            
            $exist = selectAll('tsk_management', 'tsk_task', array(), $conditions);
            
            if (count($exist) > 0) {
                $exs_task_links = array();
                foreach ($exist as $x) {
                    $exs_task = select($x['id']);
                    $exs_task_links[] = $exs_task->viewLink().' для '.corganization()->user($trg_user_id)->viewLink().' Статус: '.$exs_task->status->viewLink();
                    echo 'Задача не переназначена, т.к. найдены подобные задачи для указанного сотрудника:'.implode('',$exs_task_links); 
    
                }
            } else {
                sendEmail(array(
                    'to'=>corganization()->user($trg_user_id),
                    'subj'=>cobjectref()->attributeref('tsk_task_code')->value.' переназначена',
                    'body'=>'Вам переназначена задача от '.corganization()->user($src_user_id)->description.'!',
                    'objects'=>cobjectref(),
                    'roles'=>'tsk_executor',
                    'files'=>true
                ));
                echo cobjectref()->viewLink().' успешно переназначена сотруднику '.corganization()->user($trg_user_id)->viewLink();
            }
        }
        
        cobjectref()->flags = 0;
    }
    else
      cobjectref()->flags = 0;
    cobjectref()->description = cobjectref()->attributeref('tsk_task_code')->value;
    


    Временным прибежищем для списка исполнителей, которым будут назначены копии задач, является:
    $this->arguments['executor']
    

    Таких «аргументов» в объекте можно создать сколько угодно. В моем случае хватило одного.

    После сохранение объекта происходит создание клонов задач, если необходимо, и рассылка уведомлений.
    После сохранения объекта
    if (cobjectref()->hasAttributeref('tsk_task_base_open') && !empty(cobjectref()->attributeref('tsk_task_base_open')->value))
    {
        $base = select(cobjectref()->attributeref('tsk_task_base_open')->value);
        
        if (!empty($base) && ($base->status->code == 'crs_management_incoming_handed' || $base->status->code == 'crs_management_incoming_created'))
        {
            $base->status = 'crs_management_incoming_exec';
            $base->save();
        }
    }
    if ((cobjectref()->status->code == 'tsk_task_created') && (cobjectref()->flags == 1))
    {
        $to = cobjectref()->attributeref('tsk_task_executor')->value;
        $to = corganization()->user(is_array($to) ? $to[0] : $to);
        sendEmail(array(
            'to'=>$to,
            'subj'=>cobjectref()->attributeref('tsk_task_code')->value.' назначена',
            'body'=>'Вам назначена новая задача!',
            'objects'=>cobjectref(),
            'roles'=>'tsk_executor',
            'files'=>true
        ));
        
        echo cobjectref()->viewLink().' успешно назначена сотруднику '.$to->description;
        if (!empty(cobjectref()->attributeref('tsk_task_base_open')->value))
        {
            $base = select(cobjectref()->attributeref('tsk_task_base_open')->value);
            
            if (is_null($base))
                throw new Exception('Не найден документ указанный в основании для открытия задачи');
                
            if ($base->code == 'crs_management_incoming')
            {
                if ($base->status->code != 'crs_management_incoming_ok')
                {
                    $base->status = 'crs_management_incoming_ok';
                    $base->save();
                }
            }
        }
    }
    if (isset($this->arguments['executor'])) {
        $executors = $this->arguments['executor'];
        $ischild = isset($this->arguments['executorIsChild']) ? $this->arguments['executorIsChild'] : false;
        if (count($executors) > 0)
        {
            $new_task_links = array();
            $exs_task_links = array();
            foreach ($executors as $e) {
                $conditions = array(
                    'tsk_task_subj'=>cobjectref()->tsk_task_subj,
                    'tsk_task_base_open'=>cobjectref()->tsk_task_base_open,
                    'tsk_task_executor'=>$e,
                );
                $exist = selectAll('tsk_management', 'tsk_task', array(), $conditions);
        
                if (count($exist) > 0) {
                    foreach ($exist as $x) {
                        $exs_task = select($x['id']);
                        $exs_task_links[] = $exs_task->viewLink().' для '.corganization()->user($e)->viewLink().' Статус: '.$exs_task->status->viewLink();
                    }
                } else {
                    $new_task = new Objectref();
                    $new_task->prepare(objectDef('tsk_management','tsk_task'));
                    $new_task->attributeref('tsk_task_author')->value = cobjectref()->tsk_task_author;
                    $new_task->attributeref('tsk_task_contract')->value = cobjectref()->tsk_task_contract;
                    $new_task->attributeref('tsk_task_subj')->value = cobjectref()->tsk_task_subj;
                    $new_task->attributeref('tsk_task_description')->value = cobjectref()->tsk_task_description;
                    $new_task->attributeref('tsk_task_category')->value = cobjectref()->tsk_task_category;
                    $new_task->attributeref('tsk_task_importance')->value = cobjectref()->tsk_task_importance;
                    $new_task->attributeref('tsk_task_plan_startdate')->value = cobjectref()->tsk_task_plan_startdate;
                    $new_task->attributeref('tsk_task_plan_enddate')->value = cobjectref()->tsk_task_plan_enddate;
                    $new_task->attributeref('tsk_task_executor')->value = $e;
                    $new_task->attributeref('tsk_task_comment')->value = cobjectref()->tsk_task_comment;
                    $new_task->attributeref('tsk_task_base_open')->value = cobjectref()->tsk_task_base_open;
                    $new_task->save();
                    
                    if ($ischild === true)
                        cobjectref()->childAdd($new_task);
                    else {
                        $parents = cobjectref()->parents();
                        if (!empty($parents)) {
                            $p = select($parents[0]['id']);
                            $p->childAdd($new_task);
                        }
                    }
                        
                    $new_task_links[] = $new_task->viewLink().' для '.corganization()->user($e)->viewLink();
                }
            }
            
            if (count($exs_task_links) > 0)
                echo 'Доп. задачи не назначены, т.к. найдены подобные:'.implode('',$exs_task_links); 
    
        }
    }
    if (cobjectref()->hasAttributeref('tsk_task_notice_of_execute')) {
        if (cobjectref()->attributeref('tsk_task_notice_of_execute')->value == 1) { 
            if (cobjectref()->status->code == 'tsk_task_ok')
                sendEmail(array(
                    'to'=>corganization()->user(cobjectref()->attributeref('tsk_task_author')->value),
                    'subj'=>cobjectref()->attributeref('tsk_task_code')->value.' выполнена',
                    'body'=>'Назначенная вами задача выполнена!',
                    'objects'=>cobjectref(),
                    'roles'=>'tsk_executor',
                ));
        }
    }
    if (cobjectref()->status->code == 'tsk_task_failed')
        sendEmail(array(
            'to'=>corganization()->user(cobjectref()->attributeref('tsk_task_author')->value),
            'subj'=>cobjectref()->attributeref('tsk_task_code')->value.' отклонена',
            'body'=>'Назначенная вами задача отклонена!',
            'objects'=>cobjectref(),
            'roles'=>'tsk_executor',
        ));
    


    В конечном счете форма объекта стала выглядеть как-то так:


    Статусы


    Мудрить со статусами не стал: Создана, Принята, Выполнена, Отклонена. Принятой считается задача, с которой сотрудник ознакомился и принял к исполнению. Остальные статусы понятны из названия.


    Действия


    Небольшое число статусов ведет к лучшему понимаю процесса и уменьшению числа действий. Действий и прям получилось немного.

    Принять

    Назначение, собственно, следует из названия. Переводит задачу в статус «Принята» и устанавливает фактическую дату начала работы.
    cobjectref()->status = 'tsk_task_processed';
    cobjectref()->attributeref('tsk_task_startdate')->value = currentDateTime();
    


    Выполнить

    Успешно закрывает задачу фиксируя фактическую дату окончания работы над ней. Обязательно требует заполнения комментария. Это было одно из требований главного инженера. На «первых парах» он очень возмущался, когда подчиненные закрывали задачи без комментариев. Было совершенно непонятно, что сделано и на каком основании задача закрыта.
    Кроме этого, действие проверяет, является ли задача вложенной, и если так, то проверяет, все ли рядом стоящие с ней задачи выполнены. При положительном результате, проверяет статус вышестоящей задачи и при необходимости направляет ее исполнителю по почте уведомление о том, что наверняка задачу можно закрывать, т.к. все вложенные задачи выполнены.
    if (cobjectref()->hasAttributeref('tsk_task_efforts')) {
        if (empty(cobjectref()->attributeref('tsk_task_efforts')->value))
            throw new Exception("Не указаны трудозатраты в задаче!");
    }
    $src_classificators = classificatorChilds('task_category');
    foreach($src_classificators as $c)
        if ($c['id'] == cobjectref()->attributeref('tsk_task_category')->value)
            break;
            
    if (empty($c))
        throw new Exception("Не найдена категория задачи!");
        
    if ($c['code'] == 'task_category_answer') {
        cobjectref()->attributeref('tsk_task_base_open')->isRequired = true;
        if (empty(cobjectref()->attributeref('tsk_task_comment')->value) || empty(cobjectref()->attributeref('tsk_task_base_close')->value)) {
            echo 'Невозможно выполнить задачу категории '.$c->useLink().' при отсутствии комментария и основания для закрытия!';
            caction()->redirect = cobjectref()->updateUrl();
            return;
        } 
    } elseif (empty(cobjectref()->attributeref('tsk_task_comment')->value)) {
        echo 'Невозможно выполнить задачу при отсутствии комментария!';
        caction()->redirect = cobjectref()->updateUrl();
        return;
    }
    if (empty(cobjectref()->attributeref('tsk_task_startdate')->value)) {
        cobjectref()->attributeref('tsk_task_startdate')->value = currentDateTime();
    }
    cobjectref()->attributeref('tsk_task_enddate')->value = currentDateTime();
    cobjectref()->status = 'tsk_task_ok';
    
    $parents = cobjectref()->parents();
    if (!empty($parents)) {
        $parentId = $parents[0]['id'];
        $childTasks = selectAll('tsk_management','tsk_task',array(),array(
            'parents'=>$parentId,
            'status'=>array('tsk_task_initiated','tsk_task_created','tsk_task_processed')
            ));
            
        if (!empty($childTasks)) {
            $parentTask = select($parentId);
            if (in_array($parentTask->status->code, array('tsk_task_initiated','tsk_task_created','tsk_task_processed'))) {
                sendEmail(array(
                        'to'=>corganization()->user($parentTask->attributeref('tsk_task_executor')->value[0]),
                        'subj'=>$parentTask->attributeref('tsk_task_code')->value.' может быть закрыта?',
                        'body'=>'Предполагаю, что '.$parentTask->viewLink().' может быть закрыта, т.к. закрыты все вложенные в нее задачи!',
                    ));
            }
        }
    }
    


    Отклонить

    Очевидно из названия, что действие отклоняет задачу. Одно условие: в комментарии должна быть указана причина отклонения.
    if (empty(cobjectref()->attributeref('tsk_task_comment')->value))
        echo 'Невозможно отклонить задачу при отсутствии комментария.';
    else
    {
        cobjectref()->status = 'tsk_task_failed';
        cobjectref()->attributeref('tsk_task_enddate')->value = currentDateTime();
    }
    


    Вернуть

    Действие доступное только менеджеру процесса. Позволяет вернуть задачу из конечного статуса в статус «Принята». Необходима на случай, когда задачу закрыли по ошибке. Требуется редко, но все равно необходима.
    cobjectref()->status = 'tsk_task_processed';
    cobjectref()->attributeref('tsk_task_enddate')->value = null;
    
    <h5>Добавить подзадачу</h5>
    Предназначено для создания подзадачи к открытой задаче. Используется преимущественно ГИПами и начальниками отделов.
    $task = cobjectref();
    $new_task = new Objectref();
    $new_task->prepare(objectDef('tsk_management','tsk_task'));
    $new_task->parentrefId = $task->id;
    $new_task->attributeref('tsk_task_description')->value = $task->attributeref('tsk_task_description')->value;
    if ($task->hasAttributeref('tsk_task_contract'))
        $new_task->attributeref('tsk_task_contract')->value = $task->attributeref('tsk_task_contract')->value;
    if ($task->hasAttributeref('tsk_task_subj'))
        $new_task->attributeref('tsk_task_subj')->value = $task->attributeref('tsk_task_subj')->value;
    if ($task->hasAttributeref('tsk_task_category'))
        $new_task->attributeref('tsk_task_category')->value = $task->attributeref('tsk_task_category')->value;
    if ($task->hasAttributeref('tsk_task_base_open'))
        $new_task->attributeref('tsk_task_base_open')->value = $task->attributeref('tsk_task_base_open')->value;
        
    $new_task->attributeref('tsk_task_plan_startdate')->value = $task->attributeref('tsk_task_plan_startdate')->value;
    $new_task->attributeref('tsk_task_plan_enddate')->value = $task->attributeref('tsk_task_plan_enddate')->value;
    $new_task->status = 'tsk_task_initiated';
    caction()->redirect = urlNewObjectref($new_task);
    


    Команды


    Первый процесс, в котором понадобилось создать команды. Команды отличаются от действий тем, что выполняются в контексте процесса, а не объекта. Таким образом, позволяют обрабатывать объекты «пакетно»: все сразу или выбранные пользователем.

    Дело в том, что при интеграции процесса «Задачи» с «Перепиской» в исходящих письмах был реализован алгоритм, который при отправке исходящего письма в ответ на указанные входящие, комментирует задачи, созданные на основании соответствующих входящих и прописывает в их основании для закрытия отправляемое исходящее письмо. Уфф… Иными словами, задачи, которые были созданы при появлении входящего получают комментарии и заполненное основание для закрытия. Очень полезная фишка, т.к. часто, ГИП так занят, что закрывать задачу прямо сейчас ему некогда, а когда «руки дошли», ему нужно вспоминать, на каком основании он должен закрыть задачу. Понятно, что все надо делать вовремя, чтобы не забывать и не вспоминать, но уж если так случилось, то надо сократить время, затрачиваемое на восстановление картины в памяти.

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

    Закрыть готовые

    Команда ищет готовые к закрытию задачи и закрывает первые 10 из них. Не все только потому, чтобы сохранить хоть какой-то контроль над их закрытием. Были случаи, когда, закрывая таким образом задачи ГИП спохватывался, но было уже поздно и задачи приходилось возвращать вручную.
    Закрыть готовые
    $readyTasks = selectAll(
        'tsk_management',
        'tsk_task',
        array(),
        array(
            'tsk_task_executor'=>array('id',cuser()->id),
            'tsk_task_base_close'=>array('not like','is not null'),
            'status'=>array('and','<>tsk_task_ok','<>tsk_task_failed')
        )
    );
    // debugMode(true);
    // debug($readyTasks);
    $success = array();
    $failed = array();
    $max = 10;
    $q = 1;
    foreach ($readyTasks as $task) {
        $obj = select($task['id']);
        progress($q/$max * 100, $task['description']);
        if (!empty($obj)) {
            $obj->attributeref('tsk_task_efforts')->value = 1;
            $obj->attributeref('tsk_task_enddate')->value = currentDateTime();
            $obj->status = 'tsk_task_ok';
            try {
                $obj->save();
                $success[] = $obj->viewLink();
            } catch (Exception $e) {
                $failed[] = $obj->viewLink();
            }
        } else {
            $failed[] = $task['description'];
        }
        $q++;
        if ($q > $max) break;
    }
    if (count($success) > 0) {
        echo("Успешно закрыты следующие задачи:".implode(", ",$success)."Всего: ".count($success)); 
    
    }
    if (count($failed) > 0) {
        warning("Закрыть не удалось:".implode(", ",$failed)."Всего: ".count($failed)); 
    }
    


    Команда доступна на виде снизу (см. скриншоты ниже).

    Закрыть выбранные

    Точно такая же команда, но закрывает только выбранные пользователем задачи.
    Закрыть выбранные
    $objectrefIds = ccommand()->objectrefIds;
    $success = array();
    $failed = array();
    $cnt = count($objectrefIds);
    $q = 1;
    if ($cnt == 0)
        throw new Exception('Ничего не выбрано!');
        
    foreach ($objectrefIds as $objectrefId) {
        $obj = select($objectrefId);
        progress($q/$cnt * 100, $obj['description']);
        if (!empty($obj)) {
            $obj->attributeref('tsk_task_enddate')->value = currentDateTime();
            $obj->status = 'tsk_task_ok';
            try {
                $obj->save();
                $success[] = $obj->viewLink();
            } catch (Exception $e) {
                $failed[] = $obj->viewLink();
            }
        } else {
            $failed[] = $objectrefId;
        }
        $q++;
        if ($q > $cnt) break;
    }
    if (count($success) > 0) {
        echo("Успешно закрыты следующие задачи: ".implode(", ",$success)."Всего: ".count($success));  
    }
    if (count($failed) > 0) {
        warning("Закрыть не удалось: ".implode(", ",$failed)."Всего: ".count($failed));  
    }
    



    Виды


    Виды (выборки), важная составляющая навигацию по объектам. Позволяют гибко настроить отображение списка задач в соответствии с требованиями пользователей.

    Мои задачи

    «Интеллектуальный» вид, т.к. анализирует, в какой должности находится активный пользователь открывший его. В том случае, если его открыл начальник отдела, добавляет категории: наименование отдела и ФИО начальника. Таким образом, начальник отдела может видеть не только свои задачи, но и задачи порученные всем его подчиненным.
    Кстати, существование такого вида было одним из важных требований, выдвинутых пользователями, в частности, главным инженером, ГИПами и начальниками отделов. Не всегда такое возможно реализовать в других системах, заточенных под управление индивидуальными задачами.
    Мои задачи
    $groups = cuser()->groups();
    $isgip = false;
    $ishead = false;
    foreach($groups as $group)
        if (strncmp($group['data_one'],'09.',3) == 0) {
            $isgip = true;
            break;
        } elseif (strcmp($group['code'],'group_head_and_deputy') == 0) {
            $ishead = true;
            break;
        }
        
    $attributes = array(
        'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
        'tsk_task_base_open'=>array('link'=>'value'),
        'tsk_task_contract'=>array('link'=>'value'),
        'tsk_task_subj'=>array('limit'=>160)
    );
        
    if ($isgip) 
    {
        $categories = array($group->name, cuser()->description);
        $us = array('id');
        foreach ($group->users() as $u)
            $us[] = $u['id'];
        cviewpub()->categories = $categories;
        cviewpub()->category = is_null(cviewpub()->category) ? 0 : cviewpub()->category;
        switch(cviewpub()->category)
        {
            case 0:
                $attributes += array('tsk_task_executor'=>array('link'=>'value','inplaceEdit'=>true,'options'=>array('style'=>'width: 30%;')));
                $conditions = array('tsk_task_executor'=>$us);
                break;
            case 1:
                $conditions = array('tsk_task_executor'=>array('id',cuser()->id));
                break;
        }
    } elseif ($ishead) {
        foreach($groups as $group)
            if (is_numeric($group['data_one']))
                break;
        $us = array('id');
        foreach ($group->users() as $u)
            $us[] = $u['id'];
                
        $categories = array($group->name, cuser()->description);
        cviewpub()->categories = $categories;
        cviewpub()->category = is_null(cviewpub()->category) ? 0 : cviewpub()->category;
        switch(cviewpub()->category)
        {
            case 0:
                $attributes += array('tsk_task_executor'=>array('link'=>'value','inplaceEdit'=>true,'options'=>array('style'=>'width: 30%;')));
                $conditions = array('tsk_task_executor'=>$us);
                break;
            case 1:
                $conditions = array('tsk_task_executor'=>array('id',cuser()->id));
                break;
        }
    } else {
        $categories = array(cuser()->description);
        $conditions = array('tsk_task_executor'=>array('id',cuser()->id));
    }
    $attributes += array(
        'tsk_task_plan_startdate',
        'tsk_task_plan_enddate',
        'tsk_task_base_close'=>array('link'=>'value'),
        'tsk_task_comment',
        'status'=>array('link'=>'value', 'actions'=>array(),'options'=>array('style'=>'width: 100px;'))
    );
    $conditions['status'] = array('tsk_task_initiated','tsk_task_created','tsk_task_processed');
    cviewpub()->exec(array(
      'object'=>objectDef('tsk_management','tsk_task'),
      'attributes'=>$attributes,
      'sort'=>array(
        'tsk_task_code'=>array('enable'=>true),
        'tsk_task_base_open'=>array('enable'=>true),
        'tsk_task_contract'=>array('enable'=>true),
        'tsk_task_subj'=>array('enable'=>true),
        'tsk_task_plan_startdate'=>array('enable'=>true),
        'tsk_task_plan_enddate'=>array('default'=>'asc', 'enable'=>true)
      ),
      'conditions'=>$conditions,
      'sorting'=>true,
      'pagination'=>array('pagesize'=>10),
      'showcreate'=>true,
    ));
    




    Назначенные мной

    Понятно из названия, что вид отображает перечень задач, автором которых является активный пользователь. Также является «интеллектуальным», т.к. анализирует активного пользователя, а в случае, если он является ГИПом, добавляет две категории: группа ГИПа и ФИО ГИПа. Категории нужны для того, чтобы ГИП мог видеть как свои персональные задачи отдельно, так и задачи порученные его помощнику. ГИП и его помощник работают над одним пулом задач.
    Назначенные мной
    $groups = cuser()->groups();
    $isgip = false;
    foreach($groups as $group)
        if (strncmp($group['data_one'],'09.',3) == 0)
        {
            $isgip = true;
            break;
        }
        
    if ($isgip) 
    {
        $categories = array($group->name, cuser()->description);
        $us = array();
        foreach ($group->users() as $u)
            $us[] = $u['id'];
        cviewpub()->categories = $categories;
        cviewpub()->category = is_null(cviewpub()->category) ? 0 : cviewpub()->category;
        switch(cviewpub()->category)
        {
            case 0:
                $conditions = array('tsk_task_author'=>$us);
                break;
            case 1:
                $conditions = array('tsk_task_author'=>cuser()->id);
                break;
        }
    } 
    else 
    {
        $categories = array(cuser()->description);
        $conditions = array('tsk_task_author'=>cuser()->id);
    }
    $conditions['status'] = array('tsk_task_initiated','tsk_task_created','tsk_task_processed');
    cviewpub()->exec(array(
      'object'=>objectDef('tsk_management','tsk_task'),
      'attributes'=>array(
        'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
        'tsk_task_base_open'=>array('link'=>'value'),
        'tsk_task_contract'=>array('link'=>'value'),
        'tsk_task_subj'=>array('limit'=>160,'inplaceEdit'=>true),
        //'tsk_task_description'=>array('limit'=>160),
        'tsk_task_executor'=>array('link'=>'value','options'=>array('style'=>'width: 10%;')),
        'tsk_task_plan_startdate',
        'tsk_task_plan_enddate',
        'status'=>array('link'=>'value', 'actions'=>array(),'options'=>array('style'=>'width: 100px;'))
      ),
      'sort'=>array(
        'tsk_task_code'=>array('enable'=>true),
        'tsk_task_base_open'=>array('enable'=>true),
        'tsk_task_subj'=>array('enable'=>true),
        'tsk_task_executor'=>array('enable'=>true),
        'tsk_task_plan_startdate'=>array('enable'=>true),
        'tsk_task_plan_enddate'=>array('default'=>'asc', 'enable'=>true)
      ),
      'conditions'=>$conditions,
      'sorting'=>true,
      'pagination'=>array('pagesize'=>10),
      'showcreate'=>true,
    ));
    




    Мои завершенные

    Простой вид. Отображает перечень завершенных задач, исполнителем которых является активный пользователь.
    Мои завершенные
    cviewpub()->exec(array(
      'object'=>objectDef('tsk_management','tsk_task'),
      'attributes'=>array(
        'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
        'tsk_task_base_open'=>array('link'=>'value'),
        'tsk_task_subj'=>array('limit'=>160),
        'tsk_task_plan_startdate',
        'tsk_task_plan_enddate',
        'tsk_task_startdate',
        'tsk_task_enddate'
      ),
      'sort'=>array(
        'tsk_task_code'=>array('enable'=>true),
        'tsk_task_base_open'=>array('enable'=>true),
        'tsk_task_subj'=>array('enable'=>true),
        'tsk_task_plan_startdate'=>array('enable'=>true),
        'tsk_task_plan_enddate'=>array('default'=>'desc', 'enable'=>true)
      ),
      'conditions'=>array(
        'tsk_task_executor'=>cuser()->id,
        'status'=>'tsk_task_ok'
      ),
      'sorting'=>true,
      'pagination'=>array('pagesize'=>20)
    ));
    



    Все задачи

    Отображает полный перечень всех активных задач. Используется, как правило, для поиска чужих задач.
    Все задачи
    cviewpub()->exec(array(
      'object'=>objectDef('tsk_management','tsk_task'),
      'attributes'=>array(
        'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
        'tsk_task_base_open'=>array('link'=>'value'),
        'tsk_task_subj'=>array('limit'=>160),
        'tsk_task_executor'=>array('link'=>'value','options'=>array('style'=>'width: 10%;')),
        'tsk_task_plan_startdate',
        'tsk_task_plan_enddate',
        'status'=>array('link'=>'value', 'actions'=>array(),'options'=>array('style'=>'width: 100px;'))
      ),
      'sort'=>array(
        'tsk_task_code'=>array('enable'=>true),
        'tsk_task_base_open'=>array('enable'=>true),
        'tsk_task_subj'=>array('enable'=>true),
        'tsk_task_executor'=>array('enable'=>true),
        'tsk_task_plan_startdate'=>array('enable'=>true),
        'tsk_task_plan_enddate'=>array('default'=>'asc', 'enable'=>true)
      ),
      'conditions'=>array(
        'status'=>array('tsk_task_initiated','tsk_task_created','tsk_task_processed')
      ),
      'sorting'=>true,
      'pagination'=>array('pagesize'=>10),
      'showcreate'=>true,
    ));
    



    Все решения планерки

    Очень важный перечень задач, т.к. содержит только задачи, назначенные по решению планерки и находящиеся на контроле. При выполнении скрипта, вид анализирует перечень всех задач, выбирает из него всех исполнителей, группирует по отделам и формирует список категорий вида с наименованиями отделов.
    Таким образом, каждый сотрудник, а как правило, это начальники и главные специалисты отделов, могут легко отфильтровать из всего списка поставленных задач только свои.
    Все решения планерки
    $src_executors = selectColumnAll('tsk_management','tsk_task','tsk_task_executor',
        array(
            'status'=>array('tsk_task_initiated','tsk_task_created','tsk_task_processed'),
            'tsk_task_category'=>array('task_category_plan')
        )
    );
    $end_executors = array();
    foreach ($src_executors as $u) {
        $end_executors[] = $u['id'];
    }
    $users = corganization()->users($end_executors);
    $departments = array(0=>'Все');
    foreach ($users as $u) {
        foreach ($u->departments as $d) {
            $departments[$d['id']] = $d['name'];
        }
    }
    cviewpub()->categories = $departments;
    cviewpub()->category = is_null(cviewpub()->category) ? 0 : cviewpub()->category;
    $conditions = array(
        'status'=>array('tsk_task_initiated','tsk_task_created','tsk_task_processed'),
        'tsk_task_category'=>array('task_category_plan')
    );
    if (cviewpub()->category != '0') {
        $department = corganization()->department(cviewpub()->category);
        
        if (!empty($department)) {
            $users = $department->users();
            $c = array('id');
            foreach ($users as $u)
                $c[] = $u['id'];
                
            $conditions['tsk_task_executor'] = $c;
        }
    }
    cviewpub()->exec(array(
      'object'=>objectDef('tsk_management','tsk_task'),
      'attributes'=>array(
        'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
        'tsk_task_contract'=>array('link'=>'value'),
        'tsk_task_subj'=>array('limit'=>160),
        'tsk_task_executor'=>array('link'=>'value','options'=>array('style'=>'width: 10%;')),
        'tsk_task_plan_startdate',
        'tsk_task_plan_enddate',
        'tsk_task_startdate',
        'status'=>array('link'=>'value', 'actions'=>array(),'options'=>array('style'=>'width: 100px;'))
      ),
      'sort'=>array(
        'tsk_task_code'=>array('enable'=>true),
        'tsk_task_base_open'=>array('enable'=>true),
        'tsk_task_subj'=>array('enable'=>true),
        'tsk_task_executor'=>array('enable'=>true),
        'tsk_task_plan_startdate'=>array('enable'=>true),
        'tsk_task_plan_enddate'=>array('default'=>'asc', 'enable'=>true)
      ),
      'conditions'=>$conditions,
      'sorting'=>true,
      'pagination'=>array('pagesize'=>10),
      'showcreate'=>true,
    ));
    




    Авторский надзор

    Очень важный и непростой вид, который используют ГИПы для подготовки смет по авторскому надзору. Сложность вида заключается в том, что он на самом деле отображает информацию не только о задачах, но и об их основаниях на открытие и закрытие, т.е. о письмах. Таким образом, в виде отображается информация о трех объектах сразу! Такую возможность предоставляет easla.com при использовании атрибутов типа «Объект» и указании в описании вида атрибутов вложенных объектов через точку.
    Авторский надзор
    cviewpub()->exec(array(
      'object'=>objectDef('tsk_management','tsk_task'),
      'attributes'=>array(
        'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
        'tsk_task_contract'=>array('link'=>'value'),
        'tsk_task_contract.agr_management_contract_title'=>array('header'=>'Наименование договора','limit'=>'30'),
        'tsk_task_contract.agr_management_contract_contragent'=>array('header'=>'Контрагент'),
        'tsk_task_contract.agr_management_contract_project_manager'=>array('header'=>'ГИП'),
        'tsk_task_subj'=>array('limit'=>160),
        'tsk_task_executor'=>array('link'=>'value','options'=>array('style'=>'width: 10%;')),
        'tsk_task_executor.email',
        'tsk_task_base_open'=>array('link'=>'value','export'=>array('id','crs_management_incoming_contragent_regnum')),
        'tsk_task_base_open.crs_management_incoming_receive_date'=>array('header'=>'Дата получения входящего'),
        'tsk_task_base_close'=>array('link'=>'value','export'=>array('id','crs_management_outgoing_regnum')),
        'tsk_task_base_close.crs_management_outgoing_sentdate',
        'tsk_task_efforts',
        'status'=>array('link'=>'value', 'actions'=>array(),'options'=>array('style'=>'width: 100px;'))
      ),
      'sort'=>array(
        'tsk_task_code'=>array('enable'=>true),
        'tsk_task_base_open'=>array('enable'=>true),
        'tsk_task_subj'=>array('enable'=>true),
        'tsk_task_executor'=>array('enable'=>true),
        'tsk_task_plan_startdate'=>array('enable'=>true),
        'tsk_task_plan_enddate'=>array('default'=>'asc', 'enable'=>true)
      ),
      'conditions'=>array(
        'tsk_task_base_close'=>array('crs_management_outgoing_content'=>684)
      ),
      'sorting'=>true,
      'pagination'=>array('pagesize'=>10),
      'showcreate'=>true,
    ));
    


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


    Помимо общих видов доступных пользователю через главное меню были созданы доп. виды для объектов, в терминологии easla.com – в контексте объекта. С их помощью удалось на форме входящего и исходящего письма отображать список зависимых от него задач. Очень удобно, когда надо проанализировать, какие задачи были назначены на основании выбранного, скажем, входящего документа и в каком состоянии они находятся в настоящий момент.
    Задачи по документу
    cviewpub()->exec(array(
      'object'=>objectDef('tsk_management','tsk_task'),
      'attributes'=>array(
        'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
        'tsk_task_subj'=>array('limit'=>160),
        //'tsk_task_description'=>array('limit'=>160),
        'tsk_task_executor'=>array('link'=>'value','options'=>array('style'=>'width: 10%;')),
        'tsk_task_plan_startdate',
        'tsk_task_plan_enddate',
        'status'=>array('link'=>'value', 'actions'=>array(),'options'=>array('style'=>'width: 100px;'))
      ),
      'sort'=>array(
        'tsk_task_plan_enddate'=>array('default'=>'asc', 'enable'=>true)
      ),
      'sorting'=>true,
      'pagination'=>array('pagesize'=>10),
    ));
    




    Кроме этого, подзадачи надо было отображать детальнее, чем система предлагает это по-умолчанию, поэтому был создан дополнительный вид.
    Подзадачи
    
    $attributes = array(
        'tsk_task_code'=>array('link'=>'object','options'=>array('style'=>'width: 10%;')),
        'tsk_task_base_open'=>array('link'=>'value'),
        'tsk_task_subj'=>array('limit'=>160),
        'tsk_task_executor'=>array('link'=>'value','options'=>array('style'=>'width: 10%;')),
        'tsk_task_plan_startdate',
        'tsk_task_plan_enddate',
        'status'=>array('link'=>'value', 'actions'=>array(),'options'=>array('style'=>'width: 100px;'))
    );
        
    cviewpub()->exec(array(
      'object'=>objectDef('tsk_management','tsk_task'),
      'attributes'=>$attributes,
      'sort'=>array(
        'tsk_task_code'=>array('enable'=>true),
        'tsk_task_base_open'=>array('enable'=>true),
        'tsk_task_subj'=>array('enable'=>true),
        'tsk_task_plan_startdate'=>array('enable'=>true),
        'tsk_task_plan_enddate'=>array('default'=>'asc', 'enable'=>true)
      ),
      'sorting'=>true,
      'pagination'=>array('pagesize'=>20),
      'showcreate'=>true,
    ));
    




    Роли


    Ролей всего три: менеджер, участник, наблюдатель. Менеджер, понятно, может все. Участник не может только удалять задачи. Наблюдатель может только просматривать задачи.

    Конечно, при таком разделении прав доступа каждый сотрудник имеет право менять чужие задачи. Но, анализируя все озвученные к процессу требования и внутренние отношения в коллективе компании пришел к выводу, что разруливать права так, чтобы никто и ничего не мог изменить без весомой на то санкции окажется только «палкой в колесах». Обезопасил задачи от произвола путем использования «исторических» атрибутов. Иначе говоря, если кто-то и что-то изменит в задаче, останутся следы и будет видно, как было, как стало, и кто изменил.

    Итоги


    Лично для меня всегда основным критерием успешности проекта автоматизации является не подписанный акт или техническое задание с грифом «Выполнено», а факт эксплуатации запущенной системы.
    В настоящий момент процесс «Задачи» активной используется всеми участниками процесса. Большая заслуга в этом самого главного инженера, который дисциплинирует всех остальных личным примером. Кстати, он же является одним из основных поставщиков доп. требований к процессу.
    Очень приятной неожиданностью стало появление вида «Авторский надзор», которые используется для выгрузки данных и формирования сметы в Microsoft Excel. Оказалось, что такая смета приносит немалую прибыль организации.
    Кстати, кроме объекта «Задача» в рамках описанного процесса был реализован объект «Ознакомление», позволяющий отправлять любые документы на ознакомление с контролем исполнения. Но об этом отдельно.
    easla.com 7,69
    Онлайн система документооборота
    Поделиться публикацией
    Комментарии 37
    • +2
      Вот так онлайн система документооборота и превращается или в монструозный конструктор, или в не понятно что…
      • +1
        Почему же так скептично?
        Мой опыт показывает, что любая система документооборота постепенно обрастает новыми решениями. Вопрос упирается только в ее гибкость и, как бы, «легкочитаемость». Тогда поддерживать и развивать ее несложно.
        • +1
          Любая узкоспециализированная система рано или поздно понимает что упирается в потолок своей специфики и понимает что чего-то не хватает… И делает решение внутри себя. Ну и да. Гибкость и легкочитаемость закончится, когда к задачам добавятся обсуждения, привязки документов, сроки, отчеты, интеграции с эксченжем (надо ведь задачи с аутлуком синхронизировать?) и т.д.
          • +1
            Я здесь немного ниже описал, что в описанной организации используется еще одна система — TDMS. Между TDMS и easla.com проведена условная черта ответственности. В TDMS вся проектная документация — весь ее жизненный цикл. В easla.com вся организационно-распорядительная документация и управления.
            В том случае, когда первая система «упирается в потолок», ее поддержит вторая. И наоборот.
            Об интеграции с Exchange пока речь не шла, а вот об отображении задач в виде диаграмм Гантта — да.
      • +1
        А чем Вас неустроили уже написанные Bug треккеры?
        Или просто захотели создать свое?
        • +2
          Эээ… багтрекеры? В статье я же указал, что речь идет о проектной организации. Понимаю, что слово «проект» в современном обществе очень широкого понятия, поэтому уточню. Проектная организация — разработка проектов обустройства нефтегазовых месторождений. Сотрудниками организации являются инженеры, которые про багтрекеры слыхом не слыхивали. Попытаться затащить их в такую систему мне кажется уже страшным. :)
          Но «контрольным выстрелом» было то, что в easla.com уже был реализован процесс Переписка, а также Договора, Заказчики и прочие сопутствующие. Поэтому Задачи явились логичным продолжением. Важным критерием является гибкость системы. Как я упомянул в статье, задачи в окончательном виде родились не сразу. Сперва были простенькими, потом становились все сложнее и сложнее. Такой эволюционный процесс очень полезен для пользователей и не очень удобен для разработчика.
          И да, было желание «создать свое», но я стараюсь быть объективным в своих желаниях.
          • +1
            Например Jira — очень похоже на то, что вы сами реализовали. Плюс у нее есть поддержка плагинов.
            • +1
              Одна из целей — реализация всех бизнес-процессов на единой платформе.
              В Jira есть возможность кроме задач управлять еще корреспонденцией (входящие и исходящие), договорами, заказчиками, инцидентами, изменениями, нормативной документацией, согласованиями? Навскидку.

              • +1
                Смотря что понимать под управлением. Особенно интересно — управление Заказчиками.
                У нас управление договорами, изменениями, нормативкой и согласованиями ведется в Jira (настроены разные жизненные циклы процессов на разные проекты)
                • +1
                  С заказчиками у нас все просто. Банальный структурированный каталог контрагентов и контактов. Погляжу Jira. Заинтриговали.
                • +1
                  Т.е. Вам нужна некая смесь ERP+Redmine? Из легко поддающихся кастомизации Вам бы подошла Odoo, но она почему-то в РФ не популярна.
                  • +1
                    Скорее всего нужна не гремучая смесь, а платформа.
                    По опыту, например, для управления проектной документацией с момента создания и до момента сдачи в архив используется TDMS. На рынке полно «коробочных» продуктов, которые по стартовым возможностям превосходят TDMS. Однако, несколько лет назад мы все равно выбрали TDMS. Почему? Потому организации с абсолютно одинаковыми процессами встречаются нечасто. Правильнее сказать, организации с одинаковыми (типовыми) процессами бывают при условии, что работают в определенной нише, в которой процессы «устаканились» уже давным давно. Такие организации могут позволить себе «коробочные» или готовые облачные и онлайн сервисы с преднастроенными бизнес-процессами. Зарегистрировался. Добавил пользователей. Начал работать.
                    В нашем случае так не получается, т.к. бизнес-процессы компании не соответствуют никаким стандартам. Их надо «шить на заказ». Поэтому нужна не «смесь» нескольких продуктов, а платформа, на которой можно «замесить» несколько процессов разом.
                    Конечно, не исключаю вопросы интеграции. Но, как бы, по-крупному. Мы уже такой опыт поимели. TDMS скрещена с easla.com. TDMS скрещена с Outlook и Exchange. И easla.com умеет интегрироваться с LDAP.
                    Кроме этого, как уже написал выше, имеет значение гибкость системы. Возможность оперативно подстроиться под новые требования без остановки производства даже на 5 минут. Добавил атрибут. Описал логику и сразу в бой!
          • +1

            Изобрели редмайн )

            • +1
              Если смотреть с точки зрения только управления задачами, то наверное вы правы. Мне было важно интегрировать вроде как самостоятельный процесс управления задачами с кучкой других процессов, в частности, корреспонденцией и договорами. Все задачки привязаны к договорами и письмам. Кроме этого, как я описал в статье, интеграция очень тесная.
              Например, при отправке исходящего письма происходит автоматическое комментирование задач, на основании которых оно (письмо) и появилось! Не знаю, на сколько такое возможно в Redmine.
              • 0
                Ну не только redmine если смотреть шире то есть еще: solutions.1c.ru/catalog/project-org/features
                В отличии от примера из статьи (где описывается воплощение фантазий конкретного руководителя) в решении от 1С примерены «Best practice» (как любят говорить коллеги из SAP).
                Все что перечислили в этих системах есть (и даже больше).
                Но для внедрения подобного потребуется гораздо больше всего (времени, денег, знаний).
                • 0
                  Немного ниже упоминали 1С, но когда я лично изучал возможности продуктов 1С, то заметил, что они не умеют хранить файлы. Это был большой минус. Не знаю как сейчас, возможно ситуация изменилась. Вы в курсе?

                  Насчет «best practice» (лучших практик). Как показывает мой жизненный опыт, с ними нужно быть поосторожнее. Изучать лучшие практики нужно и полезно (ITIL, PM BOK, CobiT), но тупой перенос их в работу, не оглядываясь на окружающую действительность, может только ухудшить ситуацию.
                  Конкретный пример из жизни. Недавно на хабре была статья о ревизиях. Ревизии — яркий пример «best practice». Они активно используются на «западе». Ревизии в целом — отличный способ контролировать процесс разработки проектной документации. В нашем ГОСТ нет такого понятия. Есть только изменения, которые позволяют контролировать только процесс изменения документации после сдачи в архив, но вот до этого момента, процедуры не регламентированы. В общем, ревизии — классная штука, если пользоваться ими правильно!
                  Но их начинают использовать сейчас все кому не лень и совершенно бездумно. Ревизии заменяют изменения в наше ГОСТ. Но наш ГОСТ никто не отменял. В итоге, и ревизии по стандартам «западных» и «прозападных» компаний, и изменения по ГОСТ приходится применять одновременно! Это вносит не просто путаницу, а порой полное непонимание, что и как идентифицировать и в какой момент. И не только на стороне исполнителя, но и на стороне заказчика. Таким образом, великолепный и признанный во всем мире «best practice» не только не помогает в работе, а напротив, тормозит ее, снижает эффективность и прозрачность.
                  По хорошему, надо было на уровне министерства или как они там сейчас называются, внести изменения в ГОСТ. Отменить изменения, вместо них описать работу с ревизиями и все! Было бы здорово!
                  Кроме этого существует понятие «зрелость процесса» и «зрелость компании». Многие «best practice» предполагают довольно высокую зрелость и того и другого, поэтому их запуск в «недозрелой» компании создаст больше вопросов, чем решений. Реализация же бизнес-процесса в точности по требованиям самой компании и быстрее и понятнее конечным пользователям от главного инженера, до техника.

                  Про SAP на хабре тоже недавно была «хвалебная» статья только подтверждающая опасения многих.

                  Кстати, не стоит забывать и про финансовую составляющую. Продукты 1С и уж тем более SAP имеют такой ценник, что в условиях современного кризиса их просто не потянуть, а easla.com почти бесплатная!
            • +1
              А менеджеры в кулуарах не возмущаются инструментом, который наглядно демонстрирует их полную бесполезность?
              • +1
                Я вам больше скажу! Они не только возмущаются, но и стонут, когда главный инженер «спускает собак» и заставляет навести порядок в задачах.
                Недавно, примерно месяц назад, приезжал представитель одного заказчика и попросил провести ему экскурсию. Позадавал вопросы на местах. Дошел до ГИПа и попросил показать список задач по его проектам. А он у него весь красный! Не столько потому, что задачи не выполнены, сколько потому, что он их банально не закрыл вовремя, хотя все сделал! Крику было!
                В общем, исполнительская дисциплина потихоньку перестает хромать. Эволюция…
                • +1
                  Из приведенного текста не заметно, что развернутая система упрощает жизнь ГИПов. Несмотря на красные задачи, производственный процесс, судя по всему, успешно работает.
                  А вот жизнь главного инженера да, скорее всего упростилась, но целью добавления временных затрат подчиненных. И я бы не был настолько оптимистичен, утверждая, что процесс успешно автоматизирован, так как его внедрение происходит административными методами.

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

                  Но проблема может быть совсем другой. Например, что главный инженер-самодур, раздает по 15 незначительных задач в день (по 2 в час!) и заставляет по ним отчитываться, а каждый отчет требует у подчиненных 15-20 минут на подготовку. Поэтому сотрудники такие задачи не любят и саботируют их. В этом случае и решение, скорее всего, было бы совсем другое, и не лежащее в области автоматизации.
                  • +2
                    Вопрос оптимизации и автоматизации все-таки в разных плоскостях. В статье описывается второе.
                    По поводу «жизнь… упростилась», вы правы. Инструмент был нужен в первую очередь главному инженеру, о чем я сказал в статье. Суть в том, что именно главный инженер наиболее организованный сотрудник компании и с мощной внутренней мотивацией. Его прямые подчиненные — ГИПы, по больше части уже не так заинтересованы в существовании автоматизированного механизма управления задачами. Им удобнее сослаться на плохую память, занятость, невозможность исполнения и прочее, чем выстроить под собой процесс исполнения поручений. Собственно с этим и начал бороться главный инженер сменив прежнего.
                    Объективным доказательством сказанного является тот факт, что до смены главного инженера ни один ГИП, ни разу не пришел и не попросил хоть какую-то систему управления задачами. Даже в sharepoint или outlook!
                    Сейчас, пусть не все задачи исполняются в срок, но они по крайней мере уже не забываются, не закрываются безосновательно и будут выполнены все равно хоть и со срывом сроков. Такого раньше не было. Поручения могли просто «похоронить» и «воскресали» они только когда звонил злой заказчик и требовал о нем вспомнить!
                    Насчет самодурства, сомневаюсь, т.к. знаю его лично. Количество задач 15-20 в день для коллектива в 200 человек с минимальным сроком выполнения 8 часов (а обычно 40 часов) — пшик! Не вижу препятствий выполнять все в срок!
                    Кстати, как указано в статье, вид «Авторский надзор» оказался крайне необходимым. Раньше подобный отчет ГИП собирал вручную и мог тратить на это неделю! Конечно, не 40 часов, но пока получит от каждого данные, пока сведет их вместе, пока сформирует смету… Таким образом запрошенная в понедельник смета могла попасть заказчику только в конце недели, а то и вовсе не попасть и из-за этого теряли деньги. Сейчас смета формируется на основании отчета в течении секунд 20 + время на фильтр к виду. На все про все минуты 2! Так что, считаю, что автоматизация управления задачами получилась не надуманной. И даже с реальной, а не потенциальной, экономической выгодой.
                    • +1
                      Я скажу больше — все зависит от внутренней культуры компании и от того, куда ее ведут. Вы совершенно правы в том, что административные методы редко приводят к идеальному результату. В таком случае ГИ практически всегда назначают «самодуром». Но в случае, когда задачи ставятся, но не выполняются, выход только один — все-таки ввести учет.

                      Из-за чего задачи не выполняются — это, действительно, другой вопрос. Тут поможет анализ накопленных данных. Может получиться так, что причина — бешеная загрузка. Может — просто «тихий саботаж». Может — еще что-то. И в каждом случае рецепт исправления — свой, но в основе возможности анализа — учет. Наша задача — предоставить соответствующий инструмент. С возможностью его развития в любую сторону.
                    • 0
                      Я вам завидую изо всех сил. В моей последней организации, которую очень любят вспоминать на хабре, исключительно массовой практикой среди всех руководителей было вообще никогда принципиально не читать почту, уведомления и не отвечать на звонки. Срочные тикеты, по которым время реакции было установлено вендором в сутки, согласовывались годами, а не срочные — вообще никогда. Высшее руководство их в этом активно поддерживало, а тех, кто пытался напоминать о том, что хорошо бы всё-таки провести запланированные работы — довольно быстро изгоняли из компании.
                      • +1
                        Ну если «высшее руководство их в этом активно поддерживало», то гиблое дело. Поддержка высшего руководства очень нужна при внесении любых изменений, тем более в управлении.
                  • +1
                    Вспомнился первый опыт работы с менеджерами задач лет 15 назад. Ах, что были за времена :-)
                    • +1
                      Такое подбираются под пользователя, его видение процесса. Также как стул, ручка…
                      • 0
                        Вы изобрели 1С Документооборот.

                        Там есть всё что вы реализовали + немного ещё.

                        Понятно что вам может претить сама фирма 1С, но имеет смысл посмотреть что реализовано у 1С — возможно у вас появятся идеи что можно добавить у себя.
                        • 0
                          Могу ошибаться, но по-моему 1С Документооборот не хранит документы. Я прав или отстал?
                        • 0
                          Статья о том, как заново изобрели велосипед =) Сколько уже понаделано такого 100 % одинакового за последние 15-20 лет =) Посмотрите хотя бы в сторону Лоцман ПГС и плагинов к нему и т.д.
                          Идея для «киллер» фичи, которая будет выгодно отличать продукт от других: на основе собранных данных о выполненных проектах, автоматизированно создавать справочник норм труда/материалов. Далее в системе появится возможность делать прогнозы для того или иного развития событий проекта.
                          • 0
                            Вот только не Лоцман. Дурная весть за ним идет. Лично дело не имел, но коллеги с других организаций отзывают не очень лестно.
                            И, кстати, не стоит забывать про финансовую составляющую. Сколько будет стоить Лоцман ПГС или его аналоги, и во сколько обойдется easla.com.
                            • 0
                              Лоцман был дан в качестве примера. Не реклама. Ну а так — на вкус и цвет товарища нет =)
                              В этом продукте очень хорошо проработана автоматизация всей проектной деятельности (строительство) — можно брать смотреть в качестве примера (источника вдохновения для бизнес аналитика).
                              • +2
                                На сколько я знаю, «Лоцман» — коробочный продукт с преднастроенными бизнес-процессами. Изменить готовые процессы в нем не так то просто. Надо или иметь хорошие знания самого продукта, или платить денежки интеграторам. Причем денежки немалые (в одной из знакомых мне организаций на Лоцман потратили несколько миллионов, а «воз и ныне там»).

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

                                Кстати, насчет непохожести. Есть ГОСТ 21.1101-2013, единый для всех проектных организаций. Регламентирует правила оформления проектной документации в организациях по всей России. Но даже в организациях находящихся в границах шести кварталов нашего города оформляют документацию по-разному, хотя вся она соответствует ГОСТ. Парадокс. :)
                          • 0
                            Выглядит, в целом, неплохо. Тяжело оценить юзабилити и полноту функционала по скринам и тексту, но как старт вполне себе неплохо.
                            Персонально хочу сказать, что писать свой продукт при наличии уже зарекомендовавших себя на рынке — не самый лучшая идея, т.к. обрастая такими продуктами можно в какой-то момент попасть в точку «невозврата», когда руководство компании не захочет инвестировать в сторонние продукты, даже если они и будут в десятки раз лучше и более подходящими, отмазываясь тем, что «у нас есть свое решение и оно работает». Поэтому я бы для начала оценил рынок коммерческих систем, а после бы уже принимался писать что-то свое, в случае отсутствия чего-то подходящего (во что я не верю). Да, системы нынче стоят мягко говоря немало, но и охватываемая область бизнес-процессов в них зачастую больше, чем компания может себе представить. Опять же Вы отталкивались от требований ГИ и судя по картинкам задачи в основном по договорам, а как же остальные задачи? За ними тоже надо следить и трудозатраты отслеживать… А как же проекты без договора? Например внутренний проект компании? Или концепт?
                            Но опять же, в рамках поставленной задачи, считаю что решение вполне себе неплохо выглядит. Просто привык думать на перспективу организации в целом, т.к. это зачастую куда более важно чем решение проблемы одного человека или даже целого подразделения.
                            • 0
                              Мы уже успели попробовать альтернативный продукт. Боюсь указывать какой, «доброжелатели» заминусуют.
                              После неудачных попыток поискали другие. Системы, которые описывают свою бизнес-логику «кликами мышки» меня разочаровали. Верить в их возможности я перестал. К моменту, когда определились финансовая ситуация в организации стала такой, что покупать что-то было нереально.
                              Как указывал выше, важным критерием была гибкость системы, а easla.com позволяет описывать процессы кодом — куда уж гибче! Пусть нет блок-схем, но зато логика поведения точно такая, какая нужна.
                              Все задачи не относящиеся к проектным договорам проходят по так называемым «внутренним договорам». Нарочно созданы договоры «100» и «200» по которым выполняются внутренние задачи. Никаких проблем.
                              Спасибо за оценку моих трудов!
                              • 0
                                Оценить юзабилити можете в самой системе заимствуя процесс Задачи себе. Правда, учитывая его тесную интеграцию с другими процессами он может не завестись сразу и придется или поправить самому, или обращаться в техподдержку за помощь.
                              • 0
                                Интересная тема!
                                А на мобильных устройствах как это выглядит? Для руководства обычно важен доступ со смартфона.
                                • 0
                                  Адаптивная верстка все решает. Если руководство умеет пользоваться сотовым или планшетом, подключиться к интернет и зайти на сайт, то никаких проблем!
                                  P.S. Таким же образом однажды воспользовался доступом к официальной переписке и отправил несколько официальных писем прямо со своей соты.
                                • 0
                                  Обещали диаграмму Гантта строить по назначенным задачам. Дождались!
                                  Пока в тестовом режиме. Осталось «допилить» верстку и локализацию.
                                  Как-то так:
                                  Диаграмма Гантта
                                  Кстати, можно отдельную статью написать про «прикручивание» DHTMLX компоненты к Yii.

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

                                  Самое читаемое