Разработка в системе «1С:Предприятие» в условиях клиент-серверной архитектуры требует от программиста четкого понимания того, в каком контексте исполняется код. Очень распространенной является ситуация, когда код отлично работает при вызове из интерактивной формы или обработки на клиенте, но при переносе в фоновое (регламентное) задание или серверный общий модуль внезапно падает с критической ошибкой: «Метод объекта не обнаружен». В этой статье мы подробно разберем, почему возникает эта проблема, как устроен препроцессор платформы 1С, и рассмотрим наиболее эффективные архитектурные решения для исправления данной ошибки.
Для начала проанализируем физический смысл ошибки. Когда мы вызываем процедуру модуля объекта из серверного контекста (например, из общего модуля с установленным флагом «Сервер» или из регламентного задания), платформа должна скомпилировать модуль этого объекта для исполнения на сервере.
Если нужная нам процедура объявлена в модуле документа как Экспорт, она по идее должна быть доступна извне. Однако разработчики конфигураций очень часто ограничивают области видимости кода с помощью инструкций препроцессора. Рассмотрим типичный пример такого ограничения:
#Если Клиент Тогда
Процедура ЗаполнитьТабличныеЧастиПередПроведениемУпр(Режим) Экспорт
// Здесь находится логика заполнения табличных частей
...
КонецПроцедуры
#КонецЕсли
Инструкция #Если Клиент Тогда указывает платформе, что весь код внутри данного блока должен компилироваться только на стороне клиента (в тонком, толстом или веб-клиенте). Когда сервер приложений 1С обращается к модулю этого документа для выполнения серверного вызова, он полностью игнорирует и физически «вырезает» из компилируемого серверного контекста все, что находится внутри этой директивы. Для сервера этой процедуры просто не существует в природе. Именно поэтому при попытке выполнить код:
ДокОбъект = СсылкаНаЗаказ.ПолучитьОбъект();
ДокОбъект.ЗаполнитьТабличныеЧастиПередПроведениемУпр(Ложь);
мы получаем закономерное исключение: «Метод объекта не обнаружен (ЗаполнитьТабличныеЧастиПередПроведениемУпр)».
Прежде чем приступать к исправлению кода модуля документа, необходимо убедиться, что вызывающая сторона работает именно с объектом документа, а не с его ссылкой. Это классическая ошибка начинающих разработчиков. Процедуры модуля документа физически недоступны у типа ДокументСсылка. Они доступны только у типа ДокументОбъект.
Проанализируем правильный паттерн получения объекта в вызывающем общем модуле:
// ПРИМЕР: Неправильно (вызов из ссылки приведет к ошибке)
СсылкаНаЗаказ = Документы.ЗаказПокупателя.НайтиПоНомеру("00001", ТекущаяДата());
СсылкаНаЗаказ.ЗаполнитьТабличныеЧастиПередПроведениемУпр(Ложь); // Ошибка!
// ПРИМЕР: Правильно (сначала получаем объект)
СсылкаНаЗаказ = Документы.ЗаказПокупателя.НайтиПоНомеру("00001", ТекущаяДата());
Если ЗначениеЗаполнено(СсылкаНаЗаказ) Тогда
ДокОбъект = СсылкаНаЗаказ.ПолучитьОбъект();
ДокОбъект.ЗаполнитьТабличныеЧастиПередПроведениемУпр(Ложь);
КонецЕсли;
Если вы убедились, что в коде используется именно объект (например, созданный через СоздатьДокумент() или полученный через ПолучитьОбъект()), но ошибка «Метод объекта не обнаружен» сохраняется — значит, дело гарантированно в директивах препроцессора.
Самое очевидное и простое решение — перенести процедуру из блока #Если Клиент Тогда в общий контекст модуля документа (где нет ограничивающих директив) или поместить её под директиву, разрешающую компиляцию на сервере.
Однако здесь кроется серьезная опасность, о которой нужно помнить! Если мы просто перенесем процедуру выше директивы #Если Клиент Тогда, сервер попытается скомпилировать весь ее внутренний код. Если внутри этой процедуры используются чисто клиентские методы, сервер выдаст ошибку компиляции еще на этапе сохранения конфигурации либо упадет в режиме «1С:Предприятие» при попытке вызвать данный метод.
К чисто клиентским методам и объектам относятся:
Предупреждение(), Вопрос(), Сообщить() (в старых версиях), а также современные асинхронные аналоги, такие как ПоказатьПредупреждение().ЭтаФорма, элементам формы, динамическим спискам.Если процедура содержит подобные вызовы, ее необходимо предварительно рефакторить. Разделим интерактивную (клиентскую) логику и расчетную (прикладную) логику. Клиентские диалоги должны отрабатывать на клиенте до вызова серверной процедуры, а на сервер должны передаваться уже готовые параметры.
Если логика процедуры полностью безопасна для выполнения на сервере (например, она занимается исключительно расчетом сумм, налогов, заполнением табличных частей на основе реквизитов самого документа), мы можем смело вынести её из клиентского блока. Структура модуля объекта должна выглядеть следующим образом:
// Эта процедура доступна везде: и на клиенте, и на сервере
Процедура ЗаполнитьТабличныеЧастиПередПроведениемУпр(Режим) Экспорт
// Безопасный код, работающий только с данными объекта
Для Каждого СтрокаТЧ Из Товары Цикл
СтрокаТЧ.Сумма = СтрокаТЧ.Количество * СтрокаТЧ.Цена;
КонецЦикла;
КонецПроцедуры
#Если Клиент Тогда
// Здесь остаются только процедуры интерактивного взаимодействия с пользователем
Процедура НажатьКнопкуЗаполненияНаФорме()
// Клиентский код
КонецПроцедуры
#КонецЕсли
Если вы работаете на современных версиях платформы (1С 8.2 или 8.3), использование модуля объекта для выполнения общих расчетов считается неоптимальным с точки зрения производительности. Метод ПолучитьОбъект() — крайне «тяжелая» операция для СУБД и сервера приложений, так как она полностью считывает все данные документа, включая все его многочисленные табличные части, накладывает транзакционные блокировки и расходует оперативную память.
Для оптимизации этой логики в платформу был добавлен Модуль менеджера. Процедуры модуля менеджера выполняются на сервере и не требуют создания тяжелого объекта документа. Мы можем передать туда ссылку на документ и выполнить необходимые манипуляции.
Рассмотрим, как реализовать этот паттерн. В модуле менеджера документа ЗаказПокупателя создаем экспортную процедуру:
// В модуле менеджера документа ЗаказПокупателя
Процедура ЗаполнитьРеквизитыЗаказаПоСсылке(СсылкаНаДокумент) Экспорт
// Пример оптимальной работы с данными без создания объекта (если требуется только чтение)
// Или, если запись все же необходима, создаем объект внутри точечно:
ОбъектДокумента = СсылкаНаДокумент.ПолучитьОбъект();
// Выполняем заполнение
ОбъектДокумента.Записать();
КонецПроцедуры
Вызов такой процедуры из серверного общего модуля будет выглядеть очень изящно и не потребует лишней инициализации объектов на вызывающей стороне:
// Вызов из общего модуля на сервере
Документы.ЗаказПокупателя.ЗаполнитьРеквизитыЗаказаПоСсылке(ТекущийЗаказ);
Согласно стандартам разработки фирмы «1С» (включая стандарты разработки для Библиотеки Стандартных Подсистем — БСП), наиболее правильным архитектурным решением является полный вынос универсальной прикладной логики из модулей документов в специализированные Общие модули.
Давайте создадим общий модуль с именем ОбработкаДокументовСервер. В свойствах этого модуля обязательно должны быть установлены флажки «Сервер» и «Вызов сервера» (при необходимости), а флаг «Клиент» должен быть снят.
Перенесем туда логику заполнения документа, передавая сам заполняемый объект в качестве параметра по ссылке:
// В общем модуле "ОбработкаДокументовСервер"
Процедура ЗаполнитьТабличныеЧастиПередПроведением(ДокументОбъект, Режим) Экспорт
// Проверяем тип пришедшего объекта для безопасности
Если ТипЗнч(ДокументОбъект) <> Тип("ДокументОбъект.ЗаказПокупателя") Тогда
Возврат;
КонецЕсли;
// Выполняем расчеты и заполнение напрямую в переданном объекте
Для Каждого СтрокаТЧ Из ДокументОбъект.Товары Цикл
// Производим необходимые вычисления
СтрокаТЧ.Сумма = СтрокаТЧ.Количество * СтрокаТЧ.Цена;
КонецЦикла;
КонецПроцедуры
Теперь из любого места системы — будь то регламентное задание, обработка проведения в модуле документа или внешняя обработка — мы можем вызвать эту процедуру одинаково просто:
// Вызов из серверного контекста (регламентное задание)
НовыйДокумент = Документы.ЗаказПокупателя.СоздатьДокумент();
// Заполняем базовые реквизиты
НовыйДокумент.Дата = ТекущаяДата();
// Вызываем наш общий модуль для заполнения табличных частей
ОбработкаДокументовСервер.ЗаполнитьТабличныеЧастиПередПроведением(НовыйДокумент, Ложь);
// Записываем документ
НовыйДокумент.Записать(РежимЗаписиДокумента.Проведение);
Если вы запускаете процедуру в рамках регламентного задания, помните о важных нюансах:
Попытка...Исключение и записывайте подробную информацию об ошибках в ЖурналРегистрации.Применим этот подход на практике:
Попытка
ДокОбъект = СсылкаНаЗаказ.ПолучитьОбъект();
ДокОбъект.ЗаполнитьТабличныеЧастиПередПроведениемУпр(Ложь);
ДокОбъект.Записать(РежимЗаписиДокумента.Запись);
Исключение
// Записываем ошибку в журнал регистрации для последующего анализа администратором
ЗаписьЖурналаРегистрации("РегламентныеЗадания.ЗаполнениеЗаказов",
УровеньЖурналаРегистрации.Ошибка,
,
,
"Не удалось заполнить заказ: " + СсылкаНаЗаказ + ". Описание ошибки: " + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки
Резюмируя все вышесказанное: при возникновении ошибки видимости методов на сервере всегда проверяйте директивы препроцессора (#Если Клиент) в модуле объекта документа, избавляйтесь от интерактивных функций в расчетном коде и старайтесь переносить общую бизнес-логику в специализированные серверные общие модули или модули менеджера.