Как изменить реквизит справочника из документа в 1С 8.3: техническое решение и архитектурный разбор

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

В процессе разработки в среде «1С:Предприятие» перед начинающими программистами нередко встает задача: изменить реквизит элемента справочника при проведении или записи документа. Типичный пример — документ УстановкаЦенМ, который должен зафиксировать цену на номенклатуру (материал) из справочника Материалы.

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

Часть 1. Техническое решение: как изменить реквизит справочника из кода

Если перед вами стоит исключительно учебная или узкоспециализированная задача, где необходимо напрямую перезаписать реквизит справочника из модуля документа, давайте разберем, как сделать это правильно.

Для начала проанализируем пример кода, который часто предлагают на форумах (включая сообщение №7):


Procedure ChangeSpravochnik ()
   Spravochnik = Справочники.Номенклатура.найтиПоКоду("0000000001").ПолучитьОбъект();
   Spravochnik.Name = "ddd " + ТекущаяДата();
   Spravochnik.Save();
EndProcedure

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

  1. Смешение языков программирования (двуязычие): Написание кода на «транслите» (Spravochnik) вперемешку с русскими и английскими операторами сильно затрудняет чтение и считается дурным тоном.
  2. Отсутствие метода Save(): В платформе 1С нет метода Save(). Для записи объекта используется метод Записать() (в русской нотации) или Write() (в английской).
  3. Отсутствие проверки на пустую ссылку: Метод НайтиПоКоду() возвращает ссылку на элемент. Если элемент с кодом "0000000001" не будет найден в базе данных, метод вернет пустую ссылку. Попытка вызвать метод ПолучитьОбъект() у пустой ссылки вызовет критическую ошибку системы: «Значение не является значением объектного типа (ПолучитьОбъект)».

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


// Процедура обновляет реквизит "Цена" в карточке материала
Процедура ОбновитьЦенуВСправочнике(СсылкаНаМатериал, НоваяЦена) Экспорт
    
    // Проверяем, что ссылка на элемент справочника корректна и заполнена
    Если ЗначениеЗаполнено(СсылкаНаМатериал) Тогда
        
        // Переходим от ссылки (СправочникСсылка) к объекту (СправочникОбъект), доступному для изменения
        МатериалОбъект = СсылкаНаМатериал.ПолучитьОбъект();
        
        // Применяем новое значение к реквизиту справочника
        МатериалОбъект.Цена = НоваяЦена;
        
        // Запись объекта в базу данных выполняем в блоке Попытка...Исключение
        Попытка
            МатериалОбъект.Записать();
        Исключение
            // Регистрируем ошибку записи в Журнале регистрации для последующего анализа администратором
            ЗаписьЖурналаРегистрации("ИзменениеРеквизитаСправочника",
                УровеньЖурналаРегистрации.Ошибка,
                МатериалОбъект.Метаданные(),
                СсылкаНаМатериал,
                "Не удалось записать элемент: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
                
            // Передаем ошибку дальше, чтобы отменить транзакцию проведения документа
            ВызватьИсключение "Ошибка при обновлении цены в справочнике материалов!";
        КонецПопытки;
        
    КонецЕсли;
    
КонецПроцедуры

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

Часть 2. Почему прямое изменение справочника документом — это антипаттерн?

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

Проблема 1: Отсутствие истории изменений и нарушение временной оси

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

Если вы записываете цену непосредственно в реквизит справочника Материалы, вы перезаписываете текущее значение. В результате система «забывает» историю. Если завтра вам потребуется перепровести документ поступления материалов месячной давности или сформировать отчет по старым ценам, система подставит новую цену из справочника. Это приведет к полной потере достоверности ретроспективных данных и искажению отчетов.

Проблема 2: Коллапс при отмене проведения документа

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

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

Проблема 3: Блокировки базы данных (Deadlocks) при параллельной работе

Запись элемента справочника (`Записать()`) внутри транзакции проведения документа накладывает транзакционную блокировку СУБД на соответствующую запись таблицы справочника. Если несколько пользователей одновременно проводят документы, изменяющие одни и те же элементы справочника, возникнет конфликт блокировок. В худшем случае это приведет к взаимным блокировкам (Deadlocks) и аварийной остановке работы пользователей.

Часть 3. Правильное архитектурное решение: Регистры сведений

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

Выясним по шагам, как правильно реализовать задачу установки цен:

  1. Создадим новый объект метаданных — периодический регистр сведений ЦеныМатериалов (с периодичностью, например, «В пределах дня» или «В пределах секунды»). Режим записи установим в значение «Подчинен регистратору».
  2. В качестве измерения (Dimension) регистра укажем Материал (тип: СправочникСсылка.Материалы).
  3. В качестве ресурса (Resource) укажем Цена (тип: Число, длина 15, точность 2).
  4. Назначим наш документ УстановкаЦенМ регистратором для этого регистра сведений.

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


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    
    // Включаем запись движений по регистру "ЦеныМатериалов"
    Движения.ЦеныМатериалов.Записывать = Истина;
    
    // Очищаем старые движения документа по этому регистру (если они были)
    Движения.ЦеныМатериалов.Очистить();
    
    // Обходим табличную часть документа "Материалы"
    Для Каждого СтрокаТЧ Из Товары Цикл
        
        // Создаем новую запись в наборе записей регистра
        НоваяЗапись = Движения.ЦеныМатериалов.Добавить();
        
        // Заполняем ключевые поля записи
        НоваяЗапись.Период      = Дата; // Дата документа определяет точку на оси времени
        НоваяЗапись.Материал    = СтрокаТЧ.Материал;
        НоваяЗапись.Цена        = СтрокаТЧ.Цена;
        
    КонецЦикла;
    
КонецПроцедуры

Как прочитать актуальную цену на любую дату?

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


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ЦеныМатериаловСрезПоследних.Материал КАК Материал,
    |	ЦеныМатериаловСрезПоследних.Цена КАК АктуальнаяЦена
    |ИЗ
    |	РегистрСведений.ЦеныМатериалов.СрезПоследних(&Период, Материал = &Материал) КАК ЦеныМатериаловСрезПоследних";

Запрос.УстановитьПараметр("Период", ТекущаяДата());
Запрос.УстановитьПараметр("Материал", ВыбранныйМатериал);

РезультатЗапроса = Запрос.Выполнить();

Преимущества архитектурного подхода с регистром сведений:

← На главную