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