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