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