При разработке печатных форм в среде «1С:Предприятие» программисты часто сталкиваются с необходимостью сопоставления данных из разных документов. Классический пример — печать товарного чека или накладной из документа «Реализация товаров и услуг», когда цены или скидки необходимо получить из исходного документа «Заказ покупателя».
Ситуация значительно усложняется, когда в табличной части документа присутствуют дублирующиеся строки. Например, одна и та же номенклатура (с одинаковой серией или характеристикой) добавлена в документ несколькими строками, но по разной цене или с разными скидками. При использовании простого перебора строк в цикле система начинает во всех строках выводить цену первого найденного соответствия. В этой статье мы подробно разберем, почему возникает эта ошибка, проанализируем различные варианты её исправления и внедрим лучшие практики разработки печатных форм.
Рассмотрим фрагмент исходного кода, который часто пишут начинающие разработчики. В нем обходятся строки табличной части реализации, а для каждой строки запускается поиск по всей табличной части заказа:
Для НомерСтрокиВыборки = 2 По КоличествоСтрок Цикл
Строка = СведенияПоТабличнойЧасти[НомерСтрокиВыборки];
// ... получение стандартных цен ...
ДокумЗаказ = ВыбДокумент["Сделка"];
Если (ТипЗнч(ДокумЗаказ) = Тип("ДокументСсылка.ЗаказПокупателя")) Тогда
Для Каждого СтрокаДокумЗаказТовары из ДокумЗаказ.Товары Цикл
Если СокрЛП("" + СтрокаДокумЗаказТовары.Номенклатура.Код) = СокрЛП("" + Строка.Код)
И Строка.СерияНоменклатуры.Ссылка = СтрокаДокумЗаказТовары.СерияНоменклатуры.Ссылка Тогда
Цена = СтрокаДокумЗаказТовары.ладЦена;
Скидка = СтрокаДокумЗаказТовары.ладПроцентСкидкиНаценкиП;
Прервать; // Выход из вложенного цикла при первом совпадении
КонецЕсли;
КонецЦикла;
КонецЕсли;
// ... вывод в табличный документ ...
КонецЦикла;
Проанализируем этот алгоритм. Представим, что в заказе покупателя у нас есть две строки с одной и той же номенклатурой:
Когда цикл по строкам реализации доходит до первой строки с Товаром №1, он запускает внутренний цикл поиска по заказу. На первой же итерации внутреннего цикла условие Если выполняется, переменная Цена принимает значение 100, и срабатывает оператор Прервать. Всё работает корректно.
Однако на следующей итерации внешнего цикла, когда мы обрабатываем вторую строку с Товаром №1, внутренний цикл поиска по заказу запускается заново, с самой первой строки. Он опять натыкается на первую строку заказа (где цена 100 рублей), считает условие выполненным и прерывает работу. В итоге во вторую позицию печатной формы подставляется неверная цена. Мы столкнулись с проблемой отсутствия уникальной связи между строками.
Первый логичный способ обойти эту проблему без изменения структуры метаданных — использовать временную копию табличной части в виде объекта ТаблицаЗначений. Идея заключается в том, чтобы при каждом успешном сопоставлении удалять найденную строку из этой копии. Тогда при обработке следующего дубля система просто физически не сможет сопоставить его с уже "отработанной" строкой заказа.
Выясним, как правильно реализовать этот подход. Рассмотрим доработанный алгоритм:
// Выгружаем товары заказа в отдельную таблицу значений перед основным циклом
ТаблицаТоваровЗаказа = ВыбДокумент["Сделка"].Товары.Выгрузить();
Для НомерСтрокиВыборки = 0 По КоличествоСтрок - 1 Цикл
СтрокаРеализации = СведенияПоТабличнойЧасти[НомерСтрокиВыборки];
// Получаем базовые значения из реализации на случай, если в заказе позиция не найдется
Цена = ВыбДокумент["Товары"][СтрокаРеализации["НомерСтроки"] - 1]["Цена_Транзит"];
Скидка = ВыбДокумент["Товары"][СтрокаРеализации["НомерСтроки"] - 1]["ПроцентСкидкиНаценки"];
ДокумЗаказ = ВыбДокумент["Сделка"];
Если (ТипЗнч(ДокумЗаказ) = Тип("ДокументСсылка.ЗаказПокупателя")) Тогда
// Перебираем строки нашей временной Таблицы Значений
Для Каждого СтрокаЗаказа Из ТаблицаТоваровЗаказа Цикл
// Сравниваем ключевые поля (Код номенклатуры, Серию и Количество)
Если СокрЛП("" + СтрокаЗаказа.Номенклатура.Код) = СокрЛП("" + СтрокаРеализации.Код)
И СтрокаРеализации.СерияНоменклатуры.Ссылка = СтрокаЗаказа.СерияНоменклатуры.Ссылка
И СтрокаЗаказа.Количество = СтрокаРеализации.Количество Тогда
// Переопределяем цену и скидку данными из заказа
Цена = СтрокаЗаказа.ладЦена;
Скидка = СтрокаЗаказа.ладПроцентСкидкиНаценкиП;
// Удаляем найденную строку из временной таблицы, чтобы она больше не участвовала в поиске
ТаблицаТоваровЗаказа.Удалить(СтрокаЗаказа);
// Обязательно прерываем цикл, так как коллекция ТаблицаТоваровЗаказа была изменена!
Прервать;
КонецЕсли;
КонецЦикла;
КонецЕсли;
// Дальнейший расчет сумм и вывод параметров в макет печатной формы...
КонецЦикла;
Важное предупреждение: обратите внимание на вызов метода ТаблицаТоваровЗаказа.Удалить(СтрокаЗаказа). В платформе 1С изменение состава коллекции во время ее обхода циклом Для Каждого является потенциально опасной операцией. Однако в данном случае нас спасает немедленный вызов оператора Прервать сразу после удаления. Мы прекращаем обход измененной коллекции, поэтому системных сбоев индексов не произойдет.
Предыдущее решение полностью рабочее, но его трудно назвать элегантным и быстродействующим. Вложенные циклы создают квадратичную сложность алгоритма $O(N^2)$, что может замедлить печать документов с большим количеством строк (от 100 и более). Программисты 1С должны стремиться писать более лаконичный и производительный код.
Избавимся от внутреннего цикла Для Каждого. Для этого воспользуемся встроенным методом объекта ТаблицаЗначений — НайтиСтроки(). Этот метод принимает на вход структуру отбора и возвращает массив строк, удовлетворяющих условиям. Это работает гораздо быстрее и избавляет нас от опасного удаления элементов внутри активного цикла обхода.
Разберем данный подход по шагам:
// Подготавливаем таблицу товаров заказа
ТаблицаТоваровЗаказа = ВыбДокумент["Сделка"].Товары.Выгрузить();
Для Каждого СтрокаРеализации Из СведенияПоТабличнойЧасти Цикл
// Инициализируем переменные по умолчанию
Цена = СтрокаРеализации.Цена;
Скидка = СтрокаРеализации.Скидка;
// Формируем структуру отбора для поиска точного соответствия
ПараметрыОтбора = Новый Структура;
ПараметрыОтбора.Вставить("Номенклатура", СтрокаРеализации.Номенклатура);
ПараметрыОтбора.Вставить("СерияНоменклатуры", СтрокаРеализации.СерияНоменклатуры);
ПараметрыОтбора.Вставить("Количество", СтрокаРеализации.Количество);
// Осуществляем быстрый поиск в таблице заказа
НайденныеСтроки = ТаблицаТоваровЗаказа.НайтиСтроки(ПараметрыОтбора);
Если НайденныеСтроки.Количество() > 0 Тогда
// Берем самую первую строку из массива найденных совпадений
СтрокаЗаказа = НайденныеСтроки[0];
Цена = СтрокаЗаказа.ладЦена;
Скидка = СтрокаЗаказа.ладПроцентСкидкиНаценкиП;
// Удаляем строку из копии таблицы значений.
// Это безопасно, так как мы не находимся внутри цикла по этой таблице!
ТаблицаТоваровЗаказа.Удалить(СтрокаЗаказа);
КонецЕсли;
// Выполняем расчеты и выводим строку в табличный документ...
КонецЦикла;
Этот код читать намного проще. Он избавлен от лишней вложенности, не загромождает оперативную память лишними итерациями цикла и полностью решает проблему дублирования одинаковых номенклатурных позиций.
Хотя решения с удалением строк из таблицы значений отлично подходят для исправления существующих нетиповых механизмов, в современной разработке на платформе «1С:Предприятие 8» принято использовать более надежные архитектурные паттерны.
В современных типовых конфигурациях (таких как «1С:ERP Управление предприятием», «1С:Комплексная автоматизация» или «1С:Управление торговлей 11») связь между строками документов-оснований и документов-последователей реализуется с помощью специального реквизита — КлючСвязи (или ИдентификаторСтроки, КодСтроки).
Рассмотрим, как работает эта схема:
Если в вашей конфигурации есть такой реквизит, то задача сопоставления становится тривиальной. Вам достаточно передавать в структуру отбора метода НайтиСтроки() только один параметр — КлючСвязи:
ПараметрыОтбора = Новый Структура("КлючСвязи", СтрокаРеализации.КлючСвязи);
НайденныеСтроки = ТаблицаТоваровЗаказа.НайтиСтроки(ПараметрыОтбора);
Если НайденныеСтроки.Количество() > 0 Тогда
СтрокаЗаказа = НайденныеСтроки[0];
Цена = СтрокаЗаказа.Цена;
КонецЕсли;
Если вы разрабатываете собственную конфигурацию с нуля или существенно дорабатываете старую (например, «Управление торговлей 10.3»), мы настоятельно рекомендуем добавить реквизит КлючСвязи (тип: Число) во все товарные табличные части. Это убережет вас от костылей в коде печатных форм, отчетов и при проведении документов по регистрам.
Согласно стандартам разработки фирмы «1С», обработка больших объемов данных в циклах в оперативной памяти крайне нежелательна. Самым быстрым и правильным методом получения данных для печати является использование Запросов.
Для сопоставления одинаковых строк без уникального ключа связи в запросе применяется классический прием — автонумерация строк внутри групп. Проанализируем концептуальную схему такого запроса:
ВЫБРАТЬ
РеализацияТовары.Номенклатура КАК Номенклатура,
РеализацияТовары.СерияНоменклатуры КАК СерияНоменклатуры,
РеализацияТовары.Количество КАК Количество,
// Вычисляем порядковый номер строки для каждого одинакового товара
АВТОНОМЕРЗАПИСИ() КАК ПорядковыйНомер
ПОМЕСТИТЬ ВТ_Реализация
ИЗ
Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТовары
ГДЕ
РеализацияТовары.Ссылка = &СсылкаНаРеализацию
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ЗаказТовары.Номенклатура КАК Номенклатура,
ЗаказТовары.СерияНоменклатуры КАК СерияНоменклатуры,
ЗаказТовары.Цена КАК Цена,
АВТОНОМЕРЗАПИСИ() КАК ПорядковыйНомер
ПОМЕСТИТЬ ВТ_Заказ
ИЗ
Документ.ЗаказПокупателя.Товары КАК ЗаказТовары
ГДЕ
ЗаказТовары.Ссылка = &СсылкаНаЗаказ
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_Реализация.Номенклатура,
ВТ_Реализация.СерияНоменклатуры,
ВТ_Реализация.Количество,
ЕСТЬNULL(ВТ_Заказ.Цена, 0) КАК ЦенаЗаказа
ИЗ
ВТ_Реализация КАК ВТ_Реализация
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Заказ КАК ВТ_Заказ
ПО ВТ_Реализация.Номенклатура = ВТ_Заказ.Номенклатура
И ВТ_Реализация.СерияНоменклатуры = ВТ_Заказ.СерияНоменклатуры
И ВТ_Реализация.ПорядковыйНомер = ВТ_Заказ.ПорядковыйНомер
С помощью временных таблиц мы пронумеровали вхождения одинаковых товаров в обоих документах. Затем мы соединили их не только по полям номенклатуры, но и по вычисленному номеру вхождения (ПорядковыйНомер). Такой запрос выполнится на сервере СУБД за сотые доли секунды и вернет готовую к выводу коллекцию данных, где все цены будут сопоставлены безошибочно.
Подводя итог, мы рассмотрели четыре различных способа решения проблемы дублирующихся строк при печати в 1С: от самого простого исправления существующего алгоритма в памяти до системных изменений структуры метаданных и оптимизации запросов. Выбирайте подход в зависимости от требований производительности вашей системы и масштаба доработки!