При разработке отчетов и печатных форм в конфигурации «1С:Зарплата и управление персоналом» часто требуется получить актуальные кадровые данные сотрудника (подразделение, должность, ставку) и одновременно узнать его текущее состояние (работает, находится в отпуске, на больничном или в командировке). Попытка объединить два отдельных запроса в один нередко приводит к трудноуловимым ошибкам и потере строк в отчете. Разберем типичные ошибки, проанализируем причины их возникновения и рассмотрим правильные варианты реализации задачи.
Рассмотрим исходную ситуацию: у нас есть срез последних записей регистра сведений КадроваяИсторияСотрудников и данные из регистра СостоянияСотрудников. На форумах часто предлагают связать эти таблицы внутренним соединением с условием совпадения периодов, например:
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.СостоянияСотрудников КАК СостоянияСотрудников
ПО КадроваяИсторияСотрудниковСрезПоследних.Период = СостоянияСотрудников.Период
И КадроваяИсторияСотрудниковСрезПоследних.Сотрудник = СостоянияСотрудников.Сотрудник
Разберем, почему такая конструкция является грубой ошибкой и в реальной базе выдаст пустой или неполный результат:
Период = Период результат запроса вернет строку только в том исключительном случае, если сотрудника перевели на новую должность ровно в день ухода в отпуск. В остальных случаях строка сотрудника просто исчезнет из выборки.ВНУТРЕННЕЕ СОЕДИНЕНИЕ полностью исключит из итоговой таблицы сотрудников, у которых вовсе нет записей в таблице состояний или они не подошли под жесткие критерии отбора. Всегда следует использовать ЛЕВОЕ СОЕДИНЕНИЕ, беря за основу кадровую историю.ЛЕВОЕ СОЕДИНЕНИЕ, но в секции ГДЕ пишем условие на правую таблицу ГДЕ СостоянияСотрудников.Период В (...), СУБД отбросит все строки с NULL, сведя левое соединение к внутреннему. Условия отбора к присоединяемой таблице необходимо указывать либо внутри условия связи ПО, либо предварительно подготавливать данные во временных таблицах.Период В (ВЫБРАТЬ МАКСИМУМ(...) ГДЕ Таблица.Сотрудник = ВнешняяТаблица.Сотрудник) заставляет СУБД выполнять сканирование регистра для каждой строки основного запроса. Для списков сотрудников это приводит к серьезным задержкам.Проанализируем оптимальный способ построения чистого запроса без привлечения внутренних программных функций. Разберем алгоритм по шагам:
ГДЕ, а непосредственно в параметрах виртуальной таблицы среза (что критически важно для быстродействия). Сохраняем результат во временную таблицу ВТ_Кадры.ВТ_МаксПериодыСостояний выбираем максимальную дату события из регистра СостоянияСотрудников, которая меньше или равна требуемой дате, сгруппировав по сотруднику.Состояние, и помещаем во временную таблицу ВТ_АктуальныеСостояния.ЛЕВОЕ СОЕДИНЕНИЕ основной кадровой таблицы с актуальными состояниями исключительно по полю Сотрудник, без связывания периодов.Посмотрим на практическую реализацию этого алгоритма:
ВЫБРАТЬ
Кадры.Сотрудник КАК Сотрудник,
Кадры.Регистратор КАК Регистратор,
Кадры.Период КАК Период,
Кадры.Должность КАК Должность,
ЕСТЬNULL(Кадры.Подразделение, """") КАК Подразделение,
Кадры.КоличествоСтавок КАК КоличествоСтавок,
Кадры.ВидЗанятости КАК ВидЗанятости
ПОМЕСТИТЬ ВТ_Кадры
ИЗ
РегистрСведений.КадроваяИсторияСотрудников.СрезПоследних(
КОНЕЦПЕРИОДА(&НачалоПериода, ДЕНЬ),
Сотрудник = &Сотрудник) КАК Кадры
ГДЕ
ТИПЗНАЧЕНИЯ(Кадры.Регистратор) <> ТИП(Документ.Увольнение)
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Состояния.Сотрудник КАК Сотрудник,
МАКСИМУМ(Состояния.Период) КАК Период
ПОМЕСТИТЬ ВТ_МаксПериодыСостояний
ИЗ
РегистрСведений.СостоянияСотрудников КАК Состояния
ГДЕ
Состояния.Сотрудник = &Сотрудник
И Состояния.Период <= &НачалоПериода
СГРУППИРОВАТЬ ПО
Состояния.Сотрудник
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
МаксДаты.Сотрудник КАК Сотрудник,
Состояния.Состояние КАК Состояние
ПОМЕСТИТЬ ВТ_АктуальныеСостояния
ИЗ
ВТ_МаксПериодыСостояний КАК МаксДаты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.СостоянияСотрудников КАК Состояния
ПО МаксДаты.Сотрудник = Состояния.Сотрудник
И МаксДаты.Период = Состояния.Период
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Кадры.Сотрудник КАК Сотрудник,
Кадры.Регистратор КАК Регистратор,
Кадры.Период КАК ПериодКадров,
Кадры.Должность КАК Должность,
Кадры.Подразделение КАК Подразделение,
Кадры.КоличествоСтавок КАК КоличествоСтавок,
Кадры.ВидЗанятости КАК ВидЗанятости,
Кадры.Сотрудник.ФизическоеЛицо.Код КАК ТабельныйНомер,
Кадры.Сотрудник.ФизическоеЛицо.Наименование КАК ФИО,
ЕСТЬNULL(АктуальныеСостояния.Состояние, ЗНАЧЕНИЕ(Перечисление.СостоянияСотрудника.ПустаяСсылка)) КАК Состояние
ИЗ
ВТ_Кадры КАК Кадры
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_АктуальныеСостояния КАК АктуальныеСостояния
ПО Кадры.Сотрудник = АктуальныеСостояния.Сотрудник
В этом запросе данные кадровой истории сохраняются в полном объеме, а состояние сотрудника подтягивается корректно на указанный момент времени независимо от даты кадрового перемещения.
Выясним, почему прямое чтение регистров в конфигурации «1С:Зарплата и управление персоналом 3» не всегда является лучшим решением. В ЗУП 3.1 структура кадровых данных устроена сложно: применяются интервальные регистры сведений, документы-исправления, вытеснения периодов и расчетные механизмы. Регистр СостоянияСотрудников содержит поля интервалов (например, дату окончания события), а также периодичность в пределах года, поэтому простой срез по дате без анализа границ действия может вернуть уже завершившееся состояние.
Для надежного получения кадровых сведений в типовых конфигурациях разработчиками платформы предусмотрен общий модуль КадровыйУчет. Проанализируем, как получить аналогичный результат с помощью встроенных механизмов:
// Формируем список требуемых полей через запятую
ПоляКадров = "Сотрудник,ФизическоеЛицо,ТабельныйНомер,Подразделение,Должность,КоличествоСтавок,ВидЗанятости,Состояние";
// Вызываем типовую функцию получения данных
ТаблицаДанных = КадровыйУчет.КадровыеДанныеСотрудников(
Истина,
ПараметрСотрудник,
ПоляКадров,
КонецПериода(НачалоПериода, "ДЕНЬ")
);
Для Каждого СтрокаДанных Из ТаблицаДанных Цикл
// Доступны все запрошенные поля, включая состояние и кадровые реквизиты
ТекущееСостояние = СтрокаДанных.Состояние;
ТекущаяДолжность = СтрокаДанных.Должность;
КонецЦикла;
Если же данные требуются внутри сложного пользовательского запроса, процедура КадровыйУчет.СоздатьТаблицуКадровыхДанных позволяет сформировать и наполнить временную таблицу кадровых данных прямо в переданном объекте МенеджерВременныхТаблиц, исключая необходимость вручную разбирать интервалы и состояния в текстах запросов.