Cost Center Allocation doctype, validations and test cases
diff --git a/erpnext/accounts/doctype/cost_center_allocation/__init__.py b/erpnext/accounts/doctype/cost_center_allocation/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/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..864bef3
--- /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(doc) {
+			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..37787cb
--- /dev/null
+++ b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py
@@ -0,0 +1,87 @@
+# 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 getdate, format_date, add_days
+
+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..af318ee
--- /dev/null
+++ b/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py
@@ -0,0 +1,150 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+import unittest
+from frappe.utils import today, add_months, add_days
+from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
+from erpnext.accounts.doctype.cost_center_allocation.cost_center_allocation import (MainCostCenterCantBeChild, 
+	InvalidMainCostCenter, InvalidChildCostCenter, WrongPercentageAllocation, InvalidDateError)
+
+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/cost_center_allocation_percentage/__init__.py b/erpnext/accounts/doctype/cost_center_allocation_percentage/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/cost_center_allocation_percentage/__init__.py
diff --git a/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.json b/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.json
new file mode 100644
index 0000000..4b871ae
--- /dev/null
+++ b/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.json
@@ -0,0 +1,41 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2022-01-03 18:10:11.697198",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "cost_center",
+  "percentage"
+ ],
+ "fields": [
+  {
+   "fieldname": "cost_center",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Cost Center",
+   "options": "Cost Center",
+   "reqd": 1
+  },
+  {
+   "fieldname": "percentage",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Percentage (%)",
+   "reqd": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-01-03 18:10:20.029821",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Cost Center Allocation Percentage",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "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..9214884
--- /dev/null
+++ b/erpnext/accounts/doctype/cost_center_allocation_percentage/cost_center_allocation_percentage.py
@@ -0,0 +1,8 @@
+# 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