Как заполнить табличную часть документа данными из другого документа в 1С?

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

При разработке в 1С часто возникает задача заполнить данные в табличной части одного документа на основании информации из другого. Например, при создании документа "Посещения" необходимо для каждого сотрудника автоматически подставить плановое количество рабочих часов из утвержденного "Штатного расписания". Рассмотрим, как решить эту задачу и какие типичные ошибки могут возникнуть на этом пути.

Проанализируем ситуацию: у нас есть документ "Посещения" с табличной частью "Сотрудники", в которой есть колонки "Сотрудник" и "Часов". Также есть документ "Штатное расписание", где для каждого сотрудника на определенный месяц указано плановое количество часов. Наша цель — при открытии документа "Посещения" автоматически заполнить колонку "Часов" для всех сотрудников в табличной части.

Ошибка 1: Передача всей табличной части в параметр запроса

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


// Неправильный код
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ПериодРегистрации", НачалоМесяца(Объект.Дата));
// Ошибка здесь: в параметр передается вся табличная часть
Запрос.УстановитьПараметр("Сотрудник", Объект.Сотрудники); 

Запрос.Текст = 
    "ВЫБРАТЬ
    |    ШтатноеРасписаниеУпрСотрудники.ВсегоЧасов,
    |    ШтатноеРасписаниеУпрСотрудники.НомерСтроки
    |ИЗ
    |    Документ.ШтатноеРасписаниеУпр.Сотрудники КАК ШтатноеРасписаниеУпрСотрудники
    |ГДЕ
    |    ШтатноеРасписаниеУпрСотрудники.Ссылка.ПериодРегистрации = &ПериодРегистрации
    |    И ШтатноеРасписаниеУпрСотрудники.Сотрудник = &Сотрудник";

РезультатЗапроса = Запрос.Выполнить().Выгрузить();

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

"Неверные параметры в операции сравнения. Нельзя сравнивать поля неограниченной длины и поля несовместимых типов. И ШтатноеРасписаниеУпрСотрудники.Сотрудник >= &Сотрудник"

Причина ошибки: В параметр запроса &Сотрудник мы пытаемся передать объект Объект.Сотрудники, который является целой табличной частью. А в тексте запроса система пытается сравнить поле ШтатноеРасписаниеУпрСотрудники.Сотрудник (которое является ссылкой на одного сотрудника) с этой табличной частью. СУБД не умеет сравнивать одно значение со целой коллекцией таким образом. Это фундаментально неверный подход.

Ошибка 2: Использование "ТекущиеДанные" при открытии формы

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


// Код, который будет работать только при клике на строку, но не при открытии
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ПериодРегистрации", НачалоМесяца(Объект.Дата));
// Пытаемся взять сотрудника из текущей строки формы
Запрос.УстановитьПараметр("Сотрудник", Элементы.Сотрудники.ТекущиеДанные.Сотрудник); 
...

При попытке выполнить этот код в обработчике ПриОткрытии или ПриСозданииНаСервере мы получим ошибку:

"Значение не является значением объектного типа (Сотрудник)"

Причина ошибки: В момент открытия формы еще нет "текущей строки". Пользователь еще не кликнул ни на одну из строк табличного поля, поэтому Элементы.Сотрудники.ТекущиеДанные имеет значение Неопределено. Обращение к свойству Сотрудник у Неопределено и вызывает ошибку. Этот механизм предназначен для интерактивной работы пользователя, а не для программного заполнения данных при открытии.

Правильное решение: Обход табличной части в цикле

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

Разберем по шагам:

  1. Создаем и настраиваем объект Запрос. Текст запроса готовим заранее, но параметр &Сотрудник пока не устанавливаем.
  2. Запускаем цикл Для каждого стр Из Объект.Сотрудники Цикл. В этом цикле переменная стр на каждой итерации будет представлять одну строку табличной части.
  3. Внутри цикла: устанавливаем параметр &Сотрудник, беря его из текущей строки: Запрос.УстановитьПараметр("Сотрудник", стр.Сотрудник);.
  4. Выполняем запрос и выгружаем результат.
  5. Проверяем, что запрос вернул какие-либо данные (Если ЗначениеЗаполнено(ТЗЗапроса) Тогда...).
  6. Присваиваем полученное значение в колонку Часов текущей строки: стр.Часов = ТЗЗапроса[0].Часов;.

Посмотрим на итоговый работающий код. Этот код следует размещать в серверной процедуре, например, в обработчике ПриСозданииНаСервере.


&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)

    // 1. Создаем объект Запрос
    Запрос = Новый Запрос;
    Запрос.УстановитьПараметр("ПериодРегистрации", НачалоМесяца(Объект.Дата));

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

КонецПроцедуры

Рекомендации и оптимизация

Представленный выше код является рабочим, но его можно и нужно оптимизировать, особенно если в табличной части может быть много строк (сотни или тысячи).

Проблема: Выполнение запроса внутри цикла приводит к большому количеству обращений к серверу базы данных. Если в документе 100 сотрудников, будет выполнено 100 отдельных запросов, что неэффективно.

Оптимизированное решение: Выполнить один запрос для всех сотрудников.

  1. Собрать всех сотрудников из табличной части документа в массив.
  2. Выполнить один запрос, используя в условии оператор В: ГДЕ ШтатноеРасписаниеУпрСотрудники.Сотрудник В (&СписокСотрудников).
  3. Выгрузить результат в таблицу значений или соответствие.
  4. Обойти табличную часть документа еще раз и, используя данные из результата запроса, заполнить колонку "Часов".

Контекст выполнения: Помните, что запросы к базе данных должны выполняться на сервере. Весь код, связанный с обработкой данных через запросы, следует помещать в процедуры с директивой &НаСервере или &НаСервереБезКонтекста. Выполнение запросов на клиенте (&НаКлиенте) является плохой практикой, так как создает лишнюю нагрузку на сеть и замедляет работу системы.

← На главную