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