В промышленном производстве, особенно в пищевой, химической или фармацевтической отраслях, критически важно уметь «отмотать» историю назад. Представим ситуацию: у нас есть партия готовой колбасы, и нам нужно точно знать, из каких туш говядины она была произведена, через какие промежуточные полуфабрикаты прошла и какие документы фиксировали каждый этап. В этой статье мы подробно разберем, как спроектировать архитектуру данных и реализовать алгоритм обратной цепочки по переделам.
Для начала проанализируем вводные данные. У нас есть цепочка переделов (этапов производства). На каждом этапе потребляется одна или несколько партий сырья/полуфабрикатов и выпускается новая партия продукции. Основная сложность заключается в том, что переделов может быть много (10 и более), а на одном этапе могут смешиваться разные партии. Нам необходимо построить дерево связей от конца (готовой продукции) к началу (сырью).
Рассмотрим два основных варианта хранения связей:
Справочник.Партии реквизит ПартияРодитель. Это создает линейную иерархию.Проанализируем второй вариант как наиболее гибкий. Создадим непериодический регистр сведений СвязиПартийПроизводства со следующими измерениями:
ПартияИсточник (Тип: СправочникСсылка.Партии)ПартияРезультат (Тип: СправочникСсылка.Партии)ДокументВыпуска (Тип: ДокументСсылка)Для построения цепочки нам потребуется рекурсивная функция. Мы будем передавать в нее текущую партию продукции, а она будет искать все «партии-источники», которые пошли на ее создание, и для каждой найденной партии вызывать саму себя.
Разберем пример кода на языке 1С:
Процедура ПостроитьДеревоПроизводства(ТекущаяПартия, ДеревоРезультат, ТекущийУзел)
// Выполняем запрос к нашему регистру связей
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СвязиПартий.ПартияИсточник КАК ВходнаяПартия,
| СвязиПартий.ДокументВыпуска КАК Документ
|ИЗ
| РегистрСведений.СвязиПартийПроизводства КАК СвязиПартий
|ГДЕ
| СвязиПартий.ПартияРезультат = &ТекущаяПартия";
Запрос.УстановитьПараметр("ТекущаяПартия", ТекущаяПартия);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Добавляем информацию в дерево (например, в ДеревоЗначений на форме)
НоваяСтрока = ДеревоРезультат.Строки.Добавить();
НоваяСтрока.Партия = Выборка.ВходнаяПартия;
НоваяСтрока.Документ = Выборка.Документ;
// Рекурсивный вызов для поиска следующего уровня (предыдущего передела)
ПостроитьДеревоПроизводства(Выборка.ВходнаяПартия, ДеревоРезультат, НоваяСтрока);
КонецЦикла;
КонецПроцедуры
Важно помнить: при использовании рекурсии всегда стоит устанавливать ограничение по глубине (например, не более 50 уровней), чтобы избежать бесконечного зацикливания, если в данных возникла ошибка (например, возврат брака в переработку, который снова попал в ту же партию).
Выполнение запроса в цикле (или в рекурсии) — это классическая ошибка производительности в 1С. Если переделов много, а дерево строится для большого списка продукции, система начнет «тормозить». Выясним, как это оптимизировать.
Проанализируем ситуацию: если мы знаем максимальное количество переделов (например, их не может быть больше 10), мы можем написать один запрос, который соединяет регистр сам с собой 10 раз (левое соединение). Однако более изящным решением будет использование цикла Пока с накоплением данных во временной таблице.
Рассмотрим по шагам логику такого запроса:
ВТ_Очередь исходную партию готовой продукции.ВТ_Очередь.ВТ_Очередь для следующей итерации.В реальном производстве часто возникает ситуация «многие-ко-многим». Например, из трех партий мяса разного сорта сделали один фарш, а этот фарш пошел на пять разных видов сосисок. Чтобы отчет был достоверным, нужно считать «долю участия» каждой партии.
Рассмотрим подробнее: если в документе выпуска указано, что на 100 кг продукции потрачено 50 кг Партии А и 50 кг Партии Б, то при раскрутке цепочки мы должны зафиксировать коэффициент 0.5 для каждой ветки. В регистре СвязиПартийПроизводства стоит хранить не только ссылки, но и количество. Это позволит в финальном отчете сказать: «В этом батоне колбасы весом 1 кг содержится 300 г говядины из партии №123 и 200 г шпика из партии №456».
Если ваша база данных работает на MS SQL Server или PostgreSQL, вы можете использовать механизм рекурсивных общих табличных выражений (Recursive CTE). Это позволяет выполнить весь обход дерева на стороне сервера за один вызов.
Хотя в языке запросов 1С напрямую CTE не поддерживаются, вы можете использовать их через механизм «Прямых запросов» (если конфигурация позволяет) или через внешний источник данных. Это дает колоссальный прирост скорости на глубоких деревьях (сотни и тысячи узлов за доли секунды).
Проанализируем ситуацию с переработкой брака. В производстве колбасы брак (например, лопнувшая оболочка) может быть измельчен и добавлен обратно в фарш. С точки зрения алгоритма — это цикл. Чтобы программа не зависла, при обходе дерева мы должны проверять, не встречалась ли нам эта партия ранее в текущей ветке поиска.
Совет: используйте массив ПосещенныеПартии. Перед тем как углубиться в рекурсию для партии Х, проверьте, нет ли ее в этом массиве. Если есть — мы нашли цикл, поиск по этой ветке нужно прекратить и вывести предупреждение пользователю.
Построение обратной цепочки — это задача, требующая строгого порядка в учете. Если в документах списания (например, «Производство без заказа» или «Отчет производства за смену») обязательно указываются партии (серии) сырья, то задача сводится к простому обходу графа. Использование регистра сведений для хранения связей «Источник-Результат» является наиболее устойчивым и масштабируемым решением для любой конфигурации на платформе 1С.