Merge branch 'develop' into multiple-cost-centers-against-employee
diff --git a/erpnext/hr/doctype/department/department.js b/erpnext/hr/doctype/department/department.js
index 7db8cfb..46cfbda 100644
--- a/erpnext/hr/doctype/department/department.js
+++ b/erpnext/hr/doctype/department/department.js
@@ -6,6 +6,15 @@
 		frm.set_query("parent_department", function(){
 			return {"filters": [["Department", "is_group", "=", 1]]};
 		});
+
+		frm.set_query("payroll_cost_center", function() {
+			return {
+				filters: {
+					"company": frm.doc.company,
+					"is_group": 0
+				}
+			};
+		});
 	},
 	refresh: function(frm) {
 		// read-only for root department
diff --git a/erpnext/hr/doctype/employee/employee.js b/erpnext/hr/doctype/employee/employee.js
index 13b33e2..8c73e9c 100755
--- a/erpnext/hr/doctype/employee/employee.js
+++ b/erpnext/hr/doctype/employee/employee.js
@@ -47,6 +47,15 @@
 				}
 			};
 		});
+
+		frm.set_query("payroll_cost_center", function() {
+			return {
+				filters: {
+					"company": frm.doc.company,
+					"is_group": 0
+				}
+			};
+		});
 	},
 	onload: function (frm) {
 		frm.set_query("department", function() {
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index 88e5ca9..a2df26c 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -68,12 +68,18 @@
 		self.employee_name = ' '.join(filter(lambda x: x, [self.first_name, self.middle_name, self.last_name]))
 
 	def validate_user_details(self):
-		data = frappe.db.get_value('User',
-			self.user_id, ['enabled', 'user_image'], as_dict=1)
-		if data.get("user_image") and self.image == '':
-			self.image = data.get("user_image")
-		self.validate_for_enabled_user_id(data.get("enabled", 0))
-		self.validate_duplicate_user_id()
+		if self.user_id:
+			data = frappe.db.get_value('User',
+				self.user_id, ['enabled', 'user_image'], as_dict=1)
+
+			if not data:
+				self.user_id = None
+				return
+
+			if data.get("user_image") and self.image == '':
+				self.image = data.get("user_image")
+			self.validate_for_enabled_user_id(data.get("enabled", 0))
+			self.validate_duplicate_user_id()
 
 	def update_nsm_model(self):
 		frappe.utils.nestedset.update_nsm(self)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 716dcc0..deeeeb7 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -320,3 +320,4 @@
 erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
 erpnext.patches.v13_0.update_tax_category_for_rcm
 execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
+erpnext.patches.v14_0.set_payroll_cost_centers
\ No newline at end of file
diff --git a/erpnext/patches/v14_0/set_payroll_cost_centers.py b/erpnext/patches/v14_0/set_payroll_cost_centers.py
new file mode 100644
index 0000000..89b305b
--- /dev/null
+++ b/erpnext/patches/v14_0/set_payroll_cost_centers.py
@@ -0,0 +1,32 @@
+import frappe
+
+
+def execute():
+	frappe.reload_doc('payroll', 'doctype', 'employee_cost_center')
+	frappe.reload_doc('payroll', 'doctype', 'salary_structure_assignment')
+
+	employees = frappe.get_all("Employee", fields=["department", "payroll_cost_center", "name"])
+
+	employee_cost_center = {}
+	for d in employees:
+		cost_center = d.payroll_cost_center
+		if not cost_center and d.department:
+			cost_center = frappe.get_cached_value("Department", d.department, "payroll_cost_center")
+
+		if cost_center:
+			employee_cost_center.setdefault(d.name, cost_center)
+
+	salary_structure_assignments = frappe.get_all("Salary Structure Assignment",
+		filters = {"docstatus": ["!=", 2]},
+		fields=["name", "employee"])
+
+	for d in salary_structure_assignments:
+		cost_center = employee_cost_center.get(d.employee)
+		if cost_center:
+			assignment = frappe.get_doc("Salary Structure Assignment", d.name)
+			if not assignment.get("payroll_cost_centers"):
+				assignment.append("payroll_cost_centers", {
+					"cost_center": cost_center,
+					"percentage": 100
+				})
+				assignment.save()
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/employee_cost_center/__init__.py b/erpnext/payroll/doctype/employee_cost_center/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/payroll/doctype/employee_cost_center/__init__.py
diff --git a/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.json b/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.json
new file mode 100644
index 0000000..8fed9f7
--- /dev/null
+++ b/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.json
@@ -0,0 +1,43 @@
+{
+ "actions": [],
+ "creation": "2021-12-23 12:44:38.389283",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "cost_center",
+  "percentage"
+ ],
+ "fields": [
+  {
+   "allow_on_submit": 1,
+   "fieldname": "cost_center",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Cost Center",
+   "options": "Cost Center",
+   "reqd": 1
+  },
+  {
+   "allow_on_submit": 1,
+   "fieldname": "percentage",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Percentage (%)",
+   "non_negative": 1,
+   "reqd": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-12-23 17:39:03.410924",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Employee Cost Center",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.py b/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.py
new file mode 100644
index 0000000..6c5be97
--- /dev/null
+++ b/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class EmployeeCostCenter(Document):
+	pass
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 84c59a2..5bb32cf 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -7,6 +7,7 @@
 from frappe import _
 from frappe.desk.reportview import get_filters_cond, get_match_cond
 from frappe.model.document import Document
+from frappe.query_builder.functions import Coalesce
 from frappe.utils import (
 	DATE_FORMAT,
 	add_days,
@@ -157,11 +158,20 @@
 			Returns list of salary slips based on selected criteria
 		"""
 
-		ss_list = frappe.db.sql("""
-			select t1.name, t1.salary_structure, t1.payroll_cost_center from `tabSalary Slip` t1
-			where t1.docstatus = %s and t1.start_date >= %s and t1.end_date <= %s and t1.payroll_entry = %s
-			and (t1.journal_entry is null or t1.journal_entry = "") and ifnull(salary_slip_based_on_timesheet,0) = %s
-		""", (ss_status, self.start_date, self.end_date, self.name, self.salary_slip_based_on_timesheet), as_dict=as_dict)
+		ss = frappe.qb.DocType("Salary Slip")
+		ss_list = (
+			frappe.qb.from_(ss)
+				.select(ss.name, ss.salary_structure)
+				.where(
+					(ss.docstatus == ss_status)
+					& (ss.start_date >= self.start_date)
+					& (ss.end_date <= self.end_date)
+					& (ss.payroll_entry == self.name)
+					& ((ss.journal_entry.isnull()) | (ss.journal_entry == ""))
+					& (Coalesce(ss.salary_slip_based_on_timesheet, 0) == self.salary_slip_based_on_timesheet)
+				)
+		).run(as_dict=as_dict)
+
 		return ss_list
 
 	@frappe.whitelist()
@@ -190,13 +200,20 @@
 
 	def get_salary_components(self, component_type):
 		salary_slips = self.get_sal_slip_list(ss_status = 1, as_dict = True)
+
 		if salary_slips:
-			salary_components = frappe.db.sql("""
-				select ssd.salary_component, ssd.amount, ssd.parentfield, ss.payroll_cost_center
-				from `tabSalary Slip` ss, `tabSalary Detail` ssd
-				where ss.name = ssd.parent and ssd.parentfield = '%s' and ss.name in (%s)
-			""" % (component_type, ', '.join(['%s']*len(salary_slips))),
-				tuple([d.name for d in salary_slips]), as_dict=True)
+			ss = frappe.qb.DocType("Salary Slip")
+			ssd = frappe.qb.DocType("Salary Detail")
+			salary_components = (
+				frappe.qb.from_(ss)
+					.join(ssd)
+					.on(ss.name == ssd.parent)
+					.select(ssd.salary_component, ssd.amount, ssd.parentfield, ss.salary_structure, ss.employee)
+					.where(
+						(ssd.parentfield == component_type)
+						& (ss.name.isin(tuple([d.name for d in salary_slips])))
+					)
+			).run(as_dict=True)
 
 			return salary_components
 
@@ -204,18 +221,49 @@
 		salary_components = self.get_salary_components(component_type)
 		if salary_components:
 			component_dict = {}
+			self.employee_cost_centers = {}
 			for item in salary_components:
+				employee_cost_centers = self.get_payroll_cost_centers_for_employee(item.employee, item.salary_structure)
+
 				add_component_to_accrual_jv_entry = True
 				if component_type == "earnings":
-					is_flexible_benefit, only_tax_impact = frappe.db.get_value("Salary Component", item['salary_component'], ['is_flexible_benefit', 'only_tax_impact'])
+					is_flexible_benefit, only_tax_impact = \
+						frappe.get_cached_value("Salary Component",item['salary_component'], ['is_flexible_benefit', 'only_tax_impact'])
 					if is_flexible_benefit == 1 and only_tax_impact ==1:
 						add_component_to_accrual_jv_entry = False
+
 				if add_component_to_accrual_jv_entry:
-					component_dict[(item.salary_component, item.payroll_cost_center)] \
-						= component_dict.get((item.salary_component, item.payroll_cost_center), 0) + flt(item.amount)
+					for cost_center, percentage in employee_cost_centers.items():
+						amount_against_cost_center = flt(item.amount) * percentage / 100
+						component_dict[(item.salary_component, cost_center)] \
+							= component_dict.get((item.salary_component, cost_center), 0) + amount_against_cost_center
+
 			account_details = self.get_account(component_dict = component_dict)
 			return account_details
 
+	def get_payroll_cost_centers_for_employee(self, employee, salary_structure):
+		if not self.employee_cost_centers.get(employee):
+			ss_assignment_name = frappe.db.get_value("Salary Structure Assignment",
+				{"employee": employee, "salary_structure": salary_structure, "docstatus": 1}, 'name')
+
+			if ss_assignment_name:
+				cost_centers = dict(frappe.get_all("Employee Cost Center", {"parent": ss_assignment_name},
+					["cost_center", "percentage"], as_list=1))
+				if not cost_centers:
+					default_cost_center, department = frappe.get_cached_value("Employee", employee, ["payroll_cost_center", "department"])
+					if not default_cost_center and department:
+						default_cost_center = frappe.get_cached_value("Department", department, "payroll_cost_center")
+					if not default_cost_center:
+						default_cost_center = self.cost_center
+
+					cost_centers = {
+						default_cost_center: 100
+					}
+
+				self.employee_cost_centers.setdefault(employee, cost_centers)
+
+		return self.employee_cost_centers.get(employee, {})
+
 	def get_account(self, component_dict = None):
 		account_dict = {}
 		for key, amount in component_dict.items():
diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
index c6f3897..4f097fa 100644
--- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
@@ -120,8 +120,7 @@
 
 		employee1 = make_employee("test_employee1@example.com", payroll_cost_center="_Test Cost Center - _TC",
 			department="cc - _TC", company="_Test Company")
-		employee2 = make_employee("test_employee2@example.com", payroll_cost_center="_Test Cost Center 2 - _TC",
-			department="cc - _TC", company="_Test Company")
+		employee2 = make_employee("test_employee2@example.com", department="cc - _TC", company="_Test Company")
 
 		if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"):
 				create_account(account_name="_Test Payroll Payable",
@@ -132,8 +131,26 @@
 				frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account",
 					"_Test Payroll Payable - _TC")
 		currency=frappe.db.get_value("Company", "_Test Company", "default_currency")
+
 		make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False)
-		make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False)
+		ss = make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False)
+
+		# update cost centers in salary structure assignment for employee2
+		ssa = frappe.db.get_value("Salary Structure Assignment",
+			{"employee": employee2, "salary_structure": ss.name, "docstatus": 1}, 'name')
+
+		ssa_doc = frappe.get_doc("Salary Structure Assignment", ssa)
+		ssa_doc.payroll_cost_centers = []
+		ssa_doc.append("payroll_cost_centers", {
+			"cost_center": "_Test Cost Center - _TC",
+			"percentage": 60
+		})
+		ssa_doc.append("payroll_cost_centers", {
+			"cost_center": "_Test Cost Center 2 - _TC",
+			"percentage": 40
+		})
+
+		ssa_doc.save()
 
 		dates = get_start_end_dates('Monthly', nowdate())
 		if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}):
@@ -148,10 +165,10 @@
 			""", je)
 			expected_je = (
 				('_Test Payroll Payable - _TC', 'Main - _TC', 0.0, 155600.0),
-				('Salary - _TC', '_Test Cost Center - _TC', 78000.0, 0.0),
-				('Salary - _TC', '_Test Cost Center 2 - _TC', 78000.0, 0.0),
-				('Salary Deductions - _TC', '_Test Cost Center - _TC', 0.0, 200.0),
-				('Salary Deductions - _TC', '_Test Cost Center 2 - _TC', 0.0, 200.0)
+				('Salary - _TC', '_Test Cost Center - _TC', 124800.0, 0.0),
+				('Salary - _TC', '_Test Cost Center 2 - _TC', 31200.0, 0.0),
+				('Salary Deductions - _TC', '_Test Cost Center - _TC', 0.0, 320.0),
+				('Salary Deductions - _TC', '_Test Cost Center 2 - _TC', 0.0, 80.0)
 			)
 
 			self.assertEqual(je_entries, expected_je)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json
index 7a80e69..4e40e13 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.json
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json
@@ -12,7 +12,6 @@
   "department",
   "designation",
   "branch",
-  "payroll_cost_center",
   "column_break1",
   "status",
   "journal_entry",
@@ -463,15 +462,6 @@
    "read_only": 1
   },
   {
-   "fetch_from": "employee.payroll_cost_center",
-   "fetch_if_empty": 1,
-   "fieldname": "payroll_cost_center",
-   "fieldtype": "Link",
-   "label": "Payroll Cost Center",
-   "options": "Cost Center",
-   "read_only": 1
-  },
-  {
    "fieldname": "mode_of_payment",
    "fieldtype": "Select",
    "label": "Mode Of Payment",
@@ -647,7 +637,7 @@
  "idx": 9,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-10-08 11:47:47.098248",
+ "modified": "2021-12-23 11:47:47.098248",
  "modified_by": "Administrator",
  "module": "Payroll",
  "name": "Salary Slip",
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py
index ae83c04..4cbf948 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py
@@ -167,15 +167,12 @@
 	def postprocess(source, target):
 		if employee:
 			employee_details = frappe.db.get_value("Employee", employee,
-				["employee_name", "branch", "designation", "department", "payroll_cost_center"], as_dict=1)
+				["employee_name", "branch", "designation", "department"], as_dict=1)
 			target.employee = employee
 			target.employee_name = employee_details.employee_name
 			target.branch = employee_details.branch
 			target.designation = employee_details.designation
 			target.department = employee_details.department
-			target.payroll_cost_center = employee_details.payroll_cost_center
-			if not target.payroll_cost_center and target.department:
-				target.payroll_cost_center = frappe.db.get_value("Department", target.department, "payroll_cost_center")
 
 		target.run_method('process_salary_structure', for_preview=for_preview)
 
diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js
index 6cd897e..220bfbf 100644
--- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js
+++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js
@@ -40,28 +40,29 @@
 				}
 			}
 		});
+
+		frm.set_query("cost_center", "payroll_cost_centers", function() {
+			return {
+				filters: {
+					"company": frm.doc.company,
+					"is_group": 0
+				}
+			};
+		});
 	},
 
 	employee: function(frm) {
-		if(frm.doc.employee){
+		if (frm.doc.employee) {
 			frappe.call({
-				method: "frappe.client.get_value",
-				args:{
-					doctype: "Employee",
-					fieldname: "company",
-					filters:{
-						name: frm.doc.employee
-					}
-				},
+				method: "set_payroll_cost_centers",
+				doc: frm.doc,
 				callback: function(data) {
-					if(data.message){
-						frm.set_value("company", data.message.company);
-					}
+					refresh_field("payroll_cost_centers");
 				}
 			});
 		}
-		else{
-			frm.set_value("company", null);
+		else {
+			frm.set_value("payroll_cost_centers", []);
 		}
 	},
 
diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
index c8b98e5..197ab5f 100644
--- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
+++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
@@ -22,7 +22,9 @@
   "base",
   "column_break_9",
   "variable",
-  "amended_from"
+  "amended_from",
+  "section_break_17",
+  "payroll_cost_centers"
  ],
  "fields": [
   {
@@ -90,7 +92,8 @@
   },
   {
    "fieldname": "section_break_7",
-   "fieldtype": "Section Break"
+   "fieldtype": "Section Break",
+   "label": "Base & Variable"
   },
   {
    "fieldname": "base",
@@ -141,14 +144,29 @@
    "fieldtype": "Link",
    "label": "Payroll Payable Account",
    "options": "Account"
+  },
+  {
+   "collapsible": 1,
+   "depends_on": "employee",
+   "fieldname": "section_break_17",
+   "fieldtype": "Section Break",
+   "label": "Payroll Cost Centers"
+  },
+  {
+   "allow_on_submit": 1,
+   "fieldname": "payroll_cost_centers",
+   "fieldtype": "Table",
+   "label": "Cost Centers",
+   "options": "Employee Cost Center"
   }
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2021-03-31 22:44:46.267974",
+ "modified": "2021-12-23 17:28:09.794444",
  "modified_by": "Administrator",
  "module": "Payroll",
  "name": "Salary Structure Assignment",
+ "naming_rule": "Expression (old style)",
  "owner": "Administrator",
  "permissions": [
   {
@@ -193,6 +211,7 @@
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "title_field": "employee_name",
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py
index e1ff9ca..8359478 100644
--- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py
+++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py
@@ -5,7 +5,7 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
-from frappe.utils import getdate
+from frappe.utils import flt, getdate
 
 
 class DuplicateAssignment(frappe.ValidationError): pass
@@ -15,6 +15,10 @@
 		self.validate_dates()
 		self.validate_income_tax_slab()
 		self.set_payroll_payable_account()
+		if not self.get("payroll_cost_centers"):
+			self.set_payroll_cost_centers()
+
+		self.validate_cost_center_distribution()
 
 	def validate_dates(self):
 		joining_date, relieving_date = frappe.db.get_value("Employee", self.employee,
@@ -51,6 +55,30 @@
 							"Company", self.company, "default_currency"), "is_group": 0})
 			self.payroll_payable_account = payroll_payable_account
 
+	@frappe.whitelist()
+	def set_payroll_cost_centers(self):
+		self.payroll_cost_centers = []
+		default_payroll_cost_center = self.get_payroll_cost_center()
+		if default_payroll_cost_center:
+			self.append("payroll_cost_centers", {
+				"cost_center": default_payroll_cost_center,
+				"percentage": 100
+			})
+
+	def get_payroll_cost_center(self):
+		payroll_cost_center = frappe.db.get_value("Employee", self.employee, "payroll_cost_center")
+		if not payroll_cost_center and self.department:
+			payroll_cost_center = frappe.db.get_value("Department", self.department, "payroll_cost_center")
+
+		return payroll_cost_center
+
+	def validate_cost_center_distribution(self):
+		if self.get("payroll_cost_centers"):
+			total_percentage = sum([flt(d.percentage) for d in self.get("payroll_cost_centers", [])])
+			if total_percentage != 100:
+				frappe.throw(_("Total percentage against cost centers should be 100"))
+
+
 def get_assigned_salary_structure(employee, on_date):
 	if not employee or not on_date:
 		return None
@@ -64,6 +92,7 @@
 		})
 	return salary_structure[0][0] if salary_structure else None
 
+
 @frappe.whitelist()
 def get_employee_currency(employee):
 	employee_currency = frappe.db.get_value('Salary Structure Assignment', {'employee': employee}, 'currency')