В процессе автоматизации работы кадровой службы в программе 1С:Зарплата и Управление Персоналом, редакция 2.5 разработчики часто сталкиваются с жесткими требованиями пользователей. Одно из типовых требований — проверка корректности заполнения документов. Например, кадровик хочет убедиться, что в документе ТабельУчетаРабочегоВремениОрганизации не проставлена «Явка» (или другие рабочие виды времени) в те дни, когда по сотруднику зафиксированы отклонения (командировки, отпуска, больничные). При этом проверять нужно не только проведенные, но и сохраненные, но еще непроведенные кадровые документы.
В этой статье мы подробно разберем, как обратиться к реквизитам видов времени в документе «Табель» на определенный день, проанализируем структуру его табличной части, выявим подводные камни производительности и напишем оптимальный алгоритм проверки пересечений.
На первый взгляд задача кажется простой. В табличной части ОтработанноеВремя документа ТабельУчетаРабочегоВремениОрганизации для каждого дня месяца предусмотрены отдельные колонки. Автор темы на форуме предлагает обращаться к ним динамически, собирая текст запроса конструкцией вида:
"ВЫБРАТЬ
| ТабельУчетаРабочегоВремениОрганизацииОтработанноеВремя.Сотрудник,
| ТабельУчетаРабочегоВремениОрганизацииОтработанноеВремя.ПервыйВидВремени" + Строка(НомерДня) + " КАК ВидВремени
| ИЗ
| Документ.ТабельУчетаРабочегоВремениОрганизации.ОтработанноеВремя КАК ТабельУчетаРабочегоВремениОрганизацииОтработанноеВремя"
Давайте проанализируем этот код. С точки зрения синтаксиса встроенного языка 1С, динамическое формирование текста запроса путем конкатенации строк абсолютно корректно. Однако в этом решении кроются две серьезные проблемы:
Строка(НомерДня) для чисел больше 999 во встроенном языке 1С может приводить к добавлению неразрывных пробелов (группировка разрядов). Хотя для дней месяца (числа от 1 до 31) это не актуально, хорошим тоном и стандартом разработки 1С является явное форматирование без разделителей групп. Рекомендуется использовать функцию Формат():
ИндексСтроки = Формат(День(НужнаяДата), "ЧГ=0");
ПервыйВидВремени приведет к потере огромного пласта данных. Рассмотрим подробнее архитектуру хранения времени в ЗУП 2.5.Разработчику, проектирующему проверку табеля, критически важно знать, что для каждого из 31 дня в строке табличной части предусмотрено не по одному реквизиту вида времени, а по четыре группы реквизитов! Это сделано для того, чтобы в один и тот же день сотруднику можно было отразить несколько разных видов использования времени (например, одновременно дневную «Явку», «Ночные» часы, «Сверхурочные» или «Работа в праздники»).
Полный перечень реквизитов для каждого дня месяца (где N — номер дня от 1 до 31) выглядит следующим образом:
ПервыйВидВремениN и ПервыйЧасовNВторойВидВремениN и ВторойЧасовNТретийВидВремениN и ТретийЧасовNЧетвертыйВидВремениN и ЧетвертыйЧасовNЕсли мы будем проверять только ПервыйВидВремени + ИнндексСтроки, мы гарантированно упустим ситуации, когда «Явка» или другое рабочее время записано во вторую, третью или четвертую группу реквизитов за этот день. Следовательно, для корректной проверки конкретного дня нам необходимо анализировать все 4 группы реквизитов.
Представим, что нам нужно проверить табель за весь месяц. Если мы пойдем по пути динамического построения запроса, нам придется в цикле по дням месяца генерировать текст запроса, передавать параметры и выполнять его к базе данных 31 раз. Выполнение запросов в цикле — это грубый антипаттерн разработки в системе 1С, который катастрофически снижает производительность системы и перегружает СУБД.
Давайте найдем альтернативное, профессиональное решение. Вместо множества мелких динамических запросов мы напишем один статический запрос, который транспонирует («развернет») всю многоколоночную структуру табличной части документа в плоскую таблицу. Для этого мы применим конструкцию ОБЪЕДИНИТЬ ВСЕ (UNION ALL).
Ниже представлен пример запроса, который преобразует горизонтальную структуру табеля (по дням) в вертикальную (по датам). Для экономии места в примере показаны секции для 1-го и 2-го дня. В реальной обработке этот запрос должен быть расширен аналогичным образом до 31 дня (все секции объединяются через ОБЪЕДИНИТЬ ВСЕ):
ВЫБРАТЬ
Табель.Ссылка КАК Ссылка,
Табель.Сотрудник КАК Сотрудник,
ДОБАВИТЬКДАТЕ(Табель.Ссылка.ПериодРегистрации, ДЕНЬ, 0) КАК ДатаФакта,
Табель.ПервыйВидВремени1 КАК ВидВремени,
Табель.ПервыйЧасов1 КАК Часов
ПОМЕСТИТЬ ВТ_ФактическоеВремя
ИЗ
Документ.ТабельУчетаРабочегоВремениОрганизации.ОтработанноеВремя КАК Табель
ГДЕ
Табель.Ссылка.Дата МЕЖДУ &ДатаН И &ДатаК
И Табель.Сотрудник В ИЕРАРХИИ(&Сотрудник)
И Табель.ПервыйВидВремени1 <> ЗНАЧЕНИЕ(Справочник.КлассификаторИспользованияРабочегоВремени.ПустаяСсылка)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Табель.Ссылка,
Табель.Сотрудник,
ДОБАВИТЬКДАТЕ(Табель.Ссылка.ПериодРегистрации, ДЕНЬ, 0),
Табель.ВторойВидВремени1,
Табель.ВторойЧасов1
ИЗ
Документ.ТабельУчетаРабочегоВремениОрганизации.ОтработанноеВремя КАК Табель
ГДЕ
Табель.Ссылка.Дата МЕЖДУ &ДатаН И &ДатаК
И Табель.Сотрудник В ИЕРАРХИИ(&Сотрудник)
И Табель.ВторойВидВремени1 <> ЗНАЧЕНИЕ(Справочник.КлассификаторИспользованияРабочегоВремени.ПустаяСсылка)
// ... Повторить для ТретийВидВремени1 и ЧетвертыйВидВремени1 ...
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Табель.Ссылка,
Табель.Сотрудник,
ДОБАВИТЬКДАТЕ(Табель.Ссылка.ПериодРегистрации, ДЕНЬ, 1), // Для 2-го дня прибавляем 1 день к началу месяца
Табель.ПервыйВидВремени2,
Табель.ПервыйЧасов2
ИЗ
Документ.ТабельУчетаРабочегоВремениОрганизации.ОтработанноеВремя КАК Табель
ГДЕ
Табель.Ссылка.Дата МЕЖДУ &ДатаН И &ДатаК
И Табель.Сотрудник В ИЕРАРХИИ(&Сотрудник)
И Табель.ПервыйВидВремени2 <> ЗНАЧЕНИЕ(Справочник.КлассификаторИспользованияРабочегоВремени.ПустаяСсылка)
// ... И так далее для всех остальных дней до 31-го числа ...
Преимущества такого подхода очевидны: мы один раз обращаемся к базе данных и получаем идеально подготовленную плоскую таблицу ВТ_ФактическоеВремя с колонками: Ссылка, Сотрудник, ДатаФакта, ВидВремени, Часов. С этой таблицей теперь чрезвычайно легко работать и соединять ее с любыми другими источниками данных.
Поскольку кадровик просит анализировать «в том числе сохраненные, но непроведенные» документы, мы принципиально не можем использовать виртуальные таблицы регистров расчета (таких как ОсновныеНачисленияОрганизаций) или регистров накопления (РабочееВремяСотрудниковОрганизаций). Непроведенные документы просто не делают туда движений.
Единственный надежный способ получить эти данные — обратиться напрямую к таблицам документов-отклонений. Нам потребуется собрать информацию из следующих основных документов ЗУП 2.5:
Документ.ОтпускаОрганизаций (табличная часть РаботникиОрганизаций)Документ.КомандировкиОрганизаций (табличная часть РаботникиОрганизаций)Документ.НевыходыОрганизаций (табличная часть РаботникиОрганизаций)Документ.БольничныйЛист (данные берутся из шапки документа: ДатаНачала, ДатаОкончания)Давайте посмотрим, как можно собрать эти отклонения в одну временную таблицу ВТ_Отклонения с помощью пакетного запроса:
ВЫБРАТЬ
Отпуска.Сотрудник КАК Сотрудник,
Отпуска.ДатаНачала КАК ДатаНачала,
Отпуска.ДатаОкончания КАК ДатаОкончания,
Отпуска.Ссылка КАК ДокументОтклонения,
"Отпуск" КАК ТипОтклонения
ПОМЕСТИТЬ ВТ_Отклонения
ИЗ
Документ.ОтпускаОрганизаций.РаботникиОрганизаций КАК Отпуска
ГДЕ
НЕ Отпуска.Ссылка.Проведен
И Отпуска.Ссылка.ПометкаУдаления = ЛОЖЬ
И Отпуска.ДатаНачала <= &ДатаК
И Отпуска.ДатаОкончания >= &ДатаН
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Командировки.Сотрудник,
Командировки.ДатаНачала,
Командировки.ДатаОкончания,
Командировки.Ссылка,
"Командировка"
ИЗ
Документ.КомандировкиОрганизаций.РаботникиОрганизаций КАК Командировки
ГДЕ
НЕ Командировки.Ссылка.Проведен
И Командировки.Ссылка.ПометкаУдаления = ЛОЖЬ
И Командировки.ДатаНачала <= &ДатаК
И Командировки.ДатаОкончания >= &ДатаН
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Больничный.Сотрудник,
Больничный.ДатаНачала,
Больничный.ДатаОкончания,
Больничный.Ссылка,
"Больничный лист"
ИЗ
Документ.БольничныйЛист КАК Больничный
ГДЕ
НЕ Больничный.Проведен
И Больничный.ПометкаУдаления = ЛОЖЬ
И Больничный.ДатаНачала <= &ДатаК
И Больничный.ДатаОкончания >= &ДатаН
Обратите внимание на условия в секции ГДЕ. Мы обязательно проверяем, что документы не проведены (НЕ Проведен) и не помечены на удаление (ПометкаУдаления = ЛОЖЬ). Также накладывается стандартное условие пересечения интервалов дат: начало периода отклонения должно быть меньше или равно концу проверяемого периода, а окончание отклонения — больше или равно началу проверяемого периода.
Теперь, когда у нас в пакете запросов сформированы две чистые временные таблицы — ВТ_ФактическоеВремя (на основе табеля) и ВТ_Отклонения (на основе непроведенных документов), мы можем легко соединить их и выявить коллизии. Нас интересуют ситуации, когда дата работы по табелю попадает внутрь периода непроведенного отклонения.
Давайте напишем финальный шаг нашего пакета запросов:
ВЫБРАТЬ
ФактическоеВремя.Ссылка КАК ДокументТабель,
ФактическоеВремя.Сотрудник КАК Сотрудник,
ФактическоеВремя.ДатаФакта КАК ДатаКонфликта,
ФактическоеВремя.ВидВремени КАК ВидВремениВТабеле,
Отклонения.ДокументОтклонения КАК ДокументОтклонения,
Отклонения.ТипОтклонения КАК ТипОтклонения
ИЗ
ВТ_ФактическоеВремя КАК ФактическоеВремя
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Отклонения КАК Отклонения
ПО ФактическоеВремя.Сотрудник = Отклонения.Сотрудник
И ФактическоеВремя.ДатаФакта МЕЖДУ Отклонения.ДатаНачала И Отклонения.ДатаОкончания
ГДЕ
ФактическоеВремя.ВидВремени В (&СписокРабочихВидовВремени)
В параметр &СписокРабочихВидовВремени мы передаем массив видов времени, которые сигнализируют о присутствии сотрудника на работе (например, «Явка», «Ночные», «Вечерние», «Работа в праздники и выходные»). Если запрос возвращает строки, это означает, что у нас есть явные логические пересечения: в табеле зафиксирована работа, хотя в системе сохранен непроведенный документ, подтверждающий отсутствие сотрудника по уважительной причине.
Мы детально разобрали архитектурную задачу проверки документов «Табель» в программе 1С:ЗУП 2.5. На первый взгляд простая задача динамической подстановки имени колонки скрывала в себе серьезные ограничения производительности и неполноту выборки данных.
Применяя метод транспонирования строк табличной части через ОБЪЕДИНИТЬ ВСЕ и собирая данные непроведенных документов напрямую из таблиц СУБД, мы получили быстрое, надежное и масштабируемое решение, которое гарантированно удовлетворит даже самых требовательных кадровых аудиторов.