Как создать документ на основании при изменении реквизита в документе-основании?

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

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

Проанализируем несколько подходов к решению этой задачи, от самых простых до наиболее надежных и масштабируемых, и разберем их преимущества и недостатки.

Подход 1: Создание документа в той же транзакции (синхронный метод)

Первая мысль, которая приходит в голову — отловить момент изменения и сразу же создать новый документ. Для этого можно использовать подписки на события объекта. Разберем, какие события нам подходят и какие подводные камни нас ожидают.

Отслеживание изменения реквизита

Чтобы понять, что реквизит был изменен, лучше всего использовать событие ПередЗаписью в модуле объекта документа-основания (или в подписке на это событие). Этот обработчик срабатывает непосредственно перед сохранением данных в базу и позволяет сравнить текущие значения объекта с теми, что уже хранятся в ней.

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

Посмотрим на пример. Допустим, нам нужно отреагировать на изменение реквизита СтатусЗаказа.


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

Создание нового документа

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

Для заполнения нового документа на основании принято использовать экспортную процедуру или функцию в модуле создаваемого документа. Убедитесь, что она объявлена с ключевым словом Экспорт.

Пример кода в общем модуле, который вызывается из подписки на событие ОбработкаПроведения документа «Заказ клиента»:


Процедура СоздатьРеализациюПриПроведенииЗаказа(Источник, Отказ, РежимПроведения) Экспорт
    // Источник - это объект документа "Заказ клиента"
    Если Отказ Тогда
        Возврат;
    КонецЕсли;

    // Предположим, мы где-то ранее (например, в ПередЗаписью) определили, что нужно создать документ
    Если Источник.ДополнительныеСвойства.Свойство("СтатусИзменился") 
       И Источник.ДополнительныеСвойства.СтатусИзменился Тогда

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

КРИТИЧЕСКИ ВАЖНО: Этот подход является плохой практикой для высоконагруженных систем. Вся операция — проведение «Заказа» и создание с проведением «Реализации» — выполняется в одной большой транзакции. Это приводит к серьезным проблемам:

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

Подход 2: Асинхронный метод через очередь заданий (рекомендуемый)

Чтобы избежать проблем с длительными транзакциями и блокировками, следует разделить процессы. Проведение исходного документа должно быть быстрым и независимым. А создание связанных документов нужно вынести в отдельный, фоновый процесс. Для этого идеально подходит механизм регламентных заданий и промежуточного хранилища-очереди.

Разберем этот надежный подход по шагам.

Шаг 1. Создаем очередь заданий

Нам понадобится место для хранения информации о том, что нужно создать новый документ. Для этого можно использовать Регистр сведений. Он легковесный и отлично подходит для этой задачи.

Создадим регистр сведений, например, ОчередьСозданияРеализаций:

Шаг 2. Добавляем задачу в очередь

Теперь в событии ПередЗаписью или ОбработкаПроведения документа «Заказ клиента» вместо создания «Реализации» мы просто добавляем одну запись в наш новый регистр. Эта операция очень быстрая и не создаст большой нагрузки.


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

После выполнения этого кода «Заказ клиента» спокойно записывается, транзакция завершается, а пользователь может продолжать работу. В регистре сведений появляется новая задача.

Шаг 3. Создаем регламентное задание для обработки очереди

Теперь нужен механизм, который будет эту очередь разбирать. Создаем регламентное задание, например, «Создание реализаций по заказам», которое будет запускаться по расписанию (например, каждые 5 минут).

В общем модуле создаем процедуру, которую будет вызывать это задание.


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

            // Создаем новый документ
            НоваяРеализация = Документы.РеализацияТоваровУслуг.СоздатьДокумент();
            НоваяРеализация.ЗаполнитьПоОснованию(ЗаказКлиента.Ссылка);
            НоваяРеализация.Записать(РежимЗаписиДокумента.Проведение);
            
            // Если все успешно, помечаем задание как выполненное
            МенеджерЗаписи = РегистрыСведений.ОчередьСозданияРеализаций.СоздатьМенеджерЗаписи();
            МенеджерЗаписи.ДокументОснование = Выборка.ДокументОснование;
            МенеджерЗаписи.Прочитать();
            МенеджерЗаписи.Обработано = Истина;
            МенеджерЗаписи.Записать();
            
        Исключение
            // Если произошла ошибка, логируем ее и увеличиваем счетчик попыток
            ОписаниеОшибки = ОписаниеОшибки();
            
            МенеджерЗаписи = РегистрыСведений.ОчередьСозданияРеализаций.СоздатьМенеджерЗаписи();
            МенеджерЗаписи.ДокументОснование = Выборка.ДокументОснование;
            МенеджерЗаписи.Прочитать();
            МенеджерЗаписи.КоличествоПопыток = МенеджерЗаписи.КоличествоПопыток + 1;
            МенеджерЗаписи.ТекстОшибки = ОписаниеОшибки;
            
            // Если попыток слишком много, помечаем задание как обработанное, чтобы оно больше не выбиралось
            Если МенеджерЗаписи.КоличествоПопыток > 5 Тогда
                МенеджерЗаписи.Обработано = Истина; 
            КонецЕсли;
            
            МенеджерЗаписи.Записать();
            
            // Здесь также можно записать ошибку в Журнал регистрации
        КонецПопытки;
    КонецЦикла;
КонецПроцедуры

Преимущества асинхронного подхода:

Дополнительный момент: установка структуры подчиненности

Чтобы новый документ корректно отображался в отчете «Структура подчиненности», при его создании необходимо заполнить реквизит-основание. В большинстве типовых конфигураций для этого достаточно в новый документ «Реализация» в реквизит ДокументОснование (или с аналогичным названием) записать ссылку на «Заказ клиента». Система автоматически подхватит эту связь.

В нашей процедуре заполнения это уже учтено вызовом НоваяРеализация.ЗаполнитьПоОснованию(ЗаказКлиента.Ссылка), так как типовые механизмы заполнения обычно сами проставляют основание.

← На главную