В процессе разработки бизнес-логики в среде «1С:Предприятие 8.3» программисты часто сталкиваются с требованиями пользователей формировать номера документов по сложным правилам. Типичный пример — создание базового документа с номером вида 04.0001 (где 04 — номер месяца, а 0001 — уникальный порядковый номер) и его последующих «подчиненных» или корректирующих документов с суффиксами через косую черту (дробь), например: 04.0001/1, 04.0001/2.
Стандартный механизм автонумерации платформы 1С отлично справляется со сквозными числовыми последовательностями, но пасует перед подобными иерархическими структурами. В этой статье мы подробно разберем, почему ломается типовой парсер платформы, проанализируем технические риски параллельной работы пользователей и рассмотрим несколько надежных способов реализации такой задачи.
Давайте выясним причину, по которой стандартный механизм автонумерации платформы неприменим для документов с суффиксами вида /1, /2. Дело в том, как именно платформа 1С анализирует номера при создании нового объекта:
04.0001/1, система определит, что префикс документа равен 04.0001/, а числовая часть — 1.04.0001/2 вместо ожидаемого нового базового номера 04.0002. Вся стандартная нумерация в базе мгновенно нарушится.Именно поэтому для реализации подобных схем нам необходимо полностью отказаться от стандартной автонумерации для этого типа документов (или существенно её ограничить) и перейти на программное управление процессом.
Самый очевидный способ решить задачу — отключить автонумерацию в свойствах документа в Конфигураторе и формировать номер самостоятельно перед записью объекта. Разберем по шагам, как это реализовать.
Для вычисления очередного номера нам потребуется написать запрос, который выберет максимальный существующий номер за определенный период (в нашем случае — за текущий месяц), выделит из него числовую часть, увеличит на единицу и соберет итоговую строку.
Рассмотрим пример функции, которая генерирует новый базовый номер:
Функция ПолучитьНовыйБазовыйНомер(ТекущаяДата) Экспорт
ПрефиксМесяца = Формат(ТекущаяДата, "ДФ=ММ"); // Получим "04" для апреля
ШаблонНомера = ПрефиксМесяца + ".%";
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| МАКСИМУМ(Документы.Номер) КАК МаксНомер
|ИЗ
| Документ.НашДокумент КАК Документы
|ГДЕ
| Документы.Номер ПОДОБНО &ШаблонНомера
| И НЕ Документы.Номер ПОДОБНО ""%/%"""; // Исключаем подчиненные документы с дробью
Запрос.УстановитьПараметр("ШаблонНомера", ШаблонНомера);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
НовыйПорядковыйНомер = 1;
Если Выборка.Следующий() И ЗначениеЗаполнено(Выборка.МаксНомер) Тогда
// Извлекаем числовую часть после точки. Пример номера: "04.0001"
ЧастиНомера = СтрРазделить(Выборка.МаксНомер, ".");
Если ЧастиНомера.Количество() > 1 Тогда
ПорядковыйНомерСтрокой = ЧастиНомера[1];
НовыйПорядковыйНомер = Число(ПорядковыйНомерСтрокой) + 1;
КонецЕсли;
КонецЕсли;
// Форматируем порядковый номер до 4 знаков с ведущими нулями
ФорматированныйНомер = Формат(НовыйПорядковыйНомер, "ЧГ=0; ЧЦ=4; ЧВ=0");
Возврат ПрефиксМесяца + "." + ФорматированныйНомер;
КонецФункции
Для создания подчиненного документа логика меняется. Нам необходимо передать в функцию базовый документ, определить все его существующие подчиненные номера с косой чертой, найти максимальный суффикс и прибавить к нему единицу:
Функция ПолучитьНовыйПодчиненныйНомер(БазовыйНомер) Экспорт
ШаблонНомера = БазовыйНомер + "/%";
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| МАКСИМУМ(Документы.Номер) КАК МаксНомер
|ИЗ
| Документ.НашДокумент КАК Документы
|ГДЕ
| Документы.Номер ПОДОБНО &ШаблонНомера";
Запрос.УстановитьПараметр("ШаблонНомера", ШаблонНомера);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
НовыйСуффикс = 1;
Если Выборка.Следующий() И ЗначениеЗаполнено(Выборка.МаксНомер) Тогда
// Извлекаем цифру после косой черты
ЧастиНомера = СтрРазделить(Выборка.МаксНомер, "/");
Если ЧастиНомера.Количество() > 1 Тогда
СуффиксСтрокой = ЧастиНомера[1];
НовыйСуффикс = Число(СуффиксСтрокой) + 1;
КонецЕсли;
КонецЕсли;
Возврат БазовыйНомер + "/" + Формат(НовыйСуффикс, "ЧГ=0");
КонецФункции
Разработчики часто совершают опасную ошибку, внедряя ручную нумерацию запросами без учета многопользовательской среды. Представим ситуацию: два бухгалтера одновременно нажимают кнопку «Записать» на новых документах. Обе сессии выполняют вышеописанный запрос практически одновременно. Система вернет им одинаковый максимальный номер, например 04.0001. Обе сессии увеличат его на 1 и получат 04.0002. В результате при записи один из пользователей гарантированно получит ошибку неуникальности номера, либо база данных зависнет из-за взаимной блокировки (Deadlock).
Чтобы избежать подобных проблем, перед выполнением запроса генерации номера необходимо установить управляемую блокировку на пространство данных этого документа:
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Документ.НашДокумент");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
// Только после этого выполняем запрос получения максимального номера
// и записываем новый документ. Блокировка снимется автоматически при завершении транзакции.
Использование исключительной управляемой блокировки гарантирует, что вторая сессия будет послушно ожидать завершения транзакции первой сессии и прочитает из базы данных уже обновленный, актуальный номер.
Хотя первый способ полностью работоспособен, он имеет существенный минус: выполнение тяжелых строковых запросов по всей таблице документов при росте базы данных (когда документов становится сотни тысяч) начнет заметно снижать производительность системы. Кроме того, блокировка всей таблицы документов снижает параллельность работы пользователей.
Профессиональные разработчики 1С рекомендуют использовать для генерации номеров вспомогательный независимый Регистр сведений (назовем его ОчередныеНомераДокументов).
Структура регистра сведений:
ПериодНумерации (тип: Строка, длина 6 — для хранения периода, например "202404").БазовыйНомер (тип: Строка, длина 15 — заполняется только для подчиненных документов, для базовых оставляем пустой).ТекущийНомер (тип: Число, длина 10 — здесь хранится последнее выданное числовое значение).Рассмотрим, как красиво и производительно работает этот механизм. При создании документа мы обращаемся не к таблице документов, а к конкретной строке этого небольшого регистра. Блокировка накладывается точечно — только на одну запись регистра по ключевым измерениям!
Приведем пример программного кода для генерации очередного номера через регистр сведений:
Функция ПолучитьОчереднойНомерЧерезРегистр(ТекущийПериод, БазовыйНомерДока = "") Экспорт
// 1. Устанавливаем точечную блокировку на запись регистра
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ОчередныеНомераДокументов");
ЭлементБлокировки.УстановитьЗначение("ПериодНумерации", ТекущийПериод);
ЭлементБлокировки.УстановитьЗначение("БазовыйНомер", БазовыйНомерДока);
Блокировка.Заблокировать();
// 2. Считываем текущее значение номера
Отбор = Новый Структура("ПериодНумерации, БазовыйНомер", ТекущийПериод, БазовыйНомерДока);
ЗначенияРесурсов = РегистрыСведений.ОчередныеНомераДокументов.Получить(Отбор);
Если ЗначенияРесурсов.ТекущийНомер = Неопределено Тогда
НовоеЧислоНомера = 1;
Иначе
НовоеЧислоНомера = ЗначенияРесурсов.ТекущийНомер + 1;
EndIf;
// 3. Записываем новое значение в регистр
МенеджерЗаписи = РегистрыСведений.ОчередныеНомераДокументов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.ПериодНумерации = ТекущийПериод;
МенеджерЗаписи.БазовыйНомер = БазовыйНомерДока;
МенеджерЗаписи.ТекущийНомер = НовоеЧислоНомера;
МенеджерЗаписи.Записать(Истина);
// 4. Формируем строковое представление номера
Если ПустаяСтрока(БазовыйНомерДока) Тогда
// Формируем базовый номер: "04.0001"
ПрефиксМесяца = Сред(ТекущийПериод, 5, 2);
СтрокаНомера = ПрефиксМесяца + "." + Формат(НовоеЧислоНомера, "ЧГ=0; ЧЦ=4; ЧВ=0");
Иначе
// Формируем подчиненный номер: "04.0001/1"
СтрокаНомера = БазовыйНомерДока + "/" + Формат(НовоеЧислоНомера, "ЧГ=0");
КонецЕсли;
Возврат СтрокаНомера;
КонецФункции
Этот подход признан лучшей практикой в промышленной разработке на платформе 1С, так как он сводит к минимуму нагрузку на СУБД и полностью предотвращает конфликты блокировок у пользователей.
Зачастую бухгалтеры просят дробные номера исключительно ради красоты печатных форм (счетов-фактур, актов, накладных) и соответствия требованиям законодательства. При этом внутри базы данных 1С системные номера могут оставаться абсолютно стандартными и сквозными.
Если в вашей конфигурации используется Библиотека стандартных подсистем (БСП), мы рекомендуем использовать встроенные механизмы модификации печатных номеров. Этот подход позволяет полностью избежать изменения стандартной автонумерации:
0000-000412, 0000-000413. Это гарантирует максимальное быстродействие СУБД и бесконфликтную работу пользователей.ДокументОснование).ПрефиксацияОбъектовКлиентСерверПереопределяемый) или функции форматирования номера в модуле менеджера документа. Системный номер программно преобразуется к нужному виду непосредственно перед выводом на печать.Рассмотрим пример функции, преобразующей системный номер на печати:
Функция ПолучитьНомерНаПечать(ДокументОбъект) Экспорт
СистемныйНомер = ОбщегоНазначенияКлиентСервер.ПолучитьНомерНаПечать(ДокументОбъект.Номер);
Если ЗначениеЗаполнено(ДокументОбъект.ДокументОснование) Тогда
// Документ подчиненный, сформируем дробный номер для печати
НомерОснования = ОбщегоНазначенияКлиентСервер.ПолучитьНомерНаПечать(ДокументОбъект.ДокументОснование.Номер);
// Вычисляем порядковый номер среди подчиненных документов
ПорядковыйНомер = ПолучитьПорядковыйНомерПодчиненного(ДокументОбъект);
Возврат НомерОснования + "/" + ПорядковыйНомер;
Иначе
// Базовый документ, просто форматируем системный номер под требования
Возврат ПреобразоватьНомерПодФорматМесяца(СистемныйНомер, ДокументОбъект.Дата);
КонецЕсли;
КонецФункции
Преимущество данного метода заключается в том, что структура базы данных остается типовой. Мы не вмешиваемся в механизмы платформы 1С, не рискуем стабильностью системы, а пользователи при этом видят в печатных формах именно те дробные номера, которые требуют регламентирующие органы.
Какой бы из способов вы ни выбрали, при проектировании структуры «Базовый/Подчиненный» критически важно закладывать достаточную длину стандартного реквизита Номер в свойствах метаданных документа.
По умолчанию в большинстве типовых конфигураций 1С длина номера ограничена 11 символами. Проанализируем длину нашей будущей строки номера:
04.0001.04.0001/10 (итого 10 символов).МС00-), то итоговая строка легко превысит лимит в 11 символов.При превышении лимита длины номера платформа начнет нещадно обрезать правые символы. В результате номера документов начнут дублироваться, вызывая критические ошибки при записи. Чтобы застраховаться от этого сценария, обязательно увеличьте длину реквизита Номер в свойствах документа в Конфигураторе с запасом (например, до 15–20 символов).
Реализация нестандартной нумерации документов с использованием дробей требует от разработчика вдумчивого подхода:
Исключительной блокировки.