В разработке на платформе «1С:Предприятие 8» (особенно в управляемом приложении) часто возникает классическая задача: запретить повторный ввод документа на основании одного и того же источника. Например, для одного документа-основания разрешено создавать только один подчиненный документ определенного типа. Если пользователь пытается ввести второй подчиненный документ, система должна не просто выдать ошибку, а сразу открыть форму уже существующего документа. Это значительно повышает удобство работы (UX) и защищает базу данных от дублирования информации.
В этой статье мы подробно разберем, почему стандартный механизм платформы не позволяет сделать это в один клик из модуля объекта, и рассмотрим четыре проверенных на практике способа решения этой задачи.
Когда пользователь нажимает кнопку «Создать на основании», платформа 1С выполняет следующие шаги:
ОбработкаЗаполнения в модуле этого объекта.Попытка решить задачу «в лоб» через процедуру ОбработкаЗаполнения модуля объекта сталкивается с серьезной проблемой. На уровне модуля объекта мы находимся на сервере и не имеем доступа к клиентскому контексту. Мы не можем вызвать метод открытия формы другого документа. Если мы вызовем исключение (ВызватьИсключение) внутри процедуры ОбработкаЗаполнения, то процесс создания прервется с системной ошибкой, но пользователь увидит пугающее окно с сообщением об ошибке, что крайне нежелательно с точки зрения хорошего интерфейса.
Поэтому правильное решение задачи должно лежать на стыке клиентской логики (для удобства пользователя) и серверной защиты данных.
Это самый популярный и визуально «бесшовный» для пользователя вариант. Мы позволяем платформе создать новый объект в памяти и дойти до момента открытия его формы. Однако перед выводом формы на экран мы проверяем, нет ли уже в базе данных аналогичного документа с таким же основанием.
Если дубликат найден, мы устанавливаем параметр Отказ = Истина в процедуре ПриОткрытии (это мгновенно и безболезненно для пользователя отменяет вывод формы создаваемого дубля на экран) и сразу же программно открываем форму существующего документа.
Рассмотрим по шагам реализацию этого механизма в модуле формы нового документа-приемника:
&НаКлиенте
Процедура ПриОткрытии(Отказ)
// Проверяем, что открывается именно новый (еще не записанный в БД) документ
Если Объект.Ссылка.Пустая() Тогда
// Выполняем поиск существующего документа на сервере
НайденныйДокумент = НайтиСуществующийДокументНаСервере(Объект.ДокументОснование);
Если ЗначениеЗаполнено(НайденныйДокумент) Тогда
// Отменяем открытие текущей формы (создание дубля)
Отказ = Истина;
// Открываем форму уже существующего документа
ПараметрыФормы = Новый Структура("Ключ", НайденныйДокумент);
ОткрытьФорму("Документ.ИмяВашегоДокумента.ФормаОбъекта", ПараметрыФормы);
// Выводим пользователю информационное сообщение
ПоказатьПредупреждение(, "Документ на основании этого источника уже был создан ранее. Открыта существующая форма.");
КонецЕсли;
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Функция НайтиСуществующийДокументНаСервере(ДокументОснование)
Если Не ЗначениеЗаполнено(ДокументОснование) Тогда
Возврат Неопределено;
КонецЕсли;
// Ищем в базе данных уже существующий документ с таким же основанием
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗРЕШЕННЫЕ ПЕРВЫЕ 1
| Ссылка КАК Ссылка
|ИЗ
| Документ.ИмяВашегоДокумента КАК ДокументПриемник
|ГДЕ
| ДокументПриемник.ДокументОснование = &ДокументОснование
| И НЕ ДокументПриемник.ПометкаУдаления";
Запрос.УстановитьПараметр("ДокументОснование", ДокументОснование);
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
Возврат Выборка.Ссылка;
КонецЕсли;
Возврат Неопределено;
КонецФункции
При таком подходе пользователь нажимает кнопку ввода на основании, перед ним на долю секунды ничего не мерцает, форма нового документа закрывается до показа, и мгновенно открывается форма уже существующего документа.
Предыдущее решение имеет один небольшой минус с точки зрения производительности: сервер выполняет холостую работу по инициализации нового объекта в памяти, отрабатывает процедура ОбработкаЗаполнения, и только на этапе открытия формы процесс прерывается.
Если мы хотим сэкономить ресурсы сервера, мы можем перенести проверку на уровень документа-источника:
ОткрытьФорму с параметром Ключ.Рассмотрим пример модуля такой команды:
&НаКлиенте
Процедура ОбработкаКоманды(ПараметрКоманды, ПараметрыВыполненияКоманды)
// ПараметрКоманды — это ссылка на текущий документ-источник
НайденныйДокумент = НайтиСуществующийДокументНаСервере(ПараметрКоманды);
Если ЗначениеЗаполнено(НайденныйДокумент) Тогда
// Если подчиненный документ уже есть — просто открываем его
ПараметрыФормы = Новый Структура("Ключ", НайденныйДокумент);
ОткрытьФорму("Документ.ИмяВашегоДокумента.ФормаОбъекта", ПараметрыФормы, ПараметрыВыполненияКоманды.Источник, ПараметрыВыполненияКоманды.Уникальность, ПараметрыВыполненияКоманды.Окно, ПараметрыВыполненияКоманды.НавигационнаяСсылка);
Иначе
// Если документа нет — открываем форму создания нового и передаем основание
ПараметрыЗаполнения = Новый Структура("Основание", ПараметрКоманды);
ПараметрыФормы = Новый Структура("ЗначенияЗаполнения", ПараметрыЗаполнения);
ОткрытьФорму("Документ.ИмяВашегоДокумента.ФормаОбъекта", ПараметрыФормы, ПараметрыВыполненияКоманды.Источник, ПараметрыВыполненияКоманды.Уникальность, ПараметрыВыполненияКоманды.Окно, ПараметрыВыполненияКоманды.НавигационнаяСсылка);
КонецЕсли;
КонецПроцедуры
Если ваша конфигурация разработана на основе БСП (как большинство типовых конфигураций 1С: БП 3.0, УТ 11, ERP, ЗУП), то для ввода на основании обычно применяется механизм подсистемы «Подключаемые команды».
В этом случае разработчику не обязательно создавать команду вручную. Достаточно переопределить поведение команды в модуле менеджера документа-приемника в процедуре ПриОпределенииКомандПодключения. БСП позволяет задать для подключаемой команды собственный клиентский обработчик, в котором прописывается аналогичная логика предварительной проверки существования документа.
Важно понимать, что проверка в форме (Решение 1) отлично работает для интерактивных действий пользователя. Однако формы не создаются и не открываются, если другой программист попытается создать ваш документ программно (например, через регламентное задание, интеграцию, загрузку из XML/JSON или Web-сервис).
Например, следующий программный код обойдет все проверки в форме:
НовыйДокумент = Документы.ИмяВашегоДокумента.СоздатьДокумент();
НовыйДокумент.Заполнить(ДокументОснование);
НовыйДокумент.Записать(); // Здесь форма не открывается, и дубль запишется!
Чтобы гарантировать целостность данных на уровне СУБД, необходимо дублировать проверку в модуле объекта в обработчике события ПередЗаписью. При этом важно отключать проверку в режиме загрузки данных, чтобы не нарушать обмен данными.
Реализуем жесткую блокировку на уровне модуля объекта документа-приемника:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Если идет загрузка данных (например, обмен), проверки не выполняем
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Проверку выполняем только для новых (еще не записанных) объектов
Если ЭтоНовый() И ЗначениеЗаполнено(ДокументОснование) Тогда
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗРЕШЕННЫЕ ПЕРВЫЕ 1
| Ссылка
|ИЗ
| Документ.ИмяВашегоДокумента КАК ДокументПриемник
|ГДЕ
| ДокументПриемник.ДокументОснование = &ДокументОснование
| И НЕ ДокументПриемник.ПометкаУдаления";
Запрос.УстановитьПараметр("ДокументОснование", ДокументОснование);
Если Не Запрос.Выполнить().Пустой() Тогда
// Блокируем запись транзакции и вызываем системную ошибку
ВызватьИсключение "Ошибка: Документ на основании этого источника уже существует в базе данных!";
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Для достижения максимальной надежности и идеального интерфейса рекомендуется комбинировать подходы:
ПриОткрытии с перенаправлением на существующий документ). Это обеспечит бесшовную и красивую работу в интерфейсе.ПередЗаписью). Это защитит вашу базу данных от появления дублей при программном создании документов другими разработчиками или внешними системами интеграции.