При переносе данных о контрагентах из сторонних конфигураций (например, из 1С:Бухгалтерии предприятия или самописных систем) в современные решения на базе ERP-платформы — 1С:ERP Управление предприятием, 1С:Комплексная автоматизация или 1С:Управление торговлей — разработчики и администраторы часто сталкиваются со странным поведением системы. В карточках контрагентов все реквизиты, включая ИНН и КПП, заполнены корректно. Однако при формировании регламентированной отчетности по НДС (Декларации по НДС, Книги покупок или Книги продаж) у этих контрагентов поля ИНН и КПП остаются пустыми.
Интересно, что если открыть карточку проблемного контрагента вручную, нажать кнопку Записать и закрыть, а затем перезаполнить декларацию, то данные контрагента чудесным образом появляются в отчете. Но если контрагентов тысячи, перезаписывать каждого вручную невозможно. Простая перезапись объектов групповой обработкой справочников также не приносит желаемого результата.
В этой статье мы подробно разберем, почему возникает эта техническая коллизия, проанализируем внутреннюю архитектуру хранения данных в 1С:ERP и предоставим готовое, быстрое и правильное решение проблемы без необходимости ручного открытия форм.
Чтобы понять суть проблемы, нам необходимо заглянуть «под капот» конфигураций, построенных на современной ERP-платформе. В отличие от более старых систем, в 1С:ERP реализована концепция хранения истории изменений реквизитов контрагента. В частности, это касается таких реквизитов, как КПП, Сокращенное наименование и Полное наименование.
В процессе ведения бизнеса юридическое лицо может изменить свой КПП (например, при смене юридического адреса и переходе в другую налоговую инспекцию) или изменить свое наименование. При этом все документы прошлых периодов в информационной базе должны сохранять те реквизиты контрагента, которые действовали непосредственно на дату совершения хозяйственной операции.
Для реализации этой бизнес-логики разработчики фирмы 1С спроектировали справочник Контрагенты следующим образом:
Контрагенты.ИНН и Контрагенты.КПП. Именно их мы видим, когда открываем карточку контрагента в режиме предприятия.ИсторияКПП. Каждая строка этой табличной части содержит сам КПП и дату, начиная с которой этот КПП считается действующим (поле ДействуетС).ИсторияПолныхНаименований и ИсторияСокращенныхНаименований.Когда мы формируем Декларацию по НДС, Книгу покупок или Книгу продаж, регламентированные отчеты обращаются к базе данных с помощью сложных запросов. Эти запросы не берут КПП напрямую из шапки справочника Контрагенты. Вместо этого они обращаются к табличной части ИсторияКПП, пытаясь получить значение КПП, актуальное на дату конкретного первичного документа (например, Счет-фактура полученный или Реализация товаров и услуг).
Если при первоначальной программной загрузке данных из другой системы (через файлы XML, COM-соединение или EnterpriseData) был заполнен только основной реквизит шапки Объект.КПП, а табличная часть ИсторияКПП осталась пустой, то в Декларацию по НДС КПП не попадет, так как системе просто неоткуда взять исторические данные на дату документа.
Выясним, почему стандартные обработки группового изменения реквизитов или простая перезапись объекта кодом Объект.Записать() не исправляют ситуацию. Дело в том, что логика автоматического заполнения исторических табличных частей при изменении реквизитов шапки размещена разработчиками 1С **в модуле формы** справочника Контрагенты (в обработчиках событий формы, таких как ПередЗаписьюНаСервере или ПриЗаписиНаСервере), а не в модуле самого объекта справочника.
Проанализируем разницу в поведении системы при различных сценариях записи:
ИсторияКПП пуста, автоматически генерируют в ней строку по умолчанию с пустой датой начала действия (01.01.0001) и только после этого записывают объект в базу данных.ПолучитьОбъект() и вызывают метод Записать(). При этом контекст формы не создается, код модуля формы не выполняется, логика синхронизации игнорируется, и табличная часть истории так и остается незаполненной.На форумах часто предлагают решать подобные проблемы «в лоб». Рассмотрим пример кода, который часто используют разработчики от безысходности (этот же метод упомянут в сообщении 5 исходной темы):
ОбъектСсылкаМассив = ПолучитьМассивПроблемныхКонтрагентов();
Для Каждого ОбъектСсылка Из ОбъектСсылкаМассив Цикл
Форма = ПолучитьФорму("Справочник.Контрагенты.ФормаОбъекта", Новый Структура("Ключ", ОбъектСсылка));
Форма.Открыть();
Форма.Записать();
Форма.Закрыть();
КонецЦикла;
Давайте проанализируем этот подход. Да, он сработает, потому что принудительное открытие формы инициализирует всю необходимую логику и записывает данные корректно. Но у этого метода есть огромные недостатки:
ПолучитьФорму, Открыть) доступны исключительно на клиенте.Для того чтобы решить проблему быстро, профессионально и без использования клиентского интерфейса, мы должны написать скрипт, который напрямую заполнит недостающие строки в табличных частях ИсторияКПП, ИсторияСокращенныхНаименований и ИсторияПолныхНаименований.
Ниже представлен готовый код внешней обработки, который можно запустить в любой момент. Проанализируем его структуру.
// Шаг 1. Формируем запрос для поиска контрагентов с пустыми табличными частями истории
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Контрагенты.Ссылка КАК Ссылка,
| Контрагенты.КПП КАК КПП,
| Контрагенты.Наименование КАК Наименование,
| Контрагенты.НаименованиеПолное КАК НаименованиеПолное
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.КПП <> """"
| И НЕ Контрагенты.ЭтоГруппа
| И Контрагенты.Ссылка НЕ В
| (ВЫБРАТЬ РАЗЛИЧНЫЕ
| КонтрагентыИсторияКПП.Ссылка
| ИЗ
| Справочник.Контрагенты.ИсторияКПП КАК КонтрагентыИсторияКПП)";
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
НачатьТранзакцию();
Попытка
Пока Выборка.Следующий() Цикл
// Получаем объект контрагента для редактирования
ОбъектКонтрагента = Выборка.Ссылка.ПолучитьОбъект();
Модифицирован = Ложь;
// Проверяем и заполняем историю КПП
Если ОбъектКонтрагента.ИсторияКПП.Количество() = 0 Тогда
НоваяСтрокаКПП = ОбъектКонтрагента.ИсторияКПП.Добавить();
НоваяСтрокаКПП.КПП = Выборка.КПП;
НоваяСтрокаКПП.ДействуетС = '00010101000000'; // Устанавливаем дату "начало времен"
Модифицирован = Истина;
КонецЕсли;
// Дополнительно проверяем историю сокращенных наименований
// Это убережет от пустоты в печатных формах документов прошлых периодов
Если ОбъектКонтрагента.Метаданные().ТабличныеЧасти.Найти("ИсторияСокращенныхНаименований") <> Неопределено
И ОбъектКонтрагента.ИсторияСокращенныхНаименований.Количество() = 0 Тогда
НоваяСтрокаСокр = ОбъектКонтрагента.ИсторияСокращенныхНаименований.Добавить();
НоваяСтрокаСокр.СокращенноеНаименование = Выборка.Наименование;
НоваяСтрокаСокр.ДействуетС = '00010101000000';
Модифицирован = Истина;
КонецЕсли;
// Проверяем историю полных наименований
Если ОбъектКонтрагента.Метаданные().ТабличныеЧасти.Найти("ИсторияПолныхНаименований") <> Неопределено
И ОбъектКонтрагента.ИсторияПолныхНаименований.Количество() = 0 Тогда
НоваяСтрокаПолн = ОбъектКонтрагента.ИсторияПолныхНаименований.Добавить();
НоваяСтрокаПолн.ПолноеНаименование = Выборка.НаименованиеПолное;
НоваяСтрокаПолн.ДействуетС = '00010101000000';
Модифицирован = Истина;
КонецЕсли;
// Если объект был изменен, записываем его с отключением лишних проверок
Если Модифицирован Тогда
// Использование ОбменДанными.Загрузка позволяет избежать тяжелых проверок бизнес-логики,
// таких как проверка уникальности ИНН/КПП или корректности заполнения адресов.
ОбъектКонтрагента.ОбменДанными.Загрузка = Истина;
ОбъектКонтрагента.Записать();
КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();
Сообщить("Обработка успешно завершена! Исторические данные контрагентов восстановлены.");
Исключение
ОтменитьТранзакцию();
Сообщить("Произошла ошибка при обработке данных: " + ОписаниеОшибки(), СтатусСообщения.Важное);
КонецПопытки;
Рассмотрим подробнее важные нюансы представленного алгоритма:
ОбъектКонтрагента.Метаданные().ТабличныеЧасти.Найти(...) защищает код от падения с ошибкой на старых или кастомизированных базах данных.ОбменДанными.Загрузка = Истина. Применение этого свойства критически важно. Оно отключает выполнение кода в модуле объекта (события ПередЗаписью, ПриЗаписи), обходит проверку заполнения обязательных реквизитов и контролирует уникальность ИНН. Благодаря этому обработка десятков тысяч контрагентов занимает считанные секунды.НачатьТранзакцию() и ЗафиксироватьТранзакцию() гарантируют целостность данных в базе данных и ускоряют процесс записи на уровне СУБД.Чтобы при последующих переносах данных из внешних систем у вас не возникала аналогичная проблема с пустыми реквизитами в Декларации по НДС, рекомендуем скорректировать правила обмена:
Если вы используете правила конвертации данных (КД 2.0 или КД 3.0), обязательно добавьте обработчики событий при выгрузке или при загрузке данных контрагентов, которые будут принудительно инициализировать и заполнять строки в подчиненных табличных частях ИсторияКПП, ИсторияСокращенныхНаименований и ИсторияПолныхНаименований значениями по умолчанию. Это сэкономит ваше время и убережет от экстренного исправления ошибок перед сдачей квартальной отчетности.