Групповая обработка 1С — изменение многих справочников или документов за один запуск: заменить склад в заказах, назначить контрагентам нового менеджера, заполнить новый реквизит у документов прошлых лет. Для простых замен в типовых конфигурациях есть готовая обработка «Групповое изменение реквизитов». Когда отбор сложный, значение зависит от данных самого объекта или документов сотни тысяч, пишут свою обработку. Ниже — оба пути и код для платформы 8.3.
Обработка входит в Библиотеку стандартных подсистем, поэтому есть в большинстве типовых: Бухгалтерии 3.0, УТ 11, КА 2, ERP. Открыть её можно двумя способами:


Порядок работы: выберите вид объектов, настройте отбор и проверьте, сколько объектов в него попало, отметьте реквизиты, задайте новые значения и нажмите Изменить реквизиты. Кроме реквизитов шапки обработка меняет реквизиты табличных частей, а также дополнительные реквизиты и сведения. В актуальных версиях БСП администратору доступен режим произвольного алгоритма на встроенном языке. Так закрывается большинство задач «групповая обработка справочников» и «групповая обработка документов в 1С» без программиста. Отменить выполненное изменение кнопкой нельзя, поэтому перед массовой заменой сделайте копию базы.

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