Табели
Табель — документ на выплату за любой отрезок дат (ТЗ «Табель, зарплата и передача выплат в финансы», п.6). Сотрудник только отмечает часы; руководитель в любой день формирует табель от нужной даты, система считает начисление по условиям оплаты, и табель уходит в финансы. Месячной сдачи больше нет.
Реализовано: модуль
internal/modules/timesheets, миграции0135(таблицаtimesheet_periods),0170(история правок часов),0171(табель любого периода, строки начисленияtimesheet_lines,time_entries.timesheet_id),0172(выплата по табелю в «Платежах», счёт для зарплаты, настройки выплат).
Схема
Сотрудник: отмечает часы (таймер задачи, «Добавить часы») получает выплату и уведомление
│ ▲
▼ │
Руководитель: «Сформировать табель» с любой даты ─▶ «Подтвердить и передать в финансы»
▲ │
│ вернуть с причиной ▼
Финансы: └───────── запланированная выплата в «Платежах» ─▶ «Провести»: табель закрытЧасы и табель
Запись часов входит ровно в один табель (time_entries.timesheet_id). Пока она ни в каком — «ждёт табеля». Новые часы за любую дату принимаются всегда: если на эту дату уже есть выплаченный табель, запись войдёт в следующий группой «часы за прошлые даты». Месячных блокировок нет — таймер задачи и «Добавить часы» не отказывают из-за табеля.
| Состояние записи | Сотрудник | Руководитель, администратор |
|---|---|---|
| Ждёт табеля | меняет и удаляет | меняет, удаляет, добавляет за сотрудника |
| В черновике или возвращённом табеле | только смотрит | меняет, исключает из табеля с причиной; табель пересчитывается |
| В финансах | только смотрит | сначала отзывает табель |
| Выплачена | корректировка | корректировка сотрудника |
В сутках 24 часа: сумма записей сотрудника за день не может быть больше 24 ч — ни при добавлении, ни при правке, ни у руководителя за сотрудника (400 timeentries_day_hours_exceeded с тем, сколько уже записано и сколько ещё можно). Забытый таймер задачи записывает не больше, чем осталось до 24 ч за день.
Исключённая запись возвращается сотруднику с пометкой «Нужно поправить» (time_entries.review_note); после его правки пометка снимается, и запись попадает в следующий табель.
Отсутствия в табеле
Отпуска, больничные и отгулы (см. «Отпуска, больничные и отгулы») денег не меняют: строки начисления, оклад и смены считаются как раньше, вычет за отгул — ручной строкой. Табель только подсказывает:
- предпросмотр и карточка табеля —
absences_in_period: отсутствия сотрудника в периоде; - «Команда» — у сотрудника, который сегодня отсутствует,
absenceи метка «В отпуске до …» вместо предупреждений; - «Мои часы» (
overview.absences) — дни отсутствия закрашены в сетке месяца, праздники календаря компании затенены; - срок «Выплатить до» считается в рабочих днях календаря компании (праздники не входят).
Работа вне проектов
С этапа 5 ТЗ часы и задачи бывают вне проектов — внутренняя работа отделов (миграция 0174).
- Задачу без проекта (
POST /tasksбезproject_id) ставит любой сотрудник. Управляет ею автор, работают исполнитель и соисполнители, отдел задачи — отдел исполнителя. Статус проекта такая задача не двигает. - Часы без проекта (
project_idне передан) записываются только по задаче без проекта (400 timeentries_task_required_without_project) и только её участниками (403 timeentries_not_task_participant). Таймер задачи пишет такие часы сам. - В табеле они идут строкой «Часы» без проекта по личной ставке из условий оплаты. В трудозатраты проектов не попадают.
Формирование
POST /timesheets/preview считает табель без сохранения, POST /timesheets сохраняет (черновик или сразу «в финансы», send=true).
- «С даты» по умолчанию — день после прошлого табеля сотрудника; табелей не было — самая ранняя дата его часов без табеля. «По дату» — выбирает руководитель.
- В табель входят все часы без табеля по «По дату» включительно, кроме исключённых; записи раньше «С даты» помечены
past(«часы за прошлые даты»). - Периоды неотменённых табелей сотрудника не пересекаются (
409 timesheets_overlapс датами другого табеля); строка сотрудника блокируется на время формирования. - Свой табель формирует только администратор (
409 timesheets_self_approval_forbidden). - Срок выплаты по умолчанию — 3 рабочих дня от сегодня.
Начисление
Строки timesheet_lines — снимок на момент расчёта; черновик и возвращённый пересчитываются при каждой правке часов, смене дат, ручной строке.
| Строка | Как считается |
|---|---|
salary | Оклад целиком за каждый день начисления (число месяца или день недели из условий оплаты), попавший в период; пропущенный после прошлого табеля догоняется; один день — один раз |
hours | Часы по проекту × ставка: сначала ставка участника на проекте, иначе ставка за час из условий оплаты на дату записи; условий оплаты на дату нет вовсе — ставка за час отдела сотрудника (rate_source = department); есть условия без ставки за час — сумма 0 (часы оплачены окладом) |
shift | Дни с часами × ставка за смену |
fee | Сумма за проект, когда проект сдан или закрыт; один раз |
adjustment, deduction, fee вручную | Доплата, вычет (отгул, больничный — до учёта отсутствий), гонорар частью: POST /timesheets/:id/lines |
К выплате = начислено (удержаний нет), не меньше нуля. Суммы видят сам сотрудник, администратор и доступ к заработку (earnings_margin); остальным API отдаёт часы без сумм (amounts_visible=false).
Пересчёт: изменили условия оплаты сотрудника (впервые задали свою ставку) или отдел (его ставку) — пересчитываются невыплаченные табели («черновик», «возвращён»); табели «в финансах» и «выплачен» остаются как были — часы, оплаченные по ставке отдела, так и остаются. Часы вне табеля считаются на лету по текущим ставкам.
Суммы сотруднику: настройка «Показывать сотрудникам суммы» (payroll_settings.show_amounts_to_employees, по умолчанию включена). Выключена — в своих табелях, «Моих часах», на главной и в своих условиях оплаты сотрудник видит только часы (amounts_visible = false); администратору и тем, кому открыт заработок, суммы видны всегда.
Статусы
| Статус | Что значит | Переходы |
|---|---|---|
draft | Черновик у руководителя | send → sent, cancel → cancelled |
sent | В финансах | return (финансы) или recall (руководитель) → returned, pay (финансы) → paid |
returned | Возвращён с причиной | send → sent, cancel → cancelled |
paid | Выплачен, часы закрыты навсегда | — |
cancelled | Расформирован, часы снова ждут табеля | — |
Возврат и отзыв требуют причину. Выплату отмечают и табель из финансов возвращают финансы: администратор, запись затрат «все», accounting.manage. Каждая смена статуса — в истории (timesheet_period_events).
Уведомления: timesheet.sent — сотруднику, timesheet.payout — финансовому директору и ассистенту, timesheet.returned — тому, кто сформировал, timesheet.paid — сотруднику и руководителю, timesheet.payout_overdue — финансам, когда выплата не проведена к сроку (один раз на платёж), timesheet.stale_hours — начальнику по пятницам, если у подчинённых есть часы без табеля старше порога из настроек выплат.
Выплата в финансах
Переданный табель сам встаёт в очередь финансов (этап 3 ТЗ, п.7):
sendсоздаёт в «Платежах» запланированную выплату: исходящий платёж сотруднику (counterparty_type=employee) на сумму «к выплате», дата — «Выплатить до», назначение «Зарплата: Имя, 16.09–30.09.2026», связьpayments.timesheet_id. Табель с нулевой суммой уходит без платежа.- Счёт — «Счёт для зарплаты» (
payment_accounts.is_payroll_default, не больше одного), иначе первый активный расчётный счёт. Активных счетов нет — табель не уходит (409 payments_no_payroll_account). - Проведение платежа — в «Платежах» (
PATCH /payments/:id/status, массовоPOST /payments/complete) или кнопкой «Отметить выплату» в карточке (POST /timesheets/:id/pay {account_id?}) — закрывает табель (paid). Счёт можно сменить при проведении. returnиrecallсначала отменяют запланированную выплату, потом возвращают табель. В «Платежах» выплату по табелю не отменить (409 payments_payroll_cancel_forbidden): только возвратом табеля с причиной.- Карточка табеля (
GET /timesheets/:id) показывает выплату:payment {id, status, date, account_id, account_name, amount}. - С этапа 5 передача создаёт и задачу без проекта «Выплатить зарплату: Имя, сумма» финансовой службе: исполнитель — ассистент финдиректора (нет — финдиректор), остальные финансы — соисполнители, срок — «Выплатить до» (
payments.task_id). Проведение платежа закрывает задачу, возврат табеля — отменяет.
«Выплатить до» по умолчанию — через pay_due_workdays рабочих дней от формирования, порог напоминания — reminder_days; оба в настройках выплат (GET/PUT /payroll/settings, по умолчанию 3 и 14). Прежние месячные ведомости (/payroll/runs) остаются только для чтения.
Права
| Кто | «Команда», формирование, решения | Суммы |
|---|---|---|
| Администратор | все сотрудники, включая себя | да |
Запись earnings_margin (владелец, OPS, финдиректор) | все, кроме себя | да |
| Начальник по оргструктуре | подчинённые (рекурсивно) | нет |
others_timesheets запись | «все» или свой дивизион | нет |
Финансы, чтение заработка, чтение others_timesheets | видят табели в охвате, не формируют | по заработку |
Признаки для интерфейса в /org/me: timesheets_team, timesheets_form (= timesheets_approve), timesheets_self_approve, timesheets_amounts, timesheets_pay.
API
| Метод | Путь | Описание |
|---|---|---|
GET | /api/v1/timesheets/my/overview?from&to | «Мои часы»: лента, сегодня и неделя, невыплаченные часы и сумма, последняя выплата, свои табели |
GET | /api/v1/timesheets/team | «Команда»: часы без табеля, с какой даты, «нужно поправить», последний табель, сводка (в том числе chain_days — «скорость цепочки»: в среднем дней от часа до выплаты по табелям этого месяца) |
GET | /api/v1/timesheets | Табели в охвате (status, user_id, пагинация) |
POST | /api/v1/timesheets/preview | Расчёт без сохранения |
POST | /api/v1/timesheets | Сформировать |
GET, PATCH | /api/v1/timesheets/:id | Табель; изменить даты и срок выплаты |
POST | /api/v1/timesheets/:id/{recalculate,send,recall,return,pay,cancel} | Пересчёт и переходы |
POST, DELETE | /api/v1/timesheets/:id/lines[/:line_id] | Ручные строки |
POST | /api/v1/timesheets/:id/entries/:entry_id/exclude | Исключить запись с причиной |
GET | /api/v1/payments?kind=payroll | Выплаты по табелям (фильтр «Зарплата» в «Платежах») |
POST | /api/v1/payments/complete | Провести несколько выплат одним подтверждением |
GET, PUT | /api/v1/payroll/settings | Срок выплаты и порог напоминания |
Правка и удаление часов
С этапа 1 ТЗ «Табель, зарплата и передача выплат в финансы» (docs/specs/timesheet-payroll-spec.md, п.5) запись часов можно изменить или удалить. Кто и что может:
| Кто | Когда можно | Ограничения |
|---|---|---|
| Сам сотрудник | Своя запись, ещё не вошедшая в табель | Проект не закрыт (409 timeentries_project_closed), новая дата — в сроках проекта, при смене проекта — участие в нём. Строка из корректировки не правится (409 timeentries_entry_from_correction) |
Управляющий часами сотрудника: администратор, начальник по оргструктуре (users.manager_id, рекурсивно), проверяющий по матрице others_timesheets (запись, в своём охвате) | Любая запись без табеля или в черновике и возвращённом табеле (табель пересчитывается), в том числе по закрытому проекту и вне его сроков | Табель в финансах — сначала отозвать (409 timeentries_entry_timesheet_sent), выплаченные часы — только корректировкой (409 timeentries_entry_paid) |
Особые права управляющего действуют только на чужие часы: свои записи и администратор, и начальник правят по правилам сотрудника.
Тот же управляющий записывает часы сотруднику через POST /time-entries без прежнего права time_entries.manage и без ограничений по срокам и закрытию проекта. Сам сотрудник по закрытому проекту часы не вносит.
Каждая правка пишется в историю time_entry_events (миграция 0170): кто, действие created / updated / deleted, снимки «было» и «стало», признак правки по закрытому проекту. История переживает удаление записи. Запись, добавленную или изменённую не самим сотрудником, он получает уведомлением timesheet.entry_changed («Иванов изменил ваши часы за 12.10: 8 ч → 6 ч»), а в строке табеля — поля edited_by, edited_by_name; ещё строка несёт project_closed.
Трудозатраты пересчитываются сразу. Правка по закрытому проекту меняет только его статьи «Трудозатраты» (finance.SyncLaborCostsIncludingClosed), остальные финансы закрытого проекта не трогаются. Удалённая или перенесённая в другой проект запись забирает свою статью трудозатрат.
| Метод | Путь | Доступ | Описание |
|---|---|---|---|
PATCH | /api/v1/time-entries/:id | владелец или управляющий | {project_id?, task_id?, date?, hours?, comment?}; отсутствующее поле не меняется, task_id/comment со значением null снимаются |
DELETE | /api/v1/time-entries/:id | владелец или управляющий | 204 |
GET | /api/v1/time-entries/:id/history | владелец или управляющий | {items: [...]}, старые события первыми, с названиями проекта и задачи |
Корректировки
Корректировка нужна только для часов, которые уже в финансах или выплачены: остальные правятся напрямую (409 timesheets_correction_not_needed). Сотрудник подаёт её за день или период (entry_date…date_to, например неделя) с причиной; решает тот же круг, что формирует табели. Учтённая становится записью времени без табеля (correction_id) и войдёт в следующий табель. Убрать больше, чем было по проекту в табеле, нельзя.
| Метод | Путь | Описание |
|---|---|---|
GET, POST | /api/v1/timesheets/my/corrections | Свои корректировки; подать |
DELETE | /api/v1/timesheet-corrections/:id | Отозвать свою, пока нет решения |
POST | /api/v1/timesheet-corrections/:id/review | {approve, note} |