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