Как реализовать клиентский выбор хозяйственной операции перед стандартным вводом на основании документа?

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

При разработке в 1С часто возникает задача, когда нам необходимо предоставить пользователю выбор какого-либо параметра (например, вида хозяйственной операции) для нового документа, который создается на основании существующего. Особенность типового механизма "Ввод на основании" заключается в том, что основная логика заполнения нового документа, реализованная в процедуре ОбработкаЗаполнения, выполняется на сервере. Это означает, что прямое взаимодействие с пользователем (вывод диалога выбора) на клиенте до начала серверной обработки заполнения стандартными средствами невозможно. Рассмотрим подробнее эту проблему и различные подходы к ее решению.

Проблема взаимодействия клиента и сервера при вводе на основании

Чтобы понять сложности, с которыми мы сталкиваемся, необходимо четко представлять порядок выполнения клиентских и серверных процедур при создании документа на основании:

  1. Вызов "Ввода на основании": Пользователь нажимает кнопку "Ввести на основании" или выбирает соответствующую команду из меню.

  2. Создание объекта нового документа: Система создает новый объект документа.

  3. ОбработкаЗаполнения (на сервере): Вызывается процедура ОбработкаЗаполнения в модуле объекта нового документа. В нее передается ссылка на документ-основание. Эта процедура полностью выполняется на сервере и заполняет большинство данных нового документа. На этом этапе мы не можем вывести пользователю никаких диалогов.

  4. ПриСозданииНаСервере формы (на сервере): После заполнения объекта, но до его отображения на клиенте, вызывается серверная процедура ПриСозданииНаСервере формы нового документа. Здесь мы можем получить доступ к уже заполненному объекту и выполнить дополнительную серверную логику.

  5. ПриОткрытии формы (на клиенте): Только после выполнения всех серверных этапов происходит открытие формы документа на клиенте и вызывается клиентская процедура ПриОткрытии. Это первая точка, где мы можем взаимодействовать с пользователем через диалоги.

Как видим, наш желаемый клиентский выбор должен произойти до шага 3, что стандартными средствами не предусмотрено. Из-за этого прямолинейное решение оказывается сложным, и нам приходится искать обходные пути.

Решение 1: Анализ документа-основания для автоматического определения операции (предпочтительный подход)

Самое элегантное и часто наилучшее решение, как показал опыт автора темы, – это максимально автоматизировать выбор хозяйственной операции. Вместо того чтобы спрашивать пользователя, мы можем проанализировать данные документа-основания и на их основе сделать вывод о том, какой вид операции должен быть у создаваемого документа. Этот подход минимизирует количество кликов пользователя и исключает потенциальные ошибки выбора.

Принцип работы

Мы реализуем логику определения вида операции непосредственно в процедуре ОбработкаЗаполнения нового документа или в отдельной серверной функции, вызываемой из нее. В этой логике мы проверяем различные реквизиты, табличные части или аналитические признаки документа-основания. Например:

  1. Если в документе Списание товаров указан конкретный склад, предназначенный для "Возвратов поставщику", то в ПКО автоматически устанавливаем операцию "Возврат поставщику".

  2. Если в Списании товаров есть ссылка на документ Поступление товаров (для случаев возврата поставщику), мы можем использовать эту связь.

  3. Если в табличной части Списания товаров присутствуют определенные номенклатурные позиции или группы, которые всегда подразумевают определенный тип операции в ПКО.

Реализация в коде

Представим, что у нас есть документ ПриходныйКассовыйОрдер (ПКО) и мы хотим заполнить его на основании СписанияТоваров. Нам нужно добавить логику в процедуру ОбработкаЗаполнения модуля объекта ПриходныйКассовыйОрдер:


// Модуль объекта документа ПриходныйКассовыйОрдер
Процедура ОбработкаЗаполнения(Основание)

    // Стандартная обработка заполнения (например, заполнение суммы, контрагента и т.д.)
    // ...

    Если ТипЗнч(Основание) = Тип("ДокументСсылка.СписаниеТоваров") Тогда
        // Получаем объект документа-основания для более детального анализа
        ОбъектОснование = Основание.ПолучитьОбъект();

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

        // Также можем анализировать табличную часть
        // Например, если в списании есть номенклатура, которая является "Услугой",
        // то ПКО может быть "ОплатаУслуг"
        Для Каждого СтрокаТЧ Из ОбъектОснование.Товары Цикл
            Если СтрокаТЧ.Номенклатура.ВидНоменклатуры = Перечисления.ВидыНоменклатуры.Услуга Тогда
                // Допустим, мы нашли услугу, и это влияет на вид операции ПКО
                ЭтотОбъект.ВидОперации = Перечисления.ВидыОперацийПКО.ОплатаУслугПокупателем;
                Прервать;
            КонецЕсли;
        КонецЦикла;

        // Важно: всегда заполняем реквизит Основание для корректной работы связанных документов
        ЭтотОбъект.ДокументОснование = Основание; // Предполагаем, что есть такой реквизит
    КонецЕсли;

    // ... остальная логика заполнения
КонецПроцедуры

Преимущества и недостатки

  1. Преимущества:

    • Максимальная автоматизация, минимум действий пользователя.
    • Высокая надежность, если логика определения четко сформулирована.
    • Сохраняется стандартный механизм "Ввода на основании" без дополнительных кнопок.
    • Отлично подходит для сценариев, где вид операции однозначно определяется данными основания.
  2. Недостатки:

    • Требует наличия четких правил для определения вида операции.
    • Не подходит, если выбор операции не может быть однозначно определен программно и всегда требует участия пользователя.

Решение 2: Использование собственной команды "Ввод на основании" с клиентским выбором

Если автоматическое определение вида операции невозможно, и нам нужен явный клиентский выбор, мы можем создать собственную команду "Ввод на основании". Этот подход позволяет нам полностью контролировать процесс до создания документа и выводить диалог выбора на клиенте.

Принцип работы

Мы добавляем на форму документа-основания (например, Списание товаров) новую команду. При нажатии на эту команду, в клиентском контексте, мы сначала выводим список для выбора хозяйственной операции. После того как пользователь сделает выбор, мы программно создаем новый документ (ПКО), устанавливаем в нем выбранную операцию и вызываем стандартный метод Заполнить(), передавая основание.

Реализация по шагам

  1. Создание собственной команды на форме документа-основания

    Открываем форму документа СписаниеТоваров (или другого документа, с которого планируем вводить на основании). В редакторе формы создаем новую команду (например, "Ввести ПКО с выбором") и размещаем ее в подходящем месте, например, в группе "Команды формы" или как отдельную кнопку. Устанавливаем обработчик команды на клиенте.

    
    // В модуле формы документа СписаниеТоваров
    &НаКлиенте
    Процедура ВвестиПКОСВыбором(Команда)
        // Здесь будет код вызова выбора операции
    КонецПроцедуры
    
  2. Реализация клиентского выбора хозяйственной операции

    В обработчике нашей новой команды мы используем объект СписокЗначений и его метод ПоказатьВыборЭлемента(). Этот метод позволяет вывести список значений для выбора и обработать результат асинхронно с помощью ОписаниеОповещения.

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

    После выбора операции, нам необходимо создать сам документ ПриходныйКассовыйОрдер. Если мы открываем форму (как в примере выше с ОткрытьФорму), то параметры передаются в форму, и они станут доступны в ПриСозданииНаСервере и, возможно, в ОбработкеЗаполнения, если мы специально их туда передадим или прочитаем из формы. Если же мы хотим полностью программно создать и записать документ без открытия формы, нам потребуется серверная процедура:

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

    Если мы открываем форму, то в модуле объекта ПриходныйКассовыйОрдер в процедуре ОбработкаЗаполнения или в модуле формы в ПриСозданииНаСервере нам нужно будет получить переданные параметры:

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

Преимущества и недостатки

  1. Преимущества:

    • Полный контроль над процессом ввода на основании.
    • Явный клиентский выбор операции до создания и заполнения документа.
    • Гибкость в настройке параметров нового документа.
    • Сохраняется связь "Основание" для связанных документов.
  2. Недостатки:

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

Решение 3: Перехват в ПриОткрытии формы нового документа с последующим перезапуском

Это более сложный и менее рекомендуемый подход, но он позволяет сохранить стандартный вызов "Ввода на основании". Идея заключается в том, чтобы отменить первое открытие формы нового документа, запросить у пользователя нужный параметр на клиенте, а затем повторно открыть эту же форму, но уже с выбранным параметром.

Принцип работы

При первом открытии формы нового документа (которая была вызвана стандартным "Вводом на основании"), мы в процедуре ПриОткрытии проверяем, что документ создан на основании, но вид операции еще не определен. В этом случае мы отменяем открытие формы (устанавливаем Отказ = Истина) и используем СписокЗначений.ПоказатьВыборЭлемента для запроса операции. После выбора, в процедуре-оповещении, мы снова вызываем ОткрытьФорму для того же документа, но уже передавая выбранный вид операции как параметр. В этом случае, при повторном открытии формы, вид операции будет доступен.

Реализация по шагам

  1. Модификация процедуры ПриОткрытии формы ПКО

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

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

    После того как пользователь сделает выбор, будет вызвана эта процедура. В ней мы формируем новые параметры для формы (включая выбранную операцию) и заново открываем форму.

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

    При повторном открытии формы, переданные нами параметры (ВидОперации и Основание) будут доступны в процедуре ПриСозданииНаСервере. Здесь мы должны их сохранить в реквизиты формы, чтобы они были доступны в клиентском контексте (для ПриОткрытии) и могли быть использованы для заполнения объекта документа.

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

    Критически важный нюанс этого подхода заключается в том, что процедура ОбработкаЗаполнения будет вызвана как минимум дважды:

    • Первый раз: При самом первом вызове "Ввода на основании". На этом этапе у нас еще нет выбранного пользователем ВидаОперации.
    • Второй раз: При повторном открытии формы после выбора операции. На этом этапе ВидОперации уже будет передан и может быть доступен в ПриСозданииНаСервере и/или объекте документа.

    Это означает, что логика в ОбработкеЗаполнения должна быть либо идемпотентной (выполнение несколько раз не вызывает проблем), либо должна уметь учитывать, что ВидОперации может быть установлен, а может и нет. Если ОбработкаЗаполнения сильно зависит от ВидаОперации, возможно, придется вынести часть логики в ПриСозданииНаСервере, чтобы она выполнялась уже после установки ВидаОперации при повторном открытии.

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

Преимущества и недостатки

  1. Преимущества:

    • Сохраняет стандартный механизм вызова "Ввода на основании".
    • Позволяет получить клиентский выбор.
  2. Недостатки:

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

Важные аспекты и нюансы

Выбор оптимального решения зависит от специфики вашей задачи и требований бизнеса. Начните с анализа возможности автоматического определения операции. Если это не подходит, рассмотрите вариант с собственной командой. Перехват в ПриОткрытии является самым сложным и должен применяться с осторожностью.

← На главную