Can't set Start and End Dates in Salary Slip (#9513) (#9944)

* remove trigger from end_date

* adds new function `get_end_date`:
- it tries to calculate the appropriate end date for a given frequency
- returns an empty string if frequency is 'biweekly'
- adds test cases

* changes logic in `set_start_end_dates`:
- if start_date is empty in form, call process_payroll.get_start_end_dates
- else, call process_payroll.get_end_date

* `get_end_date` should return a dict

* changed "biweekly" to "bimonthly"

* change the behaviour of process payroll start and end date:
- when payroll frequency is changed, change start/end date as usual
- if start date is manually changed, use the frequency to calculate the end date

* clean up

* further cleanup

* in `get_end_date`, if `frequency` isn"t given, "monthly"

* remove end_date from cscript and introduce `set_end_date`

* fix tests

* removed whitespaces
diff --git a/erpnext/hr/doctype/process_payroll/process_payroll.js b/erpnext/hr/doctype/process_payroll/process_payroll.js
index 0ec7f70..a9ad429 100644
--- a/erpnext/hr/doctype/process_payroll/process_payroll.js
+++ b/erpnext/hr/doctype/process_payroll/process_payroll.js
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 // License: GNU General Public License v3. See license.txt
 
+var in_progress = false;
+
 frappe.ui.form.on("Process Payroll", {
 	onload: function (frm) {
 		frm.doc.posting_date = frappe.datetime.nowdate();
@@ -47,11 +49,12 @@
 	},
 
 	start_date: function (frm) {
-		frm.trigger("set_start_end_dates");
-	},
-
-	end_date: function (frm) {
-		frm.trigger("set_start_end_dates");
+		if(!in_progress && frm.doc.start_date){
+			frm.trigger("set_end_date");
+		}else{
+			// reset flag
+			in_progress = false
+		}
 	},
 
 	salary_slip_based_on_timesheet: function (frm) {
@@ -68,10 +71,11 @@
 				method: 'erpnext.hr.doctype.process_payroll.process_payroll.get_start_end_dates',
 				args: {
 					payroll_frequency: frm.doc.payroll_frequency,
-					start_date: frm.doc.start_date || frm.doc.posting_date
+					start_date: frm.doc.posting_date
 				},
 				callback: function (r) {
 					if (r.message) {
+						in_progress = true;
 						frm.set_value('start_date', r.message.start_date);
 						frm.set_value('end_date', r.message.end_date);
 					}
@@ -79,6 +83,21 @@
 			})
 		}
 	},
+
+	set_end_date: function(frm){
+		frappe.call({
+			method: 'erpnext.hr.doctype.process_payroll.process_payroll.get_end_date',
+			args: {
+				frequency: frm.doc.payroll_frequency,
+				start_date: frm.doc.start_date
+			},
+			callback: function (r) {
+				if (r.message) {
+					frm.set_value('end_date', r.message.end_date);
+				}
+			}
+		})
+	}
 })
 
 cur_frm.cscript.display_activity_log = function (msg) {
diff --git a/erpnext/hr/doctype/process_payroll/process_payroll.py b/erpnext/hr/doctype/process_payroll/process_payroll.py
index 7ca94b8..b58c787 100644
--- a/erpnext/hr/doctype/process_payroll/process_payroll.py
+++ b/erpnext/hr/doctype/process_payroll/process_payroll.py
@@ -3,7 +3,8 @@
 
 from __future__ import unicode_literals
 import frappe
-from frappe.utils import cint, flt, nowdate, add_days, getdate, fmt_money
+from dateutil.relativedelta import relativedelta
+from frappe.utils import cint, nowdate, add_days, getdate, fmt_money, add_to_date, DATE_FORMAT
 from frappe import _
 from erpnext.accounts.utils import get_fiscal_year
 
@@ -47,7 +48,6 @@
 			%s """% cond, {"sal_struct": sal_struct})
 			return emp_list
 
-
 	def get_filter_condition(self):
 		self.check_mandatory()
 
@@ -58,7 +58,6 @@
 
 		return cond
 
-
 	def get_joining_releiving_condition(self):
 		cond = """
 			and ifnull(t1.date_of_joining, '0000-00-00') <= '%(end_date)s'
@@ -66,7 +65,6 @@
 		""" % {"start_date": self.start_date, "end_date": self.end_date}
 		return cond
 
-
 	def check_mandatory(self):
 		for fieldname in ['company', 'start_date', 'end_date']:
 			if not self.get(fieldname):
@@ -110,7 +108,6 @@
 					ss_list.append(ss_dict)
 		return self.create_log(ss_list)
 
-
 	def create_log(self, ss_list):
 		if not ss_list or len(ss_list) < 1: 
 			log = "<p>" + _("No employee for the above selected criteria OR salary slip already created") + "</p>"
@@ -134,7 +131,6 @@
 		""" % ('%s', '%s', '%s','%s', cond), (ss_status, self.start_date, self.end_date, self.salary_slip_based_on_timesheet), as_dict=as_dict)
 		return ss_list
 
-
 	def submit_salary_slips(self):
 		"""
 			Submit all salary slips based on selected criteria
@@ -194,7 +190,6 @@
 	def format_as_links(self, salary_slip):
 		return ['<a href="#Form/Salary Slip/{0}">{0}</a>'.format(salary_slip)]
 
-
 	def get_total_salary_and_loan_amounts(self):
 		"""
 			Get total loan principal, loan interest and salary amount from submitted salary slip based on selected criteria
@@ -257,7 +252,6 @@
 
 		return payroll_payable_account	
 
-
 	def make_accural_jv_entry(self):
 		self.check_permission('write')
 		earnings = self.get_salary_component_total(component_type = "earnings") or {}
@@ -358,7 +352,6 @@
 		self.update(get_start_end_dates(self.payroll_frequency, 
 			self.start_date or self.posting_date, self.company))
 
-
 @frappe.whitelist()
 def get_start_end_dates(payroll_frequency, start_date=None, company=None):
 	'''Returns dict of start and end dates for given payroll frequency based on start_date'''
@@ -391,6 +384,29 @@
 		'start_date': start_date, 'end_date': end_date
 	})
 
+def get_frequency_kwargs(frequency_name):
+	frequency_dict = {
+		'monthly': {'months': 1},
+		'fortnightly': {'days': 14},
+		'weekly': {'days': 7},
+		'daily': {'days': 1}
+	}
+	return frequency_dict.get(frequency_name)
+
+@frappe.whitelist()
+def get_end_date(start_date, frequency):
+	start_date = getdate(start_date)
+	frequency = frequency.lower() if frequency else 'monthly'
+	kwargs = get_frequency_kwargs(frequency) if frequency != 'bimonthly' else get_frequency_kwargs('monthly')
+
+	# weekly, fortnightly and daily intervals have fixed days so no problems
+	end_date = add_to_date(start_date, **kwargs) - relativedelta(days=1)
+	if frequency != 'bimonthly':
+		return dict(end_date=end_date.strftime(DATE_FORMAT))
+
+	else:
+		return dict(end_date='')
+
 def get_month_details(year, month):
 	ysd = frappe.db.get_value("Fiscal Year", year, "year_start_date")
 	if ysd:
diff --git a/erpnext/hr/doctype/process_payroll/test_process_payroll.py b/erpnext/hr/doctype/process_payroll/test_process_payroll.py
index 6347a2a..1f9ba26 100644
--- a/erpnext/hr/doctype/process_payroll/test_process_payroll.py
+++ b/erpnext/hr/doctype/process_payroll/test_process_payroll.py
@@ -3,10 +3,12 @@
 from __future__ import unicode_literals
 
 import unittest
-import frappe
+
 import erpnext
-from frappe.utils import flt, add_months, cint, nowdate, getdate, add_days, random_string
-from frappe.utils.make_random import get_random
+import frappe
+from frappe.utils import nowdate
+from erpnext.hr.doctype.process_payroll.process_payroll import get_end_date
+
 
 class TestProcessPayroll(unittest.TestCase):
 	def test_process_payroll(self):
@@ -30,6 +32,16 @@
 			process_payroll.submit_salary_slips()
 			if process_payroll.get_sal_slip_list(ss_status = 1):
 				r = process_payroll.make_payment_entry()
+
+	def test_get_end_date(self):
+		self.assertEqual(get_end_date('2017-01-01', 'monthly'), {'end_date': '2017-01-31'})
+		self.assertEqual(get_end_date('2017-02-01', 'monthly'), {'end_date': '2017-02-28'})
+		self.assertEqual(get_end_date('2017-02-01', 'fortnightly'), {'end_date': '2017-02-14'})
+		self.assertEqual(get_end_date('2017-02-01', 'bimonthly'), {'end_date': ''})
+		self.assertEqual(get_end_date('2017-01-01', 'bimonthly'), {'end_date': ''})
+		self.assertEqual(get_end_date('2020-02-15', 'bimonthly'), {'end_date': ''})
+		self.assertEqual(get_end_date('2017-02-15', 'monthly'), {'end_date': '2017-03-14'})
+		self.assertEqual(get_end_date('2017-02-15', 'daily'), {'end_date': '2017-02-15'})
 	
 
 def get_salary_component_account(sal_comp):
diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.js b/erpnext/hr/doctype/salary_slip/salary_slip.js
index a96347a..de3276c 100644
--- a/erpnext/hr/doctype/salary_slip/salary_slip.js
+++ b/erpnext/hr/doctype/salary_slip/salary_slip.js
@@ -29,6 +29,27 @@
 		})
 	},
 
+	start_date: function(frm){
+		if(frm.doc.start_date){
+			frm.trigger("set_end_date");
+		}
+	},
+
+	set_end_date: function(frm){
+		frappe.call({
+			method: 'erpnext.hr.doctype.process_payroll.process_payroll.get_end_date',
+			args: {
+				frequency: frm.doc.payroll_frequency,
+				start_date: frm.doc.start_date
+			},
+			callback: function (r) {
+				if (r.message) {
+					frm.set_value('end_date', r.message.end_date);
+				}
+			}
+		})
+	},
+
 	company: function(frm) {
 		var company = locals[':Company'][frm.doc.company];
 		if(!frm.doc.letter_head && company.default_letter_head) {
@@ -45,11 +66,17 @@
 	},	
 
 	salary_slip_based_on_timesheet: function(frm) {
-		frm.trigger("toggle_fields")
+		frm.trigger("toggle_fields");
+		frm.set_value('start_date', '');
 	},
 	
 	payroll_frequency: function(frm) {
-		frm.trigger("toggle_fields")
+		frm.trigger("toggle_fields");
+		frm.set_value('start_date', '');
+	},
+
+	employee: function(frm){
+		frm.set_value('start_date', '');
 	},
 
 	toggle_fields: function(frm) {
@@ -74,18 +101,19 @@
 // Get leave details
 //---------------------------------------------------------------------
 cur_frm.cscript.start_date = function(doc, dt, dn){
-	return frappe.call({
-		method: 'get_emp_and_leave_details',
-		doc: locals[dt][dn],
-		callback: function(r, rt) {
-			cur_frm.refresh();
-			calculate_all(doc, dt, dn);
-		}
-	});
+	if(!doc.start_date){
+		return frappe.call({
+			method: 'get_emp_and_leave_details',
+			doc: locals[dt][dn],
+			callback: function(r, rt) {
+				cur_frm.refresh();
+				calculate_all(doc, dt, dn);
+			}
+		});
+	}
 }
 
 cur_frm.cscript.payroll_frequency = cur_frm.cscript.salary_slip_based_on_timesheet = cur_frm.cscript.start_date;
-cur_frm.cscript.end_date = cur_frm.cscript.start_date;
 
 cur_frm.cscript.employee = function(doc,dt,dn){
 	doc.salary_structure = ''