В практике разработки на платформе 1С:Предприятие часто возникает задача учета отработанного времени или стажа, когда требуется сопоставить интервалы кадровой истории (приемы, перемещения, увольнения) с интервалами отклонений (отпуска, больничные, невыходы). Основная сложность заключается в том, что периоды состояний могут начинаться и заканчиваться внутри кадровых периодов, выходить за их границы или перекрывать сразу несколько кадровых документов. Разберем подробно, как правильно разбить такие периоды на элементарные интервалы с помощью языка запросов 1С.
Проанализируем базовый алгоритм. Чтобы получить корректную шкалу непрерывных интервалов, необходимо объединить все граничные даты (даты начала и окончания работы, даты начала и окончания событий) в единый список, исключить дубликаты, а затем сформировать пары дат [ДатаНачала, ДатаОкончания]. Для каждого полученного подотрезка актуальные значения (должность, подразделение, состояние) определяются методом среза последних на дату начала отрезка.
Рассмотрим пошаговую реализацию этого алгоритма с использованием временных таблиц в пакетном запросе.
МИНИМУМ находим ближайшую следующую дату.Разберем каждый шаг на практическом примере.
Шаг 1. Получение кадровой истории. Поместим во временную таблицу данные о приемах, перемещениях и увольнениях:
ВЫБРАТЬ
РаботникиОрганизаций.Сотрудник КАК Сотрудник,
РаботникиОрганизаций.Период КАК Период,
РаботникиОрганизаций.ПодразделениеОрганизации КАК ПодразделениеОрганизации,
РаботникиОрганизаций.Должность КАК Должность,
РаботникиОрганизаций.ПричинаИзмененияСостояния КАК ПричинаИзмененияСостояния
ПОМЕСТИТЬ ВТКадроваяИстория
ИЗ
РегистрСведений.РаботникиОрганизаций КАК РаботникиОрганизаций
ГДЕ
РаботникиОрганизаций.Сотрудник = &Сотрудник
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
РаботникиОрганизаций.Сотрудник,
РаботникиОрганизаций.ПериодЗавершения,
РаботникиОрганизаций.ПодразделениеОрганизацииЗавершения,
РаботникиОрганизаций.ДолжностьЗавершения,
РаботникиОрганизаций.ПричинаИзмененияСостоянияЗавершения
ИЗ
РегистрСведений.РаботникиОрганизаций КАК РаботникиОрганизаций
ГДЕ
РаботникиОрганизаций.Сотрудник = &Сотрудник
И РаботникиОрганизаций.ПериодЗавершения <> ДАТАВРЕМЯ(1, 1, 1)
;
Шаг 2. Получение периодов отклонений (состояний). Зафиксируем моменты начала и завершения отсутствий:
ВЫБРАТЬ
СостояниеРаботниковОрганизаций.Сотрудник КАК Сотрудник,
СостояниеРаботниковОрганизаций.Период КАК Период,
СостояниеРаботниковОрганизаций.Состояние КАК Состояние
ПОМЕСТИТЬ ВТСостояния
ИЗ
РегистрСведений.СостояниеРаботниковОрганизаций КАК СостояниеРаботниковОрганизаций
ГДЕ
СостояниеРаботниковОрганизаций.Сотрудник = &Сотрудник
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
СостояниеРаботниковОрганизаций.Сотрудник,
СостояниеРаботниковОрганизаций.ПериодЗавершения,
ЗНАЧЕНИЕ(Перечисление.СостоянияРаботникаОрганизации.Работает)
ИЗ
РегистрСведений.СостояниеРаботниковОрганизаций КАК СостояниеРаботниковОрганизаций
ГДЕ
СостояниеРаботниковОрганизаций.Сотрудник = &Сотрудник
И СостояниеРаботниковОрганизаций.ПериодЗавершения <> ДАТАВРЕМЯ(1, 1, 1)
;
Шаг 3. Объединение всех граничных точек во временную таблицу. Создадим единую шкалу дат, используя оператор ОБЪЕДИНИТЬ (без слова ВСЕ) для устранения одинаковых дат:
ВЫБРАТЬ
ВТКадроваяИстория.Сотрудник КАК Сотрудник,
ВТКадроваяИстория.Период КАК Период
ПОМЕСТИТЬ ВТПериоды
ИЗ
ВТКадроваяИстория КАК ВТКадроваяИстория
ОБЪЕДИНИТЬ
ВЫБРАТЬ
ВТСостояния.Сотрудник,
ВТСостояния.Период
ИЗ
ВТСостояния КАК ВТСостояния
;
Шаг 4. Определение интервалов кадровой истории. Сформируем пары дат с помощью левого соединения таблицы самой с собой и применения группировки:
ВЫБРАТЬ
ВТПериоды_1.Сотрудник КАК Сотрудник,
ВТПериоды_1.Период КАК ДатаНачала,
МИНИМУМ(ВТПериоды_2.Период) КАК ДатаОкончания
ПОМЕСТИТЬ ВТИнтервалы
ИЗ
ВТПериоды КАК ВТПериоды_1
ЛЕВОЕ СОЕДИНЕНИЕ ВТПериоды КАК ВТПериоды_2
ПО ВТПериоды_1.Сотрудник = ВТПериоды_2.Сотрудник
И ВТПериоды_1.Период < ВТПериоды_2.Период
ГДЕ
НЕ ВТПериоды_2.Период ЕСТЬ NULL
СГРУППИРОВАТЬ ПО
ВТПериоды_1.Сотрудник,
ВТПериоды_1.Период
;
Шаг 5. Получение актуальных срезов кадровых данных и состояний. Для каждого интервала находим максимальную дату события, предшествующую или равную дате начала отрезка:
ВЫБРАТЬ
ВТИнтервалы.Сотрудник КАК Сотрудник,
ВТИнтервалы.ДатаНачала КАК ДатаНачала,
ВТИнтервалы.ДатаОкончания КАК ДатаОкончания,
ВТКадроваяИстория.ПодразделениеОрганизации КАК ПодразделениеОрганизации,
ВТКадроваяИстория.Должность КАК Должность,
ВТКадроваяИстория.ПричинаИзмененияСостояния КАК ПричинаИзмененияСостояния,
ВТСостояния.Состояние КАК Состояние
ПОМЕСТИТЬ ВТИтоговыеИнтервалы
ИЗ
ВТИнтервалы КАК ВТИнтервалы
// Получение актуальной должности и подразделения
ЛЕВОЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ
ВТИнтервалы_К.Сотрудник КАК Сотрудник,
ВТИнтервалы_К.ДатаНачала КАК ДатаНачала,
МАКСИМУМ(ВТКадроваяИстория_К.Период) КАК ПериодСобытия
ИЗ
ВТИнтервалы КАК ВТИнтервалы_К
ЛЕВОЕ СОЕДИНЕНИЕ ВТКадроваяИстория КАК ВТКадроваяИстория_К
ПО ВТИнтервалы_К.Сотрудник = ВТКадроваяИстория_К.Сотрудник
И ВТИнтервалы_К.ДатаНачала >= ВТКадроваяИстория_К.Период
СГРУППИРОВАТЬ ПО
ВТИнтервалы_К.Сотрудник,
ВТИнтервалы_К.ДатаНачала
) КАК СрезКадры
ПО ВТИнтервалы.Сотрудник = СрезКадры.Сотрудник
И ВТИнтервалы.ДатаНачала = СрезКадры.ДатаНачала
ЛЕВОЕ СОЕДИНЕНИЕ ВТКадроваяИстория КАК ВТКадроваяИстория
ПО СрезКадры.Сотрудник = ВТКадроваяИстория.Сотрудник
И СрезКадры.ПериодСобытия = ВТКадроваяИстория.Период
// Получение актуального состояния
ЛЕВОЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ
ВТИнтервалы_С.Сотрудник КАК Сотрудник,
ВТИнтервалы_С.ДатаНачала КАК ДатаНачала,
МАКСИМУМ(ВТСостояния_С.Период) КАК ПериодСобытия
ИЗ
ВТИнтервалы КАК ВТИнтервалы_С
ЛЕВОЕ СОЕДИНЕНИЕ ВТСостояния КАК ВТСостояния_С
ПО ВТИнтервалы_С.Сотрудник = ВТСостояния_С.Сотрудник
И ВТИнтервалы_С.ДатаНачала >= ВТСостояния_С.Период
СГРУППИРОВАТЬ ПО
ВТИнтервалы_С.Сотрудник,
ВТИнтервалы_С.ДатаНачала
) КАК СрезСостояния
ПО ВТИнтервалы.Сотрудник = СрезСостояния.Сотрудник
И ВТИнтервалы.ДатаНачала = СрезСостояния.ДатаНачала
ЛЕВОЕ СОЕДИНЕНИЕ ВТСостояния КАК ВТСостояния
ПО СрезСостояния.Сотрудник = ВТСостояния.Сотрудник
И СрезСостояния.ПериодСобытия = ВТСостояния.Период
;
Если конечной целью является расчет количества фактически отработанных дней сотрудником на каждой должности без невыходов, разбивать периоды на непрерывные микроотрезки не обязательно. Более простым и надежным решением является разворачивание интервалов по дням с помощью регистра сведений ГрафикиРаботыПоВидамВремени или производственного календаря:
ПричинаИзмененияСостояния <> ЗНАЧЕНИЕ(Перечисление.ПричиныИзмененияСостояния.Увольнение) и Состояние = ЗНАЧЕНИЕ(Перечисление.СостоянияРаботникаОрганизации.Работает) (или Состояние ЕСТЬ NULL).КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Календарь.ДатаКалендаря) с группировкой по сотруднику, должности и подразделению.Этот метод исключает сложные вычисления пересечений границ интервалов и гарантирует точный учет рабочих дней даже при множественных наложениях отклонений.
При использовании конструкции с самосоединением интервалов следует учитывать важные моменты:
1. Индексация временных таблиц: Соединение по условию Период1 < Период2 выполняется с квадратичной сложностью. При обработке большого массива сотрудников обязательно индексируйте временные таблицы по полям связи:
ПОМЕСТИТЬ ВТПериоды
// ...
ИНДЕКСИРОВАТЬ ПО
Сотрудник,
Период
;
2. Использование оконных функций (в платформе 1С 8.3.20 и выше): Если функциональность платформы поддерживает аналитические функции, определение даты окончания интервала выполняется в один проход без тяжелых самосоединений:
ВЫБРАТЬ
ВТПериоды.Сотрудник КАК Сотрудник,
ВТПериоды.Период КАК ДатаНачала,
LEAD(ВТПериоды.Период, 1) OVER (
ПОДРАЗДЕЛЕНИЕ ПО ВТПериоды.Сотрудник
УПОРЯДОЧИТЬ ПО ВТПериоды.Период
) КАК ДатаОкончания
ИЗ
ВТПериоды КАК ВТПериоды
3. Открытые интервалы: Если период работы или состояние сотрудника не имеет даты окончания (активное состояние), значение пустой даты в запросе необходимо заменять на фиктивную максимальную границу (например, через ВЫБОР КОГДА ПериодЗавершения = ДАТАВРЕМЯ(1, 1, 1) ТОГДА &КонецПериодаОтчета ИНАЧЕ ПериодЗавершения КОНЕЦ), иначе такие интервалы будут отсечены при фильтрациях.