Как в черном запросе 1С 7.7 отфильтровать строки с нулевыми итогами?

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

При построении отчетов и анализе регистров в 1С:Предприятии 7.7 часто возникает классическая задача: получить выборку номенклатуры по конкретному документу и оставить только те позиции, по которым остались неотгруженные остатки. Однако разработчики нередко сталкиваются с ситуацией, когда стандартные секции условий запроса либо вызывают ошибку, либо не дают желаемого результата, оставляя в отчете строки с нулевыми итогами.

Разберем детально, почему так происходит, какие ограничения заложены в механизм встроенного («черного») языка запросов 1С 7.7, а также рассмотрим надежные практические способы решения этой задачи.

В чем заключается проблема штатного запроса

Рассмотрим типовой текст запроса к оборотному регистру, с которого часто начинаются попытки решить подобную задачу:


|Период с ДатаН;
|Заказ        = Регистр.Заказы.Заказ;
|Номенклатура = Регистр.Заказы.Номенклатура;
|КолЗаказано  = Регистр.Заказы.Заказано;
|КолОтгружено = Регистр.Заказы.Отгружено;
|Функция Заказано    = Сумма(КолЗаказано);
|Функция НеОтгружено = Сумма(КолЗаказано - КолОтгружено);
|Группировка Номенклатура Без Упорядочивания Без Групп;
|Условие(Заказ в ВыбДок);

В приведенном тексте разработчики сталкиваются сразу с двумя архитектурными особенностями платформы 1С 7.7:

  1. Необходимость прямой функции ресурса: Если исключить строку Функция Заказано = Сумма(КолЗаказано); и оставить только выражение разницы Функция НеОтгружено = Сумма(КолЗаказано - КолОтгружено);, то результат запроса окажется полностью пустым. Оптимизатор чтения таблиц регистров (файлов формата .DBF) устроен так, что ресурс считывается в буфер только тогда, когда он явно фигурирует в простой агрегатной функции Сумма(Ресурс). Если же в функции записано сложное составное выражение, движок запроса не регистрирует зависимость от ресурсов, и переменные в формуле остаются нулевыми.
  2. Отсутствие аналога секции HAVING: Конструкция Условие(...) в 1С 7.7 работает исключительно на этапе обхода первичных движений (уровень SQL-фильтра WHERE). Попытка прописать проверку итогов, например Условие(НеОтгружено <> 0);, завершается ошибкой неопределенного поля, а проверка вида Условие(Запрос.НеОтгружено <> 0); не отсекает строку группировки, поскольку вычисляется непосредственно при обработке каждой записи, а не после сведения итогов по группировке Номенклатура. Добавление модификатора Когда(...) также не исключает позицию номенклатуры из результирующего дерева — оно лишь обнуляет приращение функции по конкретной записи.

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

Способ 1. Простая фильтрация при обходе выборки

Самый простой, надежный и стандартный для 1С 7.7 способ — перенести фильтрацию итогов в цикл обработки результатов. Для этого уберем составные формулы внутри агрегатов, посчитаем каждый ресурс отдельно и проверим разницу при обходе группировок.

Рассмотрим текст запроса и процедуру его обработки:


Перем Запрос, ТекстЗапроса;

Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса = "
|Период с ДатаН;
|Заказ        = Регистр.Заказы.Заказ;
|Номенклатура = Регистр.Заказы.Номенклатура;
|КолЗаказано  = Регистр.Заказы.Заказано;
|КолОтгружено = Регистр.Заказы.Отгружено;
|Функция Заказано  = Сумма(КолЗаказано);
|Функция Отгружено = Сумма(КолОтгружено);
|Группировка Номенклатура Без Упорядочивания Без Групп;
|Условие(Заказ в ВыбДок);
|";

Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
    Возврат;
КонецЕсли;

Пока Запрос.Группировка("Номенклатура") = 1 Цикл
    Остаток = Запрос.Заказано - Запрос.Отгружено;
    Если Остаток = 0 Тогда
        Продолжить; // Пропускаем полностью отгруженные позиции
    КонецЕсли;
    
    // Обработка строк, где неотгруженное количество не равно нулю
    Сообщить("Номенклатура: " + Запрос.Номенклатура + ", Осталось отгрузить: " + Остаток);
КонецЦикла;

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

Способ 2. Выгрузка в Таблицу Значений с последующим удалением лишних строк

Если результат запроса требуется отобразить в табличной части экранной формы или передать в другие процедуры, логично использовать объект ТаблицаЗначений. Мы выгружаем результат запроса целиком, после чего удаляем нулевые строки.

Разберем последовательность действий:

  1. Выполняем запрос с раздельными функциями суммирования по ресурсам.
  2. Выгружаем данные методом Запрос.Выгрузить(ТЗ, 0, 0);.
  3. Добавляем колонку для разницы или выполняем проверку прямо в цикле.
  4. Обходим строки таблицы значений с конца к началу, чтобы корректно удалить строки с нулевым остатком.

Посмотрим на пример программной реализации:


ТЗ = СоздатьОбъект("ТаблицаЗначений");
Запрос.Выгрузить(ТЗ, 1, 0);

ТЗ.НоваяКолонка("НеОтгружено", "Число", 15, 3);
Для НомерСтроки = 1 По ТЗ.КоличествоСтрок() Цикл
    ТЗ.УстановитьЗначение(НомерСтроки, "НеОтгружено", ТЗ.ПолучитьЗначение(НомерСтроки, "Заказано") - ТЗ.ПолучитьЗначение(НомерСтроки, "Отгружено"));
КонецЦикла;

// Удаляем пустые строки с конца таблицы
Для НомерСтроки = -ТЗ.КоличествоСтрок() По -1 Цикл
    ТекСтрока = -НомерСтроки;
    Если ТЗ.ПолучитьЗначение(ТекСтрока, "НеОтгружено") = 0 Тогда
        ТЗ.УдалитьСтроку(ТекСтрока);
    КонецЕсли;
КонецЦикла;

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

Способ 3. Прямая выборка движений регистра без механизма запросов

Если в регистре для измерения Заказ включен признак отбора («Быстрый отбор»), задачу для конкретного документа можно решить значительно быстрее и проще без использования тяжеловесного объекта Запрос, который в файловом режиме создает временные индексы на диске.

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


Рег = СоздатьОбъект("Регистр.Заказы");
Рег.УстановитьЗначениеФильтра("Заказ", ВыбДок, 1);

ТЗ = СоздатьОбъект("ТаблицаЗначений");
ТЗ.НоваяКолонка("Номенклатура", "Справочник.Номенклатура");
ТЗ.НоваяКолонка("Заказано", "Число", 15, 3);
ТЗ.НоваяКолонка("Отгружено", "Число", 15, 3);

Рег.ВыбратьДвижения(ДатаН, РабочаяДата());
Пока Рег.ПолучитьДвижение() = 1 Цикл
    ТЗ.НоваяСтрока();
    ТЗ.Номенклатура = Рег.Номенклатура;
    ТЗ.Заказано     = Рег.Заказано;
    ТЗ.Отгружено    = Рег.Отгружено;
КонецЦикла;

ТЗ.Свернуть("Номенклатура", "Заказано,Отгружено");

// Отсекаем строки, где все закрыто в ноль
Для Стр = -ТЗ.КоличествоСтрок() По -1 Цикл
    ТекСтр = -Стр;
    Если (ТЗ.ПолучитьЗначение(ТекСтр, "Заказано") - ТЗ.ПолучитьЗначение(ТекСтр, "Отгружено")) = 0 Тогда
        ТЗ.УдалитьСтроку(ТекСтр);
    КонецЕсли;
КонецЦикла;

Для отдельных документов выборка движений по установленному фильтру работает заметно быстрее штатного запроса в файловых информационных базах.

Способ 4. Использование прямых SQL-запросов (1С++ / 1SQLite)

Когда требуется выполнить строгую фильтрацию сгруппированных данных на уровне базы данных без выгрузки лишних строк в память 1С, применяют внешнюю компоненту 1С++ и модуль 1SQLite (для файловых DBF-баз). В прямом SQL-запросе доступна стандартная конструкция HAVING, которой не хватает штатному парсеру 7.7.

В этом случае логика отбора переносится непосредственно в SQL-выражение:


SELECT 
    Номенклатура as [Номенклатура $Справочник.Номенклатура],
    SUM(Заказано) - SUM(Отгружено) as [НеОтгружено]
FROM 
    [Регистр.Заказы]
WHERE 
    Заказ = :ВыбДок
GROUP BY 
    Номенклатура
HAVING 
    (SUM(Заказано) - SUM(Отгружено)) <> 0

Прямой запрос моментально возвращает готовую выборку строк без необходимости выполнять дополнительную ручную фильтрацию в цикле.

← На главную