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