Потребовал главный инженер автоматизировать процесс управления задачами. Поручений своим подчиненным он дает много, но вручную контролировать процесс их исполнения просто нереально. Помнить все поручения тем более невозможно. Поэтому сразу настоял на использовании подходящих средств автоматизации.
![](https://habrastorage.org/r/w1560/files/079/320/553/079320553ba84d3fa3faf4868514cb9d.png)
Нетривиальные задачи потребовали нетривиального подхода. Подробное описание с картинками и исходным кодом под катом.
Проектная организация, численностью порядка 200 человек. В высшем руководстве 4 человека, в прямом подчинении главного инженера 4 главных инженера проекта (ГИП) и столько же их помощников, плюс все начальники отделов.
Главный инженер в день может поручать от 5 до 15 задач своим прямым подчиненным, которые, в свою очередь, могут делегировать выполнение задач нескольким начальникам отделов, а те своим подчиненным. Классическая иерархическая схема. Таким образом, количество активных задач в единицу времени может достигать 600-800! Удержать их все в голове просто нереально, а в условиях хромающей исполнительской дисциплины вопрос контроля становится жизненно важным.
В организации в тот момент уже несколько лет использовался MS Project, правда, для управления проектами в целом, а не краткосрочными поручениями. Идею использовать его для управления краткосрочными поручениями отмели после краткого обсуждения.
Учитывая наличие опыта использования системы easla.com для управления корреспонденцией решили попробовать использовать ее же для управления задачами. Тем более, задачи предполагали тесную интеграцию с перепиской.
В смысле, не задача, а постановка задачи. Изначально все-таки планировалось сделать задачи простыми: тема, описание, автор, исполнитель, плановые и фактические даты. Поэтому и требования были простыми:
Немного позже, уже во время пробной эксплуатации, потребовалось расширить функционал и требований стало больше:
В общем, задачи оказались не такими простыми, как казалось на первый взгляд.
Прежде всего, вопросы вызвали задачи нескольким исполнителям. Если создать одну задачу для всех, т.е. не персональную, то вероятность ее исполнения уменьшится почти до нуля. Каждый исполнитель будет надеяться на другого. Не знаю, как у других, но у нас именно так. Поэтому все задачи должны быть индивидуальными, так что, пришлось реализовать механизм клонирования задачи каждому исполнителю.
Затем надо было определиться с важностью задачи. Ввели три типа важности и каждому назначили максимальный срок исполнения:
Трудозатраты тоже интересная тема. Некоторые сотрудники затрудняются с указанием потраченного времени. Даже не знаю, как такое пояснить, но то ли боятся поставить слишком мало, то ли боятся указать слишком много, поэтому ввели приблизительную шкалу с достаточной для анализа точностью:
Определившись с инструментом и принципами работы процесса, приступил к реализации.
В easla.com создал новый процесс «Задачи». В нем создал объект «Задача». Объект наделил следующими атрибутами.
Обычный счетчик для последовательной нумерации.
Строковый атрибут. Значение вычисляется после создания задачи и не может быть изменено пользователем. В режим «только для чтения» атрибут переводится в скрипте «При инициализации»:
Пользователь, т.е. сотрудник организации. Автором может быть любой сотрудник организации. Полный список сотрудников формируется в скрипте «При инициализации»:
Атрибуту присваивается значение активного пользователя и режим «только для чтения», чтобы не было возможности создать задачу от другого сотрудника.
Сотрудник организации, которому поручено выполнение задачи. Множественный атрибут, т.к. одна и та же задача может выполняться разными специалистами, а, скажем, ГИП соберет все вместе в одно решение. Список сотрудников формируется в скрипте «При инициализации»:
Ссылка на объект «Договор». Инициализация атрибута происходит скрипте объекта «Задача».
Обычный строковый атрибут. По началу в него вписывали номер договора, но быстро отказались от такой практики и для номера договора ввели отдельный атрибут.
Многострочный строковый атрибут для подробного описания поставленной задачи. Описание может менять только автор задачи, поэтому в скриптах «При инициализации» и «При изменении» прописано:
Целочисленный атрибут, фактически являющийся флагом, определяющий возможность уведомления автора задачи о ее завершении. Список допустимых значений и начальное значение определяется в скрипте «При инициализации»:
Классификатор, определяющий категорию задачи. Сейчас их всего три:
Список допустимых значений формируется в скрипте «При инициализации»:
При изменении категории меняется «необходимость» атрибута "Основание для закрытия".
Классификатор. Список допустимых значений и начальное значение также определяется в скрипте «При инициализации»:
«При изменении» происходит пересчет плановой даты закрытия задачи:
Функция calcPlanEndDate описана в самом объекте.
Ссылка на объект, в частности, входящий или исходящий документ, который и стал основанием для появления задачи. Список входящих и исходящих документов формируется в скрипте «При инициализации»:
Файловый атрибут. Используется редко, но он нужен, если к задаче необходимо приложить сопроводительные документы.
Плановые дата и время начала выполнения задачи. Договорились о том, что она будет назначаться со смещением +1 час к текущему времени, что и прописано в скрипте «При инициализации»:
Изменить может только автор. Отдельное внимание на функцию calendarDateAdd, она вычисляет плановую дату начала в соответствии с производственным календарем!
Плановые дата и время окончания выполнения задачи. Зависит от важности и плановой даты начала. Начальное значение вычисляется «При инициализации»:
Фактические даты начала и окончания, которые проставляются только при изменении статуса задачи.
Ссылка на объект, а именно, исходящий документ, который стал основанием для закрытия задачи. Более того, если категория задачи «Подготовка ответа на входящее», то задача не может быть закрыта, пока не будет заполнено основание для закрытия. Список доступных исходящих документов формируется в скрипте «При инициализации»:
Целочисленный атрибут, содержащий количество секунд потраченных на выполнение задачи исполнителем. Так как задачи предполагаются краткосрочные, списка допустимых значений предостаточно. Он формируется при инициализации атрибута:
Многострочный текстовый атрибут для комментирования задачи. Атрибут сохраняет историю, таким образом можно отслеживать кто, что и когда написал.
Вот и все атрибуты!
Объект «Задача» обладает непростым поведением и валидацией, которые описаны в его скриптах. Вспомогательные функции для инициализация атрибутов и вычисления даты и времени описаны в скрипте:
Отдельно обращу внимание на функцию calcPlanEndDate, которая для вычисления плановой даты и времени использует функцию calendarDateAdd. С ее помощью удается рассчитать время именно в рабочих часах с учетом производственного календаря организации.
Дополнительная инициализация атрибутов и вычисление состояния плановых дат начала и окончания осуществляется в скрипте:
Состояния атрибутов Дата начала (план) и Дата окончания (план) вычисляются исходя из текущего времени. Как только дата пропущена, атрибут раскрашивается в красный, но перед этим сперва в оранжевый и желтый, таким нехитрым образом напоминая исполнителю о важности исполнения задачи в срок.
Перед сохранением объекта важно выполнить валидацию всех введенных значений и отказать в его сохранении, если что-то не так.
Помимо выявления ошибок, происходит проверка на существование похожей задачи по трем признакам: теме, основанию для открытия и исполнителю. Если найдена точно такая же задача, то в назначении новой отказано. Очень и очень полезная фишка!
Кроме этого, как было упомянуто выше, задача должна быть назначена только одному исполнителю, а все остальные в списке должны получить ее копии, поэтому задача сохраняется только с одним исполнителем, а остальные сохраняются в аргументах объекта.
Кстати, список исполнителей анализируется на наличие в нем ГИПа. И если он найден, то задача ГИПа становится основной, а все остальные создаются как подзадачи к ней. Такое упорядочивание задач очень удобно для ГИПа.
Временным прибежищем для списка исполнителей, которым будут назначены копии задач, является:
Таких «аргументов» в объекте можно создать сколько угодно. В моем случае хватило одного.
После сохранение объекта происходит создание клонов задач, если необходимо, и рассылка уведомлений.
В конечном счете форма объекта стала выглядеть как-то так:
![](https://habrastorage.org/r/w1560/files/708/c14/958/708c14958d294192945fff3af6db7a48.png)
Мудрить со статусами не стал: Создана, Принята, Выполнена, Отклонена. Принятой считается задача, с которой сотрудник ознакомился и принял к исполнению. Остальные статусы понятны из названия.
![](https://habrastorage.org/r/w1560/files/c16/638/c84/c16638c843af49d6b3c73541c251a5d2.png)
Небольшое число статусов ведет к лучшему понимаю процесса и уменьшению числа действий. Действий и прям получилось немного.
Назначение, собственно, следует из названия. Переводит задачу в статус «Принята» и устанавливает фактическую дату начала работы.
Успешно закрывает задачу фиксируя фактическую дату окончания работы над ней. Обязательно требует заполнения комментария. Это было одно из требований главного инженера. На «первых парах» он очень возмущался, когда подчиненные закрывали задачи без комментариев. Было совершенно непонятно, что сделано и на каком основании задача закрыта.
Кроме этого, действие проверяет, является ли задача вложенной, и если так, то проверяет, все ли рядом стоящие с ней задачи выполнены. При положительном результате, проверяет статус вышестоящей задачи и при необходимости направляет ее исполнителю по почте уведомление о том, что наверняка задачу можно закрывать, т.к. все вложенные задачи выполнены.
Очевидно из названия, что действие отклоняет задачу. Одно условие: в комментарии должна быть указана причина отклонения.
Действие доступное только менеджеру процесса. Позволяет вернуть задачу из конечного статуса в статус «Принята». Необходима на случай, когда задачу закрыли по ошибке. Требуется редко, но все равно необходима.
Первый процесс, в котором понадобилось создать команды. Команды отличаются от действий тем, что выполняются в контексте процесса, а не объекта. Таким образом, позволяют обрабатывать объекты «пакетно»: все сразу или выбранные пользователем.
Дело в том, что при интеграции процесса «Задачи» с «Перепиской» в исходящих письмах был реализован алгоритм, который при отправке исходящего письма в ответ на указанные входящие, комментирует задачи, созданные на основании соответствующих входящих и прописывает в их основании для закрытия отправляемое исходящее письмо. Уфф… Иными словами, задачи, которые были созданы при появлении входящего получают комментарии и заполненное основание для закрытия. Очень полезная фишка, т.к. часто, ГИП так занят, что закрывать задачу прямо сейчас ему некогда, а когда «руки дошли», ему нужно вспоминать, на каком основании он должен закрыть задачу. Понятно, что все надо делать вовремя, чтобы не забывать и не вспоминать, но уж если так случилось, то надо сократить время, затрачиваемое на восстановление картины в памяти.
В результате такого автоматического заполнения оснований для закрытия у ГИПа скапливаются задачи готовые к закрытию, осталось «только кнопочку нажать» (так было, пока главный инженер не потребовал обязательно указывать трудозатраты в каждой задаче). Когда таких готовых задач много, их закрытие превращалось в сплошное «тыкание мышкой». Поэтому была создана команда «Закрыть готовые».
Команда ищет готовые к закрытию задачи и закрывает первые 10 из них. Не все только потому, чтобы сохранить хоть какой-то контроль над их закрытием. Были случаи, когда, закрывая таким образом задачи ГИП спохватывался, но было уже поздно и задачи приходилось возвращать вручную.
Команда доступна на виде снизу (см. скриншоты ниже).
Точно такая же команда, но закрывает только выбранные пользователем задачи.
Виды (выборки), важная составляющая навигацию по объектам. Позволяют гибко настроить отображение списка задач в соответствии с требованиями пользователей.
«Интеллектуальный» вид, т.к. анализирует, в какой должности находится активный пользователь открывший его. В том случае, если его открыл начальник отдела, добавляет категории: наименование отдела и ФИО начальника. Таким образом, начальник отдела может видеть не только свои задачи, но и задачи порученные всем его подчиненным.
Кстати, существование такого вида было одним из важных требований, выдвинутых пользователями, в частности, главным инженером, ГИПами и начальниками отделов. Не всегда такое возможно реализовать в других системах, заточенных под управление индивидуальными задачами.
![](https://habrastorage.org/r/w1560/files/2c6/460/24a/2c646024a61b4fd7a642b5f00be2a840.png)
Понятно из названия, что вид отображает перечень задач, автором которых является активный пользователь. Также является «интеллектуальным», т.к. анализирует активного пользователя, а в случае, если он является ГИПом, добавляет две категории: группа ГИПа и ФИО ГИПа. Категории нужны для того, чтобы ГИП мог видеть как свои персональные задачи отдельно, так и задачи порученные его помощнику. ГИП и его помощник работают над одним пулом задач.
![](https://habrastorage.org/r/w1560/files/1e4/ffd/a55/1e4ffda557464692be495dcf909433b7.png)
Простой вид. Отображает перечень завершенных задач, исполнителем которых является активный пользователь.
Отображает полный перечень всех активных задач. Используется, как правило, для поиска чужих задач.
Очень важный перечень задач, т.к. содержит только задачи, назначенные по решению планерки и находящиеся на контроле. При выполнении скрипта, вид анализирует перечень всех задач, выбирает из него всех исполнителей, группирует по отделам и формирует список категорий вида с наименованиями отделов.
Таким образом, каждый сотрудник, а как правило, это начальники и главные специалисты отделов, могут легко отфильтровать из всего списка поставленных задач только свои.
![](https://habrastorage.org/r/w1560/files/2c2/709/b0f/2c2709b0f1ab4c2e868d39eecbf2f195.png)
Очень важный и непростой вид, который используют ГИПы для подготовки смет по авторскому надзору. Сложность вида заключается в том, что он на самом деле отображает информацию не только о задачах, но и об их основаниях на открытие и закрытие, т.е. о письмах. Таким образом, в виде отображается информация о трех объектах сразу! Такую возможность предоставляет easla.com при использовании атрибутов типа «Объект» и указании в описании вида атрибутов вложенных объектов через точку.
Кстати, обращу внимание на опцию export используемую в настройке вида. Если настраивать вид так, чтобы в нем отображалось все необходимое количество колонок, он будет такой большой в ширину, что не поместится даже на широкоформатный экран. Но при экспорте вида в Excel, нужно много колонок. Решается с помощью опции export. В ней указано, какие именно колонки экспортировать, вместо отображаемой в виде. Круто!
![](https://habrastorage.org/r/w1560/files/fb4/fde/e2d/fb4fdee2d09d4c6a9d1c874e08d8d965.png)
Помимо общих видов доступных пользователю через главное меню были созданы доп. виды для объектов, в терминологии easla.com – в контексте объекта. С их помощью удалось на форме входящего и исходящего письма отображать список зависимых от него задач. Очень удобно, когда надо проанализировать, какие задачи были назначены на основании выбранного, скажем, входящего документа и в каком состоянии они находятся в настоящий момент.
![](https://habrastorage.org/files/e5c/fe7/59a/e5cfe759a9874fa28f111e9360709e80.gif)
Кроме этого, подзадачи надо было отображать детальнее, чем система предлагает это по-умолчанию, поэтому был создан дополнительный вид.
![](https://habrastorage.org/r/w1560/files/ef2/b20/47e/ef2b2047e6924482bfa0fe6a67956fe8.png)
Ролей всего три: менеджер, участник, наблюдатель. Менеджер, понятно, может все. Участник не может только удалять задачи. Наблюдатель может только просматривать задачи.
![](https://habrastorage.org/r/w1560/files/f3e/b5f/523/f3eb5f5230484c6fbf6a3d268ebf74b4.png)
Конечно, при таком разделении прав доступа каждый сотрудник имеет право менять чужие задачи. Но, анализируя все озвученные к процессу требования и внутренние отношения в коллективе компании пришел к выводу, что разруливать права так, чтобы никто и ничего не мог изменить без весомой на то санкции окажется только «палкой в колесах». Обезопасил задачи от произвола путем использования «исторических» атрибутов. Иначе говоря, если кто-то и что-то изменит в задаче, останутся следы и будет видно, как было, как стало, и кто изменил.
Лично для меня всегда основным критерием успешности проекта автоматизации является не подписанный акт или техническое задание с грифом «Выполнено», а факт эксплуатации запущенной системы.
В настоящий момент процесс «Задачи» активной используется всеми участниками процесса. Большая заслуга в этом самого главного инженера, который дисциплинирует всех остальных личным примером. Кстати, он же является одним из основных поставщиков доп. требований к процессу.
Очень приятной неожиданностью стало появление вида «Авторский надзор», которые используется для выгрузки данных и формирования сметы в Microsoft Excel. Оказалось, что такая смета приносит немалую прибыль организации.
Кстати, кроме объекта «Задача» в рамках описанного процесса был реализован объект «Ознакомление», позволяющий отправлять любые документы на ознакомление с контролем исполнения. Но об этом отдельно.
![](https://habrastorage.org/files/079/320/553/079320553ba84d3fa3faf4868514cb9d.png)
Нетривиальные задачи потребовали нетривиального подхода. Подробное описание с картинками и исходным кодом под катом.
Проектная организация, численностью порядка 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',
));
В конечном счете форма объекта стала выглядеть как-то так:
![](https://habrastorage.org/files/708/c14/958/708c14958d294192945fff3af6db7a48.png)
Статусы
Мудрить со статусами не стал: Создана, Принята, Выполнена, Отклонена. Принятой считается задача, с которой сотрудник ознакомился и принял к исполнению. Остальные статусы понятны из названия.
![](https://habrastorage.org/files/c16/638/c84/c16638c843af49d6b3c73541c251a5d2.png)
Действия
Небольшое число статусов ведет к лучшему понимаю процесса и уменьшению числа действий. Действий и прям получилось немного.
Принять
Назначение, собственно, следует из названия. Переводит задачу в статус «Принята» и устанавливает фактическую дату начала работы.
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,
));
![](https://habrastorage.org/files/2c6/460/24a/2c646024a61b4fd7a642b5f00be2a840.png)
Назначенные мной
Понятно из названия, что вид отображает перечень задач, автором которых является активный пользователь. Также является «интеллектуальным», т.к. анализирует активного пользователя, а в случае, если он является ГИПом, добавляет две категории: группа ГИПа и ФИО ГИПа. Категории нужны для того, чтобы ГИП мог видеть как свои персональные задачи отдельно, так и задачи порученные его помощнику. ГИП и его помощник работают над одним пулом задач.
Назначенные мной
$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,
));
![](https://habrastorage.org/files/1e4/ffd/a55/1e4ffda557464692be495dcf909433b7.png)
Мои завершенные
Простой вид. Отображает перечень завершенных задач, исполнителем которых является активный пользователь.
Мои завершенные
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,
));
![](https://habrastorage.org/files/2c2/709/b0f/2c2709b0f1ab4c2e868d39eecbf2f195.png)
Авторский надзор
Очень важный и непростой вид, который используют ГИПы для подготовки смет по авторскому надзору. Сложность вида заключается в том, что он на самом деле отображает информацию не только о задачах, но и об их основаниях на открытие и закрытие, т.е. о письмах. Таким образом, в виде отображается информация о трех объектах сразу! Такую возможность предоставляет 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. В ней указано, какие именно колонки экспортировать, вместо отображаемой в виде. Круто!
![](https://habrastorage.org/files/fb4/fde/e2d/fb4fdee2d09d4c6a9d1c874e08d8d965.png)
Помимо общих видов доступных пользователю через главное меню были созданы доп. виды для объектов, в терминологии 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),
));
![](https://habrastorage.org/files/e5c/fe7/59a/e5cfe759a9874fa28f111e9360709e80.gif)
Кроме этого, подзадачи надо было отображать детальнее, чем система предлагает это по-умолчанию, поэтому был создан дополнительный вид.
Подзадачи
$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,
));
![](https://habrastorage.org/files/ef2/b20/47e/ef2b2047e6924482bfa0fe6a67956fe8.png)
Роли
Ролей всего три: менеджер, участник, наблюдатель. Менеджер, понятно, может все. Участник не может только удалять задачи. Наблюдатель может только просматривать задачи.
![](https://habrastorage.org/files/f3e/b5f/523/f3eb5f5230484c6fbf6a3d268ebf74b4.png)
Конечно, при таком разделении прав доступа каждый сотрудник имеет право менять чужие задачи. Но, анализируя все озвученные к процессу требования и внутренние отношения в коллективе компании пришел к выводу, что разруливать права так, чтобы никто и ничего не мог изменить без весомой на то санкции окажется только «палкой в колесах». Обезопасил задачи от произвола путем использования «исторических» атрибутов. Иначе говоря, если кто-то и что-то изменит в задаче, останутся следы и будет видно, как было, как стало, и кто изменил.
Итоги
Лично для меня всегда основным критерием успешности проекта автоматизации является не подписанный акт или техническое задание с грифом «Выполнено», а факт эксплуатации запущенной системы.
В настоящий момент процесс «Задачи» активной используется всеми участниками процесса. Большая заслуга в этом самого главного инженера, который дисциплинирует всех остальных личным примером. Кстати, он же является одним из основных поставщиков доп. требований к процессу.
Очень приятной неожиданностью стало появление вида «Авторский надзор», которые используется для выгрузки данных и формирования сметы в Microsoft Excel. Оказалось, что такая смета приносит немалую прибыль организации.
Кстати, кроме объекта «Задача» в рамках описанного процесса был реализован объект «Ознакомление», позволяющий отправлять любые документы на ознакомление с контролем исполнения. Но об этом отдельно.