Серверный код 1С и диалог с человеком: схемы, которые работают

· 9 мин

Содержание

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

Типичная ситуация: при проведении документа сервер обнаруживает, что остатка на складе не хватает на 3 единицы, и надо спросить «Провести с отрицательным остатком?». Программист пишет Вопрос() в модуле объекта и получает ошибку компиляции. Ту же задачу решают по-разному для форм, записи объектов, HTTP-сервисов и фоновых заданий.

Почему серверный контекст не умеет спрашивать

Сервер 1С:Предприятия обслуживает сразу много сеансов. Код модуля объекта, общего серверного модуля или процедуры &НаСервере выполняется в рабочем процессе кластера, а у рабочего процесса нет экрана. Даже в файловом варианте, где «сервер» и клиент работают в одном процессе, платформа соблюдает то же разделение контекстов.

Что из этого следует на практике:

  • 🚫 методы Вопрос, Предупреждение, ОткрытьФорму, ПоказатьВопрос на сервере не определены — при попытке их вызвать модуль не компилируется с текстом вида Процедура или функция с указанным именем не определена (Вопрос);
  • 🔒 серверный вызов нельзя «поставить на паузу» и дождаться ответа: вызов либо завершается и возвращает результат, либо падает с исключением;
  • ⏳ если сервер держит транзакцию, любые ожидания внутри неё удерживают блокировки данных — другие пользователи ждут.

Поэтому вопрос всегда задаёт клиент. Задача программиста — разрезать алгоритм на две части: «проверить» и «выполнить с учётом ответа».

Сообщения пользователю на сервере выводить можно. Сообщить() или объект СообщениеПользователю накапливают текст, а платформа показывает его на клиенте после возврата из серверного вызова. Это информирование, а не диалог: выбор пользователь сделать не может.

Схема «сервер вернул вопрос — клиент спросил — сервер повторил»

Это основной приём для форм. Серверная функция принимает параметр с ответом и возвращает структуру: выполнено ли действие, а если нет — какой вопрос задать.

Серверная часть

&НаСервере
Функция ПровестиНаСервере(РазрешитьМинус)

	Результат = Новый Структура;
	Результат.Вставить("Выполнено", Ложь);
	Результат.Вставить("НуженОтвет", Ложь);
	Результат.Вставить("ТекстВопроса", "");

	Нехватка = РассчитатьНехватку(); // ваша проверка остатков

	Если Нехватка > 0 И Не РазрешитьМинус Тогда
		Результат.НуженОтвет = Истина;
		Результат.ТекстВопроса = СтрШаблон(
			"Не хватает %1 ед. товара. Провести с отрицательным остатком?",
			Формат(Нехватка, "ЧГ=0"));
		Возврат Результат;
	КонецЕсли;

	ДокументОбъект = РеквизитФормыВЗначение("Объект");
	ДокументОбъект.ДополнительныеСвойства.Вставить("РазрешитьМинус", РазрешитьМинус);
	ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
	ЗначениеВРеквизитФормы(ДокументОбъект, "Объект");

	Результат.Выполнено = Истина;
	Возврат Результат;

КонецФункции

Флаг передаётся в модуль объекта через ДополнительныеСвойства. В обработке проведения проверяйте ДополнительныеСвойства.Свойство("РазрешитьМинус"), чтобы документ, записанный из другого места, вёл себя как раньше.

Клиентская часть на Асинх/Ждать

Начиная с платформы 8.3.18 удобнее всего писать через асинхронные процедуры:

&НаКлиенте
Асинх Процедура Провести(Команда)

	Результат = ПровестиНаСервере(Ложь);

	Если Результат.НуженОтвет Тогда
		Ответ = Ждать ВопросАсинх(Результат.ТекстВопроса, РежимДиалогаВопрос.ДаНет);
		Если Ответ = КодВозвратаДиалога.Да Тогда
			Результат = ПровестиНаСервере(Истина);
		КонецЕсли;
	КонецЕсли;

	Если Результат.Выполнено Тогда
		ПоказатьОповещениеПользователя("Документ проведён");
	КонецЕсли;

КонецПроцедуры

Клиентская часть через ОписаниеОповещения

Для конфигураций, которые должны работать на платформе ниже 8.3.18, — классическая схема с обработчиком:

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

&НаКлиенте
Процедура ПослеОтветаНаВопрос(Ответ, ДополнительныеПараметры) Экспорт
	Если Ответ = КодВозвратаДиалога.Да Тогда
		ПровестиНаСервере(Истина);
	КонецЕсли;
КонецПроцедуры

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

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

Прерывание через ВызватьИсключение

Если ответ пользователя не нужен, а нужно остановить операцию и объяснить причину, используйте оператор ВызватьИсключение. Он прерывает серверный код, откатывает транзакцию и передаёт текст на клиент.

// серверный общий модуль
Процедура ПроверитьЛимит(Сумма, Лимит) Экспорт
	Если Сумма > Лимит Тогда
		ВызватьИсключение СтрШаблон("Сумма %1 превышает лимит %2. Операция отменена.",
			Формат(Сумма, "ЧДЦ=2"), Формат(Лимит, "ЧДЦ=2"));
	КонецЕсли;
КонецПроцедуры

На клиенте исключение ловят конструкцией Попытка … Исключение:

&НаКлиенте
Процедура Выполнить(Команда)
	Попытка
		ВыполнитьНаСервере();
	Исключение
		Инфо = ИнформацияОбОшибке();
		ПоказатьПредупреждение(, Инфо.Описание);
	КонецПопытки;
КонецПроцедуры

Можно ли превратить исключение в вопрос? Технически да: сервер бросает исключение с условным префиксом в тексте, клиент его распознаёт и спрашивает. Но разбор текста ошибки хрупок — любая правка формулировки ломает логику. Возврат структуры из предыдущего раздела надёжнее.

СпособКогда подходитРиски
Возврат структуры с вопросомНужен выбор «да/нет», выбор вариантаНужно передать ответ обратно и повторить проверку
ВызватьИсключениеОперация недопустима, выбора нетОткат всей транзакции; на клиенте текст выглядит как ошибка
Отказ = Истина + сообщениеСобытия записи и проведенияПользователь видит только отказ, без выбора

Если исключение поймано внутри серверной транзакции и транзакция продолжена, при фиксации платформа выдаст В данной транзакции уже происходили ошибки. Ловите исключение снаружи транзакции или отменяйте её явно.

Вопрос перед записью и проведением объекта

Часто программист пытается спросить в ПередЗаписью или ОбработкаПроведения модуля объекта. Это серверные события, там диалог невозможен. Вопрос переносят в клиентское событие формы ПередЗаписью.

Приём «отказать, спросить, записать снова»:

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

&НаКлиенте
Процедура ПослеВопросаОЗаписи(Ответ, Доп) Экспорт
	Если Ответ = КодВозвратаДиалога.Да Тогда
		ВопросЗадан = Истина;
		Записать(Доп.ПараметрыЗаписи);
	КонецЕсли;
КонецПроцедуры

Если для проверки нужны данные с сервера (остатки, лимиты), вызовите внутри НуженВопрос() серверную функцию без контекста — &НаСервереБезКонтекста — и передайте ей только нужные значения. Так не гоняется вся форма.

Проверку, ради которой задаётся вопрос, дублируйте на сервере. Документ могут записать обработкой, обменом или из списка, где клиентский код формы не выполняется.

Диалог с внешней системой через HTTP-сервис

HTTP-сервис — тоже серверный код, и пользователь у него — внешняя программа: сайт, мобильное приложение, бот. Диалог строят так же, как с формой: сервис отвечает «нужно подтверждение», клиент повторяет запрос с параметром подтверждения.

Функция ЗаказPOST(Запрос)

	Тело = Запрос.ПолучитьТелоКакСтроку();
	Чтение = Новый ЧтениеJSON;
	Чтение.УстановитьСтроку(Тело);
	Данные = ПрочитатьJSON(Чтение);
	Чтение.Закрыть();

	Подтверждено = (Запрос.ПараметрыЗапроса.Получить("confirm") = "1");
	Ответ = Новый Структура;

	Если Данные.Свойство("sum") И Данные.sum > 100000 И Не Подтверждено Тогда
		Ответ.Вставить("status", "need_confirm");
		Ответ.Вставить("question", "Сумма заказа больше 100 000. Подтвердить?");
		Возврат ОтветJSON(409, Ответ);
	КонецЕсли;

	// создание заказа...
	Ответ.Вставить("status", "ok");
	Возврат ОтветJSON(200, Ответ);

КонецФункции

Функция ОтветJSON(Код, Структура)
	Запись = Новый ЗаписьJSON;
	Запись.УстановитьСтроку();
	ЗаписатьJSON(Запись, Структура);
	Ответ = Новый HTTPСервисОтвет(Код);
	Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
	Ответ.УстановитьТелоИзСтроки(Запись.Закрыть(), КодировкаТекста.UTF8);
	Возврат Ответ;
КонецФункции

Что продумать при таком протоколе:

  • 📌 код ответа. 409 (конфликт) или 200 с полем status — договоритесь с разработчиком клиента заранее и опишите в документации;
  • 🔁 повторный запрос. Между первым и вторым запросом данные могли измениться, поэтому сервис заново выполняет все проверки, а не верит флагу confirm вслепую;
  • 🧾 защита от дублей. Пусть клиент передаёт свой идентификатор операции. Сервис хранит его у созданного объекта и при повторе возвращает уже созданный заказ, а не второй.

Сервис не хранит состояние между вызовами — каждый запрос самостоятельный. Всё, что нужно для продолжения, клиент присылает заново.

Фоновые и регламентные задания: куда деть вопрос

Фоновое задание выполняется в отдельном сеансе без интерфейса, регламентное — вообще без пользователя. Спросить из них нельзя ни при каких настройках.

Фоновое задание, запущенное из формы

Схема — двухэтапная:

  1. Клиент запускает задание через ФоновыеЗадания.Выполнить(...) и передаёт адрес временного хранилища.
  2. Задание делает проверку. Если нужен ответ, оно кладёт в хранилище структуру с вопросом (ПоместитьВоВременноеХранилище) и завершается.
  3. Клиент через обработчик ожидания проверяет состояние задания (ФоновыеЗадания.НайтиПоУникальномуИдентификатору, свойство Состояние).
  4. Когда задание завершено, клиент читает хранилище (ПолучитьИзВременногоХранилища), задаёт вопрос и запускает второе задание — уже с ответом.

Промежуточные сообщения задания (Сообщить() внутри фонового) клиент забирает методом ПолучитьСообщенияПользователю(Истина) у объекта фонового задания — так показывают прогресс.

В конфигурациях на БСП не пишите запуск и опрос вручную: общий модуль ДлительныеОперации и клиентский ДлительныеОперацииКлиент уже умеют запускать задание, ждать его и возвращать результат в обработчик оповещения.

Держать фоновое задание в цикле и ждать, пока пользователь запишет ответ в регистр, — плохая идея. Задание занимает рабочий процесс, а если оно в транзакции — ещё и блокировки. Завершить и перезапустить дешевле.

Регламентное задание

Здесь вопрос превращается в отложенное уведомление:

  • 📝 запишите событие в журнал регистрации: ЗаписьЖурналаРегистрации("МойОбмен.НуженОтвет", УровеньЖурналаРегистрации.Предупреждение, , , ТекстВопроса);
  • 🗂 сохраните «вопрос» в служебный регистр сведений : что случилось, по какому объекту, какие варианты действия;
  • 🔔 покажите вопрос ответственному при входе в программу или на рабочем столе, а его выбор сохраните в тот же регистр;
  • ▶️ при следующем запуске регламентное задание прочитает ответ и продолжит.

Как вернуться к операции после ответа

Самая частая проблема двухшаговой схемы — второй вызов «забывает», что было в первом. Правила, которые её снимают:

  • Передавайте ответ параметром, а не глобальной переменной. Серверные переменные модуля между вызовами не живут.
  • Повторяйте проверку. Пока пользователь думал, остаток мог измениться. Если ситуация стала другой, сервер вернёт новый вопрос, а не проведёт документ по устаревшему согласию.
  • Не держите транзакцию между шагами. Первый вызов только проверяет и ничего не пишет. Запись — во втором вызове, целиком.
  • Несколько вопросов собирайте в один. Если проверок три, верните массив вопросов и покажите их одним окном или по очереди, но с одним повторным вызовом сервера в конце.
  • Большие данные — через временное хранилище. Если первый шаг подготовил таблицу на 10 000 строк, не пересчитывайте её: поместите во временное хранилище и передайте клиенту только адрес.
Диагностика: выберите симптом

Частые вопросы

Можно ли задать вопрос из модуля менеджера или общего модуля, если он вызван с клиента?
Нет. Контекст определяется местом выполнения кода, а не тем, кто его вызвал. Серверный общий модуль работает на сервере даже при вызове из формы.
Есть ли способ спросить пользователя из внешней обработки, запущенной на сервере?
Только тем же двухэтапным способом. Код модуля обработки серверный, диалог задаёт её форма на клиенте.
Как задать вопрос с вариантами, а не «Да/Нет»?
Передайте в ПоказатьВопрос или ВопросАсинх вместо режима РежимДиалогаВопрос список значений с кнопками. Сервер может вернуть этот список в структуре вместе с текстом вопроса.
Работает ли такая схема в веб-клиенте и тонком клиенте?
Да. Неблокирующие диалоги поддерживаются во всех клиентах платформы 8.3, поэтому ПоказатьВопрос и ВопросАсинх — безопасный выбор для любого клиента.
Что делать, если вопрос нужно задать при обмене данными?
Обмен идёт без пользователя. Запишите конфликт в регистр или журнал регистрации и разберите его вручную после обмена, затем повторите загрузку.