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

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При сопоставлении данных из двух источников, сверке документов или корректировке регистров часто возникает задача: избавиться от парных строк с одинаковыми аналитическими реквизитами, но противоположными знаками числовых показателей (например, +100 и -100). Нередко разработчики искусственно умножают числовые поля одной из выборок на -1, объединяют таблицы и пытаются очистить полученный массив.

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

В чем опасность механического суммирования противоположных знаков?

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

  1. Случайное погашение не связанных сумм. Если в таблице есть строки со значениями +80, +10 и -90 по одной и той же аналитике, их сумма даст 0. Метод свертки полностью удалит эти строки, хотя фактически в исходных источниках не было прямой встречной пары для -90.
  2. Несимметричное количество одинаковых значений. Допустим, присутствуют две строки с -100 и одна строка с +100. После агрегации останется одна строка со значением -100. Если задача состояла в поиске несбалансированных операций, этот результат верен. Однако если требовалось строго отслеживать идентификаторы строк или первичные документы, связь между исходными записями будет безвозвратно потеряна.

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

Способ 1. Использование метода Свернуть() с последующей очисткой нулей

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

Посмотрим на пример реализации:


// Шаг 1. Сворачиваем таблицу значений по ключевым колонкам
// Первым параметром передаем список измерений, вторым — список суммируемых ресурсов
ТЗ.Свернуть("Контрагент, Договор, Номенклатура", "Количество, Сумма");

// Шаг 2. Удаляем строки, сальдо по которым обнулилось
// При удалении строк по индексу цикл ВСЕГДА выполняем в обратном порядке
Для Индекс = ТЗ.Количество() - 1 По 0 Цикл
    ТекущаяСтрока = ТЗ[Индекс];
    
    // Учитываем возможную погрешность вычислений с плавающей точкой
    Если Окр(ТекущаяСтрока.Количество, 3) = 0 И Окр(ТекущаяСтрока.Сумма, 2) = 0 Тогда
        ТЗ.Удалить(Индекс);
    КонецЕсли;
КонецЦикла;

Важный нюанс при удалении строк в цикле: никогда не используйте конструкцию Для Каждого Стр Из ТЗ Цикл ТЗ.Удалить(Стр). При вызове Удалить() внутри прямого обхода индексы оставшихся элементов сдвигаются влево, из-за чего итератор пропускает следующую за удаленной строку. Альтернативный безопасный вариант — предварительно собрать удаляемые строки в массив и обойти уже массив:


СтрокиКУдалению = Новый Массив;

Для Каждого Стр Из ТЗ Цикл
    Если Окр(Стр.Сумма, 2) = 0 Тогда
        СтрокиКУдалению.Добавить(Стр);
    КонецЕсли;
КонецЦикла;

Для Каждого Стр Из СтрокиКУдалению Цикл
    ТЗ.Удалить(Стр);
КонецЦикла;

Способ 2. Фильтрация на уровне языка запросов через секцию ИМЕЮЩИЕ

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

Разберем структуру запроса с секцией ИМЕЮЩИЕ:


ВЫБРАТЬ
    ИсходныеДанные.Контрагент КАК Контрагент,
    ИсходныеДанные.Номенклатура КАК Номенклатура,
    СУММА(ИсходныеДанные.Сумма) КАК СуммаОстаток
ПОМЕСТИТЬ ВТ_Итоги
ИЗ
    &ВнешняяТаблицаДанных КАК ИсходныеДанные

СГРУППИРОВАТЬ ПО
    ИсходныеДанные.Контрагент,
    ИсходныеДанные.Номенклатура

ИМЕЮЩИЕ
    СУММА(ИсходныеДанные.Сумма) <> 0
;

ВЫБРАТЬ
    ВТ_Итоги.Контрагент,
    ВТ_Итоги.Номенклатура,
    ВТ_Итоги.СуммаОстаток
ИЗ
    ВТ_Итоги КАК ВТ_Итоги

Обратим внимание на работу с временными таблицами: если в запрос передается объект ТаблицаЗначений через параметр &ВнешняяТаблицаДанных, все колонки этой таблицы должны иметь жесткую типизацию (через объект ОписательТипов). Таблица значений с нетипизированными колонками вызовет ошибку исполнения запроса.

Способ 3. Попарное исключение строк (алгоритм "один к одному")

Выясним, как поступить, если нельзя просто складывать сальдо, а требуется исключать именно симметричные зеркальные пары. Например, при наличии строк +100, -100, -100 в результате должна остаться ровно одна строка -100.

Для быстрого поиска встречной пары на больших объемах данных обязательно создадим индекс по ключевым полям поиска:


// Добавляем служебную колонку для отметки сопоставленных строк
ТЗ.Колонки.Добавить("Сопоставлено", Новый ОписаниеТипов("Булево"));

// Создаем индекс для ускорения работы метода НайтиСтроки
ТЗ.Индексы.Добавить("Номенклатура, Сумма, Сопоставлено");

ПараметрыПоиска = Новый Структура("Номенклатура, Сумма, Сопоставлено");
ПараметрыПоиска.Сопоставлено = Ложь;

Для Каждого СтрПлюс Из ТЗ Цикл
    Если СтрПлюс.Сопоставлено Или СтрПлюс.Сумма <= 0 Тогда
        Продолжить;
    КонецЕсли;
    
    // Ищем парную строку с тем же ключом и строго противоположным знаком
    ПараметрыПоиска.Номенклатура = СтрПлюс.Номенклатура;
    ПараметрыПоиска.Сумма = -СтрПлюс.Сумма;
    
    НайденныеМинусы = ТЗ.НайтиСтроки(ПараметрыПоиска);
    
    Если НайденныеМинусы.Количество() > 0 Тогда
        // Берем первую найденную встречную запись и помечаем обе строки
        СтрМинус = НайденныеМинусы[0];
        
        СтрПлюс.Сопоставлено = Истина;
        СтрМинус.Сопоставлено = Истина;
    КонецЕсли;
КонецЦикла;

// Удаляем все строки, нашедшие пару
СтрокиКУдалению = ТЗ.НайтиСтроки(Новый Структура("Сопоставлено", Истина));
Для Каждого Стр Из СтрокиКУдалению Цикл
    ТЗ.Удалить(Стр);
КонецЦикла;

ТЗ.Колонки.Удалить("Сопоставлено");

Использование ТЗ.Индексы.Добавить() критически важно: без индекса метод НайтиСтроки() сканирует таблицу линейно, что превращает алгоритм в квадратичную сложность $O(N^2)$ и приводит к зависанию программы при количестве строк более нескольких тысяч.

Итоги

При решении задач очистки таблиц от взаимоисключающих значений придерживаемся следующих правил:

  1. Если необходимо получить конечное сальдо расхождений — используем ТЗ.Свернуть() с удалением строк с нулевым значением через обратный цикл, либо секцию ИМЕЮЩИЕ СУММА(...) <> 0 в запросе.
  2. Если важно сохранить неизменными суммы конкретных строк и убрать строго зеркальные дубли — применяем алгоритм попарной маркировки через НайтиСтроки() с обязательным предварительным индексированием таблицы.
  3. При сравнении числовых остатков с нулем всегда учитываем точность округления функцией Окр(), чтобы избежать ошибок, связанных с представлением вещественных чисел в оперативной памяти.
← На главную