При разработке логистических модулей в 1С программисты часто сталкиваются с задачей распределения заказов по дням отгрузки. Типичный сценарий: у логиста есть интерактивная сетка (расписание), кликая по ячейкам которой он создает или открывает документ консолидированной отгрузки на определенный день. При этом система должна автоматически подбирать только те заказы покупателей, которые еще не были запланированы к доставке, а в самом документе — на лету пересчитывать суммарные логистические показатели (вес, объем, сумму) при ручном изменении состава заказа.
В данной статье мы подробно разберем, как спроектировать и оптимизировать такой функционал. Мы пройдем по шагам от написания производительного запроса для отбора свободных заказов до реализации интерактивного обновления строк табличной части и построения красивой печатной формы-расписания с программным объединением ячеек дат и контрагентов.
---
Первостепенная задача — получить список проведенных, обеспеченных и еще не отгруженных заказов покупателей по конкретному контрагенту на определенную дату. При этом из выборки должны исключаться те заказы, которые уже добавлены в другие (не помеченные на удаление) документы планирования отгрузки (в нашем примере это документ ПС_ЗаказыДляОтгрузки).
Частой ошибкой начинающих разработчиков является использование конструкции НЕ В (ВЫБРАТЬ...) с вложенным подзапросом. Проанализируем, почему этот подход неэффективен:
НЕ В на уровне базы данных часто преобразуется в ресурсоемкий correlated subquery (коррелированный подзапрос), из-за чего СУБД выполняет сканирование таблиц вместо индексного поиска. По мере роста базы данных отчеты и формы начнут ощутимо зависать.NULL, вся конструкция НЕ В вернет ложное значение, и итоговая выборка окажется пустой.Для решения этой проблемы применим паттерн ЛЕВОЕ СОЕДИНЕНИЕ + ЕСТЬ NULL. Мы присоединяем табличную часть распределенных заказов к исходным заказам покупателей по равенству ссылок и отсекаем записи, для которых связь не установилась (то есть документ отгрузки еще не создан).
Рассмотрим структуру оптимизированного запроса:
ВЫБРАТЬ РАЗРЕШЕННЫЕ
ЗаказПокупателя.Ссылка КАК ЗаказПокупателя,
ЗаказПокупателя.Контрагент КАК Контрагент,
ЗаказПокупателя.ДатаИсполнения КАК ДатаИсполнения,
СУММА(ЗаказПокупателяТЧ.Номенклатура.Вес * ЗаказПокупателяТЧ.Кол * ЗаказПокупателяТЧ.ЕдИзм.Коэффициент) КАК Вес,
СУММА(ЗаказПокупателяТЧ.Номенклатура.Объем * ЗаказПокупателяТЧ.Кол * ЗаказПокупателяТЧ.ЕдИзм.Коэффициент) КАК Объем,
СУММА(ЗаказПокупателяТЧ.Сумма) КАК Сумма
ИЗ
Документ.ЗаказПокупателя КАК ЗаказПокупателя
// Присоединяем статусы для проверки обеспечения и выполнения
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СтатусыОбеспеченияЗаказов КАК СтатусыОбеспеченияЗаказов
ПО СтатусыОбеспеченияЗаказов.Заказ = ЗаказПокупателя.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СтатусыВыполненияЗаказов КАК СтатусыВыполненияЗаказов
ПО СтатусыВыполненияЗаказов.Заказ = ЗаказПокупателя.Ссылка
// Присоединяем ТЧ для расчета общего веса и объема заказа
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ЗаказПокупателя.ТЧ КАК ЗаказПокупателяТЧ
ПО ЗаказПокупателя.Ссылка = ЗаказПокупателяТЧ.Ссылка
// Присоединяем табличную часть документов планирования отгрузки
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПС_ЗаказыДляОтгрузки.Заказы КАК ПС_ЗаказыДляОтгрузкиЗаказы
ПО ПС_ЗаказыДляОтгрузкиЗаказы.Заказ = ЗаказПокупателя.Ссылка
И ПС_ЗаказыДляОтгрузкиЗаказы.Ссылка.ПометкаУдаления = ЛОЖЬ
ГДЕ
ЗаказПокупателя.Проведен = ИСТИНА
И НЕ ЗаказПокупателя.ПометкаУдаления
И ЗаказПокупателя.Контрагент = &Контрагент
И ЗаказПокупателя.ДатаИсполнения <= &СчетчикМесяца
// Фильтрация по статусам выполнения и обеспечения
И СтатусыВыполненияЗаказов.Статус.Наименование <> "Оформлен"
И СтатусыВыполненияЗаказов.Статус.Наименование <> "Выполнен"
И СтатусыВыполненияЗаказов.Статус.Наименование <> "Не оформлен"
И СтатусыОбеспеченияЗаказов.Статус.Наименование <> "Не обеспечен"
// Главное условие: заказ еще НЕ привязан ни к одной отгрузке
И ПС_ЗаказыДляОтгрузкиЗаказы.Заказ ЕСТЬ NULL
СГРУППИРОВАТЬ ПО
ЗаказПокупателя.Ссылка,
ЗаказПокупателя.Контрагент,
ЗаказПокупателя.ДатаИсполнения
УПОРЯДОЧИТЬ ПО
ДатаИсполнения
Важный нюанс: В условии связи левого соединения ПС_ЗаказыДляОтгрузкиЗаказы.Ссылка.ПометкаУдаления = ЛОЖЬ мы сразу отсекаем те документы отгрузки, которые пользователь удалил или пометил на удаление. Благодаря этому заказы из удаленных документов мгновенно "выпадают" обратно в пул доступных для планирования на следующие дни.
---
Когда пользователь работает непосредственно в форме документа отгрузки и решает вручную заменить один заказ покупателя на другой в табличной части, система должна автоматически актуализировать логистические показатели строки: Вес, Объем и Сумму.
Рассмотрим реализацию этого механизма для двух типов форм: классического обычного интерфейса и современного управляемого приложения.
В событии ЗаказПриИзменении табличного поля мы получаем текущую строку через свойство ТекущиеДанные, выполняем быстрый запрос по ТЧ выбранного заказа покупателя и заполняем реквизиты строки формы.
Процедура ЗаказыЗаказПриИзменении(Элемент)
СтрокаТЧ = ЭлементыФормы.Заказы.ТекущиеДанные;
Если СтрокаТЧ = Неопределено Тогда
Возврат;
КонецЕсли;
Если Не ЗначениеЗаполнено(СтрокаТЧ.Заказ) Тогда
СтрокаТЧ.Вес = 0;
СтрокаТЧ.Объем = 0;
СтрокаТЧ.Сумма = 0;
Возврат;
КонецЕсли;
// Запрос для быстрого подсчета логистических параметров по выбранному заказу
ЗапросДанных = Новый Запрос;
ЗапросДанных.Текст =
"ВЫБРАТЬ
| СУММА(ЗаказПокупателяТЧ.Номенклатура.Вес * ЗаказПокупателяТЧ.Кол * ЗаказПокупателяТЧ.ЕдИзм.Коэффициент) КАК Вес,
| СУММА(ЗаказПокупателяТЧ.Номенклатура.Объем * ЗаказПокупателяТЧ.Кол * ЗаказПокупателяТЧ.ЕдИзм.Коэффициент) КАК Объем,
| СУММА(ЗаказПокупателяТЧ.Сумма) КАК Сумма
|ИЗ
| Документ.ЗаказПокупателя.ТЧ КАК ЗаказПокупателяТЧ
|ГДЕ
| ЗаказПокупателяТЧ.Ссылка = &СсылкаНаЗаказ";
ЗапросДанных.УстановитьПараметр("СсылкаНаЗаказ", СтрокаТЧ.Заказ);
Выборка = ЗапросДанных.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
СтрокаТЧ.Вес = Выборка.Вес;
СтрокаТЧ.Объем = Выборка.Объем;
СтрокаТЧ.Сумма = Выборка.Сумма;
КонецЕсли;
КонецПроцедуры
В управляемом приложении выполнять запросы на клиенте строго запрещено. Мы должны разделить логику: на клиенте обработать событие изменения, вызвать серверный контекстный или внеконтекстный метод для расчета весообъемных характеристик и вернуть структуру данных обратно на клиент.
&НаКлиенте
Процедура ЗаказыЗаказПриИзменении(Элемент)
СтрокаТЧ = Элементы.Заказы.ТекущиеДанные;
Если СтрокаТЧ = Неопределено Тогда
Возврат;
КонецЕсли;
Если ЗначениеЗаполнено(СтрокаТЧ.Заказ) Тогда
// Вызываем серверный метод без контекста для повышения производительности
СтруктураПараметров = ПолучитьПараметрыЗаказаНаСервере(СтрокаТЧ.Заказ);
ЗаполнитьЗначенияСвойств(СтрокаТЧ, СтруктураПараметров);
Иначе
СтрокаТЧ.Вес = 0;
СтрокаТЧ.Объем = 0;
СтрокаТЧ.Сумма = 0;
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьПараметрыЗаказаНаСервере(СсылкаНаЗаказ)
Результат = Новый Структура("Вес, Объем, Сумма", 0, 0, 0);
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЕСТЬNULL(СУММА(ЗаказПокупателяТЧ.Номенклатура.Вес * ЗаказПокупателяТЧ.Кол * ЗаказПокупателяТЧ.ЕдИзм.Коэффициент), 0) КАК Вес,
| ЕСТЬNULL(СУММА(ЗаказПокупателяТЧ.Номенклатура.Объем * ЗаказПокупателяТЧ.Кол * ЗаказПокупателяТЧ.ЕдИзм.Коэффициент), 0) КАК Объем,
| ЕСТЬNULL(СУММА(ЗаказПокупателяТЧ.Сумма), 0) КАК Сумма
|ИЗ
| Документ.ЗаказПокупателя.ТЧ КАК ЗаказПокупателяТЧ
|ГДЕ
| ЗаказПокупателяТЧ.Ссылка = &СсылкаНаЗаказ";
Запрос.УстановитьПараметр("СсылкаНаЗаказ", СсылкаНаЗаказ);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
ЗаполнитьЗначенияСвойств(Результат, Выборка);
КонецЕсли;
Возврат Результат;
КонецФункции
---
Перейдем к самой сложной, визуальной части задачи. Логисту необходимо видеть красивую печатную форму расписания отгрузок за выбранный период (например, месяц). Если в один и тот же день запланировано несколько заказов от разных контрагентов, или несколько заказов по одному контрагенту, ячейки дат и контрагентов должны красиво объединяться по вертикали.
Выясним причину основной проблемы, с которой часто сталкиваются разработчики при написании кода объединения: при наличии нескольких разных контрагентов в рамках одного дня ячейка даты объединяется не полностью, а разрывается на части. Это происходит потому, что объединение даты ошибочно выполняется внутри цикла обхода контрагентов. Чтобы объединение происходило на всю высоту строк всех контрагентов за день, расчет границ объединения даты должен производиться после завершения обработки всех контрагентов за эту дату.
Рассмотрим правильный алгоритм и код построения такого отчета по шагам:
Объединить().Проанализируем программный код реализации отчета:
Процедура СформироватьРасписаниеОтгрузок(ТабДок, ГоризонтПланирования)
ТабДок.Очистить();
МакетПечать = ПолучитьМакет("Печать_Макет");
// Определение интервала планирования
ТекущийПериод = НачалоДня(ТекущаяДата());
_Период = НачалоМесяца(ТекущийПериод);
_КонПериод = КонецМесяца(ГоризонтПланирования);
// Вывод заголовка отчета
Шапка = МакетПечать.ПолучитьОбласть("Шапка");
Шапка.Параметры.ДатаНач = _Период;
Шапка.Параметры.ДатаКон = _КонПериод;
ТабДок.Вывести(Шапка);
// Формирование запроса с итогами по иерархии планирования
ЗапросЗаказы = Новый Запрос;
ЗапросЗаказы.Текст =
"ВЫБРАТЬ
| ПС_ЗаказыДляОтгрузкиЗаказы.Ссылка.Контрагент КАК Контрагент,
| ПС_ЗаказыДляОтгрузкиЗаказы.Заказ КАК Заказ,
| ПС_ЗаказыДляОтгрузкиЗаказы.Вес КАК Вес,
| ПС_ЗаказыДляОтгрузкиЗаказы.Объем КАК Объем,
| ПС_ЗаказыДляОтгрузкиЗаказы.Сумма КАК Сумма,
| ПС_ЗаказыДляОтгрузкиЗаказы.Ссылка.ДеньОтгрузки КАК Дата
|ИЗ
| Документ.ПС_ЗаказыДляОтгрузки.Заказы КАК ПС_ЗаказыДляОтгрузкиЗаказы
|ГДЕ
| НЕ ПС_ЗаказыДляОтгрузкиЗаказы.Ссылка.ПометкаУдаления
| И ПС_ЗаказыДляОтгрузкиЗаказы.Ссылка.ДеньОтгрузки МЕЖДУ &ДатаНач И &ДатаКон
|УПОРЯДОЧИТЬ ПО
| Дата,
| Контрагент,
| Заказ
|ИТОГИ ПО
| Дата,
| Контрагент";
ЗапросЗаказы.УстановитьПараметр("ДатаНач", _Период);
ЗапросЗаказы.УстановитьПараметр("ДатаКон", _КонПериод);
Заказы = ЗапросЗаказы.Выполнить();
ВыборкаДаты = Заказы.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
// Индексы строк в табличном документе (зависят от высоты шапки, в примере шапка занимает 5 строк)
НомерСтрокиДата = 6;
НомерСтрокиКонтрагент = 6;
ОбластьСтрока = МакетПечать.ПолучитьОбласть("Таблица");
Пока _Период <= _КонПериод Цикл
ЕстьЗаказыЗаДень = Ложь;
ВыборкаДаты.Сбросить();
// Поиск данных по текущему дню в выборке запроса
Если ВыборкаДаты.НайтиСледующий(_Период, "Дата") Тогда
ЕстьЗаказыЗаДень = Истина;
ВыборкаКонтрагенты = ВыборкаДаты.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
СтрокЗаДень = 0; // Накапливаем общее число строк за весь день для объединения даты
Пока ВыборкаКонтрагенты.Следующий() Цикл
СтрокЗаказовКонтрагента = 0; // Накапливаем строки конкретного контрагента
ВыборкаЗаказы = ВыборкаКонтрагенты.Выбрать();
Пока ВыборкаЗаказы.Следующий() Цикл
СтрокЗаказовКонтрагента = СтрокЗаказовКонтрагента + 1;
// Заполнение параметров и вывод строки в документ
ОбластьСтрока.Параметры.Заполнить(ВыборкаЗаказы);
ОбластьСтрока.Параметры.Дата = Формат(_Период, "ДФ='dd.MM.yyyy (ddd)'");
ТабДок.Вывести(ОбластьСтрока);
КонецЦикла;
СтрокЗаДень = СтрокЗаДень + СтрокЗаказовКонтрагента;
// 1. Объединение ячеек Контрагента (Колонка 2)
Если СтрокЗаказовКонтрагента > 1 Тогда
ОбластьОбъединенияКг = ТабДок.Область(
НомерСтрокиКонтрагент,
2,
НомерСтрокиКонтрагент + СтрокЗаказовContragenta - 1,
2
);
ОбластьОбъединенияКг.Объединить();
КонецЕсли;
// Смещаем индекс строки контрагента на количество выведенных заказов
НомерСтрокиКонтрагент = НомерСтрокиКонтрагент + СтрокЗаказовКонтрагента;
КонецЦикла;
// 2. Объединение ячеек Даты отгрузки (Колонка 1) на ВСЮ высоту выведенных строк за день
Если СтрокЗаДень > 1 Тогда
ОбластьОбъединенияДаты = ТабДок.Область(
НомерСтрокиДата,
1,
НомерСтрокиДата + СтрокЗаДень - 1,
1
);
ОбластьОбъединенияДаты.Объединить();
КонецЕсли;
// Смещаем индекс строки даты на количество строк за день
НомерСтрокиДата = НомерСтрокиДата + СтрокЗаДень;
КонецЕсли;
// Если за день нет ни одного заказа — выводим пустую информационную строку
Если Не ЕстьЗаказыЗаДень Тогда
ОбластьСтрока.Параметры.Очистить();
ОбластьСтрока.Параметры.Дата = Формат(_Период, "ДФ='dd.MM.yyyy (ddd)'");
ТабДок.Вывести(ОбластьСтрока);
// Смещаем оба индекса на одну пустую выведенную строку
НомерСтрокиДата = НомерСтрокиДата + 1;
НомерСтрокиКонтрагент = НомерСтрокиКонтрагент + 1;
КонецЕсли;
// Переход к следующему дню
_Период = КонецДня(_Период) + 1;
КонецЦикла;
КонецПроцедуры
Разобранные в этой статье механизмы позволяют создать надежный фундамент для интерактивного планирования логистики. Использование оптимизированных левых соединений исключает падение производительности на больших объемах данных, своевременное обновление данных в формах гарантирует удобство работы пользователя, а алгоритм позиционирования строк и динамического объединения ячеек обеспечивает формирование аккуратной и понятной печатной формы.