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