---
name: HR / Payroll Default Accounts
description: Every default GL-account setting in the HRM module (salary, deductions, EOS, loans, overtime, leave accrual) — keys, AR/EN labels, definition file:line, where each is consumed when posting journal entries, and whether it is already on the first-run /setup Default-Accounts page. For the owner's task of extending /setup to cover HR.
updated: 2026-06-18
---

# HR / Payroll Default Accounts

## Scope

The HRM module posts journal entries in exactly **one** service:
`Modules/HRM/app/Services/PayrollAccountingService.php`. It handles two events:
**posting a payroll run** (`postPayroll`) and **paying it out** (`createPaymentEntry`).
No other HRM service calls `CreateJournalEntry` (verified by grepping
`Modules/HRM/app/Services` for `CreateJournalEntry`/`createJournalEntry` → only this file).

There are **10** HRM `*_account_id` settings. Only **5 of them are actually consumed**
in posting code today. The other 5 (loan receivable, EOS provision, EOS expense,
overtime expense, leave accrual) are **defined but NOT wired** — they are placeholders
for features that don't post yet (EOS/loan models carry a `journal_entry_id` column but
no service writes the configured account into a JE).

**None of the HR accounts are on the `/setup` page today** — the wizard only writes 7
core `accounting.default_*` keys (see "/setup page state" below).

All definitions live in **`Modules/Core/database/seeders/SettingDefinitionSeeder.php`**
(scope `company`, `value_type integer`, `default_value null`, `display_group hrm_accounting`).

---

## Default-account settings table

| # | Key (`hrm.` prefix) | Label EN | Label AR | Definition file:line | Consumed? | Where consumed |
|---|---------------------|----------|----------|----------------------|-----------|----------------|
| 1 | `hrm.salary_expense_account_id` | Salary Expense Account | حساب مصروف الرواتب | SettingDefinitionSeeder.php:1443 | **YES** | `PayrollAccountingService.php:24` (DR salary expense per-employee sub-account, line :160-170) |
| 2 | `hrm.salary_payable_account_id` | Salary Payable Account | حساب الرواتب المستحقة | SettingDefinitionSeeder.php:1459 | **YES** | `PayrollAccountingService.php:25` & `:63` (CR net payable on post :202-215; DR to clear on pay-out :78-89) |
| 3 | `hrm.social_insurance_account_id` | Social Insurance Account | حساب التأمينات الاجتماعية | SettingDefinitionSeeder.php:1475 | **YES** | `PayrollAccountingService.php:227` (CR deduction sub-account, keyword `'social insurance'`) |
| 4 | `hrm.medical_insurance_account_id` | Medical Insurance Account | حساب التأمين الطبي | SettingDefinitionSeeder.php:1491 | **YES** | `PayrollAccountingService.php:228` (CR deduction sub-account, keyword `'medical insurance'`) |
| 5 | `hrm.income_tax_account_id` | Income Tax Account | حساب ضريبة الدخل | SettingDefinitionSeeder.php:1507 | **YES** | `PayrollAccountingService.php:229` (CR deduction sub-account, keyword `'income tax'`) |
| 6 | `hrm.loan_receivable_account_id` | Loan Receivable Account | حساب سلف الموظفين | SettingDefinitionSeeder.php:1523 | **NO** | Not posted. Validated in `UpdateHrSettingsRequest.php:42` only. Loans deduct via payroll but no JE uses this account. |
| 7 | `hrm.eos_provision_account_id` | End of Service Provision Account | حساب مخصص نهاية الخدمة | SettingDefinitionSeeder.php:1539 | **NO** | Not posted. Validated `UpdateHrSettingsRequest.php:43`. `EosSettlement` has `journal_entry_id` but no service writes it. |
| 8 | `hrm.eos_expense_account_id` | End of Service Expense Account | حساب مصروف نهاية الخدمة | SettingDefinitionSeeder.php:1555 | **NO** | Not posted. Validated `UpdateHrSettingsRequest.php:44`. |
| 9 | `hrm.overtime_expense_account_id` | Overtime Expense Account | حساب مصروف الأوفرتايم | SettingDefinitionSeeder.php:1711 | **NO** | Not posted anywhere. Also **NOT** in `UpdateHrSettingsRequest` rules (cannot be saved via the HR settings API today). |
| 10 | `hrm.leave_accrual_account_id` | Leave Accrual Account | حساب استحقاق الإجازات | SettingDefinitionSeeder.php:1712 | **NO** | Not posted anywhere. Also **NOT** in `UpdateHrSettingsRequest` rules. |

> Note: #9 and #10 are defined on single packed lines in the "hrm_accounting (additional)" block
> (display_order 22/23). #1–#8 are the original block (display_order 14–21).

---

## How payroll posting consumes the accounts

`PayrollAccountingService::postPayroll()` (`:19-56`) — builds a payroll JE for a `Payroll`:

1. Reads `hrm.salary_expense_account_id` + `hrm.salary_payable_account_id` (`:24-25`).
   If either is missing it **throws** `RuntimeException('Missing payroll accounting settings…')`
   (`:27-29`) — so these two are the hard-required pair.
2. `buildJournalLines()` (`:128-219`) iterates payroll items per employee:
   - **DR** salary-expense **sub-account** of the configured parent, created on the fly via
     `EmployeeAccountService::getOrCreateSubAccount()` (`:160-170`). Stamped with the
     employee's department `cost_center_id` (MFG Phase-5, `:155-157`).
   - For each `deduction` detail line, matches the component name (lowercased) against the
     keyword map and **CR**s a per-employee sub-account of the matching parent (`:174-199`).
   - **CR** salary-payable sub-account for the remainder = earnings − allocated deductions
     (`:201-215`).
3. Sets payroll `status = Posted`, stores `journal_entry_id` (`:47-52`).

`getDeductionAccounts()` (`:224-231`) is the **keyword → account** map. It is **not** a
direct key lookup by component — the component's `component_name` must *contain* the keyword:

| Keyword matched in component name | Setting key read |
|-----------------------------------|------------------|
| `social insurance` | `hrm.social_insurance_account_id` |
| `medical insurance` | `hrm.medical_insurance_account_id` |
| `income tax` | `hrm.income_tax_account_id` |

If a deduction component's name doesn't contain one of these keywords, that deduction is
**not** split to its own account — it stays folded into the net salary-payable credit.
(Implication for the owner: these three accounts only "work" if the payroll deduction
components are named to contain the English keyword.)

`PayrollAccountingService::createPaymentEntry()` (`:58-121`) — the **pay-out** JE:
- **DR** each employee's salary-payable sub-account (clears the liability, `:74-89`).
- **CR** the **bank account** — note the bank account is **passed in as a parameter**
  (`$bankAccountId`, `:58`), **not** read from a setting. It comes from the pay-out request,
  so there is no `hrm.*_account_id` default for the disbursement bank.
- Sets payroll `status = Paid`, stores `payment_journal_entry_id` (`:112-117`).

---

## API surface

- **Read**: `GET` via `HrSettingController::index()`
  (`Modules/HRM/app/Http/Controllers/HrSettingController.php:37-49`) — returns *all* `hrm.*`
  definitions + current values (`getAllWithDefinitions('hrm', …)`).
- **Write**: `PUT`/bulk via `HrSettingController::update()` (`:57-77`) — keys sent without the
  `hrm.` prefix; each is whitelisted in
  `Modules/HRM/app/Http/Requests/UpdateHrSettingsRequest.php`.
  - The 8 account keys validated there: lines **37–44** (`salary_expense`, `salary_payable`,
    `social_insurance`, `medical_insurance`, `income_tax`, `loan_receivable`, `eos_provision`,
    `eos_expense`), each `nullable|integer|exists:accounts,id`.
  - ⚠️ **Gap:** `overtime_expense_account_id` and `leave_accrual_account_id` are **missing**
    from `UpdateHrSettingsRequest`, so the HR settings API silently drops them. To surface
    them on `/setup` the owner can either (a) write them through the generic core settings
    endpoint the wizard already uses, or (b) add them to `UpdateHrSettingsRequest`.

---

## /setup page state (current)

FE: `src/app/features/setup/setup-wizard.component.ts`.
The "Default Accounts" step auto-selects by COA code (`autoSelectDefaults()` :656-672) and
saves via `saveDefaults()` (:674-712). It writes **only these 7 core keys** — **zero HR keys**:

| setting_key written | auto-pick code |
|---------------------|----------------|
| `accounting.default_cash_account` | 1101 |
| `accounting.default_bank_account` | 1102 |
| `accounting.default_client_account` (receivable المدينون) | 1103 |
| `accounting.default_supplier_account` (payable الدائنون) | 2101 |
| `accounting.default_expense_account` | 5201/5101 |
| `accounting.default_revenue_account` | 41 |
| `accounting.default_check_account` (notes) | 1104 |

So **all 10 HR accounts are NOT on /setup** — they are the target of the owner's extension.

---

## Recommendation for extending /setup (HR cluster)

- **Prioritize the 5 wired accounts** (#1–#5) — these are the only ones that affect real
  postings today; #1/#2 are mandatory (posting throws without them).
- The other 5 can be shown as "future / not yet posted" so the owner sets reasonable defaults,
  but document that no JE consumes them yet.
- Reuse the existing core settings write path for `overtime`/`leave_accrual` (or extend
  `UpdateHrSettingsRequest`) since the HR settings API can't save those two today.
- Suggested COA defaults to auto-pick (parent-level, sub-accounts are auto-created per employee):
  salary expense → an HR/payroll expense parent (5xxx); salary payable → a payroll-liability
  parent (2xxx); social/medical/income-tax → liability/payable parents (2xxx).

---

## Key file references

| File | Purpose |
|------|---------|
| `Modules/Core/database/seeders/SettingDefinitionSeeder.php:1443-1569`, `:1711-1712` | All 10 HR account definitions |
| `Modules/HRM/app/Services/PayrollAccountingService.php` | The only HRM journal-posting service (post + pay-out) |
| `Modules/HRM/app/Http/Controllers/HrSettingController.php` | GET/PUT HR settings API |
| `Modules/HRM/app/Http/Requests/UpdateHrSettingsRequest.php:37-44` | Validation whitelist (8 of 10 account keys) |
| `Modules/HRM/app/Services/EmployeeAccountService.php` | `getOrCreateSubAccount()` — per-employee sub-accounts under each parent |
| `src/app/features/setup/setup-wizard.component.ts:656-712` | /setup Default-Accounts step (core keys only, no HR) |
