Как избежать задвоения начальных и конечных остатков при пересчете в валюту в СКД?

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

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

Разберем детально механизм возникновения этой проблемы и рассмотрим несколько надежных способов её решения.

Причина задвоения сумм в вычисляемых полях

Проанализируем механику работы виртуальной таблицы ОстаткиИОбороты регистра накопления. Когда в запросе выбираются поля Регистратор и ПериодСекунда (периодичность Авто), платформа 1С для корректного расчета временных срезов добавляет в результирующую выборку две служебные строки с пустым регистратором — границы периода (начальный и конечный остатки).

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

Однако на вкладке Вычисляемые поля назначить полям роль невозможно — колонка роли там попросту отсутствует. В итоге СКД воспринимает созданное вычисляемое поле (например, СуммаНачОстДолл = СуммаВзаиморасчетовНачОст * ТаблицаКурсов.Курс) как обычный оборотный ресурс и суммирует его по всем записям, включая служебные граничные точки. В результате остаток на верхних уровнях группировки завышается ровно в два раза.

Способ 1. Расчет курса в выражениях ресурсов СКД

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

В этом случае базовое поле СуммаВзаиморасчетовНачОст продолжает рассчитываться с учетом своей исходной роли остатка, а умножение на курс выполняется уже на этапе вычисления итогов.

Разберем по шагам настройку ресурсов:

  1. Удалим проблемное вычисляемое поле из вкладки Вычисляемые поля.
  2. Перейдем на вкладку Ресурсы и добавим новое поле расчета. Если в отчете есть группировка по валюте взаиморасчетов, для нее расчет пишется напрямую: Сумма(СуммаВзаиморасчетовНачОст) * ТаблицаКурсов.Курс Здесь СКД сначала агрегирует начальный остаток по правилам остаточных полей (без задвоения), а затем умножает результат на курс.
  3. Для вышестоящих группировок (например, по сотруднику, подразделению или для общего итога), где в одну строку сходятся разные валюты, простое перемножение применимо быть не может. Для них в колонке «Рассчитывать по...» укажем нужные группировки и применим функцию ВычислитьВыражениеСГруппировкойМассив:
    
    Сумма(ВычислитьВыражениеСГруппировкойМассив("Сумма(СуммаВзаиморасчетовНачОст) * ТаблицаКурсов.Курс", "Валюта"))
    
    В данном выражении компоновщик рассчитывает пересчитанный остаток по каждой отдельной валюте, формирует из них массив и затем складывает элементы массива. В результате итоги по сотруднику сходятся идеально.

Способ 2. Передача таблицы курсов во временную таблицу СКД через МенеджерВременныхТаблиц

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

Посмотрим на пример реализации в модуле отчета. В событии ПриКомпоновкеРезультата мы можем заранее поместить таблицу курсов во временную таблицу и передать этот контекст в процессор компоновки:


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

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


ВЫБРАТЬ
    ВЗСотр.Регистратор КАК Регистратор,
    ВЗСотр.ПериодСекунда КАК ПериодСекунда,
    ВЗСотр.ФизЛицо КАК ФизЛицо,
    ВЗСотр.Валюта КАК Валюта,
    ВЗСотр.СуммаВзаиморасчетовНачальныйОстаток КАК СуммаНачОст,
    ВЗСотр.СуммаВзаиморасчетовНачальныйОстаток * ЕСТЬNULL(ВТ_Курсы.Курс, 1) КАК СуммаНачОстВал
ИЗ
    РегистрНакопления.ВзаиморасчетыСПодотчетнымиЛицами.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, Авто, , ) КАК ВЗСотр
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Курсы КАК ВТ_Курсы
        ПО ВЗСотр.Валюта = ВТ_Курсы.Валюта

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

Способ 3. Расчет данных до передачи в СКД (Набор данных — Объект)

Если выгрузка курсов и расчет остатков по регистрам завязаны на уже существующие тяжелые алгоритмы конфигурации, логично выполнить все математические операции до вызова компоновщика.

Проанализируем этот подход:

  1. Формируем выборку по регистру с помощью обычного объекта Запрос.
  2. Программно соединяем полученные строки с таблицей курсов и заполняем готовые колонки: рублевые остатки, курсы и остатки в целевой валюте в единую результирующую ТаблицаЗначений.
  3. В СКД создаем единственный Набор данных — Объект и загружаем в него готовую таблицу через ВнешниеНаборыДанных.
  4. Для полей набора-объекта платформа также позволяет вручную настроить роли (указать поля периода, регистратора и роли остаточных показателей). СКД выполнит только визуализацию и вывод группировок без искажения числовых данных.
← На главную