Итак, не буду ходить вокруг да около, сразу к делу. Большинство программистов выводят печатные формы по одному из двух алгоритмов:
-
собираем данные на ходу и выводим макет (ну прямо совсем дремучий способ)
-
собираем данные по массиву объектов в коллекцию (в основном соответствие), обходим массив объектов и формируем макет (смотрим типовые - если повезет так и будет)
Я вообще редко видел другие варианты вывода на печать. А тут еще и задача интересная прилетела. Мол скажи, Саня. А какие параметры мне доступны в этой печатной форме (при изменении макета). Ну я код посмотрел, на письмо ответил и пожалел. Дальше было больше: покажи, расскажи. А это можно? А то? А почему не можно? А сейчас сломалось...
И сразу скажу, если речь про типовые - то ниже написанное вам не поможет. Там вообще редко что поможет, кроме как (все взорвать) переписать заново. Весь подход описанный в данной статье полезен будет тем, кто делает свои печатки.
Введение
Любой макет печатной формы состоит из разделов (ну или областей). Они в свою очередь уже будут содержать собственные параметры для вывода данных (а также параметры расшифровки и параметры картинок).
И по сути, при выводе печатной формы, мы должны подготовить набор этих параметров для заполнения в определенную область, получить ее (область) из макета и вывести в табличный документ. Здесь я надеюсь никому Америку не открыл и новостей не рассказал.
Сначала создадим общую процедуру подготовки печатных форм.
Функция Печать_ИмяМакета(МассивОбъектов, ОбъектыПечати, ИмяМакета)
ДанныеДляПечати = ПолучитьДанныеДляПечатиОбъектов(МассивОбъектов, "Шапка,Контрагенты,ТабличнаяЧасть1,ТабличнаяЧасть2");
ТабличныйДокумент = Новый ТабличныйДокумент;
ТабличныйДокумент.ИмяПараметровПечати = "ПАРАМЕТРЫ_ПЕЧАТИ_ИмяМакета";
ТабличныйДокумент.АвтоМасштаб = Истина;
ТабличныйДокумент.ОриентацияСтраницы = ОриентацияСтраницы.Портрет;
// и другие настройки на свой вкус, цвет и требования
ПервыйДокумент = Истина;
Для Каждого ДокументСсылка Из МассивОбъектов Цикл
Если НЕ ПервыйДокумент Тогда
ТабличныйДокумент.ВывестиГоризонтальныйРазделительСтраниц();
КонецЕсли;
ПервыйДокумент = Ложь;
// Запомним номер строки, с которой начали выводить текущий документ.
НомерСтрокиНачало = ТабличныйДокумент.ВысотаТаблицы + 1;
// Формирование печатной формы по отдельному документу
Печать_ИмяМакета_ПоДокументу(ТабличныйДокумент, ДанныеДляПечати, ДокументСсылка);
// В табличном документе зададим имя области, в которую был
// выведен объект. Нужно для возможности печати покомплектно.
УправлениеПечатью.ЗадатьОбластьПечатиДокумента(ТабличныйДокумент,
НомерСтрокиНачало, ОбъектыПечати, ДокументСсылка);
КонецЦикла;
Возврат ТабличныйДокумент;
КонецФункции
Вот теперь будем ее разбирать по кусочкам, со всеми потрохами…
Получение данных
Признаком хорошего тона будет возможность формировать печатную форму сразу по списку документов. А значит, с точки зрения минимазации обращений к БД, неразумно выполнять расчет однотипных данных для каждого документа в цикле. И расчет необходимо выполнить для всего массива разом. Казалось бы, ну выполни запрос с условием "В" и будет тебе счастье.
ВЫБРАТЬ * ИЗ Документ.ИмяДокумента КАК Т ГДЕ Т.Ссылка В (&МассивОбъектов)
Так то оно так, но вот сбор однотипных данных тоже по хорошему можно оптимизировать. Контрагенты, склады, номенклатура, данные физ. лиц, контактная информация - многие данные для вывода подобных представлений собираются с использованием общих процедур и как правило на 1 ссылку. Крайне редко я видел кэширование полученных данных. И вроде можно попробовать объяснить это необходимостью получения периодических данных. У контрагентов - наименования. У физиков - ФИО (ох уж эти права граждан). Но на самом деле все не совсем так.
Самый яркий пример - Контрагенты (Организации). Для начала, из документа в документ они будут повторяться. И из периодической информации там только Наименование, ИНН/КПП, адресная информация (может быть и больше, но не важно). Так что, предполагая что выводится не одна печатная форма, задумайтесь хотя бы над кэшированием данных непериодических данных. Хотя мое мнение, получите все данные (включая периодику) одним запросом. Методы унифицируйте и забудьте про них раз и на всегда.
Периодические поля
С периодическими полями можно пойти двумя путями:
-
подготовить временную таблицу с историей данных за период (минимальная и максимальная граница документов) + общий метод для получения данных из временной таблицы
-
выполнить сбор необходимых данных сразу. Ниже представлен запрос на получение данных наименований по контрагентам на каждый период. Аналогично собираются данные по КПП, адресам и др. периодическим данным.
ВЫБРАТЬ
Т.Контрагент КАК Контрагент,
МИНИМУМ(Т.Дата) КАК МинПериод,
МАКСИМУМ(Т.Дата) КАК МаксПериод
ПОМЕСТИТЬ ВТ_Контрагенты
ИЗ
Документ.ИмяДокумента КАК Т
ГДЕ
Т.Ссылка В (&МассивОбъектов)
СГРУППИРОВАТЬ ПО
Т.Контрагент
ИНДЕКСИРОВАТЬ ПО
Контрагент,
МаксПериод
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ИсторияНаименований.Ссылка КАК Контрагент,
ИсторияНаименований.Период КАК Период,
ИсторияНаименований.НаименованиеПолное КАК НаименованиеПолное
ПОМЕСТИТЬ ВТ_ИсторияНаименований
ИЗ
Справочник.Контрагенты.ИсторияНаименований КАК ИсторияНаименований
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Контрагенты КАК ВТ_Контрагенты
ПО ИсторияНаименований.Ссылка = ВТ_Контрагенты.Контрагент
И ИсторияНаименований.Период <= ВТ_Контрагенты.МаксПериод
ИНДЕКСИРОВАТЬ ПО
Контрагент,
Период
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВложенныйЗапрос.Контрагент КАК Контрагент,
ВложенныйЗапрос.Период КАК Период,
ВложенныйЗапрос.ПериодНаименований КАК ПериодНаименований,
ЕСТЬNULL(ИсторияНаименований.НаименованиеПолное, "") КАК НаименованиеПолное
ПОМЕСТИТЬ ВТ_Наименования
ИЗ
(ВЫБРАТЬ
Т1.Контрагент КАК Контрагент,
Т1.Период КАК Период,
МАКСИМУМ(Т2.Период) КАК ПериодНаименований
ИЗ
ВТ_ТаблицаДанных КАК Т1
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ИсторияНаименований КАК Т2
ПО Т1.Контрагент = Т2.Контрагент
И Т1.Период >= Т2.Период
СГРУППИРОВАТЬ ПО
Т1.Контрагент,
Т1.Период) КАК ВложенныйЗапрос
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ИсторияНаименований КАК ИсторияНаименований
ПО ВложенныйЗапрос.Контрагент = ИсторияНаименований.Контрагент
И ВложенныйЗапрос.ПериодНаименований = ИсторияНаименований.Период
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ТаблицаДанных.Контрагент КАК Контрагент,
ТаблицаДанных.Период КАК Период,
Контрагенты.ЮридическоеФизическоеЛицо КАК ЮридическоеФизическоеЛицо,
Контрагенты.Наименование КАК Наименование,
ВЫБОР
КОГДА ЕСТЬNULL(ДанныеНаименований.НаименованиеПолное, "") <> ""
ТОГДА ДанныеНаименований.НаименованиеПолное
ИНАЧЕ Контрагенты.НаименованиеПолное
КОНЕЦ КАК НаименованиеПолное,
Контрагенты.ИНН КАК ИНН,
Контрагенты.КПП КАК КПП,
Контрагенты.КодПоОКПО КАК КодПоОКПО,
Контрагенты.ОГРН КАК ОГРН,
Контрагенты.СтранаРегистрации КАК СтранаРегистрации,
Контрагенты.НомерГосударственнойРегистрации КАК НомерГосударственнойРегистрации,
Контрагенты.ДатаГосударственнойРегистрации КАК ДатаГосударственнойРегистрации,
ЕСТЬNULL(ДанныеНаименований.ПериодНаименований, ДАТАВРЕМЯ(1, 1, 1)) КАК ДатаНаименований
ИЗ
ВТ_ТаблицаДанных КАК ТаблицаДанных
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты
ПО ТаблицаДанных.Контрагент = Контрагенты.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Наименования КАК ДанныеНаименований
ПО ТаблицаДанных.Контрагент = ДанныеНаименований.Контрагент
И ТаблицаДанных.Период = ДанныеНаименований.Период
В представленном запросе, сбор ВТ_Контрагенты может быть оставлен в модуле менеджера объекта печати, а остальной сбор вынесен в общий модуль. Ну или идем по пути ЗУП и готовим туеву гору временных таблиц и потом используем в модуле менеджера. Хотя есть наверняка и другие более интересные и эффективные пути - буду рад выслушать. Но главное тут - думайте о том, какие данные можно собрать эффективно и единоразово.
Еще следует обратить внимание на процедуру ПолучитьДанныеДляПечатиОбъектов. Независимо от количества макетов для одного объекта метаданных, рекомендую сбор данных выполнять однотипно. А избыточность сбора для конкретного макета регулировать указанием имен наборов для подготовки (см. параметр 2 в вызове метода). Пример метода:
Функция ПолучитьДанныеДляПечатиОбъектов(знач МассивОбъектов, знач НаборДанных = "")
ИсточникиДанных = Новый Структура;
ИсточникиДанных.Вставить("Шапка" , "ПолучитьДанныеПечатиШапки");
ИсточникиДанных.Вставить("Контрагенты" , "ПолучитьДанныеПечатиКонтрагентов");
ИсточникиДанных.Вставить("ТабличнаяЧасть1" , "ПолучитьДанныеПечатиТабличнаяЧасть1");
ИсточникиДанных.Вставить("ТабличнаяЧасть2" , "ПолучитьДанныеПечатиТабличнаяЧасть2");
// ключ - имя набора данных
// значение - имя функции получения данных с единственным параметром МассивОбъектов
Возврат СформироватьДанныеДляПечати(МассивОбъектов, НаборДанных, ИсточникиДанных);
КонецФункции
Функция СформироватьДанныеДляПечати(знач МассивОбъектов, знач НаборДанных = "", знач ИсточникиДанных)
ДанныеДляПечати = Новый Структура;
// если набор не указан, выполним сбор всех доступных данных источника
Если НЕ ЗначениеЗаполнено(НаборДанных) Тогда
МассивНабора = Новый Массив;
Для Каждого КлючИЗначение Из ИсточникиДанных Цикл
МассивНабора.Добавить(КлючИЗначение.Ключ);
КонецЦикла;
Иначе
МассивНабора = СтрРазделить(НаборДанных, ",");
КонецЕсли;
Обработано = Новый Массив;
Для Каждого ИмяНабора Из МассивНабора Цикл
ИмяНабора = СокрЛП(ИмяНабора);
Если Обработано.Найти(ИмяНабора) <> Неопределено Тогда
Продолжить;
КонецЕсли;
ИмяМетода = ОбщегоНазначенияКлиентСервер.СвойствоСтруктуры(ИсточникиДанных, ИмяНабора, "");
Если НЕ ПустаяСтрока(ИмяМетода) Тогда
ДанныеНабора = Неопределено;
Выполнить СтрШаблон("ДанныеНабора = %1(МассивОбъектов);", ИмяМетода);
ДанныеДляПечати.Вставить(ИмяНабора, ДанныеНабора);
КонецЕсли;
Обработано.Добавить(ИмяНабора);
КонецЦикла;
Возврат ДанныеДляПечати;
КонецФункции
Результатом сбора данных рекомендуется возвращать в коллекции Соответствие. Где ключ - ссылка на объект печати, значение - набор требуемых данных. Не забываем, что при подготовке данных хорошим тоном будет возвращать набор простых типов для вывода в макет, чтобы не нагружать систему получением представлений ссылочных значений. Тем более, не всегда надо выводить именно представление (не забываем, что представление может быть изменено событием менеджера объекта ОбработкаПолученияПредставления).
Вывод печатной формы по ссылке
Собственно теперь разберем принцип компоновки метода Печать_ИмяМакета_ПоДокументу. Данный метод, в качестве параметров принимает:
-
ТабличныйДокумент - табличный документ для вывода результата печатной формы
-
ДанныеДляПечати - структура - набор данных по всем объектам (см. ПолучитьДанныеДляПечатиОбъектов, СформироватьДанныеДляПечати)
-
ключ - имя набора данных
-
значение - набор данных
-
-
ДокументСсылка - ссылка на элемент данных, по которому требуется вывести данные
Пример содержимого метода:
Процедура Печать_ИмяМакета_ПоДокументу(ТабличныйДокумент, ДанныеДляПечати, ДокументСсылка)
ДанныеДокумента = ПолучитьДанныеДляПечатиОбъекта(ДанныеДляПечати, ДокументСсылка);
ДанныеОбластей = Печать_ИмяМакета_ДанныеОбластей(ДанныеДокумента);
Макет = УправлениеПечатью.МакетПечатнойФормы("Документ.ИмяДокумента.ПФ_MXL_ИмяМакета");
// получение данных из ДанныеОбластей лучше выполнять через ОбщегоНазначенияКлиентСервер.СвойствоСтруктуры
// в таком случае мы можем быть защищены от ошибок и печатная форма будет сформирована, частично или полностью
ОбластьЗаголовок = Макет.ПолучитьОбласть("Заголовок");
ЗаполнитьЗначенияСвойств(ОбластьЗаголовок.Параметры, ДанныеОбластей.Заголовок);
ТабличныйДокумент.Вывести(ОбластьЗаголовок);
ОбластьШапка = Макет.ПолучитьОбласть("Шапка");
ЗаполнитьЗначенияСвойств(ОбластьШапка.Параметры, ДанныеОбластей.Шапка);
ТабличныйДокумент.Вывести(ОбластьШапка);
Для Каждого ДанныеСтроки Из ДанныеОбластей.Строки Цикл
ОбластьСтрока = Макет.ПолучитьОбласть("Строка");
ЗаполнитьЗначенияСвойств(ОбластьСтрока.Параметры, ДанныеСтроки);
ТабличныйДокумент.Вывести(ОбластьСтрока);
КонецЦикла;
ОбластьПодвал = Макет.ПолучитьОбласть("Подвал");
ЗаполнитьЗначенияСвойств(ОбластьПодвал.Параметры, ДанныеОбластей.Шапка);
ТабличныйДокумент.Вывести(ОбластьПодвал);
КонецПроцедуры
// Возвращает коллекцию данных для печати по указанному объекту
//
// Параметры:
// ДанныеДляПечати - Структура - данные для печати
// Ключ - Строка - имя коллекции
// Значение - Соответствие - значение данных по каждому объекту
// ОбъектСсылка - ЛюбаяСсылка - объект для получения данных
//
// Возвращаемое значение:
// Структура - имена совпадают с именами ключей параметра ДанныеДляПечати
//
Функция ПолучитьДанныеДляПечатиОбъекта(знач ДанныеДляПечати, знач ОбъектСсылка, знач Исключения = "") Экспорт
МассивИсключений = Новый Массив;
Если НЕ ПустаяСтрока(Исключения) Тогда
МассивИсключений = СтрРазделить(Исключения, ",");
КонецЕсли;
// предполагаем что шапка есть всегда
ДанныеДокумента = Новый Структура;
ДанныеДокумента.Вставить("Шапка", ДанныеДляПечати.Шапка.Получить(ОбъектСсылка));
Для Каждого КлючИЗначение Из ДанныеДляПечати Цикл
Если КлючИЗначение.Ключ = "Шапка" Тогда
Продолжить;
КонецЕсли;
// если значение это не соответствие (например общие данные без разделения по ссылкам), то вставляем как есть
// в противном случае предполагаем что это соответствие, где ключ - ссылка, значение - соотв. набор данных
Если МассивИсключений.Найти(КлючИЗначение.Ключ) <> Неопределено ИЛИ НЕ ТипЗнч(КлючИЗначение.Значение) = Тип("Соответствие") Тогда
ДанныеДокумента.Вставить(КлючИЗначение.Ключ, КлючИЗначение.Значение);
Иначе
ДанныеДокумента.Вставить(КлючИЗначение.Ключ, КлючИЗначение.Значение.Получить(ОбъектСсылка));
КонецЕсли;
КонецЦикла;
Возврат ДанныеДокумента;
КонецФункции
Как вы можете увидеть, в это методе мы получаем данные уже по конкретной ссылке (метод ПолучитьДанныеДляПечатиОбъекта). Далее формируем структуру с набором данных по каждой области, ну и дальше уже выводим каждую область. Я сознательно не упрощал процедуру вывода областей в ТД. По идее, области можно было просто перечислить, предполагая что возвращаемый набор будет идентичен, и обойти Для Каждого. Тут уже сами решайте, насколько детально вы бы хотели видеть вывод областей. Мне так не мешает, но создает наглядность порядку вывода.
Подготовка данных для областей
Теперь коснемся метода Печать_ИмяМакета_ДанныеОбластей. Сам метод может быть достаточно простой. Например:
Функция Печать_ИмяМакета_ДанныеОбластей(ДанныеДокумента)
ДанныеОбластей = Новый Структура;
ДанныеОбластей.Вставить("Заголовок" , ДанныеДокумента.Шапка);
ДанныеОбластей.Вставить("Шапка" , ДанныеДокумента.Шапка);
ДанныеОбластей.Вставить("Строки" , ДанныеДокумента.ТабличнаяЧасть1);
ДанныеОбластей.Вставить("Подвал" , ДанныеДокумента.Шапка);
Печать_ИмяМакета_ДанныеОбластейПереопределяемый(ДанныеОбластей, ДанныеДокумента);
Возврат ДанныеОбластей;
КонецФункции
Процедура Печать_ИмяМакета_ДанныеОбластейПереопределяемый(ДанныеОбластей, ДанныеДокумента)
// здесь может быть ваша реклама, но лучше разместить код переопределения типового поведения
КонецПроцедуры
В целом, именно в данном методе мы должны сформировать наиболее полные структуры с данными для каждой области макета. Выделение отдельного метода позволяет нам управлять этой логикой независимо от логики вывода печатной формы. Вносить изменения в этот метод в целом не требуется, поскольку для этого есть специальная процедура Печать_ИмяМакета_ДанныеОбластейПереопределяемый. Опять таки, данный отдельный метод, позволяет нам легко изменять его в расширении не переживая за кардинальные нарушения работоспособности печатной формы.
Покажем пользователю что можно?
Мы можем создать специальный публичный метод в модуле менеджера объекта, например ПолучитьДоступныеРеквизитыМакетаПечатнойФормы. Данный метод в качестве параметра может принимать имя макета, ссылку на объект для формирования данных. Вызывать необходимый метод Печать_ИмяМакета_ДанныеОбластей (соответствующий имени макета), пример реализации подобного метода
Функция ПолучитьДоступныеРеквизитыМакетаПечатнойФормы(знач ИмяМакета, знач ДокументСсылка) Экспорт
МассивОбъектов = ОбщегоНазначенияКлиентСервер.ЗначениеВМассиве(ДокументСсылка);
ДанныеДляПечати = ПолучитьДанныеДляПечатиОбъектов(МассивОбъектов); // если есть отдельный метод определяющий набор по имени макета - отлично, иначе собираем все
ДанныеПоДокументу = ПолучитьДанныеДляПечатиОбъекта(ДанныеДляПечати, ДокументСсылка);
Если ИмяМакета = "ИмяМакета" Тогда
ДанныеОбластей = Печать_ИмяМакета_ДанныеОбластей(ДанныеДокумента);
КонецЕсли;
Возврат ДанныеОбластей;
КонецФункции
Ну и финальным штрихом, нам надо разместить команду, которая по выбранному документу может показать содержимое структуры ДанныеОбластей (например в виде дерева Раздел / Имя параметра / Значение). И тут мы в принципе уже вполне можем предложить пользователю самому достаточно гибко влиять на макет печатной формы. Сам макет может быть перенастроен при помощи стандартного функционала подсистемы УправлениеПечатью из БСП, а допустимые параметры пользователь сможет “подсмотреть” при помощи нашей команды.
Подведем итог
На первый взгляд может показаться, предложенный подход несколько избыточен, однако он позволяет нам решить целый круг задач:
-
вывод на печать форм сразу по нескольким объектам
-
подготовка данных единым образом с гибким влиянием на состав данных
-
возможность расширять состав доступных реквизитов при помощи расширений или переопределяемых методов
-
четкое разделение “зон ответственности” между методами
-
единый подход к формированию любых печатных форм, с целью максимально простого восприятия своего и чужого кода
-
соблюдая несколько простых правил, мы решаем две задачи:
-
подготовка печатной формы
-
формирование представления доступных параметров макета
-
-
расширение механизмов БСП без вмешательства в саму подсистему
Наши доработки и предложение в 1С
В нашей конфигурации (о которой, возможно, я расскажу в другой раз) мы позволили себе внести несколько изменений в типовую подсистему УправлениеПечатью:
-
Добавили вызов собственных методов в события ПриСозданииНаСервере, ПриОткрытии + пара других процедур в общую форму “РедактированиеТабличногоДокумента”. Цель - показывать доступные параметры при редактировании самого макета, так удобнее.
-
Добавили регистр для хранения дополнительных версий макетов в привязке к некоторым справочникам (Организации + другие) + период действия макета + несколько возможность хранения нескольких вариантов. В документах, для которых разрешено переопределять макеты, добавили команду позволяющую назначить любой из вариантов макета. Если этого не сделано принудительно, система определит действующий на дату документа макет, с учетом привязки к организации и другим реквизитам. Ну а если определить не сможет, выведет измененный типовой или исходный вариант. Беспроигрышная комбинация.
На скриншоте как раз показан результат наших доработок. Вообще подобные изменения можно реализовать через расширение. Я не буду загадывать, появится ли оно, по идее портировать не представляет сложности.
Но давайте поступим, как это делается у “блогеров”. Если эта статья соберет 100 плюсиков, я такое расширение сделаю и даже его выложу. Правда останется вопрос, а готовы ли вы, переписать свои печатные формы под предложенную методику?
А может кто поспособствует и поможет мне выйти на 1С? Я лично готов внести изменения в БСП, сделать документацию и предоставить этот код в безвозмездное включение в библиотеку )))).
Пы.сы.
Тут оказывается 1С в БСП 3.1.6 добавила подобный механизм. Предполагается наличие СКД "ДанныеПечати" и механизм позволяет создать полностью новый макет печатной формы, на основании этих данных. Цитата документации:
- Для автоматической компоновки печатной формы по макету и данным печати в формате табличного документа см. раздел Разработка команды печати для автоматической компоновки печатной формы по макету и данным объекта. Данный способ компоновки следует использовать в качестве основного. - НОВОВВЕДЕНИЕ
- Для формирования печатной формы в формате табличного документа или комплекта табличных документов в коде см. раздел Разработка процедуры «Печать». Данный способ формирования следует использовать для сложных печатных форм, для которых автоматический способ компоновки не подходит. Для макетов таких печатных форм будут действовать ограничения в части редактирования их пользователями. В частности, будет отсутствовать список доступных реквизитов, а также не будет возможности создания пользовательского макета путем копирования поставляемого. - ПРЕДЛАГАЕМЫЕ ИЗМЕНЕНИЯ ПО ТЕМЕ СТАТЬИ
См. также
Для тех кто не знаком, не помнит что там да как в УправлениеПечатью - полезная статья как использовать подсистему Управление печатью БСП (не забываем сказать спасибо автору - quazare).
Ранние публикации
- Управление видимостью, доступностью и просмотром реквизитов формы (добавлено 24.09.17, изменено 14.06.18)
- Проверка изменений значений реквизитов формы (добавлено 24.09.17)
- Подсистема "Помощник заполнения" (добавлено 20.07.18)