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