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