Как в 1С:ЗУП 3.1 сопоставить предыдущее и текущее кадровое состояние сотрудника для вывода истории переводов?

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

При разработке кадровых отчетов и печатных форм в «1С:Зарплата и управление персоналом 3.1» часто встает задача отобразить цепочку перемещений сотрудника в виде парных записей: откуда и куда был совершен перевод (старое подразделение/должность и новое подразделение/должность на соответствующую дату). Если исходные данные получены списком событий (прием, перевод, перемещение), их необходимо сгруппировать по парам «было — стало».

Разберем типичные ошибки при обработке такой таблицы значений во встроенном языке, а также рассмотрим оптимальные варианты решения как алгоритмическим путем, так и с помощью запросов и возможностей СКД.

Причина ошибки «Индекс находится за границами массива» и неэффективность вложенных циклов

Посмотрим на первую попытку решения задачи через обход таблицы значений по индексам:


Для каждого Стр Из ТЗ Цикл
    ВсегоСтр = ТЗ.Количество();
    Для ии = 0 По ВсегоСтр - 1 Цикл
        ОбластьСтрока.Параметры.Сотрудник = Стр.Сотрудник;
        ОбластьСтрока.Параметры.старМРаб = ТЗ[ии].Подразделение;
        ОбластьСтрока.Параметры.старОплата = ТЗ[ии].ДатаПеревода;
        ОбластьСтрока.Параметры.новМРаб = ТЗ[ии + 1].Подразделение;
        ОбластьСтрока.Параметры.новОплата = ТЗ[ии + 1].ДатаПеревода;
        ТабличныйДокумент.Вывести(ОбластьСтрока);
    КонецЦикла;
КонецЦикла;

Проанализируем две критические проблемы в этом фрагменте:

  1. Выход за границы коллекции. Цикл выполняется до ВсегоСтр - 1 включительно. На последней итерации переменная ии равна последнему валидному индексу строки. Выражение ТЗ[ии + 1] пытается обратиться к следующему элементу, которого не существует, что вызывает исключение Индекс находится за границами массива. Чтобы обращаться к элементу со смещением + 1, граница цикла должна быть уменьшена: По ВсегоСтр - 2.
  2. Бессмысленный внешний цикл. Внешний цикл Для каждого Стр Из ТЗ дублирует выполнение внутреннего цикла ровно столько раз, сколько строк содержится в таблице, приводя к повторному выводу одних и тех же данных.

Попытка исправить ситуацию через двойной цикл Для каждого Стр1 ... Для каждого Стр2 с ручным сравнением сотрудников решает проблему вывода, но приводит к квадратичной сложности алгоритма O(N²), создавая значительную нагрузку при большом объеме данных.

Способ 1. Оптимальный линейный обход таблицы значений (O(N))

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

ТЗ.Сортировать("Сотрудник Возр, ДатаПеревода Возр");

После упорядочивания мы можем использовать обход по индексам до предпоследней строки включительно (ТЗ.Количество() - 2). На каждой итерации сравниваем сотрудника в текущей строке со следующим:


ВсегоСтрок = ТЗ.Количество();
НомерПП = 0;

Для Индекс = 0 По ВсегоСтрок - 2 Цикл
    СтрокаСтарая = ТЗ[Индекс];
    СтрокаНовая = ТЗ[Индекс + 1];
    
    // Проверяем, что обе строки принадлежат одному и тому же сотруднику
    Если СтрокаСтарая.Сотрудник = СтрокаНовая.Сотрудник Тогда
        НомерПП = НомерПП + 1;
        
        ОбластьСтрока.Параметры.НомерПП = НомерПП;
        ОбластьСтрока.Параметры.Сотрудник = СтрокаСтарая.Сотрудник;
        
        // Предыдущее состояние
        ОбластьСтрока.Параметры.старМРаб = СтрокаСтарая.Подразделение;
        ОбластьСтрока.Параметры.старДолжность = СтрокаСтарая.Должность;
        ОбластьСтрока.Параметры.старДата = СтрокаСтарая.ДатаПеревода;
        
        // Новое состояние
        ОбластьСтрока.Параметры.новМРаб = СтрокаНовая.Подразделение;
        ОбластьСтрока.Параметры.новДолжность = СтрокаНовая.Должность;
        ОбластьСтрока.Параметры.новДата = СтрокаНовая.ДатаПеревода;
        
        ТабличныйДокумент.Вывести(ОбластьСтрока);
    КонецЕсли;
КонецЦикла;

Разберем также альтернативный и очень удобный вариант через сохранение предыдущей строки в переменной. Этот подход исключает ручную работу с индексами массивов:


ПредыдущаяСтрока = Неопределено;
НомерПП = 0;

Для Каждого ТекСтрока Из ТЗ Цикл
    Если ПредыдущаяСтрока <> Неопределено И ПредыдущаяСтрока.Сотрудник = ТекСтрока.Сотрудник Тогда
        НомерПП = НомерПП + 1;
        
        ОбластьСтрока.Параметры.НомерПП = НомерПП;
        ОбластьСтрока.Параметры.Сотрудник = ТекСтрока.Сотрудник;
        
        ОбластьСтрока.Параметры.старМРаб = ПредыдущаяСтрока.Подразделение;
        ОбластьСтрока.Параметры.старДата = ПредыдущаяСтрока.ДатаПеревода;
        
        ОбластьСтрока.Параметры.новМРаб = ТекСтрока.Подразделение;
        ОбластьСтрока.Параметры.новДата = ТекСтрока.ДатаПеревода;
        
        ТабличныйДокумент.Вывести(ОбластьСтрока);
    КонецЕсли;
    
    ПредыдущаяСтрока = ТекСтрока;
КонецЦикла;

Способ 2. Формирование пар «было — стало» на уровне запроса

Выполнять выборку напрямую из документов Документ.КадровыйПеревод методически некорректно: перемещения сотрудников в 1С:ЗУП 3.1 могут вводиться документами «Прием на работу», «Кадровый перевод списком», «Перемещение в другое подразделение» и другими. Достоверную хронологию хранит периодический регистр сведений КадроваяИсторияСотрудников.

В языке запросов «1С:Предприятия» отсутствуют оконные функции (такие как LEAD или LAG). Чтобы получить дату следующего события в запросе, выполним левое самосоединение временной таблицы с поиском минимальной последующей даты:


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

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Т1.Сотрудник КАК Сотрудник,
    Т1.ДатаСобытия КАК СтараяДата,
    МИНИМУМ(Т2.ДатаСобытия) КАК НоваяДата
ПОМЕСТИТЬ ВТ_СвязиСобытий
ИЗ
    ВТ_История КАК Т1
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_История КАК Т2
        ПО Т1.Сотрудник = Т2.Сотрудник
            И Т1.ДатаСобытия < Т2.ДатаСобытия
СГРУППИРОВАТЬ ПО
    Т1.Сотрудник,
    Т1.ДатаСобытия
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    Связи.Сотрудник КАК Сотрудник,
    Связи.СтараяДата КАК СтараяДата,
    ИсторияСтарая.Подразделение КАК СтароеПодразделение,
    ИсторияСтарая.Должность КАК СтараяДолжность,
    Связи.НоваяДата КАК НоваяДата,
    ИсторияНовая.Подразделение КАК НовоеПодразделение,
    ИсторияНовая.Должность КАК НоваяДолжность
ИЗ
    ВТ_СвязиСобытий КАК Связи
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_История КАК ИсторияСтарая
        ПО Связи.Сотрудник = ИсторияСтарая.Сотрудник
            И Связи.СтараяДата = ИсторияСтарая.ДатаСобытия
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_История КАК ИсторияНовая
        ПО Связи.Сотрудник = ИсторияНовая.Сотрудник
            И Связи.НоваяДата = ИсторияНовая.ДатаСобытия
УПОРЯДОЧИТЬ ПО
    Сотрудник,
    СтараяДата

Важная особенность ЗУП 3.1: в конфигурации существует регистр сведений КадроваяИсторияСотрудниковИнтервальный. В нем записи уже разбиты по интервалам с полями ДатаНачала и ДатаОкончания, что позволяет получить срез истории без сложных самосоединений.

Способ 3. Использование языка выражений СКД

Если отчет строится с использованием Системы компоновки данных, писать сложные алгоритмы в коде или запросе не требуется. В СКД предусмотрена функция языка выражений ВычислитьВыражение(), способная обращаться к соседним строкам в пределах группировки.

Посмотрим, как настроить вычисляемые поля в СКД:

  1. Создаем вычисляемое поле НовоеПодразделение с выражением:
    ВычислитьВыражение("Подразделение", , , "Следующая", "Следующая")
  2. Создаем вычисляемое поле НоваяДата с выражением:
    ВычислитьВыражение("ДатаПеревода", , , "Следующая", "Следующая")
  3. В настройках группировки отчета указываем группировку по полю Сотрудник, а внутри — детальные записи. В отборе детальных записей добавляем условие: НовоеПодразделение Заполнено, чтобы исключить последнюю строку сотрудника, у которой нет последующего перевода.

Механизм СКД автоматически изолирует контекст смещения внутри группировки текущего сотрудника, что полностью исключает попадание данных одного человека к другому.

← На главную