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 0000000000..e69de29bb2 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 0000000000..864bef3ffd --- /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 0000000000..45ab886d33 --- /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 0000000000..37787cb529 --- /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 0000000000..af318eeae3 --- /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 0000000000..e69de29bb2 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 0000000000..4b871ae88d --- /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 0000000000..92148846b7 --- /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