При работе в клиент-серверном режиме «1С:Предприятия» разработчики часто сталкиваются со специфическими ошибками компиляции кода. Особенно остро эта проблема проявляется в крупных исторических конфигурациях, таких как «Управление производственным предприятием» (УПП), «Управление торговлей 10.3» или «Комплексная автоматизация 1.1», работающих в режиме обычного приложения. Типичный сценарий: при проведении документа на сервере внезапно начинают сыпаться ошибки о неопределенных глобальных переменных или отсутствующих функциях в совершенно других, казалось бы, типовых и не изменяемых в данный момент объектах.
В данной статье мы подробно разберем, почему возникает ошибка Переменная не определена (глСписокПрефиксовУзлов), выясним причины «исчезновения» вспомогательных процедур в модуле объекта и рассмотрим пошаговые методы устранения этих проблем, опираясь на лучшие практики программирования в среде 1С.
---
Рассмотрим практическую ситуацию. При проведении самописного документа в транзакции программно создается типовой документ ПеремещениеТоваров. Процедура создания и заполнения этого документа вынесена в общий привилегированный модуль, который выполняется на сервере без контекста клиента. Во время выполнения кода платформа аварийно завершает работу и выдает в системный журнал следующие сообщения об ошибках:
{Документ.ПеремещениеТоваров.МодульОбъекта(2681,82)}: Переменная не определена (глСписокПрефиксовУзлов)
Шапка.Параметры.НомерДокумента = ОбщегоНазначения.ПолучитьНомерНаПечать(Ссылка, <<?>>глСписокПрефиксовУзлов);
{Документ.ПеремещениеТоваров.МодульОбъекта(3079,77)}: Переменная не определена (глСписокПрефиксовУзлов)
Шапка.Параметры.Номер = ОбщегоНазначения.ПолучитьНомерНаПечать(ЭтотОбъект, <<?>>глСписокПрефиксовУзлов);
{Документ.ПеремещениеТоваров.МодульОбъекта(2698,29)}: Процедура или функция с указанным именем не определена (ПолучитьТаблицуСтоимостиТоваров)
ТаблицаСтоимости = <<?>>ПолучитьТаблицуСтоимостиТоваров(Ссылка);
{Документ.ПеремещениеТоваров.МодульОбъекта(2728,13)}: Процедура или функция с указанным именем не определена (ПолучитьСтоимостьТовара)
Сумма = <<?>>ПолучитьСтоимостьТовара(ТаблицаСтоимости,
У разработчика закономерно возникают вопросы: почему сервер ругается на процедуры, которые не вызываются в процессе проведения документа, и почему аналогичный механизм создания документа ОприходованиеТоваров отрабатывает без каких-либо замечаний? Разберем эти вопросы по порядку.
---
Давайте выясним, почему вообще компилятор 1С обращает внимание на процедуры печати, если на сервере выполняется только создание и запись объекта без какого-либо намека на его вывод на экран.
Дело в том, что когда на сервере происходит обращение к менеджеру документа (например, при вызове метода Документы.ПеремещениеТоваров.СоздатьДокумент() или при получении объекта по ссылке Ссылка.ПолучитьОбъект()), платформа производит полную компиляцию модуля объекта для текущего контекста (в данном случае — для сервера).
Компилятор не анализирует, какая именно ветка кода будет выполняться прямо сейчас. Он проверяет синтаксическую целостность всего модуля объекта целиком. Если в модуле присутствует хотя бы одна строчка кода, содержащая синтаксическую ошибку, обращение к неизвестной переменной или вызов несуществующей функции, компиляция всего модуля аварийно прерывается. Объект не может быть инициализирован в памяти сервера, что приводит к откату всей транзакции проведения исходного документа.
В документе ОприходованиеТоваров данная проблема не воспроизводилась по одной простой причине: его модуль объекта либо остался полностью типовым (где все вызовы клиентских процедур корректно изолированы разработчиками фирмы «1С»), либо изменения в него вносились с соблюдением правил контекстной разработки.
---
Что представляет собой переменная глСписокПрефиксовУзлов? Это глобальная переменная обычного приложения, которая объявляется в Модуле обычного приложения с ключевым словом Экспорт:
// В Модуле обычного приложения
Перем глСписокПрефиксовУзлов Экспорт;
Эта переменная инициализируется в момент запуска толстого клиента (обычно в процедуре ПриНачалеРаботыСистемы) и используется для хранения списка префиксов распределенной информационной базы (РИБ), чтобы при формировании печатных форм отсекать эти префиксы от номера документа для красивого представления пользователю.
Ключевой конфликт: модуль обычного приложения компилируется и существует исключительно в клиентском контексте. Сервер базы данных ничего не знает о существовании этого модуля и его переменных. Когда серверный компилятор пытается распарсить строку кода, содержащую обращение к глСписокПрефиксовУзлов, он не находит эту переменную в области видимости сервера и выбрасывает ошибку Переменная не определена.
---
Теперь проанализируем, почему сервер «потерял» процедуры ПолучитьТаблицуСтоимостиТоваров и ПолучитьСтоимостьТовара.
В типовых конфигурациях эти вспомогательные функции используются для расчета стоимостных показателей в печатных формах и изначально обернуты в директиву препроцессора:
#Если Клиент Тогда
Функция ПолучитьТаблицуСтоимостиТоваров(Ссылка)
// Реализация функции расчета стоимости
КонецФункции
#КонецЕсли
Это означает, что для сервера данных процедур физически не существует. Когда сторонний разработчик решает добавить свою собственную, нетиповую печатную форму непосредственно в модуль объекта, он совершает критическую ошибку — размещает код своей новой экспортной процедуры вне блока #Если Клиент Тогда.
Пример ошибочного размещения кода:
// ОШИБКА: Код находится вне директив препроцессора и компилируется на сервере!
Процедура ПечатьМоейКастомнойФормы() Экспорт
// Сервер пытается скомпилировать эту строку и падает,
// так как ПолучитьТаблицуСтоимостиТоваров определена только на клиенте!
ТаблицаСтоимости = ПолучитьТаблицуСтоимостиТоваров(Ссылка);
КонецПроцедуры
Поскольку сервер пытается скомпилировать процедуру ПечатьМоейКастомнойФормы, он видит внутри нее вызовы функций ПолучитьТаблицуСтоимостиТоваров и ПолучитьСтоимостьТовара. Но так как эти функции скрыты от сервера директивой #Если Клиент Тогда, компилятор выдает ошибку: Процедура или функция с указанным именем не определена.
---
Рассмотрим, как правильно исправить данную проблему, восстановить работоспособность проведения документов на сервере и не нарушить логику печати.
Первым делом нам необходимо строго разграничить клиентский и серверный контексты в модуле объекта. Весь код, который обращается к клиентским методам (предупреждения, диалоги, работа с локальными файлами) и глобальным переменным обычного приложения (таким как глСписокПрефиксовУзлов или глТекущийПользователь), должен быть обернут в директивы препроцессора.
Откроем модуль объекта Документ.ПеремещениеТоваров, найдем добавленные нетиповые процедуры печати и обернем их в блок:
#Если Клиент Тогда
Процедура ПечатьМоейКастомнойФормы() Экспорт
// Безопасный клиентский контекст
ТаблицаСтоимости = ПолучитьТаблицуСтоимостиТоваров(Ссылка);
Сумма = ПолучитьСтоимостьТовара(ТаблицаСтоимости, ...);
// Использование глобальной клиентской переменной здесь абсолютно легально
Шапка.Параметры.Номер = ОбщегоНазначения.ПолучитьНомерНаПечать(ЭтотОбъект, глСписокПрефиксовУзлов);
КонецПроцедуры
#КонецЕсли
После этого сервер при компиляции модуля объекта просто пропустит этот блок кода, и ошибки компиляции исчезнут.
Обратите внимание на вызов функции ОбщегоНазначения.ПолучитьНомерНаПечать(). Разработчики платформы спроектировали ее так, что второй параметр (список префиксов) является необязательным.
Если вам действительно необходимо вызвать эту функцию в коде, который должен одинаково успешно работать и на сервере, и на клиенте, используйте безопасный вызов без передачи глобальной переменной:
// Если префикс не передан, функция попытается обработать номер по умолчанию
НомерДляПечати = ОбщегоНазначения.ПолучитьНомерНаПечать(Ссылка);
Если же префиксы критически важны для формирования номера на сервере, необходимо отказаться от использования глобальной переменной глСписокПрефиксовУзлов в пользу получения данных через запросы к планам обмена информационной базы.
---
Чтобы в будущем не сталкиваться с подобными «сюрпризами» при обновлении конфигурации или написании нового функционала, мы рекомендуем придерживаться следующих правил архитектуры приложений 1С:
ПараметрыСеанса. Они инициализируются один раз при старте сеанса пользователя и работают бесшовно на всех уровнях системы.Пример реализации получения списка префиксов через кэширующий общий модуль:
// В общем серверном модуле "ПрефиксыСлужебный" (свойство "Повторное использование: На время сеанса")
Функция ПолучитьСписокПрефиксовУзлов() Экспорт
СписокПрефиксов = Новый Массив;
// Запрос к планам обмена для получения кодов узлов
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Код КАК Код
|ИЗ
| ПланОбмена.Полный";
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
СписокПрефиксов.Добавить(СокрЛП(Выборка.Код));
КонецЦикла;
Возврат СписокПрефиксов;
КонецФункции
Такой подход гарантирует высокую скорость работы за счет кэширования данных платформой «1С:Предприятие» и обеспечивает абсолютную контекстную безопасность вашего программного кода.