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