Как реализовать контроль штатного расписания при проведении документа Приём на работу в 1С?

Программист 1С v8.3 (Обычные формы) Управленческий учет
← На главную

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

Анализ исходной проблемы и исправление ошибок в коде

Разберем типичную ситуацию: разработчик пытается получить данные из двух разных регистров (регистр сведений ШтатноеРасписание и регистр накопления ЗанятиеШтатныеПозиции) с помощью двух отдельных запросов. Основная ошибка, с которой сталкиваются новички — неправильный обход выборки результата запроса.

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

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


// Получение количества ставок из штатного расписания
ЗапросПоРасписанию = Новый Запрос;
ЗапросПоРасписанию.Текст = 
    "ВЫБРАТЬ
    |   ШтатноеРасписание.КоличествоСтавок КАК КоличествоСтавок
    |ИЗ
    |   РегистрСведений.ШтатноеРасписание КАК ШтатноеРасписание
    |ГДЕ
    |   ШтатноеРасписание.Организация = &Организация
    |   И ШтатноеРасписание.Подразделение = &Подразделение
    |   И ШтатноеРасписание.Должность = &Должность";

ЗапросПоРасписанию.УстановитьПараметр("Организация", Организация);
ЗапросПоРасписанию.УстановитьПараметр("Подразделение", Подразделение);
ЗапросПоРасписанию.УстановитьПараметр("Должность", Должность);

ВыборкаРасписание = ЗапросПоРасписанию.Выполнить().Выбрать();

СтавкиРасписание = 0;
Если ВыборкаРасписание.Следующий() Тогда
    СтавкиРасписание = ВыборкаРасписание.КоличествоСтавок;
КонецЕсли;

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

Оптимизация: объединение запросов через Левое Соединение

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

Рассмотрим, как объединить получение данных в один запрос:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |   ЕСТЬNULL(ШтатноеРасписание.КоличествоСтавок, 0) КАК ЛимитСтавок,
    |   ЕСТЬNULL(ЗанятыеПозиции.КоличествоСтавокОстаток, 0) КАК ЗанятоСтавок
    |ИЗ
    |   РегистрСведений.ШтатноеРасписание.СрезПоследних(&Период, Организация = &Организация 
    |       И Подразделение = &Подразделение И Должность = &Должность) КАК ШтатноеРасписание
    |       ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ЗанятыеШтатныеПозиции.Остатки(&Период, Организация = &Организация 
    |           И Подразделение = &Подразделение И Должность = &Должность) КАК ЗанятыеПозиции
    |       ПО ШтатноеРасписание.Организация = ЗанятыеПозиции.Организация
    |           И ШтатноеРасписание.Подразделение = ЗанятыеПозиции.Подразделение
    |           И ШтатноеРасписание.Должность = ЗанятыеПозиции.Должность";

Запрос.УстановитьПараметр("Организация", Организация);
Запрос.УстановитьПараметр("Подразделение", Подразделение);
Запрос.УстановитьПараметр("Должность", Должность);
Запрос.УстановитьПараметр("Период", Дата);

Выборка = Запрос.Выполнить().Выбрать();

Использование ЛЕВОЕ СОЕДИНЕНИЕ гарантирует, что мы получим данные о лимите, даже если в регистре накопления еще нет ни одной записи о занятых ставках. Функция ЕСТЬNULL заменяет отсутствие данных на ноль, что позволяет избежать ошибок при арифметических операциях.

Работа с блокировками данных (DataLock)

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

Чтобы этого избежать, перед выполнением запроса необходимо установить явную блокировку. Разберем пример:


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

Только после вызова метода Заблокировать() можно выполнять запрос и проверять остатки. Это гарантирует, что другие сеансы не смогут изменить данные по этой позиции, пока текущий документ не закончит проведение.

Проблема перепроведения документа

Проанализируем ситуацию: бухгалтер открывает уже проведенный документ и нажимает "Провести" снова. Если мы просто проверим остатки, то увидим, что ставка уже занята (этим же самым документом!). Система выдаст ошибку, хотя на самом деле превышения нет.

Существует два основных способа решения этой проблемы:

  1. Очистка движений перед проверкой: В начале процедуры проведения мы записываем пустой набор движений документа. Тогда при выполнении запроса остатков система "забудет" про этот документ, и мы получим чистую картину свободных мест.
  2. Анализ после записи движений: Мы сначала записываем новое движение документа, а затем проверяем остатки в регистре на наличие отрицательных значений. Если остаток стал меньше нуля — вызываем исключение и отменяем транзакцию.

Рассмотрим первый вариант, как наиболее наглядный для новичков:


// Очищаем старые движения, чтобы они не мешали проверке остатков
Движения.ЗанятыеШтатныеПозиции.Записывать = Истина;
Движения.ЗанятыеШтатныеПозиции.Очистить();
Движения.ЗанятыеШтатныеПозиции.Записать();

// Далее выполняем запрос остатков и проверку условий
// ...

Методологические рекомендации

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

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

← На главную