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

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

Разработка механизмов обмена данными — одна из наиболее частых задач в мире 1С. Обычно для этих целей программисты используют готовые форматы передачи данных, такие как XML или JSON, используя встроенные инструменты платформы (например, сериализацию XDTO или специализированные функции работы с JSON). Однако на практике, особенно при прохождении собеседований или выполнении сложных учебных и тестовых заданий, накладывается жесткое ограничение: выгрузить данные нужно исключительно в простой текстовый файл (.txt) без привлечения XML, JSON или стандартных функций платформы вроде ЗначениеВСтрокуВнутр().

В этой статье мы подробно разберем, как спроектировать и написать универсальный алгоритм, способный выгрузить абсолютно любой документ из базы данных 1С в обычный TXT-файл, а затем по этим данным воссоздать точно такой же документ в другой (или этой же) информационной базе.

Архитектура текстового файла: проектируем структуру хранения

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

Рассмотрим структуру файла, которая позволит однозначно восстановить документ:

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

Шаг 1. Универсальное получение данных через Метаданные

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

Рассмотрим, как получить список всех реквизитов документа и обойти их в цикле:


// Функция преобразует значение реквизита в строку, пригодную для сохранения в TXT
Функция ПолучитьСтроковоеПредставлениеЗначения(Значение)
    ТипЗначения = ТипЗнч(Значение);
    
    // Обработка простых типов данных
    Если ТипЗначения = Тип("Число") Или ТипЗначения = Тип("Строка") Или ТипЗначения = Тип("Булево") Тогда
        Возврат "Простой|" + Строка(Значение);
    ИначеЕсли ТипЗначения = Тип("Дата") Тогда
        Возврат "Дата|" + Формат(Значение, "ДФ=yyyyMMddHHmmss");
    
    // Обработка перечислений
    ИначеЕсли Перечисления.ТипВсего().СодержитТип(ТипЗначения) Тогда
        ИмяПеречисления = Метаданные.НайтиПоТипу(ТипЗначения).Имя;
        ИмяЗначения = Строка(Значение);
        Возврат "Перечисление|" + ИмяПеречисления + "|" + ИмяЗначения;
        
    // Обработка ссылочных типов (Справочники)
    ИначеЕсли Справочники.ТипВсего().СодержитТип(ТипЗначения) Тогда
        Если Значение.Пустая() Тогда
            Возврат "Справочник|" + Метаданные.НайтиПоТипу(ТипЗначения).Имя + "|ПустаяСсылка";
        КонецЕсли;
        
        // В качестве уникального идентификатора используем GUID ссылки
        ИмяСправочника = Метаданные.НайтиПоТипу(ТипЗначения).Имя;
        УИД = Строка(Значение.УникальныйИдентификатор());
        Возврат "Справочник|" + ИмяСправочника + "|" + УИД;
        
    КонецЕсли;
    
    Возврат "НеподдерживаемыйТип|";
КонецФункции

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


Процедура ВыгрузитьДокументВТекст(СсылкаНаДокумент, ПутьКФайлу)
    
    ФайлЗаписи = Новый ЗаписьТекста(ПутьКФайлу, КодировкаТекста.UTF8);
    МетаданныеДокумента = СсылкаНаДокумент.Метаданные();
    
    // 1. Записываем тип документа
    ФайлЗаписи.ЗаписатьСтроку("[ТипДокумента]");
    ФайлЗаписи.ЗаписатьСтроку(МетаданныеДокумента.Имя);
    
    // 2. Выгружаем стандартные реквизиты (Дата, Номер, Проведен, ПометкаУдаления)
    ФайлЗаписи.ЗаписатьСтроку("[СтандартныеРеквизиты]");
    ФайлЗаписи.ЗаписатьСтроку("Номер|" + СсылкаНаДокумент.Номер);
    ФайлЗаписи.ЗаписатьСтроку("Дата|" + Формат(СсылкаНаДокумент.Дата, "ДФ=yyyyMMddHHmmss"));
    ФайлЗаписи.ЗаписатьСтроку("Проведен|" + Строка(СсылкаНаДокумент.Проведен));
    
    // 3. Выгружаем пользовательские реквизиты шапки
    ФайлЗаписи.ЗаписатьСтроку("[РеквизитыШапки]");
    Для Каждого Реквизит Из МетаданныеДокумента.Реквизиты Цикл
        ИмяРеквизита = Реквизит.Имя;
        ЗначениеРеквизита = СсылкаНаДокумент[ИмяРеквизита];
        
        СтрокаЗначения = ПолучитьСтроковоеПредставлениеЗначения(ЗначениеРеквизита);
        ФайлЗаписи.ЗаписатьСтроку(ИмяРеквизита + "=" + СтрокаЗначения);
    КонецЦикла;
    
    // 4. Выгружаем табличные части документа
    Для Каждого ТабличнаяЧасть Из МетаданныеДокумента.ТабличныеЧасти Цикл
        ИмяТЧ = ТабличнаяЧасть.Имя;
        ФайлЗаписи.ЗаписатьСтроку("[ТабличнаяЧасть." + ИмяТЧ + "]");
        
        // Получаем данные ТЧ в виде таблицы значений
        ТаблицаДанных = СсылкаНаДокумент[ИмяТЧ].Выгрузить();
        
        // Записываем заголовок колонок
        СтрокаКолонок = "";
        Для Каждого Колонка Из ТабличнаяЧасть.Реквизиты Цикл
            СтрокаКолонок = СтрокаКолонок + Колонка.Имя + ";";
        КонецЦикла;
        ФайлЗаписи.ЗаписатьСтроку(СтрокаКолонок);
        
        // Записываем строки табличной части
        Для Каждого СтрокаТЧ Из ТаблицаДанных Цикл
            СтрокаДанных = "";
            Для Каждого Колонка Из ТабличнаяЧасть.Реквизиты Цикл
                ЗначениеЯчейки = СтрокаТЧ[Колонка.Имя];
                СтрокаДанных = СтрокаДанных + ПолучитьСтроковоеПредставлениеЗначения(ЗначениеЯчейки) + ";";
            КонецЦикла;
            ФайлЗаписи.ЗаписатьСтроку(СтрокаДанных);
        КонецЦикла;
    КонецЦикла;
    
    ФайлЗаписи.Закрыть();
    Сообщить("Выгрузка успешно завершена!");
КонецПроцедуры

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

Шаг 2. Чтение текстового файла и восстановление типов

Для загрузки документа нам потребуется решить обратную задачу: прочитать текстовый файл, распознать маркеры типов и преобразовать строковые данные обратно в объекты платформы 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;
    КонецЕсли;
    
    Возврат Неопределено;
КонецФункции

Шаг 3. Создание документа на основе прочитанных данных

Теперь соберем все блоки воедино и напишем универсальный алгоритм загрузки. Мы будем использовать объект ЧтениеТекста, читать файл построчно, определять секции и наполнять реквизиты нового документа.


Процедура ЗагрузитьДокументИзТекста(ПутьКФайлу)
    
    ФайлЧтения = Новый ЧтениеТекста(ПутьКФайлу, КодировкаТекста.UTF8);
    СтрокаФайла = ФайлЧтения.ПрочитатьСтроку();
    
    НовыйДокумент = Неопределено;
    ТекущаяСекция = "";
    ТекущаяТЧ = "";
    МассивКолонокТЧ = Новый Массив;
    
    Пока СтрокаФайла <> Неопределено Цикл
        
        // Определение секций файла
        Если Найти(СтрокаФайла, "[") = 1 Тогда
            ТекущаяСекция = СтрЗаменить(СтрЗаменить(СтрокаФайла, "[", ""), "]", "");
            СтрокаФайла = ФайлЧтения.ПрочитатьСтроку();
            Продолжить;
        КонецЕсли;
        
        // Обработка секции типа документа и его создание
        Если ТекущаяСекция = "ТипДокумента" Тогда
            ИмяДокумента = СтрокаФайла;
            НовыйДокумент = Документы[ИмяДокумента].СоздатьДокумент();
            
        // Заполнение стандартных реквизитов
        ИначеЕсли ТекущаяСекция = "СтандартныеРеквизиты" Тогда
            ПараметрыРеквизита = СтроковыеФункцииКлиентСервер.РазложитьСтрокуВМассивПодстрок(СтрокаФайла, "|");
            ИмяРеквизита = ПараметрыРеквизита[0];
            ЗначениеРеквизитаТекст = ПараметрыРеквизита[1];
            
            Если ИмяРеквизита = "Номер" Тогда
                НовыйДокумент.Номер = ЗначениеРеквизитаТекст;
            ИначеЕсли ИмяРеквизита = "Дата" Тогда
                НовыйДокумент.Дата = Дата(Сред(ЗначениеРеквизитаТекст, 1, 4), Сред(ЗначениеРеквизитаТекст, 5, 2), Сред(ЗначениеРеквизитаТекст, 7, 2), Сред(ЗначениеРеквизитаТекст, 9, 2), Сред(ЗначениеРеквизитаТекст, 11, 2), Сред(ЗначениеРеквизитаТекст, 13, 2));
            КонецЕсли;
            
        // Заполнение произвольных реквизитов шапки
        ИначеЕсли ТекущаяСекция = "РеквизитыШапки" Тогда
            ПозицияРавно = Найти(СтрокаФайла, "=");
            ИмяРеквизита = СокрЛП(Лев(СтрокаФайла, ПозицияРавно - 1));
            ЗначениеРеквизитаСтрока = СокрЛП(Сред(СтрокаФайла, ПозицияРавно + 1));
            
            НовыйДокумент[ИмяРеквизита] = ВосстановитьЗначениеИзСтроки(ЗначениеРеквизитаСтрока);
            
        // Обработка табличной части
        ИначеЕсли Найти(ТекущаяСекция, "ТабличнаяЧасть.") = 1 Тогда
            ИмяТЧ = СтрЗаменить(ТекущаяСекция, "ТабличнаяЧасть.", "");
            
            // Если сменилась табличная часть или она только началась
            Если ТекущаяТЧ <> ИмяТЧ Тогда
                ТекущаяТЧ = ИмяТЧ;
                // Первая строка в секции ТЧ - это заголовок колонок
                МассивКолонокТЧ = СтроковыеФункцииКлиентСервер.РазложитьСтрокуВМассивПодстрок(СтрокаФайла, ";");
                // Удалим пустые элементы, если они возникли из-за финального разделителя ";"
                Если МассивКолонокТЧ[МассивКолонокТЧ.Количество() - 1] = "" Тогда
                    МассивКолонокТЧ.Удалить(МассивКолонокТЧ.Количество() - 1);
                КонецЕсли;
                СтрокаФайла = ФайлЧтения.ПрочитатьСтроку();
                Продолжить;
            КонецЕсли;
            
            // Добавляем новую строку в ТЧ
            НоваяСтрока = НовыйДокумент[ТекущаяТЧ].Добавить();
            ЗначенияЯчеек = СтроковыеФункцииКлиентСервер.РазложитьСтрокуВМассивПодстрок(СтрокаФайла, ";");
            
            Для Индекс = 0 По МассивКолонокТЧ.Количество() - 1 Цикл
                ИмяКолонки = МассивКолонокТЧ[Индекс];
                ЗначениеКолонкиСтрока = ЗначенияЯчеек[Индекс];
                
                НоваяСтрока[ИмяКолонки] = ВосстановитьЗначениеИзСтроки(ЗначениеКолонкиСтрока);
            КонецЦикла;
            
        КонецЕсли;
        
        СтрокаФайла = ФайлЧтения.ПрочитатьСтроку();
    КонецЦикла;
    
    ФайлЧтения.Закрыть();
    
    // Записываем воссозданный документ в базу данных
    Попытка
        НовыйДокумент.Записать(РежимЗаписиДокумента.Запись);
        Сообщить("Документ успешно загружен и сохранен с номером: " + НовыйДокумент.Номер);
    Исключение
        Сообщить("Ошибка при записи документа: " + ОписаниеОшибки());
    КонецПопытки;
    
КонецПроцедуры

Важные нюансы и подводные камни решения

Разрабатывая подобные универсальные алгоритмы, необходимо учитывать несколько критически важных моментов, с которыми вы обязательно столкнетесь при практическом использовании кода:

  1. Отсутствие ссылочных данных в базе-приемнике. Если вы переносите документ в абсолютно чистую базу данных, GUID справочников (например, Номенклатуры или Контрагентов) не найдут соответствия. В таком случае в реквизиты запишется БитаяСсылка или пустая ссылка. Для решения этой проблемы в ТЗ реальных проектов обычно закладывают требование: «Если ссылка не найдена, создать новый элемент справочника с аналогичным кодом/наименованием». Для этого структуру выгрузки ссылочных типов расширяют и дополнительно записывают Наименование и Код.
  2. Сложные типы данных и Хранилища значений. Если документ содержит реквизиты с типом ХранилищеЗначения (например, файлы или картинки), выгрузить их напрямую через строку не получится. Понадобится дополнительно кодировать двоичные данные в формат Base64 и записывать полученную строку в текстовый файл.
  3. Составные типы данных. Если реквизит имеет составной тип (например, владельцем договора может быть как Контрагент, так и Физическое лицо), наш алгоритм успешно справится с задачей, поскольку мы записываем не просто GUID, а полную информацию о типе метаданных: "Справочник|Контрагенты|УИД". При чтении платформа безошибочно определит, к какой именно таблице адресовать запрос поиска.

Таким образом, мы детально разобрали, как с помощью коллекции метаданных 1С обойти ограничения на использование JSON/XML и построить надежный, гибкий, а главное — полностью универсальный механизм сериализации объектов 1С в обычные текстовые файлы.

← На главную