Как исправить ошибку "Значение поля "Номенклатура" не может быть пустым!" при программной записи цен номенклатуры в регистр сведений в 1С:Предприятии 8.3 УТ 11.4?

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

При программной работе с данными в 1С:Предприятии, особенно при записи в регистры сведений, мы часто сталкиваемся с необходимостью корректно заполнять все обязательные поля. Одной из распространённых ошибок является "Значение поля "Номенклатура" не может быть пустым!". Эта ошибка обычно возникает, когда одно из измерений регистра (в данном случае "Номенклатура") не получает значения перед попыткой записи. Давайте подробно разберем, почему возникает эта ошибка и как её эффективно исправить при работе с регистром сведений "Цены номенклатуры" в конфигурации Управление торговлей 11.4.

Понимание регистра сведений "Цены номенклатуры"

Прежде чем углубляться в код, важно понять структуру регистра сведений "Цены номенклатуры". Этот регистр является периодическим, что означает, что он хранит значения с привязкой к конкретной дате (периоду). Основными полями, которые определяют уникальность записи, являются измерения. В УТ 11.4 для регистра "Цены номенклатуры" это, как правило:

  1. Номенклатура (обязательное измерение, ссылка на справочник "Номенклатура").
  2. ВидЦены (обязательное измерение, ссылка на справочник "Виды цен").
  3. Период (системное периодическое измерение, дата, с которой цена действует).

Кроме измерений, регистр содержит ресурсы, которые хранят саму информацию (например, Цена, Валюта), и, возможно, реквизиты. В некоторых случаях регистр может быть подчинен регистратору (документу, который осуществляет запись), и тогда поле Регистратор становится обязательным для заполнения.

Анализ исходного кода и причины ошибки

Давайте рассмотрим исходный код, который вызвал ошибку:


НоменклатураТовар = Справочники.Номенклатура.НайтиПоНаименованию(Объект.Наименование);
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
               | ЦеныНоменклатурыСрезПоследних.Период КАК Период,
               | ЦеныНоменклатурыСрезПоследних.Регистратор КАК Регистратор,
               | ЦеныНоменклатурыСрезПоследних.Номенклатура КАК Номенклатура,
               | ЦеныНоменклатурыСрезПоследних.ВидЦены КАК ВидЦены,
               | ЦеныНоменклатурыСрезПоследних.Цена КАК Цена,
               | ЦеныНоменклатурыСрезПоследних.Упаковка КАК Упаковка,
               | ЦеныНоменклатурыСрезПоследних.Валюта КАК Валюта
               |ИЗ
               | РегистрСведений.ЦеныНоменклатуры.СрезПоследних КАК ЦеныНоменклатурыСрезПоследних
               |ГДЕ
               | ЦеныНоменклатурыСрезПоследних.Номенклатура = &НоменклатураТовар";
Запрос.УстановитьПараметр("НоменклатураТовар",НоменклатураТовар);
Результат = Запрос.Выполнить;
Выборка = Результат.Выбрать;

НаборЗаписей = РегистрыСведений.ЦеныНоменклатуры.СоздатьНаборЗаписей;

Пока Выборка.Следующий Цикл
    НаборЗаписей.Отбор.Регистратор.Установить(Выборка.Регистратор);
    НаборЗаписей.Прочитать;
    НаборЗаписей.Добавить; // <-- Проблема №1: Добавление записи не в том месте

    Для Каждого Запись Из НаборЗаписей Цикл // <-- Проблема №2: Итерация по набору после добавления одной пустой записи
        Для Каждого Строка Из Объект.ПроцЦ_ТабличнаяЧасть Цикл
            Запись.Номенклатура = НоменклатураТовар;
            Запись.Период = ТекущаяДата;
            Запись.ВидЦены = Строка.ВидЦены;
            Запись.Цена = Строка.Цена;
            Запись.Валюта = Строка.ВидЦены.ВалютаЦены;
            // Сообщения для отладки
        КонецЦикла;
        НаборЗаписей.Записать; // <-- Проблема №3: Запись всего набора внутри цикла по записям
    КонецЦикла;
КонецЦикла;

Давайте проанализируем основные проблемы в этом коде, которые приводят к ошибке "Значение поля "Номенклатура" не может быть пустым!":

  1. Неверное размещение НаборЗаписей.Добавить(): Оператор НаборЗаписей.Добавить() должен вызываться внутри цикла по тем данным, которые мы хотим записать. В исходном коде он вызывается один раз после НаборЗаписей.Прочитать(). Это создает одну (возможно, пустую, если не было данных по отбору) запись в наборе. Если мы хотим добавить несколько записей из табличной части, каждую из них необходимо отдельно добавлять в набор.
  2. Неправильная итерация по НаборЗаписей: Цикл Для Каждого Запись Из НаборЗаписей Цикл после НаборЗаписей.Добавить() содержит только что созданную пустую запись. Если в наборе уже были записи после Прочитать(), к ним добавляется ещё одна. В итоге все данные из внутренней табличной части перезаписывают одну и ту же запись (последнюю в наборе), а не создают новые. Более того, внешняя часть кода читает "СрезПоследних", но пытается записать данные, что не всегда корректно, если мы хотим работать с актуальными записями по определенному регистратору.
  3. Лишние циклы и перезапись: Внутренний цикл Для Каждого Строка Из Объект.ПроцЦ_ТабличнаяЧасть Цикл постоянно перезаписывает одну и ту же запись Запись в наборе. Это означает, что в итоге в запись попадут только данные из последней строки табличной части. Это не то, что мы ожидаем при записи множества цен.
  4. Несвоевременная запись набора: Вызов НаборЗаписей.Записать() внутри цикла Для Каждого Запись Из НаборЗаписей Цикл приводит к многократной записи одного и того же (не полностью сформированного) набора, что неэффективно и может вызывать проблемы с блокировками. Запись всего набора должна производиться только один раз после того, как все записи в нем сформированы.
  5. Неполное заполнение полей: Запрос выбирает 7 полей (Период, Регистратор, Номенклатура, ВидЦены, Цена, Упаковка, Валюта), но в коде мы заполняем только 5 (Номенклатура, Период, ВидЦены, Цена, Валюта). Если Регистратор или Упаковка являются измерениями или обязательными реквизитами в вашей конфигурации регистра "Цены номенклатуры", их незаполнение также приведет к ошибке. Особенно это касается Регистратора, если регистр подчинен регистратору.

Сообщения отладки Сообщить (...) показывают, что переменные внутри цикла получают правильные значения, но это не означает, что эти значения корректно попадают в *запись, которая будет сохранена*. Ошибка "Значение поля "Номенклатура" не может быть пустым!" указывает на то, что в момент вызова НаборЗаписей.Записать() по крайней мере одна из записей в наборе не имеет заполненного измерения Номенклатура.

Корректная работа с объектом "НаборЗаписей"

Для массовой записи, изменения или удаления записей в регистре сведений мы используем объект НаборЗаписей. Давайте разберем правильный порядок действий:

  1. Создание набора записей: НаборЗаписей = РегистрыСведений.ЦеныНоменклатуры.СоздатьНаборЗаписей(); Мы получаем пустой объект, который будет содержать записи для последующей работы.
  2. Установка отбора (опционально, но часто необходимо):
    
    НаборЗаписей.Отбор.Номенклатура.Установить(НоменклатураТовар);
    НаборЗаписей.Отбор.ВидЦены.Установить(ВидЦеныЗначение); // Если нужно очистить цены только для определенного вида цены
    
    Отбор определяет, какие записи будут затронуты при операциях Прочитать(), Очистить() или Записать(Истина). Если отбор не установлен, операции будут применяться ко всему регистру, что редко бывает нужно и очень опасно. Для регистра "Цены номенклатуры" часто необходимо установить отбор по Номенклатуре. Если мы хотим очистить старые записи только для конкретного регистратора, то отбор должен быть по Регистратору. Однако, если мы хотим просто установить новые цены, нам, как правило, нужно заменить все предыдущие записи по данной номенклатуре и виду цены на новые, начиная с текущей даты.
  3. Чтение и очистка существующих записей (если требуется полная замена):
    
    НаборЗаписей.Прочитать(); // Читаем все записи, соответствующие отбору
    НаборЗаписей.Очистить();  // Удаляем все прочитанные записи из текущего набора в памяти
    
    Этот шаг нужен, если вы хотите полностью заменить все существующие записи по определенному отбору на новые. Очистить() удаляет записи из *объекта* НаборЗаписей в памяти, но не из базы данных. Фактическое удаление произойдет при вызове Записать(Истина).
  4. Добавление новых записей: Запись = НаборЗаписей.Добавить(); Этот метод создает новую, пустую строку в наборе записей в памяти и возвращает объект МенеджерЗаписи для этой строки. Его нужно вызывать для каждой новой записи, которую мы хотим добавить.
  5. Заполнение полей новой записи:
    
    Запись.Номенклатура = НоменклатураТовар;
    Запись.Период = ТекущаяДата;
    // и так далее для всех измерений, ресурсов и обязательных реквизитов
    
    Мы должны заполнить все обязательные поля (измерения и ресурсы) новой записи.
  6. Запись набора в базу данных: НаборЗаписей.Записать(Истина); // Или НаборЗаписей.Записать(); Метод Записать() без параметров или с параметром Истина удаляет все записи в базе данных, соответствующие установленному отбору, и записывает текущий набор. Если вы хотите *добавить* записи и уверены, что не будет дубликатов по измерениям + периоду, используйте Записать(Ложь). Однако для периодических регистров с уникальностью по измерениям и периоду чаще используется Записать(Истина) для обновления цен.

Исправленное решение

Применяя эти знания, давайте перепишем код, чтобы он корректно записывал цены из табличной части в регистр сведений "Цены номенклатуры". Мы будем исходить из того, что для каждой номенклатуры и вида цены мы хотим установить новую цену с текущей даты, перезаписав старые записи, если они существуют для той же номенклатуры и вида цены на текущую дату (чтобы не было дубликатов).

Первым делом, нам нужно убедиться, что НоменклатураТовар найдена. Если НайтиПоНаименованию не находит элемент, то НоменклатураТовар будет Неопределено, что приведет к ошибке при попытке присвоить её измерению.


// 1. Поиск номенклатуры
НоменклатураТовар = Справочники.Номенклатура.НайтиПоНаименованию(Объект.Наименование);
Если НоменклатураТовар = Неопределено Тогда
    Сообщить("Номенклатура с наименованием '" + Объект.Наименование + "' не найдена. Запись цен невозможна.", СтатусСообщения.Важное);
    Возврат;
КонецЕсли;

// 2. Создаем набор записей для регистра "Цены номенклатуры"
НаборЗаписей = РегистрыСведений.ЦеныНоменклатуры.СоздатьНаборЗаписей();

// 3. Устанавливаем отбор по номенклатуре. Это важно, чтобы не удалить цены других номенклатур.
// Мы хотим работать только с ценами для НоменклатураТовар.
// Однако, если мы хотим устанавливать цены для разных видов цен, то отбор по виду цены
// здесь ставить не нужно, иначе мы очистим цены только для одного вида цены.
// Если регистр подчинен регистратору, то отбор по регистратору также необходим,
// если мы хотим изменять записи, сделанные конкретным регистратором.
// В данном случае, судя по коду, мы не изменяем существующие записи, а создаем новые.
// Если требуется удалить все старые цены для данной номенклатуры до текущей даты,
// то нам потребуется более сложная логика запроса или другой подход.
// Для простоты, мы будем замещать записи только по Номенклатуре и Периоду/ВидуЦены.

// 4. Очищаем набор записей. Это позволит нам полностью переформировать записи,
// если ранее были прочитаны какие-либо данные.
// Если нам нужно удалить записи, относящиеся к определенной Номенклатуре
// и определенному ВидуЦены, с текущей даты, то отбор должен быть более точным.
// Например, если мы устанавливаем цену для 'Оптовая', то мы хотим заменить 'Оптовую' цену,
// но не 'Розничную'.

// Давайте модифицируем логику. Мы хотим для каждой строки табличной части создать новую запись
// и добавить ее в набор. Старые записи будут замещены при записи набора.

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

    // Добавляем новую запись в набор
    Запись = НаборЗаписей.Добавить();

    // Заполняем измерения и ресурсы.
    // Обратите внимание: Регистратор и Упаковка, если они есть в регистре,
    // также должны быть заполнены.
    // Если регистр не подчинен регистратору, то поле Регистратор не будет существовать.
    // Если Упаковка не используется, то оно тоже не будет существовать или может быть Неопределено.

    Запись.Номенклатура = НоменклатураТовар;
    Запись.Период = ТекущаяДата(); // Используем функцию для получения текущей даты и времени
    Запись.ВидЦены = СтрокаТЧ.ВидЦены;
    Запись.Цена = СтрокаТЧ.Цена;
    Запись.Валюта = СтрокаТЧ.ВидЦены.ВалютаЦены;

    // Если Регистратор является измерением или обязательным реквизитом, его нужно заполнить.
    // Например, если вы записываете цены из документа, Регистратором будет этот документ.
    // Припустим, что это форма обработки или элемента справочника,
    // и если регистр подчинен регистратору, то нужно выбрать, что будет регистратором.
    // Если регистр не подчинен регистратору, это поле отсутствует.
    Попытка
        Запись.Регистратор = Объект.Ссылка; // Предположим, что Объект - это документ, который записывает цены
    Исключение
        // Поле Регистратор может отсутствовать, если регистр не подчинен регистратору
    КонецПопытки;

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

    Сообщить("Добавляем запись: Номенклатура=" + Запись.Номенклатура + ", ВидЦены=" + Запись.ВидЦены + ", Цена=" + Запись.Цена + ", Валюта=" + Запись.Валюта);

КонецЦикла;

// 6. Записываем весь набор в базу данных.
// Параметр Истина (по умолчанию) означает, что будут удалены все записи,
// соответствующие установленному отбору (если был), и затем будут записаны текущие записи набора.
// В нашем случае, так как отбор по Номенклатуре (и, возможно, ВидуЦены) не был установлен для "очистки",
// и мы просто добавляем новые записи, система автоматически определит, что нужно делать:
// для существующих комбинаций (Номенклатура, ВидЦены, Период) - обновить, для новых - добавить.
// Если вы хотите гарантированно удалить все цены для данной номенклатуры и потом записать новые,
// то перед циклом нужно было бы установить отбор по Номенклатуре, вызвать Прочитать() и Очистить().
// Однако, это удалит все цены для номенклатуры, независимо от вида цены.
// Более надежный способ для установки цен на текущую дату - это создание новых записей
// и их запись. Если запись с таким же набором измерений (Номенклатура, ВидЦены) и Периодом
// уже существует, она будет обновлена. Если нет - создана новая.
// Это поведение по умолчанию при записи набора записей.

Если НаборЗаписей.Количество() > 0 Тогда
    НаборЗаписей.Записать(); // Записываем, при необходимости замещая существующие
    Сообщить("Цены номенклатуры успешно записаны.");
Иначе
    Сообщить("Нет данных для записи цен номенклатуры.");
КонецЕсли;

Объяснение исправлений

  1. Проверка Номенклатуры: Мы добавили проверку на то, что НоменклатураТовар найдена. Если нет, то дальнейшая запись не имеет смысла и может привести к ошибке.
  2. Правильное размещение НаборЗаписей.Добавить(): Метод НаборЗаписей.Добавить() теперь вызывается внутри цикла Для Каждого СтрокаТЧ Из Объект.ПроцЦ_ТабличнаяЧасть Цикл. Это гарантирует, что для каждой строки табличной части создается отдельная новая запись в наборе.
  3. Однократная запись НаборЗаписей.Записать(): Вызов НаборЗаписей.Записать() перемещен за пределы цикла, в конец процедуры. Это позволяет сформировать весь набор записей в памяти, а затем одним вызовом сохранить его в базу данных, что значительно эффективнее и исключает проблемы с многократной записью.
  4. Аккуратное заполнение полей: Каждой новой записи Запись присваиваются значения из текущей строки табличной части. Мы учли, что Период должен быть заполнен функцией ТекущаяДата() для периодических регистров, и обратили внимание на возможную необходимость заполнения Регистратора и Упаковки, если они являются обязательными в вашем регистре. Мы использовали блоки Попытка...Исключение, чтобы код был более устойчивым, если этих полей в вашей конфигурации регистра нет.
  5. Отсутствие ненужного Прочитать() и Очистить() в данном контексте: В исходной задаче не было четкой цели сначала очистить записи, а потом добавить. Если бы такая цель стояла, то необходимо было бы более аккуратно установить отбор по Номенклатуре (и, возможно, по ВидуЦены и Периоду), затем вызвать НаборЗаписей.Прочитать() и НаборЗаписей.Очистить() перед добавлением новых записей. В текущем же случае, если мы просто добавляем записи, а Записать() будет с параметром Истина (по умолчанию), то существующие записи для той же комбинации измерений и периода будут обновлены, а новые добавлены, что часто является желаемым поведением при установке цен.

Альтернативный способ записи одной записи: МенеджерЗаписи

Для случая, когда вам нужно работать только с одной записью регистра (создать новую, прочитать и изменить существующую или удалить одну запись), мы можем использовать объект МенеджерЗаписи. Это более простой способ, но менее производительный для массовых операций.


// Пример использования МенеджераЗаписи для установки одной цены
МенеджерЗаписи = РегистрыСведений.ЦеныНоменклатуры.СоздатьМенеджерЗаписи();

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

// Если регистр подчинен регистратору, его тоже нужно заполнить
Попытка
    МенеджерЗаписи.Регистратор = Объект.Ссылка;
Исключение
КонецПопытки;

// Записываем одну запись
МенеджерЗаписи.Записать();

Сообщить("Цена успешно установлена через МенеджерЗаписи.");

В нашем случае, когда мы работаем с табличной частью, содержащей несколько строк цен, НаборЗаписей является более подходящим и производительным инструментом.

Заключительные рекомендации

Всегда внимательно проверяйте структуру регистра сведений, с которым вы работаете (измерения, ресурсы, реквизиты, периодичность, подчинение регистратору). Если поле является измерением или обязательным реквизитом, оно должно быть заполнено перед попыткой записи. Если вы не уверены, какие поля обязательны, вы можете посмотреть метаданные регистра в конфигураторе или воспользоваться отладчиком, чтобы увидеть структуру объекта Запись.

Используйте сообщения отладки (Сообщить()) или точку останова для проверки значений переменных непосредственно перед вызовом НаборЗаписей.Записать(), чтобы убедиться, что все обязательные поля каждой записи в наборе заполнены корректно. Это поможет быстро локализовать источник ошибок, подобных "Значение поля "Номенклатура" не может быть пустым!".

← На главную