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