Как реализовать алгоритм последовательного списания или распределения остатков в таблице значений?

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

В практике разработчика 1С часто возникает задача, которую условно называют «списанием партий» или «алгоритмом ФИФО на таблицах». Суть задачи заключается в том, чтобы уменьшить показатели одной таблицы (например, текущие остатки или потребности) на значения из другой таблицы (фактическое наличие или поступление). Главная сложность здесь — корректно обработать ситуацию, когда одна строка списания должна затронуть несколько строк-источников, и при этом сохранить аналитические данные (поля), привязанные к конкретным строкам.

Разберем детально, как реализовать этот алгоритм, проанализируем различные подходы и выявим критические нюансы, влияющие на точность результата.

Метод 1. Классический вложенный цикл с прямым перебором

Этот способ является наиболее очевидным и простым в реализации. Мы обходим таблицу-расхода (Таблица2) и для каждой ее строки ищем подходящие строки в таблице-источнике (Таблица1).

Рассмотрим по шагам логику, предложенную в обсуждении:

  1. Берем строку из второй таблицы, где указано, сколько нужно «списать».
  2. Запускаем цикл по первой таблице в поисках нужной номенклатуры.
  3. Сравниваем количества. Если в первой таблице остатка больше, чем нужно списать — уменьшаем остаток и переходим к следующей строке расхода. Если меньше — обнуляем остаток в строке источника и продолжаем искать остатки для этой же позиции в следующих строках первой таблицы.

Пример кода реализации:


Для каждого Стр2 Из ТЗ2 Цикл
    // Пока нам есть что списывать для данной номенклатуры
    Пока Стр2.Количество <> 0 Цикл 
        Для Каждого Стр1 Из ТЗ1 Цикл
            Если Стр1.Номенклатура = Стр2.Номенклатура И Стр1.Количество > 0 Тогда
                
                Если Стр1.Количество > Стр2.Количество Тогда
                    Стр1.Количество = Стр1.Количество - Стр2.Количество;
                    Стр2.Количество = 0;
                    Прервать; // Расход исчерпан, выходим из внутреннего цикла
                ИначеЕсли Стр1.Количество <= Стр2.Количество Тогда
                    Стр2.Количество = Стр2.Количество - Стр1.Количество;
                    Стр1.Количество = 0;
                    // Продолжаем цикл, так как Стр2.Количество еще может быть > 0
                КонецЕсли;
                
            КонецЕсли;
        КонецЦикла;
        
        // Важное дополнение: прерываем бесконечный цикл, если ресурс в ТЗ1 закончился
        Прервать; 
    КонецЦикла;
КонецЦикла;

Важное замечание: В коде выше используется проверка Пока Стр2.Количество <> 0. Нужно быть осторожным: если в ТЗ1 не хватит общего количества для покрытия ТЗ2, цикл может стать бесконечным. Всегда добавляйте условие выхода, если ресурс в первой таблице полностью исчерпан.

Метод 2. Оптимизированный поиск через структуру и метод НайтиСтроки

Если Таблица1 содержит тысячи строк, последовательный перебор всей таблицы для каждой строки расхода крайне неэффективен (сложность O(N*M)). Проанализируем более производительный вариант с использованием метода НайтиСтроки().

Алгоритм действий следующий:

  1. Обходим Таблица2 (расход).
  2. Для текущей номенклатуры получаем массив подходящих строк из Таблица1.
  3. Перебираем только этот массив, распределяя количество.

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

Для еще большего ускорения на больших объемах данных рекомендуется предварительно проиндексировать таблицу значений: Таблица1.Индексы.Добавить("Номенклатура");. Это позволит методу НайтиСтроки работать практически мгновенно.

Критическая важность сортировки

Проанализируем ситуацию, возникшую в обсуждении: почему в результате получилось Поле2, а не Поле1? Это ключевой момент в задачах списания. Результат алгоритма полностью зависит от порядка строк в первой таблице.

Если мы хотим реализовать классический ФИФО (First In, First Out), то перед началом работы алгоритма необходимо отсортировать Таблица1 по дате или другому признаку приоритета:

Таблица1.Сортировать("ДатаПоступления Возр, Номенклатура");

Если изменить порядок строк, то «выживут» (останутся с ненулевым количеством) совершенно другие строки, и аналитические данные (те самые Поле1, Поле2, которые могут быть номером ГТД, партией или ценой) будут иными. В примере автора темы строки были расположены так, что сначала обнулилась первая строка, а остаток сохранился во второй — именно поэтому в итоговой таблице осталось Поле2.

Очистка результата от «нулевых» строк

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

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

Правильные подходы:

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

Проблема точности (дробные числа)

При работе с типами Число в 1С (особенно если количество получается в результате деления или сложных расчетов) могут возникать ситуации «микро-остатков» (например, 0.00000000001). Простая проверка Если Количество = 0 может не сработать.

Рекомендуем использовать проверку на точность или округление:


Если Окр(Стр.Количество, 3) <= 0 Тогда
    // Считаем строку пустой
КонецЕсли;

Резюме

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

← На главную