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