Как хранить историю изменений табличной части документа в 1С 7.7 при редактировании задним числом?

Программист 1С v7.7 Управленческий учет IT и автоматизация бизнеса
← На главную

Изменение документов задним числом — одна из самых насущных и болезненных проблем в оперативном и складском учете. Типичная ситуация: на момент инвентаризации остатки сходились, но спустя месяц бухгалтер или менеджер открывает старый документ, вносит изменения, и вся картина по складу «плывет». В штатном журнале регистрации 1С 7.7 фиксируется лишь факт изменения документа пользователем, но восстановить конкретное состояние его табличной части до перезаписи крайне сложно.

В этой статье мы подробно разберем, как технически реализовать версионирование табличных частей документов в системе 1С:Предприятие 7.7. Мы проанализируем различные подходы — от простых внешних файлов до сложных систем на уровне СУБД MS SQL Server — и выясним, какие подводные камни могут вас ожидать при реализации каждого из них.

---

Метод 1. Сохранение таблицы значений во внешние файлы

Это один из самых простых в реализации и безопасных для стабильности информационной базы методов. Его суть заключается в том, что при каждом проведении или записи измененного документа текущая табличная часть выгружается в объект ТаблицаЗначений, сериализуется и записывается в файл на жестком диске.

Для реализации этого метода мы будем использовать встроенные функции платформы 7.7 ЗначениеВФайл() и ЗначениеИзФайла(). Рассмотрим по шагам, как написать такой обработчик в модуле формы или модуле документа.


// Пример процедуры записи истории во внешний файл в модуле документа
Процедура ПриЗаписи()
    // Фиксируем изменения только если документ реально изменялся
    Если Модифицированность() = 1 Тогда
        ТЗ = СоздатьОбъект("ТаблицаЗначений");
        ВыгрузитьТабличнуюЧасть(ТЗ);
        
        // Создадим каталог для хранения истории внутри каталога базы данных
        КаталогЛогов = КаталогИБ() + "DocHistory\";
        Если ФС.СуществуетКаталог(КаталогЛогов) = 0 Тогда
            ФС.СоздатьКаталог(КаталогЛогов);
        КонецЕсли;
        
        // Формируем уникальное имя файла на основе уникального идентификатора документа,
        // текущей даты и времени записи
        ИмяФайла = КаталогЛогов + СокрЛП(ТекущийДокумент().Выбран() * Идентификатор()) + 
                   "_" + Формат(ТекущаяДата(), "ДГГГГММДД") + 
                   "_" + СтрЗаменить(ТекущееВремя(), ":", "") + ".dat";
                   
        // Записываем таблицу значений в файл
        ЗначениеВФайл(ИмяФайла, ТЗ);
        
        // Опционально: запишем лог-файл с информацией о пользователе
        ТекстЛога = СоздатьОбъект("Текст");
        ФайлЛога = СтрЗаменить(ИмяФайла, ".dat", ".log");
        ТекстЛога.ДобавитьСтроку("Пользователь: " + ИмяПользователя());
        ТекстЛога.ДобавитьСтроку("Компьютер: " + ИмяКомпьютера());
        ТекстЛога.Записать(ФайлЛога);
    КонецЕсли;
КонецПроцедуры

Преимущества метода:

  1. База данных не раздувается. Все тяжелые данные (табличные части) хранятся на диске за пределами основного файла БД (1Cv7.1CD или таблиц SQL).
  2. Файлы истории легко администрировать — их можно архивировать, переносить на другой диск или удалять по истечении срока давности (например, удалять файлы старше 3 месяцев).

Недостатки метода:

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

---

Метод 2. Сериализация в строковый реквизит документа или справочника

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

Для превращения таблицы значений в строку используется функция ЗначениеВСтрокуВнутр(), а для обратного преобразования — ЗначениеИзСтрокиВнутр().


// Пример сериализации ТЗ в реквизит документа ИсторияТЧ
Процедура СохранитьТЧвРеквизит()
    ТЗ = СоздатьОбъект("ТаблицаЗначений");
    ВыгрузитьТабличнуюЧасть(ТЗ);
    
    // Преобразуем таблицу значений в специальную внутреннюю строку 1С
    СтрокаТЗ = ЗначениеВСтрокуВнутр(ТЗ);
    
    // Записываем полученную строку в реквизит неограниченной длины
    ИсторияТЧ = СтрокаТЗ;
КонецПроцедуры

Критически важный подводный камень (Ссылочная целостность):

Разберем детально опасность использования ЗначениеВСтрокуВнутр(). В этой строке ссылки на элементы справочников (например, на номенклатуру или контрагентов) хранятся в виде внутренних идентификаторов. Если какой-то товар будет физически удален из базы данных (или помечен на удаление и удален через обработку «Удаление помеченных объектов»), то при последующей попытке прочитать историю из строки с помощью ЗначениеИзСтрокиВнутр() система вернет битую ссылку: <Объект не найден>.

Решение проблемы ссылочной целостности (XML/JSON-подход):

Чтобы избежать этой проблемы, профессиональные разработчики рекомендуют выгружать данные в XML-формат с сохранением текстовых представлений (наименований и кодов) объектов. Даже если товар удалят из справочника, в истории останется его текстовое название.


// Пример сохранения ТЗ в XML с сохранением текстовых реквизитов
Процедура СохранитьТЗвXML(ТЗ, ИмяФайла)
    xmlDoc = СоздатьОбъект("Microsoft.XMLDOM");
    Root = xmlDoc.createElement("DocumentHistory");
    xmlDoc.appendChild(Root);
    
    ТЗ.ВыбратьСтроки();
    Пока ТЗ.ПолучитьСтроку() = 1 Цикл
        RowNode = xmlDoc.createElement("Row");
        
        ТоварСсылка = ТЗ.Товар;
        ТоварID = ЗначениеВСтрокуВнутр(ТоварСсылка);
        ТоварПредставление = СокрЛП(ТоварСсылка.Наименование);
        ТоварКод = СокрЛП(ТоварСсылка.Код);
        
        RowNode.setAttribute("ItemID", ТоварID);
        RowNode.setAttribute("ItemName", ТоварПредставление);
        RowNode.setAttribute("ItemCode", ТоварКод);
        RowNode.setAttribute("Quantity", ТЗ.Количество);
        RowNode.setAttribute("Price", ТЗ.Цена);
        
        Root.appendChild(RowNode);
    КонецЦикла;
    xmlDoc.save(ИмяФайла);
КонецПроцедуры

---

Метод 3. Хранение истории в подчиненном справочнике

Чтобы не перегружать сам документ тяжелыми строками неограниченной длины, можно создать служебный подчиненный справочник, например, ИсторияДокументов. Владельцем этого справочника будет наш документ, а реквизитами — ДатаИзменения, АвторИзменений, ПричинаИзменения и строковое поле для хранения ТЗ.

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

---

Метод 4. Оптимизация: дифференциальное логирование (хранение дельт)

Записывать всю табличную часть при каждом нажатии кнопки «Записать» — крайне неэффективно. Если документ состоит из 500 строк, а пользователь изменил количество только в одной строке, сохранять все 500 строк заново бессмысленно. Проанализируем, как реализовать сохранение только измененных строк (дельт).

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


// Функция вычисления простой контрольной суммы табличной части
Функция ПолучитьКонтрольнуюСумму(ТЗ)
    СуммаКоличества = ТЗ.Итог("Количество");
    СуммаВсего = ТЗ.Итог("Сумма");
    КоличествоСтрок = ТЗ.КоличествоСтрок();
    
    // Возвращаем строку-хэш
    Возврат Шаблон("[КоличествоСтрок]_[СуммаКоличества]_[СуммаВсего]");
КонецФункции

Если полученная контрольная сумма совпадает с предыдущей сохраненной контрольной суммой — запись файла истории можно пропустить, так как никаких критических изменений в количестве и суммах не произошло.

---

Метод 5. Продвинутые решения для SQL-версий баз данных

Если ваша информационная база 1С 7.7 работает на СУБД MS SQL Server, то самым красивым, производительным и надежным решением будет перенос логирования на уровень сервера баз данных.

  1. Использование SQL-триггеров: На таблицы табличных частей документов (таблицы с префиксом _1S_DT*) вешаются системные триггеры на операции INSERT, UPDATE, DELETE. При любом изменении SQL Server мгновенно копирует старые значения в выделенную таблицу истории.
  2. Решение проблемы авторизации в триггерах: Так как в 1С 7.7 все пользователи обычно работают под одной системной учетной записью базы данных (например, sa), триггер SQL не знает, какой именно пользователь изменил строку. Чтобы решить эту проблему, при начале работы системы (в процедуре ПриНачалеРаботыСистемы) в специальную таблицу SQL записывается связка ID процесса (@@spid) и имени пользователя 1С. Триггер обращается к этой таблице и безошибочно определяет «виновника» изменения.

Этот метод гарантирует защиту данных: обойти триггер невозможно, даже если изменения вносятся прямыми SQL-запросами или внешними обработками в обход интерфейса 1С.

---

Организационные и административные меры защиты

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

Проанализируем лучшие административно-технические практики предотвращения бардака в базе:

Сочетание организационных мер (блокировка периодов) и технических средств (сохранение ТЗ во внешние файлы или служебный справочник) позволит вам полностью контролировать остатки на складах и быстро находить виновников любых расхождений.

← На главную