Как разбить пересекающиеся периоды работы и состояний сотрудников в запросе 1С

Программист 1С v8.3 (Обычные формы) 1С:Зарплата и Управление Персоналом Управленческий учет IT и автоматизация бизнеса
← На главную

В практике разработки на платформе 1С:Предприятие часто возникает задача учета отработанного времени или стажа, когда требуется сопоставить интервалы кадровой истории (приемы, перемещения, увольнения) с интервалами отклонений (отпуска, больничные, невыходы). Основная сложность заключается в том, что периоды состояний могут начинаться и заканчиваться внутри кадровых периодов, выходить за их границы или перекрывать сразу несколько кадровых документов. Разберем подробно, как правильно разбить такие периоды на элементарные интервалы с помощью языка запросов 1С.

Принцип решения задачи через формирование точек перелома

Проанализируем базовый алгоритм. Чтобы получить корректную шкалу непрерывных интервалов, необходимо объединить все граничные даты (даты начала и окончания работы, даты начала и окончания событий) в единый список, исключить дубликаты, а затем сформировать пары дат [ДатаНачала, ДатаОкончания]. Для каждого полученного подотрезка актуальные значения (должность, подразделение, состояние) определяются методом среза последних на дату начала отрезка.

Рассмотрим пошаговую реализацию этого алгоритма с использованием временных таблиц в пакетном запросе.

  1. Формирование кадровой истории сотрудника. Выбираем события изменения должностей, подразделений и состояния работы.
  2. Формирование таблицы состояний и отсутствий. Получаем периоды отпусков, больничных и иных невыходов.
  3. Сбор всех контрольных дат. Объединяем все начальные и конечные даты во временную таблицу.
  4. Получение непрерывных интервалов. Связываем таблицу дат саму с собой по условию неравенства и агрегатной функцией МИНИМУМ находим ближайшую следующую дату.
  5. Получение среза кадровых данных и состояний на каждый интервал.
  6. Фильтрация и расчет итоговых данных.

Пошаговое построение запроса

Разберем каждый шаг на практическом примере.

Шаг 1. Получение кадровой истории. Поместим во временную таблицу данные о приемах, перемещениях и увольнениях:


ВЫБРАТЬ
    РаботникиОрганизаций.Сотрудник КАК Сотрудник,
    РаботникиОрганизаций.Период КАК Период,
    РаботникиОрганизаций.ПодразделениеОрганизации КАК ПодразделениеОрганизации,
    РаботникиОрганизаций.Должность КАК Должность,
    РаботникиОрганизаций.ПричинаИзмененияСостояния КАК ПричинаИзмененияСостояния
ПОМЕСТИТЬ ВТКадроваяИстория
ИЗ
    РегистрСведений.РаботникиОрганизаций КАК РаботникиОрганизаций
ГДЕ
    РаботникиОрганизаций.Сотрудник = &Сотрудник

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    РаботникиОрганизаций.Сотрудник,
    РаботникиОрганизаций.ПериодЗавершения,
    РаботникиОрганизаций.ПодразделениеОрганизацииЗавершения,
    РаботникиОрганизаций.ДолжностьЗавершения,
    РаботникиОрганизаций.ПричинаИзмененияСостоянияЗавершения
ИЗ
    РегистрСведений.РаботникиОрганизаций КАК РаботникиОрганизаций
ГДЕ
    РаботникиОрганизаций.Сотрудник = &Сотрудник
    И РаботникиОрганизаций.ПериодЗавершения <> ДАТАВРЕМЯ(1, 1, 1)
;

Шаг 2. Получение периодов отклонений (состояний). Зафиксируем моменты начала и завершения отсутствий:


ВЫБРАТЬ
    СостояниеРаботниковОрганизаций.Сотрудник КАК Сотрудник,
    СостояниеРаботниковОрганизаций.Период КАК Период,
    СостояниеРаботниковОрганизаций.Состояние КАК Состояние
ПОМЕСТИТЬ ВТСостояния
ИЗ
    РегистрСведений.СостояниеРаботниковОрганизаций КАК СостояниеРаботниковОрганизаций
ГДЕ
    СостояниеРаботниковОрганизаций.Сотрудник = &Сотрудник

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    СостояниеРаботниковОрганизаций.Сотрудник,
    СостояниеРаботниковОрганизаций.ПериодЗавершения,
    ЗНАЧЕНИЕ(Перечисление.СостоянияРаботникаОрганизации.Работает)
ИЗ
    РегистрСведений.СостояниеРаботниковОрганизаций КАК СостояниеРаботниковОрганизаций
ГДЕ
    СостояниеРаботниковОрганизаций.Сотрудник = &Сотрудник
    И СостояниеРаботниковОрганизаций.ПериодЗавершения <> ДАТАВРЕМЯ(1, 1, 1)
;

Шаг 3. Объединение всех граничных точек во временную таблицу. Создадим единую шкалу дат, используя оператор ОБЪЕДИНИТЬ (без слова ВСЕ) для устранения одинаковых дат:


ВЫБРАТЬ
    ВТКадроваяИстория.Сотрудник КАК Сотрудник,
    ВТКадроваяИстория.Период КАК Период
ПОМЕСТИТЬ ВТПериоды
ИЗ
    ВТКадроваяИстория КАК ВТКадроваяИстория

ОБЪЕДИНИТЬ

ВЫБРАТЬ
    ВТСостояния.Сотрудник,
    ВТСостояния.Период
ИЗ
    ВТСостояния КАК ВТСостояния
;

Шаг 4. Определение интервалов кадровой истории. Сформируем пары дат с помощью левого соединения таблицы самой с собой и применения группировки:


ВЫБРАТЬ
    ВТПериоды_1.Сотрудник КАК Сотрудник,
    ВТПериоды_1.Период КАК ДатаНачала,
    МИНИМУМ(ВТПериоды_2.Период) КАК ДатаОкончания
ПОМЕСТИТЬ ВТИнтервалы
ИЗ
    ВТПериоды КАК ВТПериоды_1
        ЛЕВОЕ СОЕДИНЕНИЕ ВТПериоды КАК ВТПериоды_2
        ПО ВТПериоды_1.Сотрудник = ВТПериоды_2.Сотрудник
            И ВТПериоды_1.Период < ВТПериоды_2.Период
ГДЕ
    НЕ ВТПериоды_2.Период ЕСТЬ NULL
СГРУППИРОВАТЬ ПО
    ВТПериоды_1.Сотрудник,
    ВТПериоды_1.Период
;

Шаг 5. Получение актуальных срезов кадровых данных и состояний. Для каждого интервала находим максимальную дату события, предшествующую или равную дате начала отрезка:


ВЫБРАТЬ
    ВТИнтервалы.Сотрудник КАК Сотрудник,
    ВТИнтервалы.ДатаНачала КАК ДатаНачала,
    ВТИнтервалы.ДатаОкончания КАК ДатаОкончания,
    ВТКадроваяИстория.ПодразделениеОрганизации КАК ПодразделениеОрганизации,
    ВТКадроваяИстория.Должность КАК Должность,
    ВТКадроваяИстория.ПричинаИзмененияСостояния КАК ПричинаИзмененияСостояния,
    ВТСостояния.Состояние КАК Состояние
ПОМЕСТИТЬ ВТИтоговыеИнтервалы
ИЗ
    ВТИнтервалы КАК ВТИнтервалы
        // Получение актуальной должности и подразделения
        ЛЕВОЕ СОЕДИНЕНИЕ (
            ВЫБРАТЬ
                ВТИнтервалы_К.Сотрудник КАК Сотрудник,
                ВТИнтервалы_К.ДатаНачала КАК ДатаНачала,
                МАКСИМУМ(ВТКадроваяИстория_К.Период) КАК ПериодСобытия
            ИЗ
                ВТИнтервалы КАК ВТИнтервалы_К
                    ЛЕВОЕ СОЕДИНЕНИЕ ВТКадроваяИстория КАК ВТКадроваяИстория_К
                    ПО ВТИнтервалы_К.Сотрудник = ВТКадроваяИстория_К.Сотрудник
                        И ВТИнтервалы_К.ДатаНачала >= ВТКадроваяИстория_К.Период
            СГРУППИРОВАТЬ ПО
                ВТИнтервалы_К.Сотрудник,
                ВТИнтервалы_К.ДатаНачала
        ) КАК СрезКадры
        ПО ВТИнтервалы.Сотрудник = СрезКадры.Сотрудник
            И ВТИнтервалы.ДатаНачала = СрезКадры.ДатаНачала
        ЛЕВОЕ СОЕДИНЕНИЕ ВТКадроваяИстория КАК ВТКадроваяИстория
        ПО СрезКадры.Сотрудник = ВТКадроваяИстория.Сотрудник
            И СрезКадры.ПериодСобытия = ВТКадроваяИстория.Период

        // Получение актуального состояния
        ЛЕВОЕ СОЕДИНЕНИЕ (
            ВЫБРАТЬ
                ВТИнтервалы_С.Сотрудник КАК Сотрудник,
                ВТИнтервалы_С.ДатаНачала КАК ДатаНачала,
                МАКСИМУМ(ВТСостояния_С.Период) КАК ПериодСобытия
            ИЗ
                ВТИнтервалы КАК ВТИнтервалы_С
                    ЛЕВОЕ СОЕДИНЕНИЕ ВТСостояния КАК ВТСостояния_С
                    ПО ВТИнтервалы_С.Сотрудник = ВТСостояния_С.Сотрудник
                        И ВТИнтервалы_С.ДатаНачала >= ВТСостояния_С.Период
            СГРУППИРОВАТЬ ПО
                ВТИнтервалы_С.Сотрудник,
                ВТИнтервалы_С.ДатаНачала
        ) КАК СрезСостояния
        ПО ВТИнтервалы.Сотрудник = СрезСостояния.Сотрудник
            И ВТИнтервалы.ДатаНачала = СрезСостояния.ДатаНачала
        ЛЕВОЕ СОЕДИНЕНИЕ ВТСостояния КАК ВТСостояния
        ПО СрезСостояния.Сотрудник = ВТСостояния.Сотрудник
            И СрезСостояния.ПериодСобытия = ВТСостояния.Период
;

Альтернативный подход: Разворачивание через календарные дни

Если конечной целью является расчет количества фактически отработанных дней сотрудником на каждой должности без невыходов, разбивать периоды на непрерывные микроотрезки не обязательно. Более простым и надежным решением является разворачивание интервалов по дням с помощью регистра сведений ГрафикиРаботыПоВидамВремени или производственного календаря:

  1. Выбираем все рабочие даты интересующего периода из производственного календаря или графика сотрудника.
  2. Для каждого дня определяем срез последних по кадровым данным и регистру состояний.
  3. Накладываем фильтр: ПричинаИзмененияСостояния <> ЗНАЧЕНИЕ(Перечисление.ПричиныИзмененияСостояния.Увольнение) и Состояние = ЗНАЧЕНИЕ(Перечисление.СостоянияРаботникаОрганизации.Работает) (или Состояние ЕСТЬ NULL).
  4. Агрегируем результат через функцию КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Календарь.ДатаКалендаря) с группировкой по сотруднику, должности и подразделению.

Этот метод исключает сложные вычисления пересечений границ интервалов и гарантирует точный учет рабочих дней даже при множественных наложениях отклонений.

Оптимизация производительности и особенности платформы

При использовании конструкции с самосоединением интервалов следует учитывать важные моменты:

1. Индексация временных таблиц: Соединение по условию Период1 < Период2 выполняется с квадратичной сложностью. При обработке большого массива сотрудников обязательно индексируйте временные таблицы по полям связи:


ПОМЕСТИТЬ ВТПериоды
// ...
ИНДЕКСИРОВАТЬ ПО
    Сотрудник,
    Период
;

2. Использование оконных функций (в платформе 1С 8.3.20 и выше): Если функциональность платформы поддерживает аналитические функции, определение даты окончания интервала выполняется в один проход без тяжелых самосоединений:


ВЫБРАТЬ
    ВТПериоды.Сотрудник КАК Сотрудник,
    ВТПериоды.Период КАК ДатаНачала,
    LEAD(ВТПериоды.Период, 1) OVER (
        ПОДРАЗДЕЛЕНИЕ ПО ВТПериоды.Сотрудник
        УПОРЯДОЧИТЬ ПО ВТПериоды.Период
    ) КАК ДатаОкончания
ИЗ
    ВТПериоды КАК ВТПериоды

3. Открытые интервалы: Если период работы или состояние сотрудника не имеет даты окончания (активное состояние), значение пустой даты в запросе необходимо заменять на фиктивную максимальную границу (например, через ВЫБОР КОГДА ПериодЗавершения = ДАТАВРЕМЯ(1, 1, 1) ТОГДА &КонецПериодаОтчета ИНАЧЕ ПериодЗавершения КОНЕЦ), иначе такие интервалы будут отсечены при фильтрациях.

← На главную