Bug: KeyError 'Date of Joining' when submitting Leave Policy Assignment (Half-Yearly earned leave)
- 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
### 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