Почему в типовом отчете "Остатки ТМЦ" в 1С 7.7 используется МинОстаток вместо цены и как исправить сопутствующие ошибки?

Программист 1С v7.7 Управленческий учет Торговля и дистрибуция
← На главную

При сопровождении и доработке старых, но до сих пор активно эксплуатируемых конфигураций на платформе 1С:Предприятие 7.7 (в частности, легендарной "Торговли и Склада" редакции 9.2), разработчики часто сталкиваются с крайне странными, на первый взгляд, конструкциями в типовом коде. Одним из таких "артефактов" является использование реквизита МинОстаток (минимальный остаток) в качестве розничной цены для комиссионных товаров в отчете Остатки ТМЦ. В этой статье мы подробно разберем, зачем разработчики фирмы "1С" пошли на такой шаг, какие ошибки это за собой влечет, а также исправим сопутствующие баги отчета и разберем тонкости работы встроенного языка запросов платформы 7.7.

---

"Загадочный" код в отчете "ОстаткиТМЦ"

Рассмотрим фрагмент запроса из типового отчета ОстаткиТМЦ (модуль формы), который формируется при выборе представления цены как "отпускная цена (только розница)":


Если (ПредставлениеЦены = "отпускная цена (только розница)") Тогда
    ТекстЗапроса = ТекстЗапроса +
    "ЦенаПрод        = Регистр.ОстаткиТМЦ.ЦенаПрод, Регистр.ПартииОтданные.Номенклатура.МинОстаток;
    |Группировка ЦенаПрод;";   
КонецЕсли;

На первый взгляд этот код кажется абсурдным или ошибочным. Почему переменной запроса ЦенаПрод (цена продажи) во второй части конструкции (относящейся к регистру ПартииОтданные, где учитываются товары у комиссионеров) присваивается реквизит МинОстаток из карточки номенклатуры? Какая связь между минимальным складским остатком товара и ценой его продажи у комиссионера? Давайте разбираться.

---

Причины появления конструкции: Ограничения SQL-запросов в 1С 7.7

Для того чтобы понять логику авторов конфигурации, нам необходимо вернуться в начало 2000-х годов, когда активно развивалась SQL-версия платформы 1С:Предприятие 7.7.

В файловой версии (DBF) встроенный язык запросов прощал программистам множество вольностей. Однако при работе с базой данных MS SQL Server встроенные запросы 1С транслировались платформой в реальные SQL-запросы (через механизм T-SQL). При формировании многорегистрового запроса, который одновременно собирает данные из регистра остатков ОстаткиТМЦ и регистра партий ПартииОтданные, СУБД требует строгого сопоставления типов данных в результирующих временных таблицах.

Проанализируем структуру регистров:

  1. Регистр ОстаткиТМЦ имеет измерение ЦенаПрод (тип "Число").
  2. Регистр ПартииОтданные не имеет ни такого измерения, ни реквизита с аналогичным типом данных.

Если бы мы написали запрос простым сопоставлением только для первого регистра:

ЦенаПрод = Регистр.ОстаткиТМЦ.ЦенаПрод;

то при трансляции запроса в СУБД MS SQL Server для записей регистра ПартииОтданные поле ЦенаПрод осталось бы незаполненным (или имело бы тип NULL/неопределено). При выполнении группировки по этому полю SQL-сервер выдавал ошибку трансляции типов, либо отчет падал в аварийный режим, либо некорректно схлопывал остатки по комиссионерам.

Встроенный язык запросов 1С 7.7 не позволял напрямую присвоить константу (например, ЦенаПрод = 0 или ЦенаПрод = "") для сопоставления с полем регистра во второй части выражения. Платформа требовала, чтобы через запятую было указано реальное поле базы данных или реквизит объекта аналогичного типа (в данном случае — "Число").

В качестве "числового донора" разработчики "1С" выбрали реквизит Номенклатура.МинОстаток. Логика была проста: этот реквизит имеет числовой тип данных и почти всегда равен нулю у большинства пользователей, которые не используют автозаказ. Таким образом, для комиссионных товаров цена продажи в запросе условно занулялась, что позволяло успешно выполнить группировку и обойти жесткие ограничения SQL-транслятора.

---

Как этот "костыль" ломает отчет на практике и как его исправить

Несмотря на то, что этот код помог решить проблему совместимости с СУБД, он несет в себе скрытую угрозу. Если ваше предприятие реально использует функционал контроля минимальных остатков на складах и заполняет реквизит МинОстаток в карточках товаров, отчет ОстаткиТМЦ начнет выводить недостоверные данные по комиссионерам.

Посмотрим на результат выполнения отчета в такой ситуации: значение минимального остатка из карточки товара будет ошибочно выведено в колонку "Отпускная цена" для строк с комиссионным товаром. Более того, если у вас есть остатки товара на розничном складе (с реальной ценой продажи) и у комиссионера (где в качестве цены подставился минимальный остаток), то при несовпадении этих цифр строка товара в отчете раздвоится. Это крайне усложняет анализ остатков.

Как решить эту проблему корректно?

Рассмотрим несколько вариантов решения этой проблемы:

Вариант 1. Разделение запросов (Рекомендуемый для штатных средств)

Самый правильный способ с точки зрения классического программирования в 1С 7.7 — отказаться от сборки данных разных регистров в один сложный запрос. Вместо этого мы можем:

  1. Первым запросом получить остатки на собственных складах из регистра ОстаткиТМЦ с группировкой по розничным ценам.
  2. Вторым запросом получить остатки у комиссионеров из регистра ПартииОтданные (где розничная цена по определению отсутствует или заполняется из других источников).
  3. Выгрузить результаты обоих запросов в одну общую Таблицу Значений (ТЗ), программно сопоставить и свернуть данные, заполнив цену для комиссионеров нулем или получив ее из актуального типа цен.

Вариант 2. Переход на прямые SQL-запросы (1С++)

Если ваша база данных работает на 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С 7.7 можно встретить конструкции вида Группировка Номенклатура Все; или Группировка Склад Все ВошедшиеВЗапрос;. Непонимание этих механизмов ведет к сильному замедлению работы отчетов. Выясним разницу между ними на простых примерах.

Представим, что у нас есть следующая структура данных в базе:

1. Обычная группировка (без ключевых слов)

Рассмотрим простую группировку:


Группировка Склад;
Группировка Товар;

В результат запроса попадут только те строки, по которым реально есть остатки:

2. Группировка с ключевым словом "Все"

Добавим к группировке товаров слово Все:


Группировка Склад;
Группировка Товар Все;

В этом случае платформа выполнит полное декартово произведение складов и всех элементов справочника "Номенклатура", даже если остатков по ним нет. Результат будет выглядеть так:

Такой вариант используется крайне редко, так как он сильно раздувает выборку и замедляет работу отчета, особенно если справочник товаров насчитывает десятки тысяч позиций.

3. Группировка "Все ВошедшиеВЗапрос"

Это специальный режим, созданный в основном для построения удобных кросс-таблиц (шахматок), когда склады нужно вывести в колонки, а товары — в строки. Напишем код:


Группировка Склад;
Группировка Товар Все ВошедшиеВЗапрос;

Этот режим работает умнее. Платформа анализирует, какие товары вообще попали в выборку (в нашем случае это Товар 1 и Товар 2, так как по ним есть движения), и строит полную матрицу только для них, исключая Товар 3, по которому остатков вообще не было ни на одном складе:

Это позволяет получить ровную сетку данных для вывода в печатную форму "по колонкам", не выводя при этом лишний "мусор" из справочника номенклатуры.

Важный нюанс производительности: Использование ключевого слова Все на верхних уровнях группировки (например, Группировка Номенклатура Все;) заставляет платформу опрашивать весь справочник и сопоставлять его с результатами выборки. Если вам нужно получить только реальные остатки по фильтру, никогда не пишите Все. Убрав это слово, вы ускорите выполнение запроса в 10–15 раз!

← На главную