frappe / frappe/hrms

Bug: KeyError 'Date of Joining' when submitting Leave Policy Assignment (Half-Yearly earned leave)

Open
#4,841 1 comment 0 reactions 0 assignees View on GitHub
invalid
Dominant language
Python
Stars
8.8k
Forks
2.7k
Avg merge
1d 10h
Merged PRs (30d)
88

Description

### Information about bug

# Bug: KeyError 'Date of Joining' when submitting Leave Policy Assignment (Half-Yearly earned leave)

## Description
Submitting a **Leave Policy Assignment** throws `KeyError: 'Date of Joining'` when the linked Leave Type has `Earned Leave Frequency = Half-Yearly` and `Allocate on Day = Date of Joining`.

`Date of Joining` as `Allocate on Day` was only ever implemented for `Monthly` frequency in `get_expected_allocation_date_for_period` (`hrms/hr/utils.py`) and in the `condition_map` inside `is_earned_leave_applicable_for_current_period` (`leave_policy_assignment.py`). Selecting this valid, allowed dropdown combination on the Leave Type doctype crashes on submit instead of allocating leaves correctly.

## Configuration to reproduce

| Field | Value |
|---|---|
| Leave Period | 01-04-2026 to 31-03-2027 |
| Leave Type | Earned Leave |
| Is Earned Leave | ✅ |
| Earned Leave Frequency | Half-Yearly |
| Allocate on Day | Date of Joining |
| Leave Policy Assignment → Assignment Based On | Joining Date |
| Employee Date of Joining | 27-05-2026 |

## Steps to reproduce
1. Create a Leave Type with the config above
2. Add it to a Leave Policy
3. Create a Leave Policy Assignment for an employee (Assignment Based On = Joining Date)
4. Submit → error thrown

## Traceback
```
File "hrms/hr/utils.py", line 635, in get_expected_allocation_date_for_period
return {...}[frequency][allocate_on_day]
builtins.KeyError: 'Date of Joining'
```

## Expected behavior
Earned leave should be allocated half-yearly, based on the employee's Date of Joining within each half-year period — the assignment should submit successfully and generate the correct Earned Leave Schedule, similar to how `Monthly + Date of Joining` already works.

## Actual behavior
Submission fails with `KeyError: 'Date of Joining'` and the Leave Policy Assignment cannot be submitted.

## Root cause
1. `get_expected_allocation_date_for_period` (`hrms/hr/utils.py`) — the `half_yearly_dates` dict only defines `"First Day"` / `"Last Day"` keys, no `"Date of Joining"`.
2. `is_earned_leave_applicable_for_current_period` (`leave_policy_assignment.py`) — `condition_map["Half-Yearly"]` has no `Date of Joining` branch, so pro-rata calculation (`consider_current_period`) would silently be incorrect even after fixing the KeyError.

## Fix
A fix has been raised in #4840, which:
- Adds `"Date of Joining": doj` to the `half_yearly_dates` dict in `get_expected_allocation_date_for_period`
- Adds a `Date of Joining` condition to `half_yearly_condition` in `is_earned_leave_applicable_for_current_period`, mirroring the existing `Monthly` logic

## Screenshots

Image

### Module

HR

### Version

```json
{
"frappe": "16.19.0",
"erpnext": "16.20.1",
"hrms": "16.11.0",
}
```

### Installation method

manual install

### Relevant log output / Stack trace / Full Error Message.

```shell
Traceback with variables (most recent call last):
File "apps/frappe/frappe/app.py", line 121, in application
response = frappe.api.handle(request)
request =
response = None
e = 'Date of Joining'
File "apps/frappe/frappe/api/__init__.py", line 63, in handle
data = endpoint(**arguments)
request =
endpoint =
arguments = {'method': 'frappe.desk.form.save.savedocs'}
File "apps/frappe/frappe/api/v1.py", line 40, in handle_rpc_call
return frappe.handler.handle()
method = 'frappe.desk.form.save.savedocs'
frappe =
File "apps/frappe/frappe/handler.py", line 53, in handle
data = execute_cmd(cmd)
cmd = 'frappe.desk.form.save.savedocs'
data = None
File "apps/frappe/frappe/handler.py", line 86, in execute_cmd
return frappe.call(method, **frappe.form_dict)
cmd = 'frappe.desk.form.save.savedocs'
from_async = False
server_script = None
method =
File "apps/frappe/frappe/__init__.py", line 1129, in call
return fn(*args, **newargs)
fn =
args = ()
kwargs = {'doc': '{"name":"HR-LPOL-ASSGN-00104","owner":"Administrator","creation":"2026-07-02 15:22:25.671754","modified":"2026-07-02 15:22:25.671754","modified_by":"Administrator","docstatus":0,"idx":0,"employee":"HR-EMP-00006","employee_name":"Avinash Jowel","company":"Buildskill Intertrade Private Limited","leave_policy":"HR-LPOL-2026-00004","carry_forward":0,"assignment_based_on":"Joining Date","effective_from":"2026-05-27","effective_to":"2027-05-27","leaves_allocated":0,"doctype":"Leave Policy Assignment","__last_sync_on":"2026-07-03T04:25:52.045Z"}', 'action': 'Submit', 'cmd': 'frappe.desk.form.save.savedocs'}
newargs = {'doc': '{"name":"HR-LPOL-ASSGN-00104","owner":"Administrator","creation":"2026-07-02 15:22:25.671754","modified":"2026-07-02 15:22:25.671754","modified_by":"Administrator","docstatus":0,"idx":0,"employee":"HR-EMP-00006","employee_name":"Avinash Jowel","company":"Buildskill Intertrade Private Limited","leave_policy":"HR-LPOL-2026-00004","carry_forward":0,"assignment_based_on":"Joining Date","effective_from":"2026-05-27","effective_to":"2027-05-27","leaves_allocated":0,"doctype":"Leave Policy Assignment","__last_sync_on":"2026-07-03T04:25:52.045Z"}', 'action': 'Submit'}
File "apps/frappe/frappe/utils/typing_validations.py", line 36, in wrapper
return func(*args, **kwargs)
args = ()
kwargs = {'doc': '{"name":"HR-LPOL-ASSGN-00104","owner":"Administrator","creation":"2026-07-02 15:22:25.671754","modified":"2026-07-02 15:22:25.671754","modified_by":"Administrator","docstatus":0,"idx":0,"employee":"HR-EMP-00006","employee_name":"Avinash Jowel","company":"Buildskill Intertrade Private Limited","leave_policy":"HR-LPOL-2026-00004","carry_forward":0,"assignment_based_on":"Joining Date","effective_from":"2026-05-27","effective_to":"2027-05-27","leaves_allocated":0,"doctype":"Leave Policy Assignment","__last_sync_on":"2026-07-03T04:25:52.045Z"}', 'action': 'Submit'}
apply_condition =
func =
File "apps/frappe/frappe/desk/form/save.py", line 41, in savedocs
doc.submit()
doc = Leave Policy Assignment (HR-LPOL-ASSGN-00104)
action = 'Submit'
File "apps/frappe/frappe/utils/typing_validations.py", line 36, in wrapper
return func(*args, **kwargs)
args = (Leave Policy Assignment (HR-LPOL-ASSGN-00104))
kwargs = {}
apply_condition =
func =
File "apps/frappe/frappe/model/document.py", line 1273, in submit
return self._submit()
self = Leave Policy Assignment (HR-LPOL-ASSGN-00104)
File "apps/frappe/frappe/model/document.py", line 1254, in _submit
return self.save()
self = Leave Policy Assignment (HR-LPOL-ASSGN-00104)
File "apps/frappe/frappe/model/document.py", line 518, in save
return self._save(*args, **kwargs)
self = Leave Policy Assignment (HR-LPOL-ASSGN-00104)
args = ()
kwargs = {}
File "apps/frappe/frappe/model/document.py", line 572, in _save
self.run_post_save_methods()
self = Leave Policy Assignment (HR-LPOL-ASSGN-00104)
ignore_permissions = None
ignore_version = None
File "apps/frappe/frappe/model/document.py", line 1381, in run_post_save_methods
self.run_method("on_submit")
self = Leave Policy Assignment (HR-LPOL-ASSGN-00104)
File "apps/frappe/frappe/model/document.py", line 1184, in run_method
out = Document.hook(fn)(self, *args, **kwargs)
self = Leave Policy Assignment (HR-LPOL-ASSGN-00104)
method = 'on_submit'
args = ()
kwargs = {}
fn = .fn at 0xffff667c2a30>
File "apps/frappe/frappe/model/document.py", line 1581, in composer
return composed(self, method, *args, **kwargs)
self = Leave Policy Assignment (HR-LPOL-ASSGN-00104)
args = ()
kwargs = {}
hooks = []
method = 'on_submit'
doc_events = {'*': {'on_update': ['frappe.desk.notifications.clear_doctype_notifications', 'frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions', 'frappe.core.doctype.file.utils.attach_files_to_document', 'frappe.automation.doctype.assignment_rule.assignment_rule.apply', 'frappe.automation.doctype.assignment_rule.assignment_rule.update_due_date', 'frappe.core.doctype.user_type.user_type.apply_permissions_for_non_standard_user_type', 'frappe.core.doctype.permission_log.permission_log.make_perm_log', 'frappe.search.sqlite_search.update_doc_index'], 'after_rename': ['frappe.desk.notifications.clear_doctype_notifications'], 'on_cancel': ['frappe.desk.notifications.clear_doctype_n....leave_application.on_cancel']}, 'Leave Allocation': {'validate': ['buildskill.build_skill.custom_script.leave_allocation.leave_allocation.validate']}, 'Compensatory Leave Request': {'validate': ['buildskill.build_skill.custom_script.compensatory_leave_request.compensatory_leave_request.validate']}}
composed = .compose..runner at 0xffff5bdf8250>
compose = .compose at 0xffff5bdf80f0>
f = .fn at 0xffff667c2a30>
File "apps/frappe/frappe/model/document.py", line 1559, in runner
add_to_return_value(self, fn(self, *args, **kwargs))
self = Leave Policy Assignment (HR-LPOL-ASSGN-00104)
method = 'on_submit'
args = ()
kwargs = {}
add_to_return_value = .add_to_return_value at 0xffff5bdf8040>
fn = .fn at 0xffff667c2a30>
hooks = ()
File "apps/frappe/frappe/model/document.py", line 1181, in fn
return method_object(*args, **kwargs)
self = Leave Policy Assignment (HR-LPOL-ASSGN-00104)
args = ()
kwargs = {}
method_object = >
method = 'on_submit'
File "apps/hrms/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py", line 37, in on_submit
self.grant_leave_alloc_for_employee()
self = Leave Policy Assignment (HR-LPOL-ASSGN-00104)
File "apps/hrms/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py", line 102, in grant_leave_alloc_for_employee
leave_allocation, new_leaves_allocated = self.create_leave_allocation(
self = Leave Policy Assignment (HR-LPOL-ASSGN-00104)
leave_allocations = {}
leave_type_details = {'Leave Without Pay': {'name': 'Leave Without Pay', 'is_lwp': 1, 'is_earned_leave': 0, 'is_compensatory': 0, 'allocate_on_day': 'Last Day', 'is_carry_forward': 0, 'expire_carry_forwarded_leaves_after_days': 0, 'earned_leave_frequency': 'Monthly', 'rounding': ''}, 'Earned Leaves': {'name': 'Earned Leaves', 'is_lwp': 0, 'is_earned_leave': 1, 'is_compensatory': 0, 'allocate_on_day': 'Date of Joining', 'is_carry_forward': 1, 'expire_carry_forwarded_leaves_after_days': 0, 'earned_leave_frequency': 'Half-Yearly', 'rounding': ''}, 'Compensatory Off': {'name': 'Compensatory Off', 'is_lwp': 0, 'is_earned_leave': 0, 'is_compensatory': 1, 'allocate_on_day': 'Last Day', 'is_carry_forward': 0, 'expire_ca...quency': 'Monthly', 'rounding': ''}, 'Casual/Sick Leave': {'name': 'Casual/Sick Leave', 'is_lwp': 0, 'is_earned_leave': 1, 'is_compensatory': 0, 'allocate_on_day': 'First Day', 'is_carry_forward': 0, 'expire_carry_forwarded_leaves_after_days': 0, 'earned_leave_frequency': 'Monthly', 'rounding': ''}}
leave_policy = Leave Policy (HR-LPOL-2026-00004)
date_of_joining = 2026-05-27
leave_policy_detail = Leave Policy Detail (427ofmuujj)
leave_details = {'name': 'Earned Leaves', 'is_lwp': 0, 'is_earned_leave': 1, 'is_compensatory': 0, 'allocate_on_day': 'Date of Joining', 'is_carry_forward': 1, 'expire_carry_forwarded_leaves_after_days': 0, 'earned_leave_frequency': 'Half-Yearly', 'rounding': ''}
File "apps/hrms/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py", line 122, in create_leave_allocation
self.get_earned_leave_schedule(
self = Leave Policy Assignment (HR-LPOL-ASSGN-00104)
annual_allocation = 15.0
leave_details = {'name': 'Earned Leaves', 'is_lwp': 0, 'is_earned_leave': 1, 'is_compensatory': 0, 'allocate_on_day': 'Date of Joining', 'is_carry_forward': 1, 'expire_carry_forwarded_leaves_after_days': 0, 'earned_leave_frequency': 'Half-Yearly', 'rounding': ''}
date_of_joining = 2026-05-27
carry_forward = 0
new_leaves_allocated = 7.5
File "apps/hrms/hrms/hr/doctype/leave_policy_assignment/leave_policy_assignment.py", line 297, in get_earned_leave_schedule
date = get_expected_allocation_date_for_period(
self = Leave Policy Assignment (HR-LPOL-ASSGN-00104)
annual_allocation = 15.0
leave_details = {'name': 'Earned Leaves', 'is_lwp': 0, 'is_earned_leave': 1, 'is_compensatory': 0, 'allocate_on_day': 'Date of Joining', 'is_carry_forward': 1, 'expire_carry_forwarded_leaves_after_days': 0, 'earned_leave_frequency': 'Half-Yearly', 'rounding': ''}
date_of_joining = 2026-05-27
new_leaves_allocated = 7.5
get_expected_allocation_date_for_period =
get_monthly_earned_leave =
get_sub_period_start_and_end =
today = 2026-07-03
from_date = 2026-05-27
last_allocated_date = 2026-05-27
to_date = 2027-05-27
months_to_add = 6
periodically_earned_leave = 7.5
File "apps/hrms/hrms/hr/utils.py", line 635, in get_expected_allocation_date_for_period
return {
frequency = 'Half-Yearly'
allocate_on_day = 'Date of Joining'
date = 2026-05-27
date_of_joining = 2026-05-27
effective_from = 2026-05-27
doj = 2026-05-27
period_start = 2026-05-27
period_end = 2026-11-26
half_yearly_dates = {'First Day': datetime.date(2026, 5, 27), 'Last Day': datetime.date(2026, 11, 26)}
builtins.KeyError: 'Date of Joining'
```

### Code of Conduct

- [x] I agree to follow this project's Code of Conduct

Contributor guide

No contributing guide indexed for this repository

Research direction

Review the proposed fix in #4840, then inspect get_expected_allocation_date_for_period in hrms/hr/utils.py and is_earned_leave_applicable_for_current_period in leave_policy_assignment.py. Reproduce the Half-Yearly and Date of Joining configuration, and verify that submission succeeds and generates the expected Earned Leave Schedule.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.