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