From 22150d8175dd5cc15b2d48cee36c287b22fc5dd0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Dec 2024 17:55:23 +0530 Subject: [PATCH 01/35] refactor: budgetvaldiation class --- .../doctype/purchase_order/purchase_order.py | 6 ++ erpnext/controllers/budget_controller.py | 65 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 erpnext/controllers/budget_controller.py diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 367299fa634..59bc60e4f3d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -481,6 +481,12 @@ class PurchaseOrder(BuyingController): self.notify_update() clear_doctype_notifications(self) + def validate_budget(self): + from erpnext.controllers.budget_controller import BudgetValidation + val = BudgetValidation(self) + val.validate() + frappe.throw("stop") + def on_submit(self): super().on_submit() diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py new file mode 100644 index 00000000000..60b7640fc34 --- /dev/null +++ b/erpnext/controllers/budget_controller.py @@ -0,0 +1,65 @@ +import frappe + +# from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions +from erpnext.accounts.utils import get_fiscal_year + + +class BudgetValidation: + def __init__(self, doc: object): + self.doc = doc + self.company = doc.get("company") + self.doc_date = ( + doc.get("transaction_date") if doc.get("doctype") == "Purchase Order" else doc.get("posting_date") + ) + self.fiscal_year = get_fiscal_year(self.doc_date)[0] + self.get_dimensions() + # When GL Map is passed, there is a possibility of multiple fiscal year. + # TODO: need to handle it + + def get_dimensions(self): + self.dimensions = [] + for _x in frappe.db.get_all("Accounting Dimension"): + self.dimensions.append(frappe.get_doc("Accounting Dimension", _x.name)) + self.dimensions.extend( + [ + {"fieldname": "cost_center", "document_type": "Cost Center"}, + {"fieldname": "project", "document_type": "Project"}, + ] + ) + + def get_budget_records(self): + self.budgets = [] + for x in frappe.db.get_all( + "Budget", {"fiscal_year": self.fiscal_year, "docstatus": 1, "company": self.company} + ): + self.budgets.append(frappe.get_doc("Budget", x.name)) + + def get_active_budgets(self): + self.active_keys = set() + self.get_budget_records() + for x in self.budgets: + budget_against = frappe.scrub(x.budget_against) + dimension = x.get(budget_against) + self.active_keys = self.active_keys | set( + [(budget_against, dimension, acc.account) for acc in x.accounts] + ) + + def generate_doc_dimension_keys(self): + keys = [] + for itm in self.doc.items: + keys.extend( + [ + (dim.get("fieldname"), itm.get(dim.get("fieldname")), itm.expense_account) + for dim in self.dimensions + if itm.get(dim.get("fieldname")) + ] + ) + self.item_dimension_keys = set(keys) + + def validate(self): + self.get_active_budgets() + self.generate_doc_dimension_keys() + + print(self.active_keys) + print(self.item_dimension_keys) + print(self.active_keys & self.item_dimension_keys) From 63dae6bd42c4fd6e90d77045070b0fcd40e14e77 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 13 Dec 2024 15:10:04 +0530 Subject: [PATCH 02/35] refactor: validate only for overlapping keys --- erpnext/controllers/budget_controller.py | 47 +++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 60b7640fc34..28bc0d91814 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -34,7 +34,10 @@ class BudgetValidation: ): self.budgets.append(frappe.get_doc("Budget", x.name)) - def get_active_budgets(self): + def generate_active_budget_keys(self): + """ + key structure - (dimension_type, dimension, GL account) + """ self.active_keys = set() self.get_budget_records() for x in self.budgets: @@ -45,6 +48,9 @@ class BudgetValidation: ) def generate_doc_dimension_keys(self): + """ + key structure - (dimension_type, dimension, GL account) + """ keys = [] for itm in self.doc.items: keys.extend( @@ -56,10 +62,41 @@ class BudgetValidation: ) self.item_dimension_keys = set(keys) + def build_processing_dictionary(self): + self.budget_map = frappe._dict() + + for x in self.budgets: + budget_against = frappe.scrub(x.budget_against) + dimension = x.get(budget_against) + for acc in x.accounts: + key = (budget_against, dimension, acc.account) + if key in self.overlap: + self.budget_map[key] = frappe._dict( + {"budget_amount": acc.budget_amount, "items_to_process": []} + ) + + for itm in self.doc.items: + for dim in self.dimensions: + if itm.get(dim.get("fieldname")): + key = (dim.get("fieldname"), itm.get(dim.get("fieldname")), itm.expense_account) + + if key in self.overlap: + self.budget_map[key]["items_to_process"].append(itm) + def validate(self): - self.get_active_budgets() + self.generate_active_budget_keys() self.generate_doc_dimension_keys() - print(self.active_keys) - print(self.item_dimension_keys) - print(self.active_keys & self.item_dimension_keys) + self.overlap = self.active_keys & self.item_dimension_keys + self.build_processing_dictionary() + self.validate_for_overbooking() + + def get_booked_amount(self): + pass + + def validate_for_overbooking(self): + # Need to fetch historical amount and add them to the current document + for k, v in self.budget_map.items(): + current_amount = sum([x.amount for x in v.items_to_process]) + self.budget_map[k]["current_amount"] = current_amount + print((k, v.get("budget_amount"), current_amount)) From bd42d09592a7bf86cd99cfa55ccd7c85e9a5caef Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 20 Dec 2024 16:02:29 +0530 Subject: [PATCH 03/35] refactor: replace get_doc with sql --- erpnext/controllers/budget_controller.py | 54 +++++++++++++++++------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 28bc0d91814..ececb8f14f9 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -1,4 +1,5 @@ import frappe +from frappe import qb # from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.accounts.utils import get_fiscal_year @@ -28,24 +29,48 @@ class BudgetValidation: ) def get_budget_records(self): - self.budgets = [] - for x in frappe.db.get_all( - "Budget", {"fiscal_year": self.fiscal_year, "docstatus": 1, "company": self.company} - ): - self.budgets.append(frappe.get_doc("Budget", x.name)) + bud = qb.DocType("Budget") + bud_acc = qb.DocType("Budget Account") + query = ( + qb.from_(bud) + .inner_join(bud_acc) + .on(bud.name == bud_acc.parent) + .select( + bud.name, + bud.budget_against, + bud.company, + bud.applicable_on_material_request, + bud.action_if_annual_budget_exceeded_on_mr, + bud.action_if_accumulated_monthly_budget_exceeded_on_mr, + bud.applicable_on_purchase_order, + bud.action_if_annual_budget_exceeded_on_po, + bud.action_if_accumulated_monthly_budget_exceeded_on_po, + bud.applicable_on_booking_actual_expenses, + bud.action_if_annual_budget_exceeded, + bud.action_if_accumulated_monthly_budget_exceeded, + bud_acc.account, + bud_acc.budget_amount, + ) + .where(bud.docstatus.eq(1) & bud.fiscal_year.eq(self.fiscal_year) & bud.company.eq(self.company)) + ) + + # add dimension fields + for x in self.dimensions: + query = query.select(bud[x.get("fieldname")]) + + self.budgets = query.run(as_dict=True) def generate_active_budget_keys(self): """ key structure - (dimension_type, dimension, GL account) """ - self.active_keys = set() self.get_budget_records() + _keys = [] for x in self.budgets: budget_against = frappe.scrub(x.budget_against) dimension = x.get(budget_against) - self.active_keys = self.active_keys | set( - [(budget_against, dimension, acc.account) for acc in x.accounts] - ) + _keys.append((budget_against, dimension, x.account)) + self.active_keys = set(_keys) def generate_doc_dimension_keys(self): """ @@ -68,12 +93,11 @@ class BudgetValidation: for x in self.budgets: budget_against = frappe.scrub(x.budget_against) dimension = x.get(budget_against) - for acc in x.accounts: - key = (budget_against, dimension, acc.account) - if key in self.overlap: - self.budget_map[key] = frappe._dict( - {"budget_amount": acc.budget_amount, "items_to_process": []} - ) + key = (budget_against, dimension, x.account) + if key in self.overlap: + self.budget_map[key] = frappe._dict( + {"budget_amount": x.budget_amount, "items_to_process": []} + ) for itm in self.doc.items: for dim in self.dimensions: From 31ac9a5ea095b4eb1651410b851393d6ac00e7d6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 20 Dec 2024 16:34:02 +0530 Subject: [PATCH 04/35] refactor: make code more pythonic --- erpnext/controllers/budget_controller.py | 87 +++++++++++------------- 1 file changed, 39 insertions(+), 48 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index ececb8f14f9..fce9aae494e 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -1,7 +1,8 @@ +from collections import OrderedDict + import frappe from frappe import qb -# from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.accounts.utils import get_fiscal_year @@ -28,7 +29,7 @@ class BudgetValidation: ] ) - def get_budget_records(self): + def get_budget_records(self) -> list: bud = qb.DocType("Budget") bud_acc = qb.DocType("Budget Account") query = ( @@ -58,69 +59,59 @@ class BudgetValidation: for x in self.dimensions: query = query.select(bud[x.get("fieldname")]) - self.budgets = query.run(as_dict=True) + _budgets = query.run(as_dict=True) + return _budgets - def generate_active_budget_keys(self): + def build_budget_keys_and_map(self): """ key structure - (dimension_type, dimension, GL account) """ - self.get_budget_records() + _budgets = self.get_budget_records() _keys = [] - for x in self.budgets: - budget_against = frappe.scrub(x.budget_against) - dimension = x.get(budget_against) - _keys.append((budget_against, dimension, x.account)) - self.active_keys = set(_keys) + self.budget_map = OrderedDict() + for _bud in _budgets: + budget_against = frappe.scrub(_bud.budget_against) + dimension = _bud.get(budget_against) + key = (budget_against, dimension, _bud.account) + # TODO: ensure duplicate keys are not possible + self.budget_map[key] = _bud + self.budget_keys = self.budget_map.keys() - def generate_doc_dimension_keys(self): + def build_doc_or_item_keys_and_map(self): """ key structure - (dimension_type, dimension, GL account) """ - keys = [] - for itm in self.doc.items: - keys.extend( - [ - (dim.get("fieldname"), itm.get(dim.get("fieldname")), itm.expense_account) - for dim in self.dimensions - if itm.get(dim.get("fieldname")) - ] - ) - self.item_dimension_keys = set(keys) - - def build_processing_dictionary(self): - self.budget_map = frappe._dict() - - for x in self.budgets: - budget_against = frappe.scrub(x.budget_against) - dimension = x.get(budget_against) - key = (budget_against, dimension, x.account) - if key in self.overlap: - self.budget_map[key] = frappe._dict( - {"budget_amount": x.budget_amount, "items_to_process": []} - ) - + self.doc_or_item_map = OrderedDict() + _key = [] for itm in self.doc.items: for dim in self.dimensions: if itm.get(dim.get("fieldname")): key = (dim.get("fieldname"), itm.get(dim.get("fieldname")), itm.expense_account) + # TODO: How to handle duplicate items - same item with same dimension with same account + self.doc_or_item_map.setdefault(key, []).append(itm) + self.doc_or_item_keys = self.doc_or_item_map.keys() - if key in self.overlap: - self.budget_map[key]["items_to_process"].append(itm) + def build_to_validate_map(self): + self.overlap = self.budget_keys & self.doc_or_item_keys + self.to_validate = OrderedDict() + + for key in self.overlap: + self.to_validate[key] = OrderedDict( + { + "budget_amount": self.budget_map[key].budget_amount, + "items_to_process": self.doc_or_item_map[key], + } + ) def validate(self): - self.generate_active_budget_keys() - self.generate_doc_dimension_keys() - - self.overlap = self.active_keys & self.item_dimension_keys - self.build_processing_dictionary() + self.build_budget_keys_and_map() + self.build_doc_or_item_keys_and_map() + self.build_to_validate_map() self.validate_for_overbooking() - def get_booked_amount(self): - pass - def validate_for_overbooking(self): - # Need to fetch historical amount and add them to the current document - for k, v in self.budget_map.items(): - current_amount = sum([x.amount for x in v.items_to_process]) - self.budget_map[k]["current_amount"] = current_amount + # TODO: Need to fetch historical amount and add them to the current document + # TODO: handle applicable checkboxes + for k, v in self.to_validate.items(): + current_amount = sum([x.amount for x in v.get("items_to_process")]) print((k, v.get("budget_amount"), current_amount)) From fb667f5e09cd179368107e145c8b488407d95c18 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 20 Dec 2024 17:26:35 +0530 Subject: [PATCH 05/35] refactor: fetch billed PO amount --- erpnext/controllers/budget_controller.py | 35 +++++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index fce9aae494e..ecc2aad801c 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -13,7 +13,10 @@ class BudgetValidation: self.doc_date = ( doc.get("transaction_date") if doc.get("doctype") == "Purchase Order" else doc.get("posting_date") ) - self.fiscal_year = get_fiscal_year(self.doc_date)[0] + fy = get_fiscal_year(self.doc_date) + self.fiscal_year = fy[0] + self.fy_start_date = fy[1] + self.fy_end_date = fy[2] self.get_dimensions() # When GL Map is passed, there is a possibility of multiple fiscal year. # TODO: need to handle it @@ -109,9 +112,33 @@ class BudgetValidation: self.build_to_validate_map() self.validate_for_overbooking() + def get_ordered_amount(self): + items = set([x.item_code for x in self.doc.items]) + exp_accounts = set([x.expense_account for x in self.doc.items]) + + po = qb.DocType("Purchase Order") + poi = qb.DocType("Purchase Order Item") + + query = ( + qb.from_(po) + .inner_join(poi) + .on(po.name == poi.parent) + .select(po.name) + .where( + po.docstatus.eq(1) + & (poi.amount > poi.billed_amt) + & po.status.ne("Closed") + & poi.item_code.isin(items) + & poi.expense_account.isin(exp_accounts) + & po.transaction_date[self.fy_start_date : self.fy_end_date] + ) + ) + print("Query:", query) + def validate_for_overbooking(self): # TODO: Need to fetch historical amount and add them to the current document # TODO: handle applicable checkboxes - for k, v in self.to_validate.items(): - current_amount = sum([x.amount for x in v.get("items_to_process")]) - print((k, v.get("budget_amount"), current_amount)) + for v in self.to_validate.values(): + v["current_amount"] = sum([x.amount for x in v.get("items_to_process")]) + + self.get_ordered_amount() From 10c3bb4971addf564251fd00812be588c06a66c4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 1 Jan 2025 11:32:47 +0530 Subject: [PATCH 06/35] refactor: move validate trigger to controller --- .../doctype/purchase_order/purchase_order.py | 6 -- erpnext/controllers/budget_controller.py | 66 ++++++++++++------- erpnext/controllers/buying_controller.py | 6 ++ 3 files changed, 49 insertions(+), 29 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 59bc60e4f3d..367299fa634 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -481,12 +481,6 @@ class PurchaseOrder(BuyingController): self.notify_update() clear_doctype_notifications(self) - def validate_budget(self): - from erpnext.controllers.budget_controller import BudgetValidation - val = BudgetValidation(self) - val.validate() - frappe.throw("stop") - def on_submit(self): super().on_submit() diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index ecc2aad801c..ace6ea8af91 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -2,6 +2,8 @@ from collections import OrderedDict import frappe from frappe import qb +from frappe.query_builder import Criterion +from frappe.query_builder.functions import IfNull, Sum from erpnext.accounts.utils import get_fiscal_year @@ -94,7 +96,7 @@ class BudgetValidation: self.doc_or_item_map.setdefault(key, []).append(itm) self.doc_or_item_keys = self.doc_or_item_map.keys() - def build_to_validate_map(self): + def build_validation_map(self): self.overlap = self.budget_keys & self.doc_or_item_keys self.to_validate = OrderedDict() @@ -109,36 +111,54 @@ class BudgetValidation: def validate(self): self.build_budget_keys_and_map() self.build_doc_or_item_keys_and_map() - self.build_to_validate_map() + self.build_validation_map() self.validate_for_overbooking() - def get_ordered_amount(self): - items = set([x.item_code for x in self.doc.items]) - exp_accounts = set([x.expense_account for x in self.doc.items]) + def get_ordered_amount(self, key: tuple | None = None): + if key: + # Initialize + self.to_validate[key]["ordered_amount"] = 0 - po = qb.DocType("Purchase Order") - poi = qb.DocType("Purchase Order Item") + items = set([x.item_code for x in self.doc.items]) + exp_accounts = set([x.expense_account for x in self.doc.items]) - query = ( - qb.from_(po) - .inner_join(poi) - .on(po.name == poi.parent) - .select(po.name) - .where( - po.docstatus.eq(1) - & (poi.amount > poi.billed_amt) - & po.status.ne("Closed") - & poi.item_code.isin(items) - & poi.expense_account.isin(exp_accounts) - & po.transaction_date[self.fy_start_date : self.fy_end_date] + po = qb.DocType("Purchase Order") + poi = qb.DocType("Purchase Order Item") + + conditions = [] + conditions.append(po.company.eq(self.company)) + conditions.append(po.docstatus.eq(1)) + conditions.append(poi.amount.gt(poi.billed_amt)) + conditions.append(poi.expense_account.isin(exp_accounts)) + conditions.append(poi.item_code.isin(items)) + conditions.append(po.status.ne("Closed")) + conditions.append(po.transaction_date[self.fy_start_date : self.fy_end_date]) + + # key structure - (dimension_type, dimension, GL account) + conditions.append(poi[key[0]].eq(key[1])) + + ordered_amount = ( + qb.from_(po) + .inner_join(poi) + .on(po.name == poi.parent) + .select(Sum(IfNull(poi.amount, 0) - IfNull(poi.billed_amt, 0)).as_("amount")) + .where(Criterion.all(conditions)) + .run(as_dict=True) ) - ) - print("Query:", query) + if ordered_amount: + self.to_validate[key]["ordered_amount"] = 0 def validate_for_overbooking(self): # TODO: Need to fetch historical amount and add them to the current document # TODO: handle applicable checkboxes - for v in self.to_validate.values(): + self.ordered_amount = frappe._dict() + + for k, v in self.to_validate.items(): + # Amt from current Purchase Order is included in `self.ordered_amount` as doc is + # in submitted status by the time the validation occurs + if self.doc.doctype == "Purchase Order": + self.get_ordered_amount(k) + v["current_amount"] = sum([x.amount for x in v.get("items_to_process")]) - self.get_ordered_amount() + print(self.to_validate) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 602d6c955b3..d1aebeb233f 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -775,6 +775,12 @@ class BuyingController(SubcontractingController): self.update_fixed_asset(field, delete_asset=True) def validate_budget(self): + from erpnext.controllers.budget_controller import BudgetValidation + + val = BudgetValidation(self) + val.validate() + return + if self.docstatus == 1: for data in self.get("items"): args = data.as_dict() From 1c574561eb41e78978191f87b0a79a1a05fb7ae6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 1 Jan 2025 15:23:55 +0530 Subject: [PATCH 07/35] refactor: fetch amount booked in material request --- erpnext/controllers/budget_controller.py | 48 ++++++++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index ace6ea8af91..68e3b32247e 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -105,6 +105,8 @@ class BudgetValidation: { "budget_amount": self.budget_map[key].budget_amount, "items_to_process": self.doc_or_item_map[key], + "requested_amount": 0, + "ordered_amount": 0, } ) @@ -116,9 +118,6 @@ class BudgetValidation: def get_ordered_amount(self, key: tuple | None = None): if key: - # Initialize - self.to_validate[key]["ordered_amount"] = 0 - items = set([x.item_code for x in self.doc.items]) exp_accounts = set([x.expense_account for x in self.doc.items]) @@ -128,11 +127,11 @@ class BudgetValidation: conditions = [] conditions.append(po.company.eq(self.company)) conditions.append(po.docstatus.eq(1)) + conditions.append(po.status.ne("Closed")) + conditions.append(po.transaction_date[self.fy_start_date : self.fy_end_date]) conditions.append(poi.amount.gt(poi.billed_amt)) conditions.append(poi.expense_account.isin(exp_accounts)) conditions.append(poi.item_code.isin(items)) - conditions.append(po.status.ne("Closed")) - conditions.append(po.transaction_date[self.fy_start_date : self.fy_end_date]) # key structure - (dimension_type, dimension, GL account) conditions.append(poi[key[0]].eq(key[1])) @@ -146,19 +145,52 @@ class BudgetValidation: .run(as_dict=True) ) if ordered_amount: - self.to_validate[key]["ordered_amount"] = 0 + self.to_validate[key]["ordered_amount"] = ordered_amount[0].amount + + def get_requested_amount(self, key: tuple | None = None): + if key: + items = set([x.item_code for x in self.doc.items]) + exp_accounts = set([x.expense_account for x in self.doc.items]) + + mr = qb.DocType("Material Request") + mri = qb.DocType("Material Request Item") + + conditions = [] + conditions.append(mr.company.eq(self.company)) + conditions.append(mr.docstatus.eq(1)) + conditions.append(mr.material_request_type.eq("Purchase")) + conditions.append(mr.status.ne("Stopped")) + conditions.append(mr.transaction_date[self.fy_start_date : self.fy_end_date]) + conditions.append(mri.amount.gt(mri.billed_amt)) + conditions.append(mri.expense_account.isin(exp_accounts)) + conditions.append(mri.item_code.isin(items)) + + # key structure - (dimension_type, dimension, GL account) + conditions.append(mri[key[0]].eq(key[1])) + + requested_amount = ( + qb.from_(mr) + .inner_join(mri) + .on(mr.name == mri.parent) + .select((Sum(IfNull(mri.stock_qty, 0) - IfNull(mri.ordered_qty, 0)) * mri.rate).as_("amount")) + .where(Criterion.all(conditions)) + .run(as_dict=True) + ) + if requested_amount: + self.to_validate[key]["requested_amount"] = requested_amount[0].amount def validate_for_overbooking(self): # TODO: Need to fetch historical amount and add them to the current document # TODO: handle applicable checkboxes - self.ordered_amount = frappe._dict() - for k, v in self.to_validate.items(): # Amt from current Purchase Order is included in `self.ordered_amount` as doc is # in submitted status by the time the validation occurs if self.doc.doctype == "Purchase Order": self.get_ordered_amount(k) + if self.doc.doctype == "Material Request": + self.get_requested_amount(k) + v["current_amount"] = sum([x.amount for x in v.get("items_to_process")]) print(self.to_validate) From 7791777d1a40888227e30001bca07a18540749bf Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 1 Jan 2025 16:28:42 +0530 Subject: [PATCH 08/35] refactor: barebones validation --- erpnext/controllers/budget_controller.py | 32 ++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 68e3b32247e..b396d44aa5d 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -1,13 +1,17 @@ from collections import OrderedDict import frappe -from frappe import qb +from frappe import _, qb from frappe.query_builder import Criterion from frappe.query_builder.functions import IfNull, Sum from erpnext.accounts.utils import get_fiscal_year +class BudgetExceededError(frappe.ValidationError): + pass + + class BudgetValidation: def __init__(self, doc: object): self.doc = doc @@ -104,6 +108,7 @@ class BudgetValidation: self.to_validate[key] = OrderedDict( { "budget_amount": self.budget_map[key].budget_amount, + "budget_doc": self.budget_map[key], "items_to_process": self.doc_or_item_map[key], "requested_amount": 0, "ordered_amount": 0, @@ -179,18 +184,31 @@ class BudgetValidation: if requested_amount: self.to_validate[key]["requested_amount"] = requested_amount[0].amount + def stop_or_warn(self, validation_map): + msg = [] + if validation_map.get("budget_doc").applicable_on_purchase_order and validation_map.get( + "ordered_amount" + ) > validation_map.get("budget_amount"): + # TODO: handle monthly accumulation + # action_if_accumulated_monthly_budget_exceeded_on_po, + if validation_map.get("budget_doc").action_if_annual_budget_exceeded_on_po == "Warn": + msg.append("some warning message") + + if validation_map.get("budget_doc").action_if_annual_budget_exceeded_on_po == "Stop": + frappe.throw("Booking gone above budget", BudgetExceededError, title=_("Budget Exceeded")) + def validate_for_overbooking(self): - # TODO: Need to fetch historical amount and add them to the current document + # TODO: Need to fetch historical amount and add them to the current document; GL effect is pending # TODO: handle applicable checkboxes - for k, v in self.to_validate.items(): + for key, v in self.to_validate.items(): # Amt from current Purchase Order is included in `self.ordered_amount` as doc is # in submitted status by the time the validation occurs if self.doc.doctype == "Purchase Order": - self.get_ordered_amount(k) + self.get_ordered_amount(key) if self.doc.doctype == "Material Request": - self.get_requested_amount(k) + self.get_requested_amount(key) + + self.stop_or_warn(v) v["current_amount"] = sum([x.amount for x in v.get("items_to_process")]) - - print(self.to_validate) From 626b345caf3116f35686736b28aab6ee11dbbcab Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 10 Feb 2025 17:36:31 +0530 Subject: [PATCH 09/35] refactor: better local variables and contextual error messages --- erpnext/controllers/budget_controller.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index b396d44aa5d..33a7f5e286d 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -4,6 +4,7 @@ import frappe from frappe import _, qb from frappe.query_builder import Criterion from frappe.query_builder.functions import IfNull, Sum +from frappe.utils import get_link_to_form from erpnext.accounts.utils import get_fiscal_year @@ -184,18 +185,20 @@ class BudgetValidation: if requested_amount: self.to_validate[key]["requested_amount"] = requested_amount[0].amount - def stop_or_warn(self, validation_map): + def stop_or_warn(self, v_map): msg = [] - if validation_map.get("budget_doc").applicable_on_purchase_order and validation_map.get( - "ordered_amount" - ) > validation_map.get("budget_amount"): + budget = v_map.get("budget_doc") + if budget.applicable_on_purchase_order and v_map.get("ordered_amount") > v_map.get("budget_amount"): # TODO: handle monthly accumulation # action_if_accumulated_monthly_budget_exceeded_on_po, - if validation_map.get("budget_doc").action_if_annual_budget_exceeded_on_po == "Warn": + if budget.action_if_annual_budget_exceeded_on_po == "Warn": msg.append("some warning message") - if validation_map.get("budget_doc").action_if_annual_budget_exceeded_on_po == "Stop": - frappe.throw("Booking gone above budget", BudgetExceededError, title=_("Budget Exceeded")) + if budget.action_if_annual_budget_exceeded_on_po == "Stop": + _msg = _( + "Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name)) + ) + frappe.throw(_msg, BudgetExceededError, title=_("Budget Exceeded")) def validate_for_overbooking(self): # TODO: Need to fetch historical amount and add them to the current document; GL effect is pending From d933d0b47880ff688a24b9d02240912a97a9bc13 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 10 Feb 2025 20:33:27 +0530 Subject: [PATCH 10/35] refactor: handle budget for material request --- erpnext/controllers/budget_controller.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 33a7f5e286d..d21079a914f 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -18,7 +18,9 @@ class BudgetValidation: self.doc = doc self.company = doc.get("company") self.doc_date = ( - doc.get("transaction_date") if doc.get("doctype") == "Purchase Order" else doc.get("posting_date") + doc.get("transaction_date") + if doc.get("doctype") in ["Purchase Order", "Material Request"] + else doc.get("posting_date") ) fy = get_fiscal_year(self.doc_date) self.fiscal_year = fy[0] @@ -167,7 +169,6 @@ class BudgetValidation: conditions.append(mr.material_request_type.eq("Purchase")) conditions.append(mr.status.ne("Stopped")) conditions.append(mr.transaction_date[self.fy_start_date : self.fy_end_date]) - conditions.append(mri.amount.gt(mri.billed_amt)) conditions.append(mri.expense_account.isin(exp_accounts)) conditions.append(mri.item_code.isin(items)) @@ -200,6 +201,20 @@ class BudgetValidation: ) frappe.throw(_msg, BudgetExceededError, title=_("Budget Exceeded")) + if budget.applicable_on_material_request and v_map.get("requested_amount") > v_map.get( + "budget_amount" + ): + # TODO: handle monthly accumulation + # action_if_accumulated_monthly_budget_exceeded_on_po, + if budget.action_if_annual_budget_exceeded_on_po == "Warn": + msg.append("some warning message") + + if budget.action_if_annual_budget_exceeded_on_po == "Stop": + _msg = _( + "Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name)) + ) + frappe.throw(_msg, BudgetExceededError, title=_("Budget Exceeded")) + def validate_for_overbooking(self): # TODO: Need to fetch historical amount and add them to the current document; GL effect is pending # TODO: handle applicable checkboxes From ec466d024a12f9206de0d2bd2210f889d6e7f42a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 11 Feb 2025 10:04:57 +0530 Subject: [PATCH 11/35] refactor: minor readbility changes --- erpnext/controllers/budget_controller.py | 176 +++++++++++++---------- 1 file changed, 99 insertions(+), 77 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index d21079a914f..269a0755606 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -27,9 +27,80 @@ class BudgetValidation: self.fy_start_date = fy[1] self.fy_end_date = fy[2] self.get_dimensions() + # TODO: handle GL map + # When GL Map is passed, there is a possibility of multiple fiscal year. # TODO: need to handle it + def validate(self): + self.build_validation_map() + self.validate_for_overbooking() + + def build_validation_map(self): + self.build_budget_keys_and_map() + self.build_doc_or_item_keys_and_map() + + self.overlap = self.budget_keys & self.doc_or_item_keys + self.to_validate = OrderedDict() + + for key in self.overlap: + self.to_validate[key] = OrderedDict( + { + "budget_amount": self.budget_map[key].budget_amount, + "budget_doc": self.budget_map[key], + "items_to_process": self.doc_or_item_map[key], + "requested_amount": 0, + "ordered_amount": 0, + "actual_expenses": 0, + } + ) + + def validate_for_overbooking(self): + # TODO: Need to fetch historical amount and add them to the current document; GL effect is pending + # TODO: handle applicable checkboxes + for key, v in self.to_validate.items(): + # Amt from current Purchase Order is included in `self.ordered_amount` as doc is + # in submitted status by the time the validation occurs + if self.doc.doctype == "Purchase Order": + self.get_ordered_amount(key) + + if self.doc.doctype == "Material Request": + self.get_requested_amount(key) + + # TODO: call stack should be self-explanatory on which doctype the error got thrown + self.handle_action(v) + + v["current_amount"] = sum([x.amount for x in v.get("items_to_process")]) + + def build_budget_keys_and_map(self): + """ + key structure - (dimension_type, dimension, GL account) + """ + _budgets = self.get_budget_records() + _keys = [] + self.budget_map = OrderedDict() + for _bud in _budgets: + budget_against = frappe.scrub(_bud.budget_against) + dimension = _bud.get(budget_against) + key = (budget_against, dimension, _bud.account) + # TODO: ensure duplicate keys are not possible + self.budget_map[key] = _bud + self.budget_keys = self.budget_map.keys() + + def build_doc_or_item_keys_and_map(self): + """ + key structure - (dimension_type, dimension, GL account) + """ + self.doc_or_item_map = OrderedDict() + _key = [] + for itm in self.doc.items: + for dim in self.dimensions: + if itm.get(dim.get("fieldname")): + key = (dim.get("fieldname"), itm.get(dim.get("fieldname")), itm.expense_account) + # TODO: How to handle duplicate items - same item with same dimension with same account + self.doc_or_item_map.setdefault(key, []).append(itm) + self.doc_or_item_keys = self.doc_or_item_map.keys() + def get_dimensions(self): self.dimensions = [] for _x in frappe.db.get_all("Accounting Dimension"): @@ -74,56 +145,6 @@ class BudgetValidation: _budgets = query.run(as_dict=True) return _budgets - def build_budget_keys_and_map(self): - """ - key structure - (dimension_type, dimension, GL account) - """ - _budgets = self.get_budget_records() - _keys = [] - self.budget_map = OrderedDict() - for _bud in _budgets: - budget_against = frappe.scrub(_bud.budget_against) - dimension = _bud.get(budget_against) - key = (budget_against, dimension, _bud.account) - # TODO: ensure duplicate keys are not possible - self.budget_map[key] = _bud - self.budget_keys = self.budget_map.keys() - - def build_doc_or_item_keys_and_map(self): - """ - key structure - (dimension_type, dimension, GL account) - """ - self.doc_or_item_map = OrderedDict() - _key = [] - for itm in self.doc.items: - for dim in self.dimensions: - if itm.get(dim.get("fieldname")): - key = (dim.get("fieldname"), itm.get(dim.get("fieldname")), itm.expense_account) - # TODO: How to handle duplicate items - same item with same dimension with same account - self.doc_or_item_map.setdefault(key, []).append(itm) - self.doc_or_item_keys = self.doc_or_item_map.keys() - - def build_validation_map(self): - self.overlap = self.budget_keys & self.doc_or_item_keys - self.to_validate = OrderedDict() - - for key in self.overlap: - self.to_validate[key] = OrderedDict( - { - "budget_amount": self.budget_map[key].budget_amount, - "budget_doc": self.budget_map[key], - "items_to_process": self.doc_or_item_map[key], - "requested_amount": 0, - "ordered_amount": 0, - } - ) - - def validate(self): - self.build_budget_keys_and_map() - self.build_doc_or_item_keys_and_map() - self.build_validation_map() - self.validate_for_overbooking() - def get_ordered_amount(self, key: tuple | None = None): if key: items = set([x.item_code for x in self.doc.items]) @@ -186,47 +207,48 @@ class BudgetValidation: if requested_amount: self.to_validate[key]["requested_amount"] = requested_amount[0].amount - def stop_or_warn(self, v_map): - msg = [] + def get_actual_expenses(self, key: tuple | None = None): + if key: + pass + + def stop(self, msg): + frappe.throw(msg, BudgetExceededError, title=_("Budget Exceeded")) + + def warn(self, msg): + frappe.msgprint(msg, _("Budget Exceeded")) + + def handle_po_action(self, v_map): budget = v_map.get("budget_doc") if budget.applicable_on_purchase_order and v_map.get("ordered_amount") > v_map.get("budget_amount"): # TODO: handle monthly accumulation # action_if_accumulated_monthly_budget_exceeded_on_po, + _msg = _("Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name))) + if budget.action_if_annual_budget_exceeded_on_po == "Warn": - msg.append("some warning message") + self.warn(_msg) if budget.action_if_annual_budget_exceeded_on_po == "Stop": - _msg = _( - "Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name)) - ) - frappe.throw(_msg, BudgetExceededError, title=_("Budget Exceeded")) + self.stop(_msg) + def handle_mr_action(self, v_map): + budget = v_map.get("budget_doc") if budget.applicable_on_material_request and v_map.get("requested_amount") > v_map.get( "budget_amount" ): # TODO: handle monthly accumulation # action_if_accumulated_monthly_budget_exceeded_on_po, - if budget.action_if_annual_budget_exceeded_on_po == "Warn": - msg.append("some warning message") + _msg = _("Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name))) - if budget.action_if_annual_budget_exceeded_on_po == "Stop": - _msg = _( - "Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name)) - ) - frappe.throw(_msg, BudgetExceededError, title=_("Budget Exceeded")) + if budget.action_if_annual_budget_exceeded_on_mr == "Warn": + self.warn(_msg) - def validate_for_overbooking(self): - # TODO: Need to fetch historical amount and add them to the current document; GL effect is pending - # TODO: handle applicable checkboxes - for key, v in self.to_validate.items(): - # Amt from current Purchase Order is included in `self.ordered_amount` as doc is - # in submitted status by the time the validation occurs - if self.doc.doctype == "Purchase Order": - self.get_ordered_amount(key) + if budget.action_if_annual_budget_exceeded_on_mr == "Stop": + self.stop(_msg) - if self.doc.doctype == "Material Request": - self.get_requested_amount(key) + def handle_actual_expense_action(self, v_map): + pass - self.stop_or_warn(v) - - v["current_amount"] = sum([x.amount for x in v.get("items_to_process")]) + def handle_action(self, v_map): + self.handle_po_action(v_map) + self.handle_mr_action(v_map) + self.handle_actual_expense_action(v_map) From 791ad168833fcc53ad1945cd8f88ee7bdac35ab2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 14 Feb 2025 11:14:59 +0530 Subject: [PATCH 12/35] refactor: cleaner initialization --- erpnext/controllers/budget_controller.py | 28 ++++++++++++------------ erpnext/controllers/buying_controller.py | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 269a0755606..ddb0ac6b545 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -14,23 +14,25 @@ class BudgetExceededError(frappe.ValidationError): class BudgetValidation: - def __init__(self, doc: object): - self.doc = doc - self.company = doc.get("company") - self.doc_date = ( - doc.get("transaction_date") - if doc.get("doctype") in ["Purchase Order", "Material Request"] - else doc.get("posting_date") - ) + def __init__(self, doc: object | None = None, gl_map: list | None = None): + if doc: + self.document_type = doc.get("doctype") + self.doc = doc + self.company = doc.get("company") + self.doc_date = doc.get("transaction_date") + elif gl_map: + # When GL Map is passed, there is a possibility of multiple fiscal year. + # TODO: need to handle it + self.document_type = "GL Map" + self.gl_map = gl_map + self.company = gl_map[0].company + self.doc_date = gl_map[0].posting_date + fy = get_fiscal_year(self.doc_date) self.fiscal_year = fy[0] self.fy_start_date = fy[1] self.fy_end_date = fy[2] self.get_dimensions() - # TODO: handle GL map - - # When GL Map is passed, there is a possibility of multiple fiscal year. - # TODO: need to handle it def validate(self): self.build_validation_map() @@ -77,7 +79,6 @@ class BudgetValidation: key structure - (dimension_type, dimension, GL account) """ _budgets = self.get_budget_records() - _keys = [] self.budget_map = OrderedDict() for _bud in _budgets: budget_against = frappe.scrub(_bud.budget_against) @@ -92,7 +93,6 @@ class BudgetValidation: key structure - (dimension_type, dimension, GL account) """ self.doc_or_item_map = OrderedDict() - _key = [] for itm in self.doc.items: for dim in self.dimensions: if itm.get(dim.get("fieldname")): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index d1aebeb233f..ed2d98f3469 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -777,7 +777,7 @@ class BuyingController(SubcontractingController): def validate_budget(self): from erpnext.controllers.budget_controller import BudgetValidation - val = BudgetValidation(self) + val = BudgetValidation(doc=self) val.validate() return From d52469c51e86f231119b92d8250a0c958cee29bd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 14 Feb 2025 15:49:35 +0530 Subject: [PATCH 13/35] refactor: handle actual expense --- erpnext/controllers/budget_controller.py | 106 +++++++++++++++++------ 1 file changed, 80 insertions(+), 26 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index ddb0ac6b545..147f8278ea0 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -46,16 +46,19 @@ class BudgetValidation: self.to_validate = OrderedDict() for key in self.overlap: - self.to_validate[key] = OrderedDict( - { - "budget_amount": self.budget_map[key].budget_amount, - "budget_doc": self.budget_map[key], - "items_to_process": self.doc_or_item_map[key], - "requested_amount": 0, - "ordered_amount": 0, - "actual_expenses": 0, - } - ) + _obj = { + "budget_amount": self.budget_map[key].budget_amount, + "budget_doc": self.budget_map[key], + "requested_amount": 0, + "ordered_amount": 0, + "actual_expense": 0, + } + if self.document_type in ["Purchase Order", "Material Request"]: + _obj.update({"items_to_process": self.doc_or_item_map[key]}) + elif self.document_type == "GL Map": + _obj.update({"gl_to_process": self.doc_or_item_map[key]}) + + self.to_validate[key] = OrderedDict(_obj) def validate_for_overbooking(self): # TODO: Need to fetch historical amount and add them to the current document; GL effect is pending @@ -63,16 +66,25 @@ class BudgetValidation: for key, v in self.to_validate.items(): # Amt from current Purchase Order is included in `self.ordered_amount` as doc is # in submitted status by the time the validation occurs - if self.doc.doctype == "Purchase Order": + if self.document_type == "Purchase Order": self.get_ordered_amount(key) - if self.doc.doctype == "Material Request": + if self.document_type == "Material Request": self.get_requested_amount(key) + if self.document_type in ["Purchase Order", "Material Request"]: + v["current_amount"] = sum([x.amount for x in v.get("items_to_process", [])]) + elif self.document_type == "GL Map": + v["current_amount"] = sum([x.debit - x.credit for x in v.get("gl_to_process", [])]) + # TODO: call stack should be self-explanatory on which doctype the error got thrown + # Exit early before hitting ledger self.handle_action(v) - v["current_amount"] = sum([x.amount for x in v.get("items_to_process")]) + if self.document_type == "GL Map": + self.get_actual_expense(key) + + self.handle_action(v) def build_budget_keys_and_map(self): """ @@ -93,12 +105,20 @@ class BudgetValidation: key structure - (dimension_type, dimension, GL account) """ self.doc_or_item_map = OrderedDict() - for itm in self.doc.items: - for dim in self.dimensions: - if itm.get(dim.get("fieldname")): - key = (dim.get("fieldname"), itm.get(dim.get("fieldname")), itm.expense_account) - # TODO: How to handle duplicate items - same item with same dimension with same account - self.doc_or_item_map.setdefault(key, []).append(itm) + if self.document_type in ["Purchase Order", "Material Request"]: + for itm in self.doc.items: + for dim in self.dimensions: + if itm.get(dim.get("fieldname")): + key = (dim.get("fieldname"), itm.get(dim.get("fieldname")), itm.expense_account) + # TODO: How to handle duplicate items - same item with same dimension with same account + self.doc_or_item_map.setdefault(key, []).append(itm) + elif self.document_type == "GL Map": + for gl in self.gl_map: + for dim in self.dimensions: + if gl.get(dim.get("fieldname")): + key = (dim.get("fieldname"), gl.get(dim.get("fieldname")), gl.get("account")) + self.doc_or_item_map.setdefault(key, []).append(gl) + self.doc_or_item_keys = self.doc_or_item_map.keys() def get_dimensions(self): @@ -163,6 +183,7 @@ class BudgetValidation: conditions.append(poi.item_code.isin(items)) # key structure - (dimension_type, dimension, GL account) + # TODO: handle child node on a tree type dimension conditions.append(poi[key[0]].eq(key[1])) ordered_amount = ( @@ -194,6 +215,7 @@ class BudgetValidation: conditions.append(mri.item_code.isin(items)) # key structure - (dimension_type, dimension, GL account) + # TODO: handle child node on a tree type dimension conditions.append(mri[key[0]].eq(key[1])) requested_amount = ( @@ -207,9 +229,26 @@ class BudgetValidation: if requested_amount: self.to_validate[key]["requested_amount"] = requested_amount[0].amount - def get_actual_expenses(self, key: tuple | None = None): + def get_actual_expense(self, key: tuple | None = None): if key: - pass + gl = qb.DocType("GL Entry") + + query = ( + qb.from_(gl) + .select((Sum(gl.debit) - Sum(gl.credit)).as_("balance")) + .where( + gl.is_cancelled.eq(0) + & gl.account.eq(key[2]) + & gl.fiscal_year.eq(self.fiscal_year) + & gl.company.eq(self.company) + # TODO: handle child node on a tree type dimension + & gl[key[0]].eq(key[1]) + & gl.posting_date[self.fy_start_date : self.fy_end_date] + ) + ) + actual_expense = query.run(as_dict=True) + if actual_expense: + self.to_validate[key]["actual_expense"] = actual_expense[0].balance or 0 def stop(self, msg): frappe.throw(msg, BudgetExceededError, title=_("Budget Exceeded")) @@ -219,7 +258,9 @@ class BudgetValidation: def handle_po_action(self, v_map): budget = v_map.get("budget_doc") - if budget.applicable_on_purchase_order and v_map.get("ordered_amount") > v_map.get("budget_amount"): + if budget.applicable_on_purchase_order and v_map.get("ordered_amount") + v_map.get( + "current_amount" + ) > v_map.get("budget_amount"): # TODO: handle monthly accumulation # action_if_accumulated_monthly_budget_exceeded_on_po, _msg = _("Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name))) @@ -232,9 +273,9 @@ class BudgetValidation: def handle_mr_action(self, v_map): budget = v_map.get("budget_doc") - if budget.applicable_on_material_request and v_map.get("requested_amount") > v_map.get( - "budget_amount" - ): + if budget.applicable_on_material_request and v_map.get("requested_amount") + v_map.get( + "current_amount" + ) > v_map.get("budget_amount"): # TODO: handle monthly accumulation # action_if_accumulated_monthly_budget_exceeded_on_po, _msg = _("Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name))) @@ -246,7 +287,20 @@ class BudgetValidation: self.stop(_msg) def handle_actual_expense_action(self, v_map): - pass + budget = v_map.get("budget_doc") + frappe.pp(v_map) + if budget.applicable_on_booking_actual_expenses and v_map.get("actual_expense") + v_map.get( + "current_amount" + ) > v_map.get("budget_amount"): + # TODO: handle monthly accumulation + # action_if_accumulated_monthly_budget_exceeded_on_po, + _msg = _("Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name))) + + if budget.action_if_annual_budget_exceeded == "Warn": + self.warn(_msg) + + if budget.action_if_annual_budget_exceeded == "Stop": + self.stop(_msg) def handle_action(self, v_map): self.handle_po_action(v_map) From 388d901668b7553393b16e53b08a2db675610e45 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Feb 2025 10:57:56 +0530 Subject: [PATCH 14/35] refactor: handle monthly distribution limit --- erpnext/controllers/budget_controller.py | 140 +++++++++++++++++------ 1 file changed, 102 insertions(+), 38 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 147f8278ea0..65c5e8c3ec1 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -6,6 +6,7 @@ from frappe.query_builder import Criterion from frappe.query_builder.functions import IfNull, Sum from frappe.utils import get_link_to_form +from erpnext.accounts.doctype.budget.budget import get_accumulated_monthly_budget from erpnext.accounts.utils import get_fiscal_year @@ -41,7 +42,9 @@ class BudgetValidation: def build_validation_map(self): self.build_budget_keys_and_map() self.build_doc_or_item_keys_and_map() + self.find_overlap() + def find_overlap(self): self.overlap = self.budget_keys & self.doc_or_item_keys self.to_validate = OrderedDict() @@ -53,6 +56,17 @@ class BudgetValidation: "ordered_amount": 0, "actual_expense": 0, } + _obj.update( + { + "accumulated_monthly_budget": get_accumulated_monthly_budget( + self.budget_map[key].monthly_distribution, + self.doc_date, + self.fiscal_year, + self.budget_map[key].budget_amount, + ) + } + ) + if self.document_type in ["Purchase Order", "Material Request"]: _obj.update({"items_to_process": self.doc_or_item_map[key]}) elif self.document_type == "GL Map": @@ -61,11 +75,9 @@ class BudgetValidation: self.to_validate[key] = OrderedDict(_obj) def validate_for_overbooking(self): - # TODO: Need to fetch historical amount and add them to the current document; GL effect is pending - # TODO: handle applicable checkboxes for key, v in self.to_validate.items(): # Amt from current Purchase Order is included in `self.ordered_amount` as doc is - # in submitted status by the time the validation occurs + # in submitted status by the time validation happens if self.document_type == "Purchase Order": self.get_ordered_amount(key) @@ -77,8 +89,7 @@ class BudgetValidation: elif self.document_type == "GL Map": v["current_amount"] = sum([x.debit - x.credit for x in v.get("gl_to_process", [])]) - # TODO: call stack should be self-explanatory on which doctype the error got thrown - # Exit early before hitting ledger + # If limit breached, exit early self.handle_action(v) if self.document_type == "GL Map": @@ -183,7 +194,6 @@ class BudgetValidation: conditions.append(poi.item_code.isin(items)) # key structure - (dimension_type, dimension, GL account) - # TODO: handle child node on a tree type dimension conditions.append(poi[key[0]].eq(key[1])) ordered_amount = ( @@ -215,7 +225,6 @@ class BudgetValidation: conditions.append(mri.item_code.isin(items)) # key structure - (dimension_type, dimension, GL account) - # TODO: handle child node on a tree type dimension conditions.append(mri[key[0]].eq(key[1])) requested_amount = ( @@ -241,7 +250,6 @@ class BudgetValidation: & gl.account.eq(key[2]) & gl.fiscal_year.eq(self.fiscal_year) & gl.company.eq(self.company) - # TODO: handle child node on a tree type dimension & gl[key[0]].eq(key[1]) & gl.posting_date[self.fy_start_date : self.fy_end_date] ) @@ -258,49 +266,105 @@ class BudgetValidation: def handle_po_action(self, v_map): budget = v_map.get("budget_doc") - if budget.applicable_on_purchase_order and v_map.get("ordered_amount") + v_map.get( - "current_amount" - ) > v_map.get("budget_amount"): - # TODO: handle monthly accumulation - # action_if_accumulated_monthly_budget_exceeded_on_po, - _msg = _("Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name))) + if budget.applicable_on_purchase_order: + if v_map.get("ordered_amount") + v_map.get("current_amount") > v_map.get("budget_amount"): + _msg = _( + "Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name)) + ) - if budget.action_if_annual_budget_exceeded_on_po == "Warn": - self.warn(_msg) + if budget.action_if_annual_budget_exceeded_on_po == "Warn": + self.warn(_msg) - if budget.action_if_annual_budget_exceeded_on_po == "Stop": - self.stop(_msg) + if budget.action_if_annual_budget_exceeded_on_po == "Stop": + self.stop(_msg) + + if v_map.get("ordered_amount") + v_map.get("current_amount") > v_map.get( + "accumulated_monthly_budget" + ): + overlimit = (v_map.get("ordered_amount") + v_map.get("current_amount")) - v_map.get( + "accumulated_monthly_budget" + ) + _msg = _( + "Expenses have gone above accumulated monthly budget by {} for {}.\nCurrent accumulated limit is {}".format( + overlimit, + get_link_to_form("Budget", budget.name), + v_map.get("accumulated_monthly_budget"), + ) + ) + + if budget.action_if_accumulated_monthly_budget_exceeded_on_po == "Warn": + self.warn(_msg) + + if budget.action_if_accumulated_monthly_budget_exceeded_on_po == "Stop": + self.stop(_msg) def handle_mr_action(self, v_map): budget = v_map.get("budget_doc") - if budget.applicable_on_material_request and v_map.get("requested_amount") + v_map.get( - "current_amount" - ) > v_map.get("budget_amount"): - # TODO: handle monthly accumulation - # action_if_accumulated_monthly_budget_exceeded_on_po, - _msg = _("Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name))) + if budget.applicable_on_material_request: + if v_map.get("requested_amount") + v_map.get("current_amount") > v_map.get("budget_amount"): + _msg = _( + "Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name)) + ) - if budget.action_if_annual_budget_exceeded_on_mr == "Warn": - self.warn(_msg) + if budget.action_if_annual_budget_exceeded_on_mr == "Warn": + self.warn(_msg) + if budget.action_if_annual_budget_exceeded_on_mr == "Stop": + self.stop(_msg) - if budget.action_if_annual_budget_exceeded_on_mr == "Stop": - self.stop(_msg) + if v_map.get("requested_amount") + v_map.get("current_amount") > v_map.get( + "accumulated_monthly_budget" + ): + overlimit = (v_map.get("requested_amount") + v_map.get("current_amount")) - v_map.get( + "accumulated_monthly_budget" + ) + _msg = _( + "Expenses have gone above accumulated monthly budget by {} for {}.\nCurrent accumulated limit is {}".format( + overlimit, + get_link_to_form("Budget", budget.name), + v_map.get("accumulated_monthly_budget"), + ) + ) + + if budget.action_if_accumulated_monthly_budget_exceeded_on_po == "Warn": + self.warn(_msg) + + if budget.action_if_accumulated_monthly_budget_exceeded_on_po == "Stop": + self.stop(_msg) def handle_actual_expense_action(self, v_map): budget = v_map.get("budget_doc") frappe.pp(v_map) - if budget.applicable_on_booking_actual_expenses and v_map.get("actual_expense") + v_map.get( - "current_amount" - ) > v_map.get("budget_amount"): - # TODO: handle monthly accumulation - # action_if_accumulated_monthly_budget_exceeded_on_po, - _msg = _("Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name))) + if budget.applicable_on_booking_actual_expenses: + if v_map.get("actual_expense") + v_map.get("current_amount") > v_map.get("budget_amount"): + _msg = _( + "Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name)) + ) - if budget.action_if_annual_budget_exceeded == "Warn": - self.warn(_msg) + if budget.action_if_annual_budget_exceeded == "Warn": + self.warn(_msg) - if budget.action_if_annual_budget_exceeded == "Stop": - self.stop(_msg) + if budget.action_if_annual_budget_exceeded == "Stop": + self.stop(_msg) + + if v_map.get("actual_amount") + v_map.get("current_amount") > v_map.get( + "accumulated_monthly_budget" + ): + overlimit = (v_map.get("actual_amount") + v_map.get("current_amount")) - v_map.get( + "accumulated_monthly_budget" + ) + _msg = _( + "Expenses have gone above accumulated monthly budget by {} for {}.\nCurrent accumulated limit is {}".format( + overlimit, + get_link_to_form("Budget", budget.name), + v_map.get("accumulated_monthly_budget"), + ) + ) + + if budget.action_if_accumulated_monthly_budget_exceeded_on_po == "Warn": + self.warn(_msg) + + if budget.action_if_accumulated_monthly_budget_exceeded_on_po == "Stop": + self.stop(_msg) def handle_action(self, v_map): self.handle_po_action(v_map) From 593729ac2bfcda4a40eb5ed11de8f2f7c06bbdf3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Feb 2025 11:37:49 +0530 Subject: [PATCH 15/35] refactor: code refactor --- erpnext/controllers/budget_controller.py | 157 +++++++++++------------ 1 file changed, 77 insertions(+), 80 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 65c5e8c3ec1..9356a203218 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -264,109 +264,106 @@ class BudgetValidation: def warn(self, msg): frappe.msgprint(msg, _("Budget Exceeded")) - def handle_po_action(self, v_map): - budget = v_map.get("budget_doc") - if budget.applicable_on_purchase_order: - if v_map.get("ordered_amount") + v_map.get("current_amount") > v_map.get("budget_amount"): + def handle_po_action(self, budget_doc, budget_amt, ordered_amt, current_amt, acc_monthly): + if budget_doc.applicable_on_purchase_order: + if ordered_amt + current_amt > budget_amt: _msg = _( - "Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name)) - ) - - if budget.action_if_annual_budget_exceeded_on_po == "Warn": - self.warn(_msg) - - if budget.action_if_annual_budget_exceeded_on_po == "Stop": - self.stop(_msg) - - if v_map.get("ordered_amount") + v_map.get("current_amount") > v_map.get( - "accumulated_monthly_budget" - ): - overlimit = (v_map.get("ordered_amount") + v_map.get("current_amount")) - v_map.get( - "accumulated_monthly_budget" - ) - _msg = _( - "Expenses have gone above accumulated monthly budget by {} for {}.\nCurrent accumulated limit is {}".format( - overlimit, - get_link_to_form("Budget", budget.name), - v_map.get("accumulated_monthly_budget"), + "Expenses have gone above budget by {} for {}".format( + ((ordered_amt + current_amt) - budget_amt), + get_link_to_form("Budget", budget_doc.name), ) ) - if budget.action_if_accumulated_monthly_budget_exceeded_on_po == "Warn": + if budget_doc.action_if_annual_budget_exceeded_on_po == "Warn": self.warn(_msg) - if budget.action_if_accumulated_monthly_budget_exceeded_on_po == "Stop": + if budget_doc.action_if_annual_budget_exceeded_on_po == "Stop": self.stop(_msg) - def handle_mr_action(self, v_map): - budget = v_map.get("budget_doc") - if budget.applicable_on_material_request: - if v_map.get("requested_amount") + v_map.get("current_amount") > v_map.get("budget_amount"): - _msg = _( - "Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name)) - ) - - if budget.action_if_annual_budget_exceeded_on_mr == "Warn": - self.warn(_msg) - if budget.action_if_annual_budget_exceeded_on_mr == "Stop": - self.stop(_msg) - - if v_map.get("requested_amount") + v_map.get("current_amount") > v_map.get( - "accumulated_monthly_budget" - ): - overlimit = (v_map.get("requested_amount") + v_map.get("current_amount")) - v_map.get( - "accumulated_monthly_budget" - ) + if ordered_amt + current_amt > acc_monthly: _msg = _( "Expenses have gone above accumulated monthly budget by {} for {}.\nCurrent accumulated limit is {}".format( - overlimit, - get_link_to_form("Budget", budget.name), - v_map.get("accumulated_monthly_budget"), + ((ordered_amt + current_amt) - acc_monthly), + get_link_to_form("Budget", budget_doc.name), + acc_monthly, ) ) - if budget.action_if_accumulated_monthly_budget_exceeded_on_po == "Warn": + if budget_doc.action_if_accumulated_monthly_budget_exceeded_on_po == "Warn": self.warn(_msg) - if budget.action_if_accumulated_monthly_budget_exceeded_on_po == "Stop": + if budget_doc.action_if_accumulated_monthly_budget_exceeded_on_po == "Stop": self.stop(_msg) - def handle_actual_expense_action(self, v_map): - budget = v_map.get("budget_doc") - frappe.pp(v_map) - if budget.applicable_on_booking_actual_expenses: - if v_map.get("actual_expense") + v_map.get("current_amount") > v_map.get("budget_amount"): + def handle_mr_action(self, budget_doc, budget_amt, requested_amt, current_amt, acc_monthly): + if budget_doc.applicable_on_material_request: + if requested_amt + current_amt > budget_amt: _msg = _( - "Expenses have gone above budget: {}".format(get_link_to_form("Budget", budget.name)) - ) - - if budget.action_if_annual_budget_exceeded == "Warn": - self.warn(_msg) - - if budget.action_if_annual_budget_exceeded == "Stop": - self.stop(_msg) - - if v_map.get("actual_amount") + v_map.get("current_amount") > v_map.get( - "accumulated_monthly_budget" - ): - overlimit = (v_map.get("actual_amount") + v_map.get("current_amount")) - v_map.get( - "accumulated_monthly_budget" - ) - _msg = _( - "Expenses have gone above accumulated monthly budget by {} for {}.\nCurrent accumulated limit is {}".format( - overlimit, - get_link_to_form("Budget", budget.name), - v_map.get("accumulated_monthly_budget"), + "Expenses have gone above budget by {} for {}".format( + ((requested_amt + current_amt) - budget_amt), + get_link_to_form("Budget", budget_doc.name), ) ) - if budget.action_if_accumulated_monthly_budget_exceeded_on_po == "Warn": + if budget_doc.action_if_annual_budget_exceeded_on_mr == "Warn": + self.warn(_msg) + if budget_doc.action_if_annual_budget_exceeded_on_mr == "Stop": + self.stop(_msg) + + if requested_amt + current_amt > acc_monthly: + _msg = _( + "Expenses have gone above accumulated monthly budget by {} for {}.\nCurrent accumulated limit is {}".format( + ((requested_amt + current_amt) - acc_monthly), + get_link_to_form("Budget", budget_doc.name), + acc_monthly, + ) + ) + + if budget_doc.action_if_accumulated_monthly_budget_exceeded_on_mr == "Warn": self.warn(_msg) - if budget.action_if_accumulated_monthly_budget_exceeded_on_po == "Stop": + if budget_doc.action_if_accumulated_monthly_budget_exceeded_on_mr == "Stop": + self.stop(_msg) + + def handle_actual_expense_action(self, budget_doc, budget_amt, actual_exp, current_amt, acc_monthly): + if budget_doc.applicable_on_booking_actual_expenses: + if actual_exp + current_amt > budget_amt: + _msg = _( + "Expenses have gone above budget by {} for {}".format( + ((actual_exp + current_amt) - budget_amt), get_link_to_form("Budget", budget_doc.name) + ) + ) + + if budget_doc.action_if_annual_budget_exceeded == "Warn": + self.warn(_msg) + + if budget_doc.action_if_annual_budget_exceeded == "Stop": + self.stop(_msg) + + if actual_exp + current_amt > acc_monthly: + _msg = _( + "Expenses have gone above accumulated monthly budget by {} for {}.\nCurrent accumulated limit is {}".format( + ((actual_exp + current_amt) - acc_monthly), + get_link_to_form("Budget", budget_doc.name), + acc_monthly, + ) + ) + + if budget_doc.action_if_accumulated_monthly_budget_exceeded == "Warn": + self.warn(_msg) + + if budget_doc.action_if_accumulated_monthly_budget_exceeded == "Stop": self.stop(_msg) def handle_action(self, v_map): - self.handle_po_action(v_map) - self.handle_mr_action(v_map) - self.handle_actual_expense_action(v_map) + budget = v_map.get("budget_doc") + actual_exp = v_map.get("actual_expense") + ordered_amt = v_map.get("ordered_amount") + requested_amt = v_map.get("requested_amount") + current_amt = v_map.get("current_amount") + budget_amt = v_map.get("budget_amount") + acc_monthly_budget = v_map.get("accumulated_monthly_budget") + + self.handle_po_action(budget, budget_amt, ordered_amt, current_amt, acc_monthly_budget) + self.handle_mr_action(budget, budget_amt, requested_amt, current_amt, acc_monthly_budget) + self.handle_actual_expense_action(budget, budget_amt, actual_exp, current_amt, acc_monthly_budget) From 0e016a9c47291788e7a861ce50b643cdf88364b7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Feb 2025 13:42:19 +0530 Subject: [PATCH 16/35] refactor: replace duplicate validation with single method --- erpnext/controllers/budget_controller.py | 137 ++++++++++------------- 1 file changed, 60 insertions(+), 77 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 9356a203218..7afa6157a15 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -4,7 +4,7 @@ import frappe from frappe import _, qb from frappe.query_builder import Criterion from frappe.query_builder.functions import IfNull, Sum -from frappe.utils import get_link_to_form +from frappe.utils import flt, fmt_money, get_link_to_form from erpnext.accounts.doctype.budget.budget import get_accumulated_monthly_budget from erpnext.accounts.utils import get_fiscal_year @@ -264,95 +264,39 @@ class BudgetValidation: def warn(self, msg): frappe.msgprint(msg, _("Budget Exceeded")) - def handle_po_action(self, budget_doc, budget_amt, ordered_amt, current_amt, acc_monthly): - if budget_doc.applicable_on_purchase_order: - if ordered_amt + current_amt > budget_amt: + def handle_individual_doctype_action( + self, config, budget, budget_amt, existing_amt, current_amt, acc_monthly_budget + ): + if config.applies: + currency = frappe.get_cached_value("Company", self.company, "default_currency") + diff = (existing_amt + current_amt) - budget_amt + if diff > 0: _msg = _( "Expenses have gone above budget by {} for {}".format( - ((ordered_amt + current_amt) - budget_amt), - get_link_to_form("Budget", budget_doc.name), + fmt_money(diff, currency=currency), get_link_to_form("Budget", budget) ) ) - if budget_doc.action_if_annual_budget_exceeded_on_po == "Warn": + if config.action_for_annaul == "Warn": self.warn(_msg) - if budget_doc.action_if_annual_budget_exceeded_on_po == "Stop": + if config.action_for_annaul == "Stop": self.stop(_msg) - if ordered_amt + current_amt > acc_monthly: + monthly_diff = (existing_amt + current_amt) - acc_monthly_budget + if monthly_diff: _msg = _( "Expenses have gone above accumulated monthly budget by {} for {}.\nCurrent accumulated limit is {}".format( - ((ordered_amt + current_amt) - acc_monthly), - get_link_to_form("Budget", budget_doc.name), - acc_monthly, + fmt_money(monthly_diff, currency=currency), + get_link_to_form("Budget", budget), + fmt_money(acc_monthly_budget, currency=currency), ) ) - if budget_doc.action_if_accumulated_monthly_budget_exceeded_on_po == "Warn": + if config.action_for_monthly == "Warn": self.warn(_msg) - if budget_doc.action_if_accumulated_monthly_budget_exceeded_on_po == "Stop": - self.stop(_msg) - - def handle_mr_action(self, budget_doc, budget_amt, requested_amt, current_amt, acc_monthly): - if budget_doc.applicable_on_material_request: - if requested_amt + current_amt > budget_amt: - _msg = _( - "Expenses have gone above budget by {} for {}".format( - ((requested_amt + current_amt) - budget_amt), - get_link_to_form("Budget", budget_doc.name), - ) - ) - - if budget_doc.action_if_annual_budget_exceeded_on_mr == "Warn": - self.warn(_msg) - if budget_doc.action_if_annual_budget_exceeded_on_mr == "Stop": - self.stop(_msg) - - if requested_amt + current_amt > acc_monthly: - _msg = _( - "Expenses have gone above accumulated monthly budget by {} for {}.\nCurrent accumulated limit is {}".format( - ((requested_amt + current_amt) - acc_monthly), - get_link_to_form("Budget", budget_doc.name), - acc_monthly, - ) - ) - - if budget_doc.action_if_accumulated_monthly_budget_exceeded_on_mr == "Warn": - self.warn(_msg) - - if budget_doc.action_if_accumulated_monthly_budget_exceeded_on_mr == "Stop": - self.stop(_msg) - - def handle_actual_expense_action(self, budget_doc, budget_amt, actual_exp, current_amt, acc_monthly): - if budget_doc.applicable_on_booking_actual_expenses: - if actual_exp + current_amt > budget_amt: - _msg = _( - "Expenses have gone above budget by {} for {}".format( - ((actual_exp + current_amt) - budget_amt), get_link_to_form("Budget", budget_doc.name) - ) - ) - - if budget_doc.action_if_annual_budget_exceeded == "Warn": - self.warn(_msg) - - if budget_doc.action_if_annual_budget_exceeded == "Stop": - self.stop(_msg) - - if actual_exp + current_amt > acc_monthly: - _msg = _( - "Expenses have gone above accumulated monthly budget by {} for {}.\nCurrent accumulated limit is {}".format( - ((actual_exp + current_amt) - acc_monthly), - get_link_to_form("Budget", budget_doc.name), - acc_monthly, - ) - ) - - if budget_doc.action_if_accumulated_monthly_budget_exceeded == "Warn": - self.warn(_msg) - - if budget_doc.action_if_accumulated_monthly_budget_exceeded == "Stop": + if config.action_for_monthly == "Stop": self.stop(_msg) def handle_action(self, v_map): @@ -364,6 +308,45 @@ class BudgetValidation: budget_amt = v_map.get("budget_amount") acc_monthly_budget = v_map.get("accumulated_monthly_budget") - self.handle_po_action(budget, budget_amt, ordered_amt, current_amt, acc_monthly_budget) - self.handle_mr_action(budget, budget_amt, requested_amt, current_amt, acc_monthly_budget) - self.handle_actual_expense_action(budget, budget_amt, actual_exp, current_amt, acc_monthly_budget) + self.handle_individual_doctype_action( + frappe._dict( + { + "applies": budget.applicable_on_purchase_order, + "action_for_annual": budget.action_if_annual_budget_exceeded_on_po, + "action_for_monthly": budget.action_if_accumulated_monthly_budget_exceeded_on_po, + } + ), + budget.name, + budget_amt, + ordered_amt, + current_amt, + acc_monthly_budget, + ) + self.handle_individual_doctype_action( + frappe._dict( + { + "applies": budget.applicable_on_material_request, + "action_for_annual": budget.action_if_annual_budget_exceeded_on_mr, + "action_for_monthly": budget.action_if_accumulated_monthly_budget_exceeded_on_mr, + } + ), + budget.name, + budget_amt, + requested_amt, + current_amt, + acc_monthly_budget, + ) + self.handle_individual_doctype_action( + frappe._dict( + { + "applies": budget.applicable_on_booking_actual_expenses, + "action_for_annual": budget.action_if_annual_budget_exceeded, + "action_for_monthly": budget.action_if_accumulated_monthly_budget_exceeded, + } + ), + budget.name, + budget_amt, + actual_exp, + current_amt, + acc_monthly_budget, + ) From 3e80248cde38e99115118f7e9214f49b01998eb4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Feb 2025 13:50:15 +0530 Subject: [PATCH 17/35] refactor: PO validation happens after submission --- erpnext/controllers/budget_controller.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 7afa6157a15..e29f178b859 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -76,15 +76,17 @@ class BudgetValidation: def validate_for_overbooking(self): for key, v in self.to_validate.items(): - # Amt from current Purchase Order is included in `self.ordered_amount` as doc is - # in submitted status by the time validation happens if self.document_type == "Purchase Order": self.get_ordered_amount(key) if self.document_type == "Material Request": self.get_requested_amount(key) - if self.document_type in ["Purchase Order", "Material Request"]: + # Amt from current Purchase Order is included in `self.ordered_amount` as doc is + # in submitted status by the time validation happens + if self.document_type in "Purchase Order": + v["current_amount"] = 0 + elif self.document_type in "Material Request": v["current_amount"] = sum([x.amount for x in v.get("items_to_process", [])]) elif self.document_type == "GL Map": v["current_amount"] = sum([x.debit - x.credit for x in v.get("gl_to_process", [])]) @@ -269,11 +271,12 @@ class BudgetValidation: ): if config.applies: currency = frappe.get_cached_value("Company", self.company, "default_currency") - diff = (existing_amt + current_amt) - budget_amt - if diff > 0: + annual_diff = (existing_amt + current_amt) - budget_amt + if annual_diff > 0: _msg = _( "Expenses have gone above budget by {} for {}".format( - fmt_money(diff, currency=currency), get_link_to_form("Budget", budget) + frappe.bold(fmt_money(annual_diff, currency=currency)), + get_link_to_form("Budget", budget), ) ) @@ -286,8 +289,8 @@ class BudgetValidation: monthly_diff = (existing_amt + current_amt) - acc_monthly_budget if monthly_diff: _msg = _( - "Expenses have gone above accumulated monthly budget by {} for {}.\nCurrent accumulated limit is {}".format( - fmt_money(monthly_diff, currency=currency), + "Expenses have gone above accumulated monthly budget by {} for {}.
Configured accumulated limit is {}".format( + frappe.bold(fmt_money(monthly_diff, currency=currency)), get_link_to_form("Budget", budget), fmt_money(acc_monthly_budget, currency=currency), ) From fc24bbf5ade5ece1e2b9d513ca2394366757e3f5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Feb 2025 14:45:12 +0530 Subject: [PATCH 18/35] refactor: handle breach on total expense --- erpnext/controllers/budget_controller.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index e29f178b859..6ae39fd64e9 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -280,14 +280,14 @@ class BudgetValidation: ) ) - if config.action_for_annaul == "Warn": + if config.action_for_annual == "Warn": self.warn(_msg) - if config.action_for_annaul == "Stop": + if config.action_for_annual == "Stop": self.stop(_msg) monthly_diff = (existing_amt + current_amt) - acc_monthly_budget - if monthly_diff: + if monthly_diff > 0: _msg = _( "Expenses have gone above accumulated monthly budget by {} for {}.
Configured accumulated limit is {}".format( frappe.bold(fmt_money(monthly_diff, currency=currency)), @@ -353,3 +353,13 @@ class BudgetValidation: current_amt, acc_monthly_budget, ) + + total_diff = (ordered_amt + requested_amt + actual_exp + current_amt) - budget_amt + if total_diff > 0: + currency = frappe.get_cached_value("Company", self.company, "default_currency") + _msg = _( + "Total Expenses booked across Purchase Order, Material Request and Ledger have gone above budget by {} for {}".format( + frappe.bold(fmt_money(total_diff, currency=currency)), + get_link_to_form("Budget", budget.name), + ) + ) From fcf572e6413b93f71e86e4c155628dc1abb5a85b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Feb 2025 14:55:43 +0530 Subject: [PATCH 19/35] refactor: always query booked expenses --- erpnext/controllers/budget_controller.py | 27 +++++++++++------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 6ae39fd64e9..941df9665a7 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -76,27 +76,21 @@ class BudgetValidation: def validate_for_overbooking(self): for key, v in self.to_validate.items(): - if self.document_type == "Purchase Order": - self.get_ordered_amount(key) + self.get_ordered_amount(key) + self.get_requested_amount(key) - if self.document_type == "Material Request": - self.get_requested_amount(key) - - # Amt from current Purchase Order is included in `self.ordered_amount` as doc is - # in submitted status by the time validation happens - if self.document_type in "Purchase Order": + # Validation happens after submit for Purchase Order and + # Material Request and so will be included in the query + # result + if self.document_type in ["Purchase Order", "Material Request"]: v["current_amount"] = 0 - elif self.document_type in "Material Request": - v["current_amount"] = sum([x.amount for x in v.get("items_to_process", [])]) elif self.document_type == "GL Map": v["current_amount"] = sum([x.debit - x.credit for x in v.get("gl_to_process", [])]) # If limit breached, exit early self.handle_action(v) - if self.document_type == "GL Map": - self.get_actual_expense(key) - + self.get_actual_expense(key) self.handle_action(v) def build_budget_keys_and_map(self): @@ -206,8 +200,9 @@ class BudgetValidation: .where(Criterion.all(conditions)) .run(as_dict=True) ) + if ordered_amount: - self.to_validate[key]["ordered_amount"] = ordered_amount[0].amount + self.to_validate[key]["ordered_amount"] = ordered_amount[0].amount or 0 def get_requested_amount(self, key: tuple | None = None): if key: @@ -237,8 +232,9 @@ class BudgetValidation: .where(Criterion.all(conditions)) .run(as_dict=True) ) + if requested_amount: - self.to_validate[key]["requested_amount"] = requested_amount[0].amount + self.to_validate[key]["requested_amount"] = requested_amount[0].amount or 0 def get_actual_expense(self, key: tuple | None = None): if key: @@ -363,3 +359,4 @@ class BudgetValidation: get_link_to_form("Budget", budget.name), ) ) + self.stop(_msg) From 3064646a8f3cac8dc84db0dcfe88ab0151b4ebbb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Feb 2025 16:22:42 +0530 Subject: [PATCH 20/35] refactor: better error message --- erpnext/controllers/budget_controller.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 941df9665a7..70c9c73e181 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -88,10 +88,10 @@ class BudgetValidation: v["current_amount"] = sum([x.debit - x.credit for x in v.get("gl_to_process", [])]) # If limit breached, exit early - self.handle_action(v) + self.handle_action(key, v) self.get_actual_expense(key) - self.handle_action(v) + self.handle_action(key, v) def build_budget_keys_and_map(self): """ @@ -298,7 +298,7 @@ class BudgetValidation: if config.action_for_monthly == "Stop": self.stop(_msg) - def handle_action(self, v_map): + def handle_action(self, key, v_map): budget = v_map.get("budget_doc") actual_exp = v_map.get("actual_expense") ordered_amt = v_map.get("ordered_amount") @@ -354,9 +354,12 @@ class BudgetValidation: if total_diff > 0: currency = frappe.get_cached_value("Company", self.company, "default_currency") _msg = _( - "Total Expenses booked across Purchase Order, Material Request and Ledger have gone above budget by {} for {}".format( + "Annual Budget for Account {} against {} {} is {}. It will be exceeded by {}".format( + frappe.bold(key[2]), + frappe.bold(frappe.unscrub(key[0])), + frappe.bold(key[1]), + frappe.bold(fmt_money(budget_amt, currency=currency)), frappe.bold(fmt_money(total_diff, currency=currency)), - get_link_to_form("Budget", budget.name), ) ) self.stop(_msg) From 49bb72bcd2facdbd82dfca968a50d0331507a2bd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Feb 2025 16:31:17 +0530 Subject: [PATCH 21/35] refactor: better query parameters for PO and MR --- erpnext/controllers/budget_controller.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 70c9c73e181..89a6d009fb4 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -174,9 +174,6 @@ class BudgetValidation: def get_ordered_amount(self, key: tuple | None = None): if key: - items = set([x.item_code for x in self.doc.items]) - exp_accounts = set([x.expense_account for x in self.doc.items]) - po = qb.DocType("Purchase Order") poi = qb.DocType("Purchase Order Item") @@ -186,8 +183,11 @@ class BudgetValidation: conditions.append(po.status.ne("Closed")) conditions.append(po.transaction_date[self.fy_start_date : self.fy_end_date]) conditions.append(poi.amount.gt(poi.billed_amt)) - conditions.append(poi.expense_account.isin(exp_accounts)) - conditions.append(poi.item_code.isin(items)) + conditions.append(poi.expense_account.eq(key[2])) + + if self.document_type in ["Purchase Order", "Material Request"]: + if items := set([x.item_code for x in self.doc.items]): + conditions.append(poi.item_code.isin(items)) # key structure - (dimension_type, dimension, GL account) conditions.append(poi[key[0]].eq(key[1])) @@ -206,9 +206,6 @@ class BudgetValidation: def get_requested_amount(self, key: tuple | None = None): if key: - items = set([x.item_code for x in self.doc.items]) - exp_accounts = set([x.expense_account for x in self.doc.items]) - mr = qb.DocType("Material Request") mri = qb.DocType("Material Request Item") @@ -218,8 +215,11 @@ class BudgetValidation: conditions.append(mr.material_request_type.eq("Purchase")) conditions.append(mr.status.ne("Stopped")) conditions.append(mr.transaction_date[self.fy_start_date : self.fy_end_date]) - conditions.append(mri.expense_account.isin(exp_accounts)) - conditions.append(mri.item_code.isin(items)) + conditions.append(mri.expense_account.eq(key[2])) + + if self.document_type in ["Purchase Order", "Material Request"]: + if items := set([x.item_code for x in self.doc.items]): + conditions.append(mri.item_code.isin(items)) # key structure - (dimension_type, dimension, GL account) conditions.append(mri[key[0]].eq(key[1])) From 2ecb2fa4af49e212e320bbe60226c501d70727e2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Feb 2025 17:00:02 +0530 Subject: [PATCH 22/35] refactor: stateful variables --- erpnext/controllers/budget_controller.py | 39 +++++++++++++++--------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 89a6d009fb4..c166416ddff 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -55,6 +55,9 @@ class BudgetValidation: "requested_amount": 0, "ordered_amount": 0, "actual_expense": 0, + "current_requested_amount": 0, + "current_ordered_amount": 0, + "current_actual_exp_amount": 0, } _obj.update( { @@ -79,16 +82,13 @@ class BudgetValidation: self.get_ordered_amount(key) self.get_requested_amount(key) + self.handle_action(key, v) + # Validation happens after submit for Purchase Order and # Material Request and so will be included in the query - # result - if self.document_type in ["Purchase Order", "Material Request"]: - v["current_amount"] = 0 - elif self.document_type == "GL Map": - v["current_amount"] = sum([x.debit - x.credit for x in v.get("gl_to_process", [])]) - - # If limit breached, exit early - self.handle_action(key, v) + # result. so no need to set current document amount + if self.document_type == "GL Map": + v["current_actual_exp_amount"] = sum([x.debit - x.credit for x in v.get("gl_to_process", [])]) self.get_actual_expense(key) self.handle_action(key, v) @@ -263,16 +263,19 @@ class BudgetValidation: frappe.msgprint(msg, _("Budget Exceeded")) def handle_individual_doctype_action( - self, config, budget, budget_amt, existing_amt, current_amt, acc_monthly_budget + self, key, config, budget, budget_amt, existing_amt, current_amt, acc_monthly_budget ): if config.applies: currency = frappe.get_cached_value("Company", self.company, "default_currency") annual_diff = (existing_amt + current_amt) - budget_amt if annual_diff > 0: _msg = _( - "Expenses have gone above budget by {} for {}".format( + "Annual Budget for Account {} against {} {} is {}. It will be exceeded by {}".format( + frappe.bold(key[2]), + frappe.bold(frappe.unscrub(key[0])), + frappe.bold(key[1]), + frappe.bold(fmt_money(budget_amt, currency=currency)), frappe.bold(fmt_money(annual_diff, currency=currency)), - get_link_to_form("Budget", budget), ) ) @@ -301,13 +304,16 @@ class BudgetValidation: def handle_action(self, key, v_map): budget = v_map.get("budget_doc") actual_exp = v_map.get("actual_expense") + cur_actual_exp = v_map.get("current_actual_exp_amount") ordered_amt = v_map.get("ordered_amount") + cur_ordered_amt = v_map.get("current_ordered_amount") requested_amt = v_map.get("requested_amount") - current_amt = v_map.get("current_amount") + cur_requested_amt = v_map.get("current_requested_amount") budget_amt = v_map.get("budget_amount") acc_monthly_budget = v_map.get("accumulated_monthly_budget") self.handle_individual_doctype_action( + key, frappe._dict( { "applies": budget.applicable_on_purchase_order, @@ -318,10 +324,11 @@ class BudgetValidation: budget.name, budget_amt, ordered_amt, - current_amt, + cur_ordered_amt, acc_monthly_budget, ) self.handle_individual_doctype_action( + key, frappe._dict( { "applies": budget.applicable_on_material_request, @@ -332,10 +339,11 @@ class BudgetValidation: budget.name, budget_amt, requested_amt, - current_amt, + cur_requested_amt, acc_monthly_budget, ) self.handle_individual_doctype_action( + key, frappe._dict( { "applies": budget.applicable_on_booking_actual_expenses, @@ -346,10 +354,11 @@ class BudgetValidation: budget.name, budget_amt, actual_exp, - current_amt, + cur_actual_exp, acc_monthly_budget, ) + current_amt = cur_ordered_amt + cur_requested_amt + cur_actual_exp total_diff = (ordered_amt + requested_amt + actual_exp + current_amt) - budget_amt if total_diff > 0: currency = frappe.get_cached_value("Company", self.company, "default_currency") From b7e70bb746153a311bedb9613d7039c46d35ae2f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Feb 2025 17:44:33 +0530 Subject: [PATCH 23/35] refactor: better error message --- erpnext/controllers/budget_controller.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index c166416ddff..a7401290237 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -270,12 +270,12 @@ class BudgetValidation: annual_diff = (existing_amt + current_amt) - budget_amt if annual_diff > 0: _msg = _( - "Annual Budget for Account {} against {} {} is {}. It will be exceeded by {}".format( + "Annual Budget for Account {} against {}: {} is {}. It will be exceeded by {}".format( frappe.bold(key[2]), frappe.bold(frappe.unscrub(key[0])), frappe.bold(key[1]), - frappe.bold(fmt_money(budget_amt, currency=currency)), frappe.bold(fmt_money(annual_diff, currency=currency)), + frappe.bold(fmt_money(budget_amt, currency=currency)), ) ) @@ -288,10 +288,12 @@ class BudgetValidation: monthly_diff = (existing_amt + current_amt) - acc_monthly_budget if monthly_diff > 0: _msg = _( - "Expenses have gone above accumulated monthly budget by {} for {}.
Configured accumulated limit is {}".format( + "Accumulated Monthly Budget for Account {} against {}: {} is {}. It will be exceeded by {}".format( + frappe.bold(key[2]), + frappe.bold(frappe.unscrub(key[0])), + frappe.bold(key[1]), + frappe.bold(fmt_money(acc_monthly_budget, currency=currency)), frappe.bold(fmt_money(monthly_diff, currency=currency)), - get_link_to_form("Budget", budget), - fmt_money(acc_monthly_budget, currency=currency), ) ) From d9d2020b4639b473f98d845fab6daecdeea5ecf6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Feb 2025 17:47:37 +0530 Subject: [PATCH 24/35] refactor: allow for better translation --- erpnext/controllers/budget_controller.py | 42 ++++++++++++------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index a7401290237..1559abd0417 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -270,13 +270,13 @@ class BudgetValidation: annual_diff = (existing_amt + current_amt) - budget_amt if annual_diff > 0: _msg = _( - "Annual Budget for Account {} against {}: {} is {}. It will be exceeded by {}".format( - frappe.bold(key[2]), - frappe.bold(frappe.unscrub(key[0])), - frappe.bold(key[1]), - frappe.bold(fmt_money(annual_diff, currency=currency)), - frappe.bold(fmt_money(budget_amt, currency=currency)), - ) + "Annual Budget for Account {0} against {1}: {2} is {3}. It will be exceeded by {4}" + ).format( + frappe.bold(key[2]), + frappe.bold(frappe.unscrub(key[0])), + frappe.bold(key[1]), + frappe.bold(fmt_money(annual_diff, currency=currency)), + frappe.bold(fmt_money(budget_amt, currency=currency)), ) if config.action_for_annual == "Warn": @@ -288,13 +288,13 @@ class BudgetValidation: monthly_diff = (existing_amt + current_amt) - acc_monthly_budget if monthly_diff > 0: _msg = _( - "Accumulated Monthly Budget for Account {} against {}: {} is {}. It will be exceeded by {}".format( - frappe.bold(key[2]), - frappe.bold(frappe.unscrub(key[0])), - frappe.bold(key[1]), - frappe.bold(fmt_money(acc_monthly_budget, currency=currency)), - frappe.bold(fmt_money(monthly_diff, currency=currency)), - ) + "Accumulated Monthly Budget for Account {0} against {1}: {2} is {3}. It will be exceeded by {4}" + ).format( + frappe.bold(key[2]), + frappe.bold(frappe.unscrub(key[0])), + frappe.bold(key[1]), + frappe.bold(fmt_money(acc_monthly_budget, currency=currency)), + frappe.bold(fmt_money(monthly_diff, currency=currency)), ) if config.action_for_monthly == "Warn": @@ -365,12 +365,12 @@ class BudgetValidation: if total_diff > 0: currency = frappe.get_cached_value("Company", self.company, "default_currency") _msg = _( - "Annual Budget for Account {} against {} {} is {}. It will be exceeded by {}".format( - frappe.bold(key[2]), - frappe.bold(frappe.unscrub(key[0])), - frappe.bold(key[1]), - frappe.bold(fmt_money(budget_amt, currency=currency)), - frappe.bold(fmt_money(total_diff, currency=currency)), - ) + "Annual Budget for Account {0} against {1} {2} is {3}. It will be exceeded by {4}" + ).format( + frappe.bold(key[2]), + frappe.bold(frappe.unscrub(key[0])), + frappe.bold(key[1]), + frappe.bold(fmt_money(budget_amt, currency=currency)), + frappe.bold(fmt_money(total_diff, currency=currency)), ) self.stop(_msg) From 11f7c1e49ad3553f499999550888e7374a54188f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Feb 2025 17:51:14 +0530 Subject: [PATCH 25/35] refactor: validate on GL creation --- erpnext/accounts/general_ledger.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 959a9828e5c..4f5fe0c4023 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -20,6 +20,7 @@ from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_f from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.utils import create_payment_ledger_entry +from erpnext.controllers.budget_controller import BudgetValidation from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError @@ -39,6 +40,9 @@ def make_gl_entries( gl_map = process_gl_map(gl_map, merge_entries, from_repost=from_repost) if gl_map and len(gl_map) > 1: if gl_map[0].voucher_type != "Period Closing Voucher": + bud_val = BudgetValidation(gl_map=gl_map) + bud_val.validate() + create_payment_ledger_entry( gl_map, cancel=0, @@ -191,11 +195,12 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_r for d in gl_map: cost_center = d.get("cost_center") + # TODO: is a separate validation on cost center allocation required? # Validate budget against main cost center - if not from_repost: - validate_expense_against_budget( - d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision) - ) + # if not from_repost: + # validate_expense_against_budget( + # d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision) + # ) cost_center_allocation = get_cost_center_allocation_data( gl_map[0]["company"], gl_map[0]["posting_date"], cost_center @@ -397,8 +402,8 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False): gle.flags.notify_update = False gle.submit() - if not from_repost and gle.voucher_type != "Period Closing Voucher": - validate_expense_against_budget(args) + # if not from_repost and gle.voucher_type != "Period Closing Voucher": + # validate_expense_against_budget(args) def validate_cwip_accounts(gl_map): From f886b50e7a033f09c828960c8bec12a31cadce0e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 21 Feb 2025 12:27:57 +0530 Subject: [PATCH 26/35] refactor: handle group nodes --- erpnext/controllers/budget_controller.py | 27 ++++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 1559abd0417..8d4209d5833 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -6,14 +6,10 @@ from frappe.query_builder import Criterion from frappe.query_builder.functions import IfNull, Sum from frappe.utils import flt, fmt_money, get_link_to_form -from erpnext.accounts.doctype.budget.budget import get_accumulated_monthly_budget +from erpnext.accounts.doctype.budget.budget import BudgetError, get_accumulated_monthly_budget from erpnext.accounts.utils import get_fiscal_year -class BudgetExceededError(frappe.ValidationError): - pass - - class BudgetValidation: def __init__(self, doc: object | None = None, gl_map: list | None = None): if doc: @@ -93,6 +89,12 @@ class BudgetValidation: self.get_actual_expense(key) self.handle_action(key, v) + def get_child_nodes(self, budget_against, dimension): + lft, rgt = frappe.db.get_all( + budget_against, filters={"name": dimension}, fields=["lft", "rgt"], as_list=1 + )[0] + return frappe.db.get_all(budget_against, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, as_list=1) + def build_budget_keys_and_map(self): """ key structure - (dimension_type, dimension, GL account) @@ -102,9 +104,16 @@ class BudgetValidation: for _bud in _budgets: budget_against = frappe.scrub(_bud.budget_against) dimension = _bud.get(budget_against) - key = (budget_against, dimension, _bud.account) - # TODO: ensure duplicate keys are not possible - self.budget_map[key] = _bud + + if frappe.db.get_value(_bud.budget_against, dimension, "is_group"): + child_nodes = self.get_child_nodes(_bud.budget_against, dimension) + for child in child_nodes: + key = (budget_against, child[0], _bud.account) + self.budget_map[key] = _bud + else: + key = (budget_against, dimension, _bud.account) + # TODO: ensure duplicate keys are not possible + self.budget_map[key] = _bud self.budget_keys = self.budget_map.keys() def build_doc_or_item_keys_and_map(self): @@ -257,7 +266,7 @@ class BudgetValidation: self.to_validate[key]["actual_expense"] = actual_expense[0].balance or 0 def stop(self, msg): - frappe.throw(msg, BudgetExceededError, title=_("Budget Exceeded")) + frappe.throw(msg, BudgetError, title=_("Budget Exceeded")) def warn(self, msg): frappe.msgprint(msg, _("Budget Exceeded")) From 55cb91ce2098699b10ff74e0306f8e78c073730e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 8 Apr 2025 17:35:45 +0530 Subject: [PATCH 27/35] refactor: use meta to identify tree --- erpnext/controllers/budget_controller.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 8d4209d5833..bf54e148252 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -105,7 +105,7 @@ class BudgetValidation: budget_against = frappe.scrub(_bud.budget_against) dimension = _bud.get(budget_against) - if frappe.db.get_value(_bud.budget_against, dimension, "is_group"): + if _bud.is_tree and frappe.db.get_value(_bud.budget_against, dimension, "is_group"): child_nodes = self.get_child_nodes(_bud.budget_against, dimension) for child in child_nodes: key = (budget_against, child[0], _bud.account) @@ -179,6 +179,10 @@ class BudgetValidation: query = query.select(bud[x.get("fieldname")]) _budgets = query.run(as_dict=True) + + for x in _budgets: + x.is_tree = frappe.get_meta(x.budget_against).is_tree + return _budgets def get_ordered_amount(self, key: tuple | None = None): From 3fb5d835f2259bca7becb3bb1bf899a8ffc77e95 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 9 May 2025 16:33:38 +0530 Subject: [PATCH 28/35] refactor: validate budget on cancel as well --- erpnext/accounts/general_ledger.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 4f5fe0c4023..6d123308a6d 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -33,6 +33,10 @@ def make_gl_entries( from_repost=False, ): if gl_map: + if gl_map[0].voucher_type != "Period Closing Voucher": + bud_val = BudgetValidation(gl_map=gl_map) + bud_val.validate() + if not cancel: make_acc_dimensions_offsetting_entry(gl_map) validate_accounting_period(gl_map) @@ -40,9 +44,6 @@ def make_gl_entries( gl_map = process_gl_map(gl_map, merge_entries, from_repost=from_repost) if gl_map and len(gl_map) > 1: if gl_map[0].voucher_type != "Period Closing Voucher": - bud_val = BudgetValidation(gl_map=gl_map) - bud_val.validate() - create_payment_ledger_entry( gl_map, cancel=0, From a7202201f737fe133f7e188e0c5481bca49dcdd7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 15 May 2025 20:32:14 +0530 Subject: [PATCH 29/35] refactor: fetch monthly distribution as well --- erpnext/controllers/budget_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index bf54e148252..f983fe685ab 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -159,6 +159,7 @@ class BudgetValidation: bud.name, bud.budget_against, bud.company, + bud.monthly_distribution, bud.applicable_on_material_request, bud.action_if_annual_budget_exceeded_on_mr, bud.action_if_accumulated_monthly_budget_exceeded_on_mr, From 58556c82bb392e74cd927f028bd0acee2c0ca613 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 May 2025 07:32:52 +0530 Subject: [PATCH 30/35] refactor: handle exception approver role for budget --- erpnext/controllers/budget_controller.py | 30 ++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index f983fe685ab..0f0b4aecc7d 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -30,6 +30,9 @@ class BudgetValidation: self.fy_start_date = fy[1] self.fy_end_date = fy[2] self.get_dimensions() + self.exception_approver_role = frappe.get_cached_value( + "Company", self.company, "exception_budget_approver_role" + ) def validate(self): self.build_validation_map() @@ -276,6 +279,19 @@ class BudgetValidation: def warn(self, msg): frappe.msgprint(msg, _("Budget Exceeded")) + def execute_action(self, action, msg): + if self.exception_approver_role and self.exception_approver_role in frappe.get_roles( + frappe.session.user + ): + self.warn(msg) + return + + if action == "Warn": + self.warn(msg) + + if action == "Stop": + self.stop(msg) + def handle_individual_doctype_action( self, key, config, budget, budget_amt, existing_amt, current_amt, acc_monthly_budget ): @@ -292,12 +308,7 @@ class BudgetValidation: frappe.bold(fmt_money(annual_diff, currency=currency)), frappe.bold(fmt_money(budget_amt, currency=currency)), ) - - if config.action_for_annual == "Warn": - self.warn(_msg) - - if config.action_for_annual == "Stop": - self.stop(_msg) + self.execute_action(config.action_for_annual, _msg) monthly_diff = (existing_amt + current_amt) - acc_monthly_budget if monthly_diff > 0: @@ -310,12 +321,7 @@ class BudgetValidation: frappe.bold(fmt_money(acc_monthly_budget, currency=currency)), frappe.bold(fmt_money(monthly_diff, currency=currency)), ) - - if config.action_for_monthly == "Warn": - self.warn(_msg) - - if config.action_for_monthly == "Stop": - self.stop(_msg) + self.execute_action(config.action_for_monthly, _msg) def handle_action(self, key, v_map): budget = v_map.get("budget_doc") From d4ac042d85e28c389c808d969cfa52d5b4d5e95d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 May 2025 11:08:42 +0530 Subject: [PATCH 31/35] chore: make new budget controller configurable --- .../accounts_settings/accounts_settings.json | 17 +++++++- .../accounts_settings/accounts_settings.py | 1 + erpnext/accounts/general_ledger.py | 18 +++++---- erpnext/controllers/buying_controller.py | 40 +++++++++---------- 4 files changed, 46 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index ae4adfa8a67..8acb821b1e5 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -86,7 +86,9 @@ "legacy_section", "ignore_is_opening_check_for_reporting", "payment_request_settings", - "create_pr_in_draft_status" + "create_pr_in_draft_status", + "budget_settings", + "use_new_budget_controller" ], "fields": [ { @@ -565,6 +567,17 @@ "fieldname": "legacy_section", "fieldtype": "Section Break", "label": "Legacy Fields" + }, + { + "fieldname": "budget_settings", + "fieldtype": "Tab Break", + "label": "Budget" + }, + { + "default": "1", + "fieldname": "use_new_budget_controller", + "fieldtype": "Check", + "label": "Use New Budget Controller" } ], "grid_page_length": 50, @@ -573,7 +586,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-05-05 12:29:38.302027", + "modified": "2025-05-16 11:08:00.796886", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index bbf1c2ef060..2735653bb45 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -67,6 +67,7 @@ class AccountsSettings(Document): submit_journal_entries: DF.Check unlink_advance_payment_on_cancelation_of_order: DF.Check unlink_payment_on_cancellation_of_invoice: DF.Check + use_new_budget_controller: DF.Check use_sales_invoice_in_pos: DF.Check # end: auto-generated types diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 6d123308a6d..edf8c2e3581 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -33,7 +33,10 @@ def make_gl_entries( from_repost=False, ): if gl_map: - if gl_map[0].voucher_type != "Period Closing Voucher": + if ( + frappe.db.get_single_value("Accounts Settings", "use_new_budget_controller") + and gl_map[0].voucher_type != "Period Closing Voucher" + ): bud_val = BudgetValidation(gl_map=gl_map) bud_val.validate() @@ -196,12 +199,11 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_r for d in gl_map: cost_center = d.get("cost_center") - # TODO: is a separate validation on cost center allocation required? # Validate budget against main cost center - # if not from_repost: - # validate_expense_against_budget( - # d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision) - # ) + if not from_repost: + validate_expense_against_budget( + d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision) + ) cost_center_allocation = get_cost_center_allocation_data( gl_map[0]["company"], gl_map[0]["posting_date"], cost_center @@ -403,8 +405,8 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False): gle.flags.notify_update = False gle.submit() - # if not from_repost and gle.voucher_type != "Period Closing Voucher": - # validate_expense_against_budget(args) + if not from_repost and gle.voucher_type != "Period Closing Voucher": + validate_expense_against_budget(args) def validate_cwip_accounts(gl_map): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index ed2d98f3469..c296850428f 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -775,28 +775,28 @@ class BuyingController(SubcontractingController): self.update_fixed_asset(field, delete_asset=True) def validate_budget(self): - from erpnext.controllers.budget_controller import BudgetValidation + if frappe.db.get_single_value("Accounts Settings", "use_new_budget_controller"): + from erpnext.controllers.budget_controller import BudgetValidation - val = BudgetValidation(doc=self) - val.validate() - return + val = BudgetValidation(doc=self) + val.validate() + else: + if self.docstatus == 1: + for data in self.get("items"): + args = data.as_dict() + args.update( + { + "doctype": self.doctype, + "company": self.company, + "posting_date": ( + self.schedule_date + if self.doctype == "Material Request" + else self.transaction_date + ), + } + ) - if self.docstatus == 1: - for data in self.get("items"): - args = data.as_dict() - args.update( - { - "doctype": self.doctype, - "company": self.company, - "posting_date": ( - self.schedule_date - if self.doctype == "Material Request" - else self.transaction_date - ), - } - ) - - validate_expense_against_budget(args) + validate_expense_against_budget(args) def process_fixed_asset(self): if self.doctype == "Purchase Invoice" and not self.update_stock: From ee3d7db29d4f5e8140f3a2fdbd68326067f2e2a7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 May 2025 11:13:27 +0530 Subject: [PATCH 32/35] refactor(test): tests should use new controller --- erpnext/accounts/doctype/budget/test_budget.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index ca9665d4a9e..32fd27e0858 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -23,6 +23,9 @@ class TestBudget(ERPNextTestSuite): cls.make_monthly_distribution() cls.make_projects() + def setUp(self): + frappe.db.set_single_value("Accounts Settings", "use_new_budget_controller", True) + def test_monthly_budget_crossed_ignore(self): set_total_expense_zero(nowdate(), "cost_center") From 6fabedd0da2a7ce7b13ad5e7c9c31b44918378d3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 May 2025 12:02:07 +0530 Subject: [PATCH 33/35] refactor: cleaner code with less verbosity --- erpnext/controllers/budget_controller.py | 103 ++++++++++++----------- 1 file changed, 54 insertions(+), 49 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 0f0b4aecc7d..453c9153d77 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -39,16 +39,13 @@ class BudgetValidation: self.validate_for_overbooking() def build_validation_map(self): - self.build_budget_keys_and_map() - self.build_doc_or_item_keys_and_map() - self.find_overlap() + self.build_budget_keys() + self.build_item_keys() + self.build_to_validate_map() - def find_overlap(self): - self.overlap = self.budget_keys & self.doc_or_item_keys - self.to_validate = OrderedDict() - - for key in self.overlap: - _obj = { + def initialize_dict(self, key): + _obj = frappe._dict( + { "budget_amount": self.budget_map[key].budget_amount, "budget_doc": self.budget_map[key], "requested_amount": 0, @@ -58,23 +55,32 @@ class BudgetValidation: "current_ordered_amount": 0, "current_actual_exp_amount": 0, } - _obj.update( - { - "accumulated_monthly_budget": get_accumulated_monthly_budget( - self.budget_map[key].monthly_distribution, - self.doc_date, - self.fiscal_year, - self.budget_map[key].budget_amount, - ) - } - ) + ) + _obj.update( + { + "accumulated_monthly_budget": get_accumulated_monthly_budget( + self.budget_map[key].monthly_distribution, + self.doc_date, + self.fiscal_year, + self.budget_map[key].budget_amount, + ) + } + ) - if self.document_type in ["Purchase Order", "Material Request"]: - _obj.update({"items_to_process": self.doc_or_item_map[key]}) - elif self.document_type == "GL Map": - _obj.update({"gl_to_process": self.doc_or_item_map[key]}) + if self.document_type in ["Purchase Order", "Material Request"]: + _obj.update({"items_to_process": self.item_map[key]}) + elif self.document_type == "GL Map": + _obj.update({"gl_to_process": self.item_map[key]}) + return _obj - self.to_validate[key] = OrderedDict(_obj) + @property + def overlap(self): + return self.budget_keys & self.item_keys + + def build_to_validate_map(self): + self.to_validate = frappe._dict() + for key in self.overlap: + self.to_validate[key] = self.initialize_dict(key) def validate_for_overbooking(self): for key, v in self.to_validate.items(): @@ -87,7 +93,7 @@ class BudgetValidation: # Material Request and so will be included in the query # result. so no need to set current document amount if self.document_type == "GL Map": - v["current_actual_exp_amount"] = sum([x.debit - x.credit for x in v.get("gl_to_process", [])]) + v.current_actual_exp_amount = sum([x.debit - x.credit for x in v.get("gl_to_process", [])]) self.get_actual_expense(key) self.handle_action(key, v) @@ -98,17 +104,20 @@ class BudgetValidation: )[0] return frappe.db.get_all(budget_against, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, as_list=1) - def build_budget_keys_and_map(self): + @property + def budget_keys(self): + return self.budget_map.keys() + + def build_budget_keys(self): """ key structure - (dimension_type, dimension, GL account) """ - _budgets = self.get_budget_records() self.budget_map = OrderedDict() - for _bud in _budgets: + for _bud in self.get_budget_records(): budget_against = frappe.scrub(_bud.budget_against) dimension = _bud.get(budget_against) - if _bud.is_tree and frappe.db.get_value(_bud.budget_against, dimension, "is_group"): + if _bud.is_tree and frappe.get_cached_value(_bud.budget_against, dimension, "is_group"): child_nodes = self.get_child_nodes(_bud.budget_against, dimension) for child in child_nodes: key = (budget_against, child[0], _bud.account) @@ -117,28 +126,29 @@ class BudgetValidation: key = (budget_against, dimension, _bud.account) # TODO: ensure duplicate keys are not possible self.budget_map[key] = _bud - self.budget_keys = self.budget_map.keys() - def build_doc_or_item_keys_and_map(self): + @property + def item_keys(self): + return self.item_map.keys() + + def build_item_keys(self): """ key structure - (dimension_type, dimension, GL account) """ - self.doc_or_item_map = OrderedDict() + self.item_map = OrderedDict() if self.document_type in ["Purchase Order", "Material Request"]: for itm in self.doc.items: for dim in self.dimensions: if itm.get(dim.get("fieldname")): key = (dim.get("fieldname"), itm.get(dim.get("fieldname")), itm.expense_account) # TODO: How to handle duplicate items - same item with same dimension with same account - self.doc_or_item_map.setdefault(key, []).append(itm) + self.item_map.setdefault(key, []).append(itm) elif self.document_type == "GL Map": for gl in self.gl_map: for dim in self.dimensions: if gl.get(dim.get("fieldname")): key = (dim.get("fieldname"), gl.get(dim.get("fieldname")), gl.get("account")) - self.doc_or_item_map.setdefault(key, []).append(gl) - - self.doc_or_item_keys = self.doc_or_item_map.keys() + self.item_map.setdefault(key, []).append(gl) def get_dimensions(self): self.dimensions = [] @@ -209,17 +219,15 @@ class BudgetValidation: # key structure - (dimension_type, dimension, GL account) conditions.append(poi[key[0]].eq(key[1])) - ordered_amount = ( + if ordered_amount := ( qb.from_(po) .inner_join(poi) .on(po.name == poi.parent) .select(Sum(IfNull(poi.amount, 0) - IfNull(poi.billed_amt, 0)).as_("amount")) .where(Criterion.all(conditions)) .run(as_dict=True) - ) - - if ordered_amount: - self.to_validate[key]["ordered_amount"] = ordered_amount[0].amount or 0 + ): + self.to_validate[key].ordered_amount = ordered_amount[0].amount or 0 def get_requested_amount(self, key: tuple | None = None): if key: @@ -241,17 +249,15 @@ class BudgetValidation: # key structure - (dimension_type, dimension, GL account) conditions.append(mri[key[0]].eq(key[1])) - requested_amount = ( + if requested_amount := ( qb.from_(mr) .inner_join(mri) .on(mr.name == mri.parent) .select((Sum(IfNull(mri.stock_qty, 0) - IfNull(mri.ordered_qty, 0)) * mri.rate).as_("amount")) .where(Criterion.all(conditions)) .run(as_dict=True) - ) - - if requested_amount: - self.to_validate[key]["requested_amount"] = requested_amount[0].amount or 0 + ): + self.to_validate[key].requested_amount = requested_amount[0].amount or 0 def get_actual_expense(self, key: tuple | None = None): if key: @@ -269,9 +275,8 @@ class BudgetValidation: & gl.posting_date[self.fy_start_date : self.fy_end_date] ) ) - actual_expense = query.run(as_dict=True) - if actual_expense: - self.to_validate[key]["actual_expense"] = actual_expense[0].balance or 0 + if actual_expense := query.run(as_dict=True): + self.to_validate[key].actual_expense = actual_expense[0].balance or 0 def stop(self, msg): frappe.throw(msg, BudgetError, title=_("Budget Exceeded")) From e1f32df5b3aa0f0c313d2d7a0bc923e6cc1f057b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 May 2025 12:06:13 +0530 Subject: [PATCH 34/35] refactor: make use of frappe._dict --- erpnext/controllers/budget_controller.py | 68 +++++++++++------------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/erpnext/controllers/budget_controller.py b/erpnext/controllers/budget_controller.py index 453c9153d77..6db7cfa0cc2 100644 --- a/erpnext/controllers/budget_controller.py +++ b/erpnext/controllers/budget_controller.py @@ -329,64 +329,58 @@ class BudgetValidation: self.execute_action(config.action_for_monthly, _msg) def handle_action(self, key, v_map): - budget = v_map.get("budget_doc") - actual_exp = v_map.get("actual_expense") - cur_actual_exp = v_map.get("current_actual_exp_amount") - ordered_amt = v_map.get("ordered_amount") - cur_ordered_amt = v_map.get("current_ordered_amount") - requested_amt = v_map.get("requested_amount") - cur_requested_amt = v_map.get("current_requested_amount") - budget_amt = v_map.get("budget_amount") - acc_monthly_budget = v_map.get("accumulated_monthly_budget") - self.handle_individual_doctype_action( key, frappe._dict( { - "applies": budget.applicable_on_purchase_order, - "action_for_annual": budget.action_if_annual_budget_exceeded_on_po, - "action_for_monthly": budget.action_if_accumulated_monthly_budget_exceeded_on_po, + "applies": v_map.budget_doc.applicable_on_purchase_order, + "action_for_annual": v_map.budget_doc.action_if_annual_budget_exceeded_on_po, + "action_for_monthly": v_map.budget_doc.action_if_accumulated_monthly_budget_exceeded_on_po, } ), - budget.name, - budget_amt, - ordered_amt, - cur_ordered_amt, - acc_monthly_budget, + v_map.budget_doc.name, + v_map.budget_amount, + v_map.ordered_amount, + v_map.current_ordered_amount, + v_map.accumulated_monthly_budget, ) self.handle_individual_doctype_action( key, frappe._dict( { - "applies": budget.applicable_on_material_request, - "action_for_annual": budget.action_if_annual_budget_exceeded_on_mr, - "action_for_monthly": budget.action_if_accumulated_monthly_budget_exceeded_on_mr, + "applies": v_map.budget_doc.applicable_on_material_request, + "action_for_annual": v_map.budget_doc.action_if_annual_budget_exceeded_on_mr, + "action_for_monthly": v_map.budget_doc.action_if_accumulated_monthly_budget_exceeded_on_mr, } ), - budget.name, - budget_amt, - requested_amt, - cur_requested_amt, - acc_monthly_budget, + v_map.budget_doc.name, + v_map.budget_amount, + v_map.requested_amount, + v_map.current_requested_amount, + v_map.accumulated_monthly_budget, ) self.handle_individual_doctype_action( key, frappe._dict( { - "applies": budget.applicable_on_booking_actual_expenses, - "action_for_annual": budget.action_if_annual_budget_exceeded, - "action_for_monthly": budget.action_if_accumulated_monthly_budget_exceeded, + "applies": v_map.budget_doc.applicable_on_booking_actual_expenses, + "action_for_annual": v_map.budget_doc.action_if_annual_budget_exceeded, + "action_for_monthly": v_map.budget_doc.action_if_accumulated_monthly_budget_exceeded, } ), - budget.name, - budget_amt, - actual_exp, - cur_actual_exp, - acc_monthly_budget, + v_map.budget_doc.name, + v_map.budget_amount, + v_map.actual_expense, + v_map.current_actual_exp_amount, + v_map.accumulated_monthly_budget, ) - current_amt = cur_ordered_amt + cur_requested_amt + cur_actual_exp - total_diff = (ordered_amt + requested_amt + actual_exp + current_amt) - budget_amt + current_amt = ( + v_map.current_ordered_amount + v_map.current_requested_amount + v_map.current_actual_exp_amount + ) + total_diff = ( + v_map.ordered_amount + v_map.requested_amount + v_map.actual_expense + current_amt + ) - v_map.budget_amount if total_diff > 0: currency = frappe.get_cached_value("Company", self.company, "default_currency") _msg = _( @@ -395,7 +389,7 @@ class BudgetValidation: frappe.bold(key[2]), frappe.bold(frappe.unscrub(key[0])), frappe.bold(key[1]), - frappe.bold(fmt_money(budget_amt, currency=currency)), + frappe.bold(fmt_money(v_map.budget_amount, currency=currency)), frappe.bold(fmt_money(total_diff, currency=currency)), ) self.stop(_msg) From 3a8075198b2748fdb6878a882ef7532fbef429f6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 May 2025 13:03:36 +0530 Subject: [PATCH 35/35] chore: patch to force default new controller --- erpnext/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5583d6ccdd5..c02ce0d6785 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -412,3 +412,4 @@ execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetc erpnext.patches.v14_0.set_update_price_list_based_on erpnext.patches.v15_0.update_journal_entry_type erpnext.patches.v15_0.set_grand_total_to_default_mop +execute:frappe.db.set_single_value("Accounts Settings", "use_new_budget_controller", True)