Почему не заполняется табличная часть документа при его создании и как это исправить?

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

Разработка в среде «1С:Предприятие 8» требует строгого соблюдения стандартов проектирования и понимания жизненного цикла объектов. Часто начинающие (и даже опытные) специалисты сталкиваются с ситуацией, когда при программном создании документа на основании другого объекта его табличная часть остается пустой, хотя в отладчике весь код заполнения отрабатывает корректно. Давайте подробно разберем, почему возникает эта проблема, почему популярные «обходные пути» опасны для стабильности системы, и как решить задачу правильно и красиво.

В чем суть проблемы и почему код автора не работал?

Проанализируем ситуацию, описанную автором темы. Разработчик создал процедуру заполнения табличной части Температура и попытался вызвать её в событии ОбработкаПроведения. При этом в процедуре заполнения выполняется сложный запрос, в который передается параметр Ссылка текущего документа. На выходе из процедуры строки добавляются в табличную часть объекта, но при открытии формы документа пользователь видит пустую таблицу.

Здесь кроются сразу две серьезные архитектурные ошибки:

  1. Использование события проведения для изменения самого документа. Обработчик ОбработкаПроведения предназначен исключительно для генерации движений по регистрам накопления, сведений или бухгалтерии. К моменту начала проведения документ уже физически записан в базу данных. Любые изменения реквизитов или табличных частей объекта ЭтотОбъект внутри проведения просто останутся в оперативной памяти и не будут сохранены в СУБД, если не вызвать принудительную перезапись (что делать категорически нельзя).
  2. Выполнение запроса по ссылке нового документа. В момент создания нового документа (в обработчиках типа ПередЗаписью) у объекта в памяти уже может быть сформирована уникальная ссылка (GUID). Однако в таблицах базы данных этой записи еще физически не существует. Запрос СУБД вида ГДЕ Ссылка = &Ссылка обращается к жесткому диску и логично возвращает 0 строк. Данные просто неоткуда прочитать, пока транзакция записи не будет завершена.

Рассмотрим, чем опасен предложенный на форуме «вредный совет» с вызовом метода Записать() внутри обработчиков записи или проведения. Если принудительно вызывать перезапись объекта, система инициирует повторный запуск всей цепочки событий (ПередЗаписью, ПриЗаписи и т.д.). Это приводит к бесконечной рекурсии, жестким блокировкам таблиц базы данных и резкому падению производительности системы при одновременной работе пользователей. Даже использование свойства ДополнительныеСвойства для обхода повторного вызова является лишь «костылем», усложняющим дальнейшую поддержку кода.

---

Вариант 1. Использование события ОбработкаЗаполнения (Рекомендуемый способ)

Если наш документ создается на основании другого документа, платформа предоставляет для этого штатный и наиболее правильный инструмент — обработчик события ОбработкаЗаполнения в модуле объекта. Этот метод вызывается в самый первый момент зарождения нового объекта в памяти компьютера, еще до того, как пользователю будет показана форма документа.

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

Посмотрим на пример правильной реализации в модуле объекта:


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

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

При таком подходе пользователь сразу же при открытии формы увидит полностью заполненную табличную часть. При этом база данных не нагружается лишними транзакциями записи, а код полностью соответствует стандартам разработки фирмы «1С».

---

Вариант 2. Передача реквизитов объекта в параметры запроса в ПередЗаписью

Бывают ситуации, когда заполнение табличной части должно происходить непосредственно перед записью документа и зависеть от реквизитов, которые пользователь заполнил в шапке вручную (например, выбранный склад, организация или контрагент). Так как документ еще не записан, мы по-прежнему не можем делать запрос по условию ГДЕ Ссылка = &Ссылка.

Выясним, как обойти это ограничение. Мы можем передавать значения реквизитов шапки в параметры запроса напрямую из оперативной памяти (из свойств ЭтотОбъект):


Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    
    // Проверяем, что это новый документ и его табличная часть еще не заполнялась
    Если ЭтоНовый() И Температура.Количество() = 0 Тогда
        
        Запрос = Новый Запрос;
        Запрос.Текст = 
            "ВЫБРАТЬ
            |	ПрогнозыТемператур.Значение КАК Значение
            |ИЗ
            |	РегистрСведений.ПрогнозыТемператур КАК ПрогнозыТемператур
            |ГДЕ
            |	ПрогнозыТемператур.Организация = &Организация
            |	И ПрогнозыТемператур.Склад = &Склад";
        
        // Передаем значения реквизитов напрямую из памяти объекта
        Запрос.УстановитьПараметр("Организация", ЭтотОбъект.Организация);
        Запрос.УстановитьПараметр("Склад", ЭтотОбъект.Склад);
        
        Результат = Запрос.Выполнить();
        Если Не Результат.Пустой() Тогда
            Выборка = Результат.Выбрать();
            Пока Выборка.Следующий() Цикл
                НоваяСтрока = Температура.Добавить();
                НоваяСтрока.Значение = Выборка.Значение;
            КонецЦикла;
        КонецЕсли;
        
    КонецЕсли;
    
КонецПроцедуры

---

Вариант 3. Передача табличной части в запрос через Временную Таблицу

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

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


Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    
    Если Температура.Количество() = 0 Тогда
        
        // 1. Выгружаем ТЧ "Товары" из памяти в обычную Таблицу Значений
        ТаблицаТоваров = Товары.Выгрузить();
        
        // 2. Инициализируем менеджер временных таблиц для запроса
        МенеджерВТ = Новый МенеджерВременныхТаблиц;
        Запрос = Новый Запрос;
        Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
        
        // Передаем ТЗ как параметр запроса
        Запрос.Текст = 
            "ВЫБРАТЬ
            |	ВТ_Товары.Номенклатура КАК Номенклатура,
            |	ВТ_Товары.Количество КАК Количество
            |ПОМЕСТИТЬ ВТ_ТоварыИзПамяти
            |ИЗ
            |	&ТаблицаТоваров КАК ВТ_Товары
            |;
            |
            |////////////////////////////////////////////////////////////////////////////////
            |ВЫБРАТЬ
            |	ВТ_ТоварыИзПамяти.Номенклатура КАК Номенклатура,
            |	УсловияХранения.ТемпературныйРежим КАК ТемпературныйРежим
            |ИЗ
            |	ВТ_ТоварыИзПамяти КАК ВТ_ТоварыИзПамяти
            |	ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.УсловияХранения КАК УсловияХранения
            |		ПО ВТ_ТоварыИзПамяти.Номенклатура = УсловияХранения.Номенклатура";
            
        Запрос.УстановитьПараметр("ТаблицаТоваров", ТаблицаТоваров);
        
        РезультатЗапроса = Запрос.Выполнить();
        Если Не РезультатЗапроса.Пустой() Тогда
            Выборка = РезультатЗапроса.Выбрать();
            Пока Выборка.Следующий() Цикл
                НоваяСтрока = Температура.Добавить();
                НоваяСтрока.Номенклатура = Выборка.Номенклатура;
                НоваяСтрока.Режим = Выборка.ТемпературныйРежим;
            КонецЦикла;
        КонецЕсли;
        
    КонецЕсли;
    
КонецПроцедуры

Использование временных таблиц для данных из оперативной памяти — мощный инструмент разработчика 1С, который позволяет выполнять сложные соединения и расчеты на уровне СУБД без необходимости предварительной физической записи документа в базу.

---

Вариант 4. Метод УстановитьСсылкуНового() для особых случаев

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

Для решения этой задачи в платформу встроен механизм предварительного резервирования GUID. Сделать это можно с помощью метода УстановитьСсылкуНового():


Если ЭтотОбъект.ЭтоНовый() Тогда
    // Генерируем новый Уникальный Идентификатор и получаем пустую ссылку нужного типа
    НоваяСсылка = Документы.РеализацияТоваровУслуг.ПолучитьСсылку(Новый УникальныйИдентификатор);
    
    // Назначаем эту ссылку объекту. База данных применит её при физической записи
    ЭтотОбъект.УстановитьСсылкуНового(НоваяСсылка);
КонецЕсли;

// Теперь переменную "НоваяСсылка" можно передавать в другие процедуры и функции 
// для связывания подчиненных объектов в рамках одной транзакции.

Важное примечание: Этот метод решает проблему связывания объектов в памяти, но он не позволяет делать запросы к таблице этого документа в БД. Ссылка зарезервирована, но физическая запись в таблице СУБД появится только после успешного завершения транзакции записи объекта.

Подведем итоги

Для создания надежных и быстродействующих решений в 1С всегда следуйте золотым правилам архитектуры:

← На главную