При проектировании бизнес-логики в 1С:Предприятии 8.3 часто возникает задача ограничения создания дублирующих документов. Например, когда по определенному контрагенту, договору или за конкретный расчетный период (месяц) в системе может существовать строго один документ определенного типа. Если пользователь пытается создать второй такой же документ, система должна вовремя предупредить его или заблокировать запись.
Решение этой задачи лежит на стыке двух дисциплин: проектирования удобного пользовательского интерфейса (UX) и обеспечения транзакционной целостности базы данных на уровне СУБД. В этой статье мы подробно разберем, где именно — в форме, модуле объекта или через вспомогательные механизмы — кошерно проверять наличие дубликатов документов, как избежать «состояния гонки» (когда два пользователя одновременно создают дубликат) и как сделать этот процесс максимально комфортным для пользователя.
---
Самое очевидное решение, которое приходит в голову начинающему разработчику, — написать проверку в событии ПередЗаписью модуля объекта. На первый взгляд это логично: если дубликат найден, мы устанавливаем параметр Отказ = Истина, выводим сообщение, и документ не записывается. Однако у такого подхода есть огромный минус с точки зрения удобства использования (UX).
Представьте ситуацию: бухгалтер открывает форму нового документа, тратит 15–20 минут на заполнение многострочной табличной части, распределяет аналитику, указывает сопутствующие реквизиты. Нажимает кнопку «Провести и закрыть», и в этот момент система выдает ему лаконичное сообщение: «Документ за этот месяц уже существует. Запись отменена». Вся проделанная работа пользователя отправляется в корзину. Это вызывает колоссальное раздражение и снижает лояльность к программному продукту.
С другой стороны, если делать проверку только на клиенте в управляемой форме, мы оставляем лазейку для программного создания документов. Загрузка из внешних источников, интеграционные API, групповая обработка данных — все эти механизмы создают документы в обход экранных форм, что неминуемо приведет к появлению дублей в базе данных.
Вывод: проверка должна быть двухконтурной. Первый контур — интерактивный (в управляемой форме для удобства пользователя), второй контур — транзакционный жесткий (в модуле объекта для гарантии целостности данных).
---
Для того чтобы пользователь не тратил время на заполнение заведомо дублирующего документа, проверку необходимо выполнять в момент ввода ключевых реквизитов (например, организация, контрагент, период/дата).
Рассмотрим пошагово, как реализовать интерактивную проверку, которая не будет перегружать сервер частыми запросами и обеспечит отзывчивость интерфейса.
Шаг 1. Использование обработчиков изменения реквизитов
Для всех ключевых реквизитов формы, совокупность которых определяет уникальность документа, необходимо назначить один и тот же обработчик события ПриИзменении. Например, при изменении даты или контрагента мы вызываем проверку:
&НаКлиенте
Процедура КлючевойРеквизитПриИзменении(Элемент)
// Подключаем обработчик ожидания, чтобы не спамить сервер запросами при быстром вводе
ПодключитьОбработчикОжидания("ВыполнитьПроверкуДубликатовНаСервере", 1, Истина);
КонецПроцедуры
Использование ПодключитьОбработчикОжидания с флагом Однократно = Истина позволяет сделать паузу в 1 секунду после того, как пользователь закончил интерактивный ввод. Если он быстро меняет несколько полей, серверный запрос выполнится только один раз, когда пользователь остановится. Это значительно снижает нагрузку на систему.
Шаг 2. Серверная проверка и визуализация
Теперь реализуем процедуру проверки на сервере. Вместо вывода раздражающих модальных окон или предупреждений, которые прерывают работу, мы будем выводить мягкое предупреждение на форму (например, через специальную декорацию или подсветку полей).
&НаСервере
Процедура ВыполнитьПроверкуДубликатовНаСервере()
// Проверяем заполненность ключевых реквизитов
Если Не ЗначениеЗаполнено(Объект.Контрагент) Или Не ЗначениеЗаполнено(Объект.Дата) Тогда
Возврат;
КонецЕсли;
НачалоПериода = НачалоМесяца(Объект.Дата);
КонецПериода = КонецМесяца(Объект.Дата);
// Выполняем легкий запрос к СУБД
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| РеализацияТоваровУслуг.Ссылка КАК Ссылка
|ИЗ
| Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
|ГДЕ
| РеализацияТоваровУслуг.Контрагент = &Контрагент
| И РеализацияТоваровУслуг.Дата МЕЖДУ &НачалоПериода И &КонецПериода
| И РеализацияТоваровУслуг.Ссылка <> &ТекущаяСсылка
| И НЕ РеализацияТоваровУслуг.ПометкаУдаления";
Запрос.УстановитьПараметр("Контрагент", Объект.Контрагент);
Запрос.УстановитьПараметр("НачалоПериода", НачалоПериода);
Запрос.УстановитьПараметр("КонецПериода", КонецПериода);
Запрос.УстановитьПараметр("ТекущаяСсылка", Объект.Ссылка);
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
// Отображаем предупреждающую надпись на форме
Элементы.НадписьПредупреждениеОДубликате.Заголовок = "Внимание! За этот месяц уже зарегистрирован документ: " + Строка(Выборка.Ссылка);
Элементы.НадписьПредупреждениеОДубликате.Видимость = Истина;
// Дополнительно можно заблокировать кнопку записи, чтобы предотвратить случайное проведение
Элементы.ФормаЗаписатьИЗакрыть.Доступность = Ложь;
Элементы.ФормаПровести.Доступность = Ложь;
Иначе
Элементы.НадписьПредупреждениеОДубликате.Видимость = Ложь;
Элементы.ФормаЗаписатьИЗакрыть.Доступность = Истина;
Элементы.ФормаПровести.Доступность = Истина;
КонецЕсли;
КонецПроцедуры
Такой подход обеспечивает великолепный UX. Пользователь сразу видит, что аналогичный документ уже создан, может кликнуть по ссылке на найденный документ (если вывести его как гиперссылку) и перейти к его редактированию, не выполняя лишней работы.
---
Интерактивный уровень защищает нас от ошибок ручного ввода в форме, но не дает 100% гарантии. Если документ создается программно (например, через обработку заполнения, копирование или внешнее API), код формы не выполняется. Нам необходим жесткий контроль непосредственно при записи объекта базы данных.
Рассмотрим, где именно в модуле объекта размещать логику проверки.
Событие ПередЗаписью в модуле объекта
В модуле объекта самым правильным местом является обработчик ПередЗаписью. Нам нужно убедиться, что проверка срабатывает только тогда, когда это необходимо (например, документ записывается не в режиме обмена данными).
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Если запись происходит в рамках обмена данными, бизнес-логику обычно отключают
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Проверку имеет смысл делать только для новых документов или если изменились ключевые реквизиты
Если ЭтоНовый() Или ЗначениеРеквизитаИзменено("Контрагент") Или ЗначениеРеквизитаИзменено("Дата") Тогда
Дубликат = НайтиДубликатДокумента();
Если ЗначениеЗаполнено(Дубликат) Тогда
Отказ = Истина;
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = Шаблон("Не удалось записать документ. В текущем месяце уже существует аналогичный документ %1!", Дубликат);
Сообщение.Сообщить();
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Вспомогательная функция НайтиДубликатДокумента() выполняет обычный запрос к таблице документов, аналогичный тому, который мы рассматривали для управляемой формы.
---
Представим критическую ситуацию: в базе работают два оператора. Они одновременно открывают форму создания нового документа для одного и того же контрагента за один и тот же месяц. Оба заполняют данные и практически одновременно нажимают кнопку «Записать».
Если мы используем простую проверку через Запрос в событии ПередЗаписью, то произойдет следующее:
Мы получили явный дубликат в базе данных, несмотря на наличие всех проверок! Чтобы решить эту проблему, нам необходимо использовать механизм управляемых блокировок или вспомогательный регистр сведений.
Перед тем как выполнить запрос на проверку существования дубликата в процедуре ПередЗаписью, мы должны заблокировать комбинацию наших аналитик. Если второй пользователь попытается записать документ по тем же аналитикам, его транзакция приостановится и будет ожидать завершения первой транзакции.
Функция НайтиДубликатДокументаСБлокировкой()
// Создаем объект блокировки данных
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Документ.РеализацияТоваровУслуг");
ЭлементБлокировки.УстановитьПараметр("Контрагент", Контрагент);
// Важно: Блокировка накладывается в рамках транзакции записи
Блокировка.Заблокировать();
// Теперь выполняем запрос
Запрос = Новый Запрос;
// ... здесь текст запроса на поиск существующего документа за месяц ...
Возврат РезультатПоиска;
КонецФункции
Обратите внимание, что управляемая блокировка по реквизитам документа (не являющимся измерениями регистра) поддерживается СУБД через трансляцию в блокировку индекса, если по этим полям в метаданных конфигурации построен индекс. Поэтому для реквизита Контрагент обязательно должно быть включено индексирование!
Если требования к уникальности абсолютно жесткие, а индексировать реквизиты документа или накладывать сложные блокировки не хочется, существует классический архитектурный паттерн — использование непериодического независимого (или подчиненного) регистра сведений в качестве уникального индекса СУБД.
Создадим регистр сведений УникальныеДокументыПериода со следующей структурой:
ПериодРегистрации (Тип: Дата, например, начало месяца).Контрагент (Тип: СправочникСсылка.Контрагенты).Документ (Тип: ДокументСсылка.РеализацияТоваровУслуг).При записи документа мы программно совершаем попытку записи набора записей в этот регистр. Если в рамках СУБД запись с такими измерениями (Месяц + Контрагент) уже существует, платформа автоматически вызовет исключение нарушения уникальности на уровне базы данных, предотвращая появление дубликата в любой ситуации.
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// При записи формируем движения по вспомогательному регистру
Движения.УникальныеДокументыПериода.Записывать = Истина;
НоваяСтрока = Движения.УникальныеДокументыПериода.Добавить();
НоваяСтрока.ПериодРегистрации = НачалоМесяца(Дата);
НоваяСтрока.Контрагент = Контрагент;
НоваяСтрока.Документ = Ссылка;
КонецПроцедуры
Если система попытается записать второй документ с теми же параметрами ПериодРегистрации и Контрагент, платформа выдаст ошибку записи уникального ключа, транзакция откатится, и целостность базы данных будет надежно сохранена.
---
Необходимо отдельно упомянуть два частых сценария создания новых документов, где стандартная проверка в форме может не сработать ожидаемым образом.
1. Копирование существующего документа
Когда пользователь копирует документ, событие ПриКопировании отрабатывает на сервере до открытия формы. Копируемый объект уже содержит все реквизиты оригинала, что заведомо делает его дубликатом. В этом обработчике в модуле объекта хорошей практикой является очистка уникальных реквизитов или вывод предупреждения.
2. Ввод на основании другого документа
В сценарии создания «на основании» (событие ОбработкаЗаполнения в модуле объекта) мы можем определить дубликат еще до того, как форма вообще инициализируется. Если документ-основание пытается породить дублирующий документ за текущий месяц, мы можем сразу выбросить исключение:
Процедура ОбработкаЗаполнения(ДанныеЗаполнения, ТекстЗаполнения, СтандартнаяОбработка)
// Если на основании этих данных документ уже существует в текущем месяце
Если НайденаКопияЗаМесяц(ДанныеЗаполнения) Тогда
ВызватьИсключение "Документ этого типа для данного контрагента за текущий месяц уже создан!";
КонецЕсли;
КонецПроцедуры
Этот подход экономит ресурсы сервера и время пользователя, пресекая создание дубликата на самом раннем этапе жизненного цикла объекта.
---
Давайте сведем все рекомендации в единую и понятную систему:
ПриИзменении ключевых реквизитов в связке с ПодключитьОбработчикОжидания. Выводите мягкое предупреждение на форму через элементы декорации и динамически блокируйте кнопки записи. Это убережет нервы пользователя.ПередЗаписью всегда делайте контрольный запрос к СУБД. Это защитит базу данных от некорректного программного создания объектов.БлокировкаДанных перед выполнением проверочных запросов или пишите контрольные уникальные наборы во вспомогательные регистры сведений.Реализовав проверку дубликатов по этой двухконтурной схеме, вы получите стабильное, быстрое и дружелюбное к пользователю приложение 1С.