В торгово-складском учете нередко возникает бизнес-задача: разделить крупный заказ клиента на несколько реализаций так, чтобы итоговая сумма каждого документа не превышала определенный лимит (например, требование лимита наличных расчетов, ограничение по договору или параметры страхования груза). Рассмотрим, как реализовать подобное разбиение табличной части на встроенном языке 1С:Предприятие, какие алгоритмы можно применить и на какие неочевидные учетные детали стоит обратить особое внимание.
Сформулируем задачу техническим языком: на входе имеется табличная часть документа (или таблица значений), содержащая номенклатуру, количество, цену и сумму. Имеется фиксированный лимит — переменная ПределПоСумме. На выходе необходимо получить набор новых документов (или промежуточных таблиц значений), каждый из которых в сумме не превышает заданный лимит.
При реализации данного алгоритма разработчик сталкивается с двумя ключевыми ситуациями:
Разберем базовый подход, при котором формируется массив структур или таблиц значений. Каждая таблица значений в массиве будет представлять собой будущую накладную. Чтобы не усложнять основной цикл ветвлениями, разделим обработку на два этапа:
Посмотрим на пример предварительной подготовки таблицы и разбиения ее по лимиту:
// Подготовим исходную таблицу значений
ТаблицаТоваров = ИсходныйДокумент.Товары.Выгрузить();
// Добавим колонку для группировки по документам
ТаблицаТоваров.Колонки.Добавить("НомерПачки", Новый ОписаниеТипов("Число"));
ПределПоСумме = 150000;
ТекущийНомерПачки = 1;
ТекущаяСуммаПачки = 0;
Для Каждого Стр Из ТаблицаТоваров Цикл
// Если добавление строки превышает лимит, переходим к следующей пачке
Если ТекущаяСуммаПачки > 0 И (ТекущаяСуммаПачки + Стр.Сумма > ПределПоСумме) Тогда
ТекущийНомерПачки = ТекущийНомерПачки + 1;
ТекущаяСуммаПачки = 0;
КонецЕсли;
Стр.НомерПачки = ТекущийНомерПачки;
ТекущаяСуммаПачки = ТекущаяСуммаПачки + Стр.Сумма;
КонецЦикла;
После разметки строк номерами пачек мы можем отобрать строки для каждого нового документа через копирование таблицы с установленным отбором:
// Получаем список уникальных номеров сформированных пачек
ТаблицаПачек = ТаблицаТоваров.Скопировать(, "НомерПачки");
ТаблицаПачек.Свернуть("НомерПачки");
Для Каждого СтрПачки Из ТаблицаПачек Цикл
Отбор = Новый Структура("НомерПачки", СтрПачки.НомерПачки);
СтрокиДокумента = ТаблицаТоваров.Скопировать(Отбор);
// Передаем строки в процедуру создания нового документа
СоздатьНакладнуюПоТаблице(СтрокиДокумента, ИсходныйДокумент);
КонецЦикла;
Проанализируем ситуацию, когда строка с товаром имеет сумму, превышающую лимит, либо когда свободный остаток в текущем документе меньше суммы строки, но мы хотим дозаполнить документ оставшимся количеством. В 1С нельзя делить строку исключительно по сумме: товар имеет единицу измерения, цену и ставку НДС.
Разберем по шагам правильный алгоритм дробления строки:
ОстатокЛимита = ПределПоСумме - ТекущаяСуммаДокумента.Цел(): КоличествоДляПереноса = Цел(ОстатокЛимита / СтрокаИсходная.Цена).КоличествоДляПереноса * СтрокаИсходная.Цена. При таком подходе цена остается точной, не появляются невалидные копейки и дробные доли штучного товара.Посмотрим на пример кода функции разделения строки:
Функция ВыделитьЧастьСтроки(ИсходнаяСтрока, ДоступнаяСумма)
Если ИсходнаяСтрока.Цена = 0 Тогда
Возврат Неопределено;
КонецЕсли;
// Для штучного товара берем только целую часть
КоличествоВДокумент = Цел(ДоступнаяСумма / ИсходнаяСтрока.Цена);
Если КоличествоВДокумент = 0 Тогда
Возврат Неопределено;
КонецЕсли;
// Создаем структуру с данными отделенной строки
НоваяСтрока = Новый Структура;
НоваяСтрока.Вставить("Номенклатура", ИсходнаяСтрока.Номенклатура);
НоваяСтрока.Вставить("Количество", КоличествоВДокумент);
НоваяСтрока.Вставить("Цена", ИсходнаяСтрока.Цена);
НоваяСтрока.Вставить("Сумма", КоличествоВДокумент * ИсходнаяСтрока.Цена);
// Уменьшаем исходную строку
ИсходнаяСтрока.Количество = ИсходнаяСтрока.Количество - КоличествоВДокумент;
ИсходнаяСтрока.Сумма = ИсходнаяСтрока.Количество * ИсходнаяСтрока.Цена;
Возврат НоваяСтрока;
КонецФункции
Если мы последовательно перебираем строки таблицы, количество итоговых документов может оказаться избыточным: в документах останутся «хвосты» неиспользованного лимита, куда не поместилась следующая крупная позиция. В теории алгоритмов это классическая задача о рюкзаке (Bin Packing Problem).
Чтобы минимизировать число создаваемых накладных, применим эвристику сортировки по убыванию сумм. Достаточно перед распределением выполнить сортировку:
// Сортируем позиции от самых дорогих к более дешевым
ТаблицаТоваров.Сортировать("Сумма Убыв");
В этом случае в первую очередь распределяются наиболее дорогие позиции, а остаток лимита в документах эффективно заполняется мелкими сопутствующими товарами.
Выясним причину возможных ошибок проведения и расхождений учета при разделении накладных:
НачатьТранзакцию();
Попытка
Для Каждого Пачка Из МассивПачек Цикл
ДокументРеализация = Документы.РеализацияТоваровУслуг.СоздатьДокумент();
// Заполнение реквизитов и табличной части
ДокументРеализация.ДокументОснование = ЗаказСсылка;
ДокументРеализация.Записать(РежимЗаписиДокумента.Проведение);
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("РазбиениеДокумента", УровеньЖурналаРегистрации.Ошибка,,,
ОписаниеОшибки());
ВызватьИсключение "Ошибка при формировании накладных: " + ОписаниеОшибки();
КонецПопытки;
Обязательно заполняйте реквизит ДокументОснование в новых накладных ссылкой на исходный заказ. Это позволит сохранить целостность структуры подчиненности в типовых конфигурациях и упростит дальнейший аудит цепочки документов.