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

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

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

Простая итерация (обход циклом) двух таблиц значений — это неэффективный и медленный путь. Разберем профессиональный подход, использующий мощь языка запросов 1С и временных таблиц.

Постановка задачи

У нас есть две вводные:

  1. Таблица из документа: содержит список того, что клиент хочет вернуть (Номенклатура, Количество, Контрагент).
  2. Таблица из регистра накопления (или результат запроса к регистру): содержит историю отгрузок или текущие остатки оборудования у клиентов.

Нам нужно сравнить эти данные, чтобы:

Почему итерация циклом — плохое решение

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

Мы пойдем другим путем: используем Менеджер временных таблиц и Язык запросов.

Этап 1: Подготовка Таблицы Значений

Прежде чем передать таблицу значений (ТЗ) из документа в запрос, мы должны убедиться, что она готова. Самый важный нюанс, о котором часто забывают — типизация колонок.

Запрос в 1С не может работать с колонками, тип которых не определен или является произвольным. Если вы формируете ТЗ программно, обязательно задавайте типы явно.


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

// Заполняем ТЗ данными из табличной части документа
// ... код заполнения ...

Также перед сравнением крайне рекомендуется свернуть таблицу документа, чтобы исключить дубли строк (например, если пользователь дважды добавил один и тот же товар в документ). Это упростит дальнейшую математику.

ТЗ_Документ.Свернуть("Контрагент, Номенклатура", "Количество");

Этап 2: Использование Менеджера временных таблиц

Чтобы соединить данные из оперативной памяти (наша ТЗ) с данными из базы данных (Регистр), мы используем объект МенеджерВременныхТаблиц. Он позволяет "поместить" нашу таблицу в контекст запроса.

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

  1. Создаем менеджер временных таблиц.
  2. Создаем запрос и привязываем к нему менеджер.
  3. Передаем ТЗ как параметр и помещаем её во временную таблицу.
  4. Выполняем соединение с данными регистра.

Этап 3: Выбор типа соединения (Inner Join vs Left Join)

В обсуждении задачи часто предлагается использовать Внутреннее соединение (Inner Join). Давайте проанализируем, подходит ли оно.

При внутреннем соединении в результат попадут только те записи, которые есть и в документе, и в регистре. Если мы просто хотим найти соответствия — это сработает. Но если наша цель — найти ошибки (например, попытку вернуть товар, которого нет в отгрузках), то внутреннее соединение скроет от нас проблемные строки документа.

Поэтому правильнее использовать Левое соединение (Left Join), где главной таблицей выступает таблица документа.

Логика проверки:

Реализация решения в коде

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


Запрос = Новый Запрос;
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;

// 1. Помещаем таблицу из документа во временную таблицу
// Важно: предполагается, что ТЗ_Документ типизирована и свернута
Запрос.Текст = 
    "ВЫБРАТЬ
    |   ТЗ.Контрагент КАК Контрагент,
    |   ТЗ.Номенклатура КАК Номенклатура,
    |   ТЗ.Количество КАК КоличествоДок
    |ПОМЕСТИТЬ ВТ_Документ
    |ИЗ
    |   &ТЗ_Документ КАК ТЗ
    |ИНДЕКСИРОВАТЬ ПО
    |   Контрагент,
    |   Номенклатура";
    
Запрос.УстановитьПараметр("ТЗ_Документ", ТЗ_Документ);
Запрос.Выполнить();

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

// Устанавливаем момент времени, чтобы исключить движения самого документа, 
// если он перепроводится
Запрос.УстановитьПараметр("МоментВремени", ДокументСсылка.МоментВремени());

Результат = Запрос.Выполнить();

Если НЕ Результат.Пустой() Тогда
    // Обнаружены ошибки!
    Выборка = Результат.Выбрать();
    Пока Выборка.Следующий() Цикл
        Сообщение = Новый СообщениеПользователю;
        ТекстОшибки = СтрШаблон("Ошибка возврата: %1. Возвращается: %2 шт., числится за клиентом: %3 шт.", 
                                Выборка.Номенклатура, 
                                Выборка.КоличествоДок, 
                                Выборка.КоличествоНаРуках);
        Сообщение.Текст = ТекстОшибки;
        Сообщение.Сообщить();
    КонецЦикла;
    
    Отказ = Истина; // Запрещаем проведение документа
КонецЕсли;

Разбор ключевых моментов решения

Обратите внимание на конструкцию внутри параметров виртуальной таблицы .Остатки(...). Мы передаем туда отбор:

(Контрагент, Номенклатура) В (ВЫБРАТЬ ... ИЗ ВТ_Документ ...)

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

Дополнительные рекомендации

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

Резюме:

  1. Не используйте циклы для сравнения таблиц.
  2. Типизируйте колонки таблицы значений документа.
  3. Помещайте данные документа во временную таблицу (ПОМЕСТИТЬ).
  4. Используйте ЛЕВОЕ СОЕДИНЕНИЕ таблицы документа с таблицей остатков регистра.
  5. Проверяйте условие КоличествоВДокументе > КоличествоОстаток.
← На главную