Заявка на отпуск в Redmine

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

К сожалению, или к счастью «докручивание» Redmine упирается в написание скриптов на Ruby для шедеврального компонента, написанного соотечественником — Computed Custom Field.

Почему к сожалению? Я не программирую на Ruby on Rails, но писал на Delphi, C#, PHP и пр. В принципе, конструкции схожие, документации много, но все-же требует чуть больше времени и нет возможности отладить.

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

Почему к счастью? Потому-что на Ruby можно написать гибкую бизнес-логику и она будет вполне себе читаемой для любого программиста. Поскольку у нас ИТ компания, не будет проблем, чтобы найти разработчика, который сможет разобраться в моих скриптах и поправить если что-то нужно.

Заявка на отпуск

Реализуем заявку на отпуск, так чтобы было максимально удобно с точки зрения юзабилити. Для этого будем использовать задачи Redmine:

  1. Создадим трекер «Заявка на отпуск». Трекер необходим, поскольку поля в заявке на отпуск будут отличаться от заявки на командировку, например.
  2. Создадим новый проект «Заявки», чтобы дать к нему всем сотрудникам компании.
  3. Создадим дополнительные поля:
    1. «С какого» (тип «Дата») — для указания с какого числа начинается отпуск. Лучше использовать стандартное поле «Дата начала», поскольку в этом случае можно использовать стандартный модуль «Календарь» для отображения календарей отпусков.
    2. «До какого»(тип «Дата») — для указания до какого числа продлится отпуск. Лучше использовать стандартное поле «Дата завершения».
    3. Отпускник (тип «Пользователь») — для указания кто уходит в отпуск.
    4. Тип отсутствия (тип «Список»): отпуск, отпуск без сохранения з.п. (административный), отгул (нужно отработать).
  4. Создадим статусы:
    1. Новая — статус в котором создается заявка.
    2. На согласовании — заявку согласовывает менеджер.
    3. Согласовано — менеджер согласовал заявку.
    4. В работе — заявку обрабатывает офис-менеджер, распечатывая документ для гос. органов и подписывая у руководителя. В теории можно заменить простой ЭЦП.
    5. На согласование HR — заявка направлена на согласование HR менеджеру.
    6. Согласовано HR — HR менеджер согласовал заявку.
  5. Роли для проекта согласования заявок:
    1. Менеджер — тот, кто имеет право согласовывать заявки на отпуск.
    2. HR менеджер — менеджер, который финально согласовывает заявку на отпуск.

Заполнение поля Отпускник

Считаем, что тото, кто уходит в отпуск заполняет заявление самостоятельно. Соответственно, нет смысла заставлять его выбирать в кастомном поле User себя самого. Воспользуемся компоненом Computed Custom Field.

Как получить ID поля, статуса, пользователя, роли и пр. смотрим в предыдущих статьях по Redmine. Название полей можно раздобыть здесь.

При написании скриптов для Computed Custom Field, поскольку средств отладки нет, можно  использовать простой трюк, записывая значения в поле description. Например, для проверки как отрабатывает получение дня недели:

self.description += self.due_date.cwday + "\r\n"

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

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

#ID current field is 44
if (cfs[44].blank?)
   User.current.id
else
   cfs[44]
end

Если же нужно, чтобы в поле сохранилось значение пользователя, когда заявка в состоянии «Новая», то код будет такой:

#ID current field is 44
#ID for status "Новая" - 14
if (self.status_id == 14)
   User.current.id
else
   cfs[44]
end

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

Нужно понимать, что для упрощения количества шагов в бизнс-процессе, сотруднк сразу после создания заявки может отправить её на статус «На согласование», минуя статус «Новая». Соответственно, в скрипте нужно учесть оба вариант статуса.

Статус «Новая» необходим, если сотрудник до конца не определился с датами и пока не готов отправлять заявку на руководителя, но хочет её сохранить для дальнейшей корректировки.

Заполнение поля «Назначена»

Есть простой вариант без програмиирования назначить задачу на сотрудника. Для этого в проекте достаточно создать категорию «Заявка на отпуск» и прописать, что задачи с такой катеогорией назначаются на определенного сотрудника. Например, офис-менеджера.

При такой реализации есть несколько недостатков:

  1. Сотрудник (группа) привязанный к категории ассоциирован с проектом, а не с трекером. Соответственно, если в рамках проекта есть несколько трекеров, то при создании задачи они все будут назначены одному и тому-же сотруднику.
  2. Пользователю необходимо совершать доп. действия: он сначала выбирает трекер «Заявка на отпуск», а затем ещё и категорию «Отпуск». Это дублирование действий.
  3. Невозможно сделать гибкие условия назначения. Например, если отпускник с ролью «Верстальщик» заполнил заявку на отпуск, то для согласования задача назначается на его руководителя. Понятно, что можно не усложнять и  дать доступ сотруднику выбирать руководителя самостоятельно. Однако, в этом случае нужно писать какой-то регламент, а, соответственно, сотрудникам зачем-то тратить время на изучение. Вряд ли его внимательно прочитают, да и спустя пол-года забудут. 🙂 Это все приводит к ошибкам в процессе, которых лучше избегать.

Итак, каков примерный бизнес-процесс:

  1. Сотрудник с некоторой ролью создает заявку на отпуск, выбирает трекер «Заявка на отпуск».
  2. Если ему нужно ещё подумать, он оставляет её в статусе «Новая» и задача никому не назначается.
  3. После того как сотрудник полностью определился со срокам, он проставляет даты и статус меняет на «На согласование».
  4. Скрипт на Ruby проверяет, что задача в статусе «На согласование» и смотрит, к какой роли/группе относится пользователь в организации.
  5. Идеально, проверять не по роли/группе, а по оргструктуре, поскольку пользователь может быть добавлен в несколько ролей и в этом случае руководитель может быть выбран неверно. К сожалению, оргструктуру в Redmine можно задать только плагинами, поэтому этот вариант не рассматриваем.
  6. Назначается руководитель для согласования. Если для роли не определен руководитель, заявка назначается на офис-менеджера.
  7. После просмотра руководителем он ставит заявку в статус «Согласовано» или «Отклонено», с соответствующими комментариями.
  8. Если статус «Отклонено», то в назначенные прописывается автор заявки, чтобы внести соотвествующие правки.
  9. Все заявки на отпуск (за исключением административного и отгулов) согласованные руководителем требуют согласования HR менеджером. У менеджера есть возможность перевести заявку в статус «На согласование HR».
  10. После того как HR менеджер согласовал заявку, он проставляет статус «Согласовано HR». В этот момент заявка назначается на офис-менеджера для оформления в соотвествии с требованиями госорганов, для передачи информации в бухгалтерию и соблюдения законодательных формальностей, вроде подписания генеральным директором.

Ruby скрипт на заполнение поля «Назначена» можно поместить в любой из computed custom field. Например, в поле Отпускник.

Далее в скриптах проверку на трекер можно не делать, поскольку поле отпускник отображается только для трекера «Заявка на отпуск». Я добавил эту проверку для полноты ощущений.

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

Проверка роли

Разберем Ruby скрипт по частям. Первым делом нужно проверить, если при сохранении заявки статус «Согласование», то нужно проставить в поле «Назначено» руководителя для соответствующей роли.

# ID статуса "На согласование (new)" - 29
# ID статуса "Согласовано" - 28
# ID офис-менеджера - 28
if (tracker_id == 12) # ID for tracker "Заявка на отпуск"
if (self.status_id == 29) # ID статуса "На согласование (new)"
if (self.author.roles_for_project(project).map(&:id).include? 7) #ID for role Верстальщик
  self.assigned_to_id = 11 # ID for Coder's leader
elsif (self.author.roles_for_project(project).map(&:id).include? 6) #ID for role Тестировщик
  self.assigned_to_id = 11 # ID for Coder's leader
elsif (self.author.roles_for_project(project).map(&:id).include? 10) #ID for Дизайнер
  self.assigned_to_id = 19 # ID for Operations director
elsif (self.author.roles_for_project(project).map(&:id).include? 8) #ID for Согласователь
  self.assigned_to_id = 19 # ID for Operations director
elsif (self.author.roles_for_project(project).map(&:id).include? 12) #ID for Офис-менеджер
  self.assigned_to_id = 19 # ID for Operations director
elsif (self.author.roles_for_project(project).map(&:id).include? 11) #ID for Программисты
  self.assigned_to_id = 7 # ID for tech director
else
  self.assigned_to_id = 28 # ID for office-manager
end
end
end

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

# ID статуса "На согласование (new)" - 29
# ID статуса "Согласовано" - 28
# ID customer to general manager - 27
# ID customer to process request for vacation - 28
if (tracker_id == 12) # ID for tracker "Заявка на отпуск"
if (self.status_id == 29) # ID статуса "На согласование (new)"
if (User.current.group_ids.include? 30) #ID for the group Верстальщик
  self.assigned_to_id = 11 # ID for Руководитель верстки
elsif (self.author.group_ids.include? 29) #ID for the group Тестировщик
  self.assigned_to_id = 11 # ID for руководитель верстки
elsif (self.author.group_ids.include? 10) #ID for the group Дизайнер
  self.assigned_to_id = 19 # ID for операционный директор
elsif (self.author.group_ids.include? 32) #ID for the group Согласователь
  self.assigned_to_id = 19 # ID for операционный дтиректор
elsif (self.author.group_ids.include? 39) #ID for Офис-менеджер
  self.assigned_to_id = 19 # ID for операционный директор
elsif (User.current.group_ids.include? 38) #ID for the программисты
  self.assigned_to_id = 7 # ID for технический директор
else
  self.assigned_to_id = 28 # ID for офис-менеджер
end
end

Здесь не учтен момент, что руководитель (team lead) обычно входит в ту-же роль, что и сотрудники. Поэтому если руководтитель отдела верстки создаст заявку, в текущем скрипте она будет назначена на него. Чтобы учесть этот момент нужно для сотрудников с id 11, 19, 7 прописать согласовантом генерального директора.

# ID for general manager - 27
if (User.current.id == 11 || User.current.id == 19 || User.current.id == 7)
 self.assigned_to_id = 27 # ID for general manager
end

Подготовка документов

Если заявка в статусе «Согласовано», то она назначается на офис-менеджера

# ID статуса "Согласовано (new)" - 28
# ID офис-менеджера - 28 
if (tracker_id == 12) # ID for tracker "Заявка на отпуск" 
if (self.status_id == 28) # ID статуса "Cогласовано(new)"
 self.assigned_to_id = 28 # ID for office-manager
end
#ID field "Тип отсутствия" - 51
if (cfs[51] == "Отпуск")
 self.assigned_to_id = 19 # ID for HR manager
else
 self.assigned_to_id = 28 # ID for office-manager
end
end 

Заявка отклонена

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

#ID статуса "Отклонена" - 6
if (tracker_id == 12) # ID for tracker "Заявка на отпуск" 
if (self.status_id == 6) # ID статуса "Отклонена" 
self.assigned_to_id = self.author_id;
end

Скрываем заявку

Важный момент связанный с безопасностью — чтобы заявки на отпуска, а особенно на командировки были доступны только ограниченному кругу людей. Единственный способ скрыть задачи — выставить флаг в поле «Private» («Частный»). В этом случае задачу будет видеть сам создатель и пользователи у которых для роли прописан доступ для просмотра приватных задач.

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

Но какие ограничения накладывает этот подход организации прав:

  1. Если в одном проекте пользователь должен иметь возможность просматривать приватные сообщения, а в другом — нет, единственный вариант — создание отдельных ролей под каждый проект.
  2.  Если одному пользователю нужно дать доступ к приватным задачам, то единственный вариант — создать отдельную роль, дать для неё доступ для просмотра приватных задач и добавить её сотруднику.

Это можно сделать следующим кодом:

self.is_private = true;

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

Вычисляем кол-во дней

Сотруднику для контроля неплохо видеть сколько дней пройдет с даты начала отпуска по дату окончания. Для этого создаем computed custom field «Number of days». У меня id этого поля — 46. В него добавляем код приведеенный ниже.

Я также добавил целочисленное поле «Число дней» и у меня id — 49. По моей идее в него можно вписать кол-во дней и оно должно было прибавится к дате начала отпуска и изменить дату окончания.

К сожалению, эта часть кода отрабатывает частично, а именно присвоение переменной cfs[38] происходит, поскольку в следующем условии при вычитании  cfs[46] = cfs[38].to_date — cfs[37].to_date получается cfs[46] = cfs[49], т.е. все  правильно. Однако, после сохранения задачи поле cfs[38] остается неизменным. Подозреваю, что после изменения переменной программным образом идет копирование значения визуального компронента в переменную и оно возвращается к исходному значению. Если это так, что решить не получиться.

#ID кол-во дней - 49
#ID date from - 37
#ID date to - 38

if cfs[49].present? && cfs[37].present? && cfs[49] > 0
    cfs[38] = cfs[37].to_date + cfs[49];
end

if cfs[38].present? && cfs[37].present?
    cfs[46] = cfs[38].to_date - cfs[37].to_date + 1
else
    cfs[46]
end

Вторая часть кол отрабатывает нормально и поле «Число дней» заполняется разницей между датами начала и окончания отпуска. Естественно, праздничные дни не учитываются, календарь обычный.

Если для заполнения используются стандартные поля «Дата начала» и «Дата завершения», то код для вычисления кол-ва дней будет другим:

if start_date.present? && due_date.present?
   cfs[46] = due_date - start_date + 1
else
   cfs[46]
end

Тема задачи

Чтобы тема заявки была стандартизованная и можно было быстро ориентироваться, нужно добавить следующий код в computed custom field «Число дней». Последовательность строк в if…else имеет значение, иначе дата будет занулятся!

if start_date.present? && due_date.present?
    self.subject = self.author.name + " в отпуске c " + start_date.strftime("%F") + " по " + due_date.strftime("%F"); 
    due_date - start_date + 1;
else
    cfs[46]
end

Кто согласовал?

Для удобства фильтрации имеет смысл добавить computed custom field с типом User и назвать «Согласовано» (ID поля в примере 50). В него будет сохраняться пользователь, согласовавший заявку на отпуск. Здесь нет проверки группы пользователя, т.е. имеет ли он право согласовать. Сделан простой вариант проверки по id пользователей, которые могут согласовывать заявки на отпуск:

# ID для статуса "Согласовано (new)" - 28
if (self.status_id == 28) && (User.current.id == 11 || User.current.id == 19 || User.current.id == 7 ||
  User.current.id == 27)
  User.current.id;
else
  cfs[50]
end

Вот и всё. 🙂

Spread the love
Запись опубликована в рубрике IT рецепты, IT решения для бизнеса с метками . Добавьте в закладки постоянную ссылку.