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