Разработка кадрового учета с нуля — задача, с которой рано или поздно сталкивается любой разработчик при создании корпоративных систем на платформе «1С:Предприятие». Одним из самых коварных участков автоматизации является механизм расчета остатков ежегодных отпусков. На первый взгляд задача кажется тривиальной: достаточно ежемесячно прибавлять сотруднику положенные 2,33 дня отпуска и вычитать использованные дни при проведении документа «Отпуск». Однако за этой простотой скрываются серьезные архитектурные и законодательные подводные камни.
В этой статье мы подробно разберем, почему популярный подход с автоматическим ежемесячным начислением дней через регламентные задания является тупиковым, проанализируем архитектурные решения типовой конфигурации «1С:Зарплата и управление персоналом 3.1» и спроектируем надежную, производительную систему учета остатков отпусков для вашей самописной конфигурации.
Давайте разберем популярное заблуждение. Часто разработчики планируют архитектуру так: раз в месяц запускается фоновое задание, которое рассчитывает положенные сотруднику дни отпуска (например, 28 дней / 12 месяцев = 2,33 дня) и записывает их «плюсом» в регистр накопления. Использование отпуска фиксируется «минусом» при проведении соответствующего документа. На бумаге это выглядит логично, но на практике такая схема быстро разрушится под воздействием следующих факторов:
Таким образом, физическое хранение постоянно нарастающего итога заработанных дней в регистре накопления — это архитектурный тупик. Баланс дней отпуска должен определяться динамически.
Чтобы спроектировать правильное решение, давайте проанализируем, как эта задача решена в типовом решении 1С:ЗУП 3.1. Разработчики фирмы «1С» полностью отказались от хранения постоянно увеличивающегося баланса в физических таблицах базы данных. Вместо этого используется динамический расчет «на лету» с применением интеллектуального кэширования.
В типовой конфигурации задействована следующая структура:
Давайте разберем по шагам, как построить надежный, производительный и простой механизм остатков отпусков в вашей собственной конфигурации.
Создадим минимально необходимый набор объектов для ведения учета.
1. Регистр сведений ПраваНаОтпуска (периодический):
Будет хранить количество положенных дней отпуска в год для каждого сотрудника.
Сотрудник (СправочникСсылка.Сотрудники), ВидОтпуска (СправочникСсылка.ВидыОтпусков).КоличествоДнейВГод (Число, 15, 2).2. Регистр накопления ИспользованныеОтпуска (тип «Обороты»):
Будет фиксировать только реальное использование отпусков сотрудниками.
Сотрудник (СправочникСсылка.Сотрудники), ВидОтпуска (СправочникСсылка.ВидыОтпусков).РабочийГодНачало (Дата), РабочийГодОкончание (Дата) — важно хранить, за какой рабочий год списывается отпуск.КоличествоДней (Число, 5, 2).3. Регистр сведений ПериодыИсключаемыеИзСтажа:
Здесь будут фиксироваться периоды отсутствия сотрудников, которые сдвигают границы рабочего года (например, отпуска без сохранения оплаты, отпуска по уходу за ребенком).
Сотрудник, ДатаНачала, ДатаОкончания.КоличествоДнейИсключения.Вся магия расчета должна происходить в общем модуле (например, УчетОтпусковВызовСервера). Давайте напишем функцию, которая будет динамически рассчитывать остаток на любую произвольную дату.
Разберем по шагам логику расчета:
ИспользованныеОтпуска.Посмотрим на пример реализации этой логики на языке 1С:
Функция ПолучитьОстатокОтпуска(Сотрудник, НаДату, ВидОтпуска) Экспорт
// 1. Получаем дату приема сотрудника на работу
ДатаПриема = ОбщегоНазначения.ЗначениеРеквизитаОбъекта(Сотрудник, "ДатаПриема");
Если Не ЗначениеЗаполнено(ДатаПриема) Тогда
Возврат 0;
КонецЕсли;
// Если дата расчета меньше даты приема, остатка нет
Если НаДату < ДатаПриема Тогда
Возврат 0;
КонецЕсли;
// 2. Получаем количество дней положенного отпуска в год
ЗапросПрава = Новый Запрос;
ЗапросПрава.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| ПраваНаОтпускаСрезПоследних.КоличествоДнейВГод КАК КоличествоДнейВГод
|ИЗ
| РегистрСведений.ПраваНаОтпуска.СрезПоследних(&НаДату, Сотрудник = &Сотрудник И ВидОтпуска = &ВидОтпуска) КАК ПраваНаОтпускаСрезПоследних";
ЗапросПрава.УстановитьПараметр("НаДату", НаДату);
ЗапросПрава.УстановитьПараметр("Сотрудник", Сотрудник);
ЗапросПрава.УстановитьПараметр("ВидОтпуска", ВидОтпуска);
ВыборкаПрава = ЗапросПрава.Выполнить().Выбрать();
КоличествоДнейВГод = 28; // Значение по умолчанию по ТК РФ
Если ВыборкаПрава.Следующий() Тогда
КоличествоДнейВГод = ВыборкаПрава.КоличествоДнейВГод;
КонецЕсли;
// 3. Вычисляем исключаемые периоды (например, отпуска за свой счет свыше 14 дней в каждом рабочем году)
// Для простоты примера определим общее количество исключаемых дней в стаже
ИсключаемыхДнейСтажа = ПолучитьИсключаемыеДни(Сотрудник, ДатаПриема, НаДату);
// Вычисляем общую продолжительность работы в днях за вычетом исключаемых периодов
ОбщийСтажВДнях = (НаДату - ДатаПриема) / 86400 - ИсключаемыхДнейСтажа;
Если ОбщийСтажВДнях < 0 Тогда
ОбщийСтажВДнях = 0;
КонецЕсли;
// Рассчитываем количество полных отработанных месяцев (с учетом правила 15 дней)
// Среднее количество дней в месяце для расчета отпусков — 30.43 (или используем точный расчет по календарю)
ВсегоМесяцевРаботы = ОкруглитьСтажДоМесяцев(ОбщийСтажВДнях);
// 4. Считаем общее количество заработанных дней отпуска
ЗаработаноДнейВсего = ВсегоМесяцевРаботы * (КоличествоДнейВГод / 12);
// 5. Получаем количество фактически использованных дней отпуска
ЗапросИспользовано = Новый Запрос;
ЗапросИспользовано.Текст =
"ВЫБРАТЬ
| ЕСТЬNULL(СУММА(ИспользованныеОтпускаОбороты.КоличествоДнейОборот), 0) КАК ИспользованоДней
|ИЗ
| РегистрНакопления.ИспользованныеОтпуска.Обороты(, &НаДату, , Сотрудник = &Сотрудник И ВидОтпуска = &ВидОтпуска) КАК ИспользованныеОтпускаОбороты";
ЗапросИспользовано.УстановитьПараметр("НаДату", НаДату);
ЗапросИспользовано.УстановитьПараметр("Сотрудник", Сотрудник);
ЗапросИспользовано.УстановитьПараметр("ВидОтпуска", ВидОтпуска);
ВыборкаИспользовано = ЗапросИспользовано.Выполнить().Выбрать();
ИспользованоДней = 0;
Если ВыборкаИспользовано.Следующий() Тогда
ИспользованоДней = ВыборкаИспользовано.ИспользованоДней;
КонецЕсли;
// 6. Вычисляем итоговый остаток
ОстатокОтпуска = ЗаработаноДнейВсего - ИспользованоДней;
Возврат ОкруглитьДниОтпуска(ОстатокОтпуска);
КонецФункции
В приведенном кодовой структуре используются вспомогательные функции ПолучитьИсключаемыеДни(), ОкруглитьСтажДоМесяцев() и ОкруглитьДниОтпуска(). Они реализуют правила математического округления стажа и дней согласно внутренним регламентам организации и законодательству.
Динамический расчет «на лету» работает быстро для одного сотрудника. Однако, если вам потребуется сформировать отчет по остаткам отпусков для всей компании (например, для 1000 сотрудников на определенную дату), система столкнется со снижением производительности, выполняя сложные расчеты для каждого человека в цикле.
Для решения этой проблемы применим паттерн проектирования «Ленивый Кэш» (Lazy Cache):
КэшОстатковОтпусков с измерениями Сотрудник, ВидОтпуска, ДатаРасчета и ресурсом ОстатокДней.Как предотвратить неактуальность кэша при вводе документов «задним числом»?
Для этого в системе настраивается механизм инвалидации (сброса) кэша. Мы создадим специальную Подписку на события платформы:
ПриЗаписи для всех документов, которые могут повлиять на отпуска: «Прием на работу», «Кадровый перевод», «Отпуск», «Больничный лист», «Отпуск без сохранения оплаты».КэшОстатковОтпусков.Посмотрим на пример обработчика подписки на событие сброса кэша:
Процедура СброситьКэшОстатковОтпусковПриЗаписи(Источник, Отказ) Экспорт
// Проверяем, заполнено ли ключевое поле "Сотрудник" в источнике (документе)
Если ОбщегоНазначения.ЕстьРеквизитОбъекта(Источник, "Сотрудник") Тогда
Сотрудник = Источник.Сотрудник;
Если ЗначениеЗаполнено(Сотрудник) Тогда
// Удаляем записи кэша по данному сотруднику
НаборЗаписей = РегистрыСведений.КэшОстатковОтпусков.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Сотрудник.Установить(Сотрудник);
НаборЗаписей.Записать(); // Записываем пустой набор, стирая старые данные кэша
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Благодаря этой архитектуре система работает максимально производительно: расчеты выполняются мгновенно за счет обращения к кэшу, а при любом изменении кадровых данных прошлых или будущих периодов кэш автоматически сбрасывается, исключая любые расхождения данных.
Мы спроектировали надежный и производительный механизм кадрового учета остатков отпусков в самописной конфигурации. Давайте подведем итог ключевым правилам разработки этой подсистемы: