При работе с отчетами в 1С часто возникает задача объединения данных из разных источников, например, получение информации о продажах (выручке, себестоимости) и сопоставление ее с общим количеством поступившей номенклатуры. В самописных конфигурациях, как и в типовых, это может быть нетривиально, особенно когда приход товара может осуществляться на разные склады, а отчет строится по продажам с конкретного склада. Рассмотрим, как мы можем решить эту задачу, шаг за шагом анализируя предложенные подходы.
Основная проблема, с которой мы сталкиваемся, заключается в том, чтобы к каждой строке с финансовым результатом (продажей) привязать общее количество прихода данной номенклатуры за определенный период, при этом игнорируя или агрегируя данные по складу прихода, если это необходимо. Автор задачи изначально искал "самый первый приход", но в ходе обсуждения стало ясно, что требуется именно суммарный приход по номенклатуре.
Для начала, давайте посмотрим на исходный запрос, представленный автором. Он уже соединяет данные регистра накопления УУ_ФинРезультат с виртуальной таблицей УУ_Запасы.Остатки:
ВЫБРАТЬ РАЗРЕШЕННЫЕ
УУ_ФинРезультат.Период КАК Период,
УУ_ФинРезультат.Регистратор КАК Регистратор,
УУ_ФинРезультат.НомерСтроки КАК НомерСтроки,
УУ_ФинРезультат.Активность КАК Активность,
УУ_ФинРезультат.Организация КАК Организация,
УУ_ФинРезультат.Магазин КАК Магазин,
УУ_ФинРезультат.Контрагент КАК Контрагент,
УУ_ФинРезультат.Номенклатура КАК Номенклатура,
УУ_ФинРезультат.Количество КАК Количество,
УУ_ФинРезультат.Себестоимость КАК Себестоимость,
УУ_ФинРезультат.Сумма КАК Выручка,
УУ_ФинРезультат.ВидДеятельности КАК ВидДеятельности,
УУ_ФинРезультат.Склад КАК Склад,
УУ_ФинРезультат.Сумма - УУ_ФинРезультат.Себестоимость КАК Маржа,
УУ_ФинРезультат.Номенклатура.Код КАК НоменклатураКод,
УУ_ФинРезультат.Номенклатура.ВидНоменклатуры КАК ВидНоменклатуры,
УУ_ЗапасыОстатки.КоличествоОстаток КАК КоличествоОстаток,
УУ_ЗапасыОстатки.СуммаОстаток КАК СуммаОстаток,
РАЗНОСТЬДАТ(УУ_ЗапасыОстатки.Партия.Дата, &КонецП, ДЕНЬ) КАК ДниЖизни,
ТИПЗНАЧЕНИЯ(УУ_ФинРезультат.Регистратор) КАК ТипДокумента,
УУ_ЗапасыОстатки.Партия КАК Партия,
ТИПЗНАЧЕНИЯ(УУ_ЗапасыОстатки.Партия) КАК ТипДокумента_партия
ИЗ
РегистрНакопления.УУ_ФинРезультат КАК УУ_ФинРезультат
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.УУ_Запасы.Остатки(, ) КАК УУ_ЗапасыОстатки
ПО УУ_ФинРезультат.Организация = УУ_ЗапасыОстатки.Организация
И УУ_ФинРезультат.Магазин = УУ_ЗапасыОстатки.Магазин
И УУ_ФинРезультат.Склад = УУ_ЗапасыОстатки.Склад
И УУ_ФинРезультат.Номенклатура = УУ_ЗапасыОстатки.Номенклатура
ГДЕ
УУ_ФинРезультат.Период МЕЖДУ &НачалоП И &КонецП
Этот запрос уже предоставляет нам данные о продажах и текущих остатках. Наша задача — добавить к нему информацию об общем количестве поступлений (приходов).
Самый простой способ получить общее количество прихода по номенклатуре — это выполнить отдельный запрос к регистру накопления УУ_Запасы, отфильтровать по виду движения "Приход" и сгруппировать по номенклатуре. Этот подход был предложен в обсуждении.
Рассмотрим пример такого запроса:
ВЫБРАТЬ
СУММА(УУ_Запасы.Количество) КАК Количество,
УУ_Запасы.Номенклатура КАК Номенклатура
ИЗ
РегистрНакопления.УУ_Запасы КАК УУ_Запасы
ГДЕ
УУ_Запасы.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
СГРУППИРОВАТЬ ПО
УУ_Запасы.Номенклатура
В этом запросе мы получаем общую сумму прихода для каждой номенклатуры по всем складам и за весь период, если не указаны дополнительные условия. Это позволяет нам получить общее количество поступлений. Однако, чтобы интегрировать эти данные в наш основной запрос, нам потребуется использовать временные таблицы или вложенные запросы.
Когда требуется соединить данные с разной степенью детализации или из разных регистров, временные таблицы (ВТ) становятся незаменимым инструментом. Они позволяют разделить сложный запрос на более простые, выполняемые последовательно, и улучшают читаемость и зачастую производительность.
Проанализируем шаги по реализации этого подхода:
Создание временной таблицы с основными данными (ФинРезультат).
На первом этапе мы помещаем результат нашего исходного запроса (который уже включает данные о продажах и текущих остатках) во временную таблицу. Это позволит нам обращаться к этим данным по имени, а не переписывать весь запрос снова.
ПОМЕСТИТЬ ФинРезультат
ИЗ
РегистрНакопления.УУ_ФинРезультат КАК УУ_ФинРезультат
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.УУ_Запасы.Остатки(, ) КАК УУ_ЗапасыОстатки
ПО УУ_ФинРезультат.Организация = УУ_ЗапасыОстатки.Организация
И УУ_ФинРезультат.Магазин = УУ_ЗапасыОстатки.Магазин
И УУ_ФинРезультат.Склад = УУ_ЗапасыОстатки.Склад
И УУ_ФинРезультат.Номенклатура = УУ_ЗапасыОстатки.Номенклатура
ГДЕ
УУ_ФинРезультат.Период МЕЖДУ &НачалоП И &КонецП
Обратите внимание, что в данном случае мы выбираем все поля из оригинального запроса автора, чтобы сохранить полную детализацию для дальнейшего анализа. Для временной таблицы мы можем выбирать как все поля, так и выборочно, в зависимости от задачи.
Создание временной таблицы с агрегированными приходами (ВТ_Запасы).
На втором этапе мы формируем временную таблицу, которая будет содержать суммарное количество прихода для каждой номенклатуры за интересующий нас период. Для этого мы используем виртуальную таблицу Обороты регистра накопления УУ_Запасы. Важно понимать, что Обороты предоставляет агрегированные данные о движениях (приходах и расходах) за указанный период.
Мы выбираем только приходные движения, фильтруя по ВидДвижения = ВидДвиженияНакопления.Приход. Также важно исключить возвратные операции, если они могут искажать подсчет общего прихода (например, НЕ Запасы.Регистратор ССЫЛКА Документ.УУ_ВозвратОтПокупателя).
Критически важно не указывать в условиях для виртуальной таблицы Обороты измерение Склад, если мы хотим получить общий приход по всем складам. В противном случае, если мы укажем Склад, мы получим приход по каждому складу отдельно, что усложнит задачу объединения с продажами, которые могут быть по одному конкретному складу.
ВЫБРАТЬ
СУММА(Запасы.КоличествоПриход) КАК КоличествоПриход,
Запасы.Номенклатура КАК Номенклатура
ПОМЕСТИТЬ ВТ_Запасы
ИЗ
РегистрНакопления.УУ_Запасы.Обороты(&НачалоП, &КонецП, Регистратор, ) КАК Запасы
ГДЕ
НЕ Запасы.Регистратор ССЫЛКА Документ.УУ_ВозвратОтПокупателя
СГРУППИРОВАТЬ ПО
Запасы.Номенклатура
Здесь мы используем агрегатную функцию СУММА(Запасы.КоличествоПриход), чтобы получить общее количество прихода для каждой номенклатуры. Параметры виртуальной таблицы Обороты (&НачалоП, &КонецП, Регистратор, ) позволяют нам получить обороты за нужный период, детализируя их до регистратора, но без детализации по складу, что соответствует нашей задаче получения общего прихода по номенклатуре. Заметим, что ключевое слово РАЗЛИЧНЫЕ, которое было в исходном примере из обсуждения, в данном случае является избыточным, так как мы уже выполняем агрегацию с помощью СУММА и группировки, что гарантирует уникальность строк по Номенклатура.
Соединение временных таблиц и получение итогового результата.
На последнем шаге мы соединяем нашу основную временную таблицу ФинРезультат с временной таблицей приходов ВТ_Запасы по полю Номенклатура, используя левое соединение (ЛЕВОЕ СОЕДИНЕНИЕ). Это гарантирует, что все строки из ФинРезультат попадут в итоговый запрос, даже если для какой-то номенклатуры не было приходов (в этом случае КоличествоПриход будет NULL).
ВЫБРАТЬ
ФинРезультат.Период КАК Период,
ФинРезультат.Регистратор КАК Регистратор,
ФинРезультат.Организация КАК Организация,
ФинРезультат.Магазин КАК Магазин,
ФинРезультат.Контрагент КАК Контрагент,
ФинРезультат.Номенклатура КАК Номенклатура,
ФинРезультат.Количество КАК Количество,
ФинРезультат.Себестоимость КАК Себестоимость,
ФинРезультат.Выручка КАК Выручка,
ФинРезультат.ВидДеятельности КАК ВидДеятельности,
ФинРезультат.Склад КАК Склад,
ФинРезультат.Маржа КАК Маржа,
ФинРезультат.НоменклатураКод КАК НоменклатураКод,
ФинРезультат.ВидНоменклатуры КАК ВидНоменклатуры,
ФинРезультат.КоличествоОстаток КАК КоличествоОстаток,
ФинРезультат.СуммаОстаток КАК СуммаОстаток,
ФинРезультат.ДниЖизни КАК ДниЖизни,
ФинРезультат.ТипДокумента КАК ТипДокумента,
ФинРезультат.Партия КАК Партия,
ФинРезультат.ТипДокумента_партия КАК ТипДокумента_партия,
ВТ_Запасы.КоличествоПриход КАК КоличествоПриход
ИЗ
ФинРезультат КАК ФинРезультат
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Запасы КАК ВТ_Запасы
ПО ФинРезультат.Номенклатура = ВТ_Запасы.Номенклатура
Таким образом, к каждой строке с данными по продажам будет добавлено соответствующее общее количество прихода по данной номенклатуре. Если у вас возникнут проблемы с "завышением" суммы, как упоминал автор в сообщении 11, это может быть связано с тем, что соединение приводит к дублированию строк в ФинРезультат, если в ВТ_Запасы для одной номенклатуры каким-то образом оказалось несколько записей (что в нашем случае не должно происходить благодаря группировке по Номенклатура в ВТ_Запасы) или если вы использовали Обороты без правильной агрегации.
В некоторых более сложных случаях, когда данные о приходе и расходе имеют сильно различающиеся наборы измерений или требуют разной логики фильтрации/агрегации, может быть полезен подход с использованием оператора ОБЪЕДИНИТЬ ВСЕ. Этот подход позволяет привести данные из разных источников к единой структуре во временной таблице, а затем уже агрегировать их по нужным измерениям.
Рассмотрим общий шаблон, предложенный в обсуждении:
ВЫБРАТЬ
Т.Поле1,
Т.Поле2,
...
Т.ПолеХ,
СУММА(Т.Сумма1) КАК Сумма1,
СУММА(Т.Сумма2) КАК Сумма2,
СУММА(Т.Сумма3) КАК Сумма3,
СУММА(Т.Сумма4) КАК Сумма4
ИЗ
(ВЫБРАТЬ
Поле1,
Поле2,
...
ПолеХ,
Сумма1,
Сумма2,
0 КАК Сумма3,
0 КАК Сумма4
ИЗ
Т1
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Поле1,
Поле2,
...
ПолеХ,
0,
0,
Сумма3,
Сумма4
ИЗ
Т2) КАК Т
СГРУППИРОВАТЬ ПО
Т.Поле1,
Т.Поле2,
...
Т.ПолеХ
В этом шаблоне:
Т1 и Т2).Поле1, Поле2,...ПолеХ) и поля-ресурсы.ОБЪЕДИНИТЬ ВСЕ совпадали. Если какой-то ресурс присутствует только в одном источнике, мы заполняем его нулями в другом.СУММА()) по общим ключевым полям, собирая все данные в одну итоговую таблицу.Этот подход особенно полезен, когда:
ОБЪЕДИНИТЬ ВСЕ работает быстрее, чем ОБЪЕДИНИТЬ (не проверяет на дубликаты).В нашем случае, для привязки общего прихода к каждой строке продажи, подход с временными таблицами и ЛЕВОЕ СОЕДИНЕНИЕ (Подход 2) является более прямым и эффективным, так как нам нужно добавить всего одно агрегированное поле к существующим данным. Однако, если бы мы хотели, например, получить в одном отчете и приходы, и расходы, и продажи, и затем агрегировать их по номенклатуре за период, то ОБЪЕДИНИТЬ ВСЕ был бы очень уместен.
В обсуждении также прозвучало предложение использовать виртуальную таблицу ОстаткиИОбороты. Эта таблица является очень мощным инструментом, предоставляющим данные о начальном остатке, приходе, расходе и конечном остатке за период в одном запросе.
Виртуальная таблица РегистрНакопления.УУ_Запасы.ОстаткиИОбороты(&НачалоП, &КонецП, Авто, ДвиженияИГраницыПериода, ) может быть использована. Параметр ДвиженияИГраницыПериода позволяет получить все виды движений за период. Вы можете выбрать поля КоличествоПриход и КоличествоРасход из этой таблицы.
Однако, как было отмечено в сообщении 7, сложности могут возникнуть при попытке прямого соединения ОстаткиИОбороты с таблицей УУ_ФинРезультат, если вы хотите получить общий приход по номенклатуре, но продажи детализированы по складу. Если мы соединим по Номенклатура и Склад, то получим приход только по конкретному складу, что может быть не то, что нужно. Если же мы соединим только по Номенклатура, но в ОстаткиИОбороты будет детализация по складу, то это может привести к дублированию строк в результате соединения, если для одной номенклатуры есть движения на нескольких складах. Чтобы избежать этого, необходимо предварительно агрегировать данные из ОстаткиИОбороты, исключив Склад из группировки, если нам нужен общий приход по всей номенклатуре без учета склада, а затем уже соединять агрегированные данные.
Параметры виртуальных таблиц. Всегда внимательно указывайте параметры виртуальных таблиц (таких как &НачалоП, &КонецП, Периодичность, Условия). Неправильные параметры могут привести к неверным данным или очень медленному выполнению запроса.
Индексирование временных таблиц. Если временные таблицы содержат большой объем данных и используются в условиях соединений или фильтрации, рассмотрите возможность их индексирования. В 1С это делается с помощью инструкции ИНДЕКСИРОВАТЬ ПО после ПОМЕСТИТЬ.
ПОМЕСТИТЬ МояВременнаяТаблица
...
ИНДЕКСИРОВАТЬ ПО ПолеДляИндексации
Это может значительно ускорить выполнение запроса.
Общая сумма прихода или первый приход? Изначальный вопрос автора был про "самый первый приход". Однако, исходя из дальнейшего обсуждения ("получено 100, потом 200... Как сделать, чтобы показал отчет 300?"), становится ясно, что речь идет об общей сумме прихода за период. Если бы действительно требовался именно первый приход (например, дата первого поступления или количество первой партии), задача решалась бы сложнее, возможно, с помощью подзапроса с УПОРЯДОЧИТЬ ПО и ПЕРВЫЕ 1.
Отладка запросов. При построении сложных запросов с временными таблицами крайне полезно использовать конструктор запросов с возможностью просмотра результатов каждого шага (временной таблицы). Это позволяет быстро находить ошибки в логике и отлаживать запрос по частям.
Организация и Магазин. Обратите внимание, что в вашем основном запросе и в соединении с остатками вы используете поля Организация и Магазин. Убедитесь, что при агрегации приходов (например, в ВТ_Запасы) вы либо также группируете по этим полям, либо исключаете их, если приход должен быть общим по всем организациям/магазинам. В представленном решении для ВТ_Запасы мы группируем только по Номенклатура, что соответствует получению прихода по всем организациям и магазинам. Если требуется учитывать эти измерения для приходов, добавьте их в СГРУППИРОВАТЬ ПО в запросе ВТ_Запасы и в условие соединения с ФинРезультат.
В конечном итоге, использование временных таблиц для предварительной агрегации приходных движений, а затем левое соединение с основной выборкой данных, является наиболее гибким и понятным решением для данной задачи в 1С. Это позволяет эффективно объединять данные из разных регистров и с разной степенью детализации, при этом сохраняя производительность и читаемость кода.