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

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

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

Методология решения задачи

Существует два основных подхода к решению этой задачи. Первый — отлавливать события ПриИзменении в каждой колонке и событие ПередУдалением в табличной части. Этот путь трудозатратен и сложен в поддержке. Мы разберем второй, более элегантный подход: сравнение состояния документа непосредственно перед его записью. Это позволит нам один раз проанализировать все изменения и выдать пользователю единое окно для указания причины.

Шаг 1: Подготовка структуры метаданных

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

  1. Справочник "ПричиныИзменений": Простой справочник, где будут храниться наименования причин (например, "Ошибка ввода", "Отказ клиента", "Бой товара").
  2. Регистр сведений "ИсторияИзмененийДокументов":
    • Измерения: Документ (Тип: ДокументСсылка), ДатаИзменения (Тип: Дата), Пользователь (Тип: СправочникСсылка.Пользователи).
    • Ресурсы: Причина (Тип: СправочникСсылка.ПричиныИзменений), ОписаниеИзменений (Тип: Строка).

Шаг 2: Получение различий в данных

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

В процедуре на сервере мы выгрузим текущую табличную часть в ТаблицаЗначений и передадим её в запрос в качестве параметра. Посмотрим на реализацию логики сравнения:


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

    Запрос.УстановитьПараметр("ТЗ_Товары", Объект.Товары.Выгрузить());
    Запрос.УстановитьПараметр("Ссылка", Объект.Ссылка);
    
    Результат = Запрос.Выполнить().Выгрузить();
    
    Если Результат.Количество() = 0 Тогда
        Возврат "";
    КонецЕсли;
    
    ТекстРазличий = "";
    Для Каждого Стр Из Результат Цикл
        ТекстРазличий = ТекстРазличий + Стр.Статус + ": " + Стр.Номенклатура + " (" + Стр.Количество + "); ";
    КонецЦикла;
    
    Возврат ТекстРазличий;
    
КонецФункции

Шаг 3: Реализация диалога с пользователем (Асинхронность)

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


&НаКлиенте
Процедура ПередЗаписью(Отказ, ПараметрыЗаписи)
    
    // Если причина уже заполнена в текущем сеансе записи, не спрашиваем повторно
    Если ПараметрыЗаписи.Свойство("ПричинаВыбрана") Тогда
        Возврат;
    КонецЕсли;
    
    ТекстИзменений = ПолучитьТекстРазличий();
    
    Если ЗначениеЗаполнено(ТекстИзменений) Тогда
        Отказ = Истина; // Прерываем стандартную запись
        
        // Подготавливаем структуру для передачи в форму выбора
        ПараметрыФормы = Новый Структура("ВыборПричины", Истина);
        ОписаниеОповещения = Новый ОписаниеОповещения("ПослеВыбораПричины", ЭтотОбъект, ПараметрыЗаписи);
        
        // Открываем форму выбора причины
        ОткрытьФорму("Справочник.ПричиныИзменений.ФормаВыбора", ПараметрыФормы, ЭтотОбъект, , , , ОписаниеОповещения);
    КонецЕсли;
    
КонецПроцедуры

&НаКлиенте
Процедура ПослеВыбораПричины(РезультатВыбора, ДополнительныеПараметры) Экспорт
    
    Если РезультатВыбора <> Неопределено Тогда
        // Запоминаем выбранную причину в объекте через дополнительные свойства или реквизит
        Объект.ДополнительныеСвойства.Вставить("ВыбраннаяПричина", РезультатВыбора);
        
        // Добавляем флаг в параметры записи, чтобы не войти в рекурсию
        ДополнительныеПараметры.Вставить("ПричинаВыбрана", Истина);
        
        // Повторно вызываем запись
        ЭтотОбъект.Записать(ДополнительныеПараметры);
    Иначе
        ПоказатьПредупреждение(, "Без указания причины изменения не могут быть сохранены!");
    КонецЕсли;
    
КонецПроцедуры

Шаг 4: Сохранение данных в регистр сведений

Выясним причину, почему финальную запись в регистр лучше делать в процедуре ПриЗаписи на сервере. Это гарантирует транзакционность: если сам документ не сможет записаться (например, не хватит остатков на складе), то и запись о причине изменения не появится в регистре, что исключит появление "мусорных" данных.

Разберем, как передать причину с клиента на сервер. Мы использовали ДополнительныеСвойства. Посмотрим на серверный код:


&НаСервере
Процедура ПриЗаписи(Отказ)
    
    Если Объект.ДополнительныеСвойства.Свойство("ВыбраннаяПричина") Тогда
        
        Причина = Объект.ДополнительныеСвойства.ВыбраннаяПричина;
        ТекстЛога = ПолучитьТекстРазличий(); // Снова получаем текст изменений для записи
        
        МенеджерЗаписи = РегистрыСведений.ИсторияИзмененийДокументов.СоздатьМенеджерЗаписи();
        МенеджерЗаписи.Период = ТекущаяДата();
        МенеджерЗаписи.Документ = Объект.Ссылка;
        МенеджерЗаписи.Пользователь = Пользователи.ТекущийПользователь();
        МенеджерЗаписи.Причина = Причина;
        МенеджерЗаписи.ОписаниеИзменений = ТекстЛога;
        
        Попытка
            МенеджерЗаписи.Записать();
        Исключение
            ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Ошибка при записи лога изменений: " + КраткоеОписаниеОшибки(ИнформацияОбОшибке()));
        КонецПопытки;
        
    КонецЕсли;
    
КонецПроцедуры

Важные рекомендации и оптимизация

Рассмотрим несколько нюансов, которые помогут сделать систему более надежной:

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

← На главную