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