В практике разработки на платформе 1С:Предприятие часто возникает задача отправки печатных форм (счетов, накладных, актов) клиентам по электронной почте. Самый простой способ, который приходит в голову начинающему разработчику, — это сохранить табличный документ в файл по жестко заданному пути, а затем прикрепить этот файл к письму. Рассмотрим пример кода, с которого обычно начинаются подобные попытки:
// Пример, как делать НЕ стоит в многопользовательской среде
ТабДок = Новый ТабличныйДокумент;
// ... заполнение документа ...
ТабДок.Записать("e:\prim.pdf", ТипФайлаТабличногоДокумента.PDF);
Письмо = Новый ИнтернетПочтовоеСообщение;
Письмо.Вложения.Добавить("e:\prim.pdf");
// ... отправка ...
Такой подход имеет существенные недостатки. Если несколько пользователей одновременно нажмут кнопку отправки, возникнет конфликт доступа к файлу e:\prim.pdf. Кто-то получит чужую печатную форму, или программа выдаст ошибку блокировки файла. Кроме того, в облачных инфраструктурах или при строгих настройках безопасности сервера запись на диск C:\ или E:\ может быть вовсе запрещена.
В этой статье мы подробно разберем два надежных способа решения этой задачи: использование временных файлов и работу с потоками в памяти.
Первый метод является классическим и работает на всех версиях платформы 1С. Суть его заключается в том, что мы не придумываем имя файла сами, а просим операционную систему выделить нам уникальное имя во временном каталоге.
Рассмотрим алгоритм действий:
Разберем код реализации:
// 1. Получаем уникальное имя временного файла
// Платформа сама создаст путь в папке Temp пользователя или сервера
ИмяВременногоФайла = ПолучитьИмяВременногоФайла("pdf");
// 2. Записываем Табличный Документ
ТабДок.Записать(ИмяВременногоФайла, ТипФайлаТабличногоДокумента.PDF);
// 3. Создаем письмо и добавляем вложение
Письмо = Новый ИнтернетПочтовоеСообщение;
// Метод Добавить здесь принимает путь к файлу
Письмо.Вложения.Добавить(ИмяВременногоФайла);
// ... код отправки письма ...
// 4. Хороший тон: удаляем временный файл после отправки
УдалитьФайлы(ИмяВременногоФайла);
Преимущества: Простота реализации, работает на старых платформах (8.2, старые релизы 8.3).
Недостатки: Требуется работа с дисковой подсистемой (IO operations), что медленнее работы с памятью. Необходимо следить за удалением файлов, иначе папка Temp может переполниться.
Начиная с версии платформы 8.3.10, мы получили возможность работать с потоками в памяти. Это более современный и производительный метод, идеально подходящий для серверных процедур и облачных баз. Мы вообще не касаемся жесткого диска, все операции происходят в оперативной памяти (RAM).
Давайте проанализируем, как использовать объект ПотокВПамяти для нашей задачи.
// Создаем поток в оперативной памяти
Поток = Новый ПотокВПамяти;
// Записываем табличный документ прямо в поток
// Обратите внимание: первый параметр теперь не строковый путь, а объект Поток
ТабДок.Записать(Поток, ТипФайлаТабличногоДокумента.PDF);
// Получаем ДвоичныеДанные из потока
// После этого метода поток закрывается для записи
ДвоичныеДанныеPDF = Поток.ЗакрытьИПолучитьДвоичныеДанные();
// Формируем письмо
Письмо = Новый ИнтернетПочтовоеСообщение;
// ВАЖНЫЙ МОМЕНТ:
// При добавлении двоичных данных во вложение, вторым параметром
// нужно обязательно указать имя файла, которое увидит получатель.
// Если этого не сделать, файл может прийти как "untitled.bin" или подобное.
Письмо.Вложения.Добавить(ДвоичныеДанныеPDF, "СчетНаОплату.pdf");
Давайте рассмотрим подробнее нюансы метода Письмо.Вложения.Добавить. У него есть перегрузки. Если мы передаем путь к файлу (строку), платформа сама берет имя файла из пути. Если же мы передаем объект ДвоичныеДанные, платформа не знает, как назвать файл. Поэтому мы должны явно задать имя с расширением (например, "Order.pdf").
Преимущества:
В обоих рассмотренных способах мы использовали метод Записать. Давайте углубимся в его возможности. Часто требуется не просто создать PDF, а, например, защитить его паролем или оптимизировать размер (актуально для отправки по почте).
Для этого используем объект ПараметрыЗаписиPDF. Посмотрим на пример:
ПараметрыPDF = Новый ПараметрыЗаписиPDF;
ПараметрыPDF.ВнедрятьШрифты = Истина; // Чтобы документ выглядел одинаково везде
ПараметрыPDF.ПарольВладельца = "SecretPassword"; // Защита от редактирования
// Используем параметры при записи в поток
ТабДок.Записать(Поток, ТипФайлаТабличногоДокумента.PDF, ПараметрыPDF);
Важно помнить, где именно выполняется ваш код. Отправка почты (работа с объектом ИнтернетПочтовоеСообщение) обычно происходит на Сервере, так как там настроен доступ в Интернет и к SMTP-шлюзам.
Если табличный документ формируется у вас на клиенте (например, пользователь видит печатную форму на экране и нажимает кнопку "Отправить"), вы не можете просто так передать ТабличныйДокумент на сервер, так как это мутабельный объект (его состояние может меняться).
В этом случае, правильная схема взаимодействия будет следующей:
Разберем пример реализации перехода с Клиента на Сервер:
&НаКлиенте
Процедура ОтправитьНаПочту(Команда)
// Помещаем ТабДок во временное хранилище
// УникальныйИдентификатор нужен, чтобы хранилище жило пока открыта форма
АдресВХранилище = ПоместитьВоВременноеХранилище(Объект.ТабДок, УникальныйИдентификатор);
// Вызываем серверную процедуру
ОтправитьПечатнуюФормуНаСервере(АдресВХранилище);
КонецПроцедуры
&НаСервере
Процедура ОтправитьПечатнуюФормуНаСервере(Адрес)
// Получаем ТабДок из хранилища
ТабДокДляОтправки = ПолучитьИзВременногоХранилища(Адрес);
// Далее используем метод с потоком
Поток = Новый ПотокВПамяти;
ТабДокДляОтправки.Записать(Поток, ТипФайлаТабличногоДокумента.PDF);
ДД = Поток.ЗакрытьИПолучитьДвоичныеДанные();
// Создаем и отправляем письмо...
// ...
КонецПроцедуры
Мы выяснили, что использование жестких путей к файлам (e:\file.pdf) является плохой практикой. Для современных конфигураций 1С наиболее эффективным решением является использование объекта ПотокВПамяти. Это позволяет конвертировать табличный документ в PDF и прикрепить его к письму «на лету», не затрагивая файловую систему сервера. Если же вы поддерживаете старую конфигурацию (платформа ниже 8.3.10), используйте функцию ПолучитьИмяВременногоФайла() с последующим удалением созданного файла.