Как сгруппировать товары по пересекающимся штрихкодам, если они связаны через цепочку?

Программист Торговля и дистрибуция
← На главную

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

Проанализируем ситуацию на конкретном примере. Допустим, у нас есть следующие связи "Товар - Штрихкод":

  1. Товар1 - ШК1
  2. Товар1 - ШК2
  3. Товар2 - ШК2
  4. Товар2 - ШК3
  5. Товар3 - ШК1
  6. Товар4 - ШК3

Из этих данных видно:

В итоге, все четыре товара — Товар1, Товар2, Товар3, и Товар4 — образуют одну большую группу связанных элементов, и их нужно объединить. Проблема в том, что, например, у Товара1 и Товара4 нет общих штрихкодов, связь между ними транзитивная (через Товар2).

Почему простой запрос не поможет?

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


ВЫБРАТЬ
    Т.Штрихкод,
    КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Т.Номенклатура) КАК КоличествоНоменклатуры
ИЗ
    ИсходнаяТаблица КАК Т
СГРУППИРОВАТЬ ПО
    Т.Штрихкод
ИМЕЮЩИЕ
    КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Т.Номенклатура) > 1

Такой запрос покажет нам только прямые пересечения:

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

Вывод: стандартными средствами языка запросов 1С решить эту задачу одним махом невозможно, так как он не поддерживает рекурсивные обходы (CTE - Common Table Expressions), которые есть в полноценных SQL-серверах.

Решение через итерационный алгоритм

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

В среде 1С это можно реализовать с помощью итерационного (циклического) подхода. Разберем его по шагам.

Шаг 1. Подготовка данных

Для начала, выгрузим все связи «Номенклатура - Штрихкод» в таблицу значений для удобной обработки в памяти.


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

// ... здесь код загрузки данных в ТаблицуСвязей ...

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


ТаблицаГрупп = Новый ТаблицаЗначений;
ТаблицаГрупп.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаГрупп.Колонки.Добавить("НомерГруппы", Новый ОписаниеТипов("Число"));

// Индексируем для быстрого поиска
ТаблицаГрупп.Индексы.Добавить("Номенклатура");

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

НомерГруппы = 1;
Для Каждого Строка Из УникальнаяНоменклатура Цикл
    НоваяСтрока = ТаблицаГрупп.Добавить();
    НоваяСтрока.Номенклатура = Строка.Номенклатура;
    НоваяСтрока.НомерГруппы = НомерГруппы;
    НомерГруппы = НомерГруппы + 1;
КонецЦикла;

Шаг 2. Итерационное объединение групп

Теперь начинается самое интересное. Мы будем в цикле выполнять запрос, который находит связанные товары, и объединять их группы до тех пор, пока объединения не прекратятся.

Создадим временную таблицу в менеджере временных таблиц и поместим туда наши исходные связи.


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

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

Запрос.УстановитьПараметр("ТаблицаСвязей", ТаблицаСвязей);
ВыборкаПар = Запрос.Выполнить().Выбрать();

Теперь организуем цикл, который будет выполняться, пока происходят изменения.


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

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

Шаг 3. Получение результата

Теперь можно легко получить итоговую таблицу, соединив исходные данные с таблицей групп.


ЗапросИтог = Новый Запрос;
ЗапросИтог.Текст = 
"ВЫБРАТЬ
|    ТГ.НомерГруппы,
|    ТС.Номенклатура,
|    ТС.Штрихкод
|ИЗ
|    &ТаблицаСвязей КАК ТС
|        ВНУТРЕННЕЕ СОЕДИНЕНИЕ &ТаблицаГрупп КАК ТГ
|        ПО ТС.Номенклатура = ТГ.Номенклатура
|
|УПОРЯДОЧИТЬ ПО
|    НомерГруппы,
|    Номенклатура";

ЗапросИтог.УстановитьПараметр("ТаблицаСвязей", ТаблицаСвязей);
ЗапросИтог.УстановитьПараметр("ТаблицаГрупп", ТаблицаГрупп);

Результат = ЗапросИтог.Выполнить().Выгрузить();
// В таблице "Результат" будет искомый список

Заключение

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

← На главную