При разработке внешних печатных форм для документов списания, оприходования или реализации перед разработчиком часто возникает задача: вывести в печатную форму не просто список товаров и их количество, но и актуальные цены из регистра сведений ЦеныНоменклатуры на дату проведения документа. Начинающие разработчики часто совершают архитектурные ошибки, которые замедляют работу системы в десятки раз или приводят к выводу неверных данных.
В этой статье мы подробно разберем, как правильно спроектировать запрос для получения цен на определенную дату, избавимся от неоптимальных запросов в цикле и научимся корректно подключать готовую печатную форму к конфигурации с использованием механизмов БСП (Библиотеки стандартных подсистем).
Давайте проанализируем код, представленный автором темы. В нем содержатся две критические ошибки, которые часто допускают новички.
Ошибка первая: запрос в цикле. Посмотрим на этот фрагмент кода:
Для Каждого ТекущаяСтрока Из СсылкаНаДокумент.Товары Цикл
ЗаполнитьЗначенияСвойств(ОбластьСтрокаТЧ.Параметры, ТекущаяСтрока);
ОбластьСтрокаТЧ.Параметры.Price = ЦенаТовара(СсылкаНаДокумент, ТекущаяСтрока.Номенклатура, ТекущаяСтрока.Характеристика);
ТабличныйДокумент.Вывести(ОбластьСтрокаТЧ);
КонецЦикла;
Внутри цикла по строкам табличной части документа вызывается функция ЦенаТовара, внутри которой каждый раз инициализируется, компилируется и выполняется новый запрос к базе данных. Представим, что в документе списания содержится 500 строк. Это значит, что сервер 1С отправит к СУБД 500 отдельных запросов. Такой подход полностью парализует производительность системы при выводе печатной формы на экран. Правило разработки в 1С: никогда не выполняйте запросы внутри циклов! Все данные должны быть получены одним-единственным запросом.
Ошибка вторая: обращение к физической таблице вместо виртуальной. В тексте запроса автор обращается к таблице РегистрСведений.ЦеныНоменклатуры и накладывает условие ЦеныНоменклатуры.Период = &Период. Физическая таблица регистра хранит только те записи, в которых цена менялась. Если цена на товар была установлена неделю назад, а документ проводится сегодня, то физической записи с сегодняшней датой в регистре просто нет, и запрос вернет пустой результат. Для получения актуальных данных на любую дату необходимо использовать виртуальную таблицу СрезПоследних.
Для получения последней установленной цены на определенный момент времени платформа 1С предоставляет виртуальную таблицу СрезПоследних. Она автоматически вычисляет наиболее актуальное значение цены, предшествующее указанной дате или совпадающее с ней.
Выясним, как правильно передавать параметры в виртуальную таблицу. Часто разработчики совершают ошибку, накладывая условия в секции ГДЕ:
// НЕПРАВИЛЬНО И НЕОПТИМАЛЬНО!
ВЫБРАТЬ
Цены.Номенклатура,
Цены.Цена
ИЗ
РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период) КАК Цены
ГДЕ
Цены.ВидЦены = &ВидЦены
При таком написании СУБД сначала сформирует срез последних цен для абсолютно всей номенклатуры, которая есть в базе данных, загрузит этот гигантский объем данных в оперативную память и только потом отфильтрует по нужному виду цены. Чтобы избежать проблем с производительностью, все отборы необходимо передавать непосредственно в параметры виртуальной таблицы:
// ПРАВИЛЬНО И ОПТИМАЛЬНО
ВЫБРАТЬ
Цены.Номенклатура,
Цены.Цена
ИЗ
РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ВидЦены = &ВидЦены) КАК Цены
Спроектируем правильный запрос, который за одно обращение к базе данных выберет все строки из табличной части документа списания и сопоставит им актуальные цены из регистра сведений. Для этого мы свяжем табличную часть документа с виртуальной таблицей среза последних цен с помощью ЛЕВОГО СОЕДИНЕНИЯ.
Рассмотрим структуру этого запроса подробнее:
ВЫБРАТЬ
СписаниеТовары.НомерСтроки КАК НомерСтроки,
СписаниеТовары.Номенклатура КАК Номенклатура,
СписаниеТовары.Характеристика КАК Характеристика,
СписаниеТовары.Количество КАК Количество,
ЕСТЬNULL(ЦеныНоменклатурыСрезПоследних.Цена, 0) КАК Цена,
ЕСТЬNULL(ЦеныНоменклатурыСрезПоследних.Цена, 0) * СписаниеТовары.Количество КАК Сумма
ИЗ
Документ.СписаниеНедостачТоваров.Товары КАК СписаниеТовары
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
&Период,
ВидЦены = &ВидЦены
И Номенклатура В
(ВЫБРАТЬ
Т.Номенклатура
ИЗ
Документ.СписаниеНедостачТоваров.Товары КАК Т
ГДЕ
Т.Ссылка = &Ссылка)) КАК ЦеныНоменклатурыСрезПоследних
ПО СписаниеТовары.Номенклатура = ЦеныНоменклатурыСрезПоследних.Номенклатура
И СписаниеТовары.Характеристика = ЦеныНоменклатурыСрезПоследних.Характеристика
ГДЕ
СписаниеТовары.Ссылка = &Ссылка
УПОРЯДОЧИТЬ ПО
НомерСтроки
Проанализируем ключевые особенности этого запроса:
СрезПоследних не только ВидЦены, но и ограничили выборку номенклатуры только теми позициями, которые присутствуют в табличной части нашего документа. Для этого применен подзапрос Номенклатура В (ВЫБРАТЬ... ГДЕ Ссылка = &Ссылка). Это гарантирует, что СУБД выберет цены только для нужных товаров, минимизируя нагрузку на сервер.NULL.ЕСТЬNULL(..., 0). Она возвращает значение цены, а если оно равно NULL, заменяет его на числовой ноль.Теперь напишем процедуру формирования печатной формы на сервере, которая будет использовать наш оптимизированный запрос и выводить данные в макет без использования циклов по метаданным документа.
&НаСервере
Функция СформироватьПечатнуюФорму(СсылкаНаДокумент, ВидЦены) Экспорт
// Получаем макет внешней обработки
ЭтотОбъектОбработки = РеквизитФормыВЗначение("Объект");
Макет = ЭтотОбъектОбработки.ПолучитьМакет("Макет");
ТабДок = Новый ТабличныйДокумент;
ТабДок.АвтоМасштаб = Истина;
// Получаем области макета
ОбластьШапка = Макет.ПолучитьОбласть("Шапка");
ОбластьСтрока = Макет.ПолучитьОбласть("СтрокаТЧ");
ОбластьПодвал = Макет.ПолучитьОбласть("Подвал");
// Заполняем параметры шапки
ОбластьШапка.Параметры.ТекстЗаголовка = "Списание недостач товаров " + СсылкаНаДокумент.Номер + " от " + Формат(СсылкаНаДокумент.Дата, "ДФ=dd.MM.yyyy");
ТабДок.Вывести(ОбластьШапка);
// Инициализируем и заполняем запрос
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СписаниеТовары.НомерСтроки КАК НомерСтроки,
| СписаниеТовары.Номенклатура.Представление КАК НоменклатураПредставление,
| СписаниеТовары.Характеристика.Представление КАК ХарактеристикаПредставление,
| СписаниеТовары.Количество КАК Количество,
| ЕСТЬNULL(ЦеныНоменклатурыСрезПоследних.Цена, 0) КАК Цена,
| ЕСТЬNULL(ЦеныНоменклатурыСрезПоследних.Цена, 0) * СписаниеТовары.Количество КАК Сумма
|ИЗ
| Документ.СписаниеНедостачТоваров.Товары КАК СписаниеТовары
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
| &Период,
| ВидЦены = &ВидЦены
| И Номенклатура В
| (ВЫБРАТЬ
| Т.Номенклатура
| ИЗ
| Документ.СписаниеНедостачТоваров.Товары КАК Т
| ГДЕ
| Т.Ссылка = &Ссылка)) КАК ЦеныНоменклатурыСрезПоследних
| ПО СписаниеТовары.Номенклатура = ЦеныНоменклатурыСрезПоследних.Номенклатура
| И СписаниеТовары.Характеристика = ЦеныНоменклатурыСрезПоследних.Характеристика
|ГДЕ
| СписаниеТовары.Ссылка = &Ссылка
|УПОРЯДОЧИТЬ ПО
| НомерСтроки";
Запрос.УстановитьПараметр("Ссылка", СсылкаНаДокумент);
// Берем цены на момент времени документа
Запрос.УстановитьПараметр("Период", СсылкаНаДокумент.Дата);
Запрос.УстановитьПараметр("ВидЦены", ВидЦены);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
ИтогоКоличество = 0;
ИтогоСумма = 0;
Пока Выборка.Следующий() Цикл
// Заполняем параметры строки
ОбластьСтрока.Параметры.Заполнить(Выборка);
ТабДок.Вывести(ОбластьСтрока);
// Накапливаем итоги
ИтогоКоличество = ИтогоКоличество + Выборка.Количество;
ИтогоСумма = ИтогоСумма + Выборка.Сумма;
КонецЦикла;
// Заполняем подвал
ОбластьПодвал.Параметры.ИтогоКоличество = ИтогоКоличество;
ОбластьПодвал.Параметры.ИтогоСумма = ИтогоСумма;
ТабДок.Вывести(ОбластьПодвал);
Возврат ТабДок;
КонецФункции
Рассмотрим важный нюанс. Наш предыдущий код отлично работает, когда мы печатаем один документ. Но что делать, если пользователь выделил в списке 10 документов за разные даты и нажал кнопку «Печать»? В этом случае передать в параметр &Период одну дату невозможно — у каждого документа она своя.
Если мы разрабатываем печатную форму с возможностью групповой печати, нам потребуется виртуальный аналог "среза последних на каждую дату". Выясним, как построить такой запрос. Мы должны сгруппировать цены по максимальному периоду, который меньше или равен дате конкретного документа:
ВЫБРАТЬ
ТоварыДокументов.Ссылка КАК Ссылка,
ТоварыДокументов.Номенклатура КАК Номенклатура,
ТоварыДокументов.Характеристика КАК Характеристика,
ТоварыДокументов.Количество КАК Количество,
МАКСИМУМ(ЦеныНоменклатуры.Период) КАК Период
ПОМЕСТИТЬ ВТ_СвязиПериодов
ИЗ
Документ.СписаниеНедостачТоваров.Товары КАК ТоварыДокументов
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК ЦеныНоменклатуры
ПО ТоварыДокументов.Номенклатура = ЦеныНоменклатуры.Номенклатура
И ТоварыДокументов.Характеристика = ЦеныНоменклатуры.Характеристика
И ЦеныНоменклатуры.ВидЦены = &ВидЦены
И ЦеныНоменклатуры.Период <= ТоварыДокументов.Ссылка.Дата
ГДЕ
ТоварыДокументов.Ссылка В(&МассивДокументов)
СГРУППИРОВАТЬ ПО
ТоварыДокументов.Ссылка,
ТоварыДокументов.Номенклатура,
ТоварыДокументов.Характеристика,
ТоварыДокументов.Количество;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_СвязиПериодов.Ссылка КАК ДокументСсылка,
ВТ_СвязиПериодов.Номенклатура КАК Номенклатура,
ВТ_СвязиПериодов.Характеристика КАК Характеристика,
ВТ_СвязиПериодов.Количество КАК Количество,
ЕСТЬNULL(РегистраторЦен.Цена, 0) КАК Цена,
ЕСТЬNULL(РегистраторЦен.Цена, 0) * ВТ_СвязиПериодов.Количество КАК Сумма
ИЗ
ВТ_СвязиПериодов КАК ВТ_СвязиПериодов
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК РегистраторЦен
ПО ВТ_СвязиПериодов.Номенклатура = РегистраторЦен.Номенклатура
И ВТ_СвязиПериодов.Характеристика = РегистраторЦен.Характеристика
И ВТ_СвязиПериодов.Период = РегистраторЦен.Period
И РегистраторЦен.ВидЦены = &ВидЦены
Этот двухэтапный запрос сначала во временной таблице ВТ_СвязиПериодов находит для каждой строки каждого документа дату последней установленной цены, которая не превышает дату документа. На втором этапе мы соединяем полученную временную таблицу с физической таблицей регистра, чтобы забрать саму цену на найденный период. Это стандартный и самый надежный паттерн для групповой печати в 1С.
Для того чтобы печатная форма автоматически открывалась из документа и не требовала ручного выбора файла или ссылки, её нужно интегрировать в механизм БСП. Посмотрим, как это сделать.
В модуле объекта внешней обработки необходимо разместить стандартную функцию регистрации:
Функция СведенияОВнешнейОбработке() Экспорт
ПараметрыРегистрации = Новый Структура;
ПараметрыРегистрации.Вставить("Вид", "ПечатнаяФорма");
МассивНазначений = Новый Массив;
МассивНазначений.Добавить("Документ.СписаниеНедостачТоваров");
ПараметрыРегистрации.Вставить("Назначение", МассивНазначений);
ПараметрыРегистрации.Вставить("Наименование", "Списание недостач товаров с ценами из РС");
ПараметрыРегистрации.Вставить("Версия", "1.0");
ПараметрыРегистрации.Вставить("БезопасныйРежим", Ложь);
ПараметрыРегистрации.Вставить("Информация", "Внешняя печатная форма с выводом цен на дату документа.");
ТаблицаКоманд = ПолучитьТаблицуКоманд();
НоваяКоманда = ТаблицаКоманд.Добавить();
НоваяКоманда.Представление = "Списание недостач (с ценами)";
НоваяКоманда.Идентификатор = "ПечатьСписанияСЦенами";
НоваяКоманда.Использование = "ВызовСерверногоМетода";
НоваяКоманда.ПоказыватьОповещение = Истина;
НоваяКоманда.Модификатор = "ПечатьMXL";
ПараметрыРегистрации.Вставить("Команды", ТаблицаКоманд);
Возврат ПараметрыРегистрации;
КонецФункции
Функция ПолучитьТаблицуКоманд()
Команды = Новый ТаблицаЗначений;
Команды.Колонки.Добавить("Представление", Новый ОписаниеТипов("Строка"));
Команды.Колонки.Добавить("Идентификатор", Новый ОписаниеТипов("Строка"));
Команды.Колонки.Добавить("Использование", Новый ОписаниеТипов("Строка"));
Команды.Колонки.Добавить("ПоказыватьОповещение", Новый ОписаниеТипов("Булево"));
Команды.Колонки.Добавить("Модификатор", Новый ОписаниеТипов("Строка"));
Возврат Команды;
КонецФункции
Также в модуле объекта объявляется экспортная процедура Печать, которая является точкой входа при вызове команды из интерфейса программы:
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
ПараметрыПечати = Новый Структура;
// Получаем описание печатной формы
ПечатнаяФорма = СистемныеМеханизмыУправленияПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "ПечатьСписанияСЦенами");
Если ПечатнаяФорма <> Неопределено Тогда
// Для примера вид цены берется из первого документа или предопределенный
ВыбранныйВидЦены = Справочники.ВидыЦен.НайтиПоНаименованию("Оптовая");
// Формируем один общий документ для всех выделенных объектов
ТабличныйДокумент = Новый ТабличныйДокумент;
Для Каждого СсылкаНаДокумент Из МассивОбъектов Цикл
Если ТабличныйДокумент.ВысотаТаблицы > 0 Тогда
ТабличныйДокумент.ВывестиГоризонтальныйРазделительСтраниц();
КонецЕсли;
МакетДокумента = СформироватьПечатнуюФорму(СсылкаНаДокумент, ВыбранныйВидЦены);
ТабличныйДокумент.Вывести(МакетДокумента);
КонецЦикла;
ПечатнаяФорма.ТабличныйДокумент = ТабличныйДокумент;
ПечатнаяФорма.СинонимМакета = "Списание недостач с ценами";
КонецЕсли;
КонецПроцедуры
Использование такого подхода гарантирует совместимость вашей обработки со всеми типовыми конфигурациями 1С на платформе 8.3 (такими как Управление торговлей, Комплексная автоматизация или ERP) и позволяет пользователям открывать форму печати в один клик прямо из командной панели документа.