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