---
title: "HR"
description: "Employees and leave. A register of employees, departments and leave requests, with strict data classes: pay, bank details and national id are visible to managers only and health or union data only to a special reader group."
canonical: "https://obop.dev/apps/hr/"
lang: "en"
---

# HR

## What it is for

A register of employees, departments and leave requests, with strict data classes: pay, bank details and national id are visible to managers only and health or union data only to a special reader group. Agents never see the sensitive classes.

## What the package does and does not do

Employees, departments and leave requests. Personal data is classified strictly: pay, bank and national id are visible to managers only, and health and union data to the special reader group.

## Models

### `hr_department`

A team or department of the company

Read group: `hr.user`. Write group: `hr.manager`.

| Field | Kind | Data class | Personal data | Group | Agents see | Description |
|---|---|---|---|---|---|---|
| `name` | text | `none` | no | `hr.user` | in clear | Department name |
| `head` | text | `public_directory` | yes | `hr.user` | masked | Name of the department head |
| `cost_centre` | text | `none` | no | `hr.user` | in clear | Cost centre code used by finance |

### `hr_employee`

A person employed by the company

Read group: `hr.user`. Write group: `hr.manager`.

| Field | Kind | Data class | Personal data | Group | Agents see | Description |
|---|---|---|---|---|---|---|
| `name` | text | `public_directory` | yes | `hr.user` | masked | Full name |
| `job_title` | text | `public_directory` | yes | `hr.user` | masked | Job title |
| `work_email` | text | `public_directory` | yes | `hr.user` | masked | Work email address |
| `work_phone` | text | `public_directory` | yes | `hr.user` | masked | Work phone number |
| `department_id` | ref → hr_department | `none` | no | `hr.user` | in clear | The department the person belongs to |
| `reports_to_id` | ref → hr_employee | `none` | no | `hr.user` | in clear | The person's line manager |
| `hired_on` | date | `internal` | yes | `hr.user` | masked | First day of employment (YYYY-MM-DD) |
| `contract_type` | select (permanent, fixed_term, contractor, intern) | `confidential_hr` | yes | `hr.manager` | withheld | Type of employment contract: permanent, fixed_term, contractor, intern |
| `private_email` | text | `confidential_hr` | yes | `hr.manager` | withheld | Private email address |
| `private_phone` | text | `confidential_hr` | yes | `hr.manager` | withheld | Private phone number |
| `home_address` | longtext | `confidential_hr` | yes | `hr.manager` | withheld | Private home address |
| `birth_date` | date | `confidential_hr` | yes | `hr.manager` | withheld | Date of birth (YYYY-MM-DD) |
| `national_id` | text | `confidential_hr` | yes | `hr.manager` | withheld | National identity or social security number |
| `emergency_contact` | text | `confidential_hr` | yes | `hr.manager` | withheld | Name and phone of the person to call in an emergency |
| `salary` | money | `financial` | yes | `hr.manager` | withheld | Gross annual salary (minor currency units, e.g. cents) |
| `bank_iban` | text | `financial` | yes | `hr.manager` | withheld | Bank account salary is paid into |
| `union_member` | bool | `special_category` | yes | `hr.special_reader` | withheld | Whether the person is a trade union member |
| `work_permit_expiry` | date | `special_category` | yes | `hr.special_reader` | withheld | Date the work permit expires, for non-citizens (YYYY-MM-DD) |
| `health_note` | longtext | `special_category` | yes | `hr.special_reader` | withheld | Health condition or disability relevant to the workplace |
| `state` | select (onboarding, active, offboarded) | `none` | no | `hr.user` | in clear | Employment status: onboarding, active, offboarded |

### `hr_leave_request`

A request for time off

Read group: `hr.user`. Write group: `hr.user`.

| Field | Kind | Data class | Personal data | Group | Agents see | Description |
|---|---|---|---|---|---|---|
| `employee_id` | ref → hr_employee | `none` | no | `hr.user` | in clear | The employee asking for time off |
| `leave_kind` | select (paid, unpaid, parental, study, other) | `confidential_hr` | yes | `hr.user` | withheld | Kind of leave: paid, unpaid, parental, study, other |
| `start_on` | date | `internal` | yes | `hr.user` | masked | First day of absence (YYYY-MM-DD) |
| `end_on` | date | `internal` | yes | `hr.user` | masked | Last day of absence (YYYY-MM-DD) |
| `days` | number | `none` | no | `hr.user` | in clear | Working days requested |
| `reason` | longtext | `confidential_hr` | yes | `hr.manager` | withheld | Why the leave is requested |
| `is_sick_leave` | bool | `special_category` | yes | `hr.special_reader` | withheld | Whether the absence is due to illness |
| `medical_note` | longtext | `special_category` | yes | `hr.special_reader` | withheld | Reference to the medical certificate, if any |
| `state` | select (draft, requested, approved, refused) | `none` | no | `hr.user` | in clear | Request status: draft, requested, approved, refused |

## Workflows

### `hr_employee`

| Action | Moves | Who may run it | Approval |
|---|---|---|---|
| `hr_employee.activate` | state: onboarding → active | `hr.manager` | no |
| `hr_employee.offboard` | state: active → offboarded | `hr.manager` | yes |

### `hr_leave_request`

| Action | Moves | Who may run it | Approval |
|---|---|---|---|
| `hr_leave_request.request` | state: draft → requested | `hr.user` | no |
| `hr_leave_request.approve` | state: requested → approved | `hr.manager` | yes |
| `hr_leave_request.refuse` | state: requested → refused | `hr.manager` | yes |

## Permission groups

Groups used by this app:

- `hr.manager`
- `hr.special_reader`
- `hr.user`

## Approval steps

Steps an agent can only propose:

- `hr_employee.offboard`
- `hr_leave_request.approve`
- `hr_leave_request.refuse`

## What agents can and cannot do

An agent can read, search, create and update records of this app, and run the actions that need no approval, within the permission groups of its token.

It can only propose these steps, and a person must approve them: `hr_employee.offboard`, `hr_leave_request.approve`, `hr_leave_request.refuse`, `hr_department.delete`, `hr_employee.delete`, `hr_leave_request.delete`.

Personal data is masked as placeholder tokens: `hr_department.head`, `hr_employee.name`, `hr_employee.job_title`, `hr_employee.work_email`, `hr_employee.work_phone`, `hr_employee.hired_on`, `hr_leave_request.start_on`, `hr_leave_request.end_on`.

These fields are withheld from agents entirely: `hr_employee.contract_type`, `hr_employee.private_email`, `hr_employee.private_phone`, `hr_employee.home_address`, `hr_employee.birth_date`, `hr_employee.national_id`, `hr_employee.emergency_contact`, `hr_employee.salary`, `hr_employee.bank_iban`, `hr_employee.union_member`, `hr_employee.work_permit_expiry`, `hr_employee.health_note`, `hr_leave_request.leave_kind`, `hr_leave_request.reason`, `hr_leave_request.is_sick_leave`, `hr_leave_request.medical_note`.

## Dependencies

None.

## Install

Install it from the Customize app (Marketplace), or from the command line:

```bash
obop modules install-from hr
```

Needs a database and administrator rights.

## Build your own

[The module guide shows how apps like this one are written](/docs/reference/module-manifest/)
