Как определить изменение реквизита в независимом регистре сведений 1С

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

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

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

Давайте подробно разберем, почему возникает эта проблема, проанализируем внутренние механизмы платформы и рассмотрим несколько изящных и надежных способов ее решения.

В чем заключается сложность? Физика процесса записи набора

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

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

Получается замкнутый круг: на первом шаге у нас еще нет новых данных (набор пуст), а на втором шаге в базе данных уже нет старых данных (они стерты). Давайте разберем по шагам, как обойти это ограничение платформы.

Решение 1. Использование параметров сеанса в качестве временного буфера

Это наиболее универсальный программный способ, который работает независимо от того, откуда записываются данные — из формы, обработки или при проведении документа. Суть метода заключается в том, чтобы перехватить старые данные на первом этапе записи (когда база данных еще полна) и временно сохранить их в оперативной памяти сеанса 1С, а на втором этапе — извлечь их и сравнить с новыми значениями.

Для реализации этого механизма мы будем использовать ПараметрыСеанса. Выясним, как это реализовать на практике.

Шаг 1. В конфигураторе создадим новый параметр сеанса, например, БуферИзмененийРегистров с типом ХранилищеЗначения.

Шаг 2. В модуле набора записей нашего регистра сведений напишем следующий код в обработчике события ПередЗаписью:


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

КонецПроцедуры

Рассмотренный метод является очень надежным. Он гарантирует, что даже если транзакция записи прервется, данные в параметрах сеанса будут обновлены корректно при следующей попытке. Однако помните о необходимости своевременной очистки параметра сеанса, чтобы избежать утечек памяти.

Решение 2. Обходной путь на уровне интерфейса (Запрет редактирования в списке)

Очень часто бизнес-логика позволяет упростить задачу. Если изменения вносятся пользователями вручную, мы можем заблокировать возможность редактировать строки регистра непосредственно в списке. В этом случае пользователь будет вынужден открывать специальную форму записи регистра для внесения изменений.

Почему это решает нашу проблему? Дело в том, что в контексте формы нам одновременно доступны и старые данные из базы данных, и новые данные, которые пользователь только что ввел на экране. Проанализируем, как это настроить по шагам:

  1. Откроем свойства регистра сведений в конфигураторе и на вкладке «Основные» установим режим редактирования в значение «В диалоге». Это автоматически запретит редактирование прямо в ячейках списка.
  2. Создадим форму записи регистра сведений.
  3. В модуле формы определим обработчик события ПередЗаписьюНаСервере. В этот момент данные формы уже изменены пользователем, но запись в базу данных еще не началась. Мы можем выполнить быстрый точечный запрос к базе данных и получить старое значение до его удаления.

Посмотрим на пример реализации кода в модуле формы записи регистра:


&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
    
    // Нам доступны измерения текущей записи на форме
    НоменклатураТовара = ТекущийОбъект.Номенклатура;
    НовыйВес = ТекущийОбъект.Вес;
    
    // Прочитаем старое значение реквизита "Вес" прямо из базы данных
    СтарыйВес = ПолучитьСтарыйВесИзБД(НоменклатураТовара);
    
    Если СтарыйВес <> Неопределено И СтарыйВес <> НовыйВес Тогда
        
        // Фиксируем изменение веса и делаем отметку во втором регистре
        РегистрацияИзмененийВЭД.ЗафиксироватьИзменениеПараметров(НоменклатураТовара, "Изменение веса");
        
    КонецЕсли;
    
КонецПроцедуры

&НаСервере
Функция ПолучитьСтарыйВесИзБД(НоменклатураТовара)
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Товары.Вес КАК Вес
        |ИЗ
        |	РегистрСведений.ТоварыНаСкладах КАК Товары
        |ГДЕ
        |	Товары.Номенклатура = &Номенклатура";
    
    Запрос.УстановитьПараметр("Номенклатура", НоменклатураТовара);
    Выборка = Запрос.Выполнить().Выбрать();
    
    Если Выборка.Следующий() Тогда
        Возврат Выборка.Вес;
    КонецЕсли;
    
    Возврат Неопределено;
    
КонецФункции

Преимущества данного подхода очевидны: простота реализации, отсутствие необходимости использовать тяжелые параметры сеанса или временные хранилища, минимальная нагрузка на сервер СУБД благодаря точечному запросу по индексу.

Решение 3. Использование вспомогательного регистра «зеркала»

Если в вашей системе запись в регистр происходит из самых разных мест (документы, обмены данными, регламентные задания, внешние API) и вы не можете гарантировать использование только одной экранной формы, можно применить архитектурный паттерн «регистр-зеркало».

Рассмотрим, как устроен этот подход:

Этот способ гарантирует 100% точность работы в любых многопользовательских сценариях и при любых интеграциях, хотя и требует создания дополнительного объекта в метаданных конфигурации.

Резюме и рекомендации

Выбор конкретного метода зависит от архитектуры вашей информационной системы:

  1. Если изменения вносятся исключительно пользователями через интерфейс — используйте Решение 2 (Блокировка редактирования в списке). Это самый легкий и безопасный путь.
  2. Если запись ведется программно из разных модулей и вам нужна абсолютная универсальность — используйте Решение 1 (Параметры сеанса как буфер). Это позволит вам перехватить изменения «на лету» в единой точке входа.
  3. Если вам важно хранить историю изменений для последующего аудита — рассмотрите создание полноценного вспомогательного регистра или включение стандартного механизма История данных, предоставляемого платформой 1С версии 8.3.11 и выше.

Надеемся, эти методики помогут вам оптимизировать работу ваших регистров и успешно решить задачу контроля изменения реквизитов без переписывания сложных бизнес-процессов!

← На главную