Как распределить партии при наличии дублирующихся строк номенклатуры в документе?

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

В процессе разработки документов сопоставления, комплектации или пересортицы номенклатуры часто возникает задача списания партий исходного товара на несколько позиций нового. Например, в табличной части исходная номенклатура «Товар 1» встречается дважды: в первой строке сопоставляется с «Товар А» в количестве 3 штук, а во второй строке — с «Товар Б» в количестве 7 штук. Просто свернуть табличную часть по исходной номенклатуре нельзя, так как строки должны породить движения с привязкой к разным целевым позициям.

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

Решение 1: Динамическое уменьшение остатков в промежуточной таблице

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

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

  1. Формируем массив всей исходной номенклатуры из табличной части документа.
  2. Запросом получаем остатки партий (FIFO) по отобранным позициям и выгружаем результат в ТаблицаЗначений.
  3. Добавляем в полученную таблицу колонку ОстатокКоличество (и при необходимости ОстатокСумма), инициализируя ее начальным количеством партии.
  4. Для ускорения поиска устанавливаем индексы по колонке номенклатуры через метод Индексы.Добавить().
  5. В цикле обходим строки табличной части документа. Для каждой строки находим партии с ненулевым остатком и распределяем необходимое количество.
  6. При списании уменьшаем ОстатокКоличество в строке таблицы партий. Если остаток партии исчерпан до нуля, следующая строка документа автоматически перейдет к следующей доступной партии.

Посмотрим на пример реализации данного алгоритма:


// 1. Получаем уникальный массив номенклатуры
МассивНоменклатуры = ТабличнаяЧасть.ВыгрузитьКолонку("НоменклатураИсходная");

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

Запрос.УстановитьПараметр("МоментВремени", МоментВремени());
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);

ТаблицаОстатков = Запрос.Выполнить().Выгрузить();
ТаблицаОстатков.Колонки.Добавить("ОстатокКоличество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));
ТаблицаОстатков.Колонки.Добавить("ОстатокСумма", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));

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

ТаблицаОстатков.Индексы.Добавить("Номенклатура");

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

Решение 2: Использование алгоритма двух указателей (FIFO-курсоры)

Проанализируем ситуацию, когда строк в документе и партий в остатках много. Метод НайтиСтроки() в цикле может замедлять проведение из-за постоянного создания массивов структур. Для максимальной производительности применим технику двух указателей (курсоров):

  1. Сортируем обе таблицы (строки документа и партии) по одинаковому набору полей (Номенклатура, Характеристика).
  2. Последовательно продвигаемся по строкам обеих таблиц, используя целочисленные индексы текущих позиций.
  3. Как только количество в строке партии исчерпано, увеличиваем индекс партии на 1. Как только строка документа полностью закрыта, увеличиваем индекс строки документа.

Этот подход работает за один проход по обеим коллекциям со сложностью O(N + M), что значительно быстрее постоянного поиска по таблице.

Особенности суммового учета и защита от зависания «копеек»

При дроблении партий критически важно следить за округлением сумм. Если списывать стоимость только по формуле Окр(Цена * Количество, 2), то при закрытии последней единицы партии в регистре может остаться копеечный суммовой остаток при нулевом количестве.

Всегда используем следующее правило: если текущая строка полностью списывает весь оставшийся количественный остаток партии (СписываемКоличество = СтрПартии.ОстатокКоличество), то сумма списания берется как весь оставшийся суммовой остаток партии (СписываемСумму = СтрПартии.ОстатокСумма).

Конкурентность и управляемые блокировки

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


Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.ИсточникДанных = МассивНоменклатуры;
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Значение");
Блокировка.Заблокировать();

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

← На главную