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

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

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

Разберем архитектурные подходы и практические способы решения этой задачи с помощью дополнительного независимого регистра сведений и механизмов платформы 1С:Предприятие.

Архитектура решения: структура регистров

Для разделения оперативных данных и исторического аудита обычно используют два регистра:

  1. Основной регистр (текущее состояние): может быть подчинен регистратору или периодическим. Хранит актуальный статус для быстрого доступа.
  2. Независимый регистр сведений (история статусов): имеет периодичность (по позиции регистратора или по времени, например, Секунда) и содержит следующие ключевые поля:
    1. Измерения: Документ (ссылка на целевой документ).
    2. Реквизиты/Ресурсы: Статус, Ответственный (пользователь, выполнивший действие), ДокументИсточник (документ-регистратор события), Комментарий.

Вариант 1. Запись истории через подписку на событие набора записей основного регистра

Рассмотрим вариант, когда фиксация истории должна происходить автоматически при обновлении движений в основном регистре сведений. Для этого создадим подписку на событие ПриЗаписи для объекта РегистрСведенийНаборЗаписей.<ИмяОсновногоРегистра>.

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

Посмотрим на пример обработчика в общем модуле:


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

Вариант 2. Пакетная проверка и запись истории через запрос

Если основной регистр записывается таблицами с несколькими строками одновременно, одиночные запросы в цикле создадут повышенную нагрузку на СУБД. В такой ситуации эффективнее выгрузить набор в таблицу и сопоставить срез последних одним пакетным запросом.

Разберем реализацию с вычислением признака изменения состава полей:


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

Вариант 3. Использование событий объекта документа с передачей флага изменения

Альтернативный способ исключает лишние обращения к виртуальным таблицам при записи. Сравнение статуса можно выполнить между объектом в памяти и его ссылкой в базе данных:

  1. В обработчике ПередЗаписью документа проверяем условие ЭтотОбъект.Статус <> ЭтотОбъект.Ссылка.Статус.
  2. Если статус изменился, помещаем признак в свойство объекта: ЭтотОбъект.ДополнительныеСвойства.Вставить("СтатусИзменен", Истина).
  3. В обработчике ПриЗаписи проверяем наличие этого флага в ДополнительныеСвойства и выполняем быструю вставку новой строки в регистр истории через СоздатьМенеджерЗаписи().

Важные технические нюансы и рекомендации

  1. Точность времени и коллизии ключей: периодичность независимого регистра сведений ограничена 1 секундой. При пакетной смене статусов или быстрых повторных вызовах в течение одной секунды возникнет ошибка уникальности записи. Для надежности добавьте в состав измерений регистра истории поле УникальныйИдентификаторЗаписи (тип УникальныйИдентификатор или Число).
  2. Часовые пояса и серверное время: используйте функцию ТекущаяДатаСеанса() вместо ТекущаяДата(), чтобы время фиксации событий соответствовало часовому поясу пользователя, а не сервера 1С.
  3. Оптимизация параметров виртуальной таблицы: всегда передавайте отбор по документу непосредственно в параметры СрезПоследних(&Период, Документ = &Документ), а не в условие ГДЕ или ПО, чтобы база данных использовала индекс и не сканировала всю таблицу регистра.
  4. Флаг обмена данными: обязательно ставьте проверку Источник.ОбменДанными.Загрузка в начале всех процедур фиксации истории, чтобы избежать некорректных записей при синхронизации и переносе данных.
  5. Управляемые блокировки: при высокой параллельности работы пользователей устанавливайте явную исключительную блокировку на документ перед чтением среза последних во избежание взаимоблокировок (Deadlocks).
← На главную