При разработке внешних отчетов и обработок в 1С:Предприятие 7.7 программисты часто сталкиваются с ситуацией, когда стандартный метод получения остатков работает в форме списка справочника, но абсолютно тот же код во внешней обработке стабильно возвращает нулевые значения. Давайте разберем по шагам, почему так происходит, выясним причину такого поведения платформы и проанализируем способы оптимизации работы с регистрами.
Рассмотрим типичный пример кода, который вызывает проблему. Допустим, мы перебираем элементы справочника и пытаемся получить остатки по материалам на складе:
Спр.ВыбратьЭлементы();
Пока Спр.ПолучитьЭлемент() = 1 Цикл
Материал = Спр.ТекущийЭлемент();
ОстатокНаСкладе = Регистр.Остатки.Остаток(Материал, "Кво");
КонецЦикла;
На первый взгляд, код выглядит абсолютно корректным. Однако во внешнем отчете переменная ОстатокНаСкладе всегда равна нулю. Почему 1С "молча" выдает ноль, не сообщая об ошибке?
Особенность платформы 1С 7.7 заключается в том, что методы получения остатков (например, Остаток) не производят жесткой типизации входных параметров с прерыванием выполнения скрипта. Если мы передаем в качестве измерения значение не того типа или вида, платформа просто формирует условие поиска в базе данных, по которому ничего не находится. Следовательно, функция штатно возвращает 0.
В нашем конкретном случае проблема кроется в том, что справочник, по которому идет цикл, является подчиненным. В регистре в качестве измерения выступает не сам элемент подчиненного справочника, а его реквизит (например, ТМЦ). Передавая Спр.ТекущийЭлемент(), мы передаем ссылку на подчиненный справочник, а регистр ожидает ссылку на справочник материалов.
Исправим ошибку, передав правильный реквизит:
// Неправильно:
// Материал = Спр.ТекущийЭлемент();
// Правильно:
Материал = Спр.Материал;
ОстатокНаСкладе = Регистр.Остатки.Остаток(Материал, "Кво");
Также всегда проверяйте, не передаете ли вы группу справочника вместо элемента. Используйте проверке ЭтоГруппа() = 0 и контролируйте типы через ТипЗначенияСтр().
Проанализируем производительность нашего исправленного кода. Использование метода Остаток внутри цикла Пока Спр.ПолучитьЭлемент() = 1 Цикл — это серьезная архитектурная ошибка.
Каждый раз, когда платформа встречает этот метод, она выполняет отдельное обращение к базе данных. Если в справочнике 10 000 элементов, произойдет 10 000 запросов. В сетевом режиме работы это приведет к катастрофическому замедлению отчета. Посмотрим на правильный подход — использование так называемого "черного запроса" (стандартного объекта Запрос встроенного языка).
Объект Запрос позволяет извлечь все нужные остатки по всем материалам за одно обращение к базе данных. Разберем пример оптимизированного решения:
ТекстЗапроса = "
|Период с ВыбНачПериода по ВыбКонПериода;
|Материал = Регистр.Остатки.Материал;
|Кво = Регистр.Остатки.Кво;
|Функция КвоКонОст = КонОст(Кво);
|Группировка Материал без групп;
|";
Запрос = СоздатьОбъект("Запрос");
Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
Возврат;
КонецЕсли;
Пока Запрос.Группировка("Материал") = 1 Цикл
ТекМатериал = Запрос.Материал;
ОстатокНаСкладе = Запрос.КвоКонОст;
// Дальнейшая обработка остатков
КонецЦикла;
В этом случае платформа сама рассчитает нужные итоги, поместит их в оперативную память, и мы просто переберем уже готовую группировку, что сэкономит огромное количество времени.
Рассмотрим еще один важный нюанс. Конструкция Регистр.Остатки.Остаток() обращается к текущим итогам — то есть к остаткам на Точку Актуальности (ТА). Если ваш внешний отчет должен формироваться за прошлый период или на произвольную дату, простой вызов этого метода выдаст текущие (неверные для прошлой даты) остатки.
Если по какой-то причине вы не можете использовать запрос, то для получения остатков на дату необходимо задействовать механизм временного расчета. Выполним следующие шаги:
ВременныйРасчет.РассчитатьРегистрыНа.
Регистр.ВременныйРасчет(1);
РассчитатьРегистрыНа(НужнаяДата);
// Теперь метод Остаток вернет данные на НужнаяДата
ОстатокНаДату = Регистр.Остатки.Остаток(Материал, "Кво");
Важно: механизм временного расчета работает медленно при большом количестве элементов. В стандартном запросе эта задача решается элегантнее — достаточно просто передать параметры периода в текст запроса.
Для простых задач по получению сводных данных по измерениям (без привязки к конкретному объекту регистра) можно рассмотреть глобальную функцию СводныйОстаток. Она позволяет получить баланс по определенному измерению. Но помните, что массовый вызов этой функции в цикле также приведет к просадке скорости.
Если же мы говорим об огромных базах 1С 7.7, где стандартные запросы работают медленно (из-за трансляции платформой во временные DBF-файлы), стандартом индустрии является использование компоненты 1C++.
Используя объект ODBCRecordset, мы можем выполнять прямые SQL-запросы непосредственно к таблицам итогов регистров (RA/RG). Прямой запрос обходит встроенный движок 1С, выполняется за доли секунды и полностью решает проблемы с блокировками транзакций в крупных базах данных. Однако это требует уверенных знаний языка SQL и понимания физической структуры хранения данных 1С 7.7.