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

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

В практической работе разработчика 1С часто возникает задача распределения некоторого общего числа (количества или суммы) по строкам табличной части документа. Типичный пример: в шапке документа указано общее количество «виртуального» или комплектующего набора (например, 11 единиц), а в табличную часть необходимо подобрать исходные товары, из которых этот набор состоит, и распределить это количество так, чтобы их сумма в итоге давала ровно 11, но при этом распределенное количество по каждой строке не превышало реальный складской остаток.

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

Постановка задачи и математическая модель

Представим ситуацию, описанную автором темы. У нас есть общая потребность в количестве 11 единиц. На складе имеются три подходящих товара со следующими остатками:

Нам необходимо составить уравнение вида: Х1 + Х2 + Х3 = 11, где Хi — распределяемое количество для каждого товара. При этом на систему накладываются жесткие ограничения:

  1. Каждое распределенное значение должно быть неотрицательным целым (или дробным с заданной точностью) числом: Хi >= 0.
  2. Количество не может превышать текущий остаток товара на складе: Х1 <= 4, Х2 <= 100, Х3 <= 1.

Очевидно, что данная система уравнений имеет множество решений. Например:

Чтобы программа сделала однозначный выбор, нам нужно задать четкий критерий или алгоритм распределения. Рассмотрим основные способы реализации этой задачи.

Способ 1. Использование встроенных функций БСП

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

Обратимся к общему модулю ОбщегоНазначенияКлиентСервер и его функции РаспределитьСуммуПропорциональноКоэффициентам(). Этот метод идеально подходит, если нам нужно просто разделить число пропорционально коэффициентам (в нашем случае — остаткам) без учета жесткого ограничения «сверху» на каждую отдельную позицию (хотя в простых ситуациях это работает).

Синтаксис функции выглядит следующим образом:

ОбщегоНазначенияКлиентСервер.РаспределитьСуммуПропорциональноКоэффициентам(РаспределяемаяСумма, Коэффициенты, Точность)

В качестве параметров мы передаем:

Давайте проанализируем, как работает этот метод на практике. Напишем небольшой программный код:


Коэффициенты = Новый Массив;
Коэффициенты.Добавить(4);   // Товар 1
Коэффициенты.Добавить(100); // Товар 2
Коэффициенты.Добавить(1);   // Товар 3

МассивРезультатов = ОбщегоНазначенияКлиентСервер.РаспределитьСуммуПропорциональноКоэффициентам(11, Коэффициенты, 0);

// На выходе мы получим массив распределенных величин
Для Каждого Значение Из МассивРезультатов Цикл
    Сообщить(Значение);
КонецЦикла;

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

Способ 2. Решение проблемы погрешности округления («проблема копеек»)

При делении чисел в программной среде мы неизбежно сталкиваемся с проблемой округления. Например, если распределить 10 единиц на 3 равные части с точностью до целого, математически мы получим 3.333... на каждую строку. При округлении до целого мы получим 3, 3 и 3. Сумма составит 9, и 1 единица «потеряется».

В профессиональной разработке на 1С эту проблему решают несколькими путями:

  1. Метод максимального коэффициента (применяется в БСП): Алгоритм высчитывает погрешность округления (остаток) и принудительно добавляет ее к строке, имеющей наибольший коэффициент. В нашем примере с остатками 4, 100 и 1 любая округленная погрешность будет отнесена на Товар 2, так как его остаток (коэффициент) максимален. Это логично, так как искажение пропорции на крупной партии наименее заметно.
  2. Алгоритм накопительного буфера погрешности (алгоритм Кэхэна): В цикле обхода строк вычисляется расчетное точное значение доли. К нему прибавляется накопленная на предыдущих шагах ошибка округления, после чего текущая строка округляется, а новая ошибка записывается в буфер. Это позволяет равномерно распределить погрешность по всем строкам табличной части.

Способ 3. Итерационный алгоритм распределения с ограничением «сверху» (Каппинг)

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

Разберем логику алгоритма по шагам:

Давайте реализуем этот алгоритм на встроенном языке 1С:


Процедура РаспределитьКоличествоПоОстаткам(ТаблицаТоваров, ВсегоРаспределить) Экспорт
    // ТаблицаТоваров — это таблица значений с колонками: Товар, Остаток, Количество.
    // ВсегоРаспределить — число, которое нужно распределить (например, 11).

    // Очистим предыдущие распределения
    Для Каждого СтрокаТЧ Из ТаблицаТоваров Цикл
        СтрокаТЧ.Количество = 0;
    КонецЦикла;

    ОсталосьРаспределить = ВсегоРаспределить;
    
    // Создаем массив строк, которые еще могут принимать распределение
    АктивныеСтроки = Новый Массив;
    Для Каждого СтрокаТЧ Из ТаблицаТоваров Цикл
        Если СтрокаТЧ.Остаток > 0 Тогда
            АктивныеСтроки.Добавить(СтрокаТЧ);
        КонецЕсли;
    КонецЦикла;

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

        // Распределяем остаток суммы пропорционально текущим свободным остаткам
        ТекущееРаспределение = ОбщегоНазначенияКлиентСервер.РаспределитьСуммуПропорциональноКоэффициентам(ОсталосьРаспределить, Коэффициенты, 0);
        
        МассивСтрокДляУдаления = Новый Массив;
        БылиПревышения = Ложь;

        Для Индекс = 0 По АктивныеСтроки.ВГраница() Цикл
            Строка = АктивныеСтроки[Индекс];
            РаспределеннаяДоля = ТекущееРаспределение[Индекс];
            
            НовоеКоличество = Строка.Количество + РаспределеннаяДоля;
            
            // Если распределение превысило остаток (из-за округления или специфики пропорции)
            Если НовоеКоличество > Строка.Остаток Тогда
                ДобавкаДоМаксимума = Строка.Остаток - Строка.Количество;
                Строка.Количество = Строка.Остаток;
                ОсталосьРаспределить = ОсталосьРаспределить - ДобавкаДоМаксимума;
                
                // Фиксируем строку — удаляем ее из дальнейших итераций
                МассивСтрокДляУдаления.Добавить(Строка);
                БылиПревышения = Истина;
            Иначе
                Строка.Количество = НовоеКоличество;
                ОсталосьРаспределить = ОсталосьРаспределить - РаспределеннаяДоля;
            EndЕсли;
        КонецЦикла;

        // Удаляем зафиксированные строки из активного списка
        Для Каждого СтрокаДляУдаления Из МассивСтрокДляУдаления Цикл
            АктивныеСтроки.Удалить(АктивныеСтроки.Найти(СтрокаДляУдаления));
        КонецЦикла;

        // Если все распределилось гладко без превышений лимитов, мы закончили работу
        Если Не БылиПревышения Тогда
            Прервать;
        EndЕсли;
        
    КонецЦикла;
    
    Если ОсталосьРаспределить > 0 Тогда
        ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Внимание! На складе недостаточно товаров. Не удалось распределить: " + ОсталосьРаспределить);
    КонецЕсли;
КонецПроцедуры

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

Способ 4. Альтернативные логистические стратегии распределения

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

  1. Стратегия «Минимизация остатков» (Зачистка склада)

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

  2. Стратегия «Сохранение мелких партий» (Максимизация остатков)

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

  3. Стратегия FIFO (First In, First Out) по срокам годности

    Если мы работаем со скоропортящейся продукцией, распределение должно строиться на основе дат поступления партий. Мы сортируем таблицу товаров по возрастанию срока годности (или даты поступления) и последовательно закрываем потребность в 11 штуках, списывая самые старые партии.

Резюме

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

← На главную