При построении отчетов и анализе регистров в 1С:Предприятии 7.7 часто возникает классическая задача: получить выборку номенклатуры по конкретному документу и оставить только те позиции, по которым остались неотгруженные остатки. Однако разработчики нередко сталкиваются с ситуацией, когда стандартные секции условий запроса либо вызывают ошибку, либо не дают желаемого результата, оставляя в отчете строки с нулевыми итогами.
Разберем детально, почему так происходит, какие ограничения заложены в механизм встроенного («черного») языка запросов 1С 7.7, а также рассмотрим надежные практические способы решения этой задачи.
Рассмотрим типовой текст запроса к оборотному регистру, с которого часто начинаются попытки решить подобную задачу:
|Период с ДатаН;
|Заказ = Регистр.Заказы.Заказ;
|Номенклатура = Регистр.Заказы.Номенклатура;
|КолЗаказано = Регистр.Заказы.Заказано;
|КолОтгружено = Регистр.Заказы.Отгружено;
|Функция Заказано = Сумма(КолЗаказано);
|Функция НеОтгружено = Сумма(КолЗаказано - КолОтгружено);
|Группировка Номенклатура Без Упорядочивания Без Групп;
|Условие(Заказ в ВыбДок);
В приведенном тексте разработчики сталкиваются сразу с двумя архитектурными особенностями платформы 1С 7.7:
Функция Заказано = Сумма(КолЗаказано); и оставить только выражение разницы Функция НеОтгружено = Сумма(КолЗаказано - КолОтгружено);, то результат запроса окажется полностью пустым. Оптимизатор чтения таблиц регистров (файлов формата .DBF) устроен так, что ресурс считывается в буфер только тогда, когда он явно фигурирует в простой агрегатной функции Сумма(Ресурс). Если же в функции записано сложное составное выражение, движок запроса не регистрирует зависимость от ресурсов, и переменные в формуле остаются нулевыми.Условие(...) в 1С 7.7 работает исключительно на этапе обхода первичных движений (уровень SQL-фильтра WHERE). Попытка прописать проверку итогов, например Условие(НеОтгружено <> 0);, завершается ошибкой неопределенного поля, а проверка вида Условие(Запрос.НеОтгружено <> 0); не отсекает строку группировки, поскольку вычисляется непосредственно при обработке каждой записи, а не после сведения итогов по группировке Номенклатура. Добавление модификатора Когда(...) также не исключает позицию номенклатуры из результирующего дерева — оно лишь обнуляет приращение функции по конкретной записи.Поскольку встроенный транслятор штатных запросов не умеет отсекать строки по агрегатным итогам, перейдем к рассмотрению правильных практических вариантов обхода этого ограничения.
Самый простой, надежный и стандартный для 1С 7.7 способ — перенести фильтрацию итогов в цикл обработки результатов. Для этого уберем составные формулы внутри агрегатов, посчитаем каждый ресурс отдельно и проверим разницу при обходе группировок.
Рассмотрим текст запроса и процедуру его обработки:
Перем Запрос, ТекстЗапроса;
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса = "
|Период с ДатаН;
|Заказ = Регистр.Заказы.Заказ;
|Номенклатура = Регистр.Заказы.Номенклатура;
|КолЗаказано = Регистр.Заказы.Заказано;
|КолОтгружено = Регистр.Заказы.Отгружено;
|Функция Заказано = Сумма(КолЗаказано);
|Функция Отгружено = Сумма(КолОтгружено);
|Группировка Номенклатура Без Упорядочивания Без Групп;
|Условие(Заказ в ВыбДок);
|";
Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
Возврат;
КонецЕсли;
Пока Запрос.Группировка("Номенклатура") = 1 Цикл
Остаток = Запрос.Заказано - Запрос.Отгружено;
Если Остаток = 0 Тогда
Продолжить; // Пропускаем полностью отгруженные позиции
КонецЕсли;
// Обработка строк, где неотгруженное количество не равно нулю
Сообщить("Номенклатура: " + Запрос.Номенклатура + ", Осталось отгрузить: " + Остаток);
КонецЦикла;
Этот подход гарантирует правильное считывание полей регистра, не перегружает временные таблицы сложными вычислениями и не требует изменения структуры конфигурации.
Если результат запроса требуется отобразить в табличной части экранной формы или передать в другие процедуры, логично использовать объект ТаблицаЗначений. Мы выгружаем результат запроса целиком, после чего удаляем нулевые строки.
Разберем последовательность действий:
Запрос.Выгрузить(ТЗ, 0, 0);.Посмотрим на пример программной реализации:
ТЗ = СоздатьОбъект("ТаблицаЗначений");
Запрос.Выгрузить(ТЗ, 1, 0);
ТЗ.НоваяКолонка("НеОтгружено", "Число", 15, 3);
Для НомерСтроки = 1 По ТЗ.КоличествоСтрок() Цикл
ТЗ.УстановитьЗначение(НомерСтроки, "НеОтгружено", ТЗ.ПолучитьЗначение(НомерСтроки, "Заказано") - ТЗ.ПолучитьЗначение(НомерСтроки, "Отгружено"));
КонецЦикла;
// Удаляем пустые строки с конца таблицы
Для НомерСтроки = -ТЗ.КоличествоСтрок() По -1 Цикл
ТекСтрока = -НомерСтроки;
Если ТЗ.ПолучитьЗначение(ТекСтрока, "НеОтгружено") = 0 Тогда
ТЗ.УдалитьСтроку(ТекСтрока);
КонецЕсли;
КонецЦикла;
Такой подход удобен, если отфильтрованные данные необходимо далее сортировать, сворачивать или связывать с другими таблицами в памяти.
Если в регистре для измерения Заказ включен признак отбора («Быстрый отбор»), задачу для конкретного документа можно решить значительно быстрее и проще без использования тяжеловесного объекта Запрос, который в файловом режиме создает временные индексы на диске.
Проанализируем алгоритм:
Рег = СоздатьОбъект("Регистр.Заказы");
Рег.УстановитьЗначениеФильтра("Заказ", ВыбДок, 1);
ТЗ = СоздатьОбъект("ТаблицаЗначений");
ТЗ.НоваяКолонка("Номенклатура", "Справочник.Номенклатура");
ТЗ.НоваяКолонка("Заказано", "Число", 15, 3);
ТЗ.НоваяКолонка("Отгружено", "Число", 15, 3);
Рег.ВыбратьДвижения(ДатаН, РабочаяДата());
Пока Рег.ПолучитьДвижение() = 1 Цикл
ТЗ.НоваяСтрока();
ТЗ.Номенклатура = Рег.Номенклатура;
ТЗ.Заказано = Рег.Заказано;
ТЗ.Отгружено = Рег.Отгружено;
КонецЦикла;
ТЗ.Свернуть("Номенклатура", "Заказано,Отгружено");
// Отсекаем строки, где все закрыто в ноль
Для Стр = -ТЗ.КоличествоСтрок() По -1 Цикл
ТекСтр = -Стр;
Если (ТЗ.ПолучитьЗначение(ТекСтр, "Заказано") - ТЗ.ПолучитьЗначение(ТекСтр, "Отгружено")) = 0 Тогда
ТЗ.УдалитьСтроку(ТекСтр);
КонецЕсли;
КонецЦикла;
Для отдельных документов выборка движений по установленному фильтру работает заметно быстрее штатного запроса в файловых информационных базах.
Когда требуется выполнить строгую фильтрацию сгруппированных данных на уровне базы данных без выгрузки лишних строк в память 1С, применяют внешнюю компоненту 1С++ и модуль 1SQLite (для файловых DBF-баз). В прямом SQL-запросе доступна стандартная конструкция HAVING, которой не хватает штатному парсеру 7.7.
В этом случае логика отбора переносится непосредственно в SQL-выражение:
SELECT
Номенклатура as [Номенклатура $Справочник.Номенклатура],
SUM(Заказано) - SUM(Отгружено) as [НеОтгружено]
FROM
[Регистр.Заказы]
WHERE
Заказ = :ВыбДок
GROUP BY
Номенклатура
HAVING
(SUM(Заказано) - SUM(Отгружено)) <> 0
Прямой запрос моментально возвращает готовую выборку строк без необходимости выполнять дополнительную ручную фильтрацию в цикле.