Payroll / Timesheet — Core concepts
| Concept | Meaning |
|---|---|
| Payroll settings | Per-BU configuration in Finance Settings; stores the paid-hours basis in BusinessUnitSetting.DefaultPaidHoursOptionId. |
| Paid hours basis | The hours basis for payroll: Rostered Hours (scheduled shift hours, value 0) or Actual Hours (actual clocked hours, value 1). |
| OrgEmployee | Active staff–site link; Shift.OrgEmployeeId references this link. |
| Shift | Work shift; source of the shift row on the Timesheet, carries TimesheetStatusId and Exported. |
| EmployeeAbsence | Leave; a Date + TimeStart/TimeEnd; source of a leave-only row when approved and not overlapping a shift. |
| Shift row | Timesheet row from a Shift, with all shift-hour columns. |
| Leave-only row | Timesheet row from an approved EmployeeAbsence on a day with no shift; carries its own timesheet status. |
| Timesheet state | Status pair TimesheetStatusId + Exported flag, existing on both Shift and EmployeeAbsence with the same shape. |
| TrackingCode | Classification code (TypeId = Leave / Shift); source of the Tracking Code column in export. |
| Leave overlap hours | Hours of intersection between a leave interval and a shift interval; value of the Leave Hours column in export. |
| ReportCode | Fixed jsReport template name for Timesheet CSV export. |
| jsReport Reporting Server | The report-rendering server; the sole source that builds the CSV export file. |
Payroll settings — paid hours basis per BU
- Stored on
BusinessUnitSettingviaDefaultPaidHoursOptionId; only writable through route/api/BusinessUnitSetting/PayrollwithSettingFinancepermission. - Fixed label ↔ backend value mapping:
Rostered Hours=0,Actual Hours=1. - BU has no
BusinessUnitSettingrow → load does not error, defaults toRostered Hours(0). - A value other than
0/1on save → rejected or normalized per the validation contract. - The tab only shows when the BU enables
Staff Roster(STAFF_ROSTERpermission); the route is protected by the same rule.
Real-world example: Admin picks Actual Hours and saves. → Store the value 1, not the label. → DefaultPaidHoursOptionId = 1 is written to BusinessUnitSetting; reload still shows Actual Hours.
OrgEmployee link — auto-fixed when creating shifts
- Creating a new shift (clock-in) or next shift (clock-out) → ensure an active
OrgEmployeeexists for the shift'sEmployeeId+OrgId. - Missing → the backend creates one and assigns
Shift.OrgEmployeeId; exists → reused, no duplicate created. - Employee/org not active in the BU → entire request rejected, no
OrgEmployeecreated, no shift created. - Frontend Staff Attendance does not need to create
OrgEmployee; signature, tracking code, clock event, timesheet status, and shift hours stay unchanged.
Real-world example: Clock-out creates the next shift; the source shift has EmployeeId/OrgId but OrgEmployeeId is inactive. → Do not let the shift save with a broken link. → The backend resolves/creates an active OrgEmployee and assigns it to the new shift.
Timesheet state — status on each row
ShiftandEmployeeAbsenceshare the same status shape:TimesheetStatusId+Exported, both optional.- An absence created before this capability → behaves as a draft, not-yet-exported row.
- Timesheet status is independent of the leave approval lifecycle: approving/un-approving a row does not change the leave's status/approver/approve time, and vice versa.
- Leave not approved in its lifecycle (pending/rejected) → never appears as a timesheet row regardless of timesheet state.
Real-world example: Admin approves a leave-only row on the Timesheet. → Must not drag in leave approval. → EmployeeAbsence.TimesheetStatusId changes to approved; the leave's status/approver/approve time stay unchanged.
Leave-only row — leave on a day without a shift
- Only created when an approved
EmployeeAbsencefalls on a day without a rostered shift; leave overlapping a shift → no separate row, it sits in the shift row's leave column. - Row key is stable, deterministically derived from the source absence id; does not touch shift row keys; identical on every load of the same date range.
- The server resolves the target record from row type + absence id field, never guesses the type from the row key.
- Filtered by status tab like shift rows; an absence without a timesheet status defaults to draft.
- Actions: approve/un-approve (does not write paid-time/duration), export/un-export; once exported → selection locked.
Real-world example: Reload the same leave-only row with a narrower date range. → The row key must match last time so selection does not drift. → The key derived from the absence id is fixed, independent of result order/length.
Export mode — separated vs grouped
separated(default): one CSV record per selected timesheet row; keeps the columns in the old export order.grouped: one CSV record per staff (by identity, not display name), aggregating the total hours of the selected rows; drops shift-level columns.- Both modes render from the same re-queried dataset → report the same hours for the same selection.
- No mode supplied → treated as
separated.
Real-world example: Same 3 shifts of one staff: To Pay Hours = 4.00, 3.50, 2.00. → The two files must match. → grouped reports 9.50 = the sum of the 3 values in separated.