При разработке нестандартных схем нумерации документов в 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С предназначена только для уточнения длины строк или ограничения ссылочных типов. Преобразовать строку в число, как это делается функциями CAST или CONVERT в чистом SQL, невозможно.Найти() или SQL-функций CHARINDEX/INSTR, а также отсутствует поддержка регулярных выражений.ПОДОБНО "%/префикс%" с символом процента в начале шаблона полностью исключает использование индекса СУБД по полю Номер. База данных выполняет полное сканирование таблицы (Table Scan), что на объемах в десятки тысяч документов приводит к резкой деградации производительности.Наиболее надежное и чистое с точки зрения архитектуры 1С решение — вернуть лидирующие нули в информационную базу, чтобы числовая часть номера всегда имела фиксированную длину, а задачу сокрытия нулей решать на уровне интерфейса и печати.
Если конфигурация разработана с использованием Библиотеки Стандартных Подсистем (БСП), не требуется модифицировать десятки печатных форм и отчетов по отдельности. Достаточно переопределить формирование номера в общем модуле.
Разберем по шагам реализацию этого подхода:
ПрефиксацияОбъектовКлиентСервер.ПолучитьНомерНаПечать. В типовом виде она отсекает лидирующие нули и префиксы. Если в конфигурации используется сложный суффикс, дорабатываем эту функцию в неразрушающем режиме (или через механизм расширений), чтобы удалять нули только из префиксной части номера до первого слэша:
Функция ПолучитьНомерНаПечать(Знач НомерОбъекта, УдалитьПрефикс = Истина, УдалитьПользовательскиеПрефиксы = Истина) Экспорт
Если ПустаяСтрока(НомерОбъекта) Тогда
Возврат "";
КонецЕсли;
// Находим позицию первого разделителя суффикса
ПозицияСлэша = Найти(НомерОбъекта, "/");
Если ПозицияСлэша > 0 Тогда
ЧисловаяЧасть = Лев(НомерОбъекта, ПозицияСлэша - 1);
Суффикс = Сред(НомерОбъекта, ПозицияСлэша);
Иначе
ЧисловаяЧасть = НомерОбъекта;
Суффикс = "";
КонецЕсли;
// Удаляем лидирующие нули из левой части номера
Пока Лев(ЧисловаяЧасть, 1) = "0" И СтрДлина(ЧисловаяЧасть) > 1 Цикл
ЧисловаяЧасть = Сред(ЧисловаяЧасть, 2);
КонецЦикла;
Возврат ЧисловаяЧасть + Суффикс;
КонецФункции
Для отображения красивого номера в отчетах на СКД и в динамических списках без лидирующих нулей используем событие ОбработкаПолученияПредставления в модуле менеджера документа. Платформа будет автоматически выводить сформированную строку везде, где запрашивается представление ссылки на документ:
Процедура ОбработкаПолученияПредставления(Данные, Представление, СтандартнаяОбработка)
СтандартнаяОбработка = Ложь;
НомерБезНулей = ПрефиксацияОбъектовКлиентСервер.ПолучитьНомерНаПечать(Данные.Номер);
Представление = СтрШаблон("Реализация услуг № %1 от %2", НомерБезНулей, Формат(Данные.Дата, "ДЛФ=D"));
КонецПроцедуры
Если вернуть лидирующие нули в базу невозможно (например, из-за жестких регламентов или интеграций со сторонними системами), можно применить алгоритм виртуальной таблицы позиций. Суть метода заключается в эмуляции поиска первого разделителя / путем соединения с таблицей индексов и последующего дополнения строкового значения виртуальными нулями для корректного упорядочивания.
Посмотрим на пример запроса, решающего эту задачу:
ВЫБРАТЬ ПЕРВЫЕ 1
ИсходныеДанные.Номер КАК Номер
ИЗ
(ВЫБРАТЬ
Док.Номер КАК Номер
ИЗ
Документ.РеализацияУслуг КАК Док
ГДЕ
Док.Дата МЕЖДУ &ДатаНач И &ДатаКон
И Док.Номер ПОДОБНО &МаскаНомера) КАК ИсходныеДанные
ВНУТРЕННЕЕ СОЕДИНЕНИЕ (ВЫБРАТЬ 1 КАК Инд
ОБЪЕДИНИТЬ ВЫБРАТЬ 2
ОБЪЕДИНИТЬ ВЫБРАТЬ 3
ОБЪЕДИНИТЬ ВЫБРАТЬ 4
ОБЪЕДИНИТЬ ВЫБРАТЬ 5
ОБЪЕДИНИТЬ ВЫБРАТЬ 6
ОБЪЕДИНИТЬ ВЫБРАТЬ 7
ОБЪЕДИНИТЬ ВЫБРАТЬ 8
ОБЪЕДИНИТЬ ВЫБРАТЬ 9
ОБЪЕДИНИТЬ ВЫБРАТЬ 10) КАК Индексы
ПО (ПОДСТРОКА(ИсходныеДанные.Номер, Индексы.Инд, 1) = "/")
СГРУППИРОВАТЬ ПО
ИсходныеДанные.Номер
УПОРЯДОЧИТЬ ПО
// Дополняем выделенную подстроку нулями слева для верной сортировки
ПОДСТРОКА("0000000000" + ПОДСТРОКА(ИсходныеДанные.Номер, 1, МИНИМУМ(Индексы.Инд) - 1),
СтрДлина(ПОДСТРОКА(ИсходныеДанные.Номер, 1, МИНИМУМ(Индексы.Инд) - 1)) + 1,
10) УБЫВ
Разберем логику работы этого запроса:
"/".МИНИМУМ(Индексы.Инд) определяет позицию первого слэша в номере.ПОДСТРОКА вырезается часть номера до первого слэша и виртуально выравнивается строкой нулей, после чего выполняется корректная сортировка по убыванию.Важное предупреждение: данный запрос ресурсоемок. Его не следует вызывать при массовом проведении или в высоконагруженных операциях, так как декартово соединение со строковыми функциями создает значительную нагрузку на процессор СУБД.
Для высоконагруженных баз данных оптимальным решением является разделение составного номера на два реквизита:
Номер (Число или стандартная строка с автонумерацией): хранит исключительно числовую часть (123). Для него работает штатный механизм автонумерации платформы 1С.СуффиксНомера (Строка): хранит постоянную или переменную часть (например, "/j002/hhh/098").Если нумерация должна быть раздельной для разных суффиксов в рамках одного периода, стандартная автонумерация не справится. В этом случае правильным подходом будет использование независимого регистра сведений в качестве счетчика.
Создадим регистр сведений НумерацияДокументов со структурой:
ПериодГод (Дата или Число), Суффикс (Строка).ТекущийНомер (Число).Формирование номера выполняем в модуле объекта в обработчике ПриУстановкеНовогоНомера, обязательно устанавливая исключительную блокировку для предотвращения дублирования при параллельной работе пользователей:
Процедура ПриУстановкеНовогоНомера(СтандартнаяОбработка, Префикс)
СтандартнаяОбработка = Ложь;
// Выполняем генерацию в транзакции с блокировкой данных
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.НумерацияДокументов");
ЭлементБлокировки.УстановитьЗначение("ПериодГод", НачалоГода(ЭтотОбъект.Дата));
ЭлементБлокировки.УстановитьЗначение("Суффикс", ЭтотОбъект.СуффиксНомера);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
// Считываем текущее значение счетчика
НаборЗаписей = РегистрыСведений.НумерацияДокументов.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.ПериодГод.Установить(НачалоГода(ЭтотОбъект.Дата));
НаборЗаписей.Отбор.Суффикс.Установить(ЭтотОбъект.СуффиксНомера);
НаборЗаписей.Прочитать();
Если НаборЗаписей.Количество() > 0 Тогда
НовыйСчетчик = НаборЗаписей[0].ТекущийНомер + 1;
НаборЗаписей[0].ТекущийНомер = НовыйСчетчик;
Иначе
НовыйСчетчик = 1;
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.ПериодГод = НачалоГода(ЭтотОбъект.Дата);
НоваяЗапись.Суффикс = ЭтотОбъект.СуффиксНомера;
НоваяЗапись.ТекущийНомер = НовыйСчетчик;
КонецЕсли;
НаборЗаписей.Записать();
// Назначаем итоговый строковый номер
ЭтотОбъект.Номер = Формат(НовыйСчетчик, "ЧГ=0") + ЭтотОбъект.СуффиксНомера;
КонецПроцедуры
Такой подход устраняет проблему состояния гонки (Race Condition), исключает тяжелые запросы к таблицам документов при создании каждого нового объекта и гарантирует целостность числовой последовательности номеров.