Administration & Controls
Everything an administrator tunes: the mobility policy, the twelve settings, the permission matrix, salary and profile privacy, the daily automation, and the demo seeder — with the exact behavior each control produces.
Permissions (RBAC)
The module owns 10 permission keys, all in the Human Resource: Mobility category in the Roles editor. They appear only while the module is enabled and require no edit to core Roles. Keys are frozen once shipped, so existing grants keep resolving across upgrades.
| Permission key | Label | Grants |
|---|---|---|
movements | Employee Movements | The base key — opens the movement audit table and the HR "Employee Movement" profile tab. |
movements_create | Record Manual Movements | The "Record movement" form. |
movements_update | Correct / Reverse Movements | Recording a reversal (correction) row. |
movements_delete | Delete Movement Records | Deleting a movement (last resort — prefer a reversal). |
movements_view_salary | View Salary Columns on Movements | Reveals/sets salary everywhere: movement columns, opening ranges, and selection salary. |
internal_hiring | Manage Internal Hiring | The Internal Hiring workspace (openings, pipeline, overrides). |
internal_hiring_publish | Publish Internal Openings | Moving an opening to Published. |
internal_hiring_select | Select Internal Candidates | The one-transaction candidate selection. |
mobility_override | Decide Mobility Policy Overrides | Approving/rejecting exception requests. |
movements_settings | Manage Mobility Settings | The Mobility Settings page. |
Suggested role presets
All Mobility keys including movements_view_salary, internal_hiring_publish, internal_hiring_select, mobility_override, and movements_settings.
movements+create and internal_hiring — usually without salary and without publish/select/settings.
Nothing extra — the job portal and "My…" profile tabs work for all active staff by design.
Settings
All twelve settings are tenant-scoped and live under HR Operations → Mobility Settings (also
surfaced as a Mobility Settings tab inside core Settings). They need the
movements_settings permission. Each is sanitized on save (integers are floored at 0,
booleans normalized, and salary visibility constrained to its two allowed values), and saving writes
an audit-log entry.
| Setting | Type | Default | Effect |
|---|---|---|---|
movements_max_actions | Integer | 3 | How many mobility actions an employee may take inside the rolling window. |
movements_window_months | Integer | 6 | The length of the rolling window in months. |
movements_count_withdrawn | On / Off | On | Whether a withdrawn application still counts against the limit. |
movements_withdrawal_grace_hours | Integer | 0 | Withdrawing within this many hours of submitting releases the count (0 disables the grace rule). |
movements_min_tenure_months | Integer | 0 | Minimum tenure before applying (0 = no minimum; unknown hire dates are not blocked). |
movements_block_disciplinary | On / Off | On | Block employees with an active disciplinary case (reads the Compliance module when present). |
movements_allow_override | On / Off | On | Whether blocked employees may request an audited policy exception. |
movements_auto_post_announcement | On / Off | On | Whether publishing an opening (with its own flag ticked) posts to Announcements. |
movements_auto_post_timeline | On / Off | Off | Whether publishing an opening (with its own flag ticked) posts to the Timeline. |
movements_auto_capture | On / Off | On | Whether the daily job captures Employee Master changes as movements. |
movements_self_movement_tab | On / Off | On | Whether "My Movement History" shows on employee profiles. |
movements_salary_visibility | Choice | HR only | Who sees salary changes: HR only (permission holders) or the employee and HR. |
The mobility policy — how the rules are evaluated
The policy engine is the fairness core of the job portal. The server is the sole authority: it evaluates the policy for the informational eligibility panel, again when the employee submits, and once more when HR selects a candidate. The rules split into two kinds:
1. The opening must be accepting applications — published, within its open/close dates,
and with a free slot.
2. One active application per opening — you can't apply twice to the same opening.
3. Rolling usage limit — fewer than max_actions counted applications in the
window.
4. Minimum tenure — met, when set and the hire date is known.
5. No active disciplinary case — when disciplinary blocking is on.
How usage is counted
- An application is the creation event — there are no drafts. A single application counts once.
- The window is a rolling period computed from the tenant-local clock and matched against each application's submit time.
- By default a withdrawn application still counts. If you set a grace window and the employee withdraws within it, the count is released (treating it as an erroneous submission).
- Minimum tenure fails open: if an employee's hire date is missing or unusable, the tenure rule is skipped rather than blocking them.
Salary & profile privacy
Two independent controls govern who sees pay and personal history:
movements_view_salary (permission)
The hard gate. Without it, salary columns read Hidden, HR cannot set salary on a manual
movement or a selection, and opening salary ranges are not written.
movements_salary_visibility (setting)
Governs the employee's own view. HR only hides salary figures on the self-service
"My Movement History" tab; The employee and HR lets the employee see their own salary
changes there.
movements_self_movement_tab (setting)
Turns the "My Movement History" self-service tab on or off entirely.
Publication automation
Publishing an opening can announce it — but only when two switches agree: the tenant-wide setting and the per-opening flag.
| Channel | Tenant setting | Per-opening flag | Result on publish |
|---|---|---|---|
| Announcements | movements_auto_post_announcement | "Post to Announcements" | An audience-scoped announcement with a link back to the portal is created (needs the Announcements feature present). |
| Timeline | movements_auto_post_timeline | "Post to employee Timeline" | A timeline post is created, public or department-scoped to match the opening's audience (needs the Timeline module present). |
If the target feature isn't installed, publishing still succeeds — the post is simply skipped.
Cron jobs
Two daily jobs run on ERPat's Advanced Cron runtime. Both self-gate on module_movements
and skip cleanly if the module's tables aren't migrated on the current database.
| Job (slug) | Schedule | Behavior |
|---|---|---|
movements_capture_employee_movements |
Daily 02:2020 2 * * * |
For each employee, hashes department + position + salary and compares it to the last
snapshot. A change is classified (transfer / role change / salary adjustment) and recorded
as an Auto Capture movement; the snapshot is updated. Also respects
movements_auto_capture. First run baselines only. |
movements_close_expired_openings |
Daily 01:1010 1 * * * |
Moves published openings past their closing date to Closed. |
php erpat cron:list # see both jobs
php erpat cron:run movements_capture_employee_movements
php erpat cron:run movements_close_expired_openingsProfile tabs
The module injects three tabs into the team-member profile page:
| Tab | Audience | Gate |
|---|---|---|
| Employee Movement | HR viewing anyone's profile | movements permission. |
| My Movement History | The employee, own profile only | movements_self_movement_tab setting (salary follows movements_salary_visibility). |
| My Internal Applications | The employee, own profile only | Self-service — no permission. |
Audit trail
Every state change in the module writes a row to ERPat's system activity log, under the Human Resource and Component Settings categories. Warnings are used for the higher-impact events (reversals, deletes, status changes, candidate selection, override decisions, settings changes). This means the module is fully auditable — you can always answer "who changed this, when, and from what." The complete list of logged events is in the Data & Screen Reference.
Demo seeder
The MovementsDemo seeder loads an explorable slice of every surface and is idempotent,
connection-agnostic, schema-drift safe, and reversible.
php erpat db:seed MovementsDemo # main database
php erpat db:seed MovementsDemo --tenant=<company_key> # one tenant
php erpat db:seed MovementsDemo --all # main + every active tenant
php erpat db:seed MovementsDemo --dry-run # preview counts, write nothing
php erpat db:seed MovementsDemo --remove # soft-delete the demo rows- It attaches sample rows to the first active staff users already on the database and creates nothing if none exist.
- It seeds 2 openings (1 published, 1 draft), 1 application with 2 stage rows, 2 movements, and 1 pending override.
- Remove is a soft delete (sets
deleted = 1), matched on stable demo markers (opening codes, references, idempotency keys) — it never hard-deletes and never touches real records. - You can also drive it from the Bulk Database Seeder modal in the Tenants console.