Как правильно открыть форму записи регистра сведений из серверного кода и вернуть результат в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

В процессе разработки бизнес-логики в среде «1С:Предприятие 8.3» мы часто сталкиваемся со сложными многоступенчатыми алгоритмами импорта данных. Типичный сценарий: на сервере выполняется чтение внешнего файла (например, накладной или спецификации), выполняется поиск сопоставлений в регистре сведений, и если какая-то позиция не найдена, требуется вовлечь пользователя — открыть форму записи регистра сведений, заполнить её базовыми параметрами и дождаться, пока пользователь заполнит недостающие поля и нажмет кнопку «Записать». После этого программа должна продолжить прерванный процесс.

Попытка реализовать эту логику напрямую «в лоб» внутри серверной процедуры неизбежно приводит к аварийному завершению работы системы. Давайте подробно проанализируем ошибки в исходной архитектуре, разберем базовые принципы клиент-серверного взаимодействия платформы 1С и реализуем правильный, надежный и асинхронный паттерн решения этой задачи.

Анализ архитектурных ошибок исходного кода

В коде, приведенном в вопросе автора темы, присутствуют сразу несколько критических архитектурных ошибок. Рассмотрим их подробнее, чтобы понять, почему платформа отказывается компилировать и выполнять такую программу.

  1. Нарушение клиент-серверной границы. Функция ПолучитьДанныеОтвесовДляБД выполняется на сервере (директива компиляции &НаСервере). Внутри цикла обхода данных разработчик пытается вызвать функцию ОткрытьФормуРС, которая объявлена на клиенте (директива &НаКлиенте). Платформа 1С работает по строгому правилу: вызов клиентских процедур и функций из серверного контекста невозможен. Сервер ничего не знает об интерфейсе, отображаемых окнах и действиях пользователя. При попытке такого вызова система выдает закономерную ошибку: «Процедура или функция с указанным именем не определена».
  2. Интерактивные действия внутри цикла на сервере. Даже если бы вызов выполнялся на клиенте, открывать формы ввода данных в цикле — крайне неэффективное решение. Если во внешнем файле окажется 50 или 100 ненайденных позиций номенклатуры, у пользователя на экране одновременно или последовательно откроется лавина из сотен модальных окон. Это перегрузит систему и сделает работу пользователя невыносимой.
  3. Ожидание ссылки на запись регистра сведений. Регистр сведений — это нессылочный тип данных. В отличие от справочников или документов, запись регистра сведений не имеет уникального идентификатора (UID) и ссылки. Для однозначной идентификации записи используется так называемый Ключ записи (объект типа РегистрСведенийКлючЗаписи), который представляет собой комбинацию значений всех измерений регистра. Передавать и возвращать «ссылку» на запись регистра невозможно физически.
  4. Использование устаревшей модальности. Метод ОткрытьФормуМодально полностью запрещен к использованию в современных конфигурациях (например, при использовании интерфейса «Такси» и отключенном режиме совместимости использования модальности). Любое модальное окно блокирует весь интерфейс приложения и не поддерживается в веб-клиенте. Все интерактивные операции должны выполняться исключительно асинхронно.

Правильный паттерн проектирования: сбор пула ненайденных данных

Для того чтобы решить задачу корректно и в соответствии со стандартами разработки фирмы «1С», мы должны полностью перестроить алгоритм. Разделим его на три четких шага:

Ниже мы разберем по шагам детальную реализацию первого варианта (последовательного асинхронного сопоставления через очередь) как наиболее близкого к изначальной задумке автора.

Реализация шага 1: Серверный анализ данных

Переработаем серверную функцию. Теперь её задача — не выполнять полную транзакцию, а провести ревизию и вернуть на клиент список сопоставленных данных и список позиций, требующих внимания пользователя.


&НаСервере
Функция ПроверитьДанныеДляЗагрузки(ДанныеОтвесов, Поставщик)
    
    Результат = Новый Структура;
    Результат.Вставить("ГотовыеДанные", Новый Массив);
    Результат.Вставить("ТребуетСопоставления", Новый Массив);
    
    Для Каждого СтрДО Из ДанныеОтвесов Цикл
        
        // Пытаемся найти существующую запись в регистре сведений
        НоменклатураРС = НайтиНоменклатуру(СтрДО.Товар);
        
        Если НоменклатураРС = Неопределено Тогда
            // Запись не найдена, собираем параметры для будущего создания записи
            ПараметрыЗаписи = Новый Структура;
            ПараметрыЗаписи.Вставить("Поставщик", Поставщик);
            ПараметрыЗаписи.Вставить("Артикул", СтрДО.Товар);
            ПараметрыЗаписи.Вставить("Идентификатор", СтрДО.Товар); // Или другое уникальное поле
            
            Результат.ТребуетСопоставления.Добавить(ПараметрыЗаписи);
        Иначе
            // Сопоставление найдено успешно
            СтрокаРезультата = Новый Структура;
            СтрокаРезультата.Вставить("Номенклатура", НоменклатураРС.Номенклатура);
            СтрокаРезультата.Вставить("СерияНоменклатуры", СтрДО.Формат);
            Результат.ГотовыеДанные.Добавить(СтрокаРезультата);
        КонецЕсли;
        
    КонецЦикла;
    
    Возврат Результат;
    
КонецФункции

Реализация шагов 2 и 3: Клиентская управляющая логика и асинхронный вызов

Теперь на стороне клиента мы должны обработать результат, возвращенный сервером. Для организации бесконфликтного поочередного открытия окон мы будем использовать рекурсивный вызов клиентской процедуры, завязанный на асинхронные оповещения платформы 1С.

Рассмотрим структуру клиентского кода подробнее. Основная процедура запускает процесс проверки, и если сервер вернул ненайденные позиции, управление передается конвейеру обработки очереди.


&НаКлиенте
Процедура НачатьЗагрузкуДанных()
    
    // Вызываем серверный анализ
    РезультатПроверки = ПроверитьДанныеДляЗагрузки(Объект.ДанныеОтвесов, Объект.Поставщик);
    
    Если РезультатПроверки.ТребуетСопоставления.Количество() > 0 Тогда
        // Есть ненайденные позиции, запускаем конвейер интерактивного ввода
        ОбработатьОчередьСопоставления(РезультатПроверки.ТребуетСопоставления, 0, РезультатПроверки.ГотовыеДанные);
    Else
        // Все сопоставления есть, сразу переходим к созданию документов в базе данных
        ВыполнитьФинальнуюЗагрузку(РезультатПроверки.ГотовыеДанные);
    КонецЕсли;
    
КонецПроцедуры

Теперь спроектируем процедуру ОбработатьОчередьСопоставления. Она принимает массив ненайденных позиций, индекс текущего элемента, который нужно обработать прямо сейчас, и массив уже успешно сопоставленных данных (чтобы накапливать результат).

Обратите внимание на то, как мы передаем параметры заполнения в форму новой записи регистра сведений. Платформа 1С поддерживает стандартный параметр ЗначенияЗаполнения, который автоматически подставляет переданную структуру в соответствующие реквизиты открываемой формы записи.


&НаКлиенте
Процедура ОбработатьОчередьСопоставления(Очередь, ТекущийИндекс, НакопленныеДанные)
    
    // Проверяем, завершен ли обход очереди
    Если ТекущийИндекс >= Очередь.Количество() Тогда
        // Все элементы успешно заполнены пользователем! 
        // Повторно вызываем проверку или сразу отправляем накопленный пул на финальную запись
        ПоказатьПредупреждение(, "Все сопоставления успешно заполнены. Завершаем обработку документов.");
        ВыполнитьФинальнуюЗагрузку(НакопленныеДанные);
        Возврат;
    КонецЕсли;
    
    ТекущаяНоменклатура = Очередь[ТекущийИндекс];
    
    // Формируем параметры для открытия новой записи регистра сведений
    ПараметрыЗаполнения = Новый Структура;
    ПараметрыЗаполнения.Вставить("Владелец", ТекущаяНоменклатура.Поставщик);
    ПараметрыЗаполнения.Вставить("Артикул", ТекущаяНоменклатура.Артикул);
    ПараметрыЗаполнения.Вставить("Идентификатор", ТекущаяНоменклатура.Идентификатор);
    
    ПараметрыФормы = Новый Структура;
    ПараметрыФормы.Вставить("ЗначенияЗаполнения", ПараметрыЗаполнения);
    
    // Сохраняем контекст выполнения в дополнительных параметрах оповещения
    ДополнительныеПараметры = Новый Структура;
    ДополнительныеПараметры.Вставить("Очередь", Очередь);
    ДополнительныеПараметры.Вставить("ТекущийИндекс", ТекущийИндекс);
    ДополнительныеПараметры.Вставить("НакопленныеДанные", НакопленныеДанные);
    
    // Создаем описание оповещения, которое сработает при закрытии формы записи регистра
    Оповещение = Новый ОписаниеОповещения("ПослеЗакрытияФормыЗаписиРС", ЭтотОбъект, ДополнительныеПараметры);
    
    // Открываем форму асинхронно (НЕ модально!)
    ОткрытьФорму(
        "РегистрСведений.НоменклатураКонтрагентовБЭД.ФормаЗаписи", 
        ПараметрыФормы, 
        ЭтотОбъект, 
        УникальныйИдентификатор, 
        , 
        , 
        Оповещение
    );
    
КонецПроцедуры

Теперь нам необходим обработчик оповещения — процедура ПослеЗакрытияФормыЗаписиРС. Платформа вызовет её автоматически, как только пользователь закроет форму записи регистра сведений (сохранив изменения или просто закрыв окно крестиком).

Для того чтобы понять, внес ли пользователь изменения, мы можем проанализировать возвращаемый параметр РезультатЗакрытия. Для форм записи регистров сведений платформа при успешной записи обычно возвращает созданный РегистрСведенийКлючЗаписи. Если пользователь отказался от ввода и закрыл форму без сохранения, вернется значение Неопределено.


&НаКлиенте
Процедура ПослеЗакрытияФормыЗаписиРС(РезультатЗакрытия, ДополнительныеПараметры) Экспорт
    
    Очередь = ДополнительныеПараметры.Очередь;
    Индекс = ДополнительныеПараметры.ТекущийИндекс;
    НакопленныеДанные = ДополнительныеПараметры.НакопленныеДанные;
    
    Если РезультатЗакрытия = Неопределено Тогда
        // Пользователь отказался вводить сопоставление (закрыл форму)
        // Мы можем прервать весь процесс или спросить, желает ли он пропустить строку
        ПоказатьВопрос(
            Новый ОписаниеОповещения("ПослеРешенияОПропускеСтроки", ЭтотОбъект, ДополнительныеПараметры),
            "Вы не заполнили сопоставление для номенклатуры. Пропустить эту позицию и продолжить?",
            РежимДиалогаВопрос.ДаНет
        );
        Возврат;
    КонецЕсли;
    
    // Если запись успешно создана, нам нужно получить сопоставленную номенклатуру
    // Для этого выполним быстрый серверный вызов по ключу созданной записи
    НоваяНоменклатура = ПолучитьНоменклатуруИзКлючаЗаписи(РезультатЗакрытия);
    
    // Добавляем успешно сопоставленные данные к общему результату
    СтрокаРезультата = Новый Структура;
    СтрокаРезультата.Вставить("Номенклатура", НоваяНоменклатура);
    СтрокаРезультата.Вставить("СерияНоменклатуры", Очередь[Индекс].Артикул); // Условный пример
    НакопленныеДанные.Добавить(СтрокаРезультата);
    
    // Переходим к следующему элементу очереди сопоставления
    ОбработатьОчередьСопоставления(Очередь, Индекс + 1, НакопленныеДанные);
    
КонецПроцедуры

Добавим вспомогательный интерактивный обработчик на случай, если пользователь решил отменить создание записи для конкретной строки:


&НаКлиенте
Процедура ПослеРешенияОПропускеСтроки(РезультатВопроса, ДополнительныеПараметры) Экспорт
    
    Если РезультатВопроса = КодВозвратаДиалога.Да Тогда
        // Пользователь решил пропустить текущую строку, идем дальше к следующему элементу
        НовыйИндекс = ДополнительныеПараметры.ТекущийИндекс + 1;
        ОбработатьОчередьСопоставления(ДополнительныеПараметры.Очередь, НовыйИндекс, ДополнительныеПараметры.НакопленныеДанные);
    Else
        // Пользователь решил полностью отменить всю операцию загрузки данных
        ПоказатьПредупреждение(, "Загрузка данных прервана пользователем.");
    КонецЕсли;
    
КонецПроцедуры

Для полноты картины напишем серверную функцию извлечения номенклатуры по возвращенному ключу записи. Поскольку регистр сведений не имеет ссылок, ключ записи содержит значения измерений, по которым мы можем быстро получить нужное поле ресурса (в нашем случае — реквизит Номенклатура).


&НаСервереБезКонтекста
Функция ПолучитьНоменклатуруИзКлючаЗаписи(КлючЗаписи)
    
    Если КлючЗаписи = Неопределено Тогда
        Возврат Справочники.Номенклатура.ПустаяСсылка();
    КонецЕсли;
    
    // Создаем менеджер записи для чтения данных из БД
    МенеджерЗаписи = РегистрыСведений.НоменклатураКонтрагентовБЭД.СоздатьМенеджерЗаписи();
    ЗаполнитьЗначенияСвойств(МенеджерЗаписи, КлючЗаписи);
    МенеджерЗаписи.Прочитать();
    
    Если МенеджерЗаписи.Выбран() Тогда
        Возврат МенеджерЗаписи.Номенклатура;
    КонецЕсли;
    
    Возврат Справочники.Номенклатура.ПустаяСсылка();
    
КонецФункции

Альтернативный подход: Использование механизмов БСП

Если ваша конфигурация разработана на платформе «1С:Предприятие 8.3» с использованием Библиотеки стандартных подсистем (БСП), то во многих случаях ручное написание кода открытия форм не требуется. В БСП встроен готовый функционал общего модуля, который умеет самостоятельно анализировать переданный отбор и принимать решение: открывать существующую запись для редактирования или создавать новую с заполнением полей.

Обратите внимание на вызов клиентского метода БСП:


// Открывает форму записи регистра сведений с отбором и предустановленными значениями
ОбменДаннымиКлиент.ОткрытьФормуЗаписиРегистраСведенийПоОтбору(
    Отбор, 
    ЗначенияЗаполнения, 
    "НоменклатураКонтрагентовБЭД", 
    ЭтотОбъект
);

Данный метод берет на себя рутинную проверку наличия записи в базе данных и оптимизирует код взаимодействия. Тем не менее, общая архитектурная концепция «Сбор ненайденных позиций на сервере -> Возврат массива на клиент -> Асинхронный обход или единая форма сопоставления» остается неизменной и наиболее надежной для программирования любых интеграционных механизмов в 1С.

← На главную