Как надежно установить реквизит документа после успешного проведения и избежать повторных действий?

Программист 1С v8.3 (Управляемые формы)
← На главную

В работе с платформой 1С:Предприятие 8.3 программисты часто сталкиваются с задачей, когда необходимо изменить реквизит документа или выполнить некое действие (например, отправить письмо) только после того, как документ был успешно проведен. При этом важно учесть особенности транзакционности, поведение пользовательского интерфейса и избежать нежелательных повторных срабатываний. Давайте рассмотрим эту проблему и найдем надежные способы ее решения.

Проблема: изменение реквизита при проведении и его потеря

Исходная ситуация, с которой мы столкнулись, заключается в следующем: есть документ, в котором после успешного проведения необходимо установить некий булевый реквизит в значение Истина. При этом попытка установки этого реквизита в обработчике ПередЗаписью или ПриЗаписи может привести к нежелательным последствиям. Например, если проведение завершится ошибкой, то транзакция записи будет откачена, и реквизит не должен сохраняться как Истина.

Однако, как нам объяснили в сообщении 12, проблема глубже. Допустим, мы используем подписку на событие, которая после проведения документа устанавливает реквизит об отправке письма в Истина и, возможно, формирует запись в регистр для рассылки. Если пользователь проводит документ по кнопке "ОК" (что подразумевает запись и закрытие формы), документ проводится, закрывается, и реквизит сохраняется. Но если документ проводится через кнопку "Провести" на панели формы, он остается модифицированным (помеченным звездочкой *), и при закрытии формы пользователь может нажать "Нет" на вопрос о сохранении изменений. В этом случае все изменения, включая наш реквизит, будут утеряны. Как следствие, при последующем перепроведении документа письмо может быть отправлено повторно, если логика проверки не учитывает эти нюансы.

Таким образом, перед нами стоят две ключевые задачи:

  1. Гарантировать, что реквизит устанавливается только при успешном проведении и его изменения сохраняются.
  2. Предотвратить повторные действия (например, отправку письма) при возможных ошибках или неправильных действиях пользователя.

Решение 1: Использование независимого Регистра Сведений для хранения состояния

Первый и наиболее рекомендуемый подход – это отказаться от хранения флага "отправлено" или "обработано" непосредственно в реквизите документа. Вместо этого мы будем использовать отдельный независимый Регистр Сведений. Этот подход позволяет отделить логику учета от логики сервисных флагов и обеспечивает большую гибкость и надежность.

Разберем шаги по реализации:

  1. Создаем Регистр Сведений. Назовем его, например, СостоянияДокументов или ОтправленныеПисьма. Этот регистр должен быть:

    • Независимым: не подчинен регистратору, чтобы записи в нем могли существовать независимо от проведения документа.
    • Непериодическим.
    • Содержать измерения: Документ (тип - ДокументСсылка.ВашДокумент).
    • Содержать ресурсы: Отправлено (Булево, по умолчанию Ложь), ДатаПоследнейПопытки (Дата), СтатусОтправки (Перечисление, например, "В Очереди", "Отправлено", "ОшибкаОтправки").
  2. В процедуре ОбработкаПроведения документа (в модуле объекта) мы будем записывать намерение отправить письмо или изменить статус. Это должно происходить в той же транзакции, что и проведение документа.

    Важно: запись в регистр сведений в ОбработкаПроведения будет частью общей транзакции проведения документа. Если проведение завершится ошибкой, эта запись автоматически будет отменена.

    
    // В модуле объекта документа, процедура ОбработкаПроведения
    Процедура ОбработкаПроведения(Отказ, РежимПроведения)
    
        // ... Ваши движения документа ...
    
        Если Не Отказ Тогда
            // Документ успешно проводится, записываем намерение в Регистр Сведений
            МенеджерЗаписи = РегистрыСведений.ОтправленныеПисьма.СоздатьМенеджерЗаписи();
            МенеджерЗаписи.Документ = ЭтотОбъект.Ссылка;
    
            // Попытаемся прочитать существующую запись
            Если МенеджерЗаписи.Прочитать() Тогда
                // Запись для этого документа уже существует
                // Мы можем обновить ее статус, если нужно переотправить,
                // или просто убедиться, что она в состоянии "В очереди".
                Если МенеджерЗаписи.Отправлено Тогда
                    // Письмо уже было успешно отправлено ранее.
                    // Решаем, нужно ли переотправлять.
                    // В данном случае, мы хотим предотвратить повторную отправку,
                    // поэтому не меняем статус или сбрасываем его на "В Очереди",
                    // если есть необходимость в повторной отправке.
                    // Для предотвращения дублирования обычно оставляют как есть
                    // или сбрасывают статус для повторной обработки
                    // только при определенных условиях (например, если документ был изменен).
                    // В текущем сценарии (сообщение 12) мы хотим избежать дублирования.
                    // Поэтому здесь мы ничего не делаем, если уже Отправлено.
                Иначе
                    // Письмо было в очереди или с ошибкой, ставим снова в очередь
                    МенеджерЗаписи.Отправлено = Ложь;
                    МенеджерЗаписи.СтатусОтправки = ПредопределенноеЗначение("Перечисление.СтатусыОтправкиПисем.В_Очереди");
                    МенеджерЗаписи.ДатаПоследнейПопытки = ТекущаяДата();
                    МенеджерЗаписи.Записать();
                КонецЕсли;
            Иначе
                // Записи для этого документа еще нет, создаем новую
                МенеджерЗаписи.Отправлено = Ложь; // Помечаем как "в очереди"
                МенеджерЗаписи.СтатусОтправки = ПредопределенноеЗначение("Перечисление.СтатусыОтправкиПисем.В_Очереди");
                МенеджерЗаписи.ДатаПоследнейПопытки = ТекущаяДата();
                МенеджерЗаписи.Записать();
            КонецЕсли;
        КонецЕсли;
    
    КонецПроцедуры
    

    Эта логика гарантирует, что запись в регистр появится только если документ успешно проведен. Если же проведение завершится с Отказ = Истина, то все изменения, включая запись в регистр сведений, будут отменены.

Решение 2: Асинхронная обработка с помощью Регламентного задания

Отправка писем, формирование сложных отчетов или другие ресурсоемкие операции не должны выполняться внутри транзакции проведения документа. Это может привести к "зависанию" транзакции, блокировкам таблиц базы данных и ухудшению производительности системы. Современный стандарт разработки (рекомендации 1С и БСП) предполагает вынос таких действий в фоновые (асинхронные) процессы.

Для этого мы будем использовать Регламентное задание:

  1. Создаем Регламентное задание. Это объект конфигурации, который будет запускать наш код по расписанию (например, каждую минуту или каждые 5 минут).

  2. Разрабатываем код для Регламентного задания. В этом коде мы будем выполнять следующее:

    • Выбирать из нашего Регистра Сведений.ОтправленныеПисьма все записи, где Отправлено = Ложь или СтатусОтправки = ПредопределенноеЗначение("Перечисление.СтатусыОтправкиПисем.В_Очереди").
    • Для каждой такой записи выполнять необходимые действия (например, отправлять письмо).
    • После успешного выполнения действия обновлять запись в регистре, устанавливая Отправлено = Истина, ДатаПоследнейПопытки = ТекущаяДата() и СтатусОтправки = ПредопределенноеЗначение("Перечисление.СтатусыОтправкиПисем.Отправлено").
    • Предусматривать обработку ошибок: если отправка письма не удалась, обновлять статус на СтатусОтправки = ПредопределенноеЗначение("Перечисление.СтатусыОтправкиПисем.ОшибкаОтправки"), чтобы можно было позже проанализировать и повторить попытку.
    
    // В общем модуле, предназначенном для Регламентных заданий
    Процедура ОтправитьПисьмаДокументов() Экспорт
    
        Запрос = Новый Запрос;
        Запрос.Текст =
        "ВЫБРАТЬ
        |    ОтправленныеПисьма.Документ,
        |    ОтправленныеПисьма.СтатусОтправки
        |ИЗ
        |    РегистрСведений.ОтправленныеПисьма КАК ОтправленныеПисьма
        |ГДЕ
        |    ОтправленныеПисьма.СтатусОтправки = &СтатусВ_Очереди
        |    ИЛИ ОтправленныеПисьма.СтатусОтправки = &СтатусОшибка"; // Можно также обрабатывать ошибки
    
        Запрос.УстановитьПараметр("СтатусВ_Очереди", ПредопределенноеЗначение("Перечисление.СтатусыОтправкиПисем.В_Очереди"));
        Запрос.УстановитьПараметр("СтатусОшибка", ПредопределенноеЗначение("Перечисление.СтатусыОтправкиПисем.ОшибкаОтправки"));
    
        Выборка = Запрос.Выполнить().Выбрать();
    
        Пока Выборка.Следующий() Цикл
            МенеджерЗаписи = РегистрыСведений.ОтправленныеПисьма.СоздатьМенеджерЗаписи();
            МенеджерЗаписи.Документ = Выборка.Документ;
    
            Попытка
                // Начинаем новую транзакцию для каждой операции отправки
                НачатьТранзакцию();
    
                // Блокируем запись, чтобы избежать коллизий, если несколько регламентных заданий работают параллельно
                Блокировка = Новый БлокировкаДанных;
                ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ОтправленныеПисьма");
                ЭлементБлокировки.УстановитьЗначение("Документ", Выборка.Документ);
                ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
                Блокировка.Заблокировать();
    
                Если МенеджерЗаписи.Прочитать() Тогда
                    // Проверяем актуальный статус после блокировки и прочтения
                    Если МенеджерЗаписи.СтатусОтправки = ПредопределенноеЗначение("Перечисление.СтатусыОтправкиПисем.В_Очереди")
                        Или МенеджерЗаписи.СтатусОтправки = ПредопределенноеЗначение("Перечисление.СтатусыОтправкиПисем.ОшибкаОтправки") Тогда
    
                        // ... Здесь размещаем логику отправки письма для Выборка.Документ ...
                        // Например: ОтправитьПисьмоПоДокументу(Выборка.Документ);
    
                        // Если отправка прошла успешно
                        МенеджерЗаписи.Отправлено = Истина;
                        МенеджерЗаписи.ДатаПоследнейПопытки = ТекущаяДата();
                        МенеджерЗаписи.СтатусОтправки = ПредопределенноеЗначение("Перечисление.СтатусыОтправкиПисем.Отправлено");
                        МенеджерЗаписи.Записать();
                    КонецЕсли;
                КонецЕсли;
    
                ЗафиксироватьТранзакцию();
    
            Исключение
                // Если произошла ошибка при отправке или в транзакции
                ОтменитьТранзакцию();
    
                МенеджерЗаписи = РегистрыСведений.ОтправленныеПисьма.СоздатьМенеджерЗаписи();
                МенеджерЗаписи.Документ = Выборка.Документ;
                Если МенеджерЗаписи.Прочитать() Тогда
                    МенеджерЗаписи.СтатусОтправки = ПредопределенноеЗначение("Перечисление.СтатусыОтправкиПисем.ОшибкаОтправки");
                    МенеджерЗаписи.ДатаПоследнейПопытки = ТекущаяДата();
                    МенеджерЗаписи.Записать(); // Записываем ошибку
                КонецЕсли;
    
                ЗаписатьЖурналРегистрации("Отправка писем", УровеньЖурналаРегистрации.Ошибка, , , ОписаниеОшибки());
    
            КонецПопытки;
        КонецЦикла;
    
    КонецПроцедуры
    

Такой подход полностью снимает вопрос с блокировками при проведении документов и повторной отправкой писем, так как сама отправка происходит вне пользовательской сессии, а логика в регламентном задании обеспечивает контроль статуса и исключает дублирование.

Защита от повторных отправлений

Для предотвращения ситуации, описанной в сообщении 12 (когда письмо улетает повторно при перепроведении), важно иметь жесткую проверку. В нашей схеме с Регистром Сведений это реализуется следующим образом:

  1. При постановке в очередь (в ОбработкаПроведения): мы уже включили логику проверки существования записи в регистре. Если запись есть и письмо уже отправлено (Отправлено = Истина), мы не ставим его в очередь повторно, если это не предусмотрено бизнес-логикой (например, если документ был существенно изменен и требуется новое письмо).

  2. В Регламентном задании: Мы используем явную транзакцию и исключительную блокировку по записи регистра. Это критически важно, если у вас может быть несколько фоновых процессов или высокая конкуренция за записи. Логика такова:

    • Начинаем транзакцию.
    • Блокируем запись в регистре для конкретного документа (чтобы никакой другой процесс не мог одновременно изменить ее).
    • Повторно читаем запись и проверяем статус. Возможно, пока мы выбирали записи из запроса, другое регламентное задание уже успело обработать эту запись.
    • Если статус по-прежнему указывает на необходимость отправки, выполняем действие и обновляем запись.
    • Фиксируем транзакцию.

    Пример кода для блокировки был продемонстрирован выше в разделе "Решение 2: Асинхронная обработка с помощью Регламентного задания".

Обновление формы после проведения

Если вам все же необходимо, чтобы статус отправки (или другой реквизит) отображался в форме документа, и вы решили использовать дополнительный реквизит документа (хотя и не рекомендуется для флагов, управляющих внешними действиями), или просто хотите, чтобы форма перестала быть "модифицированной" после асинхронной обработки, мы должны обновить данные формы. Проблема, упомянутая в сообщении 12, когда форма остается модифицированной после проведения с панели, возникает потому, что изменения объекта на сервере не всегда автоматически синхронизируются с состоянием формы на клиенте.

Для решения этой проблемы используем методы обновления формы:

  1. В обработчике формы ПослеЗаписиНаСервере:

    
    // В модуле формы документа
    &НаСервере
    Процедура ПослеЗаписиНаСервере(ТекущийОбъект, ПараметрыЗаписи)
        // Эта процедура вызывается на сервере после успешной записи объекта
        // Здесь ТекущийОбъект - это уже записанный объект документа.
    
        // Проверяем, если запись была в режиме проведения
        Если ПараметрыЗаписи.Свойство("РежимЗаписи")
            И ПараметрыЗаписи.РежимЗаписи = РежимЗаписиДокумента.Проведение Тогда
    
            // Если мы изменили реквизит объекта в ОбработкеПроведения
            // или ожидаем его асинхронного изменения через Регистр Сведений
            // и хотим, чтобы форма об этом "узнала", можно перечитать объект формы.
            // Это также сбросит флаг модифицированности (*).
            ОбъектФормы.Прочитать(); // Перечитает данные объекта формы из базы данных
    
            // Если есть другие формы, которые могут быть открыты
            // и должны знать об изменении данного документа,
            // можно использовать механизм оповещения:
            ОповеститьОбИзменении(ТекущийОбъект.Ссылка);
        КонецЕсли;
    КонецПроцедуры
    
    // На клиенте, если нужно обработать оповещение
    Процедура ОбработкаОповещения(ИмяСобытия, Параметр, Источник)
        Если ИмяСобытия = "ОбновлениеСтатусаДокумента" Тогда
            // Например, если регламентное задание после отправки письма
            // оповещает об изменении статуса.
            Если Параметр = Объект.Ссылка Тогда
                Объект.Прочитать(); // Перечитываем объект, чтобы обновить данные на форме
            КонецЕсли;
        КонецЕсли;
    КонецПроцедуры
    

    Метод ОбъектФормы.Прочитать() (или Объект.Прочитать(), если мы работаем с данными формы напрямую через объект) заставляет форму перечитать свои данные из базы данных, что обновляет отображаемые реквизиты и сбрасывает признак модифицированности (*).

  2. Использование ОповеститьОбИзменении(Ссылка): Этот метод уведомляет все открытые формы, которые подписаны на оповещения для данной ссылки, о том, что объект изменился. Это полезно, если вы хотите обновить не только текущую форму, но и, например, форму списка документов или другую форму, где отображается информация об этом документе. Внутри формы-получателя должно быть реализован обработчик ОбработкаОповещения.

Альтернативные и дополнительные подходы

Помимо основных решений, существуют другие методы и концепции, которые могут быть полезны в различных сценариях:

  1. Использование реквизита "Статус" (Перечисление):

    Вместо множества булевых флагов (например, "Отправлено", "Подтверждено", "Обработано") рассмотрите возможность использования одного реквизита типа Перечисление, который будет хранить "Статус" документа. Это позволяет более гибко управлять жизненным циклом документа. Например, статусы могут быть: "Подготовлен", "В Очереди на Отправку", "Отправлен", "Ошибка Отправки", "Подтвержден", "Аннулирован". Изменение статуса на "Отправлен" должно происходить строго через специализированную процедуру, которая вызывается после успешного завершения транзакции записи или через механизм задач (Регламентное задание).

  2. Использование коллекции ДополнительныеСвойства:

    Коллекция ДополнительныеСвойства объекта документа позволяет передавать временные данные между различными обработчиками событий, которые выполняются в рамках одной транзакции (например, от ОбработкаПроведения до ПриЗаписи подписки на событие). Это может быть полезно, если вам нужно установить реквизит документа именно в конце цепочки транзакционных событий, но при этом вы не хотите использовать временные таблицы или глобальные переменные.

    
    // В процедуре ОбработкаПроведения модуля объекта документа
    Процедура ОбработкаПроведения(Отказ, РежимПроведения)
        // ... Ваши движения ...
        Если Не Отказ Тогда
            // Если проведение успешно, помечаем для последующей обработки
            ЭтотОбъект.ДополнительныеСвойства.Вставить("НужноОтметитьОтправку", Истина);
        КонецЕсли;
    КонецПроцедуры
    
    // В подписке на событие "ПриЗаписи" для вашего документа
    Процедура ПриЗаписиДокумента(Источник, Отказ)
        Если Источник.ДополнительныеСвойства.Свойство("НужноОтметитьОтправку")
            И Источник.ДополнительныеСвойства.НужноОтметитьОтправку Тогда
            // Здесь мы уверены, что документ успешно проведен
            // и эта запись происходит в рамках той же транзакции.
            // Устанавливаем реквизит документа.
            Источник.РеквизитОтправлено = Истина;
            // Не требуется вызывать Источник.Записать(), так как объект уже в процессе записи.
        КонецЕсли;
    КонецПроцедура
    

    Однако важно помнить, что этот подход не решает проблему модифицированности формы, если реквизит меняется в ОбработкаПроведения или ПриЗаписи, а пользователь потом закрывает форму без сохранения. Это лишь гарантирует, что изменение реквизита произойдет в транзакции.

  3. Особенности транзакционности в 1С 8.3:

    Мы должны четко понимать порядок вызова событий: ПередЗаписью -> ПриЗаписи (первый раз, перед проведением движений) -> ОбработкаПроведения (формирование движений) -> ПриЗаписи (второй раз, после проведения движений). Любые изменения, внесенные в объект документа в ОбработкаПроведения или в ПриЗаписи после проведения, будут частью той же транзакции. Если транзакция откатывается (например, из-за ошибки в проведении), все эти изменения также будут отменены. Это фундаментальный принцип, который мы используем для обеспечения надежности.

В конечном итоге, выбор конкретного решения зависит от сложности вашей бизнес-логики и требований к пользовательскому интерфейсу. Однако, использование независимых регистров сведений в связке с регламентными заданиями для выполнения асинхронных действий и механизмов обновления форм, является наиболее гибким, масштабируемым и надежным подходом для решения подобных задач в 1С:Предприятие.

← На главную