Как правильно программно пометить документ на удаление в 1С:УТ 11, чтобы не зависали движения в регистре себестоимости?

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

При кастомизации конфигураций на современной платформе 1С:Предприятие 8 (таких как 1С:Управление торговлей 11, 1С:Комплексная автоматизация 2 или 1С:ERP) разработчики часто сталкиваются с неприятной проблемой. После программного аннулирования или пометки на удаление документов (например, РеализацияТоваровУслуг) в регистрах накопления — особенно в регистре СебестоимостьТоваров — остаются «зависшие» движения. Более того, если помеченный документ впоследствии физически удаляется из базы, в регистре остаются записи с битыми ссылками вида <Объект не найден>.

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

Анализ проблемы: почему зависают движения по себестоимости?

Рассмотрим стандартный сценарий, который приводит к ошибке. Разработчик добавляет на форму документа или в список кнопку «Аннулировать». По нажатию этой кнопки выполняется код, который находит текущий документ и связанные с ним ордера, устанавливает признак удаления и записывает их:


// Пример НЕКОРРЕКТНОГО кода
ОбъектДокумента = СсылкаНаДокумент.ПолучитьОбъект();
ОбъектДокумента.ПометкаУдаления = Истина;
ОбъектДокумента.Записать(РежимЗаписиДокумента.Запись);

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

Выясним причину этого поведения. Она кроется в двух плоскостях: архитектурной (механизм очередей в УТ 11) и платформенной (особенности методов записи документов в 1С).

Архитектурная причина: Механизм очередей («Задания к расчету»)

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

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

  1. РегистрСведений.ЗаданияКРасчетуСебестоимости
  2. РегистрСведений.ЗаданияКРаспределениюРасчетовСКлиентами
  3. РегистрСведений.ЗаданияКЗакрытиюМесяца

Когда регламентное (фоновое) задание запускает расчет себестоимости, оно считывает эти очереди, собирает массив изменившихся документов-регистраторов и производит по ним сложные математические расчеты распределения затрат, после чего записывает результаты в регистр накопления СебестоимостьТоваров.

Если мы просто программно присвоили свойству ПометкаУдаления значение Истина и сохранили объект в режиме Запись, платформа считает документ просто «измененным». Событие полноценной отмены проведения не регистрируется в очередях «Заданий». Как результат — фоновый расчет себестоимости считает документ все еще актуальным, рассчитывает по нему данные и записывает движения поверх уже помеченного на удаление документа!

Платформенные нюансы: Разница между свойством ПометкаУдаления и методом УстановитьПометкуУдаления()

Давайте проанализируем разницу в поведении платформы при различных способах пометки документа на удаление:

Способ 1 (Неверный): Изменение свойства напрямую.


ОбъектДокумента.ПометкаУдаления = Истина;
ОбъектДокумента.Записать();

При использовании этого способа документ просто записывается в базу данных с новым флагом. При этом в модуле документа не вызывается важнейшее событие ОбработкаУдаленияПроведения. Если в свойствах документа в конфигураторе для удаления движений установлено значение «Не удалять автоматически» или «Изменять автоматически», старые движения по регистрам накопления физически остаются в базе данных.

Способ 2 (Правильный): Использование специализированного платформенного метода.

ОбъектДокумента.УстановитьПометкуУдаления(Истина);

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

Гонка процессов (Race Condition) при фоновом расчете

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

  1. Фоновое задание считывает данные из регистра ЗаданияКРасчетуСебестоимости и начинает долгий расчет распределения партий.
  2. В этот же момент пользователь нажимает кнопку «Аннулировать». Программа отменяет проведение документа и помечает его на удаление.
  3. Фоновое задание завершает свои расчеты и записывает новые рассчитанные движения обратно в базу данных, привязывая их к нашему документу. Поскольку фоновый процесс оперирует уже считанными ранее данными, он перезаписывает пустые наборы движений документа новыми цифрами.

В результате мы получаем помеченный на удаление документ с активными движениями в регистре себестоимости.

Последствия появления «битых ссылок» в регистре себестоимости

Если такие документы впоследствии удаляются стандартной обработкой «Удаление помеченных объектов», то физическая запись документа исчезает, а движения в регистре СебестоимостьТоваров остаются навсегда. Поле Регистратор для этих записей начинает содержать битую ссылку вида <Объект не найден>.

Это грозит системе серьезными проблемами:

Практическое решение: Пишем правильный код для кнопки «Аннулировать»

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

  1. Явно отменять проведение документа с помощью режима записи ОтменаПроведения.
  2. Использовать метод УстановитьПометкуУдаления(Истина).
  3. Обернуть весь процесс в транзакцию с наложением исключительной блокировки, чтобы избежать эффекта гонки процессов.

Рассмотрим пример правильной реализации процедуры аннулирования на сервере:


&НаСервере
Процедура АннулироватьДокументНаСервере(СсылкаНаДокумент)
    
    ОбъектДокумента = СсылкаНаДокумент.ПолучитьОбъект();
    Если ОбъектДокумента = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    // Открываем транзакцию для обеспечения атомарности операции
    НачатьТранзакцию();
    
    Попытка
        // Накладываем исключительную блокировку на документ
        Блокировка = Новый БлокировкаДанных;
        ЭлементБлокировки = Блокировка.Добавить("Документ.РеализацияТоваровУслуг");
        ЭлементБлокировки.УстановитьПервоначальноеЗначение("Ссылка", СсылкаНаДокумент);
        Блокировка.Заблокировать();
        
        // Шаг 1. Если документ проведен, принудительно отменяем проведение
        Если ОбъектДокумента.Проведен Тогда
            ОбъектДокумента.Записать(РежимЗаписиДокумента.ОтменаПроведения);
        КонецЕсли;
        
        // Шаг 2. Устанавливаем пометку удаления через правильный метод платформы
        ОбъектДокумента.УстановитьПометкуУдаления(Истина);
        
        // Фиксируем транзакцию, если все прошло успешно
        ЗафиксироватьТранзакцию();
        
        Сообщить("Документ успешно аннулирован!");
        
    Исключение
        // Откатываем транзакцию в случае любой ошибки блокировки или записи
        ОтменитьТранзакцию();
        
        ЗаписьЖурналаРегистрации(
            "ПрограммноеАннулирование",
            УровеньЖурналаРегистрации.Ошибка,
            ОбъектДокумента.Метаданные(),
            СсылкаНаДокумент,
            "Ошибка при аннулировании: " + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке())
        );
        
        ВызватьИсключение "Не удалось аннулировать документ по причине: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
    КонецПопытки;
    
КонецПроцедуры

Очистка существующих «битых» движений без монопольного доступа

Что делать, если проблема уже возникла и в регистре себестоимости зависли движения по некорректно удаленным или помеченным документам? Запускать тестирование и исправление (ТиИ) в монопольном режиме на рабочей базе объемом в сотни гигабайт — процедура долгая и болезненная.

Вместо этого мы можем написать простую и эффективную обработку, которая найдет все ошибочные движения и очистит их наборами записей непосредственно в работающей базе данных.

Давайте посмотрим на пример кода такой очистки:


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

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

Резюмируя наш разбор: всегда помните об архитектурной специфике учета в современных ERP-конфигурациях 1С. Никогда не используйте прямое присвоение флага ПометкаУдаления для ранее проведенных документов. Пользуйтесь правильными методами платформы, управляйте транзакциями и блокировками, и ваша база данных всегда будет находиться в абсолютно целостном и консистентном состоянии!

← На главную