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