Как в 1С распределить количество товаров по помещениям с учетом их вместимости: запрос или код?

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

В практике автоматизации складской логистики мы часто сталкиваемся с задачами распределения запасов. Одна из классических постановок звучит так: у нас есть определенное количество товара в зоне приемки (или в "Помещении 1"), и нам необходимо распределить этот товар по ячейкам хранения (Помещениям 2, 3 и т.д.), каждая из которых имеет свою максимальную вместимость. Как реализовать этот алгоритм, если мы хотим получить результат непосредственно в выборке данных?

Давайте подробно разберем эту задачу, проанализируем ограничения платформы 1С:Предприятие и найдем наиболее эффективные способы решения. Мы рассмотрим варианты реализации как на языке запросов, так и средствами встроенного языка.

Постановка задачи и анализ условий

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

Рассмотрим пример, приведенный в обсуждении:

  1. Источник: В Помещении 1 находится 200 единиц Товара А.
  2. Приемники:
    • Помещение 2 (вместимость 100 шт.)
    • Помещение 3 (вместимость 100 шт.)
  3. Цель: Переместить 100 шт. во второе помещение и 100 шт. в третье.

Ситуация может усложняться (пример 2):

  1. Источники: Помещение 4 (100 шт.), Помещение 5 (100 шт.).
  2. Приемник: Помещение 6 (вместимость 200 шт.).
  3. Цель: Забрать все из 4 и 5 и поместить в 6.

С алгоритмической точки зрения это задача аллокации (распределения) потребностей, часто называемая "закрытием по FIFO" или "расчетом нарастающего итога". Главный вопрос, который возникает у разработчиков: можно ли это сделать одним изящным запросом?

Почему решение "в лоб" через запрос — не лучшая идея

Многие разработчики, знакомые с SQL, знают о существовании циклов и переменных в T-SQL или PL/SQL. Однако, когда мы работаем с языком запросов 1С, мы должны помнить о его ключевом ограничении: в нем отсутствуют циклы и переменные, которые можно изменять в процессе выполнения запроса.

Чтобы реализовать логику "взять 200, положить 100, остаток 100 перенести к следующему шагу", нам необходим расчет нарастающего итога. В языке запросов 1С это реализуется через соединение таблицы самой с собой (Self-Join), что порождает проблему производительности.

Давайте посмотрим, как это выглядит концептуально. Чтобы посчитать, сколько товара достанется "Помещению 3", нам нужно знать, сколько уже забрало "Помещение 2". Если помещений много, сложность алгоритма растет квадратично (O(N^2)).

Если у вас 10 помещений, база данных сделает около 55 сравнений. Если же помещений будет 2000, количество операций превысит 2 миллиона. Как справедливо заметили участники обсуждения, такой подход может серьезно замедлить работу системы.

Вариант 1: Имитация распределения в запросе (Теория)

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

Представим, что у нас есть таблица потребностей (куда надо положить товар) с приоритетами.


ВЫБРАТЬ
    Потребности.Помещение КАК Помещение,
    Потребности.Вместимость КАК Вместимость,
    Потребности.Приоритет КАК Приоритет
ПОМЕСТИТЬ ВТ_Потребности
ИЗ
    &ТЗПотребности КАК Потребности
;

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


ВЫБРАТЬ
    Т1.Помещение,
    Т1.Вместимость,
    СУММА(Т2.Вместимость) КАК НарастающаяВместимость
ПОМЕСТИТЬ ВТ_Нарастающие
ИЗ
    ВТ_Потребности КАК Т1
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Потребности КАК Т2
        ПО Т1.Приоритет >= Т2.Приоритет
СГРУППИРОВАТЬ ПО
    Т1.Помещение,
    Т1.Вместимость

Теперь у нас есть поле НарастающаяВместимость. Если наш общий остаток товара равен 150, мы можем использовать конструкцию ВЫБОР для определения количества к распределению для каждой строки:


ВЫБОР
    // Если нарастающая потребность меньше остатка, берем всю емкость текущего помещения
    КОГДА НарастающаяВместимость <= &ОбщийОстатокТовара ТОГДА Вместимость
    // Если предыдущая нарастающая (Нарастающая - Текущая) уже больше остатка, то 0
    КОГДА (НарастающаяВместимость - Вместимость) >= &ОбщийОстатокТовара ТОГДА 0
    // Иначе берем хвостик
    ИНАЧЕ &ОбщийОстатокТовара - (НарастающаяВместимость - Вместимость)
КОНЕЦ КАК Распределено

Важно: Этот метод работает приемлемо только на малых выборках. На реальных объемах склада (тысячи ячеек) он "положит" производительность.

Вариант 2: Использование Системы Компоновки Данных (СКД)

Более продвинутый способ, предложенный в обсуждении, — использование возможностей СКД. Система компоновки данных имеет собственный язык выражений, который выполняется уже после получения данных из базы, но гораздо быстрее, чем запрос с само-соединением.

В СКД существует функция ВычислитьВыражение, которая позволяет обращаться к другим строкам группировки. Мы можем использовать её для расчета нарастающего итога.

Пример выражения ресурса в СКД для расчета накопленной суммы:

ВычислитьВыражение("Сумма(Вместимость)", , , "Первая", "Текущая")

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

Плюсы СКД:

Вариант 3: Распределение во встроенном языке (Рекомендуемый)

Самый надежный, понятный и производительный способ решения задачи распределения товаров — это использование встроенного языка 1С и таблиц значений. Алгоритмическая сложность такого решения — линейная (O(N)), что означает мгновенное выполнение даже для тысяч записей.

Давайте напишем алгоритм, который решает задачу автора темы.

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

Сначала получим данные запросом. Нам нужны две таблицы: Товары (что распределяем) и Помещения (куда распределяем, отсортированные по приоритету).


// Получаем остатки товаров
Запрос.Текст = "ВЫБРАТЬ Товар, Количество ОСТАТКИ ...";
ТЗ_Товары = Запрос.Выполнить().Выгрузить();

// Получаем свободные ячейки, ОБЯЗАТЕЛЬНО упорядочиваем их
Запрос.Текст = "ВЫБРАТЬ Ячейка, Вместимость ИЗ ... УПОРЯДОЧИТЬ ПО ПорядокОбхода";
ТЗ_Ячейки = Запрос.Выполнить().Выгрузить();

Шаг 2: Алгоритм распределения

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


ТаблицаРезультат = Новый ТаблицаЗначений;
ТаблицаРезультат.Колонки.Добавить("Товар");
ТаблицаРезультат.Колонки.Добавить("Ячейка");
ТаблицаРезультат.Колонки.Добавить("Количество");

Для Каждого СтрТовар Из ТЗ_Товары Цикл
    
    ОстатокКРаспределению = СтрТовар.Количество;
    
    // Бежим по ячейкам, пока есть товар
    Для Каждого СтрЯчейка Из ТЗ_Ячейки Цикл
        
        Если ОстатокКРаспределению <= 0 Тогда
            Прервать; // Товар кончился
        КонецЕсли;
        
        // Если ячейка уже заполнена (в данном примере считаем, 
        // что поле Вместимость - это свободное место), пропускаем
        Если СтрЯчейка.Вместимость <= 0 Тогда
            Продолжить;
        КонецЕсли;
        
        // Сколько можем положить в эту ячейку?
        // Либо всё что есть, либо сколько влезает
        КоличествоКПеремещению = Мин(ОстатокКРаспределению, СтрЯчейка.Вместимость);
        
        // Записываем результат
        НовСтр = ТаблицаРезультат.Добавить();
        НовСтр.Товар = СтрТовар.Товар;
        НовСтр.Ячейка = СтрЯчейка.Ячейка;
        НовСтр.Количество = КоличествоКПеремещению;
        
        // Уменьшаем остаток товара на руках
        ОстатокКРаспределению = ОстатокКРаспределению - КоличествоКПеремещению;
        
        // Уменьшаем свободное место в ячейке (важно, если ячейка используется для разных товаров)
        СтрЯчейка.Вместимость = СтрЯчейка.Вместимость - КоличествоКПеремещению;
        
    КонецЦикла;
    
КонецЦикла;

Разбор сложного случая: "Многие ко многим"

В вопросе автора упоминается ситуация, когда источников несколько (Помещения 4 и 5) и приемник один (Помещение 6). Наш алгоритм выше легко адаптируется и под это.

Если нам нужно переместить товары ИЗ нескольких мест В несколько мест, мы можем создать общую таблицу "Потребностей" (откуда взять) и таблицу "Предложений" (куда положить). Однако чаще всего на практике это делается в два этапа:

  1. Собрать весь доступный товар в виртуальный "буфер" (просуммировать количество из Помещения 4 и 5).
  2. Распределить этот буфер по списку ячеек-получателей алгоритмом, описанным выше.

Резюме

Подведем итоги. Если перед вами стоит задача распределения количества с учетом лимитов:

  1. Избегайте решения этой задачи чистым запросом. Это сложно поддерживать и это создает высокую нагрузку на СУБД из-за квадратичной сложности соединений.
  2. Если задача стоит в рамках отчета — используйте СКД и функции вычисления выражений (нарастающий итог).
  3. Для проведения документов и реальной бизнес-логики — используйте пост-обработку результатов запроса в коде (циклы и таблицы значений). Это самый быстрый и надежный метод.
← На главную