Как правильно реализовать механизм учета остатков отпусков в самописной конфигурации 1С?

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

Разработка кадрового учета с нуля — задача, с которой рано или поздно сталкивается любой разработчик при создании корпоративных систем на платформе «1С:Предприятие». Одним из самых коварных участков автоматизации является механизм расчета остатков ежегодных отпусков. На первый взгляд задача кажется тривиальной: достаточно ежемесячно прибавлять сотруднику положенные 2,33 дня отпуска и вычитать использованные дни при проведении документа «Отпуск». Однако за этой простотой скрываются серьезные архитектурные и законодательные подводные камни.

В этой статье мы подробно разберем, почему популярный подход с автоматическим ежемесячным начислением дней через регламентные задания является тупиковым, проанализируем архитектурные решения типовой конфигурации «1С:Зарплата и управление персоналом 3.1» и спроектируем надежную, производительную систему учета остатков отпусков для вашей самописной конфигурации.

Почему простое ежемесячное начисление дней (через регламентное задание) — это архитектурная ошибка?

Давайте разберем популярное заблуждение. Часто разработчики планируют архитектуру так: раз в месяц запускается фоновое задание, которое рассчитывает положенные сотруднику дни отпуска (например, 28 дней / 12 месяцев = 2,33 дня) и записывает их «плюсом» в регистр накопления. Использование отпуска фиксируется «минусом» при проведении соответствующего документа. На бумаге это выглядит логично, но на практике такая схема быстро разрушится под воздействием следующих факторов:

  1. Проблема «заднего числа»: Это главный враг любого ежемесячного линейного расчета. Кадровики регулярно вносят изменения в прошлые периоды. Например, сотрудник не вышел на работу в конце прошлого месяца. В текущем месяце выясняется, что это был прогул или неоплачиваемый отпуск. Кадровик вводит документ отсутствия «задним числом». Если дни отпуска за прошлый месяц уже были жестко начислены регламентным заданием, ваш баланс по регистру станет некорректным. Пересчитывать все движения по регистру накопления «с начала времен» при каждом изменении документов прошлых периодов — крайне неэффективно и ресурсоемко.
  2. Сдвиг рабочего года (ТК РФ): По законодательству РФ рабочий год сотрудника начинается не 1 января, а в день его приема на работу. Более того, окончание рабочего года динамически сдвигается вперед при наличии определенных видов отсутствий. Например, если сотрудник берет отпуск за свой счет суммарно более чем на 14 календарных дней в рамках своего рабочего года, все дни превышения сдвигают дату окончания его индивидуального рабочего года. Жесткое ежемесячное начисление по календарной сетке полностью лишает вас возможности отследить этот сдвиг.
  3. Правило округления излишков («Правило 15 дней»): При расчете остатка отпуска (особенно при увольнении) остаток дней рассчитывается пропорционально отработанным месяцам. При этом излишки менее половины месяца (менее 15 дней) отбрасываются, а излишки от 15 дней и более округляются до полного месяца. Считать это жестким регламентным заданием помесячно невозможно, так как расчет должен выполняться в привязке к индивидуальной дате приема сотрудника и конкретной дате расчета.

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

Как устроен учет остатков отпусков в 1С:ЗУП 3.1?

Чтобы спроектировать правильное решение, давайте проанализируем, как эта задача решена в типовом решении 1С:ЗУП 3.1. Разработчики фирмы «1С» полностью отказались от хранения постоянно увеличивающегося баланса в физических таблицах базы данных. Вместо этого используется динамический расчет «на лету» с применением интеллектуального кэширования.

В типовой конфигурации задействована следующая структура:

Проектируем правильную архитектуру для самописной конфигурации

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

Шаг 1. Проектирование структуры метаданных

Создадим минимально необходимый набор объектов для ведения учета.

1. Регистр сведений ПраваНаОтпуска (периодический):

Будет хранить количество положенных дней отпуска в год для каждого сотрудника.

2. Регистр накопления ИспользованныеОтпуска (тип «Обороты»):

Будет фиксировать только реальное использование отпусков сотрудниками.

3. Регистр сведений ПериодыИсключаемыеИзСтажа:

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

Шаг 2. Разработка алгоритма динамического расчета

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

Разберем по шагам логику расчета:

  1. Определить дату приема сотрудника на работу.
  2. Определить общее количество календарных дней, исключаемых из стажа за весь период работы до расчетной даты.
  3. Разбить весь стаж на рабочие годы, скорректировав даты их окончания на исключаемые периоды.
  4. Рассчитать количество «заработанных» месяцев в текущем (незавершенном) рабочем году с учетом правила 15 дней.
  5. Вычислить общее количество положенных (накопленных) дней отпуска за весь стаж работы.
  6. Получить сумму фактически использованных дней отпуска из регистра накопления ИспользованныеОтпуска.
  7. Вычесть использованные дни из накопленных. Полученная разница и будет актуальным остатком.

Посмотрим на пример реализации этой логики на языке 1С:


Функция ПолучитьОстатокОтпуска(Сотрудник, НаДату, ВидОтпуска) Экспорт

    // 1. Получаем дату приема сотрудника на работу
    ДатаПриема = ОбщегоНазначения.ЗначениеРеквизитаОбъекта(Сотрудник, "ДатаПриема");
    Если Не ЗначениеЗаполнено(ДатаПриема) Тогда
        Возврат 0;
    КонецЕсли;
    
    // Если дата расчета меньше даты приема, остатка нет
    Если НаДату < ДатаПриема Тогда
        Возврат 0;
    КонецЕсли;

    // 2. Получаем количество дней положенного отпуска в год
    ЗапросПрава = Новый Запрос;
    ЗапросПрава.Текст = 
        "ВЫБРАТЬ ПЕРВЫЕ 1
        |	ПраваНаОтпускаСрезПоследних.КоличествоДнейВГод КАК КоличествоДнейВГод
        |ИЗ
        |	РегистрСведений.ПраваНаОтпуска.СрезПоследних(&НаДату, Сотрудник = &Сотрудник И ВидОтпуска = &ВидОтпуска) КАК ПраваНаОтпускаСрезПоследних";
    
    ЗапросПрава.УстановитьПараметр("НаДату", НаДату);
    ЗапросПрава.УстановитьПараметр("Сотрудник", Сотрудник);
    ЗапросПрава.УстановитьПараметр("ВидОтпуска", ВидОтпуска);
    
    ВыборкаПрава = ЗапросПрава.Выполнить().Выбрать();
    КоличествоДнейВГод = 28; // Значение по умолчанию по ТК РФ
    Если ВыборкаПрава.Следующий() Тогда
        КоличествоДнейВГод = ВыборкаПрава.КоличествоДнейВГод;
    КонецЕсли;

    // 3. Вычисляем исключаемые периоды (например, отпуска за свой счет свыше 14 дней в каждом рабочем году)
    // Для простоты примера определим общее количество исключаемых дней в стаже
    ИсключаемыхДнейСтажа = ПолучитьИсключаемыеДни(Сотрудник, ДатаПриема, НаДату);

    // Вычисляем общую продолжительность работы в днях за вычетом исключаемых периодов
    ОбщийСтажВДнях = (НаДату - ДатаПриема) / 86400 - ИсключаемыхДнейСтажа;
    Если ОбщийСтажВДнях < 0 Тогда
        ОбщийСтажВДнях = 0;
    КонецЕсли;

    // Рассчитываем количество полных отработанных месяцев (с учетом правила 15 дней)
    // Среднее количество дней в месяце для расчета отпусков — 30.43 (или используем точный расчет по календарю)
    ВсегоМесяцевРаботы = ОкруглитьСтажДоМесяцев(ОбщийСтажВДнях);

    // 4. Считаем общее количество заработанных дней отпуска
    ЗаработаноДнейВсего = ВсегоМесяцевРаботы * (КоличествоДнейВГод / 12);

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

    // 6. Вычисляем итоговый остаток
    ОстатокОтпуска = ЗаработаноДнейВсего - ИспользованоДней;

    Возврат ОкруглитьДниОтпуска(ОстатокОтпуска);

КонецФункции

В приведенном кодовой структуре используются вспомогательные функции ПолучитьИсключаемыеДни(), ОкруглитьСтажДоМесяцев() и ОкруглитьДниОтпуска(). Они реализуют правила математического округления стажа и дней согласно внутренним регламентам организации и законодательству.

Шаг 3. Решение проблемы производительности при помощи Кэширования

Динамический расчет «на лету» работает быстро для одного сотрудника. Однако, если вам потребуется сформировать отчет по остаткам отпусков для всей компании (например, для 1000 сотрудников на определенную дату), система столкнется со снижением производительности, выполняя сложные расчеты для каждого человека в цикле.

Для решения этой проблемы применим паттерн проектирования «Ленивый Кэш» (Lazy Cache):

  1. Создадим непериодический регистр сведений КэшОстатковОтпусков с измерениями Сотрудник, ВидОтпуска, ДатаРасчета и ресурсом ОстатокДней.
  2. При запросе баланса система в первую очередь обращается к регистру кэша. Если запись на нужную дату найдена — она мгновенно возвращает результат.
  3. Если записи в кэше нет — выполняется тяжелый динамический расчет по приведенному выше алгоритму, после чего результат записывается в регистр кэша, чтобы при повторных вызовах не тратить ресурсы сервера.

Как предотвратить неактуальность кэша при вводе документов «задним числом»?

Для этого в системе настраивается механизм инвалидации (сброса) кэша. Мы создадим специальную Подписку на события платформы:

Посмотрим на пример обработчика подписки на событие сброса кэша:


Процедура СброситьКэшОстатковОтпусковПриЗаписи(Источник, Отказ) Экспорт

    // Проверяем, заполнено ли ключевое поле "Сотрудник" в источнике (документе)
    Если ОбщегоНазначения.ЕстьРеквизитОбъекта(Источник, "Сотрудник") Тогда
        Сотрудник = Источник.Сотрудник;
        Если ЗначениеЗаполнено(Сотрудник) Тогда
            
            // Удаляем записи кэша по данному сотруднику
            НаборЗаписей = РегистрыСведений.КэшОстатковОтпусков.СоздатьНаборЗаписей();
            НаборЗаписей.Отбор.Сотрудник.Установить(Сотрудник);
            НаборЗаписей.Записать(); // Записываем пустой набор, стирая старые данные кэша
            
        КонецЕсли;
    КонецЕсли;

КонецПроцедуры

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

Резюме

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

← На главную