Как программно запретить отмену проведения документа при наличии связанных документов в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

В процессе разработки и сопровождения конфигураций на платформе 1С:Предприятие 8 часто возникает задача контроля ссылочной целостности и бизнес-логики. Типичный пример: необходимо запретить пользователю распроводить (отменять проведение) документ-основание, если на его базе уже созданы и проведены подчиненные документы (например, документ «Монтаж», «Расходный кассовый ордер» или «Реализация товаров и услуг»).

В этой статье мы подробно разберем, как правильно реализовать такой запрет, в каком обработчике это делать, а также проанализируем критические ошибки, которые часто совершают начинающие разработчики.

В каком событии делать проверку?

Для решения нашей задачи на платформе существует два основных обработчика в модуле объекта документа:

  1. ОбработкаУдаленияПроведения
  2. ПередЗаписью

Разберем, почему опытные разработчики рекомендуют использовать именно ПередЗаписью. Обработчик ПередЗаписью вызывается при любом изменении объекта базы данных, в том числе при проведении и отмене проведения. В этот обработчик передается параметр РежимЗаписи, который принимает значение системного перечисления РежимЗаписиДокумента.ОтменаПроведения.

Использование ПередЗаписью предпочтительнее по следующим причинам:


Разбор критических ошибок из практики разработчиков

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

1. Ошибка бесконечной рекурсии (вызов метода Записать внутри ПередЗаписью)

Попытка вызвать метод Записать() внутри процедуры ПередЗаписью — это грубейшая ошибка:


// ТАК ДЕЛАТЬ НЕЛЬЗЯ!
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    ...
    Записать(РежимЗаписиДокумента.ОтменаПроведения); // Вызовет бесконечный цикл и падение системы
    Отказ = Истина;
КонецПроцедуры

Метод Записать() снова инициирует событие ПередЗаписью, которое опять вызывает Записать(). Платформа уходит в бесконечную рекурсию и аварийно завершает работу по переполнению стека. Для отмены операции достаточно просто установить параметр Отказ = Истина.

2. Использование клиентских методов на сервере

Вызов интерактивных функций, таких как Предупреждение() или Вопрос(), в модуле объекта (который компилируется и выполняется на сервере) приведет к ошибке выполнения:


// ТАК ДЕЛАТЬ НЕЛЬЗЯ!
Предупреждение("Есть Монтаж!"); // На сервере нет окон и интерфейса!

Для информирования пользователя на сервере необходимо использовать объект СообщениеПользователю.

3. Игнорирование режима загрузки данных

Если не проверять свойство ОбменДанными.Загрузка, то при любом обмене данными (например, при выгрузке/загрузке через РИБ) ваши правила проверки заблокируют перенос документов, и обмен завершится ошибкой.


Оптимальные варианты реализации запрета

Рассмотрим два наиболее эффективных и правильных способа решения нашей задачи.

Способ 1. Использование точечного SQL-запроса (Рекомендуемый)

Этот способ является самым производительным, поскольку мы проверяем наличие подчиненных документов точечным запросом к СУБД, избегая при этом получения тяжелых объектов в память. Мы проверяем не просто факт наличия связи, а именно то, что подчиненный документ «Монтаж» проведен.

Разберем код для модуля объекта документа:


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

Способ 2. Использование критериев отбора

Если в вашей конфигурации связь между документами реализована не через простой реквизит «ДокументОснование», а имеет сложную структуру, можно использовать системный механизм Критерии отбора (например, стандартный критерий СвязанныеДокументы).

Посмотрим, как правильно написать этот код:


Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    
    Если РежимЗаписи <> РежимЗаписиДокумента.ОтменаПроведения Тогда
        Возврат;
    КонецЕсли;
    
    Если ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    // Получаем массив всех связанных документов, передавая Ссылку текущего объекта
    МассивСвязанных = КритерииОтбора.СвязанныеДокументы.Найти(Ссылка);
    
    Для Каждого ТекущаяСсылка Из МассивСвязанных Цикл
        
        // Нам важен только конкретный тип подчиненного документа
        Если ТипЗнч(ТекущаяСсылка) = Тип("ДокументСсылка.Монтаж") Тогда
            
            // Блокируем отмену проведения только если подчиненный документ ПРОВЕДЕН
            Если ТекущаяСсылка.Проведен Тогда
                
                Отказ = Истина;
                
                Сообщение = Новый СообщениеПользователю;
                Сообщение.Текст = ШаблонСтроки(
                    НСтр("ru = 'Снятие с проведения запрещено! Найден проведенный документ монтажа: %1'"), 
                    Строка(ТекущаяСсылка)
                );
                Сообщение.Сообщить();
                
                Прервать; // Достаточно найти один документ для блокировки
                
            КонецЕсли;
            
        КонецЕсли;
        
    КонецЦикла;
    
КонецПроцедуры

Рекомендация по архитектуре: Подписки на события

Размещение этого кода непосредственно в модуле каждого конкретного документа засоряет конфигурацию и затрудняет последующие обновления (особенно если конфигурация находится на поддержке поставщика).

Для более изящного решения этой задачи рекомендуется использовать Подписку на события:

  1. Создайте новую подписку на событие (например, КонтрольОтменыПроведенияПриНаличииМонтажа).
  2. В качестве источника выберите типы документов, для которых необходим этот контроль (или укажите общий класс ДокументОбъект).
  3. Событие выберите: ПередЗаписью.
  4. Перенесите вышеописанный код в обработчик подписки (в общем серверном модуле), заменив обращение к свойствам Ссылка и ОбменДанными на работу с параметром Источник (например, Источник.Ссылка, Источник.ОбменДанными.Загрузка).

Таким образом, мы получим чистое, легко масштабируемое решение, которое не мешает типовому обновлению программы и надежно защищает бизнес-данные от некорректных действий пользователей.

← На главную