Merge branch 'develop' into e-commerce-refactor-develop
diff --git a/erpnext/accounts/doctype/cost_center/cost_center.js b/erpnext/accounts/doctype/cost_center/cost_center.js
index ee23b1b..632fab0 100644
--- a/erpnext/accounts/doctype/cost_center/cost_center.js
+++ b/erpnext/accounts/doctype/cost_center/cost_center.js
@@ -15,17 +15,6 @@
 				}
 			}
 		});
-
-		frm.set_query("cost_center", "distributed_cost_center", function() {
-			return {
-				filters: {
-					company: frm.doc.company,
-					is_group: 0,
-					enable_distributed_cost_center: 0,
-					name: ['!=', frm.doc.name]
-				}
-			};
-		});
 	},
 	refresh: function(frm) {
 		if (!frm.is_new()) {
diff --git a/erpnext/accounts/doctype/cost_center/cost_center.json b/erpnext/accounts/doctype/cost_center/cost_center.json
index e7fa954..7cbb290 100644
--- a/erpnext/accounts/doctype/cost_center/cost_center.json
+++ b/erpnext/accounts/doctype/cost_center/cost_center.json
@@ -16,9 +16,6 @@
   "cb0",
   "is_group",
   "disabled",
-  "section_break_9",
-  "enable_distributed_cost_center",
-  "distributed_cost_center",
   "lft",
   "rgt",
   "old_parent"
@@ -122,31 +119,13 @@
    "fieldname": "disabled",
    "fieldtype": "Check",
    "label": "Disabled"
-  },
-  {
-   "default": "0",
-   "fieldname": "enable_distributed_cost_center",
-   "fieldtype": "Check",
-   "label": "Enable Distributed Cost Center"
-  },
-  {
-   "depends_on": "eval:doc.is_group==0",
-   "fieldname": "section_break_9",
-   "fieldtype": "Section Break"
-  },
-  {
-   "depends_on": "enable_distributed_cost_center",
-   "fieldname": "distributed_cost_center",
-   "fieldtype": "Table",
-   "label": "Distributed Cost Center",
-   "options": "Distributed Cost Center"
   }
  ],
  "icon": "fa fa-money",
  "idx": 1,
  "is_tree": 1,
  "links": [],
- "modified": "2020-06-17 16:09:30.025214",
+ "modified": "2022-01-31 13:22:58.916273",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Cost Center",
@@ -189,5 +168,6 @@
  "search_fields": "parent_cost_center, is_group",
  "show_name_in_global_search": 1,
  "sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py
index 7ae0a72..07cc076 100644
--- a/erpnext/accounts/doctype/cost_center/cost_center.py
+++ b/erpnext/accounts/doctype/cost_center/cost_center.py
@@ -4,7 +4,6 @@
 
 import frappe
 from frappe import _
-from frappe.utils import cint
 from frappe.utils.nestedset import NestedSet
 
 from erpnext.accounts.utils import validate_field_number
@@ -20,24 +19,6 @@
 	def validate(self):
 		self.validate_mandatory()
 		self.validate_parent_cost_center()
-		self.validate_distributed_cost_center()
-
-	def validate_distributed_cost_center(self):
-		if cint(self.enable_distributed_cost_center):
-			if not self.distributed_cost_center:
-				frappe.throw(_("Please enter distributed cost center"))
-			if sum(x.percentage_allocation for x in self.distributed_cost_center) != 100:
-				frappe.throw(_("Total percentage allocation for distributed cost center should be equal to 100"))
-			if not self.get('__islocal'):
-				if not cint(frappe.get_cached_value("Cost Center", {"name": self.name}, "enable_distributed_cost_center")) \
-					and self.check_if_part_of_distributed_cost_center():
-					frappe.throw(_("Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center"))
-				if next((True for x in self.distributed_cost_center if x.cost_center == x.parent), False):
-					frappe.throw(_("Parent Cost Center cannot be added in Distributed Cost Center"))
-			if check_if_distributed_cost_center_enabled(list(x.cost_center for x in self.distributed_cost_center)):
-				frappe.throw(_("A Distributed Cost Center cannot be added in the Distributed Cost Center allocation table."))
-		else:
-			self.distributed_cost_center = []
 
 	def validate_mandatory(self):
 		if self.cost_center_name != self.company and not self.parent_cost_center:
@@ -64,10 +45,10 @@
 
 	@frappe.whitelist()
 	def convert_ledger_to_group(self):
-		if cint(self.enable_distributed_cost_center):
-			frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group"))
-		if self.check_if_part_of_distributed_cost_center():
-			frappe.throw(_("Cost Center Already Allocated in a Distributed Cost Center cannot be converted to group"))
+		if self.if_allocation_exists_against_cost_center():
+			frappe.throw(_("Cost Center with Allocation records can not be converted to a group"))
+		if self.check_if_part_of_cost_center_allocation():
+			frappe.throw(_("Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group"))
 		if self.check_gle_exists():
 			frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
 		self.is_group = 1
@@ -81,8 +62,17 @@
 		return frappe.db.sql("select name from `tabCost Center` where \
 			parent_cost_center = %s and docstatus != 2", self.name)
 
-	def check_if_part_of_distributed_cost_center(self):
-		return frappe.db.get_value("Distributed Cost Center", {"cost_center": self.name})
+	def if_allocation_exists_against_cost_center(self):
+		return frappe.db.get_value("Cost Center Allocation", filters = {
+			"main_cost_center": self.name,
+			"docstatus": 1
+		})
+
+	def check_if_part_of_cost_center_allocation(self):
+		return frappe.db.get_value("Cost Center Allocation Percentage", filters = {
+			"cost_center": self.name,
+			"docstatus": 1
+		})
 
 	def before_rename(self, olddn, newdn, merge=False):
 		# Add company abbr if not provided
@@ -126,8 +116,4 @@
 def get_name_with_number(new_account, account_number):
 	if account_number and not new_account[0].isdigit():
 		new_account = account_number + " - " + new_account
-	return new_account
-
-def check_if_distributed_cost_center_enabled(cost_center_list):
-	value_list = frappe.get_list("Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1)
-	return next((True for x in value_list if x[0]), False)
+	return new_account
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/cost_center/test_cost_center.py b/erpnext/accounts/doctype/cost_center/test_cost_center.py
index f8615ec..ff50a21 100644
--- a/erpnext/accounts/doctype/cost_center/test_cost_center.py
+++ b/erpnext/accounts/doctype/cost_center/test_cost_center.py
@@ -23,33 +23,6 @@
 
 		self.assertRaises(frappe.ValidationError, cost_center.save)
 
-	def test_validate_distributed_cost_center(self):
-
-		if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center - _TC'}):
-			frappe.get_doc(test_records[0]).insert()
-
-		if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}):
-			frappe.get_doc(test_records[1]).insert()
-
-		invalid_distributed_cost_center = frappe.get_doc({
-			"company": "_Test Company",
-			"cost_center_name": "_Test Distributed Cost Center",
-			"doctype": "Cost Center",
-			"is_group": 0,
-			"parent_cost_center": "_Test Company - _TC",
-			"enable_distributed_cost_center": 1,
-			"distributed_cost_center": [{
-				"cost_center": "_Test Cost Center - _TC",
-				"percentage_allocation": 40
-				}, {
-				"cost_center": "_Test Cost Center 2 - _TC",
-				"percentage_allocation": 50
-				}
-			]
-		})
-
-		self.assertRaises(frappe.ValidationError, invalid_distributed_cost_center.save)
-
 def create_cost_center(**args):
 	args = frappe._dict(args)
 	if args.cost_center_name:
diff --git a/erpnext/accounts/doctype/distributed_cost_center/__init__.py b/erpnext/accounts/doctype/cost_center_allocation/__init__.py
similarity index 100%
rename from erpnext/accounts/doctype/distributed_cost_center/__init__.py
rename to erpnext/accounts/doctype/cost_center_allocation/__init__.py
diff --git a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js
new file mode 100644
index 0000000..ab0baab
--- /dev/null
+++ b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.js
@@ -0,0 +1,19 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Cost Center Allocation', {
+	setup: function(frm) {
+		let filters = {"is_group": 0};
+		if (frm.doc.company) {
+			$.extend(filters, {
+				"company": frm.doc.company
+			});
+		}
+
+		frm.set_query('main_cost_center', function() {
+			return {
+				filters: filters
+			};
+		});
+	}
+});
diff --git a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.json b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.json
new file mode 100644
index 0000000..45ab886
--- /dev/null
+++ b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.json
@@ -0,0 +1,128 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "CC-ALLOC-.#####",
+ "creation": "2022-01-13 20:07:29.871109",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "main_cost_center",
+  "company",
+  "column_break_2",
+  "valid_from",
+  "section_break_5",
+  "allocation_percentages",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "fieldname": "main_cost_center",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Main Cost Center",
+   "options": "Cost Center",
+   "reqd": 1
+  },
+  {
+   "default": "Today",
+   "fieldname": "valid_from",
+   "fieldtype": "Date",
+   "in_list_view": 1,
+   "label": "Valid From",
+   "reqd": 1
+  },
+  {
+   "fieldname": "column_break_2",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_5",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fetch_from": "main_cost_center.company",
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "label": "Company",
+   "options": "Company",
+   "reqd": 1
+  },
+  {
+   "fieldname": "allocation_percentages",
+   "fieldtype": "Table",
+   "label": "Cost Center Allocation Percentages",
+   "options": "Cost Center Allocation Percentage",
+   "reqd": 1
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Cost Center Allocation",
+   "print_hide": 1,
+   "read_only": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2022-01-31 11:47:12.086253",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Cost Center Allocation",
+ "name_case": "UPPER CASE",
+ "naming_rule": "Expression (old style)",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "amend": 1,
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  },
+  {
+   "amend": 1,
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Accounts Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  },
+  {
+   "amend": 1,
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Accounts User",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py
new file mode 100644
index 0000000..bad3fb4
--- /dev/null
+++ b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py
@@ -0,0 +1,90 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import add_days, format_date, getdate
+
+
+class MainCostCenterCantBeChild(frappe.ValidationError):
+	pass
+class InvalidMainCostCenter(frappe.ValidationError):
+	pass
+class InvalidChildCostCenter(frappe.ValidationError):
+	pass
+class WrongPercentageAllocation(frappe.ValidationError):
+	pass
+class InvalidDateError(frappe.ValidationError):
+	pass
+
+class CostCenterAllocation(Document):
+	def validate(self):
+		self.validate_total_allocation_percentage()
+		self.validate_from_date_based_on_existing_gle()
+		self.validate_backdated_allocation()
+		self.validate_main_cost_center()
+		self.validate_child_cost_centers()
+
+	def validate_total_allocation_percentage(self):
+		total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
+
+		if total_percentage != 100:
+			frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
+
+	def validate_from_date_based_on_existing_gle(self):
+		# Check if GLE exists against the main cost center
+		# If exists ensure from date is set after posting date of last GLE
+
+		last_gle_date = frappe.db.get_value("GL Entry",
+			{"cost_center": self.main_cost_center, "is_cancelled": 0},
+			"posting_date", order_by="posting_date desc")
+
+		if last_gle_date:
+			if getdate(self.valid_from) <= getdate(last_gle_date):
+				frappe.throw(_("Valid From must be after {0} as last GL Entry against the cost center {1} posted on this date")
+					.format(last_gle_date, self.main_cost_center), InvalidDateError)
+
+	def validate_backdated_allocation(self):
+		# Check if there are any future existing allocation records against the main cost center
+		# If exists, warn the user about it
+
+		future_allocation = frappe.db.get_value("Cost Center Allocation", filters = {
+			"main_cost_center": self.main_cost_center,
+			"valid_from": (">=", self.valid_from),
+			"name": ("!=", self.name),
+			"docstatus": 1
+		}, fieldname=['valid_from', 'name'], order_by='valid_from', as_dict=1)
+
+		if future_allocation:
+			frappe.msgprint(_("Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}")
+				.format(frappe.bold(future_allocation.name), frappe.bold(format_date(future_allocation.valid_from)),
+				frappe.bold(format_date(add_days(future_allocation.valid_from, -1)))),
+				title=_("Warning!"), indicator="orange", alert=1
+			)
+
+	def validate_main_cost_center(self):
+		# Main cost center itself cannot be entered in child table
+		if self.main_cost_center in [d.cost_center for d in self.allocation_percentages]:
+			frappe.throw(_("Main Cost Center {0} cannot be entered in the child table")
+				.format(self.main_cost_center), MainCostCenterCantBeChild)
+
+		# If main cost center is used for allocation under any other cost center,
+		# allocation cannot be done against it
+		parent = frappe.db.get_value("Cost Center Allocation Percentage", filters = {
+			"cost_center": self.main_cost_center,
+			"docstatus": 1
+		}, fieldname='parent')
+		if parent:
+			frappe.throw(_("{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}")
+				.format(self.main_cost_center, parent), InvalidMainCostCenter)
+
+	def validate_child_cost_centers(self):
+		# Check if child cost center is used as main cost center in any existing allocation
+		main_cost_centers = [d.main_cost_center for d in
+			frappe.get_all("Cost Center Allocation", {'docstatus': 1}, 'main_cost_center')]
+
+		for d in self.allocation_percentages:
+			if d.cost_center in main_cost_centers:
+				frappe.throw(_("Cost Center {0} cannot be used for allocation as it is used as main cost center in other allocation record.")
+					.format(d.cost_center), InvalidChildCostCenter)
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py b/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py
new file mode 100644
index 0000000..9cf4c00
--- /dev/null
+++ b/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py
@@ -0,0 +1,156 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import unittest
+
+import frappe
+from frappe.utils import add_days, today
+
+from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+from erpnext.accounts.doctype.cost_center_allocation.cost_center_allocation import (
+	InvalidChildCostCenter,
+	InvalidDateError,
+	InvalidMainCostCenter,
+	MainCostCenterCantBeChild,
+	WrongPercentageAllocation,
+)
+from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
+
+
+class TestCostCenterAllocation(unittest.TestCase):
+	def setUp(self):
+		cost_centers = ["Main Cost Center 1", "Main Cost Center 2", "Sub Cost Center 1", "Sub Cost Center 2"]
+		for cc in cost_centers:
+			create_cost_center(cost_center_name=cc, company="_Test Company")
+
+	def test_gle_based_on_cost_center_allocation(self):
+		cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
+			{
+				"Sub Cost Center 1 - _TC": 60,
+				"Sub Cost Center 2 - _TC": 40
+			}
+		)
+
+		jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100,
+			cost_center = "Main Cost Center 1 - _TC", submit=True)
+
+		expected_values = [
+			["Sub Cost Center 1 - _TC", 0.0, 60],
+			["Sub Cost Center 2 - _TC", 0.0, 40]
+		]
+
+		gle = frappe.qb.DocType("GL Entry")
+		gl_entries = (
+			frappe.qb.from_(gle)
+			.select(gle.cost_center, gle.debit, gle.credit)
+			.where(gle.voucher_type == 'Journal Entry')
+			.where(gle.voucher_no == jv.name)
+			.where(gle.account == 'Sales - _TC')
+			.orderby(gle.cost_center)
+		).run(as_dict=1)
+
+		self.assertTrue(gl_entries)
+
+		for i, gle in enumerate(gl_entries):
+			self.assertEqual(expected_values[i][0], gle.cost_center)
+			self.assertEqual(expected_values[i][1], gle.debit)
+			self.assertEqual(expected_values[i][2], gle.credit)
+
+		cca.cancel()
+		jv.cancel()
+
+	def test_main_cost_center_cant_be_child(self):
+		# Main cost center itself cannot be entered in child table
+		cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
+			{
+				"Sub Cost Center 1 - _TC": 60,
+				"Main Cost Center 1 - _TC": 40
+			}, save=False
+		)
+
+		self.assertRaises(MainCostCenterCantBeChild, cca.save)
+
+	def test_invalid_main_cost_center(self):
+		# If main cost center is used for allocation under any other cost center,
+		# allocation cannot be done against it
+		cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
+			{
+				"Sub Cost Center 1 - _TC": 60,
+				"Sub Cost Center 2 - _TC": 40
+			}
+		)
+
+		cca2 = create_cost_center_allocation("_Test Company", "Sub Cost Center 1 - _TC",
+			{
+				"Sub Cost Center 2 - _TC": 100
+			}, save=False
+		)
+
+		self.assertRaises(InvalidMainCostCenter, cca2.save)
+
+		cca1.cancel()
+
+	def test_if_child_cost_center_has_any_allocation_record(self):
+		# Check if any child cost center is used as main cost center in any other existing allocation
+		cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
+			{
+				"Sub Cost Center 1 - _TC": 60,
+				"Sub Cost Center 2 - _TC": 40
+			}
+		)
+
+		cca2 = create_cost_center_allocation("_Test Company", "Main Cost Center 2 - _TC",
+			{
+				"Main Cost Center 1 - _TC": 60,
+				"Sub Cost Center 1 - _TC": 40
+			}, save=False
+		)
+
+		self.assertRaises(InvalidChildCostCenter, cca2.save)
+
+		cca1.cancel()
+
+	def test_total_percentage(self):
+		cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
+			{
+				"Sub Cost Center 1 - _TC": 40,
+				"Sub Cost Center 2 - _TC": 40
+			}, save=False
+		)
+		self.assertRaises(WrongPercentageAllocation, cca.save)
+
+	def test_valid_from_based_on_existing_gle(self):
+		# GLE posted against Sub Cost Center 1 on today
+		jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100,
+			cost_center = "Main Cost Center 1 - _TC", posting_date=today(), submit=True)
+
+		# try to set valid from as yesterday
+		cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
+			{
+				"Sub Cost Center 1 - _TC": 60,
+				"Sub Cost Center 2 - _TC": 40
+			}, valid_from=add_days(today(), -1), save=False
+		)
+
+		self.assertRaises(InvalidDateError, cca.save)
+
+		jv.cancel()
+
+def create_cost_center_allocation(company, main_cost_center, allocation_percentages,
+		valid_from=None, valid_upto=None, save=True, submit=True):
+	doc = frappe.new_doc("Cost Center Allocation")
+	doc.main_cost_center = main_cost_center
+	doc.company = company
+	doc.valid_from = valid_from or today()
+	doc.valid_upto = valid_upto
+	for cc, percentage in allocation_percentages.items():
+		doc.append("allocation_percentages", {
+			"cost_center": cc,
+			"percentage": percentage
+		})
+	if save:
+		doc.save()
+		if submit:
+			doc.submit()
+
+	return doc
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/distributed_cost_center/__init__.py b/erpnext/accounts/doctype/cost_center_allocation_percentage/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/distributed_cost_center/__init__.py
copy to erpnext/accounts/doctype/cost_center_allocation_percentage/__init__.py
diff --git a/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.json b/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.json
similarity index 63%
rename from erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.json
rename to erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.json
index 45b0e2d..4b871ae 100644
--- a/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.json
+++ b/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.json
@@ -1,12 +1,13 @@
 {
  "actions": [],
- "creation": "2020-03-19 12:34:01.500390",
+ "allow_rename": 1,
+ "creation": "2022-01-03 18:10:11.697198",
  "doctype": "DocType",
  "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
   "cost_center",
-  "percentage_allocation"
+  "percentage"
  ],
  "fields": [
   {
@@ -18,23 +19,23 @@
    "reqd": 1
   },
   {
-   "fieldname": "percentage_allocation",
-   "fieldtype": "Float",
+   "fieldname": "percentage",
+   "fieldtype": "Int",
    "in_list_view": 1,
-   "label": "Percentage Allocation",
+   "label": "Percentage (%)",
    "reqd": 1
   }
  ],
+ "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2020-03-19 12:54:43.674655",
+ "modified": "2022-01-03 18:10:20.029821",
  "modified_by": "Administrator",
  "module": "Accounts",
- "name": "Distributed Cost Center",
+ "name": "Cost Center Allocation Percentage",
  "owner": "Administrator",
  "permissions": [],
- "quick_entry": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
- "track_changes": 1
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.py b/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.py
new file mode 100644
index 0000000..7d20efb
--- /dev/null
+++ b/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CostCenterAllocationPercentage(Document):
+	pass
diff --git a/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.py b/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.py
deleted file mode 100644
index dcf0e3b..0000000
--- a/erpnext/accounts/doctype/distributed_cost_center/distributed_cost_center.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-# import frappe
-from frappe.model.document import Document
-
-
-class DistributedCostCenter(Document):
-	pass
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 1836db6..55bc967 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -2,6 +2,8 @@
 # License: GNU General Public License v3. See license.txt
 
 
+import copy
+
 import frappe
 from frappe import _
 from frappe.model.meta import get_field_precision
@@ -51,49 +53,57 @@
 			.format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod)
 
 def process_gl_map(gl_map, merge_entries=True, precision=None):
+	if not gl_map:
+		return []
+
+	gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
+
 	if merge_entries:
 		gl_map = merge_similar_entries(gl_map, precision)
-	for entry in gl_map:
-		# toggle debit, credit if negative entry
-		if flt(entry.debit) < 0:
-			entry.credit = flt(entry.credit) - flt(entry.debit)
-			entry.debit = 0.0
 
-		if flt(entry.debit_in_account_currency) < 0:
-			entry.credit_in_account_currency = \
-				flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency)
-			entry.debit_in_account_currency = 0.0
-
-		if flt(entry.credit) < 0:
-			entry.debit = flt(entry.debit) - flt(entry.credit)
-			entry.credit = 0.0
-
-		if flt(entry.credit_in_account_currency) < 0:
-			entry.debit_in_account_currency = \
-				flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
-			entry.credit_in_account_currency = 0.0
-
-		update_net_values(entry)
+	gl_map = toggle_debit_credit_if_negative(gl_map)
 
 	return gl_map
 
-def update_net_values(entry):
-	# In some scenarios net value needs to be shown in the ledger
-	# This method updates net values as debit or credit
-	if entry.post_net_value and entry.debit and entry.credit:
-		if entry.debit > entry.credit:
-			entry.debit = entry.debit - entry.credit
-			entry.debit_in_account_currency = entry.debit_in_account_currency \
-				- entry.credit_in_account_currency
-			entry.credit = 0
-			entry.credit_in_account_currency = 0
-		else:
-			entry.credit = entry.credit - entry.debit
-			entry.credit_in_account_currency = entry.credit_in_account_currency \
-				- entry.debit_in_account_currency
+def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
+	cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"])
+	if not cost_center_allocation:
+		return gl_map
 
-			entry.debit = 0
-			entry.debit_in_account_currency = 0
+	new_gl_map = []
+	for d in gl_map:
+		cost_center = d.get("cost_center")
+		if cost_center and cost_center_allocation.get(cost_center):
+			for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
+				gle = copy.deepcopy(d)
+				gle.cost_center = sub_cost_center
+				for field in ("debit", "credit", "debit_in_account_currency", "credit_in_company_currency"):
+					gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
+				new_gl_map.append(gle)
+		else:
+			new_gl_map.append(d)
+
+	return new_gl_map
+
+def get_cost_center_allocation_data(company, posting_date):
+	par = frappe.qb.DocType("Cost Center Allocation")
+	child = frappe.qb.DocType("Cost Center Allocation Percentage")
+
+	records = (
+		frappe.qb.from_(par).inner_join(child).on(par.name == child.parent)
+		.select(par.main_cost_center, child.cost_center, child.percentage)
+		.where(par.docstatus == 1)
+		.where(par.company == company)
+		.where(par.valid_from <= posting_date)
+		.orderby(par.valid_from, order=frappe.qb.desc)
+	).run(as_dict=True)
+
+	cc_allocation = frappe._dict()
+	for d in records:
+		cc_allocation.setdefault(d.main_cost_center, frappe._dict())\
+			.setdefault(d.cost_center, d.percentage)
+
+	return cc_allocation
 
 def merge_similar_entries(gl_map, precision=None):
 	merged_gl_map = []
@@ -145,6 +155,49 @@
 		if same_head:
 			return e
 
+def toggle_debit_credit_if_negative(gl_map):
+	for entry in gl_map:
+		# toggle debit, credit if negative entry
+		if flt(entry.debit) < 0:
+			entry.credit = flt(entry.credit) - flt(entry.debit)
+			entry.debit = 0.0
+
+		if flt(entry.debit_in_account_currency) < 0:
+			entry.credit_in_account_currency = \
+				flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency)
+			entry.debit_in_account_currency = 0.0
+
+		if flt(entry.credit) < 0:
+			entry.debit = flt(entry.debit) - flt(entry.credit)
+			entry.credit = 0.0
+
+		if flt(entry.credit_in_account_currency) < 0:
+			entry.debit_in_account_currency = \
+				flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
+			entry.credit_in_account_currency = 0.0
+
+		update_net_values(entry)
+
+	return gl_map
+
+def update_net_values(entry):
+	# In some scenarios net value needs to be shown in the ledger
+	# This method updates net values as debit or credit
+	if entry.post_net_value and entry.debit and entry.credit:
+		if entry.debit > entry.credit:
+			entry.debit = entry.debit - entry.credit
+			entry.debit_in_account_currency = entry.debit_in_account_currency \
+				- entry.credit_in_account_currency
+			entry.credit = 0
+			entry.credit_in_account_currency = 0
+		else:
+			entry.credit = entry.credit - entry.debit
+			entry.credit_in_account_currency = entry.credit_in_account_currency \
+				- entry.debit_in_account_currency
+
+			entry.debit = 0
+			entry.debit_in_account_currency = 0
+
 def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
 	if not from_repost:
 		validate_cwip_accounts(gl_map)
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
index 3bb590a..56ee500 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
@@ -29,18 +29,6 @@
 		dimension_items = cam_map.get(dimension)
 		if dimension_items:
 			data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0)
-		else:
-			DCC_allocation = frappe.db.sql('''SELECT parent, sum(percentage_allocation) as percentage_allocation
-				FROM `tabDistributed Cost Center`
-				WHERE cost_center IN %(dimension)s
-				AND parent NOT IN %(dimension)s
-				GROUP BY parent''',{'dimension':[dimension]})
-			if DCC_allocation:
-				filters['budget_against_filter'] = [DCC_allocation[0][0]]
-				ddc_cam_map = get_dimension_account_month_map(filters)
-				dimension_items = ddc_cam_map.get(DCC_allocation[0][0])
-				if dimension_items:
-					data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1])
 
 	chart = get_chart_data(filters, columns, data)
 
diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py
index 1e89b65..03ae0ae 100644
--- a/erpnext/accounts/report/financial_statements.py
+++ b/erpnext/accounts/report/financial_statements.py
@@ -387,42 +387,15 @@
 					key: value
 				})
 
-		distributed_cost_center_query = ""
-		if filters and filters.get('cost_center'):
-			distributed_cost_center_query = """
-			UNION ALL
-			SELECT posting_date,
-				account,
-				debit*(DCC_allocation.percentage_allocation/100) as debit,
-				credit*(DCC_allocation.percentage_allocation/100) as credit,
-				is_opening,
-				fiscal_year,
-				debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
-				credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency,
-				account_currency
-			FROM `tabGL Entry`,
-			(
-				SELECT parent, sum(percentage_allocation) as percentage_allocation
-				FROM `tabDistributed Cost Center`
-				WHERE cost_center IN %(cost_center)s
-				AND parent NOT IN %(cost_center)s
-				GROUP BY parent
-			) as DCC_allocation
-			WHERE company=%(company)s
-			{additional_conditions}
-			AND posting_date <= %(to_date)s
-			AND is_cancelled = 0
-			AND cost_center = DCC_allocation.parent
-			""".format(additional_conditions=additional_conditions.replace("and cost_center in %(cost_center)s ", ''))
-
-		gl_entries = frappe.db.sql("""select posting_date, account, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
+		gl_entries = frappe.db.sql("""
+			select posting_date, account, debit, credit, is_opening, fiscal_year,
+				debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
 			where company=%(company)s
 			{additional_conditions}
 			and posting_date <= %(to_date)s
-			and is_cancelled = 0
-			{distributed_cost_center_query}""".format(
-				additional_conditions=additional_conditions,
-				distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec
+			and is_cancelled = 0""".format(
+			additional_conditions=additional_conditions), gl_filters, as_dict=True
+		)
 
 		if filters and filters.get('presentation_currency'):
 			convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get('company'))
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 7f27920..4ff0297 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -176,44 +176,7 @@
 	if accounting_dimensions:
 		dimension_fields = ', '.join(accounting_dimensions) + ','
 
-	distributed_cost_center_query = ""
-	if filters and filters.get('cost_center'):
-		select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit,
-		credit*(DCC_allocation.percentage_allocation/100) as credit,
-		debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
-		credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """
-
-		distributed_cost_center_query = """
-		UNION ALL
-		SELECT name as gl_entry,
-			posting_date,
-			account,
-			party_type,
-			party,
-			voucher_type,
-			voucher_no, {dimension_fields}
-			cost_center, project,
-			against_voucher_type,
-			against_voucher,
-			account_currency,
-			remarks, against,
-			is_opening, `tabGL Entry`.creation {select_fields_with_percentage}
-		FROM `tabGL Entry`,
-		(
-			SELECT parent, sum(percentage_allocation) as percentage_allocation
-			FROM `tabDistributed Cost Center`
-			WHERE cost_center IN %(cost_center)s
-			AND parent NOT IN %(cost_center)s
-			GROUP BY parent
-		) as DCC_allocation
-		WHERE company=%(company)s
-		{conditions}
-		AND posting_date <= %(to_date)s
-		AND cost_center = DCC_allocation.parent
-		""".format(dimension_fields=dimension_fields,select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", ''))
-
-	gl_entries = frappe.db.sql(
-		"""
+	gl_entries = frappe.db.sql("""
 		select
 			name as gl_entry, posting_date, account, party_type, party,
 			voucher_type, voucher_no, {dimension_fields}
@@ -222,13 +185,11 @@
 			remarks, against, is_opening, creation {select_fields}
 		from `tabGL Entry`
 		where company=%(company)s {conditions}
-		{distributed_cost_center_query}
 		{order_by_statement}
-		""".format(
-			dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query,
-			order_by_statement=order_by_statement
-		),
-		filters, as_dict=1)
+	""".format(
+		dimension_fields=dimension_fields, select_fields=select_fields,
+		conditions=get_conditions(filters), order_by_statement=order_by_statement
+	), filters, as_dict=1)
 
 	if filters.get('presentation_currency'):
 		return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company'))
diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
index 3dcb862..f4b8731 100644
--- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
+++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
@@ -109,7 +109,6 @@
 
 def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
 	data = []
-	new_accounts = accounts
 	company_currency = frappe.get_cached_value('Company',  filters.get("company"),  "default_currency")
 
 	for d in accounts:
@@ -123,19 +122,6 @@
 			"currency": company_currency,
 			"based_on": based_on
 		}
-		if based_on == 'cost_center':
-			cost_center_doc = frappe.get_doc("Cost Center",d.name)
-			if not cost_center_doc.enable_distributed_cost_center:
-				DCC_allocation = frappe.db.sql("""SELECT parent, sum(percentage_allocation) as percentage_allocation
-					FROM `tabDistributed Cost Center`
-					WHERE cost_center IN %(cost_center)s
-					AND parent NOT IN %(cost_center)s
-					GROUP BY parent""",{'cost_center': [d.name]})
-				if DCC_allocation:
-					for account in new_accounts:
-						if account['name'] == DCC_allocation[0][0]:
-							for value in value_fields:
-								d[value] += account[value]*(DCC_allocation[0][1]/100)
 
 		for key in value_fields:
 			row[key] = flt(d.get(key, 0.0), 3)
diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json
index 203ea20..a456c7f 100644
--- a/erpnext/accounts/workspace/accounting/accounting.json
+++ b/erpnext/accounts/workspace/accounting/accounting.json
@@ -1024,6 +1024,17 @@
    "type": "Link"
   },
   {
+    "dependencies": "Cost Center",
+    "hidden": 0,
+    "is_query_report": 0,
+    "label": "Cost Center Allocation",
+    "link_count": 0,
+    "link_to": "Cost Center Allocation",
+    "link_type": "DocType",
+    "onboard": 0,
+    "type": "Link"
+   },
+  {
    "dependencies": "Cost Center",
    "hidden": 0,
    "is_query_report": 1,
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 9317afa..7ef6b42 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -345,3 +345,4 @@
 erpnext.patches.v14_0.restore_einvoice_fields
 erpnext.patches.v13_0.update_sane_transfer_against
 erpnext.patches.v12_0.add_company_link_to_einvoice_settings
+erpnext.patches.v14_0.migrate_cost_center_allocations
diff --git a/erpnext/patches/v13_0/remove_bad_selling_defaults.py b/erpnext/patches/v13_0/remove_bad_selling_defaults.py
index 5487a6c..0262539 100644
--- a/erpnext/patches/v13_0/remove_bad_selling_defaults.py
+++ b/erpnext/patches/v13_0/remove_bad_selling_defaults.py
@@ -3,6 +3,7 @@
 
 
 def execute():
+	frappe.reload_doctype('Selling Settings')
 	selling_settings = frappe.get_single("Selling Settings")
 
 	if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"):
diff --git a/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py
index 450c00e..7a8c1c6 100644
--- a/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py
+++ b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py
@@ -3,6 +3,9 @@
 
 
 def execute():
+	frappe.reload_doctype('Maintenance Visit')
+	frappe.reload_doctype('Maintenance Visit Purpose')
+
 	# Updates the Maintenance Schedule link to fetch serial nos
 	from frappe.query_builder.functions import Coalesce
 	mvp = frappe.qb.DocType('Maintenance Visit Purpose')
diff --git a/erpnext/patches/v14_0/migrate_cost_center_allocations.py b/erpnext/patches/v14_0/migrate_cost_center_allocations.py
new file mode 100644
index 0000000..3d217d8
--- /dev/null
+++ b/erpnext/patches/v14_0/migrate_cost_center_allocations.py
@@ -0,0 +1,48 @@
+import frappe
+from frappe.utils import today
+
+
+def execute():
+	for dt in ("cost_center_allocation", "cost_center_allocation_percentage"):
+		frappe.reload_doc('accounts', 'doctype', dt)
+
+	cc_allocations = get_existing_cost_center_allocations()
+	if cc_allocations:
+		create_new_cost_center_allocation_records(cc_allocations)
+
+	frappe.delete_doc('DocType', 'Distributed Cost Center', ignore_missing=True)
+
+def create_new_cost_center_allocation_records(cc_allocations):
+	for main_cc, allocations in cc_allocations.items():
+		cca = frappe.new_doc("Cost Center Allocation")
+		cca.main_cost_center = main_cc
+		cca.valid_from = today()
+
+		for child_cc, percentage in allocations.items():
+			cca.append("allocation_percentages", ({
+				"cost_center": child_cc,
+				"percentage": percentage
+			}))
+		cca.save()
+		cca.submit()
+
+def get_existing_cost_center_allocations():
+	if not frappe.get_meta("Cost Center").has_field("enable_distributed_cost_center"):
+		return
+
+	par = frappe.qb.DocType("Cost Center")
+	child = frappe.qb.DocType("Distributed Cost Center")
+
+	records = (
+		frappe.qb.from_(par)
+		.inner_join(child).on(par.name == child.parent)
+		.select(par.name, child.cost_center, child.percentage_allocation)
+		.where(par.enable_distributed_cost_center == 1)
+	).run(as_dict=True)
+
+	cc_allocations = frappe._dict()
+	for d in records:
+		cc_allocations.setdefault(d.name, frappe._dict())\
+			.setdefault(d.cost_center, d.percentage_allocation)
+
+	return cc_allocations
\ No newline at end of file
diff --git a/erpnext/patches/v14_0/migrate_crm_settings.py b/erpnext/patches/v14_0/migrate_crm_settings.py
index 30d3ea0..0c77853 100644
--- a/erpnext/patches/v14_0/migrate_crm_settings.py
+++ b/erpnext/patches/v14_0/migrate_crm_settings.py
@@ -9,8 +9,9 @@
 	], as_dict=True)
 
 	frappe.reload_doc('crm', 'doctype', 'crm_settings')
-	frappe.db.set_value('CRM Settings', 'CRM Settings', {
-		'campaign_naming_by': settings.campaign_naming_by,
-		'close_opportunity_after_days': settings.close_opportunity_after_days,
-		'default_valid_till': settings.default_valid_till
-	})
+	if settings:
+		frappe.db.set_value('CRM Settings', 'CRM Settings', {
+			'campaign_naming_by': settings.campaign_naming_by,
+			'close_opportunity_after_days': settings.close_opportunity_after_days,
+			'default_valid_till': settings.default_valid_till
+		})