Почему возникает ошибка чтения значения при проведении документа и как правильно настроить контроль остатков в 1С

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

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

Анализ исходного программного кода проведения

Рассмотрим типовой сценарий проведения документа РасходнаяНакладная по регистру накопления ОстаткиНоменклатуры с контролем отрицательных остатков, который часто вызывает затруднения:


// 1. Формирование движений по регистру
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	РасходнаяНакладнаяСписокНоменклатуры.Номенклатура,
    |	СУММА(РасходнаяНакладнаяСписокНоменклатуры.Количество) КАК Количество
    |ИЗ
    |	Документ.РасходнаяНакладная.СписокНоменклатуры КАК РасходнаяНакладнаяСписокНоменклатуры
    |ГДЕ
    |	РасходнаяНакладнаяСписокНоменклатуры.Ссылка = &Ссылка
    |	И РасходнаяНакладнаяСписокНоменклатуры.Номенклатура.ВидНоменклатуры = &ВидНоменклатуры
    |СГРУППИРОВАТЬ ПО
    |	РасходнаяНакладнаяСписокНоменклатуры.Номенклатура";

Запрос.УстановитьПараметр("ВидНоменклатуры", Перечисления.ВидыНоменклатуры.Товар);
Запрос.УстановитьПараметр("Ссылка", Ссылка);

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

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

Движения.ОстаткиНоменклатуры.БлокироватьДляИзменения = Истина;
Движения.ОстаткиНоменклатуры.Записать();

// 2. Контроль остатков
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ОстаткиНоменклатурыОстатки.Номенклатура,
    |	ОстаткиНоменклатурыОстатки.КоличествоОстаток,
    |	ОстаткиНоменклатурыОстатки.Номенклатура.Представление
    |ИЗ
    |	РегистрНакопления.ОстаткиНоменклатуры.Остатки(
    |			&Граница,
    |			Номенклатура В (&СписокТоваров)
    |				И Склад = &Склад) КАК ОстаткиНоменклатурыОстатки
    |ГДЕ
    |	ОстаткиНоменклатурыОстатки.КоличествоОстаток < 0";

Запрос.УстановитьПараметр("Граница", Новый Граница(МоментВремени));
Запрос.УстановитьПараметр("Склад", Склад);
Запрос.УстановитьПараметр("СписокТоваров", СписокНоменклатуры.ВыгрузитьКолонку("Номенклатура"));

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

Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
    Сообщение = Новый СообщениеПользователю;
    Сообщение.Текст = "По номенклатуре " + ВыборкаДетальныеЗаписи.Номенклатура.Представление + " не хватает товара. Перерасход: " + -ВыборкаДетальныеЗаписи.КоличествоОстаток;
    Сообщение.Сообщить();
    Отказ = Истина;
КонецЦикла;

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

Причина 1. Пустые данные из-за незаполненных реквизитов в пользовательском режиме

Первая и самая распространенная причина, по которой запрос не формирует движений — это несовпадение условий отбора в секции ГДЕ. Посмотрим на условие:


ГДЕ
    РасходнаяНакладнаяСписокНоменклатуры.Ссылка = &Ссылка
    И РасходнаяНакладнаяСписокНоменклатуры.Номенклатура.ВидНоменклатуры = &ВидНоменклатуры

Если в пользовательском режиме для позиции номенклатуры в справочнике Номенклатура реквизит ВидНоменклатуры не был заполнен (содержит ЗначениеНеЗаполнено или Неопределено), то условие через точку вернет Ложь. В результате:

  1. Запрос возвращает совершенно пустую выборку.
  2. Цикл Пока ВыборкаДетальныеЗаписи.Следующий() Цикл не выполняется ни разу.
  3. В регистр записывается пустой набор записей, и списание не происходит.

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

Причина 2. Ошибка обращения к представлению через точку во встроенном языке

В тексте второго запроса выбрано поле ОстаткиНоменклатурыОстатки.Номенклатура.Представление, а при выводе сообщения используется обращение:


Сообщение.Текст = "По номенклатуре " + ВыборкаДетальныеЗаписи.Номенклатура.Представление + " ...";

Проанализируем, почему это приводит к ошибке выполнения:

  1. В языке запросов 1С конструкция Номенклатура.Представление без явного псевдонима формирует колонку с автопсевдонимом НоменклатураПредставление.
  2. В цикле встроенного языка обращение ВыборкаДетальныеЗаписи.Номенклатура возвращает объект типа СправочникСсылка.Номенклатура.
  3. У ссылки нет свойства Представление. Попытка обратиться к нему через точку вызывает исключение времени выполнения: «Поле объекта не обнаружено (Представление)» или внутреннюю ошибку чтения объекта.

Правильные варианты исправления:

Вариант А: Задать явный псевдоним в запросе и обращаться к колонке результата:


// В тексте запроса:
// ОстаткиНоменклатурыОстатки.Номенклатура.Представление КАК НоменклатураПредставление

Сообщение.Текст = "По номенклатуре " + ВыборкаДетальныеЗаписи.НоменклатураПредставление + " не хватает товара...";

Вариант Б: Использовать стандартное приведение ссылки к строковому виду встроенной функцией Строка():


Сообщение.Текст = "По номенклатуре " + Строка(ВыборкаДетальныеЗаписи.Номенклатура) + " не хватает товара...";

Причина 3. Некорректное создание объекта «Граница» и вызов метода МоментВремени

Посмотрим на строку установки границы времени для виртуальной таблицы остатков:

Запрос.УстановитьПараметр("Граница", Новый Граница(МоментВремени));

Здесь допущены сразу две ошибки:

  1. МоментВремени() — это метод документа, а не свойство. Без круглых скобок передается не сам момент времени, что вызовет синтаксическую ошибку или передачу некорректного типа в конструктор.
  2. Поскольку мы используем методику проверки остатков после записи движений текущего документа, нам необходимо видеть остатки с учетом движений текущего регистратора. По умолчанию конструктор Новый Граница(...) использует вид границы ВидГраницы.Исключая. Это означает, что движения текущей накладной просто не попадут в виртуальную таблицу!

Как исправить: Обязательно вызываем метод со скобками и явно указываем включение момента времени:


Запрос.УстановитьПараметр("Граница", Новый Граница(МоментВремени(), ВидГраницы.Включая));

Причина 4. Избыточная выборка номенклатуры при проверке

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


Запрос.УстановитьПараметр("СписокТоваров", СписокНоменклатуры.ВыгрузитьКолонку("Номенклатура"));

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

Эталонный вариант: Проведение с использованием новой методики и пакетного запроса

Рассмотрим современный, оптимизированный и надежный вариант проведения документа. Объединим выборку данных документа, блокировку и расчет нехватки в один эффективный алгоритм с управляемыми блокировками:


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    
    // 1. Очищаем старые движения
    Движения.ОстаткиНоменклатуры.Очистить();
    Движения.ОстаткиНоменклатуры.Записывать = Истина;
    
    // 2. Устанавливаем управляемую блокировку данных
    Блокировка = Новый БлокировкаДанных;
    ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ОстаткиНоменклатуры");
    ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
    ЭлементБлокировки.УстановитьЗначение("Склад", Склад);
    ЭлементБлокировки.ИсточникДанных = СписокНоменклатуры;
    ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
    Блокировка.Заблокировать();
    
    // 3. Выполняем пакетный запрос для получения данных документа и текущих остатков
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Док.Номенклатура КАК Номенклатура,
        |	Док.Номенклатура.Представление КАК НоменклатураПредставление,
        |	СУММА(Док.Количество) КАК Количество
        |ПОМЕСТИТЬ ВТ_ТоварыДокумента
        |ИЗ
        |	Документ.РасходнаяНакладная.СписокНоменклатуры КАК Док
        |ГДЕ
        |	Док.Ссылка = &Ссылка
        |	И Док.Номенклатура.ВидНоменклатуры = &ВидНоменклатуры
        |СГРУППИРОВАТЬ ПО
        |	Док.Номенклатура,
        |	Док.Номенклатура.Представление
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |	ВТ.Номенклатура КАК Номенклатура,
        |	ВТ.НоменклатураПредставление КАК НоменклатураПредставление,
        |	ВТ.Количество КАК КоличествоВДокументе,
        |	ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК КоличествоОстаток
        |ИЗ
        |	ВТ_ТоварыДокумента КАК ВТ
        |	ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиНоменклатуры.Остатки(
        |			&МоментКонтроля,
        |			Склад = &Склад
        |				И Номенклатура В (ВЫБРАТЬ ВТ_ТоварыДокумента.Номенклатура ИЗ ВТ_ТоварыДокумента)) КАК Остатки
        |	ПО ВТ.Номенклатура = Остатки.Номенклатура";
    
    Запрос.УстановитьПараметр("Ссылка", Ссылка);
    Запрос.УстановитьПараметр("Склад", Склад);
    Запрос.УстановитьПараметр("ВидНоменклатуры", Перечисления.ВидыНоменклатуры.Товар);
    Запрос.УстановитьПараметр("МоментКонтроля", Новый Граница(МоментВремени(), ВидГраницы.Исключая));
    
    РезультатЗапроса = Запрос.Выполнить();
    Выборка = РезультатЗапроса.Выбрать();
    
    // 4. Проверяем остатки и формируем движения
    Пока Выборка.Следующий() Цикл
        
        // Проверка нехватки остатка
        Если Выборка.КоличествоОстаток < Выборка.КоличествоВДокументе Тогда
            Нехватка = Выборка.КоличествоВДокументе - Выборка.КоличествоОстаток;
            
            Сообщение = Новый СообщениеПользователю;
            Сообщение.Текст = "Недостаточно товара: " + Выборка.НоменклатураПредставление + 
                              ". Требуется: " + Выборка.КоличествоВДокументе + 
                              ", в наличии: " + Выборка.КоличествоОстаток + 
                              ", не хватает: " + Нехватка;
            Сообщение.Поле = "СписокНоменклатуры";
            Сообщение.УстановитьДанные(ЭтотОбъект);
            Сообщение.Сообщить();
            
            Отказ = Истина;
        КонецЕсли;
        
        // Если ошибок нет — формируем движение расхода
        Если НЕ Отказ Тогда
            Движение = Движения.ОстаткиНоменклатуры.Добавить();
            Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
            Движение.Период = Дата;
            Движение.Склад = Склад;
            Движение.Номенклатура = Выборка.Номенклатура;
            Движение.Количество = Выборка.КоличествоВДокументе;
        КонецЕсли;
        
    КонецЦикла;
    
КонецПроцедуры

Итоги и рекомендации

Подведем итоги разбора проблемы:

  1. Если запрос не возвращает строк — в первую очередь проверяем значения реквизитов в базе данных, участвующих в секции ГДЕ.
  2. Для получения читаемого наименования номенклатуры в запросе используем псевдонимы (Номенклатура.Представление КАК НоменклатураПредставление) либо функцию Строка(Ссылка) в коде.
  3. Всегда вызываем МоментВремени() со скобками и четко контролируем вид границы: ВидГраницы.Исключая — при контроле до записи, ВидГраницы.Включая — при контроле после записи.
  4. Используем новую методику проведения с управляемыми блокировками: это исключает лишнюю запись движений при нехватке товара и предотвращает блокировки параллельных сеансов пользователей.
← На главную