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