Need Detailed Technical Explanation of Leave Ledger Entry & Monthly Accrual Logic in ERPNext v15 (St

Hello Everyone,

I am working with ERPNext v15 (standard HRMS module) and would like to gain a deep technical understanding of how the Leave Ledger Entry works internally and how it impacts the overall leave management flow.

Specifically, I am looking for clarification on the following areas:

  1. Leave Ledger Entry – Core Concept
  • What exactly is the role of Leave Ledger Entry in ERPNext HRMS?

  • Is it considered the single source of truth for leave balances?

  • How does it technically interact with:

    • Leave Allocation
    • Leave Application
    • Leave Encashment (if applicable)
  1. Impact on Leave Allocation and Leave Application
  • When a Leave Allocation is created or updated, how and when is a Leave Ledger Entry generated?
  • When a Leave Application is submitted and approved, what is the internal flow that updates the Leave Ledger?
  • Which methods or DocType events trigger the ledger entry creation?
  1. Monthly Leave Accrual via Scheduler (Earned Leave Logic)

We have a yearly leave policy configured as:

  • Earned Leave (EL) = 18 days per year
  • Sick Leave (SL) = 12 days per year
  • Casual Leave (CL) = 12 days per year

The system credits monthly as follows:

  • EL → 1.5 per month
  • SL → 1 per month
  • CL → 1 per month

I would like to understand:

  • What is the exact scheduler logic responsible for this monthly distribution?

  • How does the system calculate the per-month allocation (e.g., division, rounding rules, etc.)?

  • Does this scheduler run daily and check month-end conditions, or does it run specifically at month-end?

  • How does this process update:

    • Leave Allocation
    • Leave Ledger Entry
    • Employee leave balance
  1. Code-Level Details (ERPNext v15)

Since I am using the standard HRMS module in v15, I would appreciate details on:

  • The exact Python file where the earned leave scheduler logic is written
  • The method name responsible for monthly earned leave allocation
  • Where the Leave Ledger Entry creation is triggered in the code
  • Any relevant hooks or scheduled job configuration
  1. Overall Flow Clarification

If possible, I would appreciate a clear end-to-end explanation of:

Leave Policy → Leave Policy Assignment → Leave Allocation → Scheduler Accrual → Leave Ledger Entry → Leave Application → Updated Balance

I am looking for a complete technical and functional explanation to fully understand how the leave system is designed internally in ERPNext v15.

Thank you in advance for your guidance.

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:

  1. process_expired_allocation
  2. generate_leave_encashment
  3. allocate_earned_leaves

Ref:

  • apps/hrms/hrms/hooks.py:240

Runtime flow

Daily job:

  1. Get leave types where is_earned_leave = 1
  2. Get active submitted allocations for each type
  3. Resolve annual allocation from Leave Policy Detail
  4. Check due date via check_effective_date()
  5. 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:

  1. Identify allocation context using allocation ledger rows.
  2. Aggregate consumed/encashed/expired effects from negative rows.
  3. Apply CF expiry and manual expiry adjustments.
  4. 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:

  1. Leave Type config
  2. Leave Policy Detail annual values
  3. Leave Allocation period/header totals
  4. 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/