При обновлении нетиповых или доработанных конфигураций 1С программисты часто сталкиваются с неприятной ситуацией: из-за невнимательности затираются доработки в стандартных объектах. Типичный пример — когда у документа-регистратора в процессе обновления снимается флаг («галочка») его привязки к самописному или измененному регистру накопления. В результате конфигурация базы данных успешно реструктуризируется, но все накопленные движения по этому документу в данном регистре бесследно исчезают.
Простое возвращение флага регистратора в конфигураторе обратно не возвращает данные. Платформа при обновлении физически очищает строки в таблицах СУБД, где поле регистратора соответствовало измененному документу. Давайте подробно разберем, как восстановить утерянные движения из резервной копии (бэкапа), не нарушая последовательность учета и не перепроводя документы в закрытых периодах.
Прежде чем перейти к методам восстановления, проанализируем физику процесса. В СУБД для каждого регистра накопления создается несколько таблиц: таблица движений (например, _AccumRegXXXX) и таблицы итогов. В таблице движений присутствует поле ссылки на регистратор (_Recorder). Когда при обновлении мы исключаем документ из списка регистраторов, платформа выполняет команду реструктуризации базы данных. На уровне SQL это выливается в удаление всех записей из таблицы движений, где тип регистратора соответствует исключенному документу.
Поскольку сама таблица не удаляется целиком (ведь у регистра могут быть и другие регистраторы), конфигуратор успешно выполняет обновление. Сами документы-источники (например, ПлатежныйОрдерСписаниеДенежныхСредств) остаются на месте, но их связь с регистром накопления разорвана навсегда. Нам потребуется бэкап, в котором эти движения еще физически существуют.
Это самый простой, быстрый и безопасный способ, не требующий написания сложного кода. Важно не путать эту обработку с инструментом «Универсальный обмен данными в формате XML». Последняя требует разработки правил обмена в конфигурации «Конвертация данных», что усложняет процесс.
Обработка «Выгрузка и загрузка данных XML» (поставляется на ИТС, имя файла обычно v8Exchan.epf или ВыгрузкаЗагрузкаДанныхXML83.epf) работает напрямую с метаданными идентичных конфигураций. Поскольку мы восстанавливаем данные из бэкапа той же самой базы, структуры метаданных и Уникальные Идентификаторы (UUID) документов полностью совпадают.
Рассмотрим пошагово порядок действий:
Если базы находятся на разных серверах, доступ через COM-соединение затруднен или обработка XML аварийно завершается из-за нехватки памяти (актуально для больших баз данных), мы можем использовать механизм внутренней сериализации 1С. Мы выгрузим движения в текстовый файл в базе-копии и загрузим их в рабочей базе.
Выясним, как написать код экспорта в базе-копии. Создадим внешнюю обработку и выполним следующий код:
// В базе-копии (бэкапе)
Процедура ВыгрузитьДвиженияВФайл()
ПутьКФайлу = "C:\temp\movements_backup.txt";
ТекстДанных = Новый ТекстовыйДокумент;
// Выбираем только те документы, движения по которым были утеряны
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Док.Ссылка КАК Ссылка
|ИЗ
| Документ.ПлатежныйОрдерСписаниеДенежныхСредств КАК Док
|ГДЕ
| Док.Проведен = ИСТИНА";
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Создаем набор записей по конкретному регистратору
НаборЗаписей = РегистрыНакопления.ИмяВашегоРегистра.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(Выборка.Ссылка);
НаборЗаписей.Прочитать();
Если НаборЗаписей.Количество() > 0 Тогда
// Превращаем таблицу движений в строку внутреннего формата 1С
ТаблицаСтрокой = ЗначениеВСтрокуВнутр(НаборЗаписей.Выгрузить());
// Записываем UUID документа и сериализованные движения через разделитель
СтрокаЗаписи = Строка(Выборка.Ссылка.УникальныйИдентификатор()) + "|" + ТаблицаСтрокой;
ТекстДанных.ДобавитьСтроку(СтрокаЗаписи);
КонецЕсли;
КонецЦикла;
ТекстДанных.Записать(ПутьКФайлу, КодировкаТекста.UTF8);
Сообщить("Выгрузка успешно завершена!");
КонецПроцедуры
Теперь рассмотрим, как выполнить загрузку этих данных в рабочей базе. Обратите внимание на использование важнейшего свойства ОбменДанными.Загрузка = Истина. Оно позволяет записывать наборы данных напрямую в СУБД, игнорируя платформенные проверки, подписки на события и ограничения закрытых периодов.
// В рабочей базе
Процедура ЗагрузитьДвиженияИзФайла()
ПутьКФайлу = "C:\temp\movements_backup.txt";
ТекстДанных = Новый ТекстовыйДокумент;
ТекстДанных.Прочитать(ПутьКФайлу, КодировкаТекста.UTF8);
Для НомерСтроки = 1 По ТекстДанных.КоличествоСтрок() Цикл
ТекущаяСтрока = ТекстДанных.ПолучитьСтроку(НомерСтроки);
Если ПустаяСтрока(ТекущаяСтрока) Тогда
Продолжить;
КонецЕсли;
// Находим позицию разделителя
ПозицияРазделителя = Найти(ТекущаяСтрока, "|");
Если ПозицияРазделителя = 0 Тогда
Продолжить;
КонецЕсли;
СтрокаUUID = Лев(ТекущаяСтрока, ПозицияРазделителя - 1);
СтрокаТаблицы = Сред(ТекущаяСтрока, ПозицияРазделителя + 1);
// Восстанавливаем ссылку на документ по его уникальному идентификатору
УИД = Новый УникальныйИдентификатор(СтрокаUUID);
СсылкаНаРегистратор = Документы.ПлатежныйОрдерСписаниеДенежныхСредств.ПолучитьСсылку(УИД);
// Десериализуем таблицу движений из строки
ТаблицаДвижений = ЗначениеИзСтрокиВнутр(СтрокаТаблицы);
// Записываем набор в базу данных
НаборЗаписей = РегистрыНакопления.ИмяВашегоРегистра.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(СсылкаНаРегистратор);
НаборЗаписей.Загрузить(ТаблицаДвижений);
// Отключаем бизнес-логику проведения для обхода блокировок прошлых периодов
НаборЗаписей.ОбменДанными.Загрузка = Истина;
НаборЗаписей.Записать();
КонецЦикла;
Сообщить("Загрузка движений завершена!");
КонецПроцедуры
Иногда бэкапа под рукой нет или он слишком старый, но сами документы в рабочей базе не пострадали, а алгоритм формирования движений (например, в модуле документа или в подписке на событие) остался прежним. В этом случае мы можем принудительно перепровести только нужные документы.
Однако простое перепроведение документов за прошлые года (в теме упоминается период с 2008 по 2018 год) — крайне опасная операция. Могут поехать котировки, себестоимость, взаиморасчеты и остатки по другим регистрам, которые не пострадали. Чтобы избежать этого, применим хитрый прием: запустим проведение документа внутри транзакции, прочитаем сформированные движения по нашему регистру в память, а затем отменим транзакцию (выполнив откат изменений по всем остальным регистрам).
Рассмотрим программную реализацию этого алгоритма:
Процедура ВосстановитьДвиженияВыборочнымПроведением()
ИмяРегистра = "ИмяВашегоРегистра";
// Выбираем документы, у которых пустые движения по нашему регистру
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Док.Ссылка КАК Ссылка
|ИЗ
| Документ.ПлатежныйОрдерСписаниеДенежныхСредств КАК Док
|ЛЕВОЕ СОЕДИНЕНИЕ
| РегистрНакопления.ИмяВашегоРегистра КАК ДвиженияРегистра
| ПО ДвиженияРегистра.Регистратор = Док.Ссылка
|ГДЕ
| Док.Проведен = ИСТИНА
| И ДвиженияРегистра.Регистратор ЕСТЬ NULL";
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
ДокСсылка = Выборка.Ссылка;
ДокОбъект = ДокСсылка.ПолучитьОбъект();
НачатьТранзакцию();
Попытка
// Проводим документ, чтобы сработали стандартные механизмы и подписки
ДокОбъект.Записать(РежимЗаписиДокумента.Проведение);
// Читаем движения только по нашему целевому регистру
ДокОбъект.Движения[ИмяРегистра].Прочитать();
ТаблицаДвиженийРегистра = ДокОбъект.Движения[ИмяРегистра].Выгрузить();
// Отменяем транзакцию, чтобы изменения по другим регистрам не записались в базу
ОтменитьТранзакцию();
Исключение
// В случае ошибки обязательно откатываем транзакцию
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("ВосстановлениеДвижений", УровеньЖурналаРегистрации.Ошибка, , ,
"Ошибка обработки документа: " + ДокСсылка + " Описание: " + ОписаниеОшибки());
Продолжить;
КонецПопытки;
// Теперь записываем полученную таблицу движений напрямую в базу
НаборЗаписей = РегистрыНакопления[ИмяРегистра].СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(ДокСсылка);
НаборЗаписей.Загрузить(ТаблицаДвиженийРегистра);
// Запись в режиме обмена данными исключает повторные проверки периодов
НаборЗаписей.ОбменДанными.Загрузка = Истина;
НаборЗаписей.Записать();
КонецЦикла;
Сообщить("Выборочное восстановление завершено!");
КонецПроцедуры
Данный метод идеален, когда документов немного (в теме автора их оказалось всего 37 штук). Он гарантирует, что никакие другие регистры и закрытые периоды не пострадают, а движения в пострадавшем регистре будут сформированы штатным кодом конфигурации.
Если база данных работает в клиент-серверном режиме под управлением MS SQL Server или PostgreSQL, а ее объем составляет сотни гигабайт (например, более 300 Гб), выгрузка через XML или текстовые файлы может вызвать переполнение оперативной памяти сервера или выполняться слишком долго.
В этом случае системные администраторы СУБД могут перенести записи напрямую из таблицы бэкапа в рабочую таблицу регистра накопления с помощью SQL-запроса INSERT INTO ... SELECT.
Важное предупреждение: При прямом манипулировании данными на уровне СУБД в обход сервера приложений 1С таблицы итогов (как оборотов, так и остатков) не обновятся автоматически. Если этого не сделать, ваши отчеты по данному регистру будут показывать старые или пустые данные.
Для исправления итогов после SQL-переноса обязательно выполните следующие действия в режиме «1С:Предприятие»:
Ситуация, когда доработанные связи затираются при обновлении конфигурации поставщика — это архитектурная уязвимость старых методов разработки. Чтобы навсегда застраховать себя от подобных инцидентов, используйте современные паттерны доработки конфигураций:
ОбработкаПроведения для нужных документов. В подписке можно программно инициализировать и заполнять наборы записей ваших нетиповых регистров.Мы проанализировали причины потери движений регистров накопления и разобрали четыре эффективных метода их восстановления в зависимости от объема базы данных и наличия резервных копий. Рекомендуем всегда проверять целостность кастомных связей после каждого обновления конфигурации до того, как пользователи начнут активную работу в системе.