Как заполнить документ данными из другого документа по кнопке "Заполнить" в 1С?

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

В процессе разработки в среде 1С:Предприятие 8.3 одной из самых частых задач является заполнение реквизитов и табличной части одного документа данными из другого. Это может быть как простое копирование данных (аналог ввода на основании), так и сложная выборка по определенным правилам (например, подбор остатков, FIFO или распределение оплат). При реализации этой задачи начинающие разработчики часто сталкиваются с непониманием клиент-серверного взаимодействия и контекста управляемых форм.

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

Выясняем причину ошибки «Переменная не определена» при обращении к табличной части

Рассмотрим типичную ситуацию. Разработчик создает команду «Заполнить» на форме документа, привязывает её к кнопке и пишет внутри обработчика код добавления строки в табличную часть Заказы:


НоваяСтрока = Заказы.Добавить();
НоваяСтрока.Товар = ВыбранныйТовар;

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

  1. В модуле формы мы работаем не с самим документом (типа ДокументОбъект), а с его отображением — реквизитом формы, который обычно называется Объект и имеет тип ДанныеФормыСтруктура. Напрямую обратиться к табличной части по её имени в модуле формы нельзя. Обращение должно идти строго через контекст основного реквизита: Объект.Заказы.Добавить().
  2. В модуле объекта документ представлен непосредственно типом ДокументОбъект. Здесь табличные части являются свойствами самого объекта, поэтому конструкция Заказы.Добавить() (или ЭтотОбъект.Заказы.Добавить()) работает абсолютно корректно и без ошибок.

Таким образом, если вы пишете код непосредственно в модуле формы, вам всегда нужно обращаться к реквизитам и табличным частям через префикс Объект.

Правильная архитектура работы кнопки «Заполнить»

Событие нажатия кнопки всегда начинается на клиенте (в директиве &НаКлиенте). Однако для получения данных из других документов нам обязательно потребуется обращение к базе данных, выполнение запросов или чтение объектов, что возможно только на сервере (в директиве &НаСервере или в серверных модулях).

Существует два основных подхода к реализации этой логики. Разберем каждый из них.

Вариант 1. Заполнение непосредственно в модуле формы (для простых случаев)

Если алгоритм заполнения относительно простой и не требует сложной бизнес-логики, которую планируется вызывать из других мест конфигурации (например, из групповых обработок без открытия формы), мы можем реализовать всё в модуле формы.

Для этого мы создаем клиентскую процедуру-обработчик команды, которая вызывает серверную процедуру. На сервере мы получаем данные другого документа и заполняем реквизиты текущей формы через объект Объект. Рассмотрим пример:


&НаКлиенте
Процедура Заполнить(Команда)
    // Предположим, у нас на форме есть реквизит "ДокументОснование"
    Если НЕ ЗначениеЗаполнено(Объект.ДокументОснование) Тогда
        ПоказатьПредупреждение(, "Не выбран документ-источник для заполнения!");
        Возврат;
    КонецЕсли;
    
    ЗаполнитьНаСервере();
КонецПроцедуры

&НаСервере
Процедура ЗаполнитьНаСервере()
    // Получаем объект документа-источника из базы данных
    ИсточникОбъект = Объект.ДокументОснование.ПолучитьОбъект();
    
    // 1. Очищаем текущую табличную часть на форме
    Объект.Заказы.Очистить();
    
    // 2. Копируем реквизиты шапки (если необходимо)
    Объект.Склад = ИсточникОбъект.Склад;
    Объект.Контрагент = ИсточникОбъект.Контрагент;
    
    // 3. Заполняем табличную часть в цикле
    Для Каждого СтрокаИсточника Из ИсточникОбъект.Товары Цикл
        НоваяСтрока = Объект.Заказы.Добавить();
        ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточника);
    КонецЦикла;
КонецПроцедуры

В данном примере функция ЗаполнитьЗначенияСвойств автоматически сопоставляет колонки табличной части источника и приемника по их именам и копирует данные (например, Товар, Количество, Цена, Сумма), что избавляет нас от ручного написания каждой строчки присвоения.

Вариант 2. Вызов метода объекта через конвертацию данных формы (рекомендуемый подход)

По правилам хорошего тона и стандартам разработки 1С, вся логика, связанная с изменением данных самого объекта, должна находиться в его модуле объекта. Это позволяет использовать этот же алгоритм заполнения при программном создании документов без открытия интерактивных форм (например, при обмене данными или выполнении регламентных заданий).

Для реализации этого подхода мы используем специальные функции платформы РеквизитФормыВЗначение и ЗначениеВРеквизитФормы. Они позволяют временно превратить данные формы в полноценный прикладной объект в памяти сервера, выполнить с ним любые манипуляции (включая вызов его методов) и вернуть измененный результат обратно на форму.

Шаг 1. В модуле объекта заполняемого документа пишем экспортную процедуру заполнения:


// Процедура размещается в Модуле Объекта документа
Процедура ЗаполнитьДокументПоИсточнику(ДокументИсточник) Экспорт
    
    Если ТипЗнч(ДокументИсточник) <> Тип("ДокументСсылка.ПодтверждениеОплаты") Тогда
        Возврат;
    КонецЕсли;
    
    // Заполняем реквизиты шапки напрямую
    ЭтотОбъект.Склад = ДокументИсточник.Склад;
    ЭтотОбъект.Клиент = ДокументИсточник.Клиент;
    
    // Очищаем и заполняем табличную часть
    ЭтотОбъект.Заказы.Очистить();
    
    // Загружаем данные из таблицы источника в таблицу приемника
    Для Каждого СтрокаИсточника Из ДокументИсточник.Товары Цикл
        НоваяСтрока = ЭтотОбъект.Заказы.Добавить();
        ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаИсточника);
    КонецЦикла;
    
КонецПроцедуры

Шаг 2. В модуле формы пишем вызов этого метода через трансформацию контекста:


&НаКлиенте
Процедура Заполнить(Команда)
    Если НЕ ЗначениеЗаполнено(Объект.ДокументОснование) Тогда
        Предупреждение("Пожалуйста, выберите документ-основание!");
        Возврат;
    КонецЕсли;
    
    ЗаполнитьНаСервере();
КонецПроцедуры

&НаСервере
Процедура ЗаполнитьНаСервере()
    // 1. Конвертируем данные формы в ДокументОбъект
    ДокОбъект = РеквизитФормыВЗначение("Объект");
    
    // 2. Вызываем экспортный метод, написанный в модуле объекта
    ДокОбъект.ЗаполнитьДокументПоИсточнику(Объект.ДокументОснование);
    
    // 3. Возвращаем измененный объект обратно в форму, чтобы пользователь увидел результат
    ЗначениеВРеквизитФормы(ДокОбъект, "Объект");
КонецПроцедуры

Этот способ является наиболее гибким, масштабируемым и безопасным с точки зрения архитектуры 1С.

Решение сложной задачи заполнения по методу FIFO с учетом остатков и оплат

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

Для реализации такой бизнес-логики простого копирования реквизитов из одного документа недостаточно. Нам потребуется написать алгоритм распределения в модуле объекта. Разберем этот процесс по шагам.

Шаг 1. Напишем запрос, который получает информацию по заказам и остаткам товаров на складах. Нам понадобятся:

Шаг 2. Напишем алгоритм распределения в модуле объекта. Создадим экспортную процедуру ЗаполнитьПоЗаказамИОстаткам:


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

Этот пример наглядно иллюстрирует, как совместить выборку данных, сортировку FIFO на уровне СУБД (через УПОРЯДОЧИТЬ ПО ДатаЗаказа ВОЗР) и динамическое распределение складских остатков в оперативной памяти с помощью встроенного языка 1С.

Резюме

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

  1. Никогда не пытайтесь обращаться к табличным частям по их имени напрямую в модуле формы — используйте префикс Объект (например, Объект.ИмяТабличнойЧасти).
  2. Для простых сценариев заполнения (копирование реквизитов) используйте метод ЗаполнитьЗначенияСвойств в серверной процедуре формы.
  3. Для реализации бизнес-логики и сложных алгоритмов пишите экспортные методы в модуле объекта и вызывайте их из формы с помощью связки РеквизитФормыВЗначение / ЗначениеВРеквизитФормы.
  4. Для решения задач распределения (FIFO, LIFO, резервирование) используйте комбинацию упорядоченных запросов к регистрам остатков и последующую обработку во временных таблицах значений в оперативной памяти.
← На главную