Изменение документов задним числом — одна из самых насущных и болезненных проблем в оперативном и складском учете. Типичная ситуация: на момент инвентаризации остатки сходились, но спустя месяц бухгалтер или менеджер открывает старый документ, вносит изменения, и вся картина по складу «плывет». В штатном журнале регистрации 1С 7.7 фиксируется лишь факт изменения документа пользователем, но восстановить конкретное состояние его табличной части до перезаписи крайне сложно.
В этой статье мы подробно разберем, как технически реализовать версионирование табличных частей документов в системе 1С:Предприятие 7.7. Мы проанализируем различные подходы — от простых внешних файлов до сложных систем на уровне СУБД MS SQL Server — и выясним, какие подводные камни могут вас ожидать при реализации каждого из них.
---
Это один из самых простых в реализации и безопасных для стабильности информационной базы методов. Его суть заключается в том, что при каждом проведении или записи измененного документа текущая табличная часть выгружается в объект ТаблицаЗначений, сериализуется и записывается в файл на жестком диске.
Для реализации этого метода мы будем использовать встроенные функции платформы 7.7 ЗначениеВФайл() и ЗначениеИзФайла(). Рассмотрим по шагам, как написать такой обработчик в модуле формы или модуле документа.
// Пример процедуры записи истории во внешний файл в модуле документа
Процедура ПриЗаписи()
// Фиксируем изменения только если документ реально изменялся
Если Модифицированность() = 1 Тогда
ТЗ = СоздатьОбъект("ТаблицаЗначений");
ВыгрузитьТабличнуюЧасть(ТЗ);
// Создадим каталог для хранения истории внутри каталога базы данных
КаталогЛогов = КаталогИБ() + "DocHistory\";
Если ФС.СуществуетКаталог(КаталогЛогов) = 0 Тогда
ФС.СоздатьКаталог(КаталогЛогов);
КонецЕсли;
// Формируем уникальное имя файла на основе уникального идентификатора документа,
// текущей даты и времени записи
ИмяФайла = КаталогЛогов + СокрЛП(ТекущийДокумент().Выбран() * Идентификатор()) +
"_" + Формат(ТекущаяДата(), "ДГГГГММДД") +
"_" + СтрЗаменить(ТекущееВремя(), ":", "") + ".dat";
// Записываем таблицу значений в файл
ЗначениеВФайл(ИмяФайла, ТЗ);
// Опционально: запишем лог-файл с информацией о пользователе
ТекстЛога = СоздатьОбъект("Текст");
ФайлЛога = СтрЗаменить(ИмяФайла, ".dat", ".log");
ТекстЛога.ДобавитьСтроку("Пользователь: " + ИмяПользователя());
ТекстЛога.ДобавитьСтроку("Компьютер: " + ИмяКомпьютера());
ТекстЛога.Записать(ФайлЛога);
КонецЕсли;
КонецПроцедуры
Преимущества метода:
1Cv7.1CD или таблиц SQL).Недостатки метода:
---
Вместо внешнего файла мы можем хранить сериализованную таблицу значений непосредственно внутри информационной базы. Для этого в документе (или в специальном подчиненном справочнике) создается реквизит типа Строка неограниченной длины.
Для превращения таблицы значений в строку используется функция ЗначениеВСтрокуВнутр(), а для обратного преобразования — ЗначениеИзСтрокиВнутр().
// Пример сериализации ТЗ в реквизит документа ИсторияТЧ
Процедура СохранитьТЧвРеквизит()
ТЗ = СоздатьОбъект("ТаблицаЗначений");
ВыгрузитьТабличнуюЧасть(ТЗ);
// Преобразуем таблицу значений в специальную внутреннюю строку 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(ИмяФайла);
КонецПроцедуры
---
Чтобы не перегружать сам документ тяжелыми строками неограниченной длины, можно создать служебный подчиненный справочник, например, ИсторияДокументов. Владельцем этого справочника будет наш документ, а реквизитами — ДатаИзменения, АвторИзменений, ПричинаИзменения и строковое поле для хранения ТЗ.
Этот способ позволяет хранить неограниченное количество версий документа (каждая запись — новый элемент справочника) и легко строить по ним отчеты стандартными средствами 1С.
---
Записывать всю табличную часть при каждом нажатии кнопки «Записать» — крайне неэффективно. Если документ состоит из 500 строк, а пользователь изменил количество только в одной строке, сохранять все 500 строк заново бессмысленно. Проанализируем, как реализовать сохранение только измененных строк (дельт).
Для этого мы можем написать функцию, которая сравнивает текущую ТЧ с последней сохраненной версией. Для оптимизации быстродействия мы можем сначала вычислять простую контрольную сумму табличной части (хэш).
// Функция вычисления простой контрольной суммы табличной части
Функция ПолучитьКонтрольнуюСумму(ТЗ)
СуммаКоличества = ТЗ.Итог("Количество");
СуммаВсего = ТЗ.Итог("Сумма");
КоличествоСтрок = ТЗ.КоличествоСтрок();
// Возвращаем строку-хэш
Возврат Шаблон("[КоличествоСтрок]_[СуммаКоличества]_[СуммаВсего]");
КонецФункции
Если полученная контрольная сумма совпадает с предыдущей сохраненной контрольной суммой — запись файла истории можно пропустить, так как никаких критических изменений в количестве и суммах не произошло.
---
Если ваша информационная база 1С 7.7 работает на СУБД MS SQL Server, то самым красивым, производительным и надежным решением будет перенос логирования на уровень сервера баз данных.
_1S_DT*) вешаются системные триггеры на операции INSERT, UPDATE, DELETE. При любом изменении SQL Server мгновенно копирует старые значения в выделенную таблицу истории.sa), триггер SQL не знает, какой именно пользователь изменил строку. Чтобы решить эту проблему, при начале работы системы (в процедуре ПриНачалеРаботыСистемы) в специальную таблицу SQL записывается связка ID процесса (@@spid) и имени пользователя 1С. Триггер обращается к этой таблице и безошибочно определяет «виновника» изменения.Этот метод гарантирует защиту данных: обойти триггер невозможно, даже если изменения вносятся прямыми SQL-запросами или внешними обработками в обход интерфейса 1С.
---
Никакие технические средства логирования не помогут навести порядок, если пользователи имеют бесконтрольный доступ на изменение данных прошлых периодов. Как справедливо отмечают эксперты на форумах, «бороться надо не со следствием, а с причиной».
Проанализируем лучшие административно-технические практики предотвращения бардака в базе:
ТекущаяДата() - 1 день (или несколько дней в зависимости от регламента закрытия периода).Сочетание организационных мер (блокировка периодов) и технических средств (сохранение ТЗ во внешние файлы или служебный справочник) позволит вам полностью контролировать остатки на складах и быстро находить виновников любых расхождений.