Как реализовать блокировку изменения данных в регистре сведений по времени и избежать типичных ошибок проектирования?

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

Разработка механизмов контроля и ограничения изменения данных во времени — частая задача в корпоративных информационных системах на платформе 1С. Бизнес-логика нередко требует ограничить редактирование важных реквизитов (например, материально-ответственных лиц на складе) жесткими временными рамками. Например, разрешить изменение только один раз в сутки в определенный интервал времени, а после этого заблокировать повторную перезапись до наступления следующего рабочего дня. Чтобы автоматизировать выполнение подобных сценариев в интерфейсе без риска ошибок ручного ввода, используется платформа для создания программных роботов.

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

Типичное заблуждение №1: Настройка периодичности регистра «В день» спасет от повторной записи

Рассмотрим популярный вопрос: если установить у периодического регистра сведений периодичность «В день», ограничит ли это возможность изменения данных в течение дня?

Выясним причину, почему это не работает так, как ожидает разработчик:

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

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

Типичное заблуждение №2: Ошибка проектирования структуры («Измерения» вместо «Ресурсов»)

Проанализируем структуру регистра, которую часто закладывают на этапе проектирования: два измерения — Склад (Изм1) и МатериальноОтветственноеЛицо (Изм2).

В чем заключается фатальная ошибка такого подхода? Если поле МатериальноОтветственноеЛицо (МОЛ) назначено измерением, то уникальный ключ записи формируется из комбинации значений обоих полей: Склад + МОЛ.

Давайте разберем поведение системы на примере:

  1. На Складе №1 ответственным был Иванов. В регистре существует запись: Склад №1 + Иванов.
  2. В разрешенный интервал времени мы меняем ответственного на Петрова. С точки зрения платформы 1С создается абсолютно новая запись с ключом Склад №1 + Петров.
  3. Прежняя запись с Ивановым никуда не исчезает и не перезаписывается — она продолжает находиться в базе данных, так как у нее другой уникальный ключ.

Правильное решение: Склад должен быть назначен Измерением (так как это разрез учета), а МОЛ должен выступать в роли Ресурса (так как это изменяемое значение). В таком случае при периодичности «День» на конкретном Складе в один день может быть назначен строго один МОЛ. При попытке смены МОЛ запись перезапишется, и именно этот процесс перезаписи нам необходимо перехватить и проконтролировать программно.

Типичное заблуждение №3: Блокировка редактирования в форме («ПриОткрытии»)

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


&НаКлиенте
Процедура ПриОткрытии(Отказ)
    Блок();
КонецПроцедуры

&НаСервере
Процедура Блок()
    Форма = Элементы.МатериальноОтветственноеЛицо;
    ЭтаФорма.ТолькоПросмотр = Истина;
    ДатаБлок = ТекущаяДата();
    ДатаБлок1 = НачалоДня(ДатаБлок) + 28800; // 8:00 утра
    ДатаБлок2 = НачалоДня(ДатаБлок) + 39600; // 11:00 утра
    
    Если ЭтаФорма.Запись.МатериальноОтветственноеЛицо.Пустая() Тогда
        ЭтаФорма.ТолькоПросмотр = Ложь;
    Иначе
        Если (ДатаБлок > ДатаБлок1 И ДатаБлок < ДатаБлок2) Тогда
            ЭтаФорма.ТолькоПросмотр = Ложь;
        Иначе
            ЭтаФорма.ТолькоПросмотр = Истина;
        КонецЕсли;
    КонецЕсли;
КонецПроцедуры

Разберем по шагам, почему этот код не решает поставленную задачу и небезопасен для бизнес-логики:

  1. Отсутствие памяти состояния. Представим ситуацию: пользователь зашел в форму в 9:00 (время разрешено), изменил МОЛ на Петрова и сохранил запись. Спустя минуту, в 9:01, он снова открывает эту же форму. Текущее время по-прежнему находится в интервале между 8:00 и 11:00. Код в процедуре ПриОткрытии успешно отрабатывает и вновь разрешает редактирование. Форма «не помнит», что изменение в эти сутки уже состоялось.
  2. Уязвимость обхода интерфейса. Установка свойства ТолькоПросмотр = Истина защищает данные исключительно от интерактивного изменения пользователем непосредственно в данной конкретной форме. Если запись в регистр будет производиться программно (из других документов, через групповое изменение реквизитов, фоновые задания или внешние интеграции), данный код просто не выполнится. База данных окажется незащищенной от некорректных изменений.

Правильная архитектура: Контроль транзакций на уровне базы данных

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

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

Пошаговая программная реализация блокировки по времени

Рассмотрим детальный алгоритм работы нашего будущего обработчика событий:

  1. При попытке любой записи (интерактивной или программной) мы считываем текущее состояние данных в СУБД до их перезаписи.
  2. Извлекаем значение поля ДатаПоследнегоИзменения.
  3. Рассчитываем точную дату и время разблокировки. По условию, блокировка должна действовать до 8:00 утра следующего дня. Формула расчета: к дате последнего изменения прибавляем 24 часа (86 400 секунд), берем начало полученного дня и прибавляем 8 часов (28 800 секунд).
  4. Сравниваем текущее время сервера с расчетным временем разблокировки. Если текущее время меньше времени разблокировки, прерываем транзакцию записи с установкой параметра Отказ = Истина и выводом предупреждения пользователю.
  5. Если проверка пройдена успешно (блокировки нет или ее срок истек), перед непосредственной записью в базу фиксируем текущее серверное время в поле ДатаПоследнегоИзменения.

Перейдем к реализации кода. Откроем модуль набора записей регистра сведений и напишем следующий обработчик:


Процедура ПередЗаписью(Отказ, Замещение)
    
    // Если запись происходит в процессе обмена данными, проверки обычно отключают
    Если ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    Для Каждого Запись Из ЭтотОбъект Цикл
        
        // 1. Проверяем заполненность ключевого измерения "Склад"
        Если Не ЗначениеЗаполнено(Запись.Склад) Тогда
            Продолжить;
        КонецЕсли;
        
        // 2. Запрашиваем из базы данных текущие (еще не измененные) значения ресурса и даты записи
        Запрос = Новый Запрос;
        Запрос.Текст = 
            "ВЫБРАТЬ
            |   ОтветственныеЛицаСрезаПоследних.МатериальноОтветственноеЛицо КАК МОЛ_Старый,
            |   ОтветственныеЛицаСрезаПоследних.ДатаПоследнегоИзменения КАК ДатаИзменения_Старая
            |ИЗ
            |   РегистрСведений.ОтветственныеЛица.СрезПоследних(&ПериодПроверки, Склад = &Склад) КАК ОтветственныеЛицаСрезаПоследних";
        
        // Используем текущее серверное время
        ТекущееВремяСервера = ТекущаяДата();
        
        Запрос.УстановитьПараметр("ПериодПроверки", ТекущееВремяСервера);
        Запрос.УстановитьПараметр("Склад", Запись.Склад);
        
        РезультатЗапроса = Запрос.Выполнить();
        Если Не РезультатЗапроса.Пустой() Тогда
            Выборка = РезультатЗапроса.Выбрать();
            Выборка.Следующий();
            
            // Если материально-ответственное лицо не меняется, блокировку проверять не нужно
            Если Выборка.МОЛ_Старый = Запись.МатериальноОтветственноеЛицо Тогда
                Продолжить;
            КонецЕсли;
            
            // Если дата предыдущего изменения зафиксирована, рассчитываем время блокировки
            Если ЗначениеЗаполнено(Выборка.ДатаИзменения_Старая) Тогда
                
                // Рассчитываем 8:00 утра следующего дня после даты изменения
                СекундВСутках = 86400;
                СекундДоВосьмиУтра = 28800;
                
                ДатаРазблокировки = НачалоДня(Выборка.ДатаИзменения_Старая + СекундВСутках) + СекундДоВосьмиУтра;
                
                Если ТекущееВремяСервера < ДатаРазблокировки Тогда
                    Отказ = Истина;
                    
                    // Формируем понятное сообщение для пользователя
                    ШаблонСообщения = "Внимание! Изменение ответственного лица по складу %1 заблокировано до %2.";
                    ТекстСообщения = СтрШаблон(
                        ШаблонСообщения, 
                        Запись.Склад, 
                        Формат(ДатаРазблокировки, "ДФ='дд.ММ.гггг в ЧЧ:мм'")
                    );
                    
                    ВызватьИсключение ТекстСообщения;
                КонецЕсли;
                
            КонецЕсли;
        КонецЕсли;
        
        // 3. Записываем системное время изменения, если все проверки пройдены
        Запись.ДатаПоследнегоИзменения = ТекущееВремяСервера;
        
    КонецЦикла;
    
КонецПроцедуры

Преимущества разработанного серверного подхода

Проанализируем преимущества архитектурного решения, которое мы внедрили:

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

← На главную