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

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

В процессе разработки и кастомизации конфигураций на платформе «1С:Предприятие 8» программисты часто сталкиваются с требованиями заказчиков внедрить нестандартную структуру номеров документов. Типичный пример такого запроса — формирование номера по шаблону: [Код подразделения (4 символа)] + [Номер месяца (2 символа)] + [Номер года (2 символа)] + [Порядковый номер в пределах года (4 символа)]. При этом ключевым условием является то, что порядковый номер должен быть сквозным в пределах года, а подразделение и месяц не должны делить нумерацию на отдельные независимые цепочки.

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

В чем сложность задачи и почему стандартный механизм 1С не подходит?

Платформа 1С умеет автоматически нумеровать документы и использовать префиксы. Если мы попытаемся решить задачу «в лоб» через предопределенную процедуру ПриУстановкеНовогоНомера, то сформируем префикс вида ПОДР0924 (где ПОДР — подразделение, 09 — сентябрь, 24 — год). Однако платформа трактует любую текстовую часть номера перед числовой как префикс.

Как только мы запишем документ с префиксом ПОДР0924, система выделит его в отдельный поток нумерации. При создании следующего документа для другого подразделения или в другом месяце (например, с префиксом СЛАД1024), платформа начнет нумерацию заново с единицы (0001). В результате мы получим документы ПОДР09240001 и СЛАД10240001. Но требования клиента иные: порядковый номер в конце должен возрастать линейно и непрерывно в течение всего года, независимо от подразделения и месяца (например: ПОДР09240001, а затем СЛАД10240002).

Для реализации такой логики нам придется полностью отказаться от стандартного автоматического присвоения номеров платформой и написать собственный алгоритм.

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

Проанализируем оптимальную структуру хранения данных. Пытаться вычислять последний порядковый номер прямым запросом к строковому полю Номер стандартной таблицы документов — плохое решение. Текстовый поиск с использованием операторов типа ПОДОБНО или функций работы со строками (например, ПОДСТРОКА) на базе с десятками тысяч документов будет вызывать серьезные задержки СУБД.

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

  1. Создаем в документе дополнительный реквизит ПорядковыйНомерЧислом (тип Число, длина 10, точность 0). В нем мы будем хранить чистый сквозной номер. Это позволит мгновенно находить максимальное значение простым запросом без анализа строк.
  2. В стандартном реквизите Номер (тип Строка) мы будем собирать и хранить итоговое красивое представление для пользователя.

Решение проблемы параллельной работы (Управляемые блокировки)

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

Чтобы избежать этой коллизии, расчет нового номера необходимо производить строго внутри транзакции в процедуре ПередЗаписью, предварительно установив Исключительную управляемую блокировку на данные. Блокировать всю таблицу документов не нужно — достаточно заблокировать диапазон дат текущего года, так как нумерация у нас периодическая в пределах года.

Рассмотрим по шагам программную реализацию этого механизма в модуле объекта документа.


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

Альтернативный вариант: использование независимого регистра сведений

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

Создадим регистр сведений СчетчикиНомеровДокументов со следующей структурой:

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

Проанализируем пример кода для реализации этой схемы:


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

КонецЕсли;

Запрет ручного редактирования номера пользователями

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

Для предотвращения этой проблемы выполним следующие настройки на уровне конфигурации:

  1. Откроем форму документа и для элемента управления поля Номер установим свойство ТолькоПросмотр в значение Истина (или свойство Доступность в значение Ложь).
  2. В окне свойств самого документа в Конфигураторе перейдем на вкладку «Права». Для ролей обычных пользователей (например, «Продавец» или «Бухгалтер») снимем флажок с права Редактирование номера. Это заблокирует поле на уровне платформы, оставив возможность корректировки только для администратора системы через специальные обработки.

Интеграция с Библиотекой стандартных подсистем (БСП)

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

Чтобы избежать конфликтов с БСП, рекомендуется:

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

← На главную