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