Как реализовать автонумерацию для внешней печатной формы в 1С без изменения типовой конфигурации

Программист 1С v8.3 (Обычные формы) Управленческий учет IT и автоматизация бизнеса
← На главную

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

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

---

Почему стандартные подходы не всегда работают?

На первый взгляд, задача кажется тривиальной, но при детальном анализе всплывают важные архитектурные ограничения:

  1. Создание нового документа в конфигураторе: Этот вариант часто отвергается сразу. Новый объект метаданных усложняет обновление конфигурации, раздувает базу данных и требует настройки прав доступа, интерфейсов и правил обмена.
  2. Использование номера самого документа-основания: Самый простой путь — выводить на печатную форму "Заявка на отгрузку" номер исходного "Заказа покупателя". Однако это не всегда возможно. Например, если заявок на отгрузку формируется меньше, чем самих заказов (только по определенным складам или условиям), сквозная нумерация заявок нарушится, появятся "дыры" в номерах, что недопустимо для бухгалтерского или складского учета.
  3. Конкурентная запись (Проблема lost update): Если два пользователя одновременно нажимают кнопку "Печать" для разных заказов, система может считать из базы один и тот же максимальный номер, прибавить к нему единицу и присвоить обоим документам одинаковый номер заявки. Без правильного кэширования и блокировок эта проблема обязательно возникнет в многопользовательской среде.

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

---

Решение 1. Собственный регистр сведений и транзакционный механизм блокировок (Рекомендуемый вариант)

Это классический, наиболее надежный способ реализации автонумерации. Мы будем использовать независимый регистр сведений для хранения соответствия "Заказ покупателя — Номер заявки".

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

Шаг 1. Создание регистра сведений

Создадим новый независимый регистр сведений НомераЗаявокНаОтгрузку со следующей структурой:

Шаг 2. Реализация безопасного получения номера

Чтобы гарантировать уникальность номера при одновременной работе пользователей, процедура получения номера должна выполняться строго внутри транзакции с установкой исключительной управляемой блокировки. Разберем по шагам код, который будет вызываться в модуле внешней печатной формы при формировании макета:


Функция ПолучитьНовыйНомерЗаявки(ДокументСсылка) Экспорт
    
    // Начинаем транзакцию для обеспечения целостности данных
    НачатьТранзакцию();
    
    Попытка
        // 1. Устанавливаем управляемую блокировку на регистр сведений,
        // чтобы другие сеансы не могли прочитать максимальный номер до окончания нашей транзакции
        Блокировка = Новый БлокировкаДанных;
        ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.НомераЗаявокНаОтгрузку");
        ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
        Блокировка.Заблокировать();
        
        // 2. Проверяем, возможно для этого заказа номер уже был сгенерирован ранее
        Запрос = Новый Запрос;
        Запрос.Текст = 
            "ВЫБРАТЬ
            |	НомераЗаявок.НомерЗаявки КАК НомерЗаявки
            |ИЗ
            |	РегистрСведений.НомераЗаявокНаОтгрузку КАК НомераЗаявок
            |ГДЕ
            |	НомераЗаявок.Заказ = &Заказ";
        
        Запрос.УстановитьПараметр("Заказ", ДокументСсылка);
        РезультатЗапроса = Запрос.Выполнить();
        
        Если Не РезультатЗапроса.Пустой() Тогда
            Выборка = РезультатЗапроса.Выбрать();
            Выборка.Следующий();
            // Номер уже существует, возвращаем его и закрываем транзакцию
            ЗафиксироватьТранзакцию();
            Возврат Выборка.НомерЗаявки;
        КонецЕсли;
        
        // 3. Если номера нет, получаем текущий максимум по всей таблице
        ЗапросМакс = Новый Запрос;
        ЗапросМакс.Текст = 
            "ВЫБРАТЬ
            |	ЕСТЬNULL(МАКСИМУМ(НомераЗаявок.НомерЗаявки), 0) КАК МаксНомер
            |ИЗ
            |	РегистрСведений.НомераЗаявокНаОтгрузку КАК НомераЗаявок";
            
        РезультатМакс = ЗапросМакс.Выполнить();
        ВыборкаМакс = РезультатМакс.Выбрать();
        ВыборкаМакс.Следующий();
        
        НовыйНомер = ВыборкаМакс.МаксНомер + 1;
        
        // 4. Записываем сгенерированный номер в регистр
        МенеджерЗаписи = РегистрыСведений.НомераЗаявокНаОтгрузку.СоздатьМенеджерЗаписи();
        МенеджерЗаписи.Заказ = ДокументСсылка;
        МенеджерЗаписи.НомерЗаявки = НовыйНомер;
        МенеджерЗаписи.Записать(Истина);
        
        // Фиксируем транзакцию, снимая все блокировки
        ЗафиксироватьТранзакцию();
        Возврат НовыйНомер;
        
    Исключение
        // В случае любой ошибки откатываем транзакцию назад
        ОтменитьТранзакцию();
        ЗаписатьВЖурналРегистрации("ПечатьЗаявки.ОшибкаНумерации",
            УровеньЖурналаРегистрации.Ошибка, , , 
            ОписаниеОшибки());
        ВызватьИсключение "Не удалось сформировать уникальный номер заявки. Пожалуйста, попробуйте еще раз.";
    КонецПопытки;
    
КонецФункции

Этот код гарантирует, что даже если сто пользователей одновременно нажмут кнопку печати, СУБД выстроит их запросы в очередь благодаря управляемой блокировке РежимБлокировкиДанных.Исключительный, и каждый документ получит свой уникальный порядковый номер без пропусков и дублей.

---

Решение 2. Использование типового механизма дополнительных сведений

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

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

Пошаговая настройка механизма дополнительных сведений:

  1. Откройте программу в режиме предприятия под правами Администратора.
  2. Перейдите в меню Сервис -> Настройка дополнительных сведений.
  3. Найдите в списке объект Документ "Заказ покупателя" и добавьте для него новое свойство с наименованием Номер заявки на отгрузку и типом значения Число.
  4. В модуле внешней печатной формы напишите код, который будет обращаться к типовому регистру сведений ЗначенияСвойствОбъектов для получения и записи этого свойства.

Ниже представлен пример кода, который автоматизирует работу с доп. сведениями при печати:


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

Важная деталь: Поле Значение в типовом регистре ЗначенияСвойствОбъектов имеет составной тип. Поиск максимального значения по составному полю (МАКСИМУМ(ЗначенияСвойств.Значение)) на больших объемах данных может работать неоптимально, так как СУБД сложнее использовать индексы. Этот вариант идеален при небольшом документообороте (до нескольких тысяч заказов в месяц).

---

Решение 3. Динамическая генерация номера по маске (Без хранения в базе)

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

Вы можете формировать уникальный номер динамически прямо "на лету" в коде печатной формы, используя номер самого Заказа покупателя и модифицируя его по определенному алгоритму.

Примеры алгоритмов динамической генерации:

Давайте посмотрим, как просто реализуется этот метод в коде:


Функция СформироватьДинамическийНомерЗаявки(ДокументОбъект) Экспорт
    
    // Очищаем номер документа от лишних префиксов и нулей
    ЧистыйНомерЗаказа = СокрЛП(ДокументОбъект.Номер);
    
    // Удаляем лидирующие нули для лучшей читаемости
    Пока Лев(ЧистыйНомерЗаказа, 1) = "0" Цикл
        ЧистыйНомерЗаказа = Сред(ЧистыйНомерЗаказа, 2);
    КонецЦикла;
    
    // Превращаем префикс организации/базы в удобный формат
    // Например, префикс "УП" превращаем в суффикс "-УП"
    Префикс = ДокументОбъект.Организация.Префикс;
    Если ЗначениеЗаполнено(Префикс) Тогда
        ЧистыйНомерЗаказа = СтрЗаменить(ЧистыйНомерЗаказа, Префикс, "");
    КонецЕсли;
    
    // Формируем красивый номер для бухгалтерии
    НомерЗаявки = "ЗАО-" + ЧистыйНомерЗаказа;
    
    Возврат НомерЗаявки;
    
КонецФункции

Преимущества этого подхода очевидны:

  1. Абсолютно нулевая нагрузка на базу данных. Нет транзакций, нет записи в таблицы, нет риска блокировок.
  2. Идеальная отказоустойчивость: код никогда не выдаст ошибку "Объект заблокирован другим пользователем".
  3. Не требуется вносить изменения в метаданные и снимать конфигурацию с поддержки. Все изменения локализованы внутри одного файла внешней печатной формы.

---

Резюме: какую схему выбрать?

Для того чтобы принять окончательное решение, проанализируем ваши условия работы:

← На главную