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