Почему не передается тип цен в запрос 1С и как правильно пересчитать табличную часть документа программно?

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

При разработке в среде «1С:Предприятие 8» на базе комплексных конфигураций (таких как «Управление торговым предприятием» или «Управление производственным предприятием») программисты часто сталкиваются с неочевидным поведением запросов и ошибками при программном формировании документов. В этой статье мы подробно разберем две важнейшие темы: почему СУБД может возвращать пустой результат при передаче заполненного параметра типа цен и как правильно пересчитать суммы в табличной части создаваемого документа без обращения к элементам формы.

Часть 1. Разница между типами цен и почему не передавался параметр

Рассмотрим классическую ситуацию. Разработчик пытается получить срез последних цен в модуле объекта документа ПоступлениеТоваровУслуг. На первый взгляд код написан абсолютно верно, а отладчик подтверждает, что переменная параметра заполнена. Тем не менее, запрос возвращает пустой результат.

Давайте проанализируем исходный код запроса:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ЦеныНоменклатурыСрезПоследних.Цена
    |ИЗ
    |	РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&ТекДата, ТипЦен = &ТипЦен) КАК ЦеныНоменклатурыСрезПоследних";

Запрос.УстановитьПараметр("ТекДата", ТекДата);
Запрос.УстановитьПараметр("ТипЦен", ДоговорКонтрагента.ТипЦен);
РезультатЗапроса = Запрос.Выполнить();

Для того чтобы понять, почему этот запрос не возвращает данные, нам необходимо детально изучить структуру метаданных конфигурации. В системе «1С:Предприятие» разделяются понятия собственных цен компании (цен продажи) и цен наших поставщиков или конкурентов. Для этого используются два совершенно разных справочника:

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

Теперь обратим внимание на реквизит ТипЦен в справочнике ДоговорыКонтрагентов. Этот реквизит имеет составной тип данных, включающий в себя ссылки на оба вышеупомянутых справочника. Наполнение этого реквизита зависит от вида договора:

Поскольку логика выполняется в документе поступления (закупка у поставщика), договор имеет вид «С поставщиком». Следовательно, выражение ДоговорКонтрагента.ТипЦен возвращает ссылку на тип данных ТипыЦенНоменклатурыКонтрагентов.

В то же время в запросе обращение идет к регистру сведений ЦеныНоменклатуры, измерение ТипЦен которого жестко типизировано под справочник ТипыЦенНоменклатуры. Из-за несовпадения типов данных СУБД не может сопоставить значения, фильтр по параметру не срабатывает, и результат выборки всегда остается пустым.

Как решить проблему с типами цен правильно?

Некоторые разработчики пытаются решить эту проблему «в лоб», используя поиск по наименованию:


ТипЦенПараметр = Справочники.ТипыЦенНоменклатуры.НайтиПоНаименованию(ДоговорКонтрагента.ТипЦен.Наименование);

Важно: Подобное решение является грубым антипаттерном программирования в 1С! Поиск по наименованию крайне ненадежен: если пользователь изменит название типа цен (например, исправит опечатку), код перестанет работать. Кроме того, операция текстового поиска создает избыточную нагрузку на СУБД.

Проанализируем два корректных варианта решения проблемы в зависимости от бизнес-задачи.

Вариант А. Нам нужно получить связанную цену продажи

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


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ЦеныНоменклатурыСрезПоследних.Цена
    |ИЗ
    |	РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&ТекДата, ТипЦен = &ТипЦен) КАК ЦеныНоменклатурыСрезПоследних";

Запрос.УстановитьПараметр("ТекДата", ТекущаяДата());
// Передаем связанный тип цен компании
Запрос.УстановитьПараметр("ТипЦен", ДоговорКонтрагента.ТипЦен.ТипЦеныНоменклатуры);

РезультатЗапроса = Запрос.Выполнить();

Вариант Б. Нам нужно получить именно цену закупки от этого поставщика

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


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ЦеныНоменклатурыКонтрагентовСрезПоследних.Цена
    |ИЗ
    |	РегистрСведений.ЦеныНоменклатурыКонтрагентов.СрезПоследних(&ТекДата, ТипЦен = &ТипЦен И Номенклатура = &Номенклатура) КАК ЦеныНоменклатурыКонтрагентовСрезПоследних";

Запрос.УстановитьПараметр("ТекДата", ТекущаяДата());
Запрос.УстановитьПараметр("ТипЦен", ДоговорКонтрагента.ТипЦен);
Запрос.УстановитьПараметр("Номенклатура", ВыбНоменклатура);

РезультатЗапроса = Запрос.Выполнить();

---

Часть 2. Программное создание документов и расчет табличной части

Перейдем ко второму вопросу. Часто возникает задача программного создания документов (например, генерация нескольких документов ПоступлениеТоваровУслуг на основании одной РеализацииТоваровУслуг). При этом в создаваемых документах необходимо заполнить табличные части и рассчитать суммы, НДС и прочие реквизиты строк.

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


// Данный код вызовет ошибку при выполнении на сервере или в фоновом задании!
Процедура ПересчитатьУслуги()
    РасшифровкаВсегоЧасов = Расшифровка.Итог("КоличествоЧасов");
    ЭлементыФормы.Услуги.ТекущиеДанные.Количество = РасшифровкаВсегоЧасов;
    СтрокаТабличнойЧасти = ЭлементыФормы.Услуги.ТекущиеДанные;
    
    // Рассчитать реквизиты табличной части.
    ОбработкаТабличныхЧастей.РассчитатьСуммуTabЧасти(СтрокаТабличнойЧасти, ЭтотОбъект);
    ОбработкаТабличныхЧастей.РассчитатьСуммуНДСТабЧасти(СтрокаТабличнойЧасти, ЭтотОбъект);
КонецПроцедуры

Выясним причину неизбежного падения этого кода. Использование объекта ЭлементыФормы (или просто Элементы в управляемом интерфейсе) допустимо только в контексте открытой интерактивной формы на стороне клиента. Если вы создаете документы программно (например, в фоновом режиме, в модуле объекта или в общем модуле на сервере), никакой формы физически не существует. Обращение к ЭлементыФормы вызовет критическую ошибку «Переменная не определена (ЭлементыФормы)».

Как правильно переписать логику расчета?

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

Давайте перепишем процедуру пересчета так, чтобы её можно было использовать в любом контексте:


// Процедура может быть размещена в модуле создаваемого документа или в общем модуле
Процедура РассчитатьСтрокуТабличнойЧастиПрограммно(СтрокаТабличнойЧасти, ДокументОбъект) Экспорт
    
    // Рассчитываем сумму строки (Количество * Цена) с учетом скидок
    ОбработкаТабличныхЧастей.РассчитатьСуммуТабЧасти(СтрокаТабличнойЧасти, ДокументОбъект);
    
    // Рассчитываем сумму НДС для строки исходя из ставки НДС и суммы
    ОбработкаТабличныхЧастей.РассчитатьСуммуНДСТабЧасти(СтрокаТабличнойЧасти, ДокументОбъект);
    
КонецПроцедуры

Разбираем по шагам процесс программного создания документов

Для реализации группового или пакетного создания документов лучше всего вынести логику в общий серверный модуль или написать внешнюю обработку. Это позволит не снимать типовую конфигурацию с поддержки. Рассмотрим пошаговый алгоритм создания документов-приемников на основе документа-источника:


// Пример размещения процедуры в серверном общем модуле
Процедура СоздатьПоступленияНаОснованииРеализации(ДокументСсылкаРеализация) Экспорт
    
    // 1. Создаем новый объект документа в оперативной памяти
    НовыйДокумент = Документы.ПоступлениеТоваровУслуг.СоздатьДокумент();
    
    // Устанавливаем базовые реквизиты шапки
    НовыйДокумент.Дата = ТекущаяДата();
    НовыйДокумент.Организация = ДокументСсылкаРеализация.Организация;
    НовыйДокумент.Контрагент = ДокументСсылкаРеализация.Контрагент;
    НовыйДокумент.ДоговорКонтрагента = ДокументСсылкаРеализация.ДоговорКонтрагента;
    
    // Заполняем счета учета расчетов по умолчанию для данной конфигурации
    НовыйДокумент.ЗаполнитьСчетаУчетаРасчетов();
    
    // 2. Заполняем табличную часть "Услуги"
    Для Каждого СтрокаРеализации Из ДокументСсылкаРеализация.Услуги Цикл
        
        // Добавляем новую строку в табличную часть создаваемого объекта
        НоваяСтрока = НовыйДокумент.Услуги.Добавить();
        
        // Копируем значения совпадающих реквизитов (Номенклатура, Содержание и т.д.)
        ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаРеализации);
        
        // Задаем новое количество на основании нашего расчетного алгоритма
        НоваяСтрока.Количество = СтрокаРеализации.Количество; // Здесь может быть ваша формула расчета
        НоваяСтрока.Цена = СтрокаРеализации.Цена;
        
        // Заполняем счета учета для номенклатуры в строке
        НовыйДокумент.ЗаполнитьСчетаУчетаВСтрокеТабЧасти(НоваяСтрока, "Услуги", Истина);
        
        // 3. Вызываем расчет сумм, передавая строку объекта и сам объект документа
        РассчитатьСтрокуТабличнойЧастиПрограммно(НоваяСтрока, НовыйДокумент);
        
    КонецЦикла;
    
    // 4. Записываем и проводим документ базы данных
    Попытка
        НовыйДокумент.Записать(РежимЗаписиДокумента.Проведение);
        Сообщить("Успешно создан и проведен документ: " + НовыйДокумент.Ссылка);
    Исключение
        // Обрабатываем возможные ошибки проведения (блокировки, нехватка остатков и др.)
        НовыйДокумент.Записать(РежимЗаписиДокумента.Запись);
        ОбщегоНазначения.СообщитьОбОшибке("Документ записан, но не проведен! Ошибка: " + ОписаниеОшибки());
    КонецПопытки;
    
КонецПроцедуры

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

← На главную