При разработке в среде 1С:Предприятие 8.3 (Управляемые формы) программисты часто сталкиваются с задачей автоматизации создания подчиненных документов. Типичный пример: на основании строк табличной части одного документа (например, ЗаказНаПроизводство) требуется создать несколько отдельных документов другого типа (например, СборкаЗапасов) по одному документу на каждую строку. При этом бизнес-логика требует не просто записать эти документы в базу данных в фоновом режиме, а показать их пользователю на экране незаписанными, чтобы он мог проверить данные, при необходимости отредактировать их и самостоятельно принять решение о записи или отмене.
Попытка реализовать эту логику напрямую «в лоб» через процедуру ОбработкаЗаполнения в модуле объекта целевого документа неизбежно приводит к ошибке. Давайте детально разберем причины возникновения этой проблемы, проанализируем архитектурные ограничения платформы 1С и построим надежное, отказоустойчивое решение.
Основная ошибка, которую совершают начинающие разработчики (и которая наглядно показана в первом сообщении темы), заключается в попытке вызвать метод открытия формы ОткрытьФорму() внутри процедуры ОбработкаЗаполнения модуля объекта целевого документа.
Выясним причину этой ошибки. Процедура ОбработкаЗаполнения выполняется исключительно на сервере в контексте объекта базы данных. На сервере не существует таких понятий, как «окно», «интерфейс» или «форма». Платформа 1С строго разграничивает контексты: любые интерактивные действия (открытие окон, предупреждения, вопросы пользователю) могут выполняться только в клиентском потоке (на тонком или веб-клиенте). Серверный код физически не способен инициализировать открытие окна на стороне пользователя.
Для корректного решения этой задачи нам потребуется перенести логику управления интерфейсом в модуль формы исходного документа (документа-основания) и организовать правильное взаимодействие между клиентом и сервером.
Чтобы реализовать корректное открытие множества незаписанных документов в цикле, нам необходимо решить три ключевые проблемы:
ОткрытьФорму("Документ.СборкаЗапасов.Форма.ФормаДокумента"), платформа 1С откроет только одно окно. Все последующие вызовы в цикле будут просто активизировать и перезаписывать это же самое открытое окно. Чтобы открыть несколько отдельных независимых окон новых документов, необходимо использовать специальный параметр — Ключ уникальности формы.НомерСтроки табличной части для связи документов крайне опасно. Если пользователь отсортирует табличную часть документа-основания, удалит среднюю строку или добавит новую, нумерация строк изменится («поедет»), и связь между документами будет безвозвратно нарушена. Решением является использование уникальных идентификаторов строк (UUID).Рассмотрим подробнее по шагам, как реализовать каждый из этих этапов.
Для надежного связывания документов добавим в конфигурацию уникальные идентификаторы строк. Выполним следующие настройки в конфигураторе:
ЗаказНаПроизводство) в табличную часть Продукция добавим новый реквизит:
КлючСвязиСтрокиСтрока (длина 36, для хранения UUID) или УникальныйИдентификатор.ДокументСборки с типом ссылки на создаваемый документ ДокументСсылка.СборкаЗапасов. Это позволит выводить ссылку на созданный документ прямо в строке ТЧ.СборкаЗапасов добавим реквизиты шапки:
ДокументСсылка.ЗаказНаПроизводствоСтрока (длина 36).Теперь напишем серверный механизм генерации уникальных ключей для строк ТЧ документа-источника при его записи. В модуле объекта документа ЗаказНаПроизводство разместим следующий код:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Гарантируем наличие уникального ключа у каждой строки ТЧ перед сохранением
Для Каждого СтрокаТЧ Из Продукция Цикл
Если Не ЗначениеЗаполнено(СтрокаТЧ.КлючСвязиСтроки) Тогда
СтрокаТЧ.КлючСвязиСтроки = Строка(Новый УникальныйИдентификатор);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Переходим к разработке интерфейсной части. На форме документа-источника создадим кнопку «Создать документы сборки» и привяжем к ней клиентскую процедуру.
Проанализируем ситуацию: перед созданием подчиненных документов крайне важно убедиться, что сам родительский документ уже записан в базу данных, так как нам потребуется передавать его ссылку в качестве основания. Если документ изменен или еще не записан, мы должны предложить пользователю сохранить его.
В модуле формы документа-источника напишем следующий код:
&НаКлиенте
Процедура СоздатьДокументыСборки(Команда)
// Проверяем, записан ли текущий документ в БД
Если Объект.Ссылка.Пустая() Тогда
ПоказатьПредупреждение(, "Перед созданием подчиненных документов необходимо записать текущий документ!");
Возврат;
КонецЕсли;
// Проверяем наличие модифицированности (несохраненных изменений)
Если Модифицированность Тогда
ОписаниеОповещения = Новый ОписаниеОповещения("ПослеПодтвержденияЗаписи", ЭтотОбъект);
ПоказатьВопрос(ОписаниеОповещения, "Документ изменен. Для продолжения его необходимо записать. Записать?", РежимДиалогаВопрос.ДаНет);
Возврат;
КонецЕсли;
ВыполнитьСозданиеПодчиненныхДокументов();
КонецПроцедуры
&НаКлиенте
Процедура ПослеПодтвержденияЗаписи(РезультатВопроса, ДополнительныеПараметры) Экспорт
Если РезультатВопроса = КодВозвратаДиалога.Да Тогда
Если Записать() Тогда
ВыполнитьСозданиеПодчиненныхДокументов();
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Теперь детально разберем саму процедуру создания и открытия форм в цикле. Обратите внимание на использование четвертого параметра метода ОткрытьФорму() — именно он отвечает за уникальность каждого открываемого окна:
&НаКлиенте
Процедура ВыполнитьСозданиеПодчиненныхДокументов()
Для Каждого СтрокаТЧ Из Объект.Продукция Цикл
// Если для этой строки документ уже был создан и записан ранее - пропускаем
Если ЗначениеЗаполнено(СтрокаТЧ.ДокументСборки) Тогда
Продолжить;
КонецЕсли;
// Формируем структуру параметров заполнения для передачи в новый документ
ПараметрыЗаполнения = Новый Структура;
ПараметрыЗаполнения.Вставить("ДокументОснование", Объект.Ссылка);
ПараметрыЗаполнения.Вставить("КлючСвязиСтроки", СтрокаТЧ.КлючСвязиСтроки);
ПараметрыЗаполнения.Вставить("Номенклатура", СтрокаТЧ.Номенклатура);
ПараметрыЗаполнения.Вставить("Количество", СтрокаТЧ.Количество);
// Передаем параметры через стандартное свойство "ЗначенияЗаполнения"
ПараметрыФормы = Новый Структура;
ПараметрыФормы.Вставить("ЗначенияЗаполнения", ПараметрыЗаполнения);
// Генерируем уникальный ключ для каждого окна
УникальныйКлючФормы = Новый УникальныйИдентификатор;
// Открываем форму нового документа интерактивно
ОткрытьФорму(
"Документ.СборкаЗапасов.Форма.ФормаДокумента", // Имя открываемой формы
ПараметрыФормы, // Параметры открытия
ЭтаФорма, // Владелец формы (наша форма заказа)
УникальныйКлючФормы // Ключ уникальности
);
КонецЦикла;
КонецПроцедуры
Теперь нам необходимо научить создаваемый документ СборкаЗапасов правильно принимать переданные параметры и заполнять свои реквизиты. Поскольку мы передали структуру в параметр ЗначенияЗаполнения, платформа автоматически вызовет процедуру ОбработкаЗаполнения в модуле объекта нового документа.
Откроем модуль объекта документа СборкаЗапасов и пропишем алгоритм разбора параметров:
Процедура ОбработкаЗаполнения(ДанныеЗаполнения, СтандартнаяОбработка)
Если ТипЗнч(ДанныеЗаполнения) = Тип("Структура") Тогда
// Заполняем реквизиты шапки документа
Если ДанныеЗаполнения.Свойство("ДокументОснование") Тогда
ЭтотОбъект.ДокументОснование = ДанныеЗаполнения.ДокументОснование;
КонецЕсли;
Если ДанныеЗаполнения.Свойство("КлючСвязиСтроки") Тогда
ЭтотОбъект.КлючСвязиСтроки = ДанныеЗаполнения.КлючСвязиСтроки;
КонецЕсли;
// Заполняем табличную часть нового документа (например, ТЧ "Запасы")
НоваяСтрока = ЭтотОбъект.Запасы.Добавить();
Если ДанныеЗаполнения.Свойство("Номенклатура") Тогда
НоваяСтрока.Номенклатура = ДанныеЗаполнения.Номенклатура;
КонецЕсли;
Если ДанныеЗаполнения.Свойство("Количество") Тогда
НоваяСтрока.Количество = ДанныеЗаполнения.Количество;
КонецЕсли;
// Отключаем стандартную обработку заполнения, если требуется полностью кастомная логика
СтандартнаяОбработка = Ложь;
КонецЕсли;
КонецПроцедуры
Давайте разберем, как связать форму источника и формы новых документов после того, как пользователь нажмет кнопку «Записать» в одном из новых документов.
Для этого используется механизм глобальных оповещений управляемого интерфейса 1С. В модуле формы документа СборкаЗапасов найдем или переопределим предопределенную клиентскую процедуру ПослеЗаписи:
&НаКлиенте
Процедура ПослеЗаписи(ПараметрыЗаписи)
// Структура с данными для отправки родительской форме
ДанныеОповещения = Новый Структура;
ДанныеОповещения.Вставить("Ссылка", Объект.Ссылка);
ДанныеОповещения.Вставить("КлючСвязиСтроки", Объект.КлючСвязиСтроки);
// Отправляем широковещательное оповещение по всей системе
Оповестить("СозданДокументСборкиПоСтроке", ДанныеОповещения, ЭтотОбъект);
КонецПроцедуры
Теперь вернемся в модуль формы документа-источника ЗаказНаПроизводство. Нам необходимо перехватить отправленное оповещение и сопоставить записанный документ с соответствующей строкой табличной части по уникальному ключу связи. Для этого задействуем предопределенную процедуру формы ОбработкаОповещения:
&НаКлиенте
Процедура ОбработкаОповещения(ИмяСобытия, Параметр, Источник)
// Проверяем, наше ли это событие
Если ИмяСобытия = "СозданДокументСборкиПоСтроке" Тогда
// Анализируем полученную структуру данных
Если ТипЗнч(Параметр) = Тип("Структура") И Параметр.Свойство("КлючСвязиСтроки") Тогда
// Ищем строку табличной части по уникальному ключу связи
ПараметрыПоиска = Новый Структура("КлючСвязиСтроки", Параметр.КлючСвязиСтроки);
НайденныеСтроки = Объект.Продукция.НайтиСтроки(ПараметрыПоиска);
Если НайденныеСтроки.Количество() > 0 Тогда
СтрокаТЧ = НайденныеСтроки[0];
// Проверяем, не была ли ссылка уже установлена ранее
Если СтрокаТЧ.ДокументСборки <> Параметр.Ссылка Тогда
СтрокаТЧ.ДокументСборки = Параметр.Ссылка;
// Сигнализируем интерфейсу о необходимости перерисовать ТЧ
Модифицированность = Истина;
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Проанализируем другую ситуацию: что делать, если из одной строки ТЧ родительского документа требуется передавать в дочерний документ не 3-4 простых реквизита, а огромный объем сложных структурированных данных (например, спецификации, вложенные таблицы, дерево настроек)? Передача сверхбольших структур через параметры формы на клиенте может сильно замедлить работу интерфейса и перегрузить сетевой трафик между клиентом и сервером.
Для таких случаев применяется профессиональный паттерн проектирования на базе Временного хранилища:
ПоместитьВоВременноеХранилище(Данные, УникальныйИдентификаторФормы). Метод возвращает строковый адрес вида e1cib/tempstorage/....ПараметрыФормы.Вставить("АдресДанныхЗаполнения", АдресВХ);ПриСозданииНаСервере этот адрес извлекается из параметров, данные считываются функцией ПолучитьИзВременногоХранилища(Адрес) и раскладываются по реквизитам формы дочернего документа.Этот подход гарантирует высокую скорость работы веб-клиента и тонкого клиента даже при передаче колоссальных объемов информации между открываемыми формами.
Мы успешно решили поставленную задачу и разработали архитектурно верное решение. Перенос логики генерации окон на сторону клиента с использованием механизма Уникальность позволил обойти ограничения клиент-серверного разделения. Внедрение уникальных ключей строк ТЧ полностью защитило логику программы от изменения порядка строк пользователем, а механизм Оповестить обеспечил оперативную и красивую обратную связь в интерфейсе программы.