В процессе разработки документов сопоставления, комплектации или пересортицы номенклатуры часто возникает задача списания партий исходного товара на несколько позиций нового. Например, в табличной части исходная номенклатура «Товар 1» встречается дважды: в первой строке сопоставляется с «Товар А» в количестве 3 штук, а во второй строке — с «Товар Б» в количестве 7 штук. Просто свернуть табличную часть по исходной номенклатуре нельзя, так как строки должны породить движения с привязкой к разным целевым позициям.
Если в цикле по строкам документа каждый раз выполнять поиск партий, возникает ошибка: для каждой строки выбираются одни и те же первые партии, что приводит к повторному списанию и отрицательным остаткам. Разберем основные способы и алгоритмы решения этой задачи.
Рассмотрим классический и наиболее наглядный подход: перед формированием движений мы один раз получаем остатки партий по всей исходной номенклатуре документа в отдельную таблицу значений, добавляем в нее служебную колонку свободного остатка и по мере обхода строк табличной части уменьшаем доступное количество в строках партий.
Разберем алгоритм по шагам:
ТаблицаЗначений.ОстатокКоличество (и при необходимости ОстатокСумма), инициализируя ее начальным количеством партии.Индексы.Добавить().ОстатокКоличество в строке таблицы партий. Если остаток партии исчерпан до нуля, следующая строка документа автоматически перейдет к следующей доступной партии.Посмотрим на пример реализации данного алгоритма:
// 1. Получаем уникальный массив номенклатуры
МассивНоменклатуры = ТабличнаяЧасть.ВыгрузитьКолонку("НоменклатураИсходная");
// 2. Получаем остатки партий запросом
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Партии.Номенклатура КАК Номенклатура,
| Партии.Партия КАК Партия,
| Партии.КоличествоОстаток КАК Количество,
| Партии.СуммаОстаток КАК Сумма
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(&МоментВремени, Номенклатура В (&МассивНоменклатуры)) КАК Партии
|УПОРЯДОЧИТЬ ПО
| Партии.Партия.МоментВремени ВОЗР";
Запрос.УстановитьПараметр("МоментВремени", МоментВремени());
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
ТаблицаОстатков = Запрос.Выполнить().Выгрузить();
ТаблицаОстатков.Колонки.Добавить("ОстатокКоличество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));
ТаблицаОстатков.Колонки.Добавить("ОстатокСумма", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
Для Каждого СтрОстатка Из ТаблицаОстатков Цикл
СтрОстатка.ОстатокКоличество = СтрОстатка.Количество;
СтрОстатка.ОстатокСумма = СтрОстатка.Сумма;
КонецЦикла;
ТаблицаОстатков.Индексы.Добавить("Номенклатура");
// 3. Распределение партий по строкам документа
Для Каждого СтрТЧ Из ТабличнаяЧасть Цикл
НужноСписать = СтрТЧ.Количество;
Отбор = Новый Структура("Номенклатура", СтрТЧ.НоменклатураИсходная);
СтрокиПартий = ТаблицаОстатков.НайтиСтроки(Отбор);
Для Каждого СтрПартии Из СтрокиПартий Цикл
Если НужноСписать = 0 Тогда
Прервать;
КонецЕсли;
Если СтрПартии.ОстатокКоличество <= 0 Тогда
Продолжить;
КонецЕсли;
СписываемКоличество = Мин(НужноСписать, СтрПартии.ОстатокКоличество);
// Расчет суммы списания
Если СписываемКоличество = СтрПартии.ОстатокКоличество Тогда
СписываемСумму = СтрПартии.ОстатокСумма;
Иначе
СписываемСумму = Окр(СтрПартии.Сумма * (СписываемКоличество / СтрПартии.Количество), 2);
КонецЕсли;
// Формирование движения
Движение = Движения.ТоварыНаСкладах.ДобавитьРасход();
Движение.Период = Дата;
Движение.Номенклатура = СтрТЧ.НоменклатураИсходная;
Движение.Партия = СтрПартии.Партия;
Движение.Количество = СписываемКоличество;
Движение.Сумма = СписываемСумму;
// Корректировка остатка в служебной таблице
СтрПартии.ОстатокКоличество = СтрПартии.ОстатокКоличество - СписываемКоличество;
СтрПартии.ОстатокСумма = СтрПартии.ОстатокСумма - СписываемСумму;
НужноСписать = НужноСписать - СписываемКоличество;
КонецЦикла;
Если НужноСписать > 0 Тогда
// Обработка нехватки остатка
КонецЕсли;
КонецЦикла;
Проанализируем ситуацию, когда строк в документе и партий в остатках много. Метод НайтиСтроки() в цикле может замедлять проведение из-за постоянного создания массивов структур. Для максимальной производительности применим технику двух указателей (курсоров):
Номенклатура, Характеристика).Этот подход работает за один проход по обеим коллекциям со сложностью O(N + M), что значительно быстрее постоянного поиска по таблице.
При дроблении партий критически важно следить за округлением сумм. Если списывать стоимость только по формуле Окр(Цена * Количество, 2), то при закрытии последней единицы партии в регистре может остаться копеечный суммовой остаток при нулевом количестве.
Всегда используем следующее правило: если текущая строка полностью списывает весь оставшийся количественный остаток партии (СписываемКоличество = СтрПартии.ОстатокКоличество), то сумма списания берется как весь оставшийся суммовой остаток партии (СписываемСумму = СтрПартии.ОстатокСумма).
При выполнении проведения в управляемом режиме блокировок перед чтением таблицы остатков необходимо установить управляемую блокировку на пространство регистра с помощью объекта БлокировкаДанных. Это гарантирует, что параллельные транзакции не спишут те же самые партии до момента завершения текущей транзакции.
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.ИсточникДанных = МассивНоменклатуры;
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Значение");
Блокировка.Заблокировать();
Применение динамического контроля остатков в памяти или техники двух курсоров позволяет корректно распределять партии по любым сложным комбинациям дублирующихся строк номенклатуры без необходимости искусственного сворачивания табличной части документа.