Столкнулся в практике со случаем, когда при записи одного документа в некоторых ситуациях автоматически формируется другой, и этот другой в 95% случаев записывается и проводится штатным способом, а в остальных - проведение приводит к ошибке. Но 95% нельзя игнорировать и глупо отказываться от получения готового документа там, где только 5% неуспешны, ведь это явная экономия времени и небесконечной энергии сотрудников! Проблема в том, что если второстепенный документ записан, но не проведен, то в основном документе выскакивает ошибка "В данной транзакции уже происходили ошибки". Некоторые подробности:
1. При этом процесс записи основного документа не выполняется, так как транзакция полностью отменена
2. успешно записанный второстепенный документ также теряется вместе с неявной транзакцией записи
3. ошибка может указывать на определенную строчку в коде либо быть общей с предложением завершить работу (в примере есть переключатель для выведения ошибки - что характерно, если обращений к базе при записи больше не было, то будет общая ошибка)
4. ситуация также осложнялась тем, что нарушался принцип идемпотентности, поскольку повторная запись уже не приводила к записи второстепенного документа - такое бывает.
Что я испробовал, прежде чем сдаваться перед этой ошибкой:
1. Выделить процедуру простой записи второстепенного документа в отдельную транзакцию - она тоже отменялась
2. При ошибке в проведении пытаться еще раз записать второстепенный документ, но в отдельной транзакции - получил ту же ошибку "В данной транзакции..."
3. Выделять проведение в отдельную транзакцию со своей попыткой - основная транзакция также остается сбойной!
Все дело в том, что по факту уровень вложенности транзакций (где-то видел фразу о том, что 1С вообще не поддерживает вложенных транзакций) не влияет на момент отмены транзакции из-за ошибок, как никакого влияния не оказывает и сам факт того, что ошибка была поймана блоком Попытка... Исключение. Наоборот, в этом блоке и стоит искать причину такой ошибки, ведь реально ошибку мы не видим, но транзакция портится!
Так что же делать? Отказываться от проведения второстепенного документа? Но экономия времени и сил на 95% документов... Нет. Тут и встали на помощь фоновые задания, ведь известно, что они выполняются в отдельном сеансе, а значит, и текущая транзакция на них не распространяется! Осталось только решить несколько вопросов:
1. как передать в функцию текущее состояние еще не записанного объекта
2. как получить результаты выполнения второстепенной задачи
3. как вообще вызвать функцию, которую не хочется помещать дальше модуля менеджера объекта
4. как не потерять полезных сообщений при работе с функцией
Не буду ходить вокруг да около, сразу стало понятно, что все эти проблемы уже решены в моем модуле многопоточного выполнения функций. И хотя изначально он не предназначен для обработки одного объекта, но никто ведь не запретит (и даже не узнает))).
Для примера дополнил тестовую конфигурацию из последней статьи следующим примером. Представим, что директор издал очень жесткий указ, чтобы ни один Счет на оплату не проводился без основания в виде Служебной записки. За нарушение - ставят к стенке, без разговоров. Никакие оправдания, что не успел, не действуют. В такой ситуации надо выручать коллег, и программист тут же написал следующий код в событии ПередЗаписью:
Если Не ЗначениеЗаполнено(ДокументОснование) Или ДокументОснование.ПолучитьОбъект() = Неопределено Тогда
СлужебнаяЗаписка = Документы.СлужебнаяЗаписка.СоздатьДокумент();
СлужебнаяЗаписка.Дата = ТекущаяДатаСеанса();
СлужебнаяЗаписка.УстановитьНовыйНомер();
СлужебнаяЗаписка.Комментарий = Комментарий;
СлужебнаяЗаписка.СуммаДокумента = СуммаДокумента;
Попытка
СлужебнаяЗаписка.Записать(РежимЗаписиДокумента.Запись);
ДокументЗаписан = Истина;
СлужебнаяЗаписка.Записать(РежимЗаписиДокумента.Проведение);
Исключение
Если ДокументЗаписан = Истина Тогда
Сообщить("Служебное основание записано, не проведено", СтатусСообщения.Важное);
ЗаписьЖурналаРегистрации("Доработки.Отлавливание ошибок", УровеньЖурналаРегистрации.Ошибка,
Метаданные.Документы.СлужебнаяЗаписка, СлужебнаяЗаписка.Ссылка,
"Не удалось провести служебную записку. Причина: " + ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
Иначе
Отказ = Истина;
Сообщить("Не удалось ни записать, ни провести служебную записку", СтатусСообщения.ОченьВажное);
ЗаписьЖурналаРегистрации("Доработки.Отлавливание ошибок", УровеньЖурналаРегистрации.Ошибка,
Метаданные.Документы.СчетНаОплату, Ссылка, "Не удалось записать служебную записку. Причина: "
+ ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецЕсли;
КонецПопытки;
ДокументОснование = СлужебнаяЗаписка.Ссылка;
КонецЕсли;
Ожидая таким образом, что если документ не будет проведен, то хотя бы запишется и сотрудник сможет провести его вручную, выполнив условия. Каково же было его удивление, когда он увидел в журнале транзакций такое:
То есть очевидно, что запись оказалась отменена вместе с проведением! А у пользователя вышла ошибка "В данной транзакции уже происходили ошибки" и ничего!
Но этому программисту повезло, и он был хорошо знаком с механизмом фоновых заданий, поэтому быстро набросал процедуру для выполнения того же в фоновом задании:
Результат = Новый Структура("Успешно, Отказ, Ссылка", Ложь, Ложь);
Если ОписанияОбъекта.Количество() <> 1 Тогда
ВызватьИсключение "Передано больше одного описания объекта";
КонецЕсли;
ОписаниеОбъекта = ОписанияОбъекта[0];
СлужебнаяЗаписка = Документы.СлужебнаяЗаписка.СоздатьДокумент();
СлужебнаяЗаписка.Дата = ТекущаяДатаСеанса();
СлужебнаяЗаписка.УстановитьНовыйНомер();
СлужебнаяЗаписка.Комментарий = ОписаниеОбъекта.Комментарий;
СлужебнаяЗаписка.СуммаДокумента = ОписаниеОбъекта.СуммаДокумента;
Попытка
СлужебнаяЗаписка.Записать(РежимЗаписиДокумента.Запись);
ДокументЗаписан = Истина;
Результат.Ссылка = СлужебнаяЗаписка.Ссылка;
СлужебнаяЗаписка.Записать(РежимЗаписиДокумента.Проведение);
Результат.Успешно = Истина;
Исключение
Если ДокументЗаписан = Истина Тогда
Сообщить("Служебное основание записано, не проведено", СтатусСообщения.Важное);
ЗаписьЖурналаРегистрации("Доработки.Отлавливание ошибок", УровеньЖурналаРегистрации.Ошибка,
Метаданные.Документы.СлужебнаяЗаписка, СлужебнаяЗаписка.Ссылка,
"Не удалось провести служебную записку. Причина: " + ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
Иначе
Результат.Отказ = Истина;
Сообщить("Не удалось ни записать, ни провести служебную записку", СтатусСообщения.ОченьВажное);
ЗаписьЖурналаРегистрации("Доработки.Отлавливание ошибок", УровеньЖурналаРегистрации.Ошибка,
Метаданные.Документы.СчетНаОплату, ОписаниеОбъекта.Ссылка, "Не удалось записать служебную записку. Причина: "
+ ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецЕсли;
КонецПопытки;
ПоместитьВоВременноеХранилище(Результат, АдресРезультата);
, где ОписаниеОбъекта - это структура со всеми реквизитами и табличными частями ЭтогоОбъекта из модуля объекта. Как его получить, да просто:
Функция ОписаниеОбъекта(Знач Объект, ТипЗначения = Неопределено) Экспорт
ОписаниеОбъекта = Новый Структура;
Если ТипЗнч(Объект) = Тип("ДанныеФормыСтруктура") Тогда
Если ТипЗнч(ТипЗначения) <> Тип("Тип") Или СтрНайти(ВРег(ТипЗначения), "ОБЪЕКТ") = 0 Тогда
ВызватьИсключение "Тип реквизита формы должен быть указан явно как ...Объект";
КонецЕсли;
Объект = ДанныеФормыВЗначение(Объект, ТипЗначения);
КонецЕсли;
МетаданныеОбъекта = Объект.Метаданные();
Для Каждого Реквизит Из МетаданныеОбъекта.СтандартныеРеквизиты Цикл
ОписаниеОбъекта.Вставить(Реквизит.Имя, Объект[Реквизит.Имя]);
КонецЦикла;
Для Каждого Реквизит Из МетаданныеОбъекта.Реквизиты Цикл
ОписаниеОбъекта.Вставить(Реквизит.Имя, Объект[Реквизит.Имя]);
КонецЦикла;
Для Каждого ТабличнаяЧасть Из МетаданныеОбъекта.ТабличныеЧасти Цикл
ОписаниеОбъекта.Вставить(ТабличнаяЧасть.Имя, Объект[ТабличнаяЧасть.Имя].Выгрузить());
КонецЦикла;
Возврат ОписаниеОбъекта;
КонецФункции
Теперь осталось только запустить процедуру в фоновом задании и получить результат:
ОписаниеОбъекта = ДлительныеОперацииСервер.ОписаниеОбъекта(ЭтотОбъект);
НастройкиВыполнения = ДлительныеОперацииСервер.НастройкиВыполнения(1, 1, , Истина);
МассивДанных = Новый Массив;
МассивДанных.Добавить(ОписаниеОбъекта);
РезультатыВыполнения = ДлительныеОперацииСервер.ВыполнитьФункциюПорциями(МассивДанных,
"Документы.СчетНаОплату.СоздатьСчетОснование", Новый Массив, НастройкиВыполнения);
Если РезультатыВыполнения.Количество() Тогда
Если РезультатыВыполнения[0] = Неопределено Или РезультатыВыполнения[0].Отказ Тогда
Отказ = Истина;
Иначе
ДокументОснование = РезультатыВыполнения[0].Ссылка;
КонецЕсли;
Иначе
Отказ = Истина;
КонецЕсли;
На этом все поставленные задачи решены:

"А был бы я бедный?" - подумал программист, - "Сколько кода мне пришлось бы написать?". И быстро набросал код, необходимый для вызова фонового задания вручную:
ОписаниеОбъекта = ДлительныеОперацииСервер.ОписаниеОбъекта(ЭтотОбъект);
ПараметрыФункции = Новый Массив;
ПараметрыФункции.Добавить(ОписаниеОбъекта);
АдресРезультата = ПоместитьВоВременноеХранилище(Неопределено, Новый УникальныйИдентификатор);
ПараметрыФункции.Добавить(АдресРезультата);
ПараметрыВызова = Новый Массив;
ПараметрыВызова.Добавить("Документы.СчетНаОплату.СоздатьСлужебноеОснование");
ПараметрыВызова.Добавить(ПараметрыФункции);
Задание = ФоновыеЗадания.Выполнить("ОбщегоНазначения.ВыполнитьМетодКонфигурации", ПараметрыВызова);
Задание.ОжидатьЗавершенияВыполнения();
Для Каждого Сообщение Из Задание.ПолучитьСообщенияПользователю() Цикл
Сообщение.Сообщить();
КонецЦикла;
РезультатыВыполнения = ПолучитьИзВременногоХранилища(АдресРезультата);
Если РезультатыВыполнения = Неопределено Тогда
Отказ = Истина;
Иначе
Если РезультатыВыполнения.Отказ Тогда
Отказ = Истина;
Иначе
ДокументОснование = РезультатыВыполнения.Ссылка;
КонецЕсли;
КонецЕсли;
Как обычно - зная, что хочешь сделать, можно написать не намного сложнее, но зачем? Код функции создания служебки также немного изменил.
Вывод. Здесь был показан конкретный пример ошибки, но он может быть и любым другим. Главное, чтобы была возможность выделить в отдельный метод и передать сериализуемые параметры (легко ищется в синтакс-помощнике по слову "Сериализуется"). Сериализуется не все, что можно передать с клиента на сервер - например, нельзя сериализовать ДанныеФормыСтруктура, то есть реквизит "Объект" формы, как и саму форму, конечно. Результат также должен быть сериализуемым, если он нужен. В остальном ограничений быть не должно, единственное, что нужно учитывать при реализации прямого алгоритма вызова фонового задания, так это что, ВыполнитьМетодКонфигурации может устанавливать безопасный режим, соответственно, например, никаких HTTPСоединение, если у вас это работает:
ПрофильБезопасностиИБ = СтандартныеПодсистемыСервер.ПрофильБезопасностиДляВыполнитьВычислить();
Если ЗначениеЗаполнено(ПрофильБезопасностиИБ) Тогда
УстановитьБезопасныйРежим(ПрофильБезопасностиИБ);
КонецЕсли;
С ВыполнитьФункциюПорциями такого ограничения нет, там код считается безопасным только потому, что вызывается из базы (раньше в БСП было так).
И ВСЕГДА записывайте в журнал регистрации ошибки, пойманные блоком Попытка, чтобы поиск причины "В данной транзакции уже происходили ошибки" не превращался в пытку! Иначе зачем он вообще нужен)
Совместимость 8.3.25 - из-за паузы, ее переделал на совместимую с файловыми базами. Если у вас клиент-серверная, хватайте Паузу-Оптовик для легаси кода из статьи, она совместима с более низкими версиями. Главное, не используйте за основу паузу отрицания - в контексте данной статьи это то, что совсем нельзя!
Вступайте в нашу телеграмм-группу Инфостарт