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