Как сформировать печатную форму в MS Word без ошибок БСП?

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

При разработке внешних печатных форм (ВПФ) на платформе 1С:Предприятие 8.3 программисты часто сталкиваются с необходимостью выгрузки данных не в стандартный табличный документ, а непосредственно в текстовый формат Microsoft Word (.doc или .docx). Обычно это требуется для создания сложных договоров, соглашений или официальных писем, где важна точная разметка текстовых блоков.

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

Почему возникает ошибка при формировании файла Word?

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

Алгоритмы БСП строго контролируют возвращаемый результат. Они ожидают, что в коллекции печатных форм для каждого идентификатора команды будет заполнен объект типа ТабличныйДокумент. Если процедура завершает работу, а табличный документ остался пустым или вообще не был создан (так как вместо него напрямую инициализировался COM-объект Word.Application), БСП сигнализирует об ошибке формирования макета.

Для решения этой проблемы мы можем использовать два основных подхода. Рассмотрим их подробнее.

Способ 1. Использование клиентского метода вызова (ВызовКлиентскогоМетода)

Если вам обязательно нужно использовать COM-объект (например, из-за наличия старых шаблонов с закладками или макросами), логику формирования необходимо полностью перенести на сторону клиента. На сервере создавать COM-объект MS Word категорически не рекомендуется, так как это может привести к зависанию процессов WINWORD.EXE в сессиях операционной системы сервера.

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

Во-первых, настроим модуль объекта нашей обработки. Нам нужно указать БСП, что команда должна выполняться на клиенте. Для этого изменим тип использования команды на ТипКомандыВызовКлиентскогоМетода(). Посмотрим на пример кода регистрации:


Функция СведенияОВнешнейОбработке() Экспорт
    
    ПараметрыРегистрации = Новый Структура;
    МассивНазначений = Новый Массив;
    МассивНазначений.Добавить("Документ.РеализацияТоваровУслуг");
    
    ПараметрыРегистрации.Вставить("Вид", ДополнительныеОтчетыИОбработкиКлиентСервер.ВидОбработкиПечатнаяФорма());
    ПараметрыРегистрации.Вставить("Версия", "1.0");
    ПараметрыРегистрации.Вставить("Наименование", "Договор в MS Word");
    ПараметрыРегистрации.Вставить("БезопасныйРежим", Ложь);
    ПараметрыРегистрации.Вставить("Информация", "Формирование договора в формате Word на клиенте");
    ПараметрыРегистрации.Вставить("Назначение", МассивНазначений);
    
    ТаблицаКоманд = ПолучитьТаблицуКоманд();
    
    НоваяКоманда = ТаблицаКоманд.Добавить();
    НоваяКоманда.Представление = "Договор (Word)";
    НоваяКоманда.Идентификатор = "ДоговорWord";
    // Указываем БСП использовать клиентский метод вызова
    НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовКлиентскогоМетода();
    НоваяКоманда.ПоказыватьОповещение = Истина;
    НоваяКоманда.Модификатор = "ПечатьDOC";
    
    ПараметрыРегистрации.Вставить("Команды", ТаблицаКоманд);
    
    Возврат ПараметрыРегистрации;
    
КонецФункции

Функция ПолучитьТаблицуКоманд()
    Команды = Новый ТаблицаЗначений;
    Команды.Колонки.Добавить("Представление", Новый ОписаниеТипов("Строка"));
    Команды.Колонки.Добавить("Идентификатор", Новый ОписаниеТипов("Строка"));
    Команды.Колонки.Добавить("Использование", Новый ОписаниеТипов("Строка"));
    Команды.Колонки.Добавить("ПоказыватьОповещение", Новый ОписаниеТипов("Булево"));
    Команды.Колонки.Добавить("Модификатор", Новый ОписаниеТипов("Строка"));
    Возврат Команды;
КонецФункции

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

Проанализируем структуру модуля управляемой формы нашей обработки:


&НаКлиенте
Процедура Печать(ИдентификаторКоманды, МассивОбъектов) Экспорт
    
    // Проверяем наличие установленного MS Word на компьютере пользователя
    Попытка
        Word = Новый COMОбъект("Word.Application");
    Исключение
        ПоказатьПредупреждение(, "Ошибка при запуске Microsoft Word. Убедитесь, что приложение установлено.");
        Возврат;
    КонецПопытки;
    
    // Получаем двоичные данные макета и данные для заполнения с сервера
    СтруктураДанных = ПолучитьДанныеИШаблонСервер(МассивОбъектов);
    
    Если СтруктураДанных = Неопределено Тогда
        Word.Quit();
        Возврат;
    КонецЕсли;
    
    // Сохраняем двоичные данные макета во временный файл на клиенте
    ИмяВременногоФайла = КаталогВременныхФайлов() + "ШаблонДоговора.docx";
    СтруктураДанных.МакетДвоичныеДанные.Записать(ИмяВременногоФайла);
    
    Попытка
        // Открываем документ на основе созданного временного файла шаблона
        ДокументWord = Word.Documents.Add(ИмяВременногоФайла);
        ДокументWord.Activate();
        
        // Заполняем закладки (Bookmarks) данными
        ЗаполнитьЗакладку(ДокументWord, "Контрагент", Строка(СтруктураДанных.Контрагент));
        ЗаполнитьЗакладку(ДокументWord, "НомерДоговора", Строка(СтруктураДанных.НомерДоговора));
        ЗаполнитьЗакладку(ДокументWord, "ДатаДоговора", Формат(СтруктураДанных.ДатаДоговора, "ДЛФ=DD"));
        ЗаполнитьЗакладку(ДокументWord, "СуммаДокумента", Строка(СтруктураДанных.СуммаДокумента));
        
        // Делаем окно приложения MS Word видимым для пользователя
        Word.Visible = Истина;
        Word.Activate();
        
    Исключение
        Word.Quit();
        ПоказатьПредупреждение(, "Произошла ошибка при заполнении документа Word: " + ОписаниеОшибки());
    КонецПопытки;
    
    // Очищаем временный файл шаблона
    Попытка
        УдалитьФайлы(ИмяВременногоФайла);
    Исключение
        // Игнорируем ошибку удаления, если файл занят
    КонецПопытки;

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

&НаСервереБезКонтекста
Функция ПолучитьДанныеИШаблонСервер(МассивОбъектов)
    
    Результат = Новый Структура;
    
    // Выполняем запрос к базе данных для получения необходимых параметров
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   Реализация.ДоговорКонтрагента.НомерДоговора КАК НомерДоговора,
        |   Реализация.ДоговорКонтрагента.ДатаДоговора КАК ДатаДоговора,
        |   Реализация.Контрагент.Наименование КАК Контрагент,
        |   Реализация.СуммаДокумента КАК СуммаДокумента
        |ИЗ
        |   Документ.РеализацияТоваровУслуг КАК Реализация
        |ГДЕ
        |   Реализация.Ссылка В(&МассивОбъектов)";
        
    Запрос.УстановитьПараметр("МассивОбъектов", МассивОбъектов);
    Выборка = Запрос.Выполнить().Выбрать();
    
    Если Выборка.Следующий() Тогда
        ЗаполнитьЗначенияСвойств(Результат, Выборка);
    Иначе
        Возврат Неопределено;
    КонецЕсли;
    
    // Получаем макет как двоичные данные. Макет должен быть добавлен в обработку с типом "Двоичные данные"
    ЭтотОбъект = РеквизитФормыВЗначение("Объект");
    Результат.Вставить("МакетДвоичныеДанные", ЭтотОбъект.ПолучитьМакет("МакетДоговора"));
    
    Возврат Результат;
    
КонецФункции

&НаКлиенте
Процедура ЗаполнитьЗакладку(ДокументWord, ИмяЗакладки, Значение)
    
    Если ДокументWord.Bookmarks.Exists(ИмяЗакладки) Тогда
        // Используем обращение к Range напрямую (это быстрее и надежнее)
        ДокументWord.Bookmarks(ИмяЗакладки).Range.Text = Значение;
    КонецЕсли;
    
КонецПроцедуры

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

Способ 2. Использование штатных механизмов БСП для печати в Word (Рекомендуемый)

Рассмотрим современную альтернативу, которая не требует использования COM-объектов. Начиная с версии БСП 2.x, разработчикам доступен удобный инструмент для работы с офисными документами на уровне сервера. Этот метод полностью кроссплатформенный (будет стабильно работать на серверах под управлением ОС Linux) и не вызывает проблем с зависшими процессами.

Для реализации этого метода выполним следующие шаги:

  1. Создайте макет документа Word. Вместо использования механизма закладок (Bookmarks) добавьте в текст шаблона обычные параметры в фигурных скобках вида: {v8 Контрагент}, {v8 НомерДоговора}.
  2. Сохраните файл с расширением .docx и импортируйте его в макеты вашей внешней обработки как Двоичные данные.
  3. В модуле объекта вернитесь к обычному серверному методу печати, но примените процедуры общего модуля УправлениеПечатью.

Проанализируем пример серверного кода в модуле объекта обработки:


Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
    
    ИдентификаторКоманды = "ДоговорWord";
    
    // Получаем описание печатной формы из коллекции БСП
    ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, ИдентификаторКоманды);
    
    Если ПечатнаяФорма <> Неопределено Тогда
        
        // СЕКРЕТНЫЙ ШАГ: Инициализируем пустой ТабличныйДокумент.
        // Это предотвратит появление системной ошибки "Печатная форма не была создана"
        ПечатнаяФорма.ТабличныйДокумент = Новый ТабличныйДокумент;
        
        // Получаем двоичные данные нашего файла шаблона из макета
        ДвоичныеДанныеМакета = ПолучитьМакет("МакетДоговора");
        
        // Инициализируем макет офисного документа средствами БСП
        Макет = УправлениеПечатью.ИнициализироватьМакетОфисногоДокумента(ДвоичныеДанныеМакета, Неопределено);
        ПечатнаяФормаОфис = УправлениеПечатью.ИнициализироватьПечатнуюФорму(Неопределено, Макет.НастройкиСтраницыМакета, Макет);
        
        // Описываем структуру областей документа
        СтруктураОбластей = Новый Структура;
        УправлениеПечатью.ДобавитьОписаниеОбласти(СтруктураОбластей, "ОсновнойТекст", "Общая");
        
        // Получаем данные для заполнения параметров договора
        ДанныеДоговора = ПолучитьДанныеДоговора(МассивОбъектов[0]);
        
        // Заполняем параметры в области
        ОбластьТекста = УправлениеПечатью.ОбластьМакета(Макет, СтруктураОбластей.ОсновнойТекст);
        УправлениеПечатью.ПрисоединитьОбластьИЗаполнитьПараметры(ПечатнаяФормаОфис, ОбластьТекста, ДанныеДоговора);
        
        // Формируем готовый файл во временном хранилище
        АдресХранилища = УправлениеПечатью.СформироватьДокумент(ПечатнаяФормаОфис);
        
        // Освобождаем ресурсы сервера
        УправлениеПечатью.ОчиститьСсылки(ПечатнаяФормаОфис, Ложь);
        УправлениеПечатью.ОчиститьСсылки(Макет);
        
        // Передаем сформированный файл БСП для последующего открытия на клиенте
        ОфисныеДокументы = Новый Соответствие;
        ОфисныеДокументы.Вставить(АдресХранилища, "Договор_Контрагента.docx");
        
        ПечатнаяФорма.ОфисныеДокументы = ОфисныеДокументы;
        
    КонецЕсли;
    
КонецПроцедуры

Функция ПолучитьДанныеДоговора(СсылкаДокумента)
    
    Результат = Новый Структура;
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   Реализация.ДоговорКонтрагента.НомерДоговора КАК НомерДоговора,
        |   Реализация.ДоговорКонтрагента.ДатаДоговора КАК ДатаДоговора,
        |   Реализация.Контрагент.Наименование КАК Контрагент,
        |   Реализация.СуммаДокумента КАК СуммаДокумента
        |ИЗ
        |   Документ.РеализацияТоваровУслуг КАК Реализация
        |ГДЕ
        |   Реализация.Ссылка = &СсылкаДокумента";
        
    Запрос.УстановитьПараметр("СсылкаДокумента", СсылкаДокумента);
    Выборка = Запрос.Выполнить().Выбрать();
    
    Если Выборка.Следующий() Тогда
        Результат.Вставить("Контрагент", Выборка.Контрагент);
        Результат.Вставить("НомерДоговора", Выборка.НомерДоговора);
        Результат.Вставить("ДатаДоговора", Формат(Выборка.ДатаДоговора, "ДЛФ=DD"));
        Результат.Вставить("СуммаДокумента", Выборка.СуммаДокумента);
    КонецЕсли;
    
    Возврат Результат;
    
КонецФункции

Важный нюанс: добавление пустого табличного документа через ПечатнаяФорма.ТабличныйДокумент = Новый ТабличныйДокумент; является обязательным условием для корректной работы БСП. Платформа видит, что реквизит заполнен, не прерывает работу ошибкой, а свойство ОфисныеДокументы указывает клиентскому приложению открыть и скачать сформированный файл Word.

Важные оптимизации при работе с COM-объектом Word

Проанализируем производительность заполнения шаблонов Word при использовании первого способа. Многие начинающие разработчики используют конструкции следующего вида:


Word.Bookmarks("Контрагент").Select();
Word.Application.Selection.TypeText(ЗначениеКонтрагента);

Использование метода Selection — это крайне неэффективный путь. Каждый вызов метода Select() заставляет операционную систему визуально перерисовывать курсор в окне текстового редактора. Если ваш документ содержит десятки параметров для заполнения, формирование файла будет происходить медленно, а сам экран пользователя начнет мерцать.

Чтобы избежать этой проблемы, заполняйте параметры напрямую через обращение к свойству диапазона закладки Range:

Word.Bookmarks("ИмяЗакладки").Range.Text = "Новое значение";

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

← На главную