Приветствуем вас, уважаемые коллеги! Сегодня мы детально проанализируем одну из самых известных и коварных архитектурных проблем, возникающих при разработке отчетов в Системе компоновки данных (СКД) платформы 1С:Предприятие. Речь пойдет о ситуации, когда при попытке объединить данные из двух разных остаточных регистров накопления (например, ТоварыНаСкладах и СвободныеОстатки) в одном запросе с детализацией до Регистратора, конечные остатки в группировках верхнего уровня начинают загадочным образом обнуляться или становиться пустыми.
В этой статье мы подробно разберем, почему СКД ведет себя подобным образом на физическом уровне, и рассмотрим три надежных способа решения этой проблемы, которые помогут вам раз и навсегда победить некорректное поведение остатков.
---
Давайте разберем по шагам, что происходит внутри механизма СКД, когда мы пишем классический запрос с использованием конструкции ОБЪЕДИНИТЬ ВСЕ (UNION ALL). Рассмотрим исходную структуру проблемного запроса, который выбирает данные из первого регистра, помещает их во временную таблицу, а затем объединяет со вторым регистром:
ВЫБРАТЬ
ТоварыНаСкладахОстаткиИОбороты.Склад КАК Склад,
ТоварыНаСкладахОстаткиИОбороты.Номенклатура КАК Номенклатура,
ТоварыНаСкладахОстаткиИОбороты.Регистратор КАК Регистратор,
ТоварыНаСкладахОстаткиИОбороты.ВНаличииНачальныйОстаток КАК ВНаличииНачальныйОстаток,
ТоварыНаСкладахОстаткиИОбороты.ВНаличииПриход КАК ВНаличииПриход,
ТоварыНаСкладахОстаткиИОбороты.ВНаличииРасход КАК ВНаличииРасход,
ТоварыНаСкладахОстаткиИОбороты.ВНаличииКонечныйОстаток КАК ВНаличииКонечныйОстаток,
ТоварыНаСкладахОстаткиИОбороты.ПериодСекунда КАК ПериодСекунда,
ТоварыНаСкладахОстаткиИОбороты.ПериодДень КАК ПериодДень
ПОМЕСТИТЬ Товары
ИЗ
РегистрНакопления.ТоварыНаСкладах.ОстаткиИОбороты(, , Авто, Движения, ) КАК ТоварыНаСкладахОстаткиИОбороты;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Товары.Склад КАК Склад,
Товары.Номенклатура КАК Номенклатура,
Товары.Регистратор КАК Регистратор,
Товары.ВНаличииНачальныйОстаток КАК ВНаличииНачальныйОстаток,
Товары.ВНаличииПриход КАК ВНаличииПриход,
Товары.ВНаличииРасход КАК ВНаличииРасход,
Товары.ВНаличииКонечныйОстаток КАК ВНаличииКонечныйОстаток,
Товары.ПериодСекунда КАК ПериодСекунда,
Товары.ПериодДень КАК ПериодДень,
0 КАК ВРезервеСоСкладаКонечныйОстаток
ИЗ
Товары КАК Товары
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
СвободныеОстаткиОстаткиИОбороты.Склад,
СвободныеОстаткиОстаткиИОбороты.Номенклатура,
СвободныеОстаткиОстаткиИОбороты.Регистратор,
0,
0,
0,
0,
СвободныеОстаткиОстаткиИОбороты.ПериодСекунда,
СвободныеОстаткиОстаткиИОбороты.ПериодДень,
СвободныеОстаткиОстаткиИОбороты.ВРезервеСоСкладаКонечныйОстаток
ИЗ
РегистрНакопления.СвободныеОстатки.ОстаткиИОбороты(, , Авто, Движения, ) КАК СвободныеОстаткиОстаткиИОбороты
На первый взгляд, запрос выглядит логично. Но когда этот запрос передается в СКД, платформа начинает самостоятельно управлять расчетом остатков. СКД не просто выполняет ваш запрос, она строит единую временную шкалу на основе полей периодов (ПериодСекунда, Регистратор) в разрезе указанных измерений (Склад, Номенклатура).
Представим хронологическую последовательность документов по одной конкретной номенклатуре:
ТоварыНаСкладах). Конечный остаток в наличии равен 12 шт. Резерв в этой записи равен 0 (так как запись пришла из первой ветки запроса).СвободныеОстатки). Резерв равен 5 шт. При этом поле ВНаличииКонечныйОстаток в этой ветке жестко прописано как 0.Для СКД эта выборка представляет собой единый поток записей. Когда СКД рассчитывает итог по группировке Номенклатура, она ищет самую последнюю хронологическую точку на оси времени для данной номенклатуры. Этой точкой оказывается Заказ клиента №5. СКД смотрит на значение поля ВНаличииКонечныйОстаток в этой последней строке и видит там 0. В результате в итоговом поле группировки выводится 0 (или пустое значение), хотя реальный остаток товара на складе равен 12 шт.
Если бы последним по времени оказался документ из регистра остатков товаров, итог рассчитался бы верно. Именно поэтому у автора вопроса ошибка проявлялась хаотично, в зависимости от того, какой документ оказывался последним по времени проведения.
---
Для того чтобы СКД не смешивала временные шкалы разных регистров, нам нужно явно указать системе, что эти записи относятся к разным логическим цепочкам. Мы заставим СКД рассчитывать остатки независимо по каждой из этих ветвей.
Рассмотрим по шагам, как это реализовать:
РазделУчета:
// ... Первая часть запроса (Товары на складах) ...
ВЫБРАТЬ
Товары.Склад КАК Склад,
Товары.Номенклатура КАК Номенклатура,
Товары.Регистратор КАК Регистратор,
Товары.ВНаличииНачальныйОстаток КАК ВНаличииНачальныйОстаток,
Товары.ВНаличииКонечныйОстаток КАК ВНаличииКонечныйОстаток,
Товары.ПериодСекунда КАК ПериодСекунда,
"Остатки" КАК РазделУчета,
0 КАК ВРезервеСоСкладаКонечныйОстаток
ИЗ
Товары КАК Товары
ОБЪЕДИНИТЬ ВСЕ
// ... Вторая часть запроса (Свободные остатки) ...
ВЫБРАТЬ
СвободныеОстатки.Склад,
СвободныеОстатки.Номенклатура,
СвободныеОстатки.Регистратор,
0,
0,
СвободныеОстатки.ПериодСекунда,
"Резервы" КАК РазделУчета,
СвободныеОстатки.ВРезервеСоСкладаКонечныйОстаток
ИЗ
РегистрНакопления.СвободныеОстатки.ОстаткиИОбороты(...) КАК СвободныеОстатки
РазделУчета и откроем его свойства (колонку «Роль»).РазделУчета флаг «Измерение».Почему это решает проблему? Пометка поля как обязательного измерения заставляет СКД рассчитывать итоги параллельно по двум независимым координатным осям: Склад + Номенклатура + "Остатки" и Склад + Номенклатура + "Резервы". Теперь документы из ветки резервов больше не будут «перекрывать» и обнулять остатки товаров, поскольку они находятся в разных измерениях. Выводить само поле РазделУчета в структуру отчета пользователю при этом совершенно не обязательно.
---
Вместо жесткого разделения на разделы учета мы можем объяснить СКД, что отсутствие движения по конкретному регистру в документе — это не математический ноль, а отсутствие данных. Для этого нужно заменить нули на значения NULL и правильно настроить роли ресурсов остатков.
Выполним следующие шаги:
0 на NULL в запросе. Это критически важно, поскольку для СКД значение 0 — это конкретный остаток, равный нулю, а NULL означает «данных по этому ресурсу в этой строке нет, проигнорируй её при анализе остатка».
// В первой части запроса вместо резерва пишем NULL:
NULL КАК ВРезервеСоСкладаКонечныйОстаток
// Во второй части запроса вместо остатков пишем NULL:
NULL КАК ВНаличииНачальныйОстаток,
NULL КАК ВНаличииКонечныйОстаток
ПериодСекунда укажем тип роли «Период» с порядковым номером 1.Регистратор укажем тип роли «Период» с порядковым номером 2, а также обязательно установим флаг «Игнорировать NULL». Это укажет СКД, что если регистратор пустой или равен NULL, его не нужно учитывать в хронологической цепочке.ВНаличииНачальныйОстаток в роли установим тип «Начальный остаток», а в поле «Имя группы» напишем, например, ГруппаТовары.ВНаличииКонечныйОстаток установим тип «Конечный остаток» и укажем точно такое же имя группы — ГруппаТовары.ГруппаРезервы.Когда у каждого остатка есть своя изолированная группа и значения незаполненных полей передаются как NULL, компоновщик данных корректно склеивает хронологию и не затирает остатки одного регистра движениями другого.
---
Если вы хотите написать максимально «чистый» и поддерживаемый код, откажитесь от использования ОБЪЕДИНИТЬ ВСЕ непосредственно в SQL-запросе. В СКД заложен великолепный визуальный механизм, который справляется с этой задачей на уровне платформы гораздо лучше.
Рассмотрим, как перевести отчет на этот механизм:
НаборДанныхТовары, куда поместим запрос только к регистру ТоварыНаСкладах.НаборДанныхРезервы, содержащий запрос только к регистру СвободныеОстатки.ОбъединениеОстатков.НаборДанныхТовары и НаборДанныхРезервы) внутрь этого объединения.Склад, Номенклатура и Регистратор в обоих наборах имеют одинаковые пути к данным).Преимущества этого подхода: При использовании набора данных «Объединение» платформа 1С автоматически понимает, что остатки приходят из физически разных источников. СКД самостоятельно оптимизирует запросы к СУБД, создаст необходимые временные таблицы и выполнит корректное сопоставление хронологии по регистраторам без риска затереть конечные итоги. Этот способ считается наиболее профессиональным и рекомендуемым при проектировании сложных аналитических отчетов.