From f00a63b69dcbe10c5f5e40e13112e33541f1a25f Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 7 Oct 2025 12:46:30 +0530 Subject: [PATCH 1/2] perf: optimize validate_qty method to eliminate N+1 query problem --- erpnext/controllers/status_updater.py | 89 +++++++++++++++++++++------ 1 file changed, 69 insertions(+), 20 deletions(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index fe0a273b6ed..c366a4a7cf2 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -265,6 +265,8 @@ class StatusUpdater(Document): # if target_ref_field is not specified, the programmer does not want to validate qty / amount continue + items_to_validate = [] + # get unique transactions to update for d in self.get_all_children(): if hasattr(d, "qty") and d.qty < 0 and not self.get("is_return"): @@ -286,31 +288,63 @@ class StatusUpdater(Document): ) if d.doctype == args["source_dt"] and d.get(args["join_field"]): - args["name"] = d.get(args["join_field"]) - - is_from_pp = ( - hasattr(d, "production_plan_sub_assembly_item") - and frappe.db.get_value( - "Production Plan Sub Assembly Item", - d.production_plan_sub_assembly_item, - "type_of_manufacturing", + items_to_validate.append( + frappe._dict( + { + "name": d.get(args["join_field"]), + "production_plan_sub_assembly_item": d.get( + "production_plan_sub_assembly_item" + ), + "idx": d.idx, + "child_doc": d, + } ) - == "Subcontract" ) - args["item_code"] = "production_item" if is_from_pp else "item_code" - # get all qty where qty > target_field - item = frappe.db.sql( - """select `{item_code}` as item_code, `{target_ref_field}`, - `{target_field}`, parenttype, parent from `tab{target_dt}` - where `{target_ref_field}` < `{target_field}` - and name=%s and docstatus=1""".format(**args), - args["name"], - as_dict=1, + if items_to_validate: + pp_sub_assembly_items = [ + item.production_plan_sub_assembly_item + for item in items_to_validate + if item.production_plan_sub_assembly_item + ] + + pp_subcontract_items = [] + if pp_sub_assembly_items: + pp_subcontract_items = frappe.db.get_all( + "Production Plan Sub Assembly Item", + filters={ + "name": ("in", pp_sub_assembly_items), + "type_of_manufacturing": "Subcontract", + }, + pluck="name", ) + + regular_items = [] + pp_items = [] + + for item in items_to_validate: + if item.production_plan_sub_assembly_item in pp_subcontract_items: + pp_items.append(item.name) + else: + regular_items.append(item.name) + + item_details = [] + + # Query regular items with item_code field + if regular_items: + item_details.extend(self.fetch_items_with_pending_qty(args, "item_code", regular_items)) + + # Query production plan items with production_item field + if pp_items: + item_details.extend(self.fetch_items_with_pending_qty(args, "production_item", pp_items)) + + item_lookup = {item.name: item for item in item_details} + + for child_item in items_to_validate: + item = item_lookup.get(child_item.name) + if item: - item = item[0] - item["idx"] = d.idx + item["idx"] = child_item.idx item["target_ref_field"] = args["target_ref_field"].replace("_", " ") # if not item[args['target_ref_field']]: @@ -323,6 +357,21 @@ class StatusUpdater(Document): elif item[args["target_ref_field"]]: self.check_overflow_with_allowance(item, args) + def fetch_items_with_pending_qty(self, args, item_field, items): + return frappe.db.sql( + """select name,`{item_code}` as item_code, `{target_ref_field}`, + `{target_field}`, parenttype, parent from `tab{target_dt}` + where `{target_ref_field}` < `{target_field}` + and name in %(names)s and docstatus=1""".format( + item_code=item_field, + target_ref_field=args["target_ref_field"], + target_field=args["target_field"], + target_dt=args["target_dt"], + ), + {"names": items}, + as_dict=1, + ) + def check_overflow_with_allowance(self, item, args): """ Checks if there is overflow condering a relaxation allowance From f1f61ff61b907de50e7ae909c3f50b6e9044c002 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Thu, 9 Oct 2025 17:51:51 +0530 Subject: [PATCH 2/2] refactor: replace SQL query with Query Builder in fetch_items_with_pending_qty method --- erpnext/controllers/status_updater.py | 31 ++++++++++++++++----------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index c366a4a7cf2..538887977d3 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -358,18 +358,25 @@ class StatusUpdater(Document): self.check_overflow_with_allowance(item, args) def fetch_items_with_pending_qty(self, args, item_field, items): - return frappe.db.sql( - """select name,`{item_code}` as item_code, `{target_ref_field}`, - `{target_field}`, parenttype, parent from `tab{target_dt}` - where `{target_ref_field}` < `{target_field}` - and name in %(names)s and docstatus=1""".format( - item_code=item_field, - target_ref_field=args["target_ref_field"], - target_field=args["target_field"], - target_dt=args["target_dt"], - ), - {"names": items}, - as_dict=1, + doctype = frappe.qb.DocType(args["target_dt"]) + item_field = doctype[item_field] + target_ref_field = doctype[args["target_ref_field"]] + target_field = doctype[args["target_field"]] + + return ( + frappe.qb.from_(doctype) + .select( + doctype.name, + item_field.as_("item_code"), + target_ref_field, + target_field, + doctype.parenttype, + doctype.parent, + ) + .where(target_ref_field < target_field) + .where(doctype.name.isin(items)) + .where(doctype.docstatus == 1) + .run(as_dict=True) ) def check_overflow_with_allowance(self, item, args):