Вступление
Представьте себе стандартную картину: конец отчетного периода. Бухгалтер закрывает год, переносит обороты, сверяет остатки. Всё идет по плану… И вдруг — ошибка. Документ, который был проведен месяц назад и не вызывал нареканий, при попытке закрытия месяца или перепроведении выдает фатальное сообщение: «Не заполнены паспортные данные контрагента», «Не указана ставка НДС» или что-то подобное.
Но ведь документ уже был проведен! И он уже сформировал движения в регистрах. Почему же теперь, когда ничего не менялось, система вдруг начала считать его некорректным?
Знакомая ситуация? Если да — вы столкнулись с классическим противоречием между жесткостью бизнес-логики и динамикой реальной базы данных.
Проблема возникает в двух типичных случаях:
-
В логику проведения документа добавили новую проверку. Например, потребовали обязательное наличие ИНН у контрагента. Старые документы, проведенные до этого требования, при перепроведении начинают «падать», хотя их движения абсолютно корректны и менять их не нужно.
-
Изменились данные в связанных объектах. Скажем, у контрагента удалили паспортные данные (по ошибке или в рамках обновления). Документ от этого не перестал быть правдивым — он просто «ссылается» на уже несуществующий реквизит, но движения его по-прежнему верны.
Результат один: ранее корректная работа системы превращается в головную боль. Бухгалтер не может закрыть месяц, программист срочно пишет обходные обработки, а бизнес теряет время и нервы. И всё это — в самый напряженный момент отчетного периода.
Очевидный, но неправильный путь — просто отключить проверки. Так нельзя: мы потеряем контроль над качеством данных. Нужно другое решение: научить систему отличать ситуацию, когда документ действительно ошибочен, от ситуации, когда он просто «старый» или ссылается на изменившиеся, но не критичные для его движений данные.
В этой статье мы предлагаем конкретный технический подход, который позволяет решить проблему элегантно и с минимальным вмешательством в существующую логику.
Проверка на отличия созданных движений и уже сделанных
В подобной ситуации, если проверка все-таки нужна, но документ был проведен ранее, чем добавлена эта проверка, либо документ проведен, но данные в другом объекте поменяли. Можно проверять движения созданные документом в настоящий момент и движения, которые уже записаны в регистры. И если движения не различаются, пропускать проверку.
Для этого можно сделать проверку на одинаковые движения, создаваемые документом в данный момент и уже записанных движений в регистр.
//Определяет одинаковое ли движение совершает документ с уже записанными регистрами
Функция ОдинаковоеДвижение(Ссылка) Экспорт
Движения = Ссылка.Движения;
ТаблицыСовпадают = Истина;
КоллекцияДляПоискаРегистра = КоллекцияРегистров();
Для каждого Движение Из Движения Цикл
ПроверяемыйОбъектХЗ = Движение.Выгрузить();
КопияДвижения = ПроверяемыйОбъектХЗ.Скопировать();
КопияДвижения.Очистить();
ВыборкаЗаписей = ЗаписиРегистра(Ссылка, Тип(Движение), КоллекцияДляПоискаРегистра);
Пока ВыборкаЗаписей.Следующий() Цикл
СтрокаДвижений = КопияДвижения.Добавить();
ЗаполнитьЗначенияСвойств(СтрокаДвижений, ВыборкаЗаписей);
КонецЦикла;
ПроверяемыйОбъект = ПроверяемыйОбъектХЗ.Скопировать();
Если НЕ ОдинаковыеТаблицы(ПроверяемыйОбъект, КопияДвижения) Тогда
ТаблицыСовпадают = Ложь;
КонецЕсли;
КонецЦикла;
Возврат ТаблицыСовпадают
КонецФункции // ()
Функция должна срабатывать когда документ уже сформировал свои движения и готовится к записать свои данные в базу данных.
Разберем подробнее это решение.
Первой строкой мы получаем сформированные движения документа:
Движения = Ссылка.Движения;
Далее ставим флаг по умолчанию, что таблицы совпадают, и получаем коллекцию регистров из метаданных, сопоставляем ее с экземплярами объектов, чтобы получать данные.
Выглядит коллекция регистров примерно так:
//Возвращает регистры из метаданных с сопоставлением регистров объектов
Функция КоллекцияРегистров()
СоответствиеРегистровИМетаданных = Новый Соответствие;
Для каждого РегистрБухгалтерииМетаданных Из Метаданные.РегистрыБухгалтерии Цикл
СоответствиеРегистровИМетаданных.Вставить(РегистрБухгалтерииМетаданных, РегистрыБухгалтерии[РегистрБухгалтерииМетаданных.Имя]);
КонецЦикла;
Для каждого РегистрСведенийМетаданных Из Метаданные.РегистрыСведений Цикл
СоответствиеРегистровИМетаданных.Вставить(РегистрСведенийМетаданных, РегистрыСведений[РегистрСведенийМетаданных.Имя]);
КонецЦикла;
Для каждого РегистрНакопленияМетаданных Из Метаданные.РегистрыНакопления Цикл
СоответствиеРегистровИМетаданных.Вставить(РегистрНакопленияМетаданных, РегистрыНакопления[РегистрНакопленияМетаданных.Имя]);
КонецЦикла;
Для каждого РегистрРасчетаМетаданных Из Метаданные.РегистрыРасчета Цикл
СоответствиеРегистровИМетаданных.Вставить(РегистрРасчетаМетаданных, РегистрыРасчета[РегистрРасчетаМетаданных.Имя]);
КонецЦикла;
Возврат СоответствиеРегистровИМетаданных
КонецФункции // ()
Собираются эти данные довольно долгое количество времени, поэтому можно ограничить количество сопоставляемых регистров теми, которые нужны проверяемому документу, либо хранить коллекцию в переменной в ИБ.
Далее строим цикл по таблицам регистров, по которым документ совершил движение.
Для каждого Движение Из Движения Цикл
ПроверяемыйОбъектХЗ = Движение.Выгрузить();
КопияДвижения = ПроверяемыйОбъектХЗ.Скопировать();
КопияДвижения.Очистить();
ВыборкаЗаписей = ЗаписиРегистра(Ссылка, Тип(Движение), КоллекцияДляПоискаРегистра);
Пока ВыборкаЗаписей.Следующий() Цикл
СтрокаДвижений = КопияДвижения.Добавить();
ЗаполнитьЗначенияСвойств(СтрокаДвижений, ВыборкаЗаписей);
КонецЦикла;
ПроверяемыйОбъект = ПроверяемыйОбъектХЗ.Скопировать();
Если НЕ ОдинаковыеТаблицы(ПроверяемыйОбъект, КопияДвижения) Тогда
ТаблицыСовпадают = Ложь;
КонецЕсли;
КонецЦикла;
В теле цикла: получаем таблицу значений с движениями регистра по документу. Получаем пустую копию этой таблицы для заполнения ее данными регистров из базы данных, так как в таблице с движениями документа и таблице регистра из базы данным могут быть разные колонки.
Далее в переменной ВыборкаЗаписей получаем записи по одному из движений документа из регистра в базе данных.
ВыборкаЗаписей = ЗаписиРегистра(Ссылка, Тип(Движение), КоллекцияДляПоискаРегистра);
Внутри этой функции мы получаем метаданные регистра по переданному движению документа с помощью функции НайтиПоТипу(). Далее из коллекции сопоставлений регистров и их экземпляров ищем экземпляр регистра и получаем выборку данных по ссылке документа(Регистратору).
Далее сама функция получения записей регистра:
Функция ЗаписиРегистра(Регистратор, Движение, КоллекцияДляПоискаРегистра)
СвойстваРегистра = Метаданные.НайтиПоТипу(Движение);
РегистрДляПоиска = КоллекцияДляПоискаРегистра[СвойстваРегистра];
Возврат РегистрДляПоиска.ВыбратьПоРегистратору(Регистратор);
КонецФункции
Теперь необходимо поместить полученные записи из регистров базы данных в пустую копию таблицы движений и сравнить две таблицы с движениями (Таблицу значений с сформированными движениями документа и таблицу значений с движениями регистра из базы данных).
Пока ВыборкаЗаписей.Следующий() Цикл
СтрокаДвижений = КопияДвижения.Добавить();
ЗаполнитьЗначенияСвойств(СтрокаДвижений, ВыборкаЗаписей);
КонецЦикла;
ПроверяемыйОбъект = ПроверяемыйОбъектХЗ.Скопировать();
Если НЕ ОдинаковыеТаблицы(ПроверяемыйОбъект, КопияДвижения) Тогда
ТаблицыСовпадают = Ложь;
КонецЕсли;
Для сравнения двух таблиц, да и в целом коллекций можно использовать функцию в библиотеке стандартных подсистем(БСП):
ОбщегоНазначения.КоллекцииИдентичны()
Так после прохождения всего цикла, если в движениях документа и движениях регистров в базе данных есть различия, то флаг ТаблицыСовпадают изменит свое значение на Ложь.
Заключение
Разработанный механизм проверки идентичности движений документа до его фактической записи в базу данных решает важную и, к сожалению, распространенную проблему: ситуацию, когда ранее корректно проведенный документ при повторном проведении (или в ходе регламентных операций) неожиданно начинает выдавать ошибку.
Предложенное решение позволяет аккуратно обойти эту проблему, не жертвуя при этом целостностью и логикой бизнес-процессов. Ключевая идея проста: не блокировать проведение, если фактические данные в регистрах не изменились. Вместо жесткой проверки «всегда и для всех» система анализирует: есть ли реальная разница между тем, что документ «хочет» записать сейчас, и тем, что уже записано в базе данных.
Предложенная функция ОдинаковоеДвижение() и вспомогательные механизмы – это пример прагматичного подхода к разработке: не «ужесточать правила ради правил», а учитывать реальный контекст работы системы. Документ, который не меняет движений, не должен становиться препятствием для работы пользователей, даже если формально проверка, добавленная позже, считает его «неправильным».
В конечном счете, лучшая проверка – та, которая не мешает работать, но при этом надежно защищает данные. Представленный механизм делает шаг именно в этом направлении.
Основные преимущества подхода:
-
Предотвращение ложных ошибок. Документы, которые были проведены и не затрагивают изменившиеся данные, продолжают работать без сбоев. Это особенно критично в закрытые периоды, когда «перепроведение» документа технически не должно менять движений.
-
Стабильность в критический момент. Решение напрямую адресовано описанной в статье проблеме – невозможности закрыть месяц из-за внезапно «полетевшего» документа. Автоматическая проверка на совпадение движений позволяет системе «пропустить» те документы, которые не вносят изменений, и сообщить об ошибке только там, где действительно есть расхождения.
В заключение хочу дополнить что данное решение лишь одно из возможных, которое можно применить в подобных ситуациях. Проверки в документах нужны, но если проверка при проведении документа и документ при проведении не меняет данных в базе данных, то такая проверка не имеет смысла.
Вступайте в нашу телеграмм-группу Инфостарт