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