Как правильно сопоставить одинаковые строки в табличной части при печати в 1С?

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

При разработке печатных форм в среде «1С:Предприятие» программисты часто сталкиваются с необходимостью сопоставления данных из разных документов. Классический пример — печать товарного чека или накладной из документа «Реализация товаров и услуг», когда цены или скидки необходимо получить из исходного документа «Заказ покупателя».

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

В чем причина ошибки? Анализ неоптимального кода

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


Для НомерСтрокиВыборки = 2 По КоличествоСтрок Цикл
    Строка = СведенияПоТабличнойЧасти[НомерСтрокиВыборки];
    
    // ... получение стандартных цен ...
    
    ДокумЗаказ = ВыбДокумент["Сделка"];
    Если (ТипЗнч(ДокумЗаказ) = Тип("ДокументСсылка.ЗаказПокупателя")) Тогда
        Для Каждого СтрокаДокумЗаказТовары из ДокумЗаказ.Товары Цикл
            Если СокрЛП("" + СтрокаДокумЗаказТовары.Номенклатура.Код) = СокрЛП("" + Строка.Код) 
               И Строка.СерияНоменклатуры.Ссылка = СтрокаДокумЗаказТовары.СерияНоменклатуры.Ссылка Тогда
                
                Цена = СтрокаДокумЗаказТовары.ладЦена;
                Скидка = СтрокаДокумЗаказТовары.ладПроцентСкидкиНаценкиП;
                Прервать; // Выход из вложенного цикла при первом совпадении
                
            КонецЕсли;
        КонецЦикла;
    КонецЕсли;
    
    // ... вывод в табличный документ ...
КонецЦикла;

Проанализируем этот алгоритм. Представим, что в заказе покупателя у нас есть две строки с одной и той же номенклатурой:

  1. Товар №1 (Код: 0001, Серия: Основная) — Цена: 100 рублей.
  2. Товар №1 (Код: 0001, Серия: Основная) — Цена: 200 рублей.

Когда цикл по строкам реализации доходит до первой строки с Товаром №1, он запускает внутренний цикл поиска по заказу. На первой же итерации внутреннего цикла условие Если выполняется, переменная Цена принимает значение 100, и срабатывает оператор Прервать. Всё работает корректно.

Однако на следующей итерации внешнего цикла, когда мы обрабатываем вторую строку с Товаром №1, внутренний цикл поиска по заказу запускается заново, с самой первой строки. Он опять натыкается на первую строку заказа (где цена 100 рублей), считает условие выполненным и прерывает работу. В итоге во вторую позицию печатной формы подставляется неверная цена. Мы столкнулись с проблемой отсутствия уникальной связи между строками.

Решение №1: Использование временной таблицы значений и удаление найденных строк

Первый логичный способ обойти эту проблему без изменения структуры метаданных — использовать временную копию табличной части в виде объекта ТаблицаЗначений. Идея заключается в том, чтобы при каждом успешном сопоставлении удалять найденную строку из этой копии. Тогда при обработке следующего дубля система просто физически не сможет сопоставить его с уже "отработанной" строкой заказа.

Выясним, как правильно реализовать этот подход. Рассмотрим доработанный алгоритм:


// Выгружаем товары заказа в отдельную таблицу значений перед основным циклом
ТаблицаТоваровЗаказа = ВыбДокумент["Сделка"].Товары.Выгрузить();

Для НомерСтрокиВыборки = 0 По КоличествоСтрок - 1 Цикл
    СтрокаРеализации = СведенияПоТабличнойЧасти[НомерСтрокиВыборки];
    
    // Получаем базовые значения из реализации на случай, если в заказе позиция не найдется
    Цена = ВыбДокумент["Товары"][СтрокаРеализации["НомерСтроки"] - 1]["Цена_Транзит"];
    Скидка = ВыбДокумент["Товары"][СтрокаРеализации["НомерСтроки"] - 1]["ПроцентСкидкиНаценки"];
    
    ДокумЗаказ = ВыбДокумент["Сделка"];
    Если (ТипЗнч(ДокумЗаказ) = Тип("ДокументСсылка.ЗаказПокупателя")) Тогда
        
        // Перебираем строки нашей временной Таблицы Значений
        Для Каждого СтрокаЗаказа Из ТаблицаТоваровЗаказа Цикл
            
            // Сравниваем ключевые поля (Код номенклатуры, Серию и Количество)
            Если СокрЛП("" + СтрокаЗаказа.Номенклатура.Код) = СокрЛП("" + СтрокаРеализации.Код) 
               И СтрокаРеализации.СерияНоменклатуры.Ссылка = СтрокаЗаказа.СерияНоменклатуры.Ссылка 
               И СтрокаЗаказа.Количество = СтрокаРеализации.Количество Тогда
                
                // Переопределяем цену и скидку данными из заказа
                Цена = СтрокаЗаказа.ладЦена;
                Скидка = СтрокаЗаказа.ладПроцентСкидкиНаценкиП;
                
                // Удаляем найденную строку из временной таблицы, чтобы она больше не участвовала в поиске
                ТаблицаТоваровЗаказа.Удалить(СтрокаЗаказа);
                
                // Обязательно прерываем цикл, так как коллекция ТаблицаТоваровЗаказа была изменена!
                Прервать; 
                
            КонецЕсли;
        КонецЦикла;
        
    КонецЕсли;
    
    // Дальнейший расчет сумм и вывод параметров в макет печатной формы...
КонецЦикла;

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

Решение №2: Изящный поиск с помощью метода НайтиСтроки()

Предыдущее решение полностью рабочее, но его трудно назвать элегантным и быстродействующим. Вложенные циклы создают квадратичную сложность алгоритма $O(N^2)$, что может замедлить печать документов с большим количеством строк (от 100 и более). Программисты 1С должны стремиться писать более лаконичный и производительный код.

Избавимся от внутреннего цикла Для Каждого. Для этого воспользуемся встроенным методом объекта ТаблицаЗначений — НайтиСтроки(). Этот метод принимает на вход структуру отбора и возвращает массив строк, удовлетворяющих условиям. Это работает гораздо быстрее и избавляет нас от опасного удаления элементов внутри активного цикла обхода.

Разберем данный подход по шагам:


// Подготавливаем таблицу товаров заказа
ТаблицаТоваровЗаказа = ВыбДокумент["Сделка"].Товары.Выгрузить();

Для Каждого СтрокаРеализации Из СведенияПоТабличнойЧасти Цикл
    
    // Инициализируем переменные по умолчанию
    Цена = СтрокаРеализации.Цена;
    Скидка = СтрокаРеализации.Скидка;
    
    // Формируем структуру отбора для поиска точного соответствия
    ПараметрыОтбора = Новый Структура;
    ПараметрыОтбора.Вставить("Номенклатура", СтрокаРеализации.Номенклатура);
    ПараметрыОтбора.Вставить("СерияНоменклатуры", СтрокаРеализации.СерияНоменклатуры);
    ПараметрыОтбора.Вставить("Количество", СтрокаРеализации.Количество);
    
    // Осуществляем быстрый поиск в таблице заказа
    НайденныеСтроки = ТаблицаТоваровЗаказа.НайтиСтроки(ПараметрыОтбора);
    
    Если НайденныеСтроки.Количество() > 0 Тогда
        // Берем самую первую строку из массива найденных совпадений
        СтрокаЗаказа = НайденныеСтроки[0];
        
        Цена = СтрокаЗаказа.ладЦена;
        Скидка = СтрокаЗаказа.ладПроцентСкидкиНаценкиП;
        
        // Удаляем строку из копии таблицы значений. 
        // Это безопасно, так как мы не находимся внутри цикла по этой таблице!
        ТаблицаТоваровЗаказа.Удалить(СтрокаЗаказа);
    КонецЕсли;
    
    // Выполняем расчеты и выводим строку в табличный документ...
КонецЦикла;

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

Решение №3: Системный подход с использованием ключей связи

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

В современных типовых конфигурациях (таких как «1С:ERP Управление предприятием», «1С:Комплексная автоматизация» или «1С:Управление торговлей 11») связь между строками документов-оснований и документов-последователей реализуется с помощью специального реквизита — КлючСвязи (или ИдентификаторСтроки, КодСтроки).

Рассмотрим, как работает эта схема:

  1. При создании и проведении документа «Заказ покупателя» каждой строке его табличной части присваивается уникальный числовой идентификатор (например, 1, 2, 3 и т.д.) или UUID (уникальный идентификатор).
  2. При вводе документа «Реализация товаров и услуг» на основании заказа платформа копирует эти идентификаторы в аналогичную колонку таблицы товаров реализации.
  3. В результате, даже если у нас есть пять одинаковых строк с разной ценой, каждая пара строк (в заказе и в реализации) имеет абсолютно уникальный общий ключ связи.

Если в вашей конфигурации есть такой реквизит, то задача сопоставления становится тривиальной. Вам достаточно передавать в структуру отбора метода НайтиСтроки() только один параметр — КлючСвязи:


ПараметрыОтбора = Новый Структура("КлючСвязи", СтрокаРеализации.КлючСвязи);
НайденныеСтроки = ТаблицаТоваровЗаказа.НайтиСтроки(ПараметрыОтбора);

Если НайденныеСтроки.Количество() > 0 Тогда
    СтрокаЗаказа = НайденныеСтроки[0];
    Цена = СтрокаЗаказа.Цена;
КонецЕсли;

Если вы разрабатываете собственную конфигурацию с нуля или существенно дорабатываете старую (например, «Управление торговлей 10.3»), мы настоятельно рекомендуем добавить реквизит КлючСвязи (тип: Число) во все товарные табличные части. Это убережет вас от костылей в коде печатных форм, отчетов и при проведении документов по регистрам.

Решение №4: Оптимальное получение данных через Запрос (Best Practice)

Согласно стандартам разработки фирмы «1С», обработка больших объемов данных в циклах в оперативной памяти крайне нежелательна. Самым быстрым и правильным методом получения данных для печати является использование Запросов.

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


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

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ЗаказТовары.Номенклатура КАК Номенклатура,
    ЗаказТовары.СерияНоменклатуры КАК СерияНоменклатуры,
    ЗаказТовары.Цена КАК Цена,
    АВТОНОМЕРЗАПИСИ() КАК ПорядковыйНомер
ПОМЕСТИТЬ ВТ_Заказ
ИЗ
    Документ.ЗаказПокупателя.Товары КАК ЗаказТовары
ГДЕ
    ЗаказТовары.Ссылка = &СсылкаНаЗаказ
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ_Реализация.Номенклатура,
    ВТ_Реализация.СерияНоменклатуры,
    ВТ_Реализация.Количество,
    ЕСТЬNULL(ВТ_Заказ.Цена, 0) КАК ЦенаЗаказа
ИЗ
    ВТ_Реализация КАК ВТ_Реализация
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Заказ КАК ВТ_Заказ
        ПО ВТ_Реализация.Номенклатура = ВТ_Заказ.Номенклатура
        И ВТ_Реализация.СерияНоменклатуры = ВТ_Заказ.СерияНоменклатуры
        И ВТ_Реализация.ПорядковыйНомер = ВТ_Заказ.ПорядковыйНомер

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

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

← На главную