Рассмотрим популярную практическую задачу, которая часто встречается в качестве тестового задания для программистов-стажеров: необходимо выгрузить любой документ из базы 1С в текстовый файл, а затем загрузить его обратно, создав точную копию. При этом условие строго запрещает использовать стандартные механизмы сериализации, такие как XDTO, ЗначениеВФайл или УникальныеИдентификаторы.
Разберем по шагам, как правильно работать с метаданными объектов, динамически обращаться к реквизитам табличных частей, корректно форматировать данные и избегать распространенных ошибок при парсинге текста.
Проанализируем ситуацию, с которой столкнулся разработчик. При попытке перебрать строки табличной части система выдает ошибку: Итератор для значения не определен. Посмотрим на исходный фрагмент кода:
МетаданныеДокументаТЧ = ВыбранныйДокумент.Метаданные().ТабличныеЧасти;
Для каждого ТабличнаяЧасть Из МетаданныеДокументаТЧ Цикл
Для каждого СтрокаТабличнойЧасти Из ТабличнаяЧасть Цикл // Ошибка возникает здесь
// ...
КонецЦикла;
КонецЦикла;
Выясним причину ошибки. В цикле переменная ТабличнаяЧасть содержит не саму коллекцию строк документа, а объект метаданных (описание структуры таблицы в конфигураторе). У объекта метаданных нет строк с данными, поэтому цикл Для каждого к нему применить невозможно. Чтобы получить физические данные строк, необходимо обратиться к самому документу, передав ему имя табличной части.
Рассмотрим подробнее, как правильно обойти и шапку документа, и его табличные части. Для получения значения реквизита по его строковому имени в 1С используются квадратные скобки: ДокументОбъект[ИмяРеквизита].
Исправленный алгоритм обхода выглядит так:
ВыбранныйДокумент = Объект.Документ.ПолучитьОбъект();
МетаданныеДок = ВыбранныйДокумент.Метаданные();
// 1. Обход реквизитов шапки
Для Каждого РеквизитМД из МетаданныеДок.Реквизиты Цикл
ЗначениеРеквизита = ВыбранныйДокумент[РеквизитМД.Имя];
// Логика записи значения...
КонецЦикла;
// 2. Обход табличных частей
Для каждого ТЧ_МД Из МетаданныеДок.ТабличныеЧасти Цикл
// Получаем реальную коллекцию строк табличной части документа
КоллекцияСтрокТЧ = ВыбранныйДокумент[ТЧ_МД.Имя];
Для каждого СтрокаТЧ Из КоллекцияСтрокТЧ Цикл
// Обходим реквизиты внутри конкретной табличной части
Для каждого РеквизитТЧ_МД Из ТЧ_МД.Реквизиты Цикл
ЗначениеВСтроке = СтрокаТЧ[РеквизитТЧ_МД.Имя];
// Логика записи значения строки...
КонецЦикла;
КонецЦикла;
КонецЦикла;
Разберем скрытый подвох, который часто ломает загрузку данных. Если мы просто прибавим значение к строке (например, "" + ЗначениеРеквизита), платформа 1С преобразует его с учетом локальных настроек пользователя.
Например, число 15000 превратится в строку "15 000" (с неразрывным пробелом), а дата примет вид "12.05.2024 14:00:00". При обратной загрузке система может не распознать такой формат. Чтобы этого избежать, применим функцию Формат():
Функция ПолучитьСтроковоеПредставление(Значение)
ТипЗначения = ТипЗнч(Значение);
Если ТипЗначения = Тип("Число") Тогда
Возврат Формат(Значение, "ЧГ=0"); // Убираем группировку разрядов
ИначеЕсли ТипЗначения = Тип("Дата") Тогда
Возврат Формат(Значение, "ДФ=yyyyMMddHHmmss"); // Универсальный формат без точек
ИначеЕсли ТипЗначения = Тип("Булево") Тогда
Возврат Формат(Значение, "БЛ=Ложь; БИ=Истина");
Иначе
Возврат Строка(Значение);
КонецЕсли;
КонецФункции
Посмотрим на самую сложную часть задания. Документ содержит ссылки на справочники (например, "Контрагенты" или "Номенклатура"). Выгружать всю структуру справочника рекурсивно — избыточно и выходит за рамки подобных задач. Нам достаточно выгрузить ключевые поля, по которым мы сможем найти объект при загрузке (обычно это Код или Наименование).
Проанализируем, как определить, что перед нами ссылка:
Если Справочники.ТипВсеСсылки().СодержитТип(ТипЗнч(ЗначениеРеквизита)) Тогда
// Это справочник. Выгружаем его вид и ключевое поле.
ИмяСправочника = ЗначениеРеквизита.Метаданные().Имя;
КлючПоиска = ЗначениеРеквизита.Код; // Или Наименование
СтрокаДляЗаписи = "СсылкаСправочник" + Символы.Таб + ИмяСправочника + Символы.Таб + КлючПоиска;
КонецЕсли;
Рассмотрим процесс формирования самого текстового файла. В исходном примере использовался символ двоеточия ":" как разделитель ключа и значения. Это плохая практика: если в текстовом комментарии документа пользователь напишет двоеточие, алгоритм загрузки сломается.
Также вместо загрузки всего документа в память через объект ТекстовыйДокумент, рекомендуем использовать потоковые объекты ЗаписьТекста и ЧтениеТекста. В качестве разделителя лучше использовать табуляцию Символы.Таб.
Запись = Новый ЗаписьТекста(ИмяФайла, КодировкаТекста.UTF8);
// Обязательно выгружаем вид документа, чтобы знать, что создавать при загрузке
Запись.ЗаписатьСтроку("ВидДокумента" + Символы.Таб + МетаданныеДок.Имя);
// Пример записи реквизита
Запись.ЗаписатьСтроку("Шапка" + Символы.Таб + РеквизитМД.Имя + Символы.Таб + ПолучитьСтроковоеПредставление(Значение));
Запись.Закрыть();
Теперь разберем алгоритм загрузки. Нам нужно прочитать файл построчно, определить вид документа, создать его и заполнить реквизиты. Для динамического создания документа по его строковому имени используется конструкция Документы[ИмяТипаДокумента].СоздатьДокумент().
Чтение = Новый ЧтениеТекста(ИмяФайла, КодировкаТекста.UTF8);
СтрокаТекста = Чтение.ПрочитатьСтроку();
НовыйДок = Неопределено;
Пока СтрокаТекста <> Неопределено Цикл
МассивДанных = СтрРазделить(СтрокаТекста, Символы.Таб, Ложь);
ТипЗаписи = МассивДанных[0];
Если ТипЗаписи = "ВидДокумента" Тогда
ИмяДокумента = МассивДанных[1];
НовыйДок = Документы[ИмяДокумента].СоздатьДокумент();
НовыйДок.Дата = ТекущаяДата();
ИначеЕсли ТипЗаписи = "Шапка" И НовыйДок <> Неопределено Тогда
ИмяРеквизита = МассивДанных[1];
СтроковоеЗначение = МассивДанных[2];
// Здесь потребуется обратное преобразование типов
// Для упрощения примера показана прямая запись
НовыйДок[ИмяРеквизита] = СтроковоеЗначение;
ИначеЕсли ТипЗаписи = "СсылкаСправочник" И НовыйДок <> Неопределено Тогда
ИмяСправочника = МассивДанных[1];
КлючПоиска = МассивДанных[2];
// Ищем ссылку в базе приемнике
НайденнаяСсылка = Справочники[ИмяСправочника].НайтиПоКоду(КлючПоиска);
НовыйДок[ИмяРеквизита] = НайденнаяСсылка;
КонецЕсли;
СтрокаТекста = Чтение.ПрочитатьСтроку();
КонецЦикла;
Если НовыйДок <> Неопределено Тогда
НовыйДок.Записать(РежимЗаписиДокумента.Запись);
КонецЕсли;
Чтение.Закрыть();
Подведем итоги: для успешного выполнения подобного задания мы рассмотрели динамическое обращение к коллекциям через квадратные скобки. Мы выяснили, что перебирать нужно данные объекта, а не сами метаданные. Использование универсального форматирования дат и надежных разделителей (табуляции) делает парсинг текстового файла устойчивым к ошибкам, а потоковое чтение экономит оперативную память.