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

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

Рассмотрим популярную практическую задачу, которая часто встречается в качестве тестового задания для программистов-стажеров: необходимо выгрузить любой документ из базы 1С в текстовый файл, а затем загрузить его обратно, создав точную копию. При этом условие строго запрещает использовать стандартные механизмы сериализации, такие как XDTO, ЗначениеВФайл или УникальныеИдентификаторы.

Разберем по шагам, как правильно работать с метаданными объектов, динамически обращаться к реквизитам табличных частей, корректно форматировать данные и избегать распространенных ошибок при парсинге текста.

Анализ проблемы: почему не работает цикл по табличной части?

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


МетаданныеДокументаТЧ = ВыбранныйДокумент.Метаданные().ТабличныеЧасти;
Для каждого ТабличнаяЧасть Из МетаданныеДокументаТЧ Цикл
    Для каждого СтрокаТабличнойЧасти Из ТабличнаяЧасть Цикл // Ошибка возникает здесь
        // ...
    КонецЦикла; 
КонецЦикла;

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

Шаг 1. Правильный динамический обход метаданных документа

Рассмотрим подробнее, как правильно обойти и шапку документа, и его табличные части. Для получения значения реквизита по его строковому имени в 1С используются квадратные скобки: ДокументОбъект[ИмяРеквизита].

Исправленный алгоритм обхода выглядит так:


ВыбранныйДокумент = Объект.Документ.ПолучитьОбъект();
МетаданныеДок = ВыбранныйДокумент.Метаданные();

// 1. Обход реквизитов шапки
Для Каждого РеквизитМД из МетаданныеДок.Реквизиты Цикл
    ЗначениеРеквизита = ВыбранныйДокумент[РеквизитМД.Имя];
    // Логика записи значения...
КонецЦикла;

// 2. Обход табличных частей
Для каждого ТЧ_МД Из МетаданныеДок.ТабличныеЧасти Цикл
    
    // Получаем реальную коллекцию строк табличной части документа
    КоллекцияСтрокТЧ = ВыбранныйДокумент[ТЧ_МД.Имя];
    
    Для каждого СтрокаТЧ Из КоллекцияСтрокТЧ Цикл
        
        // Обходим реквизиты внутри конкретной табличной части
        Для каждого РеквизитТЧ_МД Из ТЧ_МД.Реквизиты Цикл
            ЗначениеВСтроке = СтрокаТЧ[РеквизитТЧ_МД.Имя];
            // Логика записи значения строки...
        КонецЦикла;
        
    КонецЦикла; 
КонецЦикла;

Шаг 2. Форматирование примитивных типов (числа и даты)

Разберем скрытый подвох, который часто ломает загрузку данных. Если мы просто прибавим значение к строке (например, "" + ЗначениеРеквизита), платформа 1С преобразует его с учетом локальных настроек пользователя.

Например, число 15000 превратится в строку "15 000" (с неразрывным пробелом), а дата примет вид "12.05.2024 14:00:00". При обратной загрузке система может не распознать такой формат. Чтобы этого избежать, применим функцию Формат():


Функция ПолучитьСтроковоеПредставление(Значение)
    ТипЗначения = ТипЗнч(Значение);
    
    Если ТипЗначения = Тип("Число") Тогда
        Возврат Формат(Значение, "ЧГ=0"); // Убираем группировку разрядов
    ИначеЕсли ТипЗначения = Тип("Дата") Тогда
        Возврат Формат(Значение, "ДФ=yyyyMMddHHmmss"); // Универсальный формат без точек
    ИначеЕсли ТипЗначения = Тип("Булево") Тогда
        Возврат Формат(Значение, "БЛ=Ложь; БИ=Истина");
    Иначе
        Возврат Строка(Значение);
    КонецЕсли;
КонецФункции

Шаг 3. Обработка ссылочных типов (Справочники, Перечисления)

Посмотрим на самую сложную часть задания. Документ содержит ссылки на справочники (например, "Контрагенты" или "Номенклатура"). Выгружать всю структуру справочника рекурсивно — избыточно и выходит за рамки подобных задач. Нам достаточно выгрузить ключевые поля, по которым мы сможем найти объект при загрузке (обычно это Код или Наименование).

Проанализируем, как определить, что перед нами ссылка:


Если Справочники.ТипВсеСсылки().СодержитТип(ТипЗнч(ЗначениеРеквизита)) Тогда
    // Это справочник. Выгружаем его вид и ключевое поле.
    ИмяСправочника = ЗначениеРеквизита.Метаданные().Имя;
    КлючПоиска = ЗначениеРеквизита.Код; // Или Наименование
    СтрокаДляЗаписи = "СсылкаСправочник" + Символы.Таб + ИмяСправочника + Символы.Таб + КлючПоиска;
КонецЕсли;

Шаг 4. Использование потоковой записи и надежных разделителей

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

Также вместо загрузки всего документа в память через объект ТекстовыйДокумент, рекомендуем использовать потоковые объекты ЗаписьТекста и ЧтениеТекста. В качестве разделителя лучше использовать табуляцию Символы.Таб.


Запись = Новый ЗаписьТекста(ИмяФайла, КодировкаТекста.UTF8);

// Обязательно выгружаем вид документа, чтобы знать, что создавать при загрузке
Запись.ЗаписатьСтроку("ВидДокумента" + Символы.Таб + МетаданныеДок.Имя);

// Пример записи реквизита
Запись.ЗаписатьСтроку("Шапка" + Символы.Таб + РеквизитМД.Имя + Символы.Таб + ПолучитьСтроковоеПредставление(Значение));

Запись.Закрыть();

Шаг 5. Обратная загрузка и динамическое создание документа

Теперь разберем алгоритм загрузки. Нам нужно прочитать файл построчно, определить вид документа, создать его и заполнить реквизиты. Для динамического создания документа по его строковому имени используется конструкция Документы[ИмяТипаДокумента].СоздатьДокумент().


Чтение = Новый ЧтениеТекста(ИмяФайла, КодировкаТекста.UTF8);
СтрокаТекста = Чтение.ПрочитатьСтроку();

НовыйДок = Неопределено;

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

Если НовыйДок <> Неопределено Тогда
    НовыйДок.Записать(РежимЗаписиДокумента.Запись);
КонецЕсли;

Чтение.Закрыть();

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

← На главную