При сопровождении и доработке старых, но до сих пор активно эксплуатируемых конфигураций на платформе 1С:Предприятие 7.7 (в частности, легендарной "Торговли и Склада" редакции 9.2), разработчики часто сталкиваются с крайне странными, на первый взгляд, конструкциями в типовом коде. Одним из таких "артефактов" является использование реквизита МинОстаток (минимальный остаток) в качестве розничной цены для комиссионных товаров в отчете Остатки ТМЦ. В этой статье мы подробно разберем, зачем разработчики фирмы "1С" пошли на такой шаг, какие ошибки это за собой влечет, а также исправим сопутствующие баги отчета и разберем тонкости работы встроенного языка запросов платформы 7.7.
---
Рассмотрим фрагмент запроса из типового отчета ОстаткиТМЦ (модуль формы), который формируется при выборе представления цены как "отпускная цена (только розница)":
Если (ПредставлениеЦены = "отпускная цена (только розница)") Тогда
ТекстЗапроса = ТекстЗапроса +
"ЦенаПрод = Регистр.ОстаткиТМЦ.ЦенаПрод, Регистр.ПартииОтданные.Номенклатура.МинОстаток;
|Группировка ЦенаПрод;";
КонецЕсли;
На первый взгляд этот код кажется абсурдным или ошибочным. Почему переменной запроса ЦенаПрод (цена продажи) во второй части конструкции (относящейся к регистру ПартииОтданные, где учитываются товары у комиссионеров) присваивается реквизит МинОстаток из карточки номенклатуры? Какая связь между минимальным складским остатком товара и ценой его продажи у комиссионера? Давайте разбираться.
---
Для того чтобы понять логику авторов конфигурации, нам необходимо вернуться в начало 2000-х годов, когда активно развивалась SQL-версия платформы 1С:Предприятие 7.7.
В файловой версии (DBF) встроенный язык запросов прощал программистам множество вольностей. Однако при работе с базой данных MS SQL Server встроенные запросы 1С транслировались платформой в реальные SQL-запросы (через механизм T-SQL). При формировании многорегистрового запроса, который одновременно собирает данные из регистра остатков ОстаткиТМЦ и регистра партий ПартииОтданные, СУБД требует строгого сопоставления типов данных в результирующих временных таблицах.
Проанализируем структуру регистров:
ОстаткиТМЦ имеет измерение ЦенаПрод (тип "Число").ПартииОтданные не имеет ни такого измерения, ни реквизита с аналогичным типом данных.Если бы мы написали запрос простым сопоставлением только для первого регистра:
ЦенаПрод = Регистр.ОстаткиТМЦ.ЦенаПрод;
то при трансляции запроса в СУБД MS SQL Server для записей регистра ПартииОтданные поле ЦенаПрод осталось бы незаполненным (или имело бы тип NULL/неопределено). При выполнении группировки по этому полю SQL-сервер выдавал ошибку трансляции типов, либо отчет падал в аварийный режим, либо некорректно схлопывал остатки по комиссионерам.
Встроенный язык запросов 1С 7.7 не позволял напрямую присвоить константу (например, ЦенаПрод = 0 или ЦенаПрод = "") для сопоставления с полем регистра во второй части выражения. Платформа требовала, чтобы через запятую было указано реальное поле базы данных или реквизит объекта аналогичного типа (в данном случае — "Число").
В качестве "числового донора" разработчики "1С" выбрали реквизит Номенклатура.МинОстаток. Логика была проста: этот реквизит имеет числовой тип данных и почти всегда равен нулю у большинства пользователей, которые не используют автозаказ. Таким образом, для комиссионных товаров цена продажи в запросе условно занулялась, что позволяло успешно выполнить группировку и обойти жесткие ограничения SQL-транслятора.
---
Несмотря на то, что этот код помог решить проблему совместимости с СУБД, он несет в себе скрытую угрозу. Если ваше предприятие реально использует функционал контроля минимальных остатков на складах и заполняет реквизит МинОстаток в карточках товаров, отчет ОстаткиТМЦ начнет выводить недостоверные данные по комиссионерам.
Посмотрим на результат выполнения отчета в такой ситуации: значение минимального остатка из карточки товара будет ошибочно выведено в колонку "Отпускная цена" для строк с комиссионным товаром. Более того, если у вас есть остатки товара на розничном складе (с реальной ценой продажи) и у комиссионера (где в качестве цены подставился минимальный остаток), то при несовпадении этих цифр строка товара в отчете раздвоится. Это крайне усложняет анализ остатков.
Как решить эту проблему корректно?
Рассмотрим несколько вариантов решения этой проблемы:
Самый правильный способ с точки зрения классического программирования в 1С 7.7 — отказаться от сборки данных разных регистров в один сложный запрос. Вместо этого мы можем:
ОстаткиТМЦ с группировкой по розничным ценам.ПартииОтданные (где розничная цена по определению отсутствует или заполняется из других источников).Если ваша база данных работает на MS SQL Server, лучшим решением будет использование внешней компоненты 1С++ и класса ODBCRecordset. Написание прямого SQL-запроса полностью избавляет от подобных ограничений платформы. В прямом SQL-запросе мы можем легко объединить таблицы с помощью конструкции UNION ALL и принудительно вернуть числовой ноль для отсутствующих полей:
SELECT
Номенклатура AS [Товар $Справочник.Номенклатура],
ЦенаПрод AS Цена,
Количество AS Остаток
FROM $РегистрИтоги.ОстаткиТМЦ
UNION ALL
SELECT
Номенклатура AS [Товар $Справочник.Номенклатура],
0 AS Цена, -- Здесь мы легко и безопасно зануляем цену для комиссионера
Количество AS Остаток
FROM $РегистрИтоги.ПартииОтданные
Такой запрос отработает в десятки раз быстрее штатного и никогда не приведет к раздвоению строк из-за заполненного минимального остатка.
---
Параллельно в отчете ОстаткиТМЦ существует еще один давний баг — неверный расчет количества у комиссионеров, если в настройках отчета выбран вывод в "базовых" или "основных" единицах, отличных от единиц хранения.
Выясним причину: при получении данных из регистра ПартииОтданные платформа не учитывает коэффициент единицы измерения (КФ), заданный для номенклатуры. На собственных складах пересчет работает корректно, а по комиссионерам выводится "сырое" количество из регистра.
Для исправления этого бага найдем в модуле отчета блок вывода строк и добавим пересчет количества с учетом коэффициента единицы измерения. Проанализируем пример исправления кода:
// Было в типовой конфигурации:
КоличествоДляВывода = Запрос.Количество;
// Должно быть с исправлением:
Если ВыводитьВЕдиницах = "Основных" Тогда
КоэффициентПересчета = Запрос.Номенклатура.БазоваяЕдиница.Коэффициент;
Если ПустоеЗначение(КоэффициентПересчета) = 0 Тогда
КоличествоДляВывода = Запрос.Количество / КоэффициентПересчета;
Иначе
КоличествоДляВывода = Запрос.Количество;
КонецЕсли;
КонецЕсли;
---
Еще одна классическая "корявка", часто встречающаяся в типовом коде этого отчета (в частности, при установке фильтров по юридическим лицам), связана с неоптимальным использованием циклов:
Если ВыбРазделитель2.Выбран() = 1 Тогда
СписокФирм = СоздатьОбъект("СписокЗначений");
СпрФирм = СоздатьОбъект("Справочник.Фирмы");
СпрФирм.ВыбратьЭлементы();
Пока СпрФирм.ПолучитьЭлемент() = 1 Цикл
Если СпрФирм.ЮрЛицо = ВыбРазделитель2 Тогда
СписокФирм.ДобавитьЗначение(СпрФирм.ТекущийЭлемент());
КонецЕсли;
// ОШИБКА: Установка фильтров находится внутри цикла!
РегПартии.УстановитьЗначениеФильтра("Фирма", СписокФирм, 2);
РегОтданные.УстановитьЗначениеФильтра("Фирма", СписокФирм, 2);
КонецЦикла;
КонецЕсли;
Разберем, почему этот код работает медленно. Метод УстановитьЗначениеФильтра вызывается на каждой итерации цикла обхода справочника Фирм. Если в базе заведено множество фирм, платформа будет постоянно перезаписывать настройки фильтра объектов регистров, тратя на это колоссальные ресурсы процессора.
Для оптимизации нам нужно просто вынести установку фильтров за пределы цикла:
Если ВыбРазделитель2.Выбран() = 1 Тогда
СписокФирм = СоздатьОбъект("СписокЗначений");
СпрФирм = СоздатьОбъект("Справочник.Фирмы");
СпрФирм.ВыбратьЭлементы();
Пока СпрФирм.ПолучитьЭлемент() = 1 Цикл
Если СпрФирм.ЮрЛицо = ВыбРазделитель2 Тогда
СписокФирм.ДобавитьЗначение(СпрФирм.ТекущийЭлемент());
КонецЕсли;
КонецЦикла;
// ПРАВИЛЬНО: Устанавливаем фильтр один раз после заполнения списка значений
РегПартии.УстановитьЗначениеФильтра("Фирма", СписокФирм, 2);
РегОтданные.УстановитьЗначениеФильтра("Фирма", СписокФирм, 2);
КонецЕсли;
---
Часто в отчетах 1С 7.7 можно встретить конструкции вида Группировка Номенклатура Все; или Группировка Склад Все ВошедшиеВЗапрос;. Непонимание этих механизмов ведет к сильному замедлению работы отчетов. Выясним разницу между ними на простых примерах.
Представим, что у нас есть следующая структура данных в базе:
Рассмотрим простую группировку:
Группировка Склад;
Группировка Товар;
В результат запроса попадут только те строки, по которым реально есть остатки:
Добавим к группировке товаров слово Все:
Группировка Склад;
Группировка Товар Все;
В этом случае платформа выполнит полное декартово произведение складов и всех элементов справочника "Номенклатура", даже если остатков по ним нет. Результат будет выглядеть так:
Такой вариант используется крайне редко, так как он сильно раздувает выборку и замедляет работу отчета, особенно если справочник товаров насчитывает десятки тысяч позиций.
Это специальный режим, созданный в основном для построения удобных кросс-таблиц (шахматок), когда склады нужно вывести в колонки, а товары — в строки. Напишем код:
Группировка Склад;
Группировка Товар Все ВошедшиеВЗапрос;
Этот режим работает умнее. Платформа анализирует, какие товары вообще попали в выборку (в нашем случае это Товар 1 и Товар 2, так как по ним есть движения), и строит полную матрицу только для них, исключая Товар 3, по которому остатков вообще не было ни на одном складе:
Это позволяет получить ровную сетку данных для вывода в печатную форму "по колонкам", не выводя при этом лишний "мусор" из справочника номенклатуры.
Важный нюанс производительности: Использование ключевого слова Все на верхних уровнях группировки (например, Группировка Номенклатура Все;) заставляет платформу опрашивать весь справочник и сопоставлять его с результатами выборки. Если вам нужно получить только реальные остатки по фильтру, никогда не пишите Все. Убрав это слово, вы ускорите выполнение запроса в 10–15 раз!