В практике разработки на платформе 1С:Предприятие часто возникает задача контроля данных при проведении документов. Рассмотрим классическую ситуацию: у нас есть документ возврата оборудования, и нам необходимо убедиться, что возвращаемая номенклатура действительно ранее отгружалась этому контрагенту, и возвращаемое количество не превышает отгруженное.
Простая итерация (обход циклом) двух таблиц значений — это неэффективный и медленный путь. Разберем профессиональный подход, использующий мощь языка запросов 1С и временных таблиц.
У нас есть две вводные:
Нам нужно сравнить эти данные, чтобы:
Начинающие разработчики часто пытаются решить эту задачу методом вложенных циклов: берут строку из документа и бегут поиском по таблице регистра. При большом объеме данных это приводит к квадратичной сложности алгоритма и существенному падению производительности. Кроме того, сложно корректно обработать дубли строк в документе.
Мы пойдем другим путем: используем Менеджер временных таблиц и Язык запросов.
Прежде чем передать таблицу значений (ТЗ) из документа в запрос, мы должны убедиться, что она готова. Самый важный нюанс, о котором часто забывают — типизация колонок.
Запрос в 1С не может работать с колонками, тип которых не определен или является произвольным. Если вы формируете ТЗ программно, обязательно задавайте типы явно.
// Пример создания типизированной таблицы значений
ТЗ_Документ = Новый ТаблицаЗначений;
ТЗ_Документ.Колонки.Добавить("Контрагент", Новый ОписаниеТипов("СправочникСсылка.Контрагенты"));
ТЗ_Документ.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТЗ_Документ.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));
// Заполняем ТЗ данными из табличной части документа
// ... код заполнения ...
Также перед сравнением крайне рекомендуется свернуть таблицу документа, чтобы исключить дубли строк (например, если пользователь дважды добавил один и тот же товар в документ). Это упростит дальнейшую математику.
ТЗ_Документ.Свернуть("Контрагент, Номенклатура", "Количество");
Чтобы соединить данные из оперативной памяти (наша ТЗ) с данными из базы данных (Регистр), мы используем объект МенеджерВременныхТаблиц. Он позволяет "поместить" нашу таблицу в контекст запроса.
Разберем алгоритм по шагам:
В обсуждении задачи часто предлагается использовать Внутреннее соединение (Inner Join). Давайте проанализируем, подходит ли оно.
При внутреннем соединении в результат попадут только те записи, которые есть и в документе, и в регистре. Если мы просто хотим найти соответствия — это сработает. Но если наша цель — найти ошибки (например, попытку вернуть товар, которого нет в отгрузках), то внутреннее соединение скроет от нас проблемные строки документа.
Поэтому правильнее использовать Левое соединение (Left Join), где главной таблицей выступает таблица документа.
Логика проверки:
NULL — значит, отгрузки не было вовсе. Ошибка!КоличествоВДокументе > ОстатокПоРегистру — значит, пытаются вернуть больше, чем числится. Ошибка!Давайте посмотрим на итоговый пример кода, который реализует правильную проверку с учетом количества и наличия отгрузки.
Запрос = Новый Запрос;
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
// 1. Помещаем таблицу из документа во временную таблицу
// Важно: предполагается, что ТЗ_Документ типизирована и свернута
Запрос.Текст =
"ВЫБРАТЬ
| ТЗ.Контрагент КАК Контрагент,
| ТЗ.Номенклатура КАК Номенклатура,
| ТЗ.Количество КАК КоличествоДок
|ПОМЕСТИТЬ ВТ_Документ
|ИЗ
| &ТЗ_Документ КАК ТЗ
|ИНДЕКСИРОВАТЬ ПО
| Контрагент,
| Номенклатура";
Запрос.УстановитьПараметр("ТЗ_Документ", ТЗ_Документ);
Запрос.Выполнить();
// 2. Основной запрос: соединяем документ с остатками регистра
// Используем виртуальную таблицу Остатки для получения актуальных данных
Запрос.Текст =
"ВЫБРАТЬ
| ВТ_Документ.Контрагент,
| ВТ_Документ.Номенклатура,
| ВТ_Документ.КоличествоДок,
| ЕСТЬNULL(ОборудованиеОстатки.КоличествоОстаток, 0) КАК КоличествоНаРуках
|ИЗ
| ВТ_Документ КАК ВТ_Документ
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОборудованиеУКлиентов.Остатки(
| &МоментВремени,
| (Контрагент, Номенклатура) В
| (ВЫБРАТЬ
| Т.Контрагент,
| Т.Номенклатура
| ИЗ
| ВТ_Документ КАК Т)
| ) КАК ОборудованиеОстатки
| ПО ВТ_Документ.Контрагент = ОборудованиеОстатки.Контрагент
| И ВТ_Документ.Номенклатура = ОборудованиеОстатки.Номенклатура
|ГДЕ
| (ЕСТЬNULL(ОборудованиеОстатки.КоличествоОстаток, 0) < ВТ_Документ.КоличествоДок)";
// Устанавливаем момент времени, чтобы исключить движения самого документа,
// если он перепроводится
Запрос.УстановитьПараметр("МоментВремени", ДокументСсылка.МоментВремени());
Результат = Запрос.Выполнить();
Если НЕ Результат.Пустой() Тогда
// Обнаружены ошибки!
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщение = Новый СообщениеПользователю;
ТекстОшибки = СтрШаблон("Ошибка возврата: %1. Возвращается: %2 шт., числится за клиентом: %3 шт.",
Выборка.Номенклатура,
Выборка.КоличествоДок,
Выборка.КоличествоНаРуках);
Сообщение.Текст = ТекстОшибки;
Сообщение.Сообщить();
КонецЦикла;
Отказ = Истина; // Запрещаем проведение документа
КонецЕсли;
Обратите внимание на конструкцию внутри параметров виртуальной таблицы .Остатки(...). Мы передаем туда отбор:
(Контрагент, Номенклатура) В (ВЫБРАТЬ ... ИЗ ВТ_Документ ...)
Это критически важно для производительности. Если не установить этот отбор, система сначала рассчитает остатки по всему регистру (по всем клиентам и товарам), и только потом соединит их с вашим маленьким документом. Передавая список из временной таблицы внутрь параметров виртуальной таблицы, мы заставляем 1С рассчитывать остатки только по нужным нам позициям.
Для корректной работы в многопользовательском режиме не забывайте про блокировки. Перед чтением остатков необходимо установить БлокировкаДанных на регистр по набору измерений (Контрагент, Номенклатура), присутствующих в документе. Иначе возможна ситуация, когда два менеджера одновременно проведут возврат одного и того же "последнего" холодильника, загнав остаток в минус.
Резюме:
ПОМЕСТИТЬ).ЛЕВОЕ СОЕДИНЕНИЕ таблицы документа с таблицей остатков регистра.КоличествоВДокументе > КоличествоОстаток.