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