Как сохранять историю изменения реквизита документа в текстовую строку в 1С?

Программист 1С v8.3 (Обычные формы) IT и автоматизация бизнеса
← На главную

При работе в среде 1С:Предприятие часто возникает прикладная задача: фиксировать историю изменения определенного реквизита шапки документа. Например, если пользователь несколько раз меняет вручную нестандартный входящий номер документа или связанный приходный ордер, система должна автоматически накапливать эти значения в отдельном строковом поле (например, в виде списка через запятую: "1, 2, 5").

В этой статье мы подробно разберем, как решить эту задачу в рамках обычных форм 1С v8.3, проанализируем популярные решения с форумов, выявим их скрытые уязвимости и предложим наиболее производительные и архитектурно правильные варианты реализации.

Вариант 1. Сравнение значений в обработчике события ПередЗаписью (Решение с форума)

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

Рассмотрим логику этого подхода. До тех пор пока документ не записан, мы имеем доступ к старому значению реквизита через обращение к свойству Ссылка. Текущее измененное значение доступно напрямую по имени реквизита.

Давайте посмотрим на базовый пример реализации, предложенный участниками обсуждения:


Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    
    // Сравниваем текущее значение на форме со значением в базе данных
    Если ПриходныйОрдер <> Ссылка.ПриходныйОрдер Тогда
        
        // Формируем историю изменений в текстовой строке
        Если ЗначениеЗаполнено(Ссылка.ПриходныйОрдер) Тогда
            ПриходныйОрдерСтрока = ПриходныйОрдерСтрока + ?(ПустаяСтрока(ПриходныйОрдерСтрока), "", ", ") + СокрЛП(Ссылка.ПриходныйОрдер);
        КонецЕсли;
        
    КонецЕсли;

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

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

Анализ уязвимостей и оптимизация кода

Проанализируем критические проблемы базового алгоритма:

  1. Проблема нового документа (первая запись): Если мы создаем новый документ, у него еще нет физической записи в базе данных. Метод Ссылка.ПриходныйОрдер вернет пустое значение. При этом условие сравнения может сработать некорректно, попытавшись записать пустоту в историю изменений.
  2. Просадка производительности СУБД (Обращение через точку): Конструкция Ссылка.ИмяРеквизита заставляет платформу считывать весь объект целиком из базы данных. Если ваш документ содержит огромную табличную часть (сотни или тысячи строк), такое обращение приведет к полной загрузке объекта в память сервера приложений и БД, что существенно замедлит запись.
  3. Повторное дублирование при ошибках транзакции: Если при записи или проведении возникнет ошибка (например, сработает блокировка или отказ в подписке на события), транзакция откатится, но данные на форме останутся измененными. При повторном нажатии кнопки «Записать» код сработает еще раз, продублировав значение в истории.

Выясним, как написать код правильно. Для оптимизации мы исключим новые документы с помощью функции ЭтоНовый(), а старое значение получим быстрым точечным запросом к СУБД. Это гарантирует максимальное быстродействие.

Рассмотрим более надежный и профессиональный вариант кода для модуля объекта:


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

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

Вариант 2. Интерактивное отслеживание в событии формы ПриИзменении

Если вам требуется выводить историю изменений мгновенно на форму еще до того, как пользователь нажал кнопку «Записать», логику необходимо перенести непосредственно в модуль формы. Для этого используется событие ПриИзменении соответствующего поля ввода.

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

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

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


Перем СтароеЗначениеНомера;

Процедура ПриОткрытии()
    // Фиксируем значение реквизита на момент открытия документа
    СтароеЗначениеНомера = ЭтотОбъект.ПриходныйОрдер;
КонецПроцедуры

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

Использование событий формы делает интерфейс более интерактивным, однако помните, что если пользователь закроет документ без сохранения, история изменений на форме сбросится и не попадет в базу данных, что логично для транзакционной модели 1С.

Архитектурно правильные альтернативы (Best Practices)

Накапливание истории изменений в плоскую текстовую строку через запятую — это рабочий, но довольно примитивный подход, который накладывает серьезные ограничения. По такой строке невозможно построить структурированный аналитический отчет, нельзя узнать, какой именно пользователь внес изменения, а также исключить случайное ручное стирание данных.

Если вы хотите реализовать задачу на уровне современных стандартов разработки 1С, рекомендуем рассмотреть две альтернативы:

А. Использование типового версионирования объектов БСП

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

Чтобы задействовать этот механизм, достаточно выполнить несколько простых шагов в режиме 1С:Предприятие:

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

Б. Реализация подчиненного регистра сведений

Если версионирование всего документа для вас избыточно, правильным решением с точки зрения архитектуры баз данных будет создание подчиненного Регистра сведений (например, ИсторияИзмененияПараметровДокументов).

Структура такого регистра должна быть следующей:

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

← На главную