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