Когда мы разрабатываем функциональность в 1С:Предприятии, часто возникает задача выполнять определенные действия с документами не только в интерактивном режиме через формы, но и автоматически, например, по расписанию. Именно с такой ситуацией сталкиваемся, когда хотим использовать регламентное задание для обработки документов. Давайте подробно разберем, почему прямой вызов операций с формами из регламентного задания не работает и как правильно перестроить архитектуру кода для решения этой задачи.
Первоначально, мы видим, что вы пытаетесь открыть форму документа в общем модуле, который затем будет запущен регламентным заданием. В коде используются такие конструкции, как ПолучитьФорму() и Форма.Открыть. Эти методы предназначены для работы с пользовательским интерфейсом и выполняются исключительно на стороне клиента.
// Пример клиентского кода, который не будет работать на сервере
Форма = ПолучитьФорму("Документ.ЗаказКлиента.ФормаОбъекта");
Форма.Открыть;
Форма.РучноеСнятиеСРезерваНаСервере;
Форма.Закрыть;
Суть проблемы заключается в следующем:
&НаКлиенте) позволяет работать с формами, элементами управления, инициировать диалоги с пользователем.&НаСервере) позволяет работать с данными информационной базы, выполнять запросы, получать объекты данных и изменять их, но без какого-либо взаимодействия с пользователем или формами.Попытки выполнить клиентский код на сервере приводят к ошибкам типа "Метод задания не найден" или "Процедура или функция с указанным именем не определена (ПолучитьФорму)", что мы и наблюдаем в вашем случае.
Основной принцип, который мы должны усвоить, заключается в разделении ответственности. Все, что касается непосредственной обработки данных, расчетов, изменений регистров и документов, должно выполняться на сервере. А все, что касается отображения данных и взаимодействия с пользователем, – на клиенте.
Так как регламентное задание работает исключительно на сервере, нам необходимо перенести всю бизнес-логику, которая ранее располагалась в модуле формы документа, в серверные модули. Наиболее подходящими местами для размещения такой логики являются:
Документ.ЗаказКлиента.МодульОбъекта). Это идеальное место для процедур, которые оперируют конкретным экземпляром документа, изменяют его реквизиты или табличные части, а затем записывают.Документ.ЗаказКлиента.МодульМенеджера). Этот модуль используется для операций с типом документа в целом, например, для выполнения запросов по всем документам данного типа, создания новых документов или выполнения операций, не привязанных к конкретному экземпляру.В вашем случае, поскольку процедура РучноеСнятиеСРезерваНаСервере работает с конкретным объектом документа (ДокОбъект.Дата, ДокОбъект.Товары и т.д.) и его записью (ДокОбъект.Записать(...)), оптимальным решением будет перенести её в модуль объекта документа "ЗаказКлиента".
Давайте пошагово разберем, как мы можем перенести вашу процедуру из модуля формы в модуль объекта документа и затем вызвать её из регламентного задания.
Мы берем вашу процедуру РучноеСнятиеСРезерваНаСервере и перемещаем её в модуль объекта документа "ЗаказКлиента". Внутри модуля объекта, вместо переменной, которая содержит объект документа (например, ДокОбъект), мы можем использовать ключевое слово ЭтотОбъект. Это означает текущий объект документа, с которым работает данный модуль.
Исходный код процедуры (в модуле формы):
// В модуле формы Документа "ЗаказКлиента"
&НаСервере
Процедура РучноеСнятиеСРезерваНаСервере Экспорт
// ... ваш код, который использует ДокОбъект = ВыборкаЗаказы.Ссылка.ПолучитьОбъект;
// ... и затем ДокОбъект.Дата, ДокОбъект.Товары, ДокОбъект.Записать(...)
КонецПроцедуры
Модифицированный код процедуры (в модуле объекта документа "ЗаказКлиента"):
Мы назовем ее, например, СнятьРезервПриПросрочке(), чтобы отразить её логику и сделаем её экспортной, чтобы можно было вызвать извне.
// В модуле объекта документа "ЗаказКлиента"
Процедура СнятьРезервПриПросрочке() Экспорт
// Запрос для отбора заказов (этот запрос не должен быть в процедуре объекта,
// так как он выбирает МНОГО заказов. Он должен быть в общем модуле или модуле менеджера,
// который вызывает эту процедуру для КАЖДОГО заказа.)
// Сейчас мы его оставим для примера, но отметим, что он будет выполняться из общего модуля.
// В данном контексте, "ЭтотОбъект" уже является текущим документом ЗаказКлиента,
// с которым мы работаем.
// Вместо ВыборкаЗаказы.Ссылка.ПолучитьОбъект() мы используем ЭтотОбъект
// Вместо ВыборкаЗаказы.ПроцентОплаты мы можем обратиться к регистру напрямую,
// либо сделать параметр для этой процедуры (но это усложнит пример).
// Для простоты примера предположим, что данные о состоянии будут получены внутри или переданы.
// Текущие данные документа
ДатаИзДокумента = ЭтотОбъект.Дата;
Индивидуальный = ЭтотОбъект.ИндивидуальныйСрокОплатыДляСнятияСРезерва;
Если НЕ ЗначениеЗаполнено(Индивидуальный) Тогда
Индивидуальный = Константы.ОбщийСрокОплатыДляСнятияСРезерва.Получить();
КонецЕсли;
// ВНИМАНИЕ: Запрос в цикле или внутри процедуры объекта, которая вызывается для каждого объекта,
// может быть неоптимальным. Лучше собирать все необходимые данные одним запросом в общем модуле.
// Для демонстрации переноса, мы оставляем его здесь.
ЗапросКалендарь = Новый Запрос;
ЗапросКалендарь.Текст = "ВЫБРАТЬ
| КалендарныеГрафики.Календарь.Ссылка КАК КалендарьСсылка,
| КалендарныеГрафики.Год КАК Год,
| КалендарныеГрафики.ДатаГрафика КАК ДатаГрафика,
| КалендарныеГрафики.ДеньВключенВГрафик КАК ДеньВключенВГрафик,
| КалендарныеГрафики.Календарь.Наименование КАК КалендарьНаименование
|ИЗ
| РегистрСведений.КалендарныеГрафики КАК КалендарныеГрафики
|ГДЕ
| КалендарныеГрафики.Календарь.Наименование = &Наименование
| И КалендарныеГрафики.ДатаГрафика > &ДатаДокумента
|УПОРЯДОЧИТЬ ПО
| ДатаГрафика"; // Добавим сортировку для корректного поиска
ЗапросКалендарь.УстановитьПараметр("Наименование", "Производственный календарь");
ЗапросКалендарь.УстановитьПараметр("ДатаДокумента", ДатаИзДокумента);
РезультатКалендарь = ЗапросКалендарь.Выполнить();
ТЗКалендарь = РезультатКалендарь.Выгрузить();
// Расчет даты начала просрочки (добавлен для полноты логики, отсутствующей в исходном коде)
ДатаНачалаПросрочки = Неопределено;
СчетчикДней = 0;
Для Каждого Элемент Из ТЗКалендарь Цикл
Если Элемент.ДеньВключенВГрафик Тогда
СчетчикДней = СчетчикДней + 1;
КонецЕсли;
Если СчетчикДней = Индивидуальный Тогда
ДатаНачалаПросрочки = Элемент.ДатаГрафика;
Прервать;
КонецЕсли;
КонецЦикла;
// Проверка на просрочку и условия оплаты/отгрузки
Если ДатаНачалаПросрочки <> Неопределено Тогда
Если ТекущаяДата() > ДатаНачалаПросрочки Тогда
// Теперь нам нужно получить ПроцентОплаты и ПроцентОтгрузки.
// Эти данные были в ВыборкаЗаказы.
// Либо мы должны передать их параметрами в эту процедуру,
// либо получить их здесь отдельным запросом.
// Для примера, давайте получим их здесь.
ЗапросСостояние = Новый Запрос;
ЗапросСостояние.Текст = "ВЫБРАТЬ
| СостоянияЗаказовКлиентов.ПроцентОплаты КАК ПроцентОплаты,
| СостоянияЗаказовКлиентов.ПроцентОтгрузки КАК ПроцентОтгрузки
|ИЗ
| РегистрСведений.СостоянияЗаказовКлиентов КАК СостоянияЗаказовКлиентов
|ГДЕ
| СостоянияЗаказовКлиентов.Заказ = &Заказ";
ЗапросСостояние.УстановитьПараметр("Заказ", ЭтотОбъект.Ссылка);
РезультатСостояние = ЗапросСостояние.Выполнить().Выбрать();
Если РезультатСостояние.Следующий() Тогда
Если РезультатСостояние.ПроцентОплаты <> 100 Тогда
Если РезультатСостояние.ПроцентОтгрузки <> 100 Тогда
// Ваш код для работы с ТЧ и регистрами
// ВНИМАНИЕ: Запрос к РегистрНакопления.ТоварыКОтгрузке.Остатки
// с параметром Документы.ЗаказКлиента.НайтиПоНомеру("ТДУТ-000004", '20210626')
// является заглушкой для отладки и должен быть заменен на реальную ссылку.
// Здесь мы будем использовать ссылку на текущий объект.
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| ТоварыКОтгрузкеОстатки.ДокументОтгрузки.Ссылка КАК ДокументОтгрузкиСсылка,
| ТоварыКОтгрузкеОстатки.ВРезервеОстаток КАК ВРезервеОстаток,
| ТоварыКОтгрузкеОстатки.КОтгрузкеОстаток КАК КОтгрузкеОстаток,
| ТоварыКОтгрузкеОстатки.КОформлениюОстаток КАК КОформлениюОстаток,
| ТоварыКОтгрузкеОстатки.Номенклатура КАК Номенклатура
|ИЗ
| РегистрНакопления.ТоварыКОтгрузке.Остатки КАК ТоварыКОтгрузкеОстатки
|ГДЕ
| ТоварыКОтгрузкеОстатки.ДокументОтгрузки.Ссылка = &Ссылка";
Запрос.УстановитьПараметр("Ссылка", ЭтотОбъект.Ссылка); // Используем ссылку на текущий документ
Результат = Запрос.Выполнить().Выгрузить();
Индекс = 1;
ОстатокОтгрузкиДляДругойСтроки = 0;
Для каждого ЭлементНовый Из ЭтотОбъект.Товары Цикл // Используем ЭтотОбъект.Товары
Ном = ЭлементНовый.Номенклатура;
СтрРез = Результат.Найти(Ном, "Номенклатура");
// Важно: Проверьте, что СтрРез не Неопределено перед обращением к его свойствам
Если СтрРез = Неопределено Тогда
Продолжить; // Если номенклатура не найдена в остатках, пропускаем
КонецЕсли;
Если ЭлементНовый.ВариантОбеспечения = Перечисления.ВариантыОбеспечения.Отгрузить Тогда
РазницаДляПереноса = СтрРез.КОформлениюОстаток - СтрРез.КОтгрузкеОстаток;
Если ОстатокОтгрузкиДляДругойСтроки = 0 Тогда
ОстатокОтгрузкиДляДругойСтроки = ЭлементНовый.КоличествоУпаковок - РазницаДляПереноса;
Если ОстатокОтгрузкиДляДругойСтроки < 0 Тогда
Продолжить;
Иначе
Если Индекс = 1 Тогда
// Добавляем новую строку и корректируем текущую
НоваяСтрока = ЭтотОбъект.Товары.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, ЭлементНовый);
ЭлементНовый.КоличествоУпаковок = ЭлементНовый.КоличествоУпаковок - СтрРез.КОтгрузкеОстаток;
Индекс = Индекс + 1;
Иначе
// Корректируем текущую строку
ЭлементНовый.КоличествоУпаковок = СтрРез.КОтгрузкеОстаток;
ЭлементНовый.ВариантОбеспечения = Перечисления.ВариантыОбеспечения.НеТребуется;
КонецЕсли;
КонецЕсли;
КонецЕсли;
Если ЭлементНовый.ВариантОбеспечения <> Перечисления.ВариантыОбеспечения.Отгрузить Тогда
ЭлементНовый.ВариантОбеспечения = Перечисления.ВариантыОбеспечения.НеТребуется;
КонецЕсли;
КонецЕсли;
Если ЭлементНовый.ВариантОбеспечения <> Перечисления.ВариантыОбеспечения.Отгрузить И ЭлементНовый.ВариантОбеспечения <> Перечисления.ВариантыОбеспечения.НеТребуется Тогда
ЭлементНовый.ВариантОбеспечения = Перечисления.ВариантыОбеспечения.НеТребуется;
КонецЕсли;
КонецЦикла;
// Запись документа после всех изменений
ЭтотОбъект.Записать(РежимЗаписиДокумента.Проведение);
КонецЕсли; // ПроцентОтгрузки <> 100
КонецЕсли; // ПроцентОплаты <> 100
КонецЕсли; // РезультатСостояние.Следующий()
КонецЕсли; // ТекущаяДата() > ДатаНачалаПросрочки
КонецЕсли; // ДатаНачалаПросрочки <> Неопределено
КонецПроцедуры
Обратите внимание, что мы убрали внешний цикл по заказам из этой процедуры, так как она теперь будет вызываться для одного конкретного объекта документа. Также мы вставили часть логики для определения ДатаНачалаПросрочки и получения процентов оплаты/отгрузки, которая ранее могла быть частью внешнего цикла.
Мы создадим новый или используем существующий общий модуль (например, АвтоСнятиеСРезерваВЗаказеКлиента), который будет иметь свойства "Сервер" и "Вызов сервера" (или "Глобальный"). В этом модуле мы разместим процедуру, которая будет вызвана регламентным заданием.
// В общем модуле "АвтоСнятиеСРезерваВЗаказеКлиента" (Сервер, ВызовСервера)
Процедура АвтоматическоеСнятиеСРезерваВЗаказеКлиентаПриПросрочке() Экспорт
// Запрос для отбора заказов, которые необходимо обработать
ЗапросДокументЗаказ = Новый Запрос;
ЗапросДокументЗаказ.Текст = "ВЫБРАТЬ
| ЗаказКлиента.Ссылка КАК Ссылка,
| СостоянияЗаказовКлиентов.Состояние КАК Состояние,
| СостоянияЗаказовКлиентов.ПроцентОплаты КАК ПроцентОплаты,
| СостоянияЗаказовКлиентов.ПроцентОтгрузки КАК ПроцентОтгрузки
|ИЗ
| РегистрСведений.СостоянияЗаказовКлиентов КАК СостоянияЗаказовКлиентов
| ЛЕВОЕ СОЕДИНЕНИЕ Документ.ЗаказКлиента КАК ЗаказКлиента
| ПО СостоянияЗаказовКлиентов.Заказ = ЗаказКлиента.Ссылка
|ГДЕ
| СостоянияЗаказовКлиентов.Состояние <> &Состояние
| И СостоянияЗаказовКлиентов.Состояние <> &Состояние1
| И ЗаказКлиента.Дата МЕЖДУ &Дата1 И &Дата2";
// Устанавливаем параметры запроса.
// Обратите внимание: даты и состояния должны быть актуальными для ваших нужд.
ЗапросДокументЗаказ.УстановитьПараметр("Состояние", Перечисления.СостоянияЗаказовКлиентов.Закрыт);
ЗапросДокументЗаказ.УстановитьПараметр("Состояние1", Перечисления.СостоянияЗаказовКлиентов.ГотовКЗакрытию);
ЗапросДокументЗаказ.УстановитьПараметр("Дата1", Дата('2021',4,30)); // Используем функцию Дата() для создания дат
ЗапросДокументЗаказ.УстановитьПараметр("Дата2", Дата('2021',6,27)); // или НачалоДня()/КонецДня() для текущих дат
ВыборкаЗаказы = ЗапросДокументЗаказ.Выполнить().Выбрать();
// Обход заказов и вызов процедуры в модуле объекта для каждого
Пока ВыборкаЗаказы.Следующий() Цикл
Попытка
// Получаем объект документа по ссылке
ДокОбъект = ВыборкаЗаказы.Ссылка.ПолучитьОбъект();
// Вызываем нашу экспортную процедуру из модуля объекта
ДокОбъект.СнятьРезервПриПросрочке(); // Здесь мы вызываем новую процедуру
// В случае успешного выполнения можно записать сообщение в журнал регистрации
ЗаписьЖурналаРегистрации("АвтоматическоеСнятиеРезерва", УровеньЖурналаРегистрации.Информация, , ВыборкаЗаказы.Ссылка, "Резерв по заказу " + ВыборкаЗаказы.Ссылка + " успешно снят.");
Исключение
// В случае ошибки записываем ее в журнал регистрации
ЗаписьЖурналаРегистрации("АвтоматическоеСнятиеРезерва", УровеньЖурналаРегистрации.Ошибка, , ВыборкаЗаказы.Ссылка, ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
КонецПроцедуры
В этом общем модуле мы выполняем первоначальный запрос для получения списка документов, которые требуют обработки. Затем в цикле для каждого найденного документа:
ВыборкаЗаказы.Ссылка).ПолучитьОбъект() для получения объекта документа. Это позволяет нам работать с его реквизитами и вызывать его экспортные процедуры.СнятьРезервПриПросрочке(), которую мы разместили в модуле объекта.Попытка...Исключение и записывать информацию о выполнении (или об ошибках) в Журнал регистрации 1С. Это позволит нам отслеживать работу регламентного задания, поскольку оно выполняется в фоновом режиме без пользовательского интерфейса.После того как код готов, нам нужно настроить регламентное задание:
ИмяОбщегоМодуля.ИмяЭкспортнойПроцедуры, например, АвтоСнятиеСРезерваВЗаказеКлиента.АвтоматическоеСнятиеСРезерваВЗаказеКлиентаПриПросрочке.Убедитесь, что общий модуль, содержащий процедуру для регламентного задания, имеет флажок "Сервер" и при необходимости "Вызов сервера" (если планируется вызывать её и с клиента). Для регламентных заданий достаточно "Сервер".
Мы обратим внимание на несколько важных моментов, которые помогут нам улучшить наш код и обеспечить его надежную работу:
НачатьТранзакцию(), ЗафиксироватьТранзакцию(), ОтменитьТранзакцию()).ЗаписьЖурналаРегистрации() позволяет нам отслеживать ход выполнения регламентного задания, фиксировать ошибки и успешно обработанные документы. Это критически важно для отладки и мониторинга работы в продуктивной среде.Мы видим, что попытка открыть форму документа из регламентного задания является фундаментальным архитектурным нарушением в 1С:Предприятии, так как серверный контекст не предназначен для работы с клиентским интерфейсом. Правильное решение заключается в переносе всей бизнес-логики, не требующей интерактивного взаимодействия, в серверные модули — в частности, в модуль объекта документа для операций с конкретным документом, или в общий модуль для более универсальной логики. Это позволит нам эффективно автоматизировать процессы обработки данных в 1С:Предприятии, обеспечивая надежность и производительность.