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