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