В процессе разработки в среде 1С программисты часто сталкиваются с важной архитектурной особенностью платформы — транзакционностью. Типичный пример: при проведении документа (например, РеализацияТоваровУслуг) выполняется проверка контрагента на превышение лимита долга или просрочку платежей. Если проверка не пройдена, программа должна заблокировать проведение документа и одновременно сохранить подробный отчет об ошибке в базу данных (например, в свойства документа или специальный регистр сведений), чтобы менеджер или супервайзер мог проанализировать причину отказа.
Однако здесь разработчик упирается в логику работы СУБД: если в процедуре ОбработкаПроведения установить параметр Отказ = Истина, система инициирует отмену всей транзакции. Все изменения, которые мы пытались записать в базу данных в рамках этой транзакции (включая запись ошибок в регистры сведений или изменение реквизитов документа), безвозвратно откатываются. В итоге проведение заблокировано, но и причина отказа нигде не зафиксирована.
В этой статье мы подробно разберем, как обойти это ограничение платформы 1С. Мы проанализируем как простые "быстрые" решения, так и профессиональные архитектурные подходы, которые используются в крупных корпоративных системах.
Самое очевидное решение, к которому часто приходят начинающие разработчики, — перенести алгоритм проверки из модуля объекта в модуль формы документа, перехватив событие перед записью.
Выясним, как это работает. Когда пользователь нажимает кнопку "ОК" или "Провести", управление сначала передается в модуль формы. Мы можем выполнить проверку лимитов прямо там. Если проверка не пройдена, мы записываем информацию об ошибке в независимый регистр сведений (или свойства документа) в рамках отдельной транзакции записи, а затем отменяем проведение еще до того, как начнется основная транзакция проведения объекта.
Рассмотрим пример реализации в модуле формы документа:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Проверяем, что документ именно проводится интерактивно
Если РежимПроведения = РежимПроведенияДокумента.Проведение Тогда
// Запускаем алгоритм проверки контрагента
РезультатПроверки = ПроверитьДобросовестностьПокупателя();
Если РезультатПроверки.ПревышенЛимит Тогда
// Записываем причину отказа в базу данных
ЗаписатьПричинуОтказа(ЭтотОбъект.Ссылка, РезультатПроверки.ОписаниеОшибки);
// Сообщаем пользователю и блокируем проведение
ОбщегоНазначенияКлиентСервер.СообщитьПользователю(РезультатПроверки.ОписаниеОшибки);
Отказ = Истина;
Возврат;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Почему этот подход считается плохой практикой?
Несмотря на то, что этот метод решает задачу "в лоб" при работе пользователя в интерфейсе, он обладает критическими недостатками:
Поэтому данный метод подходит только для простых локальных баз с исключительно ручным вводом документов, но крайне не рекомендуется для промышленных решений.
Чтобы обойти транзакционный откат, нам необходимо сделать запись в базу данных в рамках другого, независимого сеанса связи с СУБД. В современной платформе 1С для этого идеально подходят фоновые задания.
Проанализируем этот метод по шагам. Когда в процедуре ОбработкаПроведения в модуле объекта мы понимаем, что проверку пройти не удалось, мы не просто пишем отказ, а программно запускаем фоновое задание, передавая ему параметры нашей ошибки. Фоновое задание запускается на сервере в отдельном потоке (сеансе). Оно имеет свою собственную транзакцию, которая успешно фиксируется. После этого основная транзакция проведения документа завершается ошибкой (Отказ = Истина) и откатывается, но запись, сделанная фоновым заданием, остается в базе данных!
Рассмотрим программную реализацию. В модуле объекта документа пишем следующий код:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
РезультатПроверки = ПроверитьЛимитыДолга();
Если РезультатПроверки.ЕстьОшибки Тогда
// Подготавливаем параметры для фонового задания
ПараметрыЗапуска = Новый Массив;
ПараметрыЗапуска.Добавить(ЭтотОбъект.Ссылка);
ПараметрыЗапуска.Добавить(РезультатПроверки.ТекстОшибки);
// Запускаем фоновое задание для записи в обход текущей транзакции
ФоновыеЗадания.Выполнить("ОбщегоНазначенияСервер.ЗаписатьОтказПроведенияФоновымПотоком", ПараметрыЗапуска);
// Выводим сообщение пользователю и отменяем транзакцию
ВызватьИсключение РезультатПроверки.ТекстОшибки;
Отказ = Истина;
Возврат;
КонецЕсли;
// Основная логика проведения (движения по регистрам)...
КонецПроцедуры
В общем серверном модуле ОбщегоНазначенияСервер (у которого установлены флаги "Сервер" и "Вызов сервера") размещаем процедуру, которая будет выполняться фоновым заданием:
Процедура ЗаписатьОтказПроведенияФоновымПотоком(ДокументСсылка, ТекстОшибки) Экспорт
// Создаем запись в независимом регистре сведений
МенеджерЗаписи = РегистрыСведений.РегистрацияОтказовОтгрузки.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Период = ТекущаяДата();
МенеджерЗаписи.Документ = ДокументСсылка;
МенеджерЗаписи.ПричинаОтказа = ТекстОшибки;
МенеджерЗаписи.Пользователь = ПараметрыСеанса.ТекущийПользователь;
Попытка
МенеджерЗаписи.Записать(Истина);
Исключение
ЗаписатьВЖурналРегистрации("ЗаписьОтказовФоновое",
УровеньЖурналаРегистрации.Ошибка, , ,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
КонецПроцедуры
Этот способ является наиболее надежным и современным. Он гарантирует сохранение данных независимо от того, как именно проводится документ (вручную пользователем или программно через фоновые процессы).
Этот метод часто незаслуженно обходят стороной, считая его сложным в реализации. Однако у него есть колоссальное преимущество — он работает максимально быстро и вообще не нагружает транзакцию проведения дополнительными блокировками баз данных.
Суть механизма заключается в следующем:
Посмотрим на пример кода записи в Журнал регистрации в модуле объекта документа:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
РезультатПроверки = ПроверитьЛимитыДолга();
Если РезультатПроверки.ЕстьОшибки Тогда
// Формируем структуру данных для последующего разбора регламентным заданием
СтруктураДанных = Новый Структура;
СтруктураДанных.Вставить("ДокументСсылка", ЭтотОбъект.Ссылка);
СтруктураДанных.Вставить("ПричинаОтказа", РезультатПроверки.ТекстОшибки);
// Преобразуем структуру в XML или JSON для надежного сохранения в лог
ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.УстановитьСтроку();
ЗаписатьXML(ЗаписьXML, СтруктураДанных);
СтрокаДанных = ЗаписьXML.Закрыть();
// Пишем лог, который никогда не откатится транзакцией
ЗаписатьВЖурналРегистрации("Документы.РеализацияТоваровУслуг.ОтказПроведения",
УровеньЖурналаРегистрации.Предупреждение,
ЭтотОбъект.Метаданные(),
ЭтотОбъект.Ссылка,
СтрокаДанных);
Отказ = Истина;
Возврат;
КонецЕсли;
КонецПроцедуры
Затем регламентное задание считывает данные с помощью метода ВыгрузитьЖурналРегистрации() по фильтру события "Документы.РеализацияТоваровУслуг.ОтказПроведения", десериализует полученные строки и аккуратно записывает их в предназначенный для отчетов регистр сведений.
В крупных учетных системах (таких как 1С:ERP или 1С:Комплексная автоматизация) разработчики часто отказываются от концепции "жесткого" запрета проведения на уровне СУБД. Вместо этого применяется концепция "мягкого проведения" или статусной модели.
Посмотрим, как устроен этот процесс:
Отказ = Ложь, транзакция успешно завершается на уровне базы данных, и все изменения сохраняются.Статус (например, со значениями: "Черновик", "На согласовании", "Заблокирован", "К отгрузке")."Заблокирован", а в специальную табличную часть или регистр сведений записывается детальная причина блокировки."Заблокирован" или "На согласовании", то движения по регистрам остатков товаров и взаиморасчетов вообще не формируются (наборы записей движений остаются пустыми).Разберем пример реализации такой бизнес-логики в модуле объекта:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
РезультатПроверки = ПроверитьЛимитыДолга();
Если РезультатПроверки.ЕстьОшибки Тогда
// Документ проводится, но переходит в заблокированное состояние
ЭтотОбъект.Статус = Перечисления.СтатусыРеализаций.Заблокирован;
ЭтотОбъект.ПричинаБлокировки = РезультатПроверки.ТекстОшибки;
// Записываем информацию напрямую в реквизиты документа
ЭтотОбъект.Записать(РежимЗаписиДокумента.Запись);
// Важно: движения по складам и взаиморасчетам не делаем!
Движения.ТоварыНаСкладах.Записывать = Ложь;
Движения.ВзаиморасчетыСКонтрагентами.Записывать = Ложь;
// Выводим информационное предупреждение менеджеру
ОбщегоНазначенияКлиентСервер.СообщитьПользователю(
"Внимание! Документ заблокирован из-за превышения лимитов долга контрагента.");
Возврат;
КонецЕсли;
// Если проверки пройдены, переводим в статус "К отгрузке" и делаем движения
ЭтотОбъект.Статус = Перечисления.СтатусыРеализаций.КОтгрузке;
Движения.ТоварыНаСкладах.Записывать = Истина;
// ...алгоритм заполнения движений...
КонецПроцедуры
Преимущества данного метода:
Мы рассмотрели различные варианты решения проблемы транзакционного отката в 1С при блокировке проведения документов. Подведем итог, какой метод выбрать для вашей задачи:
Отказ = Истина) лучшим современным решением является использование фоновых заданий (Вариант 2).