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