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