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