Как правильно перезаписать все элементы справочника в 1С 7.7, запустить логику ПриЗаписи и разделить ИНН/КПП

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

При администрировании и обновлении устаревших информационных баз на платформе 1С:Предприятие 7.7 программисты часто сталкиваются с необходимостью массовой перезаписи элементов справочников. Обычно это требуется для выполнения групповых расчетов, заполнения новых реквизитов или запуска алгоритмов нормализации данных. Однако простая попытка обхода справочника "в лоб" часто приводит к неожиданным техническим сбоям: выборка элементов сбивается и обрабатывает не все элементы, а предопределенные процедуры валидации данных (такие как ПриЗаписи()) просто игнорируются системой.

В рамках этой статьи мы подробно разберем, почему возникают эти проблемы на уровне ядра платформы 1С 7.7, и как их правильно решать с точки зрения лучших практик разработки. Также мы рассмотрим практический кейс по нормализации данных: разделение склеенных строк ИНН/КПП у контрагентов и определение типа контрагента (ИП или Юрлицо) без привязки к текстовому наименованию.

Почему сбивается выборка при прямой перезаписи элементов и как это исправить

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


Процедура Сформировать()
    Спр = СоздатьОбъект("Справочник.Контрагенты");
    Спр.ВыбратьЭлементы();
    Пока Спр.ПолучитьЭлемент() = 1 Цикл
        Если Спр.ЭтоГруппа() = 0 Тогда
            // Какие-то изменения реквизитов
            Спр.Записать();
        КонецЕсли;
    КонецЦикла;
КонецПроцедуры

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

Когда мы вызываем метод ВыбратьЭлементы(), платформа 1С 7.7 создает динамический курсор выборки на основе индексов таблицы базы данных. По умолчанию выборка строится по иерархии с сортировкой по коду или наименованию. Как только внутри цикла мы меняем реквизиты элемента и вызываем метод Записать(), СУБД обновляет индексные таблицы. Физическое или логическое положение элемента в индексе меняется. В результате курсор выборки "теряется": он может перескочить через сотни элементов вперед (и они останутся необработанными) или вернуться назад (что приведет к бесконечному циклу).

Для предотвращения сбивания выборки мы рекомендуем использовать двухпроходный алгоритм с использованием буфера данных (объекта СписокЗначений или ТаблицаЗначений). Проанализируем этот подход на примере:


Процедура ПерезаписатьСправочник()
    Спр = СоздатьОбъект("Справочник.БП_ДоговорыСервисногоОбслуживания");
    СписокЭлементов = СоздатьОбъект("СписокЗначений");
    
    // Шаг 1: Собираем ссылки на все элементы в плоский список без иерархии
    // Параметр 0 в ВыбратьЭлементы отключает учет иерархии групп
    Спр.ВыбратьЭлементы(0); 
    Пока Спр.ПолучитьЭлемент() = 1 Цикл
        Если Спр.ЭтоГруппа() = 0 Тогда
            СписокЭлементов.ДобавитьЗначение(Спр.ТекущийЭлемент());
        КонецЕсли;
    КонецЦикла;
    
    КоличествоЭлементов = СписокЭлементов.РазмерСписка();
    Сообщить("Собрано элементов для обработки: " + КоличествоЭлементов);
    
    // Шаг 2: Обходим список и безопасно модифицируем элементы
    Для Номер = 1 По КоличествоЭлементов Цикл
        ТекущаяСсылка = СписокЭлементов.ПолучитьЗначение(Номер);
        
        // Позиционируем объект справочника по найденной ссылке
        Если Спр.НайтиЭлемент(ТекущаяСсылка) = 1 Тогда
            Состояние("Обработка договора №: " + Спр.НомерДоговора);
            
            // Здесь выполняем необходимые изменения реквизитов
            // Например: Спр.Реквизит = НовоеЗначение;
            
            Спр.Записать();
        КонецЕсли;
    КонецЦикла;
    
    Сообщить("Обработка успешно завершена!");
КонецПроцедуры

Этот метод гарантирует, что каждый элемент будет обработан ровно один раз, независимо от того, какие реквизиты изменяются и как перестраиваются индексы таблицы в базе данных.

Вызов логики ПриЗаписи() при программной модификации

Вторая проблема, с которой сталкиваются разработчики в 1С 7.7, формулируется так: «Я добавил код валидации и заполнения реквизитов в процедуру ПриЗаписи(), но при программном вызове Спр.Записать() эта логика не срабатывает!»

Разберем, почему так происходит. В платформе 1С 7.7, в отличие от 1С 8.x, **не существует** событийных процедур на уровне модуля объекта справочника или документа. Предопределенная процедура ПриЗаписи() пишется исключительно внутри модуля формы. Она перехватывает интерактивное нажатие пользователем кнопки "ОК" или "Записать" на форме элемента.

Когда вы выполняете код Спр.Записать() в обработке, запись происходит напрямую в базу данных на уровне ядра СУБД, минуя интерфейсную часть. Форма элемента не открывается, и код из ее модуля физически не может быть выполнен.

Для решения этой архитектурной особенности применяют два основных подхода:

  1. Вынос общей логики в Глобальный модуль: Создается сквозная процедура, принимающая объект справочника. Эта процедура вызывается как из формы (внутри локальной ПриЗаписи()), так и из внешних обработок перед программным вызовом метода Записать().
  2. Использование внешних компонент (1С++): Позволяет перехватывать события записи объектов на уровне ядра платформы.

Рассмотрим реализацию первого, классического и наиболее надежного способа.

В Глобальном модуле вашей конфигурации определим процедуру с ключевым словом Экспорт:


Процедура глОбработкаПриЗаписиДоговора(КонтекстСправочника) Экспорт
    // Сюда переносим всю логику автоматического заполнения реквизитов
    Если ПустоеЗначение(КонтекстСправочника.ДатаДоговора) = 1 Тогда
        КонтекстСправочника.ДатаДоговора = РабочаяДата();
    КонецЕсли;
    
    // Пример валидации данных перед записью
    Если КонтекстСправочника.Сумма < 0 Тогда
        Предупреждение("Сумма договора не может быть отрицательной!");
    КонецЕсли;
КонецПроцедуры

Теперь в Модуле формы элемента справочника договоров мы вызываем эту глобальную процедуру:


Процедура ПриЗаписи()
    // Передаем текущий контекст формы (ТекущийОбъект) в глобальный модуль
    глОбработкаПриЗаписиДоговора(Контекст);
КонецПроцедуры

В нашей внешней обработке групповой перезаписи код будет выглядеть следующим образом:


// ... код выборки элементов в список значений ...
Для Номер = 1 По КоличествоЭлементов Цикл
    ТекущаяСсылка = СписокЭлементов.ПолучитьЗначение(Номер);
    Если Спр.НайтиЭлемент(ТекущаяСсылка) = 1 Тогда
        // Сначала вызываем единую бизнес-логику заполнения
        глОбработкаПриЗаписиДоговора(Спр);
        
        // Теперь записываем объект в БД
        Спр.Записать();
    КонецЕсли;
КонецЦикла;

Таким образом, мы полностью избавляемся от дублирования кода и гарантируем идентичность выполнения бизнес-логики как при ручном вводе документов пользователями, так и при автоматической пакетной обработке данных.

Надежное разделение ИНН и КПП для контрагентов

Рассмотрим практическую задачу, упомянутую в обсуждении: у многих контрагентов в поле "ИНН" записана объединенная строка вида "ИНН/КПП" или "ИНН\КПП". Нам необходимо очистить поле "ИНН", перенести КПП в соответствующий реквизит и корректно обработать различие по длине ИНН для Юрлиц (10 символов) и Физических лиц/ИП (12 символов).

Попытки определять тип контрагента по вхождению подстрок "ИП" или "ООО" в его наименовании крайне ненадежны: название может начинаться с кавычек, содержать опечатки, пробелы или другие организационно-правовые формы (ЗАО, АО, Представительство). Гораздо надежнее анализировать структуру самого числового кода ИНН в соответствии с требованиями налогового законодательства РФ.

Напишем надежную функцию разбора строки ИНН/КПП, которая учитывает любые разделители (косую черту в обе стороны) и распределяет данные на основе длины строки:


Процедура НормализоватьИННиКПП(ОбъектКонтрагента) Экспорт
    ИсходнаяСтрока = СокрЛП(ОбъектКонтрагента.ИНН);
    
    Если ПустоеЗначение(ИсходнаяСтрока) = 1 Тогда
        Возврат;
    КонецЕсли;
    
    НовыйИНН = ИсходнаяСтрока;
    НовыйКПП = "";
    
    // Ищем разделители "/" или "\"
    ПозРазделителя = Найти(ИсходнаяСтрока, "/");
    Если ПозРазделителя = 0 Тогда
        ПозРазделителя = Найти(ИсходнаяСтрока, "\");
    КонецЕсли;
    
    Если ПозРазделителя > 0 Тогда
        // Строка содержит явный разделитель ИНН/КПП
        НовыйИНН = СокрЛП(Лев(ИсходнаяСтрока, ПозРазделителя - 1));
        НовыйКПП = СокрЛП(Прав(ИсходнаяСтрока, СтрДлина(ИсходнаяСтрока) - ПозРазделителя));
    Иначе
        // Разделителя нет, но длина строки превышает стандартную длину ИНН юрлица (10)
        // и не равна длине ИНН физлица/ИП (12).
        // Вероятно, КПП приклеен к ИНН без пробелов и знаков деления (например, 19 символов)
        ДлинаСтроки = СтрДлина(ИсходнаяСтрока);
        Если (ДлинаСтроки > 10) И (ДлинаСтроки <> 12) Тогда
            НовыйИНН = Лев(ИсходнаяСтрока, 10);
            НовыйКПП = Прав(ИсходнаяСтрока, ДлинаСтроки - 10);
        КонецЕсли;
    EndIf;
    
    // Финальная очистка от возможных нечисловых символов
    ОбъектКонтрагента.ИНН = СокрЛП(НовыйИНН);
    
    // Проверяем наличие реквизита КПП у справочника перед записью
    // (в некоторых старых конфигурациях 7.7 реквизита КПП могло не быть)
    Попытка
        ОбъектКонтрагента.КПП = СокрЛП(НовыйКПП);
    Исключение
        // Реквизит КПП отсутствует в метаданных справочника
    КонецПопытки;
КонецПроцедуры

Объединяем все решения в единый скрипт обработки

Теперь объединим все три разобранные технологии в единую готовую процедуру для внешней обработки. Скрипт сначала соберет все элементы справочника в буфер, затем обойдет их, применит функцию разбора ИНН/КПП, вызовет глобальные процедуры валидации и запишет элементы без риска нарушить динамический курсор СУБД:


Процедура ВыполнитьГрупповуюОбработку()
    КонтрагентыСпр = СоздатьОбъект("Справочник.Контрагенты");
    БуферЭлементов = СоздатьОбъект("СписокЗначений");
    
    // 1. Собираем элементы справочника в буфер без учета иерархии
    КонтрагентыСпр.ВыбратьЭлементы(0);
    Пока КонтрагентыСпр.ПолучитьЭлемент() = 1 Цикл
        Если КонтрагентыСпр.ЭтоГруппа() = 0 Тогда
            БуферЭлементов.ДобавитьЗначение(КонтрагентыСпр.ТекущийЭлемент());
        КонецЕсли;
    КонецЦикла;
    
    ВсегоЭлементов = БуферЭлементов.РазмерСписка();
    Сообщить("Начало обработки. Всего элементов: " + ВсегоЭлементов);
    
    // 2. Поочередно обрабатываем элементы из буфера
    Для Позиция = 1 По ВсегоЭлементов Цикл
        ТекущаяСсылка = БуферЭлементов.ПолучитьЗначение(Позиция);
        
        Если КонтрагентыСпр.НайтиЭлемент(ТекущаяСсылка) = 1 Тогда
            Состояние("Обработка контрагента: " + КонтрагентыСпр.Наименование);
            
            // Выполняем нормализацию ИНН и КПП по нашему алгоритму
            НормализоватьИННиКПП(КонтрагентыСпр);
            
            // Симулируем вызов логики "ПриЗаписи" из глобального модуля
            Попытка
                глОбработкаПриЗаписиКонтрагента(КонтрагентыСпр);
            Исключение
                // Если глобальной процедуры нет, продолжаем обработку
            КонецПопытки;
            
            // Безопасно сохраняем изменения в базу данных
            КонтрагентыСпр.Записать();
        КонецЕсли;
    КонецЦикла;
    
    Сообщить("Групповая перезапись успешно завершена!");
КонецПроцедуры

Используя данный подход при разработке под платформу 1С:Предприятие 7.7, вы полностью защитите свои базы данных от частичной обработки элементов, зависания индексов СУБД и рассинхронизации реквизитов из-за невыполненных процедур бизнес-логики.

← На главную