Merge branch 'develop' into fix-earned-leaves-allocation
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 6b85927..6d27f4a 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -546,7 +546,7 @@
 		from erpnext.hr.utils import allocate_earned_leaves
 		i = 0
 		while(i<14):
-			allocate_earned_leaves()
+			allocate_earned_leaves(ignore_duplicates=True)
 			i += 1
 		self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
 
@@ -554,7 +554,7 @@
 		frappe.db.set_value('Leave Type', leave_type, 'max_leaves_allowed', 0)
 		i = 0
 		while(i<6):
-			allocate_earned_leaves()
+			allocate_earned_leaves(ignore_duplicates=True)
 			i += 1
 		self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
 
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
index 355370f..41a9558 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
@@ -8,7 +8,7 @@
 import frappe
 from frappe import _, bold
 from frappe.model.document import Document
-from frappe.utils import date_diff, flt, formatdate, get_datetime, getdate
+from frappe.utils import date_diff, flt, formatdate, get_datetime, get_last_day, getdate
 
 
 class LeavePolicyAssignment(Document):
@@ -108,8 +108,8 @@
 	def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
 		from erpnext.hr.utils import get_monthly_earned_leave
 
-		current_month = get_datetime().month
-		current_year = get_datetime().year
+		current_month = get_datetime(frappe.flags.current_date).month or get_datetime().month
+		current_year = get_datetime(frappe.flags.current_date).year or get_datetime().year
 
 		from_date = frappe.db.get_value("Leave Period", self.leave_period, "from_date")
 		if getdate(date_of_joining) > getdate(from_date):
@@ -119,10 +119,14 @@
 		from_date_year = get_datetime(from_date).year
 
 		months_passed = 0
+
 		if current_year == from_date_year and current_month > from_date_month:
 			months_passed = current_month - from_date_month
+			months_passed = add_current_month_if_applicable(months_passed)
+
 		elif current_year > from_date_year:
 			months_passed = (12 - from_date_month) + current_month
+			months_passed = add_current_month_if_applicable(months_passed)
 
 		if months_passed > 0:
 			monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
@@ -134,6 +138,17 @@
 		return new_leaves_allocated
 
 
+def add_current_month_if_applicable(months_passed):
+	date = getdate(frappe.flags.current_date) or getdate()
+	last_day_of_month = get_last_day(date)
+
+	# if its the last day of the month, then that month should also be considered
+	if last_day_of_month == date:
+		months_passed += 1
+
+	return months_passed
+
+
 @frappe.whitelist()
 def create_assignment_for_multiple_employees(employees, data):
 
diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
index 3b7f8ec..3455bae 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
@@ -4,7 +4,7 @@
 import unittest
 
 import frappe
-from frappe.utils import add_months, get_first_day, getdate
+from frappe.utils import add_months, get_first_day, get_last_day, getdate
 
 from erpnext.hr.doctype.leave_application.test_leave_application import (
 	get_employee,
@@ -125,6 +125,69 @@
 		}, "total_leaves_allocated")
 		self.assertEqual(leaves_allocated, 0)
 
+	def test_earned_leave_allocation_for_passed_months(self):
+		employee = get_employee()
+		leave_type = create_earned_leave_type("Test Earned Leave")
+		leave_period = create_leave_period("Test Earned Leave Period",
+			start_date=get_first_day(add_months(getdate(), -1)))
+		leave_policy = frappe.get_doc({
+			"doctype": "Leave Policy",
+			"title": "Test Leave Policy",
+			"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
+		}).insert()
+
+		# Case 1: assignment created one month after the leave period, should allocate 1 leave
+		frappe.flags.current_date = get_first_day(getdate())
+		data = {
+			"assignment_based_on": "Leave Period",
+			"leave_policy": leave_policy.name,
+			"leave_period": leave_period.name
+		}
+		leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
+
+		leaves_allocated = frappe.db.get_value("Leave Allocation", {
+			"leave_policy_assignment": leave_policy_assignments[0]
+		}, "total_leaves_allocated")
+		self.assertEqual(leaves_allocated, 1)
+
+	def test_earned_leave_allocation_for_passed_months_on_month_end(self):
+		employee = get_employee()
+		leave_type = create_earned_leave_type("Test Earned Leave")
+		leave_period = create_leave_period("Test Earned Leave Period",
+			start_date=get_first_day(add_months(getdate(), -2)))
+		leave_policy = frappe.get_doc({
+			"doctype": "Leave Policy",
+			"title": "Test Leave Policy",
+			"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}]
+		}).insert()
+
+		# Case 2: assignment created on the last day of the leave period's latter month
+		# should allocate 1 leave for current month even though the month has not ended
+		# since the daily job might have already executed
+		frappe.flags.current_date = get_last_day(getdate())
+
+		data = {
+			"assignment_based_on": "Leave Period",
+			"leave_policy": leave_policy.name,
+			"leave_period": leave_period.name
+		}
+		leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
+
+		leaves_allocated = frappe.db.get_value("Leave Allocation", {
+			"leave_policy_assignment": leave_policy_assignments[0]
+		}, "total_leaves_allocated")
+		self.assertEqual(leaves_allocated, 3)
+
+		# if the daily job is not completed yet, there is another check present
+		# to ensure leave is not already allocated to avoid duplication
+		from erpnext.hr.utils import allocate_earned_leaves
+		allocate_earned_leaves()
+
+		leaves_allocated = frappe.db.get_value("Leave Allocation", {
+			"leave_policy_assignment": leave_policy_assignments[0]
+		}, "total_leaves_allocated")
+		self.assertEqual(leaves_allocated, 3)
+
 	def tearDown(self):
 		frappe.db.rollback()
 
@@ -137,14 +200,14 @@
 		doctype="Leave Type",
 		is_earned_leave=1,
 		earned_leave_frequency="Monthly",
-		rounding=0.5,
-		max_leaves_allowed=6
+		rounding=0.5
 	)).insert()
 
 
-def create_leave_period(name):
+def create_leave_period(name, start_date=None):
 	frappe.delete_doc_if_exists("Leave Period", name, force=1)
-	start_date = get_first_day(getdate())
+	if not start_date:
+		start_date = get_first_day(getdate())
 
 	return frappe.get_doc(dict(
 		name=name,
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index 0febce1..2a07e56 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -237,7 +237,7 @@
 
 		create_leave_encashment(leave_allocation=leave_allocation)
 
-def allocate_earned_leaves():
+def allocate_earned_leaves(ignore_duplicates=False):
 	'''Allocate earned leaves to Employees'''
 	e_leave_types = get_earned_leaves()
 	today = getdate()
@@ -265,9 +265,9 @@
 				from_date  = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
 
 			if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date):
-				update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
+				update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates)
 
-def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
+def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates=False):
 		earned_leaves = get_monthly_earned_leave(annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding)
 
 		allocation = frappe.get_doc('Leave Allocation', allocation.name)
@@ -277,9 +277,12 @@
 			new_allocation = e_leave_type.max_leaves_allowed
 
 		if new_allocation != allocation.total_leaves_allocated:
-			allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
 			today_date = today()
-			create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
+
+			if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation):
+				allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
+				create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
+
 
 def get_monthly_earned_leave(annual_leaves, frequency, rounding):
 	earned_leaves = 0.0
@@ -297,6 +300,23 @@
 	return earned_leaves
 
 
+def is_earned_leave_already_allocated(allocation, annual_allocation):
+	from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
+		get_leave_type_details,
+	)
+
+	leave_type_details = get_leave_type_details()
+	date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining")
+
+	assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment)
+	leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type,
+		annual_allocation, leave_type_details, date_of_joining)
+
+	if allocation.total_leaves_allocated >= leaves_for_passed_months:
+		return True
+	return False
+
+
 def get_leave_allocations(date, leave_type):
 	return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy
 		from `tabLeave Allocation`