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