
Массовое удаление документов по определенному отбору — стандартная задача при работе с экономическими программами. Но в 1С:ERP нет штатного инструмента, позволяющего выбрать, например, все документы за определенный период и удалить их.
Чтобы это сделать, нужные документы приходится сначала разыскать в разных разделах программы и пометить на удаление. Для автоматизации этой задачи я решил создать внешнюю обработку. Но все оказалось не так просто — и все не просто так. Одной обработки оказалось недостаточно: понадобились еще и довольно нетривиальные настройки самой 1С:ERP.
Код внешней обработки, границы ее применения, а также описание необходимых настроек 1С:ERP и составляют содержание этой статьи.
Внешняя обработка
Решение тестировалось на версии 1C 8.3.27.2130 для разработчиков, конфигурация: 1С:ERP Управление предприятием 2 (2.5.25.80). В качестве базы примера приведено решение одного экзаменационного билета Специалист «Производство и ремонты».
Для решения задачи была создана внешняя обработка с управляемой формой. Ее интерфейс предельно простой: пользователь задает период, обработка находит попавшие в него документы, после чего можно выбрать нужные документы и массово пометить их на удаление.

На форме три реквизита:
НачалоПериода — тип
Дата;КонецПериода — тип
Дата;СписокДокументов — тип
ТаблицаЗначений.
Первые два реквизита определяют временной интервал отбора. Реквизит СписокДокументов используется для вывода найденных документов и содержит шесть колонок:
Колонка | Тип | Назначение |
Выбран | Булево | Признак того, что документ должен быть обработан |
ВидДокумента | Строка | Вид найденного документа |
Дата | Дата | Дата документа |
Номер | Строка | Номер документа |
Проведен | Булево | Признак проведения документа |
Ссылка | Произвольный | Ссылка на конкретный объект 1С |
На последнюю колонку стоит обратить особое внимание. Ссылка должна иметь тип Произвольный, а не Строка. Обработка работает с документами разных типов, поэтому в этой колонке должна храниться сама ссылка на объект, а не ее текстовое представление. В дальнейшем именно через нее обработка получает объект документа и выполняет необходимые действия.
Для управления обработкой созданы четыре команды:

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

Все не так просто
Документы помечаются на удаление, но при попытке их удалить, удаляются не все. В системе все взаимосвязано и один‑единственный документ Заказ давальца, например, связан с массой объектов (это еще не все попали на экран).

Вручную одолеть такое немыслимо, и начались поиски, что может предложить система для удаления вот этого всего.
Решение нашлось довольно быстро. При анализе объектов, мешающих удалению документов, выяснилось, что значительная часть связей возникает не между самими первичными документами, а с данными, сформированными при выполнении регламентных операций закрытия месяца. В частности, удалению мешали записи, связанные с расчетом себестоимости. Нужно было найти штатный механизм, который позволит самой 1С:ERP убрать результаты ранее выполненного расчета.
Очистка расчетных движений
Такой механизм обнаружился в настройках закрытия месяца. Открываем Регламентные операции по закрытию месяца в подсистеме Финансовый результат и контроллинг, переходим в Настройки → Настройка параметров операций закрытия месяца и включаем техническую настройку: Режим отладки расчета = Да.


После этого для операции Распределение затрат и расчет себестоимости становятся доступны дополнительные параметры отладки. Для нашей задачи нужны два из них:
Перед расчетом очищать старые расчетные движения = Да;
Не выполнять расчет себестоимости = Да.
Первый параметр заставляет систему перед выполнением операции очистить ранее сформированные расчетные движения. Второй позволяет на этом остановиться: заново рассчитывать себестоимость документов, которые мы собираемся удалить, очевидно, не требуется.

Жмем кнопку Сохранить и закрыть.
После установки параметров достаточно выполнить только операцию Распределение затрат и расчет себестоимости. Выполнять заново все операции закрытия месяца не нужно.

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

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

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

