В процессе разработки механизмов ценообразования в 1С часто возникает задача: необходимо определить скидку на конкретный товар, если в системе эта скидка назначена не на сам элемент, а на всю группу (папку) номенклатуры. Сложность заключается в том, что товар может находиться на любом уровне вложенности, а скидка может быть установлена как на непосредственного родителя, так и на группу верхнего уровня.
В этой статье мы подробно разберем, как правильно построить алгоритм поиска таких скидок, проанализируем типичные ошибки начинающих разработчиков и рассмотрим наиболее эффективные способы решения задачи с точки зрения производительности системы.
Проанализируем ситуацию, когда разработчик пытается решить задачу "в лоб". Часто встречается код, где сначала выбираются действующие скидки, а затем для каждой строки документа выполняется отдельный запрос для проверки вхождения товара в группу. Рассмотрим пример такого (неправильного) подхода:
Для Каждого Стр Из Товары Цикл
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Ссылка = &Номенклатура
| И Номенклатура.Ссылка В ИЕРАРХИИ(&ГруппаСкидок)";
Запрос.УстановитьПараметр("Номенклатура", Стр.Номенклатура);
Запрос.УстановитьПараметр("ГруппаСкидок", ГруппаИзРегистра);
// Выполнение запроса...
КонецЦикла;
Почему это плохо? Если в документе 100 строк, система выполнит 100 отдельных обращений к базе данных. Это создает колоссальную нагрузку на сервер и замедляет работу пользователя. В программировании 1С использование Запрос.Выполнить() внутри цикла считается одной из самых грубых ошибок проектирования. Наша задача — получить все данные одним запросом.
Рассмотрим более правильный подход. Мы можем поместить товары из документа во временную таблицу и соединить их с регистром сведений. Важно помнить: оператор В ИЕРАРХИИ нельзя использовать в условии связи (после ключевого слова ПО), но его можно использовать в секции ГДЕ.
Разберем по шагам создание оптимального запроса:
ВТ_Товары, куда выгружаем всю номенклатуру из заказа.ВТ_Скидки, куда выбираем актуальные записи из регистра РегистрСведений.СкидкиАкции.
ВЫБРАТЬ
ЗаказТовары.Номенклатура КАК Номенклатура
ПОМЕСТИТЬ ВТ_Товары
ИЗ
Документ.ЗаказПокупателя.Товары КАК ЗаказТовары
ГДЕ
ЗаказТовары.Ссылка = &Ссылка;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Скидки.ГруппаТоваров КАК ГруппаТоваров,
Скидки.Скидка КАК ПроцентСкидки
ПОМЕСТИТЬ ВТ_Скидки
ИЗ
РегистрСведений.СкидкиАкции.СрезПоследних(&ДатаАктуальности, Прайс = &ТипЦен) КАК Скидки;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_Товары.Номенклатура,
ВТ_Скидки.ПроцентСкидки
ИЗ
ВТ_Товары КАК ВТ_Товары,
ВТ_Скидки КАК ВТ_Скидки
ГДЕ
ВТ_Товары.Номенклатура В ИЕРАРХИИ (ВТ_Скидки.ГруппаТоваров)
Обратите внимание на использование виртуальной таблицы СрезПоследних. Это критически важно для работы с периодическими регистрами сведений. Если использовать физическую таблицу (как в исходном сообщении автора темы), можно получить неактуальные или дублирующиеся данные.
Если справочник Номенклатура имеет огромный объем, а скидок в регистре тоже много, предыдущий вариант может работать медленно из-за специфики выполнения В ИЕРАРХИИ. В таких случаях мы применим метод "от обратного": выясним, в какие группы входит каждый товар, и поищем скидки именно по этим группам.
Выясним причину, почему это эффективно: поиск по конкретному списку ссылок (оператор В) работает значительно быстрее, чем иерархический поиск.
Разберем алгоритм реализации:
Родитель, Родитель.Родитель и так далее (если глубина иерархии ограничена и известна).Пример кода для получения родителей (упрощенно):
СписокГрупп = Новый Массив;
Для Каждого Стр Из Товары Цикл
ТекущийРодитель = Стр.Номенклатура.Родитель;
Пока ЗначениеЗаполнено(ТекущийРодитель) Цикл
Если СписокГрупп.Найти(ТекущийРодитель) = Неопределено Тогда
СписокГрупп.Добавить(ТекущийРодитель);
КонецЕсли;
ТекущийРодитель = ТекущийРодитель.Родитель;
КонецЦикла;
КонецЦикла;
Затем передаем СписокГрупп в запрос в качестве параметра &МассивГрупп и используем условие ГДЕ ГруппаТоваров В (&МассивГрупп).
Проанализируем ситуацию: скидка 5% установлена на группу "Бытовая техника", а скидка 10% — на подгруппу "Холодильники". Какой процент должен получить товар "Холодильник Минск"?
Обычно действует правило: наиболее близкая к товару скидка является приоритетной. Чтобы реализовать это в запросе, нам нужно знать "уровень" каждой группы. При получении родителей можно вычислять их уровень вложенности. При сопоставлении данных мы выберем ту запись, у которой уровень вложенности максимальный (самый глубокий).
Если мы используем СКД (Систему компоновки данных), платформа может обработать иерархию автоматически, если правильно настроить связи наборов данных. Однако для простых документов чаще всего достаточно программного сопоставления через Соответствие или Структуру для быстрого поиска в памяти.
Если подобные расчеты проводятся часто (например, при каждом подборе товара в корзину), рассмотрим дополнительные методы ускорения:
СкидкиАкции (особенно ГруппаТоваров и Прайс) проиндексированы.ПовторноИспользуемыхЗначениях.КатегорияСкидки, поиск превращается в прямое соединение двух таблиц, что является максимально быстрым вариантом для SQL-сервера.Подведем итог: для решения задачи поиска скидок по группам следует избегать запросов в цикле. Оптимальным решением будет использование временных таблиц с оператором В ИЕРАРХИИ в секции ГДЕ, либо предварительный сбор массива всех родителей номенклатуры для точечного запроса к регистру.