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