Как правильно реализовать создание одного документа при проведении другого в 1С и почему нельзя делать это в ОбработкеПроведения?

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

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

В чем заключается ошибка автора темы?

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

Размещение кода создания и записи других объектов базы данных (в данном случае документа «Реализация товаров и услуг») внутри процедуры ОбработкаПроведения другого документа («Маршрут поездки») порождает целый комплекс критических проблем:

  1. Проблема длинных транзакций и блокировок (Deadlocks). Проведение любого документа в 1С всегда выполняется в рамках транзакции базы данных. Транзакция — это неделимая последовательность манипуляций с данными. Пока транзакция проведения «Маршрута поездки» не завершена, СУБД накладывает исключительные блокировки на изменяемые таблицы. Если внутри этой транзакции мы начинаем создавать, заполнять и записывать *другой* документ («Реализация»), транзакция неоправданно затягивается во времени. В многопользовательской базе это гарантированно приведет к ошибкам ожидания блокировок у других пользователей. Хуже того, высока вероятность возникновения взаимных блокировок (deadlocks), когда два процесса пытаются заблокировать ресурсы друг друга в разном порядке.
  2. Нарушение целостности и логики транзакций. Если в процессе проведения «Маршрута поездки» после создания «Реализации» произойдет какая-либо ошибка (например, не хватит остатков товаров на складе, сработает ограничение прав доступа или проверка в подписке на события), вся транзакция будет отменена (произойдет rollback). Однако, если в коде записи создаваемого документа использовались внешние вызовы или нетипичные транзакционные механизмы, система может прийти в рассогласованное состояние. Еще более критичная ситуация возникает при отмене проведения исходного документа. Если пользователь решит распровести «Маршрут поездки», созданные ранее документы «Реализация товаров и услуг» останутся в системе нетронутыми. База данных мгновенно наполнится «мусорными» несвязанными документами, нарушающими логику учета.
  3. Использование клиентского или неактуального контекста на сервере. В коде автора мы видим вызовы функций типа глЗначениеПеременной("глТекущийПользователь") и УправлениеПользователями.ПолучитьЗначениеПоУмолчанию(...). Использование глобального контекста и переменных сеанса внутри модуля объекта (особенно в транзакции проведения) — крайне плохая практика. В современных конфигурациях на управляемых формах при выполнении кода на сервере (например, при групповом перепроведении документов в фоновом задании) эти переменные будут недоступны, что вызовет аварийное завершение работы системы.
  4. Интерактивные методы внутри фоновых процессов. Автор возвращает из функции СписокСообщений для вывода пользователю. Но ОбработкаПроведения — это чисто серверное событие. Оно может выполняться при групповом перепроведении документов, обмене данными или работе регламентных заданий, где никакого пользователя у экрана нет. Пытаться вернуть интерактивный результат из транзакции проведения — грубая ошибка разработки.

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

---

Решение №1. Интерактивное создание через механизм «Ввод на основании» (Рекомендуемый метод)

Если бизнес-процесс предполагает, что решение о создании «Реализации» принимает пользователь, этот процесс должен быть полностью интерактивным. Документ «Реализация товаров и услуг» должен создаваться на основании «Маршрута поездки».

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

Шаг 1. В конфигураторе для документа «РеализацияТоваровУслуг» в свойстве «Вводится на основании» указываем документ «МаршрутПоездки».

Шаг 2. В модуле объекта документа «РеализацияТоваровУслуг» описываем обработчик события ОбработкаЗаполнения. Проанализируем пример кода:


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

При таком подходе пользователь нажимает кнопку «Создать на основании» прямо в форме «Маршрута поездки». На экране открывается заполненный, но еще не записанный документ «Реализация товаров и услуг». Пользователь может проверить правильность заполнения данных, при необходимости отредактировать их и самостоятельно нажать кнопку «Провести». Это полностью безопасно для транзакционной целостности СУБД.

---

Решение №2. Создание документа вне транзакции (В событии формы «ПослеЗаписи»)

Если бизнес-требования жестко настаивают на том, что документ «Реализация» должен создаваться автоматически и программно именно в момент проведения «Маршрута», мы можем сделать это. Но выполнять генерацию нужно вне транзакции проведения.

Для этого мы переносим логику создания документа из модуля объекта в модуль формы документа «Маршрут поездки». Мы будем использовать обработчик события формы ПослеЗаписиНаСервере. Выясним, почему это безопасно: в этот момент транзакция проведения текущего документа «Маршрут поездки» уже полностью завершена, все блокировки сняты, данные успешно записаны в базу данных.

Разберем пример реализации в модуле формы документа «Маршрут поездки»:


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

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

Этот способ решает ключевую проблему: создание зависимых документов происходит после фиксации основной транзакции в СУБД. Если проведение «Маршрута» завершится ошибкой, создание «Реализаций» даже не начнется. СУБД не будет испытывать нагрузок от избыточных вложенных транзакций.

---

Решение №3. Использование очереди и регламентных заданий (Асинхронный метод для High-Load систем)

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

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

Проанализируем шаги реализации этой схемы:

  1. Создается простой технический регистр сведений ОчередьСозданияРеализаций со структурой:
    • Измерение: ДокументМаршрут (тип ДокументСсылка.МаршрутПоездки).
    • Ресурс: ДатаДобавления (тип Дата).
  2. В модуле объекта «Маршрут поездки» в обработчике ОбработкаПроведения (или в подписке на события ПриПроведении) мы пишем сверхбыстрый код, который делает всего одну запись в наш новый регистр сведений. Это занимает доли миллисекунды и никак не нагружает транзакцию.
  3. Настраивается регламентное (фоновое) задание, которое запускается сервером 1С по расписанию (например, каждые 30 секунд или 1 минуту).
  4. Фоновое задание считывает накопленные в регистре ОчередьСозданияРеализаций записи, в безопасном режиме (в фоне, не мешая пользователям) генерирует, заполняет и проводит документы «Реализация товаров и услуг». После успешного создания строки из регистра очереди удаляются.

Рассмотрим пример кода для регламентного задания:


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

Этот асинхронный паттерн гарантирует максимальное быстродействие системы. Ни один пользователь не столкнется с зависанием интерфейса при проведении документов, а все зависимые документы будут создаваться стабильно и в строгом соответствии с бизнес-логикой системы.

Резюме

При проектировании учетных систем в 1С всегда помните о главном правиле: транзакция проведения документа должна быть максимально короткой и содержать только движения по регистрам этого же документа. Создание новых ссылочных объектов (документов, справочников) внутри транзакции проведения — это мина замедленного действия. Используйте интерактивный ввод на основании, обработку в событиях формы ПослеЗаписи или механизмы фоновых очередей, чтобы ваша база данных работала быстро, стабильно и без системных сбоев.

← На главную