При разработке в среде «1С:Предприятие» одной из самых частых задач является получение актуальных данных из периодических регистров сведений (например, цен номенклатуры, курсов валют или специальных кодов печати). Однако при реализации этой операции начинающие разработчики регулярно сталкиваются с рядом классических ошибок. Эти ошибки связаны с непониманием разницы между обычными и управляемыми формами, неправильной передачей параметров в функции общих модулей, а также с неоптимальным выбором методов работы с базой данных.
В этой статье мы подробно разберем, почему возникают ошибки «Значение не является значением объектного типа (Дата)» и «Недостаточно фактических параметров», как правильно передавать контекст в общие модули и как переписать неоптимальный код получения данных на быстрый и безопасный запрос к виртуальной таблице СрезПоследних.
Проанализируем ситуацию, представленную в начале обсуждения. Разработчик пытается вызвать функцию общего модуля из формы документа «Отчет производства за смену» в конфигурации «Бухгалтерия предприятия 2.0» (БП 2.0):
КодПечати=РаботаСоСправочниками.КодПечати(Объект.Дата, КодПечати);
При этом платформа генерирует ошибку о том, что программа пытается обратиться к свойству Дата у объекта, который не имеет такого свойства (то есть значение переменной перед точкой равно Неопределено).
Выясним причину этой ошибки. Конфигурация «Бухгалтерия предприятия 2.0» построена на обычных формах (толстый клиент). В обычных формах архитектура работы с данными существенно отличается от управляемых форм (которые используются в БП 3.0):
Объект (тип ДанныеФормыСтруктура). Поэтому обращение к реквизитам документа всегда происходит через эту переменную: Объект.Дата, Объект.Контрагент и так далее.Объект по умолчанию просто не существует. Все реквизиты основного объекта (в данном случае документа) доступны в модуле формы напрямую по своим именам. То есть обращаться к дате нужно как Дата или, если требуется явно указать объект, через ключевое слово ЭтотОбъект.Дата.Попытка использовать синтаксис управляемых форм (Объект.Дата) в модуле обычной формы приводит к тому, что система пытается найти переменную Объект. Не найдя ее среди локальных переменных и реквизитов, 1С инициализирует ее как Неопределено, вызывая критическую ошибку при обращении через точку.
Решение проблемы: Для исправления этой ошибки в обычной форме необходимо убрать обращение к несуществующему объекту:
// Неправильно для обычной формы:
// КодПечати=РаботаСоСправочниками.КодПечати(Объект.Дата, КодПечати);
// Правильно для обычной формы:
КодПечати = РаботаСоСправочниками.КодПечати(Дата, ОбъектПереданный);
После изменения обращения к дате разработчик столкнулся со следующей ошибкой компиляции/выполнения: «Недостаточно фактических параметров». Посмотрим на сигнатуру функции, объявленной в общем модуле РаботаСоСправочниками:
Функция КодПечати(АктуальнаяДата, номенклатура, ЭлементНоменклатуры) Экспорт
Функция ожидает на вход три обязательных параметра. Однако разработчик вызывает ее следующим образом:
КодПечати = РаботаСоСправочниками.КодПечати(Дата, КодПечати);
Здесь передается всего два параметра вместо трех. В языке 1С, если при объявлении параметров функции не заданы их значения по умолчанию, все параметры считаются строго обязательными. Попытка передать меньшее количество аргументов всегда приводит к аварийному завершению программы.
Кроме того, здесь присутствует грубейшая логическая ошибка. Функция ожидает получить ссылку на справочник Номенклатура, а автор передает в качестве второго параметра переменную КодПечати, которая, скорее всего, является строкой или числом. Естественно, такой код работать не будет, даже если совпадет количество параметров.
Решение проблемы: Нам необходимо привести в соответствие объявление функции и ее вызов. Рассмотрим, как правильно спроектировать эту функцию.
Если нам нужно получить код печати для конкретного элемента номенклатуры на определенную дату, функция должна принимать ровно два параметра: дату и ссылку на номенклатуру.
Объявление функции в общем модуле РаботаСоСправочниками должно выглядеть так:
Функция ПолучитьКодПечати(АктуальнаяДата, СсылкаНоменклатуры) Экспорт
Отбор = Новый Структура("Номенклатура", СсылкаНоменклатуры);
// Получаем последнюю запись из периодического регистра сведений
ЗначенияРесурсов = РегистрыСведений.КодПечати.ПолучитьПоследнее(АктуальнаяДата, Отбор);
Возврат ЗначенияРесурсов.КодПечати;
КонецФункции
А вызов этой функции из модуля формы документа (например, при изменении номенклатуры в табличной части или при заполнении реквизитов) должен выглядеть следующим образом:
// Предположим, ТекущаяНоменклатура — это ссылка на справочник Номенклатура
КодПечати = РаботаСоСправочниками.ПолучитьКодПечати(Дата, ТекущаяНоменклатура);
Метод менеджера регистра сведений ПолучитьПоследнее() удобен для быстрого получения данных в одну строчку кода, однако у него есть особенности, о которых необходимо знать каждому разработчику:
Неопределено. Если в регистре вообще нет записей, удовлетворяющих вашему отбору, метод все равно вернет объект типа Структура. При этом все свойства структуры (соответствующие ресурсам регистра) будут заполнены значениями по умолчанию для их типов данных (например, число вернет 0, строка — "", ссылка — ПустаяСсылка()).ПолучитьПоследнее() выполняет прямой запрос к базе данных под капотом. Если вы вызываете этот метод внутри цикла (например, при обходе строк табличной части документа при его проведении или при выводе строк в отчет), это вызовет лавинообразное падение производительности (проблема N+1 запросов). Платформа будет делать отдельный запрос к СУБД для каждой строки, что категорически запрещено стандартами разработки 1С.Получить().Для обеспечения максимальной производительности системы и соответствия стандартам разработки 1С, получение среза последних данных всегда рекомендуется выполнять с помощью запросов. Особенно это критично, если получение данных происходит в процедурах проведения документов.
Рассмотрим, как правильно переписать нашу функцию в общем модуле с использованием запроса к виртуальной таблице СрезПоследних.
Функция ПолучитьКодПечатиЗапросом(АктуальнаяДата, СсылкаНоменклатуры) Экспорт
// Инициализируем запрос
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| КодПечатиСрезПоследних.КодПечати КАК КодПечати
|ИЗ
| РегистрСведений.КодПечати.СрезПоследних(&Период, Номенклатура = &Номенклатура) КАК КодПечатиСрезПоследних";
// Устанавливаем параметры запроса
Запрос.УстановитьПараметр("Период", АктуальнаяДата);
Запрос.УстановитьПараметр("Номенклатура", СсылкаНоменклатуры);
// Выполняем запрос
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
Возврат Выборка.КодПечати;
КонецЕсли;
// Если запись не найдена, возвращаем пустое значение (например, пустую строку)
Возврат "";
КонецФункции
Проанализируем преимущества использования запроса в данном сценарии:
СрезПоследних(&Период, Номенклатура = &Номенклатура). Это позволяет СУБД моментально найти нужную запись с использованием индексов регистра, минимизируя нагрузку на сервер.ПолучитьПоследнее(), мы можем точно определить с помощью метода РезультатЗапроса.Пустой(), была ли вообще запись в регистре, и обработать эту ситуацию явным образом.= на оператор В.Посмотрим на пример того, как правильно получать коды печати для всей табличной части документа за один запрос, чтобы не вызывать функцию в цикле. Это классический паттерн разработки высоконагруженных систем.
Процедура ЗаполнитьКодыПечатиВТабличнойЧасти(ДокументОбъект) Экспорт
// Шаг 1: Собираем всю уникальную номенклатуру из табличной части в массив
МассивНоменклатуры = ДокументОбъект.Товары.ВыгрузитьКолонку("Номенклатура");
// Очищаем массив от дублей для оптимизации запроса
ОбщегоНазначенияКлиентСервер.СвернутьМассив(МассивНоменклатуры);
Если МассивНоменклатуры.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// Шаг 2: Получаем актуальные коды печати для всего массива номенклатуры одним запросом
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| КодПечатиСрезПоследних.Номенклатура КАК Номенклатура,
| КодПечатиСрезПоследних.КодПечати КАК КодПечати
|ИЗ
| РегистрСведений.КодПечати.СрезПоследних(&Период, Номенклатура В (&МассивНоменклатуры)) КАК КодПечатиСрезПоследних";
Запрос.УстановитьПараметр("Период", ДокументОбъект.Дата);
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
// Выгружаем результат в соответствие для быстрого поиска в памяти сервера
ТаблицаКодов = Запрос.Выполнить().Выгрузить();
СоответствиеКодов = Новый Соответствие;
Для Каждого СтрокаТаблицы Из ТаблицаКодов Цикл
СоответствиеКодов.Вставить(СтрокаТаблицы.Номенклатура, СтрокаТаблицы.КодПечати);
КонецЦикла;
// Шаг 3: Быстро заполняем коды печати в табличной части документа без обращений к базе данных
Для Каждого СтрокаТовара Из ДокументОбъект.Товары Цикл
НайденныйКод = СоответствиеКодов.Получить(СтрокаТовара.Номенклатура);
Если НайденныйКод <> Неопределено Тогда
СтрокаТовара.КодПечати = НайденныйКод;
Иначе
СтрокаТовара.КодПечати = ""; // Значение по умолчанию, если кода нет в регистре
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Используя данный подход, мы делаем ровно один запрос к базе данных независимо от того, сколько строк содержится в документе — 5 или 5000. Это гарантирует быструю работу интерфейса и стабильную работу системы под высокой нагрузкой.