При ведении кадрового учета на платформе 1С часто возникает потребность структурировать справочник Сотрудники (или СотрудникиОрганизаций), разложив элементы по иерархическим папкам в соответствии с подразделениями компании. Если в базе числятся сотни или тысячи сотрудников, ручное перетаскивание карточек занимает огромное количество времени. Попытки написать простую обработку переноса без учета архитектуры платформы нередко приводят к ошибкам: запросам внутри цикла, неверному изменению реквизитов и зависанию базы данных.
В этой статье мы подробно разберем, почему физическая иерархия по подразделениям в типовых конфигурациях не всегда оправдана, рассмотрим типовые инструменты платформы без программирования, а также напишем надежный и быстрый программный алгоритм массового переноса сотрудников в целевые группы.
Прежде чем приступать к программному переносу элементов, проанализируем, как устроен учет сотрудников в типовых конфигурациях на обычных формах (таких как 1С:ЗУП 2.5, 1С:УПП 1.3 или 1С:Комплексная автоматизация 1.1).
В этих решениях подразделение сотрудника не является жестким реквизитом самого справочника СотрудникиОрганизаций. Актуальное место работы хранится в периодическом регистре сведений РаботникиОрганизаций. Разделение справочника на физические папки отделов несет в себе следующие риски:
КадровоеПеремещение запись в регистре обновится автоматически, но в справочнике сотрудник останется лежать в старой папке. Возникнет расхождение данных, требующее повторного ручного или регламентного переноса.Родитель у элементов.Если бизнес-задача все же требует жесткого распределения элементов по группам, разберем типовые и программные способы решения.
Для разового перемещения сотен элементов не обязательно писать код с нуля. В составе типовых конфигураций и на диске ИТС присутствуют готовые универсальные инструменты:
Сервис -> Прочие обработки. Позволяет выбрать справочник СотрудникиОрганизаций, настроить отбор по актуальному подразделению (или списку сотрудников) и массово установить новое значение для реквизита Родитель.РаботникиОрганизаций и применить групповое действие переноса в группу для отобранных ссылок.Посмотрим на пример типичного некорректного кода, часто встречающегося при первых попытках автоматизировать перенос:
Функция ВыборОтдела(ВыбСотрудник)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| РаботникиОрганизаций.Сотрудник,
| РаботникиОрганизаций.ПодразделениеОрганизации
|ИЗ
| РегистрСведений.РаботникиОрганизаций КАК РаботникиОрганизаций
|ГДЕ
| РаботникиОрганизаций.ПодразделениеОрганизации = &Отдел";
Запрос.УстановитьПараметр("Отдел", Отдел);
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
Если СокрЛП(Выборка.Сотрудник) = СокрЛП(ВыбСотрудник) Тогда
Возврат Выборка.ПодразделениеОрганизации;
КонецЕсли;
КонецЦикла;
КонецФункции
Процедура ПереносВГруппу(Данные)
Спр = Справочники.Сотрудники.Выбрать();
ГруппаОтдела = Справочники.Сотрудники.НайтиПоНаименованию(Отдел);
Пока Спр.Следующий() Цикл
ИмяОтдела = ВыборОтдела(Спр.Наименование);
Если ЗначениеЗаполнено(ИмяОтдела) И ЗначениеЗаполнено(Спр.Наименование) Тогда
Объект = ГруппаОтдела.ПолучитьОбъект();
Объект.Родитель = ЭтотОбъект.Отдел;
Объект.Записать();
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Выясним основные причины неработоспособности такого подхода:
РегистрСведений.РаботникиОрганизаций возвращает всю историю изменений. Если сотрудник менял отделы несколько раз, алгоритм вернет некорректные или дублирующиеся данные. Необходимо обращаться к виртуальной таблице СрезПоследних.СокрЛП(Выборка.Сотрудник) = СокрЛП(ВыбСотрудник) крайне ненадежно, так как на предприятии могут работать полные однофамильцы. Связывать данные в 1С всегда следует по уникальным ссылкам (Ссылка).Объект = ГруппаОтдела.ПолучитьОбъект() код пытается изменить реквизиты самой папки подразделения, вместо того чтобы открыть на запись объект сотрудника (Спр.ПолучитьОбъект()) и назначить ему нового родителя.Решим задачу эффективно. Построим алгоритм по следующим правилам:
Рассмотрим пример готовой процедуры для внешней обработки:
Процедура ВыполнитьПереносСотрудниковПоОтделам() Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Сотрудники.Ссылка КАК СотрудникСсылка,
| Сотрудники.Родитель КАК ТекущийРодитель,
| КадрыСрез.ПодразделениеОрганизации КАК Подразделение,
| КадрыСрез.ПодразделениеОрганизации.Наименование КАК ИмяПодразделения
|ИЗ
| Справочник.СотрудникиОрганизаций КАК Сотрудники
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.РаботникиОрганизаций.СрезПоследних(&Период, ) КАК КадрыСрез
| ПО Сотрудники.Ссылка = КадрыСрез.Сотрудник
|ГДЕ
| НЕ Сотрудники.ЭтоГруппа
| И НЕ Сотрудники.ПометкаУдаления
| И КадрыСрез.ПодразделениеОрганизации <> ЗНАЧЕНИЕ(Справочник.ПодразделенияОрганизаций.ПустаяСсылка)";
Запрос.УстановитьПараметр("Период", ТекущаяДата());
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
Сообщить("Данные для переноса не найдены.");
Возврат;
КонецЕсли;
Выборка = РезультатЗапроса.Выбрать();
// Соответствие для кэширования ссылок на группы, чтобы не искать их в базе на каждой итерации
КэшГрупп = Новый Соответствие;
ВсегоОбработано = 0;
ВсегоПеренесено = 0;
Пока Выборка.Следующий() Цикл
ВсегоОбработано = ВсегоОбработано + 1;
ИмяГруппы = СокрЛП(Выборка.ИмяПодразделения);
Если ПустаяСтрока(ИмяГруппы) Тогда
Продолжить;
КонецЕсли;
// Получаем ссылку на группу из кэша или ищем/создаем в справочнике
ЦелеваяГруппа = КэшГрупп[ИмяГруппы];
Если ЦелеваяГруппа = Неопределено Тогда
ЦелеваяГруппа = Справочники.СотрудникиОрганизаций.НайтиПоНаименованию(ИмяГруппы, Истина);
// Если группа подразделения еще не создана в справочнике сотрудников - создаем ее
Если ЦелеваяГруппа.Пустая() Тогда
НоваяГруппа = Справочники.СотрудникиОрганизаций.СоздатьГруппу();
НоваяГруппа.Наименование = ИмяГруппы;
НоваяГруппа.Записать();
ЦелеваяГруппа = НоваяГруппа.Ссылка;
КонецЕсли;
КэшГрупп.Вставить(ИмяГруппы, ЦелеваяГруппа);
КонецЕсли;
// Если сотрудник уже находится в нужной группе, пропускаем запись
Если Выборка.ТекущийРодитель = ЦелеваяГруппа Тогда
Продолжить;
КонецЕсли;
// Переносим сотрудника в целевую группу
Попытка
СотрудникОбъект = Выборка.СотрудникСсылка.ПолучитьОбъект();
СотрудникОбъект.Родитель = ЦелеваяГруппа;
СотрудникОбъект.Записать();
ВсегоПеренесено = ВсегоПеренесено + 1;
Исключение
Сообщить("Ошибка при переносе сотрудника " + Выборка.СотрудникСсылка + ": " + ОписаниеОшибки(), СтатусСообщения.Важное);
КонецПопытки;
КонецЦикла;
Сообщить(СтрШаблон("Обработка завершена. Проверено сотрудников: %1, успешно перенесено: %2", ВсегоОбработано, ВсегоПеренесено));
КонецПроцедуры
Если в информационной базе числятся десятки тысяч сотрудников, выполнение одной долгой операции может вызвать таймауты блокировок у других пользователей. Для масштабных баз рекомендуется применять следующие доработки:
НачатьТранзакцию() и ЗафиксироватьТранзакцию().Объект.ОбменДанными.Загрузка = Истина перед вызовом метода Записать(), если конфигурация содержит тяжелые подписки на события при изменении карточки сотрудника, не влияющие на смену папки.Применение единого запроса с виртуальной таблицей СрезПоследних и кэшированием созданных групп обеспечивает максимальное быстродействие и гарантирует целостность кадровых данных в системе.