Как проверить наличие позиций номенклатуры в подчиненных документах иерархической структуры 1С?

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

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

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

1. Обход структуры подчиненности кодом 1С

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

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

Для работы со структурой подчиненности в 1С существуют стандартные механизмы. Хотя некоторые конфигурации могут предлагать свои общие функции, например, ОбщегоНазначения.ПолучитьСтруктуруПодчиненности(), или методы вроде ПолучитьСписокДокументовПоКритериюОтбора(), в основе лежит итеративный или рекурсивный обход. В типовых конфигурациях мы часто можем использовать методы менеджеров документов.

Разберем по шагам:

  1. Начало обхода: Начинаем с исходного (головного) документа, который является точкой отсчета для нашей проверки.

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

    Например, если нам известен тип подчиненного документа, мы можем использовать метод ПолучитьСписокПодчиненных() у менеджера документа:

    
    // Предположим, что ДокументОснованиеСсылка - это ссылка на наш головной документ
    СписокПодчиненныхРеализаций = Документы.РеализацияТоваровУслуг.ПолучитьСписокПодчиненных(ДокументОснованиеСсылка);
    

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

  3. Проверка наличия строк: Для каждого документа в собранном списке мы должны проверить, есть ли в его табличной части "Товары" (или аналогичной, где хранится номенклатура) хотя бы одна строка. Самый эффективный способ сделать это через запрос — использовать оператор ВЫБРАТЬ ПЕРВЫЕ 1. Если запрос возвращает пустую выборку, это означает, что табличная часть пуста.

  4. Условие пропуска: Если на любом этапе обхода мы обнаруживаем документ без строк номенклатуры, мы немедленно прекращаем проверку и принимаем решение о пропуске головного документа.

Пример концептуального кода для обхода структуры и проверки:


Функция ПроверитьНаличиеПозицийВПодчиненныхДокументах(ГоловнойДокументСсылка)

    // Используем СписокЗначений для хранения всех документов, которые нужно проверить,
    // и для организации обхода "в ширину" или "в глубину".
    // Важно: в реальной задаче нужно избегать циклических ссылок, если они возможны.
    ДокументыДляПроверки = Новый СписокЗначений();
    ДокументыДляПроверки.Добавить(ГоловнойДокументСсылка); // Начинаем с головного документа

    ПроверенныеДокументы = Новый Соответствие(); // Для отслеживания уже проверенных документов и предотвращения зацикливания

    Индекс = 0;
    Пока Индекс < ДокументыДляПроверки.Количество() Цикл
        ТекущаяСсылка = ДокументыДляПроверки.Получить(Индекс).Значение;

        Если ПроверенныеДокументы.Получить(ТекущаяСсылка) <> Неопределено Тогда
            Индекс = Индекс + 1;
            Продолжить; // Этот документ уже был проверен
        КонецЕсли;

        ПроверенныеДокументы.Вставить(ТекущаяСсылка, Истина);

        // Получаем объект документа для доступа к табличным частям
        Попытка
            ТекущийДокументОбъект = ТекущаяСсылка.ПолучитьОбъект();
        Исключение
            // Возможно, документ был удален или ссылка недействительна.
            // В зависимости от логики, можно пропустить или считать ошибкой.
            Сообщить("Не удалось получить объект документа по ссылке: " + ТекущаяСсылка);
            Индекс = Индекс + 1;
            Продолжить;
        КонецПопытки;

        // Проверяем наличие строк в табличной части "Товары" (или другой, где номенклатура)
        // Важно: нужно знать имя табличной части для каждого типа документа.
        // Здесь - пример для документа с ТЧ "Товары".
        Если ТекущийДокументОбъект.Метаданные().ТабличныеЧасти.Найти("Товары") <> Неопределено Тогда
            Если ТекущийДокументОбъект.Товары.Количество() = 0 Тогда
                Возврат Ложь; // Найден документ без номенклатуры
            КонецЕсли;
        ИначеЕсли ТекущийДокументОбъект.Метаданные().ТабличныеЧасти.Найти("Услуги") <> Неопределено Тогда
            // Пример для другой ТЧ, если номенклатура может быть там
            Если ТекущийДокументОбъект.Услуги.Количество() = 0 Тогда
                Возврат Ложь;
            КонецЕсли;
        КонецЕсли;
        // Добавьте проверки для других типов табличных частей, если необходимо

        // Теперь получаем прямых потомков текущего документа
        // В зависимости от конфигурации, это можно сделать по-разному.
        // Пример для "Реализации товаров и услуг", подчиненной "Заказу клиента"
        // или универсальный подход через ссылки на основании.

        // Универсальный подход (может быть медленным для больших баз):
        ЗапросПодчиненных = Новый Запрос;
        ЗапросПодчиненных.Текст =
            "ВЫБРАТЬ РАЗЛИЧНЫЕ
            |    Док.Ссылка
            |ИЗ
            |    Документ.ВсеДокументы КАК Док
            |ГДЕ
            |    Док.Основание = &Основание";

        ЗапросПодчиненных.УстановитьПараметр("Основание", ТекущаяСсылка);
        ВыборкаПодчиненных = ЗапросПодчиненных.Выполнить().Выбрать();

        Пока ВыборкаПодчиненных.Следующий() Цикл
            Если ПроверенныеДокументы.Получить(ВыборкаПодчиненных.Ссылка) = Неопределено Тогда
                ДокументыДляПроверки.Добавить(ВыборкаПодчиненных.Ссылка);
            КонецЕсли;
        КонецЦикла;

        Индекс = Индекс + 1;
    КонецЦикла;

    Возврат Истина; // Все документы в структуре содержат номенклатуру
Кодбиг>
<p>Этот подход достаточно гибок, но его производительность может снижаться на очень больших структурах, так как он подразумевает многократное получение объектов документов и выполнение запросов.</p>

<h3>2. Использование запросов с ЛЕВОЕ СОЕДИНЕНИЕ</h3>

<p>Для оптимизации проверки, особенно если нас интересует определенный уровень подчиненности или относительно простая структура, мы можем использовать язык запросов 1С с оператором <code>ЛЕВОЕ СОЕДИНЕНИЕ</code>. Этот метод позволяет выполнять проверку на стороне СУБД, что часто более производительно.</p>

<p><strong>Принцип работы:</strong> Мы соединяем головной документ с его подчиненными документами, а затем эти подчиненные документы — с их табличными частями. Если подчиненный документ не имеет строк в своей табличной части, соответствующая запись из табличной части будет отсутствовать, и поля, полученные из нее через <code>ЛЕВОЕ СОЕДИНЕНИЕ</code>, будут иметь значение <code>NULL</code>. Это позволяет нам отфильтровать документы по отсутствию строк.</p>

<p><strong>Ограничения:</strong> Прямое использование <code>ЛЕВОЕ СОЕДИНЕНИЕ</code> в одном запросе эффективно для проверки прямой подчиненности или нескольких фиксированных уровней. Для произвольной, глубокой иерархии без ограничений на количество уровней, построение единого запроса становится чрезвычайно сложным или невозможным без использования временных таблиц или многошагового подхода.</p>

<p><strong>Пример запроса для прямой подчиненности:</strong></p>
<p>Предположим, у нас есть документ "Заказ клиента", которому напрямую подчиняется "Реализация товаров и услуг". Мы хотим найти "Заказы клиента", у которых есть хотя бы одна подчиненная "Реализация" без строк товаров.</p>
<codebig>
ВЫБРАТЬ РАЗЛИЧНЫЕ
    ЗаказКлиента.Ссылка КАК СсылкаНаЗаказКлиента
ИЗ
    Документ.ЗаказКлиента КАК ЗаказКлиента
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК Реализация
        ПО ЗаказКлиента.Ссылка = Реализация.ДокументОснование // Предполагается наличие поля "ДокументОснование"

        ЛЕВОЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК ТоварыРеализации
        ПО Реализация.Ссылка = ТоварыРеализации.Ссылка
ГДЕ
    ТоварыРеализации.Ссылка ЕСТЬ NULL // Если ссылки на ТЧ нет, значит, строк нет

Этот запрос выберет все ЗаказыКлиента, у которых есть подчиненная РеализацияТоваровУслуг, но в этой Реализации нет ни одной строки в табличной части Товары. Если ваша логика требует проверки всех типов подчиненных документов, запрос усложнится множественными ЛЕВОЕ СОЕДИНЕНИЕ с различными табличными частями и условиями.

Оптимизация проверки наличия строк в табличной части:

Если нам нужно просто быстро проверить наличие *хотя бы одной* строки в табличной части (без необходимости получать сами данные), мы можем использовать следующую конструкцию в отдельном запросе:


ВЫБРАТЬ ПЕРВЫЕ 1
    ТабличнаяЧасть.Номенклатура
ИЗ
    Документ.ИмяДокумента.ТабличнаяЧасть КАК ТабличнаяЧасть
ГДЕ
    ТабличнаяЧасть.Ссылка = &СсылкаНаДокумент

Если такой запрос возвращает пустую выборку, значит, строк нет. Этот подход очень производителен, так как СУБД прекращает поиск после нахождения первой же строки.

3. Применение специализированного регистра накопления

Для самых сложных сценариев, где требуется высокая производительность, глубокая аналитика по иерархии документов, или когда проверки становятся частью регулярных бизнес-процессов, наиболее надежным и эффективным решением может стать использование собственного РегистраНакопления.

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

Структура регистра:

  1. Измерения:

    • ГоловнойДокумент (Тип: ДокументСсылка.ЗаказКлиента, или универсальный ДокументСсылка.ВсеДокументы): Ссылка на самый верхний документ в цепочке, для которого мы хотим вести учет.
    • ПодчиненныйДокумент (Тип: ДокументСсылка.ВсеДокументы): Ссылка на текущий документ, который фиксирует движение или факт наличия строк.
    • ТипДокумента (Тип: Строка, или Перечисление, или Метаданные.Типы): Для удобства анализа и фильтрации.
  2. Ресурсы:

    • НаличиеНоменклатуры (Тип: Булево): Истина, если в документе были строки с номенклатурой; Ложь, если их не было.
    • КоличествоПозиций (Тип: Число): Количество уникальных позиций номенклатуры или общее количество строк. (Опционально, для дополнительной аналитики).
  3. Реквизиты: Дополнительная информация, если нужна.

Механизм заполнения:

Регистр заполняется при проведении каждого документа, который может быть частью структуры подчиненности. В обработчике события ОбработкаПроведения (или в модуле объекта документа) мы анализируем табличную часть документа. Если номенклатура присутствует, создаем запись с НаличиеНоменклатуры = Истина. Если нет — с НаличиеНоменклатуры = Ложь.

Также важно предусмотреть запись в регистр ссылки на "головной" документ. Это может быть реализовано путем рекурсивного поиска основного документа при проведении каждого подчиненного, что может быть ресурсоемко. Альтернативный подход — при проведении головного документа регистрировать его и все его прямые подчиненные, а те, в свою очередь, регистрируют своих потомков, и так далее, передавая ссылку на самый верхний "ГоловнойДокумент".

Пример запроса к регистру:

Чтобы проверить головной документ, мы можем запросить регистр:


ВЫБРАТЬ
    ДвиженияПоНаличиюНоменклатуры.ГоловнойДокумент
ИЗ
    РегистрНакопления.НаличиеНоменклатурыВДокументах КАК ДвиженияПоНаличиюНоменклатуры
ГДЕ
    ДвиженияПоНаличиюНоменклатуры.ГоловнойДокумент = &ТекущийГоловнойДокумент
    И ДвиженияПоНаличиюНоменклатуры.НаличиеНоменклатуры = ЛОЖЬ

Если этот запрос вернет хотя бы одну строку, это означает, что в структуре подчиненности данного головного документа есть хотя бы один документ без номенклатуры, и ГоловнойДокумент должен быть пропущен или помечен.

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

Недостатки:

Вопросы производительности и выбор метода

При выборе метода всегда следует учитывать объем данных и глубину иерархии документов. Давайте посмотрим на основные моменты:

  1. Небольшие объемы и простая иерархия (1-2 уровня): В этом случае вполне подойдут программный обход с использованием ПолучитьОбъект() и проверкой ТабличнаяЧасть.Количество(), а также запросы с ЛЕВОЕ СОЕДИНЕНИЕ. Запросы будут быстрее.

  2. Средние объемы и умеренно глубокая иерархия: Программный обход может стать узким местом из-за частых обращений к базе данных (ПолучитьОбъект()). Здесь начинаем рассматривать более сложные запросы с использованием временных таблиц (если поддерживается в вашей версии 1С и позволяет уровень вложенности) или многоступенчатые запросы. Однако, для произвольной глубины, программный обход остается основным подходом, но его нужно оптимизировать, например, кешированием уже проверенных документов и минимизацией вызовов ПолучитьОбъект(), используя запросы ВЫБРАТЬ ПЕРВЫЕ 1.

  3. Большие объемы данных и сложная, глубокая иерархия: Здесь наиболее оптимальным является создание специализированного РегистраНакопления. Он обеспечивает максимальную производительность для получения агрегированных данных, поскольку все расчеты выполняются "на лету" при проведении документов, а не во время запроса.

Ключевой момент: Всегда стремитесь выполнять максимальный объем работы на стороне СУБД с помощью запросов, так как это, как правило, гораздо быстрее, чем обработка данных в коде 1С.

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

← На главную