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

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

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

Суть проблемы: пустые значения и особенности учета

Представим ситуацию: у нас есть временная таблица с ежедневными данными по сотрудникам. Для сотрудника Иванова за период с 1 по 15 число есть только плановое время, но нет сведений о рабочем месте (должности, подразделении), поскольку он еще не был принят на работу. С 16 по 30 число Иванов уже работал, и по нему есть все необходимые кадровые данные, а также плановое и фактическое время.

Требования к отчету таковы, что плановое время сотрудника должно учитываться за весь период целиком, независимо от даты приема на работу. Например, если норма за квартал составляет 440 часов, и Иванов принят только в середине первого месяца, его плановое время для расчета коэффициента премирования все равно берется за полный квартал. Более того, плановое время, начисленное до приема на работу, должно быть суммировано с плановым временем его первого рабочего периода, чтобы не создавать отдельные строки с пустыми значениями.

Стандартная группировка в запросе, например, по ФИО, Должность, Подразделение с агрегатными функциями СУММА(План) и МАКСИМУМ(Должность), приведет к нежелательным результатам:

  1. Будет создана отдельная строка для Иванова с пустыми значениями Должность и Подразделение, содержащая плановое время за период до приема.
  2. Для каждого изменения кадровых данных (перевод в другое подразделение, изменение должности или оклада) будут созданы отдельные строки, что в целом верно, но "пустая" строка остается обособленной.

Наша задача — "присоединить" плановое время из этой "пустой" строки к плановому времени первой "рабочей" строки сотрудника.

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

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

Для реализации этой стратегии мы будем использовать возможности системы 1С для работы с интервальными регистрами сведений и построения сложных запросов с временными таблицами. Мы эмулируем функциональность LAG/LEAD (функции для доступа к предыдущим/следующим строкам) с помощью самосоединений и подзапросов.

Шаг 1: Получение исходных данных

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


// Пример получения исходных данных (может быть результатом соединения нескольких источников)
ВЫБРАТЬ
    План.Сотрудник.ФИО КАК ФИО,
    План.Дата КАК Дата,
    План.ПлановоеВремя КАК ПлановоеВремя,
    ЕСТЬNULL(Кадры.Должность, NULL) КАК Должность, // Будет NULL, если нет кадровых данных на дату
    ЕСТЬNULL(Кадры.Подразделение, NULL) КАК Подразделение, // Будет NULL, если нет кадровых данных на дату
    ЕСТЬNULL(Кадры.ГрафикРаботы, NULL) КАК ГрафикРаботы,
    ... // Другие кадровые данные, если они важны для группировки
ПОМЕСТИТЬ ВТ_ИсходныеДанные
ИЗ
    РегистрСведений.ПлановоеВремяСотрудников КАК План // Пример регистра
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КадроваяИсторияСотрудников.СрезПоследних(&ДатаПериодаОтчета, ) КАК Кадры
        ПО План.Сотрудник = Кадры.Сотрудник
ГДЕ
    План.Дата МЕЖДУ &ДатаНачалаОтчета И &ДатаОкончанияОтчета
    // Дополнительные условия отбора, если нужны
;

Шаг 2: Определение первых кадровых данных для каждого сотрудника

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


ВЫБРАТЬ
    Кадры.Сотрудник КАК ФИО,
    Кадры.Должность КАК ПерваяДолжность,
    Кадры.Подразделение КАК ПервоеПодразделение,
    Кадры.ГрафикРаботы КАК ПервыйГрафикРаботы,
    Кадры.Дата КАК ПерваяДатаПриема
ПОМЕСТИТЬ ВТ_ПервыеКадровыеДанные
ИЗ
    РегистрСведений.КадроваяИсторияСотрудников КАК Кадры
ГДЕ
    Кадры.Должность ЕСТЬ НЕ NULL // Ищем только записи с фактическими кадровыми данными
    И Кадры.Период >= &ДатаНачалаОтчета // Или Кадры.Период в пределах нужного диапазона
;

// Для каждого сотрудника нам нужна только самая ранняя запись
ВЫБРАТЬ
    ПервыеДанные.ФИО,
    ПервыеДанные.ПерваяДолжность,
    ПервыеДанные.ПервоеПодразделение,
    ПервыеДанные.ПервыйГрафикРаботы,
    МИНИМУМ(ПервыеДанные.ПерваяДатаПриема) КАК ПерваяДатаПриемаФактическая
ПОМЕСТИТЬ ВТ_ПервыеКадровыеДанныеОчищенные
ИЗ
    ВТ_ПервыеКадровыеДанные КАК ПервыеДанные
СГРУППИРОВАТЬ ПО
    ПервыеДанные.ФИО,
    ПервыеДанные.ПерваяДолжность,
    ПервыеДанные.ПервоеПодразделение,
    ПервыеДанные.ПервыйГрафикРаботы
;

Важный момент: если сотрудник принимался несколько раз, нам нужна самая первая запись, соответствующая требованиям отчета. Возможно, придется использовать более сложные запросы с ТОП 1 и сортировкой.

Шаг 3: Заполнение пустых значений в исходных данных

Теперь мы можем соединить наши исходные данные с определенными первыми кадровыми данными и заполнить NULL значения.


ВЫБРАТЬ
    ИсходныеДанные.ФИО,
    ИсходныеДанные.Дата,
    ИсходныеДанные.ПлановоеВремя,
    ИсходныеДанные.Факт,
    ВЫБОР
        КОГДА ИсходныеДанные.Должность ЕСТЬ NULL
            ТОГДА ПервыеКадры.ПерваяДолжность
        ИНАЧЕ ИсходныеДанные.Должность
    КОНЕЦ КАК Должность,
    ВЫБОР
        КОГДА ИсходныеДанные.Подразделение ЕСТЬ NULL
            ТОГДА ПервыеКадры.ПервоеПодразделение
        ИНАЧЕ ИсходныеДанные.Подразделение
    КОНЕЦ КАК Подразделение,
    ВЫБОР
        КОГДА ИсходныеДанные.ГрафикРаботы ЕСТЬ NULL
            ТОГДА ПервыеКадры.ПервыйГрафикРаботы
        ИНАЧЕ ИсходныеДанные.ГрафикРаботы
    КОНЕЦ КАК ГрафикРаботы,
    ... // Аналогично для других полей
ПОМЕСТИТЬ ВТ_ПодготовленныеДанные
ИЗ
    ВТ_ИсходныеДанные КАК ИсходныеДанные
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПервыеКадровыеДанныеОчищенные КАК ПервыеКадры
        ПО ИсходныеДанные.ФИО = ПервыеКадры.ФИО
ГДЕ
    ИсходныеДанные.Дата < ПервыеКадры.ПерваяДатаПриемаФактическая
    И ИсходныеДанные.Должность ЕСТЬ NULL // Применяем "заполнитель" только для пустых строк до приема
    ИЛИ ИсходныеДанные.Должность ЕСТЬ НЕ NULL // И оставляем рабочие записи как есть
;

// Объединяем "заполненные" пустые строки с уже имеющимися рабочими данными
// (Если ИсходныеДанные уже содержали Должность, то мы ее не перезаписываем)
ВЫБРАТЬ
    ТекДанные.ФИО,
    ТекДанные.Дата,
    ТекДанные.ПлановоеВремя,
    ТекДанные.Факт,
    ТекДанные.Должность,
    ТекДанные.Подразделение,
    ТекДанные.ГрафикРаботы
ИЗ
    ВТ_ПодготовленныеДанные КАК ТекДанные
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    ИсходныеДанные.ФИО,
    ИсходныеДанные.Дата,
    ИсходныеДанные.ПлановоеВремя,
    ИсходныеДанные.Факт,
    ИсходныеДанные.Должность,
    ИсходныеДанные.Подразделение,
    ИсходныеДанные.ГрафикРаботы
ИЗ
    ВТ_ИсходныеДанные КАК ИсходныеДанные
ГДЕ
    ИсходныеДанные.Должность ЕСТЬ НЕ NULL;

В результате этого шага мы получаем временную таблицу ВТ_ПодготовленныеДанные, где для каждого дня, включая период до приема на работу, у нас есть "притянутые" кадровые данные (должность, подразделение, график), соответствующие первому рабочему месту сотрудника. Теперь нет "пустых" строк, которые мешали бы группировке.

Шаг 4: Окончательная группировка по периодам изменений

Теперь, когда у нас нет проблем с пустыми значениями, мы можем выполнить группировку по ФИО и всем изменяющимся кадровым реквизитам. Однако, чтобы получить периоды "от и до", нам понадобится более сложная логика, которую мы рассмотрим в разделе "Формирование периодов (ДатыНачала - ДатыОкончания)". Если же требуется просто суммировать план и факт по уникальным комбинациям кадровых данных, это можно сделать стандартной группировкой:


ВЫБРАТЬ
    ПодготовленныеДанные.ФИО,
    ПодготовленныеДанные.Должность,
    ПодготовленныеДанные.Подразделение,
    ПодготовленныеДанные.ГрафикРаботы,
    СУММА(ПодготовленныеДанные.ПлановоеВремя) КАК СуммаПлана,
    СУММА(ПодготовленныеДанные.Факт) КАК СуммаФакта
ПОМЕСТИТЬ ВТ_ИтоговыеДанные
ИЗ
    ВТ_ПодготовленныеДанные КАК ПодготовленныеДанные
СГРУППИРОВАТЬ ПО
    ПодготовленныеДанные.ФИО,
    ПодготовленныеДанные.Должность,
    ПодготовленныеДанные.Подразделение,
    ПодготовленныеДанные.ГрафикРаботы
;

Этот подход успешно решает проблему объединения планового времени из "пустых" строк с первым рабочим периодом, так как теперь все строки содержат кадровые данные, и группировка выполняется по актуальным значениям.

Стратегия 2: Многоэтапная агрегация и корректировка

Если предварительное заполнение пустых значений по каким-то причинам не подходит или требует слишком сложных условий, можно использовать многоэтапный подход с расчетом общего планового времени отдельно, а затем его распределением.

Шаг 1: Расчет общего планового времени для "пустых" периодов

Сначала мы суммируем плановое время по сотрудникам для тех дней, когда у них не было явных кадровых данных (до приема на работу).


ВЫБРАТЬ
    ИсходныеДанные.ФИО КАК ФИО,
    СУММА(ИсходныеДанные.ПлановоеВремя) КАК ПланДоПриема
ПОМЕСТИТЬ ВТ_ПланДоПриема
ИЗ
    ВТ_ИсходныеДанные КАК ИсходныеДанные
ГДЕ
    ИсходныеДанные.Должность ЕСТЬ NULL
    И ИсходныеДанные.Подразделение ЕСТЬ NULL
СГРУППИРОВАТЬ ПО
    ИсходныеДанные.ФИО
;

Шаг 2: Группировка данных по рабочим периодам

Затем мы группируем данные только для тех периодов, когда сотрудник фактически работал и имел кадровые данные. Здесь мы получаем план и факт, относящиеся к конкретным должностям/подразделениям.


ВЫБРАТЬ
    ИсходныеДанные.ФИО,
    ИсходныеДанные.Должность,
    ИсходныеДанные.Подразделение,
    ИсходныеДанные.ГрафикРаботы,
    СУММА(ИсходныеДанные.ПлановоеВремя) КАК ПланРабочийПериод,
    СУММА(ИсходныеДанные.Факт) КАК ФактРабочийПериод,
    МИН(ИсходныеДанные.Дата) КАК ДатаНачалаПериода // Для определения первого рабочего периода
ПОМЕСТИТЬ ВТ_РабочиеПериоды
ИЗ
    ВТ_ИсходныеДанные КАК ИсходныеДанные
ГДЕ
    ИсходныеДанные.Должность ЕСТЬ НЕ NULL
СГРУППИРОВАТЬ ПО
    ИсходныеДанные.ФИО,
    ИсходныеДанные.Должность,
    ИсходныеДанные.Подразделение,
    ИсходныеДанные.ГрафикРаботы
УПОРЯДОЧИТЬ ПО
    ИсходныеДанные.ФИО,
    ДатаНачалаПериода
;

Шаг 3: Определение первого рабочего периода для каждого сотрудника

Нам необходимо определить, какая из строк в ВТ_РабочиеПериоды является первой для каждого сотрудника. Именно к ней мы будем добавлять план до приема.


ВЫБРАТЬ
    РабочиеПериоды.ФИО,
    МИНИМУМ(РабочиеПериоды.ДатаНачалаПериода) КАК ПерваяДатаРаботы
ПОМЕСТИТЬ ВТ_ПерваяДатаРаботы
ИЗ
    ВТ_РабочиеПериоды КАК РабочиеПериоды
СГРУППИРОВАТЬ ПО
    РабочиеПериоды.ФИО
;

Шаг 4: Объединение и корректировка планового времени

Теперь мы можем объединить все три временные таблицы. Для строки, соответствующей первому рабочему периоду сотрудника, мы добавим сумму ПланДоПриема.


ВЫБРАТЬ
    РабочиеПериоды.ФИО,
    РабочиеПериоды.Должность,
    РабочиеПериоды.Подразделение,
    РабочиеПериоды.ГрафикРаботы,
    ВЫБОР
        КОГДА РабочиеПериоды.ДатаНачалаПериода = ПерваяДатаРаботы.ПерваяДатаРаботы
            ТОГДА РабочиеПериоды.ПланРабочийПериод + ЕСТЬNULL(ПланДоПриема.ПланДоПриема, 0)
        ИНАЧЕ РабочиеПериоды.ПланРабочийПериод
    КОНЕЦ КАК ИтоговыйПлан,
    РабочиеПериоды.ФактРабочийПериод КАК ИтоговыйФакт
ИЗ
    ВТ_РабочиеПериоды КАК РабочиеПериоды
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПланДоПриема КАК ПланДоПриема
        ПО РабочиеПериоды.ФИО = ПланДоПриема.ФИО
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПерваяДатаРаботы КАК ПерваяДатаРаботы
        ПО РабочиеПериоды.ФИО = ПерваяДатаРаботы.ФИО
;

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

Формирование периодов (ДатыНачала - ДатыОкончания)

После того как мы сгруппировали данные по всем необходимым реквизитам (должность, подразделение, график и т.д.), нам часто требуется представить результат не просто в виде набора строк с агрегированными значениями, а в виде периодов "от и до", когда эти реквизиты были неизменны. Например, вместо строк:

Мы хотим получить:

Для этого нам понадобится определить "точки изменения" и построить периоды между ними.

Шаг 1: Определение уникальных точек изменений

Сначала мы выбираем все уникальные комбинации ФИО, Даты и всех ключевых полей, которые определяют период (Должность, Подразделение, ГрафикРаботы и т.д.). Эти даты будут границами наших периодов.


ВЫБРАТЬ РАЗЛИЧНЫЕ
    ПодготовленныеДанные.ФИО,
    ПодготовленныеДанные.Дата,
    ПодготовленныеДанные.Должность,
    ПодготовленныеДанные.Подразделение,
    ПодготовленныеДанные.ГрафикРаботы
    ... // Все поля, изменение которых определяет новый период
ПОМЕСТИТЬ ВТ_ТочкиИзменений
ИЗ
    ВТ_ПодготовленныеДанные КАК ПодготовленныеДанные // Используем данные после заполнения пустых значений
УПОРЯДОЧИТЬ ПО
    ФИО, Дата;

Шаг 2: Построение периодов через самосоединение

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


ВЫБРАТЬ
    Тек.ФИО,
    Тек.Должность,
    Тек.Подразделение,
    Тек.ГрафикРаботы,
    Тек.Дата КАК ДатаНачала,
    // Находим дату следующей точки изменения и вычитаем 1 день
    // Если следующей точки нет, то используем конец отчетного периода
    ЕСТЬNULL(
        ДОБАВИТЬКДАТЕ(
            (ВЫБРАТЬ МИНИМУМ(След.Дата)
             ИЗ ВТ_ТочкиИзменений КАК След
             ГДЕ След.ФИО = Тек.ФИО
               И След.Дата > Тек.Дата
               И (След.Должность <> Тек.Должность
                  ИЛИ След.Подразделение <> Тек.Подразделение
                  ИЛИ След.ГрафикРаботы <> Тек.ГрафикРаботы
                  // ... ИЛИ другие поля изменились
                  )
            ), ДЕНЬ, -1),
        КОНЕЦПЕРИОДА(&ДатаОкончанияОтчета, ДЕНЬ)) КАК ДатаОкончания
ПОМЕСТИТЬ ВТ_ПериодыБезАгрегации
ИЗ
    ВТ_ТочкиИзменений КАК Тек
;

Пояснение к подзапросу для ДатаОкончания: Мы ищем минимальную дату из ВТ_ТочкиИзменений (то есть, ближайшую следующую дату) для того же сотрудника, которая строго больше текущей даты и при этом хотя бы одно из ключевых полей изменилось. Если такая следующая дата найдена, мы отнимаем от нее один день, чтобы получить дату окончания текущего периода. Если следующая дата не найдена (это последняя точка изменения для сотрудника), то период заканчивается в &ДатаОкончанияОтчета.

Шаг 3: Агрегация данных по сформированным периодам

Теперь, когда у нас есть четкие границы периодов, мы можем соединить их с исходными данными и выполнить агрегацию.


ВЫБРАТЬ
    Периоды.ФИО,
    Периоды.Должность,
    Периоды.Подразделение,
    Периоды.ГрафикРаботы,
    Периоды.ДатаНачала,
    Периоды.ДатаОкончания,
    СУММА(ИсходныеДанные.ПлановоеВремя) КАК ПланЗаПериод,
    СУММА(ИсходныеДанные.Факт) КАК ФактЗаПериод
ИЗ
    ВТ_ПериодыБезАгрегации КАК Периоды
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ПодготовленныеДанные КАК ИсходныеДанные
        ПО Периоды.ФИО = ИсходныеДанные.ФИО
        И ИсходныеДанные.Дата МЕЖДУ Периоды.ДатаНачала И Периоды.ДатаОкончания
СГРУППИРОВАТЬ ПО
    Периоды.ФИО,
    Периоды.Должность,
    Периоды.Подразделение,
    Периоды.ГрафикРаботы,
    Периоды.ДатаНачала,
    Периоды.ДатаОкончания
УПОРЯДОЧИТЬ ПО
    Периоды.ФИО,
    Периоды.ДатаНачала
;

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

Заключение

Как мы видим, группировка строк с пустыми значениями и формирование периодов в 1С — это многоэтапная задача, требующая построения нескольких временных таблиц и использования сложных соединений и подзапросов. Мы рассмотрели две основные стратегии решения проблемы: предварительное заполнение пустых значений кадровых данных и многоэтапную агрегацию с корректировкой. Первая стратегия часто оказывается более предпочтительной, так как упрощает последующую логику группировки, делая данные более "чистыми" и полными. Также мы подробно разобрали, как сформировать периоды "от и до" на основе точек изменений в кадровой истории сотрудника. Выбор конкретного подхода всегда зависит от специфики бизнес-требований и объема обрабатываемых данных.

← На главную