Запись файлов из кода 1С 8.3: табличный документ, двоичные данные, диалог и расписание

· 9 мин

Содержание

Программно «Сохранить как» в 1С делают в два хода. На сервере формируете файл методом Записать() (у табличного документа, двоичных данных или текстового документа) и кладёте результат во временное хранилище. На клиенте отдаёте его пользователю через ПолучитьФайлССервераАсинх() (платформа 8.3.18 и новее) или НачатьПолучениеФайлаССервера() на более ранних релизах.

Такая схема одинаково работает в тонком клиенте, веб-клиенте и при работе через интернет . Файл, записанный прямо на сервере, лежит на диске сервера, а не на компьютере бухгалтера. Ниже код для печатных форм, отчётов, произвольных файлов и выгрузки по расписанию. Все примеры на встроенном языке платформы 8.3 с интерфейсом «Такси».

Как записать табличный документ в PDF, Excel и другие форматы

Отчёт, печатная форма, макет — всё это объект ТабличныйДокумент. У него есть метод Записать(ИмяФайла, ТипФайла). Второй параметр — значение системного перечисления ТипФайлаТабличногоДокумента.

ЗначениеЧто получитсяКогда брать
PDFPDF-документпечатные формы для отправки контрагенту
XLSXкнига Excelотчёты, которые будут дорабатывать
XLSстарый формат Excelтолько если адресат не открывает XLSX
DOCXдокумент Wordписьма, договоры из макетов
ODSтаблица LibreOfficeдля пользователей свободных офисных пакетов
MXLродной формат 1Сдля обмена между базами 1С
HTMLвеб-страницапубликация, вложение в письмо
TXT, ANSITXTтекстпростые выгрузки без форматирования

Базовый вариант для печатной формы выглядит так. Функция серверная, вызывается из формы:

&НаСервере
Функция ПечатнаяФормаВPDF()

    ТабДок = СформироватьПечатнуюФорму(); // ваша функция, возвращает ТабличныйДокумент

    ИмяВременногоФайла = ПолучитьИмяВременногоФайла("pdf");
    ТабДок.Записать(ИмяВременногоФайла, ТипФайлаТабличногоДокумента.PDF);

    Данные = Новый ДвоичныеДанные(ИмяВременногоФайла);
    УдалитьФайлы(ИмяВременногоФайла);

    Возврат ПоместитьВоВременноеХранилище(Данные, УникальныйИдентификатор);

КонецФункции

Три момента, на которых чаще всего спотыкаются:

  • 🗂️ ПолучитьИмяВременногоФайла("pdf") даёт уникальное имя в каталоге временных файлов. Два пользователя, печатающих одновременно, не перезапишут файлы друг друга.
  • 🧹 УдалитьФайлы() сразу после чтения. Иначе каталог временных файлов на сервере за год распухает до гигабайтов.
  • 🔑 УникальныйИдентификатор формы во втором параметре хранилища. Данные живут, пока открыта форма, и удаляются при её закрытии.

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

Перед записью можно подправить параметры страницы: ТабДок.ОриентацияСтраницы = ОриентацияСтраницы.Ландшафт, ТабДок.АвтоМасштаб = Истина. На PDF они влияют напрямую: что не помещается по ширине, уйдёт на следующий лист.

Отчёт на системе компоновки данных

Для регламентной выгрузки отчёт формируют без формы. Табличный документ получают из схемы компоновки, а затем записывают тем же методом Записать():

Схема = Отчеты.ОстаткиТоваров.ПолучитьМакет("ОсновнаяСхемаКомпоновкиДанных");

Компоновщик = Новый КомпоновщикМакетаКомпоновкиДанных;
Макет = Компоновщик.Выполнить(Схема, Схема.НастройкиПоУмолчанию);

Процессор = Новый ПроцессорКомпоновкиДанных;
Процессор.Инициализировать(Макет);

ТабДок = Новый ТабличныйДокумент;
Вывод = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
Вывод.УстановитьДокумент(ТабДок);
Вывод.Вывести(Процессор);

Имя отчёта ОстаткиТоваров здесь условное, подставьте своё. Если нужны не настройки по умолчанию, а сохранённый вариант с отборами, загрузите его настройки в компоновщик настроек и передайте в Выполнить() их.

Двоичные данные, потоки и текстовые файлы

Не всё, что нужно сохранить, — табличный документ. Вложение из справочника, картинка, XML от банка, CSV для маркетплейса — это двоичные данные или текст.

Двоичные данные

ДвоичныеДанные пишутся одной строкой:

Данные = ХранилищеФайла.Получить(); // из реквизита типа ХранилищеЗначения
Данные.Записать(ПолноеИмяФайла);

Из строки двоичные данные получают функцией ПолучитьДвоичныеДанныеИзСтроки(Текст, КодировкаТекста.UTF8). Из Base64 — ПолучитьДвоичныеДанныеИзBase64Строки(). Так обычно разбирают ответы веб-сервисов, где файл приходит строкой.

Потоки

Начиная с 8.3.9 в платформе есть потоки: ФайловыйПоток, ПотокВПамяти. Они нужны, когда файл большой и держать его целиком в памяти рабочего процесса дорого.

&НаСервере
Процедура СкопироватьВФайл(Данные, ПолноеИмяФайла)

    ПотокЗаписи = Новый ФайловыйПоток(ПолноеИмяФайла, РежимОткрытияФайла.Создать);
    ПотокЧтения = Данные.ОткрытьПотокДляЧтения();

    ПотокЧтения.КопироватьВ(ПотокЗаписи);

    ПотокЧтения.Закрыть();
    ПотокЗаписи.Закрыть();

КонецПроцедуры

Закрывайте поток всегда, в том числе при ошибке. Незакрытый ФайловыйПоток держит файл заблокированным, пока не завершится рабочий процесс. Следующая попытка записи упадёт, а удалить файл не даст Windows.

Текст, CSV и JSON

Для построчной записи берите ЗаписьТекста, она сама ставит переводы строк:

Запись = Новый ЗаписьТекста(ПолноеИмяФайла, КодировкаТекста.UTF8);
Запись.ЗаписатьСтроку("Артикул;Наименование;Остаток");
Для Каждого Строка Из ТаблицаОстатков Цикл
    Запись.ЗаписатьСтроку(СтрШаблон("%1;%2;%3",
        Строка.Артикул, Строка.Наименование, Формат(Строка.Остаток, "ЧГ=0; ЧН=0")));
КонецЦикла;
Запись.Закрыть();

Формат(..., "ЧГ=0") убирает пробелы-разделители разрядов. Без него остаток 12500 попадёт в CSV как «12 500», и Excel сочтёт его текстом.

JSON пишут через ЗаписьJSON: ОткрытьФайл(ПолноеИмяФайла), затем ЗаписатьJSON(Запись, Структура), затем Закрыть().

Где выполняется запись: на клиенте или на сервере

Главный вопрос, который решают до написания кода, — на каком компьютере окажется файл. Код в &НаСервере пишет на диск сервера 1С. Код в &НаКлиенте пишет на компьютер пользователя.

СценарийГде писатьЧем передать
Пользователь нажал кнопку «Сохранить в PDF»сформировать на сервереПолучитьФайлССервераАсинх()
Файловая база, тонкий клиент, путь на локальном дискеможно прямо на клиентеДанные.Записать() на клиенте
Веб-клиенттолько через передачу файлабраузер скачивает сам
Выгрузка в сетевую папку по расписаниюна сервереничего, файл сразу на месте
Несколько файлов пользователюна сервере, упаковать в ZIPодин архив вместо десятка диалогов

Клиентская часть для кнопки печати:

&НаКлиенте
Асинх Процедура СохранитьВPDF(Команда)

    Адрес = ПечатнаяФормаВPDF();
    ИмяФайла = "Акт_" + СтрЗаменить(Объект.Номер, "/", "-") + ".pdf";

    Результат = Ждать ПолучитьФайлССервераАсинх(Адрес, ИмяФайла);
    Если Результат = Неопределено Тогда
        Возврат; // пользователь отменил сохранение
    КонецЕсли;

КонецПроцедуры

Для платформы ниже 8.3.18 ключевых слов Асинх и Ждать нет. Тот же результат даёт вызов с описанием оповещения:

Оповещение = Новый ОписаниеОповещения("ПослеСохраненияФайла", ЭтотОбъект);
НачатьПолучениеФайлаССервера(Оповещение, Адрес, ИмяФайла);

Синхронный ПолучитьФайл() при режиме модальности «Не использовать» вызывает исключение. В современных конфигурациях этот режим включён почти везде, поэтому старый код из интернета с ПолучитьФайл() придётся переписать.

Несколько файлов пакуйте в архив на сервере: Новый ЗаписьZipФайла(ИмяАрхива), затем Добавить(ИмяФайла) для каждого файла и Записать(). Пользователь получит один диалог вместо пяти.

Диалог выбора файла и сборка пути

Когда пользователь должен сам выбрать папку и имя, используйте ДиалогВыбораФайла в режиме сохранения. Работает в тонком клиенте; в веб-клиенте нужно расширение работы с файлами.

&НаКлиенте
Процедура ВыбратьКудаСохранить(Команда)

    Диалог = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.Сохранение);
    Диалог.Заголовок = "Куда сохранить отчёт";
    Диалог.Фильтр = "Книга Excel (*.xlsx)|*.xlsx|Документ PDF (*.pdf)|*.pdf";
    Диалог.ПолноеИмяФайла = "Остатки_" + Формат(ТекущаяДата(), "ДФ=yyyyMMdd") + ".xlsx";

    Диалог.Показать(Новый ОписаниеОповещения("ПослеВыбораПути", ЭтотОбъект));

КонецПроцедуры

&НаКлиенте
Процедура ПослеВыбораПути(ВыбранныеФайлы, ДополнительныеПараметры) Экспорт

    Если ВыбранныеФайлы = Неопределено Тогда
        Возврат;
    КонецЕсли;

    ПолныйПуть = ВыбранныеФайлы[0];
    Расширение = НРег(Новый Файл(ПолныйПуть).Расширение); // ".xlsx" или ".pdf"

    Адрес = ОтчетВФайлНаСервере(Расширение); // сервер выбирает тип по расширению
    Данные = ПолучитьИзВременногоХранилища(Адрес);
    Данные.Записать(ПолныйПуть);

КонецПроцедуры

Для сборки путей в коде пригодятся:

  • 📁 КаталогВременныхФайлов() — временный каталог текущего компьютера (клиента или сервера, смотря где выполняется код).
  • ➗ ПолучитьРазделительПути() — \ в Windows и / в Linux. Сервер 1С на Linux не поймёт путь, склеенный через обратный слеш.
  • 🔍 Новый Файл(Путь).Существует() — проверка каталога перед записью.
  • 🏗️ СоздатьКаталог(Путь) — создаёт каталог вместе с недостающими родительскими.

Чистим имя файла

Номер документа и наименование контрагента почти всегда содержат символы, запрещённые в именах файлов Windows: \ / : * ? " < > |. Простая функция замены:

&НаКлиентеНаСервереБезКонтекста
Функция БезопасноеИмя(Знач Имя)
    Запрещенные = "\/:*?""<>|";
    Для Сч = 1 По СтрДлина(Запрещенные) Цикл
        Имя = СтрЗаменить(Имя, Сред(Запрещенные, Сч, 1), "_");
    КонецЦикла;
    Возврат СокрЛП(Имя);
КонецФункции

Счёт «ООО “Ромашка” №12/3» превратится в «ООО Ромашка №12_3» и запишется без ошибки.

Ошибки при программном сохранении и как их перехватить

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

Попытка
    ТабДок.Записать(ПолноеИмяФайла, ТипФайлаТабличногоДокумента.XLSX);
Исключение
    ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
    ЗаписьЖурналаРегистрации("Выгрузка.ЗаписьФайла",
        УровеньЖурналаРегистрации.Ошибка, , , ТекстОшибки);
    ВызватьИсключение КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
КонецПопытки;

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

Диагностика: выберите симптом

Отдельно об отмене. Если пользователь закрыл диалог, асинхронный метод возвращает Неопределено. Это не ошибка, и выводить сообщение «Не удалось сохранить» в этом случае не нужно.

Сохранение файлов по расписанию: регламентное задание

Ежедневная выгрузка остатков в папку обмена, PDF реестра для руководителя к 9 утра — это регламентное задание. Код задания выполняется на сервере без формы и без пользователя, поэтому диалогов и передачи на клиент тут нет.

Порядок настройки:

  1. В Конфигураторе или в расширении создайте серверный общий модуль и экспортную процедуру выгрузки.
  2. Добавьте объект метаданных «Регламентное задание» и укажите эту процедуру в его имени метода.
  3. Задайте расписание. В конфигурациях на БСП его удобно менять из режима предприятия: Администрирование → Обслуживание → Регламентные и фоновые задания.

Сама процедура:

Процедура ВыгрузитьОстаткиВПапку() Экспорт

    Каталог = "\\fileserver\exchange\ostatki\";
    Метка = Формат(ТекущаяДатаСеанса(), "ДФ=yyyyMMdd_HHmm");
    ИмяЧерновика = Каталог + "ostatki_" + Метка + ".tmp";
    ИмяИтоговое  = Каталог + "ostatki_" + Метка + ".xlsx";

    Попытка
        ТабДок = СформироватьОтчетОстатков();
        ТабДок.Записать(ИмяЧерновика, ТипФайлаТабличногоДокумента.XLSX);
        ПереместитьФайл(ИмяЧерновика, ИмяИтоговое);
    Исключение
        ЗаписьЖурналаРегистрации("Выгрузка остатков",
            УровеньЖурналаРегистрации.Ошибка, , ,
            ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
        ВызватьИсключение;
    КонецПопытки;

КонецПроцедуры

Приём с черновиком .tmp и ПереместитьФайл() защищает принимающую сторону. Скрипт или сервис, который забирает файлы по маске *.xlsx, никогда не схватит наполовину записанный файл.

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

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

Для клиент-серверной базы проверьте, что учётная запись службы сервера 1С имеет права на запись в \\fileserver\exchange. Это самая частая причина, по которой задание вручную из Конфигуратора отрабатывает, а по расписанию падает.

Частые вопросы

Можно ли сохранить табличный документ без временного файла?
В свежих релизах платформы Записать() принимает поток, например ПотокВПамяти. Проверьте сигнатуру метода в синтакс-помощнике своей версии. Вариант с временным файлом работает на любом релизе 8.3.
Почему в XLSX слетает ширина колонок и формат чисел?
Табличный документ переносит оформление в Excel приблизительно. Задайте ширину колонок и формат чисел в макете явно, а итоговые суммы выводите числом, а не строкой.
Как сохранить сразу несколько печатных форм в один PDF?
Соберите их в один ТабличныйДокумент: выводите формы по очереди и вставляйте горизонтальный разделитель страниц между ними. Затем запишите один раз в PDF.
Можно ли записать файл прямо на компьютер пользователя из регламентного задания?
Нет. Задание выполняется без клиентского сеанса пользователя. Пишите в общую сетевую папку, к которой у пользователя есть доступ, или отправляйте файл по почте.
Как задать имя файла по умолчанию, но дать пользователю его поменять?
Передайте имя вторым параметром в ПолучитьФайлССервераАсинх(). Платформа покажет диалог с этим именем, а пользователь сможет выбрать другую папку и исправить его.