В практике разработчика 1С часто возникает задача, которую условно называют «списанием партий» или «алгоритмом ФИФО на таблицах». Суть задачи заключается в том, чтобы уменьшить показатели одной таблицы (например, текущие остатки или потребности) на значения из другой таблицы (фактическое наличие или поступление). Главная сложность здесь — корректно обработать ситуацию, когда одна строка списания должна затронуть несколько строк-источников, и при этом сохранить аналитические данные (поля), привязанные к конкретным строкам.
Разберем детально, как реализовать этот алгоритм, проанализируем различные подходы и выявим критические нюансы, влияющие на точность результата.
Этот способ является наиболее очевидным и простым в реализации. Мы обходим таблицу-расхода (Таблица2) и для каждой ее строки ищем подходящие строки в таблице-источнике (Таблица1).
Рассмотрим по шагам логику, предложенную в обсуждении:
Пример кода реализации:
Для каждого Стр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, цикл может стать бесконечным. Всегда добавляйте условие выхода, если ресурс в первой таблице полностью исчерпан.
Если Таблица1 содержит тысячи строк, последовательный перебор всей таблицы для каждой строки расхода крайне неэффективен (сложность O(N*M)). Проанализируем более производительный вариант с использованием метода НайтиСтроки().
Алгоритм действий следующий:
Таблица2 (расход).Таблица1.
Для каждого СтрРасход Из Таблица2 Цикл
ПараметрыОтбора = Новый Структура("Номенклатура", СтрРасход.Номенклатура);
НайденныеСтроки = Таблица1.НайтиСтроки(ПараметрыОтбора);
ОстатокКРаспределению = СтрРасход.Количество;
Для Каждого СтрИсточник Из НайденныеСтроки Цикл
Если ОстатокКРаспределению <= 0 Тогда
Прервать; // Все распределили
КонецЕсли;
Если СтрИсточник.Количество <= 0 Тогда
Продолжить; // В этой строке источника уже пусто
КонецЕсли;
СписываемоеКолво = Мин(СтрИсточник.Количество, ОстатокКРаспределению);
СтрИсточник.Количество = СтрИсточник.Количество - СписываемоеКолво;
ОстатокКРаспределению = ОстатокКРаспределению - СписываемоеКолво;
КонецЦикла;
КонецЦикла;
Для еще большего ускорения на больших объемах данных рекомендуется предварительно проиндексировать таблицу значений: Таблица1.Индексы.Добавить("Номенклатура");. Это позволит методу НайтиСтроки работать практически мгновенно.
Проанализируем ситуацию, возникшую в обсуждении: почему в результате получилось Поле2, а не Поле1? Это ключевой момент в задачах списания. Результат алгоритма полностью зависит от порядка строк в первой таблице.
Если мы хотим реализовать классический ФИФО (First In, First Out), то перед началом работы алгоритма необходимо отсортировать Таблица1 по дате или другому признаку приоритета:
Таблица1.Сортировать("ДатаПоступления Возр, Номенклатура");
Если изменить порядок строк, то «выживут» (останутся с ненулевым количеством) совершенно другие строки, и аналитические данные (те самые Поле1, Поле2, которые могут быть номером ГТД, партией или ценой) будут иными. В примере автора темы строки были расположены так, что сначала обнулилась первая строка, а остаток сохранился во второй — именно поэтому в итоговой таблице осталось Поле2.
После выполнения вычитания в Таблица1 останутся строки с нулевым количеством. Чаще всего их нужно удалить. Рассмотрим, как сделать это правильно.
Ошибочный подход: Удалять строки прямо в процессе цикла Для каждого. Это приведет к ошибке «Индекс находится за границами коллекции», так как после удаления строки индексы всех последующих строк смещаются.
Правильные подходы:
Для Индекс = -(Таблица1.Количество() - 1) По 0 Цикл
Стр = Таблица1[ -Индекс ];
Если Стр.Количество = 0 Тогда
Таблица1.Удалить(Стр);
КонецЕсли;
КонецЦикла;
ТЗ_Результат = Таблица1.Скопировать(Новый Структура("Количество", 0)); (но это удалит только те строки, где ровно 0, и требует инверсии логики). Лучше использовать метод Скопировать и затем удалить ненужное.При работе с типами Число в 1С (особенно если количество получается в результате деления или сложных расчетов) могут возникать ситуации «микро-остатков» (например, 0.00000000001). Простая проверка Если Количество = 0 может не сработать.
Рекомендуем использовать проверку на точность или округление:
Если Окр(Стр.Количество, 3) <= 0 Тогда
// Считаем строку пустой
КонецЕсли;
Для успешной реализации последовательного уменьшения значений необходимо:
НайтиСтроки с предварительным индексированием.