Как получить максимальный составной строковый номер документа в 1С и избежать сбоев нумерации?

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

При разработке нестандартных схем нумерации документов в 1С разработчики часто сталкиваются с ситуацией, когда номер документа содержит изменяющиеся суффиксы или префиксы через разделители, например 123/j002/hhh/098. При попытке выбрать максимальный номер запросом с упорядочиванием по убыванию платформа возвращает значение 99/j002/hhh/098 вместо ожидаемого 123/.... Выясним причину такого поведения платформы и подробно разберем все способы решения этой задачи — от архитектурно правильных до обходных путей на уровне запросов.

Почему строковый номер сортируется некорректно: физика процесса

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

Когда мы сравниваем строки "99" и "123", СУБД сравнивает первый символ: код символа '9' (ASCII 57) больше кода символа '1' (ASCII 49). Следовательно, выражение "99" > "123" возвращает Истина. Документ с номером 99 закономерно оказывается первым при сортировке по убыванию.

В платформе 1С эта проблема изначально решена с помощью лидирующих нулей. Если номера имеют одинаковую фиксированную длину числовой части (например, 6 знаков), строки выглядят как "000099" и "000123". При сравнении первых символов нули совпадают, а при переходе к значимым разрядам "000123" становится больше "000099". Попытка удалить лидирующие нули непосредственно из базы данных неизбежно приводит к сбою как встроенной автонумерации, так и пользовательских запросов.

Ограничения языка запросов 1С при работе со строками

Попытка «на лету» преобразовать строковую часть номера в число прямо в запросе наталкивается на фундаментальные ограничения платформы:

  1. Отсутствие приведения типов строка-число: функция ВЫРАЗИТЬ() в языке запросов 1С предназначена только для уточнения длины строк или ограничения ссылочных типов. Преобразовать строку в число, как это делается функциями CAST или CONVERT в чистом SQL, невозможно.
  2. Отсутствие функций поиска подстроки: в языке запросов нет эквивалента функции Найти() или SQL-функций CHARINDEX/INSTR, а также отсутствует поддержка регулярных выражений.
  3. Неэффективность индексов при масках: использование конструкции ПОДОБНО "%/префикс%" с символом процента в начале шаблона полностью исключает использование индекса СУБД по полю Номер. База данных выполняет полное сканирование таблицы (Table Scan), что на объемах в десятки тысяч документов приводит к резкой деградации производительности.

Решение 1: Возврат лидирующих нулей и централизованное форматирование номера на печать (Рекомендуемое)

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

Если конфигурация разработана с использованием Библиотеки Стандартных Подсистем (БСП), не требуется модифицировать десятки печатных форм и отчетов по отдельности. Достаточно переопределить формирование номера в общем модуле.

Разберем по шагам реализацию этого подхода:

  1. С помощью внешней обработки приводим существующие номера к единой длине, дополняя числовую часть перед первым слэшем нулями до требуемой разрядности (например, до 6 символов).
  2. Для вывода на печать используем типовой механизм БСП. Обратим внимание на функцию ПрефиксацияОбъектовКлиентСервер.ПолучитьНомерНаПечать. В типовом виде она отсекает лидирующие нули и префиксы. Если в конфигурации используется сложный суффикс, дорабатываем эту функцию в неразрушающем режиме (или через механизм расширений), чтобы удалять нули только из префиксной части номера до первого слэша:

Функция ПолучитьНомерНаПечать(Знач НомерОбъекта, УдалитьПрефикс = Истина, УдалитьПользовательскиеПрефиксы = Истина) Экспорт

    Если ПустаяСтрока(НомерОбъекта) Тогда
        Возврат "";
    КонецЕсли;

    // Находим позицию первого разделителя суффикса
    ПозицияСлэша = Найти(НомерОбъекта, "/");
    Если ПозицияСлэша > 0 Тогда
        ЧисловаяЧасть = Лев(НомерОбъекта, ПозицияСлэша - 1);
        Суффикс = Сред(НомерОбъекта, ПозицияСлэша);
    Иначе
        ЧисловаяЧасть = НомерОбъекта;
        Суффикс = "";
    КонецЕсли;

    // Удаляем лидирующие нули из левой части номера
    Пока Лев(ЧисловаяЧасть, 1) = "0" И СтрДлина(ЧисловаяЧасть) > 1 Цикл
        ЧисловаяЧасть = Сред(ЧисловаяЧасть, 2);
    КонецЦикла;

    Возврат ЧисловаяЧасть + Суффикс;

КонецФункции

Для отображения красивого номера в отчетах на СКД и в динамических списках без лидирующих нулей используем событие ОбработкаПолученияПредставления в модуле менеджера документа. Платформа будет автоматически выводить сформированную строку везде, где запрашивается представление ссылки на документ:


Процедура ОбработкаПолученияПредставления(Данные, Представление, СтандартнаяОбработка)
    
    СтандартнаяОбработка = Ложь;
    НомерБезНулей = ПрефиксацияОбъектовКлиентСервер.ПолучитьНомерНаПечать(Данные.Номер);
    Представление = СтрШаблон("Реализация услуг № %1 от %2", НомерБезНулей, Формат(Данные.Дата, "ДЛФ=D"));
    
КонецПроцедуры

Решение 2: Получение максимального номера запросом с имитацией поиска слэша

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

Посмотрим на пример запроса, решающего эту задачу:


ВЫБРАТЬ ПЕРВЫЕ 1
    ИсходныеДанные.Номер КАК Номер
ИЗ
    (ВЫБРАТЬ
        Док.Номер КАК Номер
    ИЗ
        Документ.РеализацияУслуг КАК Док
    ГДЕ
        Док.Дата МЕЖДУ &ДатаНач И &ДатаКон
        И Док.Номер ПОДОБНО &МаскаНомера) КАК ИсходныеДанные
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ (ВЫБРАТЬ 1 КАК Инд
        ОБЪЕДИНИТЬ ВЫБРАТЬ 2
        ОБЪЕДИНИТЬ ВЫБРАТЬ 3
        ОБЪЕДИНИТЬ ВЫБРАТЬ 4
        ОБЪЕДИНИТЬ ВЫБРАТЬ 5
        ОБЪЕДИНИТЬ ВЫБРАТЬ 6
        ОБЪЕДИНИТЬ ВЫБРАТЬ 7
        ОБЪЕДИНИТЬ ВЫБРАТЬ 8
        ОБЪЕДИНИТЬ ВЫБРАТЬ 9
        ОБЪЕДИНИТЬ ВЫБРАТЬ 10) КАК Индексы
        ПО (ПОДСТРОКА(ИсходныеДанные.Номер, Индексы.Инд, 1) = "/")
СГРУППИРОВАТЬ ПО
    ИсходныеДанные.Номер
УПОРЯДОЧИТЬ ПО
    // Дополняем выделенную подстроку нулями слева для верной сортировки
    ПОДСТРОКА("0000000000" + ПОДСТРОКА(ИсходныеДанные.Номер, 1, МИНИМУМ(Индексы.Инд) - 1), 
              СтрДлина(ПОДСТРОКА(ИсходныеДанные.Номер, 1, МИНИМУМ(Индексы.Инд) - 1)) + 1, 
              10) УБЫВ

Разберем логику работы этого запроса:

  1. Таблица чисел от 1 до 10 соединяется со строкой номера. Условие соединения проверяет каждый символ номера на равенство разделителю "/".
  2. Агрегатная функция МИНИМУМ(Индексы.Инд) определяет позицию первого слэша в номере.
  3. С помощью ПОДСТРОКА вырезается часть номера до первого слэша и виртуально выравнивается строкой нулей, после чего выполняется корректная сортировка по убыванию.

Важное предупреждение: данный запрос ресурсоемок. Его не следует вызывать при массовом проведении или в высоконагруженных операциях, так как декартово соединение со строковыми функциями создает значительную нагрузку на процессор СУБД.

Решение 3: Разделение номера на числовой реквизит и суффикс

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

  1. Основной реквизит Номер (Число или стандартная строка с автонумерацией): хранит исключительно числовую часть (123). Для него работает штатный механизм автонумерации платформы 1С.
  2. Дополнительный реквизит СуффиксНомера (Строка): хранит постоянную или переменную часть (например, "/j002/hhh/098").

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

Создадим регистр сведений НумерацияДокументов со структурой:

Формирование номера выполняем в модуле объекта в обработчике ПриУстановкеНовогоНомера, обязательно устанавливая исключительную блокировку для предотвращения дублирования при параллельной работе пользователей:


Процедура ПриУстановкеНовогоНомера(СтандартнаяОбработка, Префикс)

    СтандартнаяОбработка = Ложь;

    // Выполняем генерацию в транзакции с блокировкой данных
    Блокировка = Новый БлокировкаДанных;
    ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.НумерацияДокументов");
    ЭлементБлокировки.УстановитьЗначение("ПериодГод", НачалоГода(ЭтотОбъект.Дата));
    ЭлементБлокировки.УстановитьЗначение("Суффикс", ЭтотОбъект.СуффиксНомера);
    ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
    Блокировка.Заблокировать();

    // Считываем текущее значение счетчика
    НаборЗаписей = РегистрыСведений.НумерацияДокументов.СоздатьНаборЗаписей();
    НаборЗаписей.Отбор.ПериодГод.Установить(НачалоГода(ЭтотОбъект.Дата));
    НаборЗаписей.Отбор.Суффикс.Установить(ЭтотОбъект.СуффиксНомера);
    НаборЗаписей.Прочитать();

    Если НаборЗаписей.Количество() > 0 Тогда
        НовыйСчетчик = НаборЗаписей[0].ТекущийНомер + 1;
        НаборЗаписей[0].ТекущийНомер = НовыйСчетчик;
    Иначе
        НовыйСчетчик = 1;
        НоваяЗапись = НаборЗаписей.Добавить();
        НоваяЗапись.ПериодГод = НачалоГода(ЭтотОбъект.Дата);
        НоваяЗапись.Суффикс = ЭтотОбъект.СуффиксНомера;
        НоваяЗапись.ТекущийНомер = НовыйСчетчик;
    КонецЕсли;

    НаборЗаписей.Записать();

    // Назначаем итоговый строковый номер
    ЭтотОбъект.Номер = Формат(НовыйСчетчик, "ЧГ=0") + ЭтотОбъект.СуффиксНомера;

КонецПроцедуры

Такой подход устраняет проблему состояния гонки (Race Condition), исключает тяжелые запросы к таблицам документов при создании каждого нового объекта и гарантирует целостность числовой последовательности номеров.

← На главную