В процессе разработки в среде «1С:Предприятие» перед начинающими программистами нередко встает задача: изменить реквизит элемента справочника при проведении или записи документа. Типичный пример — документ УстановкаЦенМ, который должен зафиксировать цену на номенклатуру (материал) из справочника Материалы.
В этой статье мы подробно разберем, как решить эту задачу чисто технически, проанализируем ошибки в коде, обсудим, почему прямое изменение реквизитов справочника документами считается грубым нарушением архитектурных стандартов 1С, и рассмотрим правильный, профессиональный вариант реализации с использованием периодических регистров сведений.
Если перед вами стоит исключительно учебная или узкоспециализированная задача, где необходимо напрямую перезаписать реквизит справочника из модуля документа, давайте разберем, как сделать это правильно.
Для начала проанализируем пример кода, который часто предлагают на форумах (включая сообщение №7):
Procedure ChangeSpravochnik ()
Spravochnik = Справочники.Номенклатура.найтиПоКоду("0000000001").ПолучитьОбъект();
Spravochnik.Name = "ddd " + ТекущаяДата();
Spravochnik.Save();
EndProcedure
В данном коде содержится сразу несколько критических ошибок, из-за которых программа выдаст исключение:
Spravochnik) вперемешку с русскими и английскими операторами сильно затрудняет чтение и считается дурным тоном.Save(). Для записи объекта используется метод Записать() (в русской нотации) или Write() (в английской).НайтиПоКоду() возвращает ссылку на элемент. Если элемент с кодом "0000000001" не будет найден в базе данных, метод вернет пустую ссылку. Попытка вызвать метод ПолучитьОбъект() у пустой ссылки вызовет критическую ошибку системы: «Значение не является значением объектного типа (ПолучитьОбъект)».Рассмотрим исправленный, безопасный и оптимизированный вариант кода на русском языке, который можно разместить, например, в модуле проведения документа:
// Процедура обновляет реквизит "Цена" в карточке материала
Процедура ОбновитьЦенуВСправочнике(СсылкаНаМатериал, НоваяЦена) Экспорт
// Проверяем, что ссылка на элемент справочника корректна и заполнена
Если ЗначениеЗаполнено(СсылкаНаМатериал) Тогда
// Переходим от ссылки (СправочникСсылка) к объекту (СправочникОбъект), доступному для изменения
МатериалОбъект = СсылкаНаМатериал.ПолучитьОбъект();
// Применяем новое значение к реквизиту справочника
МатериалОбъект.Цена = НоваяЦена;
// Запись объекта в базу данных выполняем в блоке Попытка...Исключение
Попытка
МатериалОбъект.Записать();
Исключение
// Регистрируем ошибку записи в Журнале регистрации для последующего анализа администратором
ЗаписьЖурналаРегистрации("ИзменениеРеквизитаСправочника",
УровеньЖурналаРегистрации.Ошибка,
МатериалОбъект.Метаданные(),
СсылкаНаМатериал,
"Не удалось записать элемент: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
// Передаем ошибку дальше, чтобы отменить транзакцию проведения документа
ВызватьИсключение "Ошибка при обновлении цены в справочнике материалов!";
КонецПопытки;
КонецЕсли;
КонецПроцедуры
Посмотрим на ключевые моменты этого кода. Для модификации данных мы обязательно преобразуем СправочникСсылка в тип СправочникОбъект с помощью метода ПолучитьОбъект(), поскольку ссылки в 1С предназначены только для чтения. Метод Записать() физически сохраняет изменения в СУБД.
Теперь выясним причину, по которой опытные разработчики категорически не рекомендуют использовать этот подход в реальных бизнес-приложениях. Рассмотрим три фундаментальные проблемы такого решения.
Справочники в системе 1С предназначены для хранения условно-постоянной информации (наименование, артикул, штрихкод, страна происхождения). Документы же отражают хозяйственные операции на оси времени.
Если вы записываете цену непосредственно в реквизит справочника Материалы, вы перезаписываете текущее значение. В результате система «забывает» историю. Если завтра вам потребуется перепровести документ поступления материалов месячной давности или сформировать отчет по старым ценам, система подставит новую цену из справочника. Это приведет к полной потере достоверности ретроспективных данных и искажению отчетов.
Проанализируем жизненный цикл документа. Документ можно провести, а затем отменить проведение. При использовании стандартных механизмов платформы (регистров) отмена проведения автоматически удаляет все движения документа.
Если же ваш документ напрямую изменил реквизит в карточке справочника, то при отмене проведения документа платформа не вернет реквизиту справочника его предыдущее значение. Платформа 1С не отслеживает историю изменений справочников на системном уровне. Чтобы вернуть старую цену, вам придется писать сложнейшие алгоритмы восстановления данных, что неэффективно и ненадежно.
Запись элемента справочника (`Записать()`) внутри транзакции проведения документа накладывает транзакционную блокировку СУБД на соответствующую запись таблицы справочника. Если несколько пользователей одновременно проводят документы, изменяющие одни и те же элементы справочника, возникнет конфликт блокировок. В худшем случае это приведет к взаимным блокировкам (Deadlocks) и аварийной остановке работы пользователей.
Для хранения информации, изменяющейся во времени (такой как цены, курсы валют, штатное расписание), в платформе «1С:Предприятие» предназначен специальный объект метаданных — Регистр сведений.
Выясним по шагам, как правильно реализовать задачу установки цен:
ЦеныМатериалов (с периодичностью, например, «В пределах дня» или «В пределах секунды»). Режим записи установим в значение «Подчинен регистратору».Материал (тип: СправочникСсылка.Материалы).Цена (тип: Число, длина 15, точность 2).УстановкаЦенМ регистратором для этого регистра сведений.Теперь при проведении документа мы не будем трогать карточку справочника напрямую. Документ сформирует движения (записи) в регистр сведений. Рассмотрим пример кода в модуле проведения документа:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// Включаем запись движений по регистру "ЦеныМатериалов"
Движения.ЦеныМатериалов.Записывать = Истина;
// Очищаем старые движения документа по этому регистру (если они были)
Движения.ЦеныМатериалов.Очистить();
// Обходим табличную часть документа "Материалы"
Для Каждого СтрокаТЧ Из Товары Цикл
// Создаем новую запись в наборе записей регистра
НоваяЗапись = Движения.ЦеныМатериалов.Добавить();
// Заполняем ключевые поля записи
НоваяЗапись.Период = Дата; // Дата документа определяет точку на оси времени
НоваяЗапись.Материал = СтрокаТЧ.Материал;
НоваяЗапись.Цена = СтрокаТЧ.Цена;
КонецЦикла;
КонецПроцедуры
Благодаря тому, что данные хранятся в периодическом регистре сведений, мы можем легко получить цену на любой момент времени с помощью виртуальной таблицы СрезПоследних. Посмотрим на пример запроса, который вернет актуальные цены:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныМатериаловСрезПоследних.Материал КАК Материал,
| ЦеныМатериаловСрезПоследних.Цена КАК АктуальнаяЦена
|ИЗ
| РегистрСведений.ЦеныМатериалов.СрезПоследних(&Период, Материал = &Материал) КАК ЦеныМатериаловСрезПоследних";
Запрос.УстановитьПараметр("Период", ТекущаяДата());
Запрос.УстановитьПараметр("Материал", ВыбранныйМатериал);
РезультатЗапроса = Запрос.Выполнить();
Преимущества архитектурного подхода с регистром сведений: