Как правильно вывести розничные цены из регистра сведений в печатную форму ценников в 1С?

Программист 1С v8.3 (Управляемые формы) 1С:Розница Управленческий учет Торговля и дистрибуция
← На главную

При разработке внешних печатных форм в среде 1С:Предприятие часто возникает задача вывода данных, которых изначально нет в самом документе. Классический пример — печать ценников или этикеток напрямую из документа Поступление товаров. В документе содержатся только оптовые цены поставщика, а для покупателей на ценниках необходимо отобразить актуальные розничные цены, которые хранятся в периодическом регистре сведений ЦеныНоменклатуры.

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


Анализ исходной проблемы и ошибок в коде

Рассмотрим ситуацию, с которой столкнулся автор темы. Для вывода розничной цены был написан запрос, соединяющий табличную часть документа с виртуальной таблицей РегистрСведений.ЦеныНоменклатуры.СрезПоследних. Однако при выводе данных в макет возникла ошибка: если в документе больше одной строки, то у всех товаров выводилась одна и та же цена (последняя из выборки).

Давайте проанализируем фрагмент исходного кода, содержащий критическую ошибку:


Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    // Пустой цикл! Перебор выборки происходит вхолостую
КонецЦикла;

Для Каждого СтрокаТаблицы Из СсылкаНаОбъект.Товары Цикл
    ЗаполнитьЗначенияСвойств(ОбластьЦенника.Параметры, Выборка);
    ОбластьЦенника.Параметры.Цена2 = Выборка.Цена;
    // ... дальнейший вывод области
КонецЦикла;

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

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

После первой попытки исправления кода автор перенес заполнение параметров внутрь цикла выборки, но оставил вывод области табличного документа за его пределами. Рассмотрим эту ошибку:


Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    ЗаполнитьЗначенияСвойств(ОбластьЦенника.Параметры, Выборка);
    ОбластьЦенника.Параметры.Цена2 = Выборка.Цена;
    ОбластьЦенника.Параметры.Наимен = Выборка.Номенклатура;
КонецЦикла;

// Вывод в табличный документ остался снаружи цикла!
Если ТекСтолбец = 0 Тогда
    ТабДокумент.Вывести(ОбластьЦенника);
Иначе
    ТабДокумент.Присоединить(ОбластьЦенника);
КонецЕсли;

При такой структуре цикл выборки отрабатывает корректно, последовательно записывая значения каждого товара в параметры области ОбластьЦенника.Параметры. Но так как метод Вывести() или Присоединить() вызывается уже после завершения цикла, в печатную форму попадает только один-единственный ценник — для самого последнего товара из выборки запроса.


Правильное решение: Вывод данных внутри цикла выборки

Чтобы исправить обе ошибки, необходимо полностью синхронизировать процесс обхода выборки запроса и вывод областей в итоговый табличный документ. Перебирать строки документа через Для Каждого нет никакой необходимости, поскольку запрос уже отобрал все нужные данные непосредственно из документа и соединил их с ценами. Мы должны использовать только один цикл — по выборке результата запроса.

Реализуем корректный алгоритм вывода данных по шагам:

  1. Выполняем запрос и получаем выборку данных.
  2. Запускаем цикл Пока Выборка.Следующий().
  3. Внутри цикла заполняем параметры конкретной области макета актуальными данными текущей строки выборки.
  4. Внутри этого же цикла производим вывод области в табличный документ с помощью методов Вывести() или Присоединить().
  5. Увеличиваем счетчики строк и столбцов для позиционирования ценников на листе формата А4 (например, сетка 3 столбца на 10 строк).

Посмотрим на исправленный и рабочий вариант процедуры печати:


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

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


Критические архитектурные оптимизации решения

Проанализируем слабые стороны работающего, но неоптимального кода, и внесем необходимые правки.

1. Оптимизация работы с виртуальной таблицей СрезПоследних

В исходном запросе используется левое соединение физической таблицы документа с виртуальной таблицей РегистрСведений.ЦеныНоменклатуры.СрезПоследних без предварительной фильтрации параметров виртуальной таблицы. Это классический антипаттерн разработки 1С.

В чем опасность: СУБД (особенно под управлением PostgreSQL) при таком построении запроса может сначала попытаться рассчитать срез последних для всех записей регистра цен (коих в рабочей базе могут быть миллионы), а уже затем выполнить соединение с несколькими строками документа. Это гарантированно вызовет критическое зависание системы при попытке печати ценников.

Решение: Использование временных таблиц. Сначала мы выгружаем во временную таблицу только те товары, которые присутствуют в нашем документе, индексируем её, а затем передаем этот список в качестве фильтра в параметры виртуальной таблицы среза последних.

2. Обязательный учет характеристик номенклатуры

В конфигурации "1С:Розница 2.2" (и во многих других современных ERP-решениях) учет товаров и цен часто ведется в разрезе характеристик (размер, цвет, объем).

В чем опасность: Если соединять таблицы только по полю Номенклатура, игнорируя характеристики, то для товаров с разными характеристиками и ценами запрос вернет либо неверную цену, либо "размножит" строки в ценниках, создав дубли по декартову произведению соединяемых таблиц.

Решение: Обязательно связываем таблицы как по полю Номенклатура, так и по полю Характеристика.

3. Безопасное получение вида цен

Метод Справочники.ВидыЦен.НайтиПоНаименованию("Розничная") крайне ненадежен. Если конечный пользователь переименует розничный вид цен (например, в "Розничная (основная)"), печать ценников сломается, так как параметр запроса примет значение Неопределено или пустую ссылку.

Решение: Правильнее передавать вид цены из настроек магазина, параметров печати, либо использовать предопределенные элементы, если они предусмотрены конфигурацией.

4. Выбор правильной даты для получения цен

Использование ТекущаяДата() для среза цен не всегда логично. Документ поступления товаров мог быть оформлен и проведен в прошлом месяце. Если за это время розничные цены изменились, то при открытии старого документа печатная форма отобразит сегодняшние цены, а не те, что действовали на момент проведения документа. Целесообразно использовать дату самого документа.


Итоговый профессиональный вариант кода

С учетом всех вышеперечисленных рекомендаций и требований стандартов разработки фирмы "1С", перепишем нашу функцию печати ценников. Рассмотрим по шагам оптимизированный код запроса с использованием временных таблиц:


Функция СформироватьПечатнуюФормуОптимально(СсылкаНаОбъект, ОбъектыПечати) Экспорт
    
    ТабДокумент = Новый ТабличныйДокумент;
    Макет = ПолучитьМакет("Макет");
    
    ОбластьЦенника = Макет.ПолучитьОбласть("Строка|Столбец1");
    
    ТекСтолбец = 0;
    ТекСтрока  = 0;
    
    // Определяем дату среза цен - берем дату документа
    ДатаСреза = СсылкаНаОбъект.Дата;
    
    // В реальной практике вид цены лучше получать из настроек Магазина/Склада документа.
    // В данном примере используем безопасный поиск с проверкой на заполнение.
    ВидЦеныРозничная = Справочники.ВидыЦен.НайтиПоНаименованию("Розничная");
    Если Не ЗначениеЗаполнено(ВидЦеныРозничная) Тогда
        ВызватьИсключение "В справочнике 'Виды цен' не найден вид цены с наименованием 'Розничная'!";
    КонецЕсли;
    
    Запрос = Новый Запрос;
    Запрос.Текст =
        "ВЫБРАТЬ РАЗЛИЧНЫЕ
        |   ПоступлениеТовары.Номенклатура КАК Номенклатура,
        |   ПоступлениеТовары.Характеристика КАК Характеристика
        |ПОМЕСТИТЬ ВТ_ТоварыДокумента
        |ИЗ
        |   Документ.ПоступлениеТоваров.Товары КАК ПоступлениеТовары
        |ГДЕ
        |   ПоступлениеТовары.Ссылка = &Ссылка
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ИНДЕКСИРОВАТЬ ВТ_ТоварыДокумента.Номенклатура,
        |   ВТ_ТоварыДокумента.Характеристика
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |   ВТ_ТоварыДокумента.Номенклатура КАК Номенклатура,
        |   ВТ_ТоварыДокумента.Характеристика КАК Характеристика,
        |   ЕСТЬNULL(ЦеныНоменклатурыСрезПоследних.Цена, 0) КАК Цена
        |ИЗ
        |   ВТ_ТоварыДокумента КАК ВТ_ТоварыДокумента
        |       ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
        |               &ДатаСреза,
        |               ВидЦены = &ВидЦены
        |                   И (Номенклатура, Характеристика) В
        |                       (ВЫБРАТЬ
        |                           Т.Номенклатура,
        |                           Т.Характеристика
        |                       ИЗ
        |                           ВТ_ТоварыДокумента КАК Т)) КАК ЦеныНоменклатурыСрезПоследних
        |       ПО ВТ_ТоварыДокумента.Номенклатура = ЦеныНоменклатурыСрезПоследних.Номенклатура
        |           И ВТ_ТоварыДокумента.Характеристика = ЦеныНоменклатурыСрезПоследних.Характеристика";
        
    Запрос.УстановитьПараметр("Ссылка", СсылкаНаОбъект);
    Запрос.УстановитьПараметр("ДатаСреза", ДатаСреза);
    Запрос.УстановитьПараметр("ВидЦены", ВидЦеныРозничная);
    
    Выборка = Запрос.Выполнить().Выбрать();
    
    Пока Выборка.Следующий() Цикл
        
        // Очищаем параметры области перед заполнением во избежание наложения данных
        ОбластьЦенника.Параметры.Сбросить();
        
        // Заполняем параметры области
        ОбластьЦенника.Параметры.Цена2 = Выборка.Price; // Значение цены
        
        // Формируем красивое представление Номенклатуры с характеристикой
        ПредставлениеТовара = СокрЛП(Выборка.Номенклатура);
        Если ЗначениеЗаполнено(Выборка.Характеристика) Тогда
            ПредставлениеТовара = ПредставлениеТовара + " (" + СокрЛП(Выборка.Характеристика) + ")";
        КонецЕсли;
        
        ОбластьЦенника.Параметры.Наимен = ПредставлениеТовара;
        
        // Вывод области в табличный документ
        Если ТекСтолбец = 0 Тогда
            ТабДокумент.Вывести(ОбластьЦенника);
        Иначе
            ТабДокумент.Присоединить(ОбластьЦенника);
        КонецЕсли;
        
        // Логика формирования плитки ценников на странице
        ТекСтолбец = ТекСтолбец + 1;
        Если ТекСтолбец = 3 Тогда
            ТекСтрока  = ТекСтрока + 1;
            ТекСтолбец = 0;
        КонецЕсли;
        
        Если ТекСтрока = 10 Тогда
            ТекСтрока = 0;
            ТабДокумент.ВывестиГоризонтальныйРазделительСтраниц();
        КонецЕсли;              
        
    КонецЦикла;
    
    Возврат ТабДокумент;
    
КонецФункции

Проанализируем ключевые преимущества оптимизированного варианта:

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

← На главную