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