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