Hello, and welcome to the community.
Your question is well-detailed and structured in a way that makes it the perfect prompt. It’s like a dream for AI agents.
ERPNext v15 does not come with a standard HRMS module. It lives in a new, separate app: Frappe HRMS.
ERPNext HRMS v15 - Leave Ledger Entry Internal Deep Dive
Deep technical walkthrough of how Leave Ledger Entry drives allocation, application, encashment, expiry, and scheduler-based earned leave accrual.
- Bench versions used:
Frappe 15.96.0, ERPNext 15.94.3, HRMS 15.58.1
- Primary ledger creator:
hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.py:create_leave_ledger_entry()
- Scheduler hook:
hrms/hooks.py:scheduler_events['daily_long']
1) Leave Ledger Entry - Core Concept
Leave Ledger Entry is the transaction journal for leave movement. Every leave-changing business event eventually writes here as one or more signed entries.
- Positive
leaves: allocation or corrective reversal.
- Negative
leaves: leave consumed, encashed, or expired.
- Corrective entries can appear for backdated actions if expiry entries already exist.
Core identity fields are transaction_type and transaction_name, linking each row to the source document (Leave Allocation, Leave Application, Leave Encashment).
Refs:
apps/hrms/hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.py:64
apps/hrms/hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.json:57
apps/hrms/hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.json:65
Is it the single source of truth?
- For leave movement and computed balances: effectively yes.
- For policy/rule metadata: no. Rules still come from
Leave Type, Leave Policy, Leave Policy Assignment, allocation periods.
Balance readers:
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:974
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:1021
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:1196
2) Where Ledger Entries Are Triggered
| Source |
Trigger |
Ledger effect |
Leave Allocation |
on_submit() |
Credit rows for new allocation and optional carry-forward component |
Leave Allocation |
on_update_after_submit() (when new_leaves_allocated changes) |
Delta row (positive/negative adjustment) |
Leave Allocation |
allocate_leaves_manually() |
Additional credit row from chosen date |
Leave Application |
on_submit() (approved) |
Debit row(s), split when needed |
Leave Encashment |
on_submit() |
Debit row for encashed days, optional corrective credit |
| Earned leave scheduler |
allocate_earned_leaves() |
Additional periodic credit row |
| Expiry scheduler |
process_expired_allocation() |
Expiry debit row (is_expired = 1) |
| Cancellation |
create_leave_ledger_entry(..., submit=False) |
Deletes rows for the transaction |
Refs:
apps/hrms/hrms/hr/doctype/leave_allocation/leave_allocation.py:79
apps/hrms/hrms/hr/doctype/leave_allocation/leave_allocation.py:95
apps/hrms/hrms/hr/doctype/leave_allocation/leave_allocation.py:320
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:101
apps/hrms/hrms/hr/doctype/leave_encashment/leave_encashment.py:45
apps/hrms/hrms/hr/utils.py:348
apps/hrms/hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.py:130
apps/hrms/hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.py:86
3) Leave Allocation → Ledger Internal Flow
When allocation is submitted, ledger posting is called from LeaveAllocation.on_submit().
- If
unused_leaves > 0, it posts carry-forward credit (is_carry_forward = 1).
- Then it posts new allocation credit (
is_carry_forward = 0).
- If a previous allocation exists and carry-forward is enabled, previous unused can be expired (debit expiry row).
Refs:
apps/hrms/hrms/hr/doctype/leave_allocation/leave_allocation.py:295
apps/hrms/hrms/hr/doctype/leave_allocation/leave_allocation.py:311
apps/hrms/hrms/hr/doctype/leave_allocation/leave_allocation.py:82
On update after submit:
leaves_to_be_added = new_leaves_allocated - existing_posted_allocation
- Only delta is posted.
Refs:
apps/hrms/hrms/hr/doctype/leave_allocation/leave_allocation.py:96
apps/hrms/hrms/hr/doctype/leave_allocation/leave_allocation.py:105
apps/hrms/hrms/hr/doctype/leave_allocation/leave_allocation.py:116
On cancel:
- Deletes ledger rows for that allocation.
- Cancellation blocked if linked leave applications exist.
Refs:
apps/hrms/hrms/hr/doctype/leave_allocation/leave_allocation.py:87
apps/hrms/hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.py:87
apps/hrms/hrms/hr/doctype/leave_ledger_entry/leave_ledger_entry.py:36
flowchart TD
A[Leave Allocation Submit] --> B[LeaveAllocation.create_leave_ledger_entry]
B --> C{unused_leaves > 0?}
C -->|Yes| D[Post +CF credit row]
C -->|No| E[Skip CF row]
D --> F[Post +New Allocation credit row]
E --> F
F --> G{carry_forward and previous allocation?}
G -->|Yes| H[Expire previous balance: post debit expiry row]
G -->|No| I[Done]
4) Leave Application → Ledger Internal Flow
Leave Application.on_submit() writes to ledger only when approved.
- Default: single debit row (
leaves = -total_leave_days).
- If application spans allocation boundaries (negative allowed case), it can split into two debit rows.
- If carry-forward expiry falls inside the requested interval, it splits around expiry.
Refs:
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:101
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:732
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:744
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:773
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:834
Backdated approval after expiry:
- May post corrective credit on allocation end date for non-carry-forward types.
Ref:
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:114
Cancel application:
- Deletes linked ledger rows by transaction name.
Ref:
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:132
5) Leave Encashment → Ledger Internal Flow
On submit:
- Debit row:
leaves = -encashment_days
- Updates
Leave Allocation.total_leaves_encashed
- Optional corrective credit when posting after allocation expiry for non-carry-forward type
On cancel:
- Rolls back encashed counter
- Deletes linked ledger rows
Refs:
apps/hrms/hrms/hr/doctype/leave_encashment/leave_encashment.py:45
apps/hrms/hrms/hr/doctype/leave_encashment/leave_encashment.py:171
apps/hrms/hrms/hr/doctype/leave_encashment/leave_encashment.py:250
apps/hrms/hrms/hr/doctype/leave_encashment/leave_encashment.py:57
Note:
- Auto leave encashment scheduler creates draft encashment docs.
- Ledger impact happens when encashment is submitted.
Refs:
apps/hrms/hrms/hr/utils.py:324
apps/hrms/hrms/hr/doctype/leave_encashment/leave_encashment.py:335
6) Monthly Earned Leave Accrual via Scheduler (Exact Logic)
Scheduler hook and order
allocate_earned_leaves is registered under daily_long.
Actual order in daily_long:
process_expired_allocation
generate_leave_encashment
allocate_earned_leaves
Ref:
apps/hrms/hrms/hooks.py:240
Runtime flow
Daily job:
- Get leave types where
is_earned_leave = 1
- Get active submitted allocations for each type
- Resolve annual allocation from
Leave Policy Detail
- Check due date via
check_effective_date()
- If due, compute earned amount, update allocation total, create ledger credit
Refs:
apps/hrms/hrms/hr/utils.py:348
apps/hrms/hrms/hr/utils.py:476
apps/hrms/hrms/hr/utils.py:524
apps/hrms/hrms/hr/utils.py:385
apps/hrms/hrms/hr/utils.py:516
Formula
Base periodic earned amount:
earned = annual_allocation / divisor
divisor = {Yearly: 1, Half-Yearly: 2, Quarterly: 4, Monthly: 12}
Pro-rata (when applicable, e.g., DOJ in period):
earned *= (actual_days_from_DOJ_to_period_end / total_days_in_period)
Rounding:
0.25 -> round(x * 4) / 4
0.5 -> round(x * 2) / 2
1.0 -> round(x)
blank -> no explicit rounding
Refs:
apps/hrms/hrms/hr/utils.py:427
apps/hrms/hrms/hr/utils.py:438
apps/hrms/hrms/hr/utils.py:453
apps/hrms/hrms/hr/utils.py:462
apps/hrms/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py:318
Your policy example (EL 18, SL 12, CL 12)
If all are configured as earned leave with monthly frequency:
- EL:
18/12 = 1.5 per month
- SL:
12/12 = 1 per month
- CL:
12/12 = 1 per month
Important:
- Monthly scheduler accrual applies only when
is_earned_leave = 1.
- Otherwise SL/CL are typically allocated up-front via assignment logic (possibly pro-rated), not monthly scheduler accrual.
Also:
- During assignment creation, earned leave may be seeded for passed periods (backdated assignment scenario), then scheduler continues periodic accrual.
Refs:
apps/hrms/hrms/hr/doctype/leave_type/leave_type.json:155
apps/hrms/hrms/hr/doctype/leave_type/leave_type.json:162
apps/hrms/hrms/hr/doctype/leave_type/leave_type.json:214
apps/hrms/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py:160
Where allocation + ledger update happens in scheduler:
allocation.db_set("total_leaves_allocated", ...)
create_additional_leave_ledger_entry(...) → allocation.create_leave_ledger_entry()
Refs:
apps/hrms/hrms/hr/utils.py:414
apps/hrms/hrms/hr/utils.py:415
apps/hrms/hrms/hr/utils.py:516
apps/hrms/hrms/hr/doctype/leave_allocation/leave_allocation.py:295
sequenceDiagram
participant S as daily_long Scheduler
participant U as allocate_earned_leaves()
participant LT as Leave Type (earned)
participant LA as Leave Allocation
participant LLE as Leave Ledger Entry
S->>U: Run daily
U->>LT: get_earned_leaves()
loop each earned leave type
U->>LA: get_leave_allocations(today, leave_type)
loop each active allocation
U->>U: check_effective_date(...)
alt due today
U->>U: earned = get_monthly_earned_leave(...)
U->>LA: db_set(total_leaves_allocated += earned)
U->>LA: create_additional_leave_ledger_entry(allocation, earned, today)
LA->>LLE: submit credit row
else not due
U-->>U: skip
end
end
end
7) Balance Computation Path (Why Ledger Matters)
Balance flow:
- Identify allocation context using allocation ledger rows.
- Aggregate consumed/encashed/expired effects from negative rows.
- Apply CF expiry and manual expiry adjustments.
- Return display balance and consumption-safe balance.
Refs:
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:974
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:1021
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:1108
apps/hrms/hrms/hr/doctype/leave_application/leave_application.py:1196
8) End-to-End Flow
flowchart LR
A[Leave Type + Leave Policy configured] --> B[Leave Policy Assignment submitted]
B --> C[Leave Allocation created/submitted]
C --> D[Allocation credit ledger entries posted]
D --> E[Daily scheduler may add earned credits]
E --> F[Employee submits Leave Application]
F --> G[On approval submit, debit ledger entries posted]
G --> H[Optional Leave Encashment submit posts debit]
H --> I[Daily expiry job posts expiry debits when due]
I --> J[get_leave_balance_on computes current balance from ledger]
Audit order:
Leave Type config
Leave Policy Detail annual values
Leave Allocation period/header totals
Leave Ledger Entry rows in date+creation order
9) Quick SQL for Debugging
SELECT
name, transaction_type, transaction_name,
from_date, to_date, leaves,
is_carry_forward, is_expired, is_lwp, docstatus, creation
FROM `tabLeave Ledger Entry`
WHERE employee = 'EMP-0001'
AND leave_type = 'Earned Leave'
AND docstatus = 1
ORDER BY from_date, creation;
Interpretation: running sum of leaves is the movement balance timeline.
10) Direct Answers to Your Questions
- Role of Leave Ledger Entry: Transaction journal for all leave credits/debits.
- Single source of truth: For movement/balance math yes; policy metadata comes from other doctypes.
- Allocation trigger timing: Submit, update-after-submit delta, manual add, scheduler add.
- Application trigger timing: On submit when approved; can split into multiple ledger rows.
- Encashment trigger timing: On submit debit + possible corrective row; cancel removes.
- Monthly scheduler logic:
daily_long + check_effective_date() + annual/divisor + pro-rata + rounding.
- Where updates happen:
hrms.hr.utils.allocate_earned_leaves() updates allocation total and posts ledger via create_additional_leave_ledger_entry().
For better readability and to see diagrams included, you may copy my answer to https://markdownviewer.pages.dev/