В практической деятельности дистрибьюторских, производственных и логистических компаний (особенно в сферах FMCG, доставки воды, хлебобулочной или молочной продукции) ежедневно возникает одна и та же задача. Оператору или диспетчеру стола заказов необходимо оперативно ввести в систему огромное количество однотипных документов реализации. Представьте ситуацию: есть определенный маршрут доставки, на котором находятся около 150 контрагентов (точек доставки), и фиксированный ассортимент из 15–20 ходовых позиций номенклатуры. Вводить 150 отдельных документов вручную стандартными средствами — это колоссальная трата времени, чреватая множеством ошибок ввода.
Для решения этой проблемы идеально подходит специальный эргономичный интерфейс, который в профессиональной среде разработчиков 1С называют «матрицей заказов», «шахматкой» или «точковкой». В этой статье мы подробно разберем, как правильно спроектировать и реализовать такой механизм на управляемых формах 1С 8.3, используя производительный и гибкий подход.
Прежде чем приступать к написанию кода, проанализируем структуру хранения данных. Начинающие разработчики часто совершают архитектурную ошибку: пытаются создать в базе данных документы или регистры, физически повторяющие структуру «шахматки» (где колонками выступает сама номенклатура). Делать так категорически нельзя. База данных 1С оптимизирована под хранение «плоских» таблиц.
Правильный подход строится на паттерне «Распаковка — Запаковка»:
Tab или Enter и заполняет только количество купленного товара. Все пересчеты сумм и итогов происходят мгновенно на клиенте.Рассмотрим по шагам техническую реализацию каждого этапа.
При выводе матрицы на форму может возникнуть проблема: если контрагент ранее не заказывал какую-то позицию номенклатуры, то в физической таблице продаж нет связи между ними. Нам же на форме обязательно нужна полная сетка, где пересекаются каждый контрагент и каждый товар из списка. Для этого в запросе мы применим декартово произведение (соединение по условию ИСТИНА).
Проанализируем пример запроса, который подготавливает данные для нашей матрицы:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| МаршрутыСрезПоследних.Контрагент КАК Контрагент,
| Номенклатура.Ссылка КАК Номенклатура
|ПОМЕСТИТЬ ВТ_Сетка
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
| ПОЛНОЕ СОЕДИНЕНИЕ РегистрСведений.МаршрутыДоставки.СрезПоследних(&Период, Маршрут = &Маршрут) КАК МаршрутыСрезПоследних
| ПО (ИСТИНА)
|ГДЕ
| Номенклатура.ЭтоГруппа = ЛОЖЬ
| И Номенклатура.ПометкаУдаления = ЛОЖЬ
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Сетка.Контрагент КАК Контрагент,
| Сетка.Номенклатура КАК Номенклатура,
| ЕСТЬNULL(ЦеныНоменклатурыСрезПоследних.Цена, 0) КАК Цена
|ИЗ
| ВТ_Сетка КАК Сетка
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ВидЦены = &ВидЦены) КАК ЦеныНоменклатурыСрезПоследних
| ПО Сетка.Номенклатура = ЦеныНоменклатурыСрезПоследних.Номенклатура";
Запрос.УстановитьПараметр("Маршрут", Объект.Маршрут);
Запрос.УстановитьПараметр("Период", ТекущаяДата());
Запрос.УстановитьПараметр("ВидЦены", Объект.ВидЦены);
РезультатЗапроса = Запрос.Выполнить();
Этот запрос генерирует гарантированно полную матрицу связей всех контрагентов маршрута со всей доступной номенклатурой и сразу подтягивает актуальные цены для быстрой калькуляции на форме.
Выводить такую сетку через ТабличныйДокумент не рекомендуется — это замедлит интерфейс и усложнит обработку ввода данных. Мы воспользуемся динамическим конструированием реквизита формы с типом ТаблицаЗначений.
Разберем, как программно создать колонки на сервере при открытии формы или выборе маршрута. Нам необходимо создать базовую колонку «Контрагент», а затем динамически добавить по одной колонке для каждого товара, который вошел в наш список номенклатуры.
&НаСервере
Процедура ПерестроитьТаблицуНаФорме(РезультатЗапроса)
// 1. Очищаем старые динамические колонки, если они были созданы ранее
МассивУдаляемыхРеквизитов = Новый Массив;
Для Каждого Реквизит Из ПолучитьРеквизиты("ДеревоЗаказов") Цикл
Если Найти(Реквизит.Имя, "Номенклатура_") > 0 Тогда
МассивУдаляемыхРеквизитов.Добавить("ДеревоЗаказов." + Реквизит.Имя);
КонецЕсли;
КонецЦикла;
// Также удаляем элементы формы, связанные с этими колонками
Для Каждого Элемент Из Элементы.ДеревоЗаказов.ПодчиненныеЭлементы Цикл
Если Найти(Элемент.Имя, "ДеревоЗаказовНоменклатура_") > 0 Тогда
Элементы.Удалить(Элемент);
КонецЕсли;
КонецЦикла;
// 2. Создаем массив новых реквизитов для таблицы формы
МассивНовыхРеквизитов = Новый Массив;
// Получаем список номенклатуры для колонок из результатов нашего запроса
ТаблицаНоменклатуры = РезультатЗапроса.Выгрузить();
ТаблицаНоменклатуры.Свернуть("Номенклатура");
Для Каждого СтрокаНоменклатуры Из ТаблицаНоменклатуры Цикл
// Формируем уникальное имя колонки на основе УИД или Кода номенклатуры
ИмяКолонки = "Номенклатура_" + СтрокаНоменклатуры.Номенклатура.Код;
// Создаем описание типов для колонки (ввод только неотрицательного количества)
МассивТипов = Новый Массив;
МассивТипов.Добавить(Тип("Число"));
ОписаниеТиповЧисло = Новый ОписаниеТипов(МассивТипов, Новый КвалификаторыЧисла(10, 0, ДопустимыйЗнак.Неотрицательный));
НовыйРеквизит = Новый РеквизитФормы(ИмяКолонки, ОписаниеТиповЧисло, "ДеревоЗаказов", СтрокаНоменклатуры.Номенклатура.Наименование, Истина);
МассивНовыхРеквизитов.Добавить(НовыйРеквизит);
КонецЦикла;
// Выполняем транзакцию изменения структуры реквизитов формы
ИзменитьРеквизиты(МассивНовыхРеквизитов, МассивУдаляемыхРеквизитов);
// 3. Создаем элементы интерфейса (колонки в таблице на форме)
Для Каждого НовыйРеквизит Из МассивНовыхРеквизитов Цикл
ИмяЭлемента = "ДеревоЗаказов" + НовыйРеквизит.Имя;
НовыйЭлемент = Элементы.Добавить(ИмяЭлемента, Тип("ПолеФормы"), Элементы.ДеревоЗаказов);
НовыйЭлемент.Вид = ВидПоляФормы.ПолеВвода;
НовыйЭлемент.ПутьКДанным = "ДеревоЗаказов." + НовыйРеквизит.Имя;
НовыйЭлемент.УстановитьДействие("ПриИзменении", "ПодключитьОбработчикИзмененияКоличества");
КонецЦикла;
КонецПроцедуры
Обратите внимание на то, что мы программно привязали к событию ПриИзменении каждой созданной колонки единый обработчик ПодключитьОбработчикИзмененияКоличества. Это позволит нам мгновенно реагировать на любые изменения в ячейках таблицы.
После создания структуры колонок нам нужно перенести данные из плоской таблицы запроса в программно созданную таблицу на форме. Мы обходим выборку запроса, ищем нужную строку контрагента (или создаем новую) и заполняем значение в динамически созданную колонку по ее имени.
&НаСервере
Процедура ЗаполнитьДанныеШахматки(РезультатЗапроса)
// Получаем объект таблицы на форме
ТЗ_Формы = РеквизитФормыВЗначение("ДеревоЗаказов");
ТЗ_Формы.Очистить();
Выборка = РезультатЗапроса.Выбрать();
ТекущийКонтрагент = Неопределено;
НоваяСтрока = Неопределено;
Пока Выборка.Следующий() Цикл
Если ТекущийКонтрагент <> Выборка.Контрагент Тогда
ТекущийКонтрагент = Выборка.Контрагент;
НоваяСтрока = ТЗ_Формы.Добавить();
НоваяСтрока.Контрагент = ТекущийКонтрагент;
КонецЕсли;
// Определяем имя динамической колонки
ИмяКолонки = "Номенклатура_" + Выборка.Номенклатура.Код;
// Заполняем количество по умолчанию (например, 0 или план продаж)
НоваяСтрока[ИмяКолонки] = 0;
// Также сохраняем цены для быстрого пересчета (можно хранить во временной ТЗ на форме)
// ... код сохранения цен контрагентов ...
КонецЦикла;
ЗначениеВРеквизитФормы(ТЗ_Формы, "ДеревоЗаказов");
КонецПроцедуры
Для того чтобы интерфейс работал максимально плавно и отзывчиво, мы не должны делать серверные вызовы при изменении каждой ячейки. Пересчет сумм по строке (всего по клиенту) и по колонке (всего по товару) должен производиться исключительно на клиенте.
Рассмотрим, как устроен единый обработчик изменений в динамических ячейках:
&НаКлиенте
Процедура ПодключитьОбработчикИзмененияКоличества(Элемент)
СтрокаТаблицы = Элементы.ДеревоЗаказов.ТекущиеДанные;
Если СтрокаТаблицы = Неопределено Тогда
Возврат;
КонецЕсли;
// Получаем имя измененной колонки (например, "ДеревоЗаказовНоменклатура_00001")
ИмяЭлемента = Элемент.Имя;
ИмяКолонки = СтрЗаменить(ИмяЭлемента, "ДеревоЗаказов", "");
КодНоменклатуры = СтрЗаменить(ИмяКолонки, "Номенклатура_", "");
// Получаем введенное пользователем количество
ВведенноеКоличество = СтрокаТаблицы[ИмяКолонки];
// Выполняем моментальный расчет суммы по строке
// Для этого используем кэшированные на клиенте цены номенклатуры
ЦенаТовара = ПолучитьЦенуНоменклатурыИзКэша(СтрокаТаблицы.Контрагент, КодНоменклатуры);
СуммаСтрокиНовая = ВведенноеКоличество * ЦенаТовара;
// Обновляем общий итог по текущему контрагенту на клиенте
СтрокаТаблицы.ВсегоСумма = СтрокаТаблицы.ВсегоСумма + СуммаСтрокиНовая;
// Также можно обновить подвалы колонок для визуального контроля итогов по всей номенклатуре
ОбновитьИтогиПоКолонкеНаКлиенте(ИмяКолонки);
КонецПроцедуры
Такой подход обеспечивает невероятно высокую скорость отклика интерфейса. Оператор может вводить данные в стиле «Excel», переходя от ячейки к ячейке без задержек на передачу данных по сети.
Когда оператор проверил все итоги и убедился в корректности заполнения матрицы, он нажимает кнопку «Оформить документы». Наша задача на сервере — обойти коллекцию строк, сгруппировать данные по контрагентам и создать соответствующие документы в единой транзакции.
Проанализируем процедуру групповой генерации документов:
&НаСервере
Процедура ОформитьДокументыНаСервере()
ДеревоДанных = РеквизитФормыВЗначение("ДеревоЗаказов");
// Будем собирать все созданные документы в транзакции, чтобы гарантировать целостность
НачатьТранзакцию();
Попытка
Для Каждого СтрокаКлиента Из ДеревоДанных Цикл
// Проверим, были ли вообще заказы по этому контрагенту
ЕстьЗаказыПоКлиенту = Ложь;
ТаблицаНовыхТоваров = Новый ТаблицаЗначений;
ТаблицаНовыхТоваров.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТаблицаНовыхТоваров.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
// Обходим динамические колонки в строке клиента
Для Каждого Колонка Из ДеревоДанных.Колонки Цикл
Если Найти(Колонка.Имя, "Номенклатура_") > 0 Тогда
Количество = СтрокаКлиента[Колонка.Имя];
Если Количество > 0 Тогда
ЕстьЗаказыПоКлиенту = Истина;
// Определяем ссылку на номенклатуру по коду из имени колонки
КодТовара = СтрЗаменить(Колонка.Имя, "Номенклатура_", "");
СсылкаНоменклатуры = Справочники.Номенклатура.НайтиПоКоду(КодТовара);
НоваяСтрокаТовара = ТаблицаНовыхТоваров.Добавить();
НоваяСтрокаТовара.Номенклатура = СсылкаНоменклатуры;
НоваяСтрокаТовара.Количество = Количество;
КонецЕсли;
КонецЕсли;
КонецЦикла;
// Если клиент что-то заказал, создаем для него документ "Реализация товаров и услуг"
Если ЕстьЗаказыПоКлиенту Тогда
НовыйДокумент = Документы.РеализацияТоваровУслуг.СоздатьДокумент();
НовыйДокумент.Дата = РабочаяДата;
НовыйДокумент.Контрагент = СтрокаКлиента.Контрагент;
НовыйДокумент.Организация = Объект.Организация;
НовыйДокумент.ЗаполнитьЗначенияСвойствПоУмолчанию();
// Заполняем табличную часть документа
Для Каждого СтрокаТовара Из ТаблицаНовыхТоваров Цикл
НоваяСтрокаТЧ = НовыйДокумент.Товары.Добавить();
НоваяСтрокаТЧ.Номенклатура = СтрокаТовара.Номенклатура;
НоваяСтрокаТЧ.Количество = СтрокаТовара.Количество;
// Заполняем цены и рассчитываем суммы
НовыйДокумент.ЗаполнитьЦеныИЗначенияПоляТЧ(НоваяСтрокаТЧ);
КонецЦикла;
// Записываем и проводим документ
НовыйДокумент.Записать(РежимЗаписиДокумента.Проведение);
КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();
ПоказатьПредупреждение(, "Все документы успешно созданы и проведены!");
Исключение
ОтменитьТранзакцию();
ЗаписатьЛогОшибок(ОписаниеОшибки());
ВызватьИсключение "Ошибка при групповом создании документов! Изменения отменены. Подробности: " + ОписаниеОшибки();
КонецПопытки;
КонецПроцедуры
При внедрении подобного АРМ на реальных предприятиях полезно реализовать несколько улучшений, которые существенно облегчат жизнь операторам:
Таким образом, использование динамических таблиц на управляемых формах 1С 8.3 позволяет полностью перенести привычную пользователям логику работы из Excel прямо в информационную систему. Это исключает двойной ввод данных, минимизирует риски ошибок за счет автоматического контроля цен и остатков, а также обеспечивает высокую скорость и комфорт работы персонала.