Почему заказ получает статус «Отгружен», если реализация не создалась, и как правильно записать документы программно?

Программист 1С v8.3 (Управляемые формы) Управленческий учет IT и автоматизация бизнеса
← На главную

В практике разработки на платформе 1С:Предприятие нередко встречается задача автоматического ввода связанных документов: например, создание документа РеализацияТоваровУслуг из документа ЗаказПокупателя по нажатию специальной команды. При этом часто требуется сразу провести созданный документ и изменить состояние исходного заказа. Однако при неаккуратной организации транзакций и некорректной обработке ошибок разработчики сталкиваются с неприятной коллизией: заказ переходит в статус «Отгружен», но связанная накладная в базе данных так и не появляется.

Разберем детально, почему так происходит, какие скрытые механизмы платформы 1С приводят к подобному поведению и как грамотно переписать алгоритм, чтобы гарантировать целостность данных.

Анализ исходного кода и проявление проблемы

Рассмотрим типовой фрагмент кода, в котором разработчик пытается найти существующую реализацию по текущему заказу, а при ее отсутствии — создать новую, провести ее и изменить статус заказа:


// Поиск реализации по текущему заказу
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |   Реализация.Ссылка
    |ИЗ
    |   Документ.Реализация КАК Реализация
    |ГДЕ
    |   Реализация.Заказ = &Заказ
    |   И Реализация.ПометкаУдаления = ЛОЖЬ";
Запрос.УстановитьПараметр("Заказ", ЭтотОбъект.Объект.Ссылка);
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();

Результат = Неопределено;
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
    Результат = ВыборкаДетальныеЗаписи.Ссылка;
КонецЦикла;

Если НЕ ЗначениеЗаполнено(Результат) Тогда
    Реализация = Документы.Реализация.СоздатьДокумент();
    Реализация.Склад = Справочники.Склады.НайтиПоНаименованию("Склад товаров");
    Реализация.Заказ = ЭтотОбъект.Объект.Ссылка;
    Реализация.Автор = ПараметрыСеанса.ТекущийПользователь;
    Реализация.Дата = ТекущаяДата();
    
    Для Каждого Стр Из ЭтотОбъект.Объект.Пряники Цикл
        Если Стр.Пряник.ВестиКоличественныйУчетПоСкладу Тогда
            НоваяСтрока = Реализация.Пряники.Добавить();
            ЗаполнитьЗначенияСвойств(НоваяСтрока, Стр);
        КонецЕсли;
    КонецЦикла;
    
    Попытка
        Реализация.Записать(РежимЗаписиДокумента.Проведение);
    Исключение
    КонецПопытки;
    
    Попытка
        Если Реализация.Проведен Тогда
            ЭтотОбъект.Объект.СтатусЗаказа = Перечисления.СтатусыЗаказов.Отгружен;
            ЭтотОбъект.Записать();
        КонецЕсли;
    Исключение
    КонецПопытки;
КонецЕсли;

На первый взгляд разработчика логика выглядит стройной: сначала пытаемся провести документ, а затем, если свойство Проведен равно Истина, фиксируем отгрузку в заказе. В большинстве случаев этот код отрабатывает штатно. Но в редких ситуациях накладная не создается, а статус заказа всё равно переключается на «Отгружен». Выясним причину этой проблемы.

Почему проверка «Если Реализация.Проведен» дает ложный результат

Проанализируем поведение платформы 1С:Предприятие при вызове метода Записать(РежимЗаписиДокумента.Проведение). Это ключевая особенность объектной модели, о которой часто забывают:

  1. Когда инициируется запись с проведением, платформа еще на этапе подготовки данных в памяти взводит свойство Проведен в значение Истина у текущего экземпляра объекта.
  2. Далее стартует транзакция записи, вызываются обработчики событий (ПередЗаписью, ОбработкаПроведения, подписки на события).
  3. Если на этапе проведения возникает ошибка (например, не хватило остатка товаров, сработала проверка заполнения или сгенерировано исключение), транзакция базы данных откатывается. Сам документ в базу не записывается.
  4. В блоке Попытка ... Исключение ошибка успешно глушится пустым блоком. Управление передается дальше.
  5. При этом объект в оперативной памяти сервера не уничтожается и не сбрасывается к исходному состоянию! Свойство Реализация.Проведен продолжает содержать значение Истина.

В результате условие Если Реализация.Проведен Тогда выполняется успешно. После этого происходит отдельная запись объекта формы заказа, переводящая его в статус «Отгружен». Таким образом, мы получаем грубейшую рассинхронизацию учетных данных.

Сопутствующие скрытые ошибки алгоритма

Помимо поведения свойства в памяти, в данном коде присутствуют и другие опасные места:

  1. Пустые блоки Исключение: Проглатывание исключений без записи в журнал регистрации и без информирования пользователя приводит к «тихим» ошибкам, которые невозможно диагностировать в рабочей базе.
  2. Раздельные неявные транзакции: Запись реализации и запись заказа происходят независимо. Если первая завершилась успешно, а вторая упала (или наоборот), система оказывается в несогласованном состоянии.
  3. Ненадежный поиск по наименованию: Строка Справочники.Склады.НайтиПоНаименованию("Склад товаров") вернет пустую ссылку, если наименование склада отредактируют, поставят случайный двойной пробел или невидимый символ. Если реквизит «Склад» обязателен к заполнению, проведение накладной неизбежно вызовет отказ.
  4. Фильтрация строк табличной части: Если в заказе не окажется ни одной строки с признаком ВестиКоличественныйУчетПоСкладу, будет предпринята попытка провести накладную с пустой табличной частью, что во многих конфигурациях заблокировано проверками.

Правильное решение: использование единой явной транзакции

Для гарантированной целостности данных обе операции — запись нового документа и обновление статуса текущего — должны выполняться в единой транзакции. Если одна из операций завершается неудачей, откатиться должны все изменения без исключения.

Рассмотрим, как грамотно реализовать этот механизм на стороне сервера:


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

КонецПроцедуры

Архитектурные рекомендации по улучшению кода

Чтобы исключить появление подобных проблем в будущем, разберем ключевые правила построения связанных документов:

  1. Не проверять статус проведения по объекту в памяти: Если вам действительно требуется проверить факт проведения документа после блока Исключение, делайте это запросом к реальной таблице базы данных либо проверяйте реквизит через свежую ссылку: Реализация.Ссылка.Проведен. Свойство объектной переменной в памяти после неудачной записи не отражает состояние базы данных.
  2. Использовать механизм «Ввод на основании»: Логику переноса реквизитов и строк лучше выносить в событие ОбработкаЗаполнения модуля документа Реализация. В таком случае программное создание сведется к двум вызовам:
    
    Реализация = Документы.Реализация.СоздатьДокумент();
    Реализация.Заполнить(Объект.Ссылка);
    
    Это исключит дублирование правил заполнения в разных модулях конфигурации.
  3. Защита от параллельного нажатия (блокировки данных): Если с базой одновременно работают несколько менеджеров или пользователь случайно дважды нажал кнопку «Отгрузить», могут возникнуть дубли накладных. Чтобы этого избежать, перед проверкой на наличие реализации следует установить управляемую блокировку на заказ с помощью объекта БлокировкаДанных в пределах открытой транзакции.
  4. Отказ от поиска по строковому наименованию: Значения служебных складов, контрагентов или статей затрат не следует искать методом НайтиПоНаименованию. Используйте предопределенные элементы, константы или настройки пользователя.

Объединение логически зависимых действий в конструкцию НачатьТранзакцию() ... ЗафиксироватьТранзакцию() гарантирует, что статус заказа никогда не изменится на «Отгружен» без успешно записанной и проведенной накладной.

← На главную