В практике разработчика 1С часто возникает задача распределения некоторого общего ресурса (например, остатка товара на складе) по списку потребностей (заказам). Ситуация осложняется, когда общего остатка недостаточно для удовлетворения всех заказов, и требуется распределить его по определенному правилу: например, первым пришедшим (FIFO) или, наоборот, начиная с самых последних (LIFO). В этой статье мы подробно разберем, как реализовать подобную логику без использования циклов в коде 1С, переложив вычисления на сторону SQL-сервера или механизмы Системы компоновки данных (СКД).
Представим ситуацию: у нас есть три заказа. Заказ №1 требует 5 штук товара, Заказ №2 — 10 штук, Заказ №3 — 7 штук. Итого общая потребность составляет 22 штуки. Однако на складе в данный момент имеется всего 8 штук. Нам необходимо распределить эти 8 штук по заказам, начиная с самого последнего (Заказ №3). Проанализируем ситуацию: Заказ №3 должен получить свои 7 штук полностью, а оставшаяся 1 штука должна отойти Заказу №2. Заказ №1 в этом случае не получит ничего.
Для решения этой задачи мы рассмотрим два основных метода:
ВычислитьВыражение), что позволяет динамически менять порядок распределения в настройках отчета.Рассмотрим подробнее способ, основанный на использовании временных таблиц и соединения таблицы самой с собой. Основная идея заключается в том, чтобы для каждой строки заказа вычислить сумму количеств всех предыдущих (или последующих) заказов.
Разберем по шагам алгоритм создания такого запроса:
ВТ_Заказы. Очень важно обеспечить уникальную сортировку строк, используя дату, номер или ссылку на документ.ВТ_Заказы (как Т1) с этой же таблицей (как Т2) по условию Т1.Номенклатура = Т2.Номенклатура и Т1.Порядок >= Т2.Порядок.Т1, мы получаем сумму Т2.Количество, которая и будет нарастающим итогом.Посмотрим на пример кода запроса:
ВЫБРАТЬ
Заказы.Номенклатура,
Заказы.ЗаказПокупателя,
Заказы.Количество КАК ВЗаказе,
СУММА(ЗаказыАналоги.Количество) КАК НарастающийИтог,
МАКСИМУМ(Остатки.КоличествоОстаток) КАК ОбщийОстаток
ПОМЕСТИТЬ ВТ_Распределение
ИЗ
ВТ_Заказы КАК Заказы
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Заказы КАК ЗаказыАналоги
ПО Заказы.Номенклатура = ЗаказыАналоги.Номенклатура
И Заказы.Дата >= ЗаказыАналоги.Дата
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Остатки КАК Остатки
ПО Заказы.Номенклатура = Остатки.Номенклатура
ГРУППИРОВАТЬ ПО
Заказы.Номенклатура,
Заказы.ЗаказПокупателя,
Заказы.Количество
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_Распределение.Номенклатура,
ВТ_Распределение.ЗаказПокупателя,
ВТ_Распределение.ВЗаказе,
ВЫБОР
КОГДА ВТ_Распределение.ОбщийОстаток - (ВТ_Распределение.НарастающийИтог - ВТ_Распределение.ВЗаказе) > ВТ_Распределение.ВЗаказе
ТОГДА ВТ_Распределение.ВЗаказе
КОГДА ВТ_Распределение.ОбщийОстаток - (ВТ_Распределение.НарастающийИтог - ВТ_Распределение.ВЗаказе) > 0
ТОГДА ВТ_Распределение.ОбщийОстаток - (ВТ_Распределение.НарастающийИтог - ВТ_Распределение.ВЗаказе)
ИНАЧЕ 0
КОНЕЦ КАК РаспределенныйОстаток
ИЗ
ВТ_Распределение КАК ВТ_Распределение
В данном примере функция ВЫБОР реализует математическую логику: мы проверяем, сколько остатка "дошло" до текущей строки, вычитая из общего остатка сумму всех предыдущих строк. Если остатка больше, чем нужно на текущую строку — берем полное количество заказа. Если меньше — берем то, что осталось. Если остаток уже исчерпан — выводим 0.
Если задача стоит в рамках формирования отчета, использование СКД будет более гибким решением. Выясним причину: СКД позволяет использовать функцию ВычислитьВыражение, которая умеет считать нарастающие итоги по группировкам без сложных соединений в запросе.
Проанализируем ситуацию: нам нужно создать вычисляемое поле или ресурс, в котором будет рассчитываться распределение. Формула для ресурса Распределено будет выглядеть следующим образом:
ВЫБОР
КОГДА ЕстьNULL(ОстатокТовара, 0) - (ВычислитьВыражение("Сумма(КоличествоЗаказа)", , , "Первая", "Текущая") - КоличествоЗаказа) > КоличествоЗаказа
ТОГДА КоличествоЗаказа
КОГДА ЕстьNULL(ОстатокТовара, 0) - (ВычислитьВыражение("Сумма(КоличествоЗаказа)", , , "Первая", "Текущая") - КоличествоЗаказа) > 0
ТОГДА ЕстьNULL(ОстатокТовара, 0) - (ВычислитьВыражение("Сумма(КоличествоЗаказа)", , , "Первая", "Текущая") - КоличествоЗаказа)
ИНАЧЕ 0
КОНЕЦ
В этой формуле параметры "Первая" и "Текущая" указывают СКД, что суммирование должно идти от самой первой строки группировки до текущей включительно. Это и есть классический нарастающий итог.
Важный момент: если нам нужно распределять с конца (LIFO), как просил автор темы, нам достаточно в настройках СКД установить обратный порядок сортировки для детальных записей или группировок. Механизм СКД автоматически пересчитает нарастающий итог в соответствии с заданным порядком.
При использовании метода с самосоединением в запросе следует помнить о производительности. Если в таблице ВТ_Заказы будет несколько тысяч строк для одной и той же номенклатуры, количество соединений будет расти в геометрической прогрессии (квадратичная зависимость). Для оптимизации рекомендуем:
Номенклатура и полям сортировки во временных таблицах.Ссылка, чтобы исключить ситуацию, когда у двух документов совпадает время до секунды. Если время совпадет, условие >= сработает некорректно для обеих строк.Математически универсальная формула, которую можно использовать в 1С для лаконичности, выглядит так:
Распределено = МАКСИМУМ(0, МИНИМУМ(КоличествоВЗаказе, ОбщийОстаток - (НарастающийИтог - КоличествоВЗаказе)))
Она заменяет громоздкую конструкцию ВЫБОР и работает идентично, корректно отсекая отрицательные значения и значения, превышающие потребность заказа.
Мы рассмотрели два мощных инструмента для распределения остатков. Использование запроса с самосоединением идеально подходит для обработки данных внутри процедур проведения документов или сложных расчетов в коде. В то же время механизмы СКД предоставляют невероятную гибкость для аналитических отчетов, позволяя пользователю менять приоритеты распределения "на лету" простым изменением сортировки в настройках отчета.
Оба метода позволяют избежать "циклов в циклах", что положительно сказывается на стабильности системы и считается признаком высокого профессионализма разработчика 1С.