Разработка механизмов обмена данными — одна из наиболее частых задач в мире 1С. Обычно для этих целей программисты используют готовые форматы передачи данных, такие как XML или JSON, используя встроенные инструменты платформы (например, сериализацию XDTO или специализированные функции работы с JSON). Однако на практике, особенно при прохождении собеседований или выполнении сложных учебных и тестовых заданий, накладывается жесткое ограничение: выгрузить данные нужно исключительно в простой текстовый файл (.txt) без привлечения XML, JSON или стандартных функций платформы вроде ЗначениеВСтрокуВнутр().
В этой статье мы подробно разберем, как спроектировать и написать универсальный алгоритм, способный выгрузить абсолютно любой документ из базы данных 1С в обычный TXT-файл, а затем по этим данным воссоздать точно такой же документ в другой (или этой же) информационной базе.
Прежде чем приступать к написанию кода, необходимо определить правила разметки нашего TXT-файла. Поскольку стандартных тегов у нас нет, мы должны создать собственную структуру разметки. Популярным и наиболее надежным решением является построение текстового файла с использованием специальных секций и уникальных разделителей строк.
Рассмотрим структуру файла, которая позволит однозначно восстановить документ:
Документ.ПоступлениеТоваровУслуг).В качестве разделителя значений внутри строки удобно использовать символ, который гарантированно не встретится в текстах реквизитов, например, вертикальную черту | или тильду ~.
Для того чтобы наш алгоритм подходил для любого документа, мы категорически не можем обращаться к реквизитам напрямую (например, писать Выборка.Организация). Вместо этого мы будем использовать объектную модель метаданных конфигурации 1С. Объект метаданных позволяет динамически узнать, какие реквизиты и табличные части есть у данного документа.
Рассмотрим, как получить список всех реквизитов документа и обойти их в цикле:
// Функция преобразует значение реквизита в строку, пригодную для сохранения в TXT
Функция ПолучитьСтроковоеПредставлениеЗначения(Значение)
ТипЗначения = ТипЗнч(Значение);
// Обработка простых типов данных
Если ТипЗначения = Тип("Число") Или ТипЗначения = Тип("Строка") Или ТипЗначения = Тип("Булево") Тогда
Возврат "Простой|" + Строка(Значение);
ИначеЕсли ТипЗначения = Тип("Дата") Тогда
Возврат "Дата|" + Формат(Значение, "ДФ=yyyyMMddHHmmss");
// Обработка перечислений
ИначеЕсли Перечисления.ТипВсего().СодержитТип(ТипЗначения) Тогда
ИмяПеречисления = Метаданные.НайтиПоТипу(ТипЗначения).Имя;
ИмяЗначения = Строка(Значение);
Возврат "Перечисление|" + ИмяПеречисления + "|" + ИмяЗначения;
// Обработка ссылочных типов (Справочники)
ИначеЕсли Справочники.ТипВсего().СодержитТип(ТипЗначения) Тогда
Если Значение.Пустая() Тогда
Возврат "Справочник|" + Метаданные.НайтиПоТипу(ТипЗначения).Имя + "|ПустаяСсылка";
КонецЕсли;
// В качестве уникального идентификатора используем GUID ссылки
ИмяСправочника = Метаданные.НайтиПоТипу(ТипЗначения).Имя;
УИД = Строка(Значение.УникальныйИдентификатор());
Возврат "Справочник|" + ИмяСправочника + "|" + УИД;
КонецЕсли;
Возврат "НеподдерживаемыйТип|";
КонецФункции
Используя эту вспомогательную функцию, мы можем написать универсальный код для выгрузки шапки документа:
Процедура ВыгрузитьДокументВТекст(СсылкаНаДокумент, ПутьКФайлу)
ФайлЗаписи = Новый ЗаписьТекста(ПутьКФайлу, КодировкаТекста.UTF8);
МетаданныеДокумента = СсылкаНаДокумент.Метаданные();
// 1. Записываем тип документа
ФайлЗаписи.ЗаписатьСтроку("[ТипДокумента]");
ФайлЗаписи.ЗаписатьСтроку(МетаданныеДокумента.Имя);
// 2. Выгружаем стандартные реквизиты (Дата, Номер, Проведен, ПометкаУдаления)
ФайлЗаписи.ЗаписатьСтроку("[СтандартныеРеквизиты]");
ФайлЗаписи.ЗаписатьСтроку("Номер|" + СсылкаНаДокумент.Номер);
ФайлЗаписи.ЗаписатьСтроку("Дата|" + Формат(СсылкаНаДокумент.Дата, "ДФ=yyyyMMddHHmmss"));
ФайлЗаписи.ЗаписатьСтроку("Проведен|" + Строка(СсылкаНаДокумент.Проведен));
// 3. Выгружаем пользовательские реквизиты шапки
ФайлЗаписи.ЗаписатьСтроку("[РеквизитыШапки]");
Для Каждого Реквизит Из МетаданныеДокумента.Реквизиты Цикл
ИмяРеквизита = Реквизит.Имя;
ЗначениеРеквизита = СсылкаНаДокумент[ИмяРеквизита];
СтрокаЗначения = ПолучитьСтроковоеПредставлениеЗначения(ЗначениеРеквизита);
ФайлЗаписи.ЗаписатьСтроку(ИмяРеквизита + "=" + СтрокаЗначения);
КонецЦикла;
// 4. Выгружаем табличные части документа
Для Каждого ТабличнаяЧасть Из МетаданныеДокумента.ТабличныеЧасти Цикл
ИмяТЧ = ТабличнаяЧасть.Имя;
ФайлЗаписи.ЗаписатьСтроку("[ТабличнаяЧасть." + ИмяТЧ + "]");
// Получаем данные ТЧ в виде таблицы значений
ТаблицаДанных = СсылкаНаДокумент[ИмяТЧ].Выгрузить();
// Записываем заголовок колонок
СтрокаКолонок = "";
Для Каждого Колонка Из ТабличнаяЧасть.Реквизиты Цикл
СтрокаКолонок = СтрокаКолонок + Колонка.Имя + ";";
КонецЦикла;
ФайлЗаписи.ЗаписатьСтроку(СтрокаКолонок);
// Записываем строки табличной части
Для Каждого СтрокаТЧ Из ТаблицаДанных Цикл
СтрокаДанных = "";
Для Каждого Колонка Из ТабличнаяЧасть.Реквизиты Цикл
ЗначениеЯчейки = СтрокаТЧ[Колонка.Имя];
СтрокаДанных = СтрокаДанных + ПолучитьСтроковоеПредставлениеЗначения(ЗначениеЯчейки) + ";";
КонецЦикла;
ФайлЗаписи.ЗаписатьСтроку(СтрокаДанных);
КонецЦикла;
КонецЦикла;
ФайлЗаписи.Закрыть();
Сообщить("Выгрузка успешно завершена!");
КонецПроцедуры
Проанализировав данный код, можно увидеть, что программа обходит метаданные реквизитов шапки и табличных частей абсолютно любого документа, преобразует значения в специальный строковый формат и записывает их в текстовый файл.
Для загрузки документа нам потребуется решить обратную задачу: прочитать текстовый файл, распознать маркеры типов и преобразовать строковые данные обратно в объекты платформы 1С. Особое внимание следует уделить поиску ссылочных типов. Если в файле записан GUID справочника, нам нужно попытаться найти элемент по этому идентификатору в базе-приемнике.
Создадим функцию восстановления значения из нашей строковой записи:
Функция ВосстановитьЗначениеИзСтроки(СтрокаДанных)
ЧастиСтроки = СтроковыеФункцииКлиентСервер.РазложитьСтрокуВМассивПодстрок(СтрокаДанных, "|");
Если ЧастиСтроки.Количество() = 0 Тогда
Возврат Неопределено;
КонецЕсли;
ТипДанных = ЧастиСтроки[0];
Если ТипДанных = "Простой" Тогда
ЗначениеТекст = ЧастиСтроки[1];
// Попытка преобразования в Булево
Если ВРег(ЗначениеТекст) = "ИСТИНА" Тогда
Возврат Истина;
ИначеЕсли ВРег(ЗначениеТекст) = "ЛОЖЬ" Тогда
Возврат Ложь;
КонецЕсли;
// Попытка преобразования в Число
Попытка
Возврат Число(ЗначениеТекст);
Исключение
Возврат ЗначениеТекст; // Остается строкой
КонецПопытки;
ИначеЕсли ТипДанных = "Дата" Тогда
// Парсим дату из формата yyyyMMddHHmmss
Попытка
Возврат Дата(Сред(ЧастиСтроки[1], 1, 4), Сред(ЧастиСтроки[1], 5, 2), Сред(ЧастиСтроки[1], 7, 2), Сред(ЧастиСтроки[1], 9, 2), Сред(ЧастиСтроки[1], 11, 2), Сред(ЧастиСтроки[1], 13, 2));
Исключение
Возврат Дата('00010101000000');
КонецПопытки;
ИначеЕсли ТипДанных = "Перечисление" Тогда
ИмяПеречисления = ЧастиСтроки[1];
ИмяЗначения = ЧастиСтроки[2];
Попытка
Возврат Перечисления[ИмяПеречисления][ИмяЗначения];
Исключение
Возврат Неопределено;
КонецПопытки;
ИначеЕсли ТипДанных = "Справочник" Тогда
ИмяСправочника = ЧастиСтроки[1];
УИДСтрока = ЧастиСтроки[2];
Если УИДСтрока = "ПустаяСсылка" Тогда
Возврат Справочники[ИмяСправочника].ПустаяСсылка();
КонецЕсли;
// Восстановление ссылки по Уникальному Идентификатору
СсылкаУИД = Новый УникальныйИдентификатор(УИДСтрока);
СсылкаНаЭлемент = Справочники[ИмяСправочника].ПолучитьСсылку(СсылкаУИД);
// Проверяем, существует ли элемент в базе данных
Если ОбщегоНазначения.СсылкаСуществует(СсылкаНаЭлемент) Тогда
Возврат СсылкаНаЭлемент;
Иначе
// Если элемент не найден по UUID, можно реализовать логику поиска по наименованию или коду
Возврат Справочники[ИмяСправочника].ПустаяСсылка();
EndIf;
КонецЕсли;
Возврат Неопределено;
КонецФункции
Теперь соберем все блоки воедино и напишем универсальный алгоритм загрузки. Мы будем использовать объект ЧтениеТекста, читать файл построчно, определять секции и наполнять реквизиты нового документа.
Процедура ЗагрузитьДокументИзТекста(ПутьКФайлу)
ФайлЧтения = Новый ЧтениеТекста(ПутьКФайлу, КодировкаТекста.UTF8);
СтрокаФайла = ФайлЧтения.ПрочитатьСтроку();
НовыйДокумент = Неопределено;
ТекущаяСекция = "";
ТекущаяТЧ = "";
МассивКолонокТЧ = Новый Массив;
Пока СтрокаФайла <> Неопределено Цикл
// Определение секций файла
Если Найти(СтрокаФайла, "[") = 1 Тогда
ТекущаяСекция = СтрЗаменить(СтрЗаменить(СтрокаФайла, "[", ""), "]", "");
СтрокаФайла = ФайлЧтения.ПрочитатьСтроку();
Продолжить;
КонецЕсли;
// Обработка секции типа документа и его создание
Если ТекущаяСекция = "ТипДокумента" Тогда
ИмяДокумента = СтрокаФайла;
НовыйДокумент = Документы[ИмяДокумента].СоздатьДокумент();
// Заполнение стандартных реквизитов
ИначеЕсли ТекущаяСекция = "СтандартныеРеквизиты" Тогда
ПараметрыРеквизита = СтроковыеФункцииКлиентСервер.РазложитьСтрокуВМассивПодстрок(СтрокаФайла, "|");
ИмяРеквизита = ПараметрыРеквизита[0];
ЗначениеРеквизитаТекст = ПараметрыРеквизита[1];
Если ИмяРеквизита = "Номер" Тогда
НовыйДокумент.Номер = ЗначениеРеквизитаТекст;
ИначеЕсли ИмяРеквизита = "Дата" Тогда
НовыйДокумент.Дата = Дата(Сред(ЗначениеРеквизитаТекст, 1, 4), Сред(ЗначениеРеквизитаТекст, 5, 2), Сред(ЗначениеРеквизитаТекст, 7, 2), Сред(ЗначениеРеквизитаТекст, 9, 2), Сред(ЗначениеРеквизитаТекст, 11, 2), Сред(ЗначениеРеквизитаТекст, 13, 2));
КонецЕсли;
// Заполнение произвольных реквизитов шапки
ИначеЕсли ТекущаяСекция = "РеквизитыШапки" Тогда
ПозицияРавно = Найти(СтрокаФайла, "=");
ИмяРеквизита = СокрЛП(Лев(СтрокаФайла, ПозицияРавно - 1));
ЗначениеРеквизитаСтрока = СокрЛП(Сред(СтрокаФайла, ПозицияРавно + 1));
НовыйДокумент[ИмяРеквизита] = ВосстановитьЗначениеИзСтроки(ЗначениеРеквизитаСтрока);
// Обработка табличной части
ИначеЕсли Найти(ТекущаяСекция, "ТабличнаяЧасть.") = 1 Тогда
ИмяТЧ = СтрЗаменить(ТекущаяСекция, "ТабличнаяЧасть.", "");
// Если сменилась табличная часть или она только началась
Если ТекущаяТЧ <> ИмяТЧ Тогда
ТекущаяТЧ = ИмяТЧ;
// Первая строка в секции ТЧ - это заголовок колонок
МассивКолонокТЧ = СтроковыеФункцииКлиентСервер.РазложитьСтрокуВМассивПодстрок(СтрокаФайла, ";");
// Удалим пустые элементы, если они возникли из-за финального разделителя ";"
Если МассивКолонокТЧ[МассивКолонокТЧ.Количество() - 1] = "" Тогда
МассивКолонокТЧ.Удалить(МассивКолонокТЧ.Количество() - 1);
КонецЕсли;
СтрокаФайла = ФайлЧтения.ПрочитатьСтроку();
Продолжить;
КонецЕсли;
// Добавляем новую строку в ТЧ
НоваяСтрока = НовыйДокумент[ТекущаяТЧ].Добавить();
ЗначенияЯчеек = СтроковыеФункцииКлиентСервер.РазложитьСтрокуВМассивПодстрок(СтрокаФайла, ";");
Для Индекс = 0 По МассивКолонокТЧ.Количество() - 1 Цикл
ИмяКолонки = МассивКолонокТЧ[Индекс];
ЗначениеКолонкиСтрока = ЗначенияЯчеек[Индекс];
НоваяСтрока[ИмяКолонки] = ВосстановитьЗначениеИзСтроки(ЗначениеКолонкиСтрока);
КонецЦикла;
КонецЕсли;
СтрокаФайла = ФайлЧтения.ПрочитатьСтроку();
КонецЦикла;
ФайлЧтения.Закрыть();
// Записываем воссозданный документ в базу данных
Попытка
НовыйДокумент.Записать(РежимЗаписиДокумента.Запись);
Сообщить("Документ успешно загружен и сохранен с номером: " + НовыйДокумент.Номер);
Исключение
Сообщить("Ошибка при записи документа: " + ОписаниеОшибки());
КонецПопытки;
КонецПроцедуры
Разрабатывая подобные универсальные алгоритмы, необходимо учитывать несколько критически важных моментов, с которыми вы обязательно столкнетесь при практическом использовании кода:
БитаяСсылка или пустая ссылка. Для решения этой проблемы в ТЗ реальных проектов обычно закладывают требование: «Если ссылка не найдена, создать новый элемент справочника с аналогичным кодом/наименованием». Для этого структуру выгрузки ссылочных типов расширяют и дополнительно записывают Наименование и Код.ХранилищеЗначения (например, файлы или картинки), выгрузить их напрямую через строку не получится. Понадобится дополнительно кодировать двоичные данные в формат Base64 и записывать полученную строку в текстовый файл."Справочник|Контрагенты|УИД". При чтении платформа безошибочно определит, к какой именно таблице адресовать запрос поиска.Таким образом, мы детально разобрали, как с помощью коллекции метаданных 1С обойти ограничения на использование JSON/XML и построить надежный, гибкий, а главное — полностью универсальный механизм сериализации объектов 1С в обычные текстовые файлы.