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

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

При разработке обработок загрузки данных или интеграции отчетов с документами в «1С:Предприятие 8» программисты часто сталкиваются с задачей переноса информации из плоских или сводных таблиц во внутренние табличные части документов. Особую сложность вызывают случаи, когда структура колонок исходного отчета (например, разбивка по цветам, размерам или характеристикам) должна быть спроецирована на конкретные реквизиты табличной части документа (например, СебестБелый, СебестМатов и так далее).

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

Анализ исходной проблемы

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

  1. Массив колонок — содержит список имен колонок ТЗ, из которых нужно прочитать значения (например, числовые показатели себестоимости).
  2. Массив цветов — содержит строковые названия характеристик (например, "белый", "матов"), по которым определяется, в какой именно реквизит табличной части документа должна записаться себестоимость.

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

---

Способ 1. Поиск подстроки и сопоставление по именам колонок

Если структура табличной части документа уже жестко задана в конфигураторе (созданы реквизиты СебестБелый, СебестМатов и т.д.), мы можем применить обход колонок таблицы значений с анализом их содержимого. Рассмотрим пример, в котором программа анализирует имена колонок или их значения и на основании этого принимает решение о заполнении реквизитов.

Этот способ основан на использовании функции СтрНайти для поиска ключевых слов («белый», «матов») в именах колонок таблицы значений:


// Предположим, что ДокументОбъект уже создан или получен для редактирования
ТЧНов = ДокументОбъект.ТЧ.Добавить();
ТЧНов.Номенклатура = НайденнаяНоменклатура;

Для Каждого Колонка Из ТЗ.Колонки Цикл
    ИмяКолонки = Колонка.Имя;
    ЗначениеЯчейки = СтрокаТЗ[ИмяКолонки];
    
    // Если ячейка пустая, пропускаем ее
    Если Не ЗначениеЗаполнено(ЗначениеЯчейки) Тогда
        Продолжить;
    КонецЕсли;
    
    // Проверяем вхождение ключевых слов в имя колонки
    Если СтрНайти(ВРег(ИмяКолонки), "БЕЛЫЙ") > 0 Тогда
        ТЧНов.СебестБелый = ЗначениеЯчейки;
    ИначеЕсли СтрНайти(ВРег(ИмяКолонки), "МАТОВ") > 0 Тогда
        ТЧНов.СебестМатов = ЗначениеЯчейки;
    КонецЕсли;
КонецЦикла;

Преимущества: Простота реализации. Код понятен и легко поддается отладке.

Недостатки: Код остается жестким. Если в конфигурацию добавится новый цвет (например, «Глянцевый» или «Красный»), разработчику придется вручную дописывать новые блоки ИначеЕсли в программный код.

---

Способ 2. Динамическое обращение к реквизитам (Универсальный подход)

В платформе «1С:Предприятие 8» реализован мощный механизм обращения к свойствам объектов через квадратные скобки. Вместо написания громоздких конструкций Если ... ИначеЕсли мы можем собирать имя реквизита табличной части динамически прямо в цикле.

Давайте разберем по шагам, как это реализовать, используя массив соответствия цветов:


// Подготовим массив соответствия цветов реквизитам
// Ключ - это имя колонки в ТЗ, Значение - суффикс реквизита в ТЧ документа
СоответствиеЦветов = Новый Соответствие;
СоответствиеЦветов.Вставить("МДФ_Белый", "Белый");
СоответствиеЦветов.Вставить("МДФ_Матов", "Матов");

Для Каждого СтрТЗ Из ТЗ Цикл
    НоваяСтрокаТЧ = ДокументОбъект.ТЧ.Добавить();
    НоваяСтрокаТЧ.Номенклатура = ПолучитьНоменклатуру(СтрТЗ._2); // Ниже разберем, как делать это быстро
    
    Для Каждого КлючИЗначение Из СоответствиеЦветов Цикл
        ИмяКолонкиТЗ = КлючИЗначение.Ключ;
        СуффиксРеквизита = КлючИЗначение.Значение;
        
        // Формируем имя реквизита ТЧ динамически, например "СебестБелый"
        ИмяРеквизитаТЧ = "Себест" + СуффиксРеквизита;
        
        // Безопасное чтение значения из ТЗ
        ЗначениеПоказателя = СтрТЗ[ИмяКолонкиТЗ];
        
        Если ЗначениеЗаполнено(ЗначениеПоказателя) Тогда
            // Записываем данные в реквизит ТЧ через квадратные скобки
            НоваяСтрокаТЧ[ИмяРеквизитаТЧ] = Число(ЗначениеПоказателя);
        КонецЕсли;
    КонецЦикла;
КонецЦикла;

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


ИмяРеквизитаТЧ = "Себест" + СуффиксРеквизита;
МетаданныеТЧ = ДокументОбъект.Метаданные().ТабличныеЧасти.ТЧ;

Если МетаданныеТЧ.Реквизиты.Найти(ИмяРеквизитаТЧ) <> Неопределено Тогда
    НоваяСтрокаТЧ[ИмяРеквизитаТЧ] = Число(ЗначениеПоказателя);
Иначе
    ЗаписьЖурналаРегистрации("ЗагрузкаДанных", УровеньЖурналаРегистрации.Предупреждение, , , 
        "Не найден реквизит ТЧ: " + ИмяРеквизитаТЧ);
КонецЕсли;

---

Оптимизация производительности: Избавляемся от проблемы N+1

Посмотрим на код автора из первого сообщения темы:

НоменСсылка = Справочники.Номенклатура.НайтиПоРеквизиту("НаименованиеПолное", стр._2);

Вызов метода НайтиПоРеквизиту внутри цикла по строкам таблицы значений — это критическая ошибка, которая в разы снижает производительность системы. Если в таблице отчета будет 1000 строк, система выполнит 1000 отдельных запросов к базе данных. Это классическая проблема N+1.

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

Проанализируем оптимальный шаблон загрузки:


// Шаг 1. Собираем уникальные наименования из ТЗ
МассивНаименований = Новый Массив;
Для Каждого Стр Из ТЗ Цикл
    Если ЗначениеЗаполнено(Стр._2) Тогда
        МассивНаименований.Добавить(Стр._2);
    КонецЕсли;
КонецЦикла;

// Очищаем массив от дублей
ОбщегоНазначенияКлиентСервер.СвернутьМассив(МассивНаименований);

// Шаг 2. Получаем ссылки одним запросом
СоответствиеНоменклатуры = Новый Соответствие;

Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	Номенклатура.Ссылка КАК Ссылка,
    |	Номенклатура.НаименованиеПолное КАК НаименованиеПолное
    |ИЗ
    |	Справочник.Номенклатура КАК Номенклатура
    |ГДЕ
    |	Номенклатура.НаименованиеПолное В(&МассивНаименований)";

Запрос.УстановитьПараметр("МассивНаименований", МассивНаименований);
Выборка = Запрос.Выполнить().Выбрать();

Пока Выборка.Следующий() Цикл
    СоответствиеНоменклатуры.Вставить(Выборка.НаименованиеПолное, Выборка.Ссылка);
КонецЦикла;

// Шаг 3. Основной цикл заполнения документа с быстрым поиском по соответствию
Для Каждого СтрТЗ Из ТЗ Цикл
    
    // Получаем ссылку из соответствия в ОЗУ - это происходит мгновенно
    НоменСсылка = СоответствиеНоменклатуры[СтрТЗ._2];
    
    Если НоменСсылка = Неопределено Тогда
        // Обработка ситуации, когда номенклатура не найдена в базе
        Продолжить;
    КонецЕсли;
    
    ТЧНов = ДокументОбъект.ТЧ.Добавить();
    ТЧНов.Номенклатура = НоменСсылка;
    
    // Код заполнения колонок...
КонецЦикла;

---

Безопасная очистка данных в таблице значений

В ходе обсуждения автор упомянул, что перед обработкой он удаляет «лишние строки» из исходной ТЗ. При работе с таблицами значений важно помнить золотое правило программирования в 1С: никогда не удаляйте строки из коллекции внутри цикла Для каждого по этой же самой коллекции.

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

  1. Обход таблицы с конца по индексу:
    
    Для Индекс = -(ТЗ.Количество() - 1) По 0 Цикл
        ТекущийИндекс = -Индекс;
        СтрокаТЗ = ТЗ[ТекущийИндекс];
        Если СтрокаТЗ.Показатель = 0 Тогда
            ТЗ.Удалить(СтрокаТЗ);
        КонецЕсли;
    КонецЦикла;
    
  2. Использование метода НайтиСтроки(): Находим ненужные строки по отбору и удаляем их из таблицы в отдельном цикле.

---

Архитектурный совет: Переход к использованию характеристик

В сообщении №15 участники форума справедливо отмечают, что проектирование табличной части документа с колонками под каждый цвет (СебестБелый, СебестМатов, РентГлянц) — это неудачное архитектурное решение (антипаттерн).

Если бизнес-процессы компании предусматривают регулярное появление новых видов отделки, цветов или материалов, правильным подходом будет перепроектирование структуры метаданных:

Посмотрим, как упростится и станет универсальным код заполнения ТЧ документа при такой правильной структуре:


// Каждому цвету теперь соответствует отдельная строка в документе
Для Каждого СтрТЗ Из ТЗ Цикл
    
    НоменСсылка = СоответствиеНоменклатуры[СтрТЗ._2];
    Если НоменСсылка = Неопределено Тогда
        Продолжить;
    КонецЕсли;
    
    Для Каждого ИмяКолонки Из МассивКолонок Цикл
        ЗначениеСебестоимости = СтрТЗ[ИмяКолонки];
        
        Если ЗначениеЗаполнено(ЗначениеСебестоимости) Тогда
            НоваяСтрока = ДокументОбъект.ТЧ.Добавить();
            НоваяСтрока.Номенклатура = НоменСсылка;
            
            // Находим или получаем ссылку на элемент справочника Цвета/Характеристики
            НоваяСтрока.Цвет = ПолучитьСсылкаНаЦветПоИмениКолонки(ИмяКолонки);
            НоваяСтрока.Себестоимость = Число(ЗначениеСебестоимости);
        КонецЕсли;
    КонецЦикла;
    
КонецЦикла;

При таком подходе база данных будет работать оптимально, а появление нового цвета в отчетах поставщиков не потребует привлечения программиста для изменения конфигурации и написания нового кода.

← На главную