При разработке в среде «1С:Предприятие 8» на базе комплексных конфигураций (таких как «Управление торговым предприятием» или «Управление производственным предприятием») программисты часто сталкиваются с неочевидным поведением запросов и ошибками при программном формировании документов. В этой статье мы подробно разберем две важнейшие темы: почему СУБД может возвращать пустой результат при передаче заполненного параметра типа цен и как правильно пересчитать суммы в табличной части создаваемого документа без обращения к элементам формы.
Рассмотрим классическую ситуацию. Разработчик пытается получить срез последних цен в модуле объекта документа ПоступлениеТоваровУслуг. На первый взгляд код написан абсолютно верно, а отладчик подтверждает, что переменная параметра заполнена. Тем не менее, запрос возвращает пустой результат.
Давайте проанализируем исходный код запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныНоменклатурыСрезПоследних.Цена
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&ТекДата, ТипЦен = &ТипЦен) КАК ЦеныНоменклатурыСрезПоследних";
Запрос.УстановитьПараметр("ТекДата", ТекДата);
Запрос.УстановитьПараметр("ТипЦен", ДоговорКонтрагента.ТипЦен);
РезультатЗапроса = Запрос.Выполнить();
Для того чтобы понять, почему этот запрос не возвращает данные, нам необходимо детально изучить структуру метаданных конфигурации. В системе «1С:Предприятие» разделяются понятия собственных цен компании (цен продажи) и цен наших поставщиков или конкурентов. Для этого используются два совершенно разных справочника:
Справочник.ТипыЦенНоменклатуры — содержит типы цен, которые компания использует для продажи товаров клиентам (например: Оптовая, Розничная, Дилерская). Данные по этим ценам хранятся в регистре сведений ЦеныНоменклатуры.Справочник.ТипыЦенНоменклатурыКонтрагентов — содержит типы цен, которые устанавливают наши поставщики. Этот справочник подчинен справочнику Контрагенты. Данные по этим ценам хранятся в регистре сведений ЦеныНоменклатурыКонтрагентов.Теперь обратим внимание на реквизит ТипЦен в справочнике ДоговорыКонтрагентов. Этот реквизит имеет составной тип данных, включающий в себя ссылки на оба вышеупомянутых справочника. Наполнение этого реквизита зависит от вида договора:
ТипыЦенНоменклатуры.ТипыЦенНоменклатурыКонтрагентов.Поскольку логика выполняется в документе поступления (закупка у поставщика), договор имеет вид «С поставщиком». Следовательно, выражение ДоговорКонтрагента.ТипЦен возвращает ссылку на тип данных ТипыЦенНоменклатурыКонтрагентов.
В то же время в запросе обращение идет к регистру сведений ЦеныНоменклатуры, измерение ТипЦен которого жестко типизировано под справочник ТипыЦенНоменклатуры. Из-за несовпадения типов данных СУБД не может сопоставить значения, фильтр по параметру не срабатывает, и результат выборки всегда остается пустым.
Некоторые разработчики пытаются решить эту проблему «в лоб», используя поиск по наименованию:
ТипЦенПараметр = Справочники.ТипыЦенНоменклатуры.НайтиПоНаименованию(ДоговорКонтрагента.ТипЦен.Наименование);
Важно: Подобное решение является грубым антипаттерном программирования в 1С! Поиск по наименованию крайне ненадежен: если пользователь изменит название типа цен (например, исправит опечатку), код перестанет работать. Кроме того, операция текстового поиска создает избыточную нагрузку на СУБД.
Проанализируем два корректных варианта решения проблемы в зависимости от бизнес-задачи.
В типовых конфигурациях справочник ТипыЦенНоменклатурыКонтрагентов имеет специальный реквизит ТипЦеныНоменклатуры, который как раз указывает на связанный тип цен нашей компании. Мы можем напрямую обратиться к этому реквизиту через точку от объекта договора:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныНоменклатурыСрезПоследних.Цена
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&ТекДата, ТипЦен = &ТипЦен) КАК ЦеныНоменклатурыСрезПоследних";
Запрос.УстановитьПараметр("ТекДата", ТекущаяДата());
// Передаем связанный тип цен компании
Запрос.УстановитьПараметр("ТипЦен", ДоговорКонтрагента.ТипЦен.ТипЦеныНоменклатуры);
РезультатЗапроса = Запрос.Выполнить();
Если задача состоит в получении закупочной цены, которую нам установил поставщик, то мы должны обращаться не к регистру ЦеныНоменклатуры, а к специализированному регистру ЦеныНоменклатурыКонтрагентов. В этом случае типы данных совпадут идеально:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныНоменклатурыКонтрагентовСрезПоследних.Цена
|ИЗ
| РегистрСведений.ЦеныНоменклатурыКонтрагентов.СрезПоследних(&ТекДата, ТипЦен = &ТипЦен И Номенклатура = &Номенклатура) КАК ЦеныНоменклатурыКонтрагентовСрезПоследних";
Запрос.УстановитьПараметр("ТекДата", ТекущаяДата());
Запрос.УстановитьПараметр("ТипЦен", ДоговорКонтрагента.ТипЦен);
Запрос.УстановитьПараметр("Номенклатура", ВыбНоменклатура);
РезультатЗапроса = Запрос.Выполнить();
---
Перейдем ко второму вопросу. Часто возникает задача программного создания документов (например, генерация нескольких документов ПоступлениеТоваровУслуг на основании одной РеализацииТоваровУслуг). При этом в создаваемых документах необходимо заполнить табличные части и рассчитать суммы, НДС и прочие реквизиты строк.
Посмотрим на типичную ошибку начинающих разработчиков при попытке реализовать расчет параметров строки:
// Данный код вызовет ошибку при выполнении на сервере или в фоновом задании!
Процедура ПересчитатьУслуги()
РасшифровкаВсегоЧасов = Расшифровка.Итог("КоличествоЧасов");
ЭлементыФормы.Услуги.ТекущиеДанные.Количество = РасшифровкаВсегоЧасов;
СтрокаТабличнойЧасти = ЭлементыФормы.Услуги.ТекущиеДанные;
// Рассчитать реквизиты табличной части.
ОбработкаТабличныхЧастей.РассчитатьСуммуTabЧасти(СтрокаТабличнойЧасти, ЭтотОбъект);
ОбработкаТабличныхЧастей.РассчитатьСуммуНДСТабЧасти(СтрокаТабличнойЧасти, ЭтотОбъект);
КонецПроцедуры
Выясним причину неизбежного падения этого кода. Использование объекта ЭлементыФормы (или просто Элементы в управляемом интерфейсе) допустимо только в контексте открытой интерактивной формы на стороне клиента. Если вы создаете документы программно (например, в фоновом режиме, в модуле объекта или в общем модуле на сервере), никакой формы физически не существует. Обращение к ЭлементыФормы вызовет критическую ошибку «Переменная не определена (ЭлементыФормы)».
Программный расчет должен оперировать исключительно объектами базы данных и их коллекциями строк, абстрагируясь от элементов интерфейса. Мы должны передавать в качестве параметров конкретную строку табличной части создаваемого объекта (тип ДокументТабличнаяЧастьСтрока) и сам объект документа (тип ДокументОбъект).
Давайте перепишем процедуру пересчета так, чтобы её можно было использовать в любом контексте:
// Процедура может быть размещена в модуле создаваемого документа или в общем модуле
Процедура РассчитатьСтрокуТабличнойЧастиПрограммно(СтрокаТабличнойЧасти, ДокументОбъект) Экспорт
// Рассчитываем сумму строки (Количество * Цена) с учетом скидок
ОбработкаТабличныхЧастей.РассчитатьСуммуТабЧасти(СтрокаТабличнойЧасти, ДокументОбъект);
// Рассчитываем сумму НДС для строки исходя из ставки НДС и суммы
ОбработкаТабличныхЧастей.РассчитатьСуммуНДСТабЧасти(СтрокаТабличнойЧасти, ДокументОбъект);
КонецПроцедуры
Для реализации группового или пакетного создания документов лучше всего вынести логику в общий серверный модуль или написать внешнюю обработку. Это позволит не снимать типовую конфигурацию с поддержки. Рассмотрим пошаговый алгоритм создания документов-приемников на основе документа-источника:
// Пример размещения процедуры в серверном общем модуле
Процедура СоздатьПоступленияНаОснованииРеализации(ДокументСсылкаРеализация) Экспорт
// 1. Создаем новый объект документа в оперативной памяти
НовыйДокумент = Документы.ПоступлениеТоваровУслуг.СоздатьДокумент();
// Устанавливаем базовые реквизиты шапки
НовыйДокумент.Дата = ТекущаяДата();
НовыйДокумент.Организация = ДокументСсылкаРеализация.Организация;
НовыйДокумент.Контрагент = ДокументСсылкаРеализация.Контрагент;
НовыйДокумент.ДоговорКонтрагента = ДокументСсылкаРеализация.ДоговорКонтрагента;
// Заполняем счета учета расчетов по умолчанию для данной конфигурации
НовыйДокумент.ЗаполнитьСчетаУчетаРасчетов();
// 2. Заполняем табличную часть "Услуги"
Для Каждого СтрокаРеализации Из ДокументСсылкаРеализация.Услуги Цикл
// Добавляем новую строку в табличную часть создаваемого объекта
НоваяСтрока = НовыйДокумент.Услуги.Добавить();
// Копируем значения совпадающих реквизитов (Номенклатура, Содержание и т.д.)
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаРеализации);
// Задаем новое количество на основании нашего расчетного алгоритма
НоваяСтрока.Количество = СтрокаРеализации.Количество; // Здесь может быть ваша формула расчета
НоваяСтрока.Цена = СтрокаРеализации.Цена;
// Заполняем счета учета для номенклатуры в строке
НовыйДокумент.ЗаполнитьСчетаУчетаВСтрокеТабЧасти(НоваяСтрока, "Услуги", Истина);
// 3. Вызываем расчет сумм, передавая строку объекта и сам объект документа
РассчитатьСтрокуТабличнойЧастиПрограммно(НоваяСтрока, НовыйДокумент);
КонецЦикла;
// 4. Записываем и проводим документ базы данных
Попытка
НовыйДокумент.Записать(РежимЗаписиДокумента.Проведение);
Сообщить("Успешно создан и проведен документ: " + НовыйДокумент.Ссылка);
Исключение
// Обрабатываем возможные ошибки проведения (блокировки, нехватка остатков и др.)
НовыйДокумент.Записать(РежимЗаписиДокумента.Запись);
ОбщегоНазначения.СообщитьОбОшибке("Документ записан, но не проведен! Ошибка: " + ОписаниеОшибки());
КонецПопытки;
КонецПроцедуры
Использование такого подхода гарантирует, что ваш код отработает корректно как при интерактивном нажатии кнопки пользователем на форме, так и при вызове из регламентного (фонового) задания по расписанию в ночное время, когда никакого графического интерфейса нет.