Отпуска, больничные и отгулы
Модуль отвечает на вопрос «кто и когда отсутствует» и денег не двигает: ни отпускных, ни вычетов (ТЗ «Отпуска, больничные и отгулы»). Сотрудник подаёт отсутствие, руководитель и генеральный директор решают, вся система видит, что человека нет.
Реализовано: этап 1 — модуль
internal/modules/absences, пакетinternal/workcalendar(календарь компании), миграция0175(company_calendarс праздниками РФ на 2026–2027,absences,absence_events,absence_settings); этап 2 — встраивание в табель, прогресс, напоминания, помощника, командировки, чаты и главную, миграция0176(вид сообщенияauto_reply); этап 3 — остатки, переработка, корректировки, Excel, помощник, загрузка людей, миграция0177(absence_adjustments,users.vacation_days_per_year).
Виды
| Вид | kind | Решение | Дни |
|---|---|---|---|
| Отпуск | vacation | Руководитель, затем генеральный директор | Календарные без праздников из календаря компании |
| Больничный | sick | Действует сразу; руководитель принимает к сведению | Календарные; конец может быть открыт (date_to = NULL) |
| Отгул за свой счёт | unpaid | Руководитель, затем генеральный директор | Рабочие |
| Отгул в счёт переработки | overtime_off | Руководитель, затем генеральный директор | Рабочие |
Половина дня (half = am | pm) — только у отсутствия в один день и не у больничного; стоит 0,5 дня. Утро и вторая половина одного дня не пересекаются. Число дней (days) — снимок на момент подачи или правки.
Статусы
pending ──руководитель──▶ pending_director ──генеральный директор──▶ approved ──дата начала──▶ active ──после окончания──▶ done
│ │ │
└──── rejected (с причиной) ◀┘ cancelled (до начала) ◀┘
больничный ──────────────────────────────────────────▶ approved / active сразу- Руководитель —
users.manager_id, иначе руководитель основного отдела. Нет его, он же генеральный директор или сам сотрудник — шаг пропускается. - Генеральный директор —
absence_settings.director_id, иначе первый активный владелец (OWNER). Его собственная заявка сразу «Согласовано»; заявка, поданная администратором, — тоже. - Руководитель, подавший заявку за подчинённого, проходит первый уровень сам. Больничный, заведённый руководителем, сразу «принят к сведению».
- Переходы по датам делает воркер раз в час (
approved → active → done); чтение «кто отсутствует сегодня» от воркера не зависит.
Правила
| Правило | Что происходит |
|---|---|
| Общие дни с другим отсутствием | 409 absences_overlap. Больничный поверх отпуска или отгула разрешён: тот заканчивается накануне больничного (или отменяется, если начинался позже), руководитель получает уведомление |
| Командировка в эти дни (не отменена и не завершена) | Отпуск и отгулы — 409 absences_trip_overlap; больничный — разрешён, организатор командировки получает уведомление |
| Начало в прошлом | Сотрудник: отпуск и отгулы — 400 absences_past_date, больничный — до 60 дней назад. Руководитель, генеральный директор, администратор, кадры — любая дата; прошедшее сразу «Завершено» |
Отпуск позже срока подачи (notice_days, 14) | Предупреждение late и пометка «поздняя заявка» у согласующих |
Отгулов за свой счёт больше порога за год (unpaid_warn_days, 5) | Предупреждение unpaid_many |
| Сроки задач сотрудника в эти дни | Предупреждение tasks_due со списком задач |
Все проверки и предупреждения показывает POST /absences/check до подачи; blocking — причина, по которой заявку не подать.
Правка (PATCH, то же тело, что и подача). На согласовании — свободно. Согласованное сотрудник только сокращает; сдвиг или удлинение возвращают заявку на согласование. У действующего меняется только дата окончания: раньше — свободно, позже — больничный продлевает кто угодно из имеющих доступ, отпуск и отгул — руководитель. Каждая правка — в истории (absence_events) с автором и прежними датами.
Отмена — до даты начала: сотрудник без причины, решающие — с причиной. После начала — «Закрыть раньше» (POST /absences/:id/close {date}). Начавшееся или прошедшее отсутствие не удаляется: администратор и кадры помечают его ошибочным (POST /absences/:id/void {note}) — статус «Отменено», дни возвращаются в остаток, в истории — voided.
Кто что видит
| Кто | Что |
|---|---|
| Сотрудник | Свои отсутствия целиком |
| Коллеги по отделам (основному и дополнительным) | Вид, даты, кто на связи (limited = true): комментарий, решения и история скрыты |
| Руководитель | Подчинённые (рекурсивно) и отделы, которыми руководит, — целиком; решает первый уровень, принимает больничный к сведению |
| Генеральный директор | Все; утверждает второй уровень, отзывает согласованное |
Чтение others_timesheets | «Все» — все сотрудники, «свой» — свои отделы |
Администратор, запись users_and_rights | Все; заводят за любого; календарь компании и настройки |
Метку «отсутствует до…» (GET /absences/today?user_ids=) видит каждый. Признаки для интерфейса в /org/me: absences_decide, absences_director, absences_manage.
Файлы отсутствия (скан больничного, entity_type = absence) — только сотрудник, его руководитель, генеральный директор и администратор; глобальное чтение их не открывает.
Календарь компании
company_calendar: особые дни — holiday (праздник), day_off (перенесённый выходной), workday (рабочая суббота). При запуске заполнен производственным календарём РФ на 2026–2027. Пакет internal/workcalendar — одна функция «рабочий ли день» и счёт рабочих и календарных дней; ведёт администратор на странице «Календарь и отсутствия» (GET/PUT/DELETE /calendar).
Влияние на систему (этап 2)
Отсутствие ничего не запрещает и денег не считает. Оно меняет то, что видно, кого не дёргать и какие дни рабочие.
| Где | Что |
|---|---|
| Рабочий день | workcalendar.Personal — календарь компании без полных дней отсутствия сотрудника. Им считаются серия дней в «Прогрессе» (отпуск и праздник серию не рвут), срок выплаты табеля, черновик часов помощника, пятничное напоминание руководителю (в последний рабочий день недели) |
| Напоминания о часах | В праздник компании не приходят никому; за день отсутствия — ни сотруднику, ни его руководителю (timeentries.ReminderCalendar) |
| Кольцо дня | GET /progress/me → away: в день отсутствия кольцо показывает вид отсутствия вместо «0 из 8 ч»; половина дня — норма 4 ч |
| Табель | Подсказки absences_in_period, метка в «Команде», дни отсутствия в «Моих часах» — см. Табели |
| Метки у имени | GET /absences/today?user_ids=&date= — один запрос на экран; фронт показывает «В отпуске до 15.10, на связи Петров» через общий AppUser (задачи, «Команда», команда проекта, сотрудники, чаты) |
| Задачи и часы | Срок задачи в отсутствие ответственного и запись часов в свой день отсутствия — предупреждения, не запрет |
| Командировки | Командировку на дни отпуска или отгула (не больничного) не создать: 409 trips_absence_overlap |
| Чаты | Статус собеседника в списке и шапке личного чата. Автоответ: на первое за день личное сообщение каждого собеседника отсутствующий «отвечает» текстом из заявки, сообщение вида auto_reply; в групповых чатах автоответа нет |
| Главная | В «Расписании» — группа «Отсутствия»; руководителю и генеральному директору — виджет «Кто отсутствует» (сегодня, неделя, заявки на решение) |
| Помощник | Утренняя сводка: «Сегодня отсутствуют: …» и число заявок на решение; в праздник сводки нет |
Остатки и переработка (этап 3)
GET /absences/balance?user_id=&year= — свои остатки каждому, чужие — тем, кто видит сотрудника подробно.
- Отпуск, дни. Остаток года = норма + перенос с прошлого года + корректировки года − использовано. Норма — личная (
users.vacation_days_per_year,PATCH /absences/users/:id/norm) или компании (absence_settings.vacation_days_per_year, 28). Использовано — согласованные, идущие и прошедшие отпуска по году начала; на согласовании — отдельно, в остаток не входят. Неиспользованное переносится целиком, минус тоже. Первый год расчёта — самый ранний из отпусков и корректировок. - Переработка, часы. С даты
overtime_since: в рабочий день — часы сверх 8 (в день половины отсутствия — сверх 4), в выходной, праздник и день отсутствия — все часы. Минус отгулы в счёт переработки: 8 ч за день, 4 ч за половину. Правка часов задним числом сразу меняет остаток. - Корректировки (
POST /absences/users/:id/adjustments, администратор и кадры):{balance: vacation|overtime, year, amount, reason}; стартовый остаток вводится так же с причиной «остаток на 01.01.2026». - Предупреждения в
POST /absences/check:over_balance(отпуск длиннее свободного остатка),over_overtime(не хватает переработки);vacation_left/overtime_left— свободный остаток до заявки. Согласовать можно, остаток уходит в минус.
Выгрузка GET /absences/export?year&department_id — «График отсутствий <год>.xlsx»: лист «Отсутствия» (кто согласовал — только по сотрудникам, видимым подробно) и лист «Остатки».
«Загрузка людей» на главной считает норму месяца каждого сотрудника: общая минус 8 ч за каждый рабочий день отсутствия (4 ч за половину) — поля norm_hours, absent_hours.
Помощник: инструменты absences (кто отсутствует в период), my_absences (свой остаток, переработка, отсутствия года) и propose_absence — предложение подать заявку: в чате появляется карточка с расчётом дней, согласующими и предупреждениями (POST /absences/check от имени сотрудника), подаёт сам сотрудник кнопкой «Подать».
Табель: в карточке черновика с отгулом за свой счёт в периоде — кнопка «Вычет за отгул»: подставляет причину и дни, сумму вводит руководитель. Быстрая отметка часов в день полного отсутствия не открывается. На карточках доски задач — значок у аватара отсутствующего ответственного.
Интерфейс: плитки «Остаток отпуска» и «Переработка» в «Табель → Отсутствия», кнопка «Выгрузить в Excel» решающим и кадрам, вкладка «Отсутствия» в карточке сотрудника (норма, остатки по годам, корректировки), карточка «Отпуск» в профиле.
Уведомления
| Тип | Кому |
|---|---|
absence.requested | Тому, чьё решение сейчас нужно |
absence.sick | Руководителю (больничный заведён, продлён, закрыт; отпуск укорочен); организатору пересекающейся командировки |
absence.decided | Сотруднику: согласовано, утверждено, отклонено, отозвано, оформлено за него |
absence.pending_reminder | Решающему: заявка без решения два рабочих дня, больничный не принят к сведению за рабочий день (раз в рабочий день после 9:00) |
absence.sick_open | Руководителю и сотруднику: открытый больничный дольше open_sick_reminder_days, затем раз в неделю |
absence.starts_tomorrow | Руководителю одним списком: в следующий рабочий день начинаются отсутствия подчинённых |
Живые обновления: любая запись в /absences и /calendar рассылает entity_changed с сущностью absence (кроме POST /absences/check).