При работе с конфигурациями 1С часто возникает необходимость запускать дополнительную логику, например, процедуру расчета остатков, после того как основной документ был изменен или проведен. Особенно это актуально, когда требуется модифицировать поведение типовой конфигурации без ее снятия с поддержки, используя расширения. В нашем случае, мы рассмотрим ситуацию с документом «ЗаказПокупателя» в конфигурации 1С:УНФ 1.6 на платформе 1С 8.3.20, где процедура расчета остатков должна выполняться после успешного проведения документа.
Основная сложность заключается в том, что стандартные обработчики событий модуля объекта, такие как ПередЗаписью и ПриЗаписи, выполняются внутри транзакции записи документа. Это означает, что изменения документа еще не зафиксированы в базе данных окончательно, и обращение к ним может привести к неактуальным данным или ошибкам, если транзакция будет отменена. Нам нужен механизм, который гарантированно срабатывает после того, как все изменения документа сохранены и его движения сформированы.
Прежде чем погружаться в решения, давайте рассмотрим, как платформа 1С обрабатывает запись документов. Это поможет нам выбрать наиболее подходящий момент для выполнения наших расчетов.
ПередЗаписью и ПриЗаписи (модуль объекта): Эти обработчики, как мы уже упомянули, выполняются внутри транзакции записи документа. Если в них возникнет ошибка, вся операция записи (и проведения) будет отменена. В этот момент изменения документа еще не зафиксированы в базе данных окончательно. Следовательно, запускать процедуру расчета остатков, которая должна оперировать уже измененными данными, здесь не рекомендуется, так как это может привести к некорректным результатам или блокировкам.
ПослеЗаписи (модуль формы на клиенте): Это событие срабатывает только на клиенте после успешной записи документа, но только при интерактивных действиях пользователя через форму. Оно не будет вызвано при программной записи документа или при проведении на сервере. Поэтому этот вариант не является универсальным для наших целей.
ПослеЗаписиНаСервере (модуль формы на сервере): Аналогично предыдущему пункту, это событие формы, срабатывает на сервере, но после успешной записи документа. Однако, оно также привязано к контексту формы и не является универсальным решением для фоновых или программных изменений, происходящих вне пользовательского интерфейса.
Суть проблемы: Нам нужен механизм, который гарантированно запускается после того, как документ успешно проведен и все его движения сформированы и записаны в базу данных, и при этом работает как для интерактивных, так и для программных изменений документа, а также доступен из расширения.
Теперь, когда мы прояснили контекст, рассмотрим несколько эффективных способов решения этой задачи.
Подписки на события — это мощный и гибкий механизм в 1С, который позволяет "подключаться" к стандартным событиям объектов конфигурации, не изменяя саму конфигурацию. Это делает их идеальным инструментом для использования в расширениях. Данное решение было выбрано автором на форуме и подтвердило свою эффективность.
Для проведения документа – подписка на событие ОбработкаПроведения:
Это событие идеально подходит, когда нам нужно выполнить дополнительную логику в момент проведения документа. Код в обработчике подписки будет выполнен в рамках той же транзакции, что и основное проведение документа. Это означает, что если в вашем расчете остатков произойдет ошибка, то и само проведение документа будет отменено, что обеспечивает атомарность всей операции. То есть, либо весь документ проводится вместе с расчетом остатков, либо ничего не проводится.
Давайте разберем по шагам, как мы можем создать такую подписку в расширении:
Создаем расширение: Сначала откройте или создайте новое расширение в режиме конфигуратора.
Добавляем подписку на события: В дереве объектов расширения найдите узел "Подписки на события" и добавьте новую подписку.
Настраиваем параметры подписки:
Имя: Присвоим понятное имя, например, РасчетОстатков_ЗаказПокупателя_Проведение. Это имя будет использоваться для идентификации подписки.
Событие: Выбираем ОбработкаПроведения. Это событие вызывается непосредственно перед формированием движений документа при его проведении.
Источник: Укажите Документ.ЗаказПокупателя. Это означает, что наша подписка будет реагировать только на проведение документов типа "ЗаказПокупателя".
Обработчик: Создайте новую экспортную процедуру в общем модуле вашего расширения (или в новом общем модуле расширения). Например, назовем ее МойОбщийМодульРасширения.ОбработкаПроведенияЗаказПокупателя. Этот общий модуль должен быть "Серверным".
Реализуем код обработчика: В модуле обработчика добавьте следующий код:
// МойОбщийМодульРасширения
Процедура ОбработкаПроведенияЗаказПокупателя(Источник, Отказ, РежимПроведения) Экспорт
Если ТипЗнч(Источник) = Тип("ДокументОбъект.ЗаказПокупателя") Тогда
// Здесь мы можем быть уверены, что работаем с документом "ЗаказПокупателя"
// Источник - это объект документа, который проводится
Сообщить("Выполняется расчет остатков после проведения ЗаказаПокупателя: " + Источник.Номер + " от " + Источник.Дата);
// Вызовем нашу процедуру расчета остатков
// Предполагается, что эта процедура также определена в вашем расширении
МойОбщийМодульРасширения.РассчитатьОстаткиПоЗаказу(Источник);
КонецЕсли;
КонецПроцедуры
// Отдельная процедура расчета остатков, которую мы вызываем
// Её также размещаем в МойОбщийМодульРасширения
Процедура РассчитатьОстаткиПоЗаказу(ДокументЗаказПокупателя) Экспорт
// Здесь размещаем ваш код для расчета остатков.
// Например, можно обойти строки табличной части документа,
// выполнить запросы к регистрам накопления для актуализации данных,
// или записать вспомогательную информацию.
// Помните, что мы все еще внутри транзакции проведения.
// Если расчет сложный и меняет много данных,
// лучше рассмотреть фоновое задание (см. следующее решение),
// чтобы не блокировать систему.
Сообщить("Расчет остатков для Заказа " + ДокументЗаказПокупателя.Номер + " успешно выполнен.");
КонецПроцедуры
В этом примере Источник — это объект документа "ЗаказПокупателя", который мы проводим. Передав его в нашу процедуру РассчитатьОстаткиПоЗаказу, мы можем работать с его данными.
Для отмены проведения и записи без проведения – подписка на событие ПриЗаписи:
Если нам необходимо реагировать не только на проведение, но и на отмену проведения или просто на запись документа без проведения (например, если документ был сохранен, но не проведен), мы можем использовать подписку на событие ПриЗаписи. В этом случае особенно важно анализировать параметры, чтобы понять контекст записи и избежать лишних расчетов.
Рассмотрим создание такой подписки:
Добавляем еще одну подписку на события: Аналогично предыдущему шагу, создайте новую подписку в расширении.
Настраиваем параметры подписки:
Имя: Например, РасчетОстатков_ЗаказПокупателя_ПриЗаписи.
Событие: Выберите ПриЗаписи. Это событие вызывается при записи объекта в базу данных.
Источник: Документ.ЗаказПокупателя.
Обработчик: МойОбщийМодульРасширения.ПриЗаписиЗаказПокупателя.
Реализуем код обработчика: В модуле обработчика добавьте:
// МойОбщийМодульРасширения
Процедура ПриЗаписиЗаказПокупателя(Источник, Отказ) Экспорт
Если ТипЗнч(Источник) = Тип("ДокументОбъект.ЗаказПокупателя") Тогда
// Источник - это объект документа, который записывается
// Отказ - булево, если установлено в Истина, запись будет отменена
// Проверим, является ли документ проведенным
// ИЛИ если он был проведен, а сейчас отменяется проведение
// Для точного определения изменения состояния "Проведен", возможно, потребуется
// получить исходное состояние документа до записи (например, через ДокументСсылка.ПолучитьОбъект()).
// Для простоты примера, проверим текущее состояние и, при необходимости,
// можно добавить более сложную логику.
Если Источник.Проведен Тогда
Сообщить("Выполняется расчет остатков после записи проведенного ЗаказаПокупателя: " + Источник.Номер);
МойОбщийМодульРасширения.РассчитатьОстаткиПоЗаказу(Источник);
ИначеЕсли НЕ Источник.Проведен И ПроверитьПроведениеДоЗаписи(Источник.Ссылка) Тогда
// Предположим, что документ был проведен, но сейчас его снимают с проведения
Сообщить("Выполняется расчет остатков после отмены проведения ЗаказаПокупателя: " + Источник.Номер);
МойОбщийМодульРасширения.РассчитатьОстаткиПоЗаказу(Источник);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
// Вспомогательная процедура для определения, был ли документ проведен до текущей записи
// Эту процедуру нужно будет доработать для получения достоверной информации.
// Например, можно сравнивать текущее состояние с состоянием из базы данных.
Функция ПроверитьПроведениеДоЗаписи(СсылкаНаДокумент) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ДокументЗаказПокупателя.Проведен
|ИЗ
| Документ.ЗаказПокупателя КАК ДокументЗаказПокупателя
|ГДЕ
| ДокументЗаказПокупателя.Ссылка = &Ссылка";
Запрос.УстановитьПараметр("Ссылка", СсылкаНаДокумент);
Результат = Запрос.Выполнить().Выбрать();
Если Результат.Следующий() Тогда
Возврат Результат.Проведен;
КонецЕсли;
Возврат Ложь; // Если документ новый или не найден
КонецФункции
Важно: Функция ПроверитьПроведениеДоЗаписи потребуется для точного определения, был ли документ проведен до текущей записи, чтобы отреагировать на отмену проведения. Её реализация может быть сложнее, если нужно учитывать все нюансы изменения состояния документа. В рамках подписки на событие ПриЗаписи мы еще находимся внутри транзакции.
Если процедура расчета остатков является ресурсоемкой, требует длительного времени выполнения или ее не обязательно выполнять в той же транзакции, что и проведение документа, то идеальным решением будет асинхронный запуск через фоновые задания. Этот метод гарантирует, что основная операция (проведение документа) не будет замедлена, а расчет произойдет после того, как документ будет гарантированно записан и его транзакция завершена.
Как это работает:
Вместо непосредственного вызова процедуры расчета в подписке на событие (или другом обработчике), мы инициируем создание и запуск фонового задания. Это задание будет выполнено сервером 1С в отдельном потоке после того, как текущая транзакция (например, проведение документа) будет успешно завершена. Таким образом, расчеты выполняются "в фоне", не блокируя пользователя.
Преимущества асинхронного подхода:
Пользователь не ждет: Основная операция (например, проведение документа) завершается быстро, что существенно улучшает пользовательский опыт.
Независимость транзакций: Ваша процедура гарантированно запускается после того, как транзакция записи документа успешно завершена. Ее выполнение происходит в отдельной транзакции или вообще без нее, не влияя на атомарность проведения.
Снижение блокировок: Длительные операции расчета остатков, выполняемые в фоновом режиме, снижают вероятность возникновения блокировок таблиц базы данных в момент интерактивной работы пользователей.
Отказоустойчивость: Фоновые задания могут иметь встроенные механизмы повторного выполнения в случае временных сбоев, что повышает надежность системы.
Реализация фонового задания:
Мы модифицируем наш обработчик подписки на событие ОбработкаПроведения (или ПриЗаписи), чтобы он запускал фоновое задание. Сначала нам потребуется определить процедуру, которая будет выполняться в фоновом задании. Эта процедура должна быть экспортной и находиться в общем модуле, который может быть вызван на сервере (обычно это общий модуль с установленным флагом "Сервер" и "Вызов сервера").
// МойОбщийМодульРасширенияФоновыеЗадания (можно создать новый общий модуль специально для фоновых заданий)
// Убедитесь, что для модуля установлен флаг "Сервер" и "Вызов сервера"
Процедура ВыполнитьРасчетОстатковВФоне(СсылкаНаДокументЗаказПокупателя) Экспорт
Если ТипЗнч(СсылкаНаДокументЗаказПокупателя) = Тип("ДокументСсылка.ЗаказПокупателя") Тогда
// Получаем объект документа по ссылке, чтобы работать с его актуальными данными
ДокументОбъект = СсылкаНаДокументЗаказПокупателя.ПолучитьОбъект();
Если ДокументОбъект <> Неопределено Тогда
Сообщить("Фоновый расчет остатков для ЗаказаПокупателя: " + ДокументОбъект.Номер + " от " + ДокументОбъект.Дата + " начат.");
// Здесь размещаем ваш код расчета остатков.
// Теперь мы работаем с *уже проведенным* и записанным в базу документом.
// Можно использовать ту же процедуру, что и в предыдущем примере.
МойОбщийМодульРасширения.РассчитатьОстаткиПоЗаказу(ДокументОбъект);
Сообщить("Фоновый расчет остатков для ЗаказаПокупателя: " + ДокументОбъект.Номер + " завершен.");
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Теперь изменим наш обработчик подписки (например, ОбработкаПроведенияЗаказПокупателя), чтобы он запускал это фоновое задание:
// МойОбщийМодульРасширения
Процедура ОбработкаПроведенияЗаказПокупателя(Источник, Отказ, РежимПроведения) Экспорт
Если ТипЗнч(Источник) = Тип("ДокументОбъект.ЗаказПокупателя") Тогда
// Проверяем, что документ проводится успешно, прежде чем запускать фоновое задание
Если НЕ Отказ Тогда
// Запускаем фоновое задание, передавая ссылку на проведенный документ
ИмяМетода = "МойОбщийМодульРасширенияФоновыеЗадания.ВыполнитьРасчетОстатковВФоне";
Параметры = Новый Массив();
Параметры.Добавить(Источник.Ссылка); // Передаем ссылку, а не объект
// Создаем фоновое задание. Третий параметр - описание задания,
// которое будет видно в списке фоновых заданий.
ФоновыеЗадания.Выполнить(ИмяМетода, Параметры, "Расчет остатков по Заказу " + Источник.Номер + " от " + Источник.Дата);
Сообщить("Инициировано фоновое задание для расчета остатков по Заказу: " + Источник.Номер);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Примечание: При запуске фоновых заданий всегда предпочтительнее передавать ссылки на объекты, а не сами объекты. Это минимизирует объем данных, передаваемых между потоками, и гарантирует, что внутри фонового задания мы получим актуальный объект документа из базы данных, который уже был записан.
Платформа 1С предоставляет удобный механизм модификации процедур и функций базовой конфигурации напрямую в расширении с помощью аннотаций &Перед, &После и &Вместо. Этот подход позволяет встроить вашу логику непосредственно в тело типовой процедуры, как бы "обертывая" ее.
Как это работает:
Мы можем расширить процедуру ОбработкаПроведения, которая находится в модуле объекта документа ЗаказПокупателя. Для этого в расширении создается одноименная процедура, но с добавлением аннотации &После.
Ваш код, помеченный &После, будет выполнен сразу после завершения типовой процедуры проведения, но все еще внутри той же транзакции, что и само проведение документа. Это важно понимать для контроля целостности данных.
Реализация:
Находим модуль объекта: В расширении найдите документ ЗаказПокупателя, затем перейдите в модуль его объекта.
Создаем расширенную процедуру: Создайте новую процедуру (или модифицируйте существующую, если она уже есть в расширении для этого модуля) с именем ОбработкаПроведения и аннотацией &После:
// В модуле объекта расширенного документа "ЗаказПокупателя"
// (расширение -> Документы -> ЗаказПокупателя -> Модуль объекта)
&После("ОбработкаПроведения")
Процедура Расш_ОбработкаПроведения(Отказ, РежимПроведения)
// "ЭтотОбъект" неявно доступен и является объектом документа
// Мы находимся в контексте объекта документа, который только что был проведен
// Проверим, что проведение прошло успешно (Отказ = Ложь)
Если НЕ Отказ Тогда
Сообщить("Расчет остатков через &После для ЗаказаПокупателя: " + ЭтотОбъект.Номер + " от " + ЭтотОбъект.Дата);
МойОбщийМодульРасширения.РассчитатьОстаткиПоЗаказу(ЭтотОбъект);
КонецЕсли;
КонецПроцедуры
Важный нюанс: Как мы уже указывали ранее, код в процедуре с аннотацией &После для ОбработкаПроведения будет выполняться внутри общей транзакции проведения документа. Это означает, что если ваша процедура расчета остатков изменяет данные и в ней произойдет ошибка, то вся транзакция проведения документа будет отменена. Используйте этот метод, если ваш расчет критически важен для целостности проведения документа и не является слишком ресурсоемким.
Мы рассмотрели несколько эффективных подходов для запуска процедуры расчета остатков после изменения документа "Заказ покупателя" в расширении 1С. Выбор конкретного метода зависит от требований к вашему расчету, к производительности и к целостности данных:
Подписки на события (ОбработкаПроведения, ПриЗаписи): Это универсальное и надежное решение, которое хорошо подходит для большинства случаев. Оно работает как при интерактивном, так и при программном изменении документа. Позволяет контролировать момент выполнения в рамках транзакции проведения, обеспечивая атомарность операции.
Асинхронное выполнение через фоновые задания: Этот метод предпочтителен для ресурсоемких и длительных расчетов, которые не критичны для немедленного завершения транзакции проведения. Он обеспечивает лучший пользовательский опыт, так как проведение документа завершается быстро, снижает нагрузку на базу данных и гарантирует выполнение расчета после завершения основной транзакции.
Аннотации расширения (&После): Удобный способ встраивания кода напрямую в типовые процедуры без использования подписок. Подходит, если ваш расчет неразрывно связан с логикой проведения документа и должен выполняться внутри той же транзакции, не являясь при этом слишком тяжелым или потенциально конфликтным.
Для вашего конкретного случая, когда требуется "расчет остатков", который может быть достаточно сложным и потенциально влиять на производительность системы, мы настоятельно рекомендуем рассмотреть асинхронный запуск через фоновые задания. Это позволит документу проводиться быстро, не задерживая пользователя, а все необходимые расчеты будут выполнены в фоновом режиме, минимизируя блокировки и повышая общую производительность системы.
Если же расчет остатков является быстрой операцией и должен быть синхронно связан с проведением (т.е., либо проводится документ вместе с расчетом, либо ничего не проводится), то использование подписки на ОбработкаПроведения или аннотации &После("ОбработкаПроведения") будет корректным и безопасным выбором.
Всегда тщательно анализируйте требования к производительности, целостности данных и пользовательскому опыту при выборе оптимального механизма для внедрения вашей дополнительной логики в 1С.