From 0b9156b92bb83fec611db20b8b0ea81468d1bc6b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 31 Jan 2024 11:32:17 +0530 Subject: [PATCH] perf: timeout for auto material request through reorder level --- .../doctype/stock_entry/test_stock_entry.py | 42 +++++ erpnext/stock/reorder_item.py | 165 +++++++++++++----- 2 files changed, 167 insertions(+), 40 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 23dacc8343dac..9f3435ec31521 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1785,6 +1785,48 @@ def test_negative_batch(self): self.assertRaises(frappe.ValidationError, se1.cancel) + def test_auto_reorder_level(self): + from erpnext.stock.reorder_item import reorder_item + + item_doc = make_item( + "Test Auto Reorder Item - 001", + properties={"stock_uom": "Kg", "purchase_uom": "Nos", "is_stock_item": 1}, + uoms=[{"uom": "Nos", "conversion_factor": 5}], + ) + + if not frappe.db.exists("Item Reorder", {"parent": item_doc.name}): + item_doc.append( + "reorder_levels", + { + "warehouse_reorder_level": 0, + "warehouse_reorder_qty": 10, + "warehouse": "_Test Warehouse - _TC", + "material_request_type": "Purchase", + }, + ) + + item_doc.save(ignore_permissions=True) + + frappe.db.set_single_value("Stock Settings", "auto_indent", 1) + + mr_list = reorder_item() + + frappe.db.set_single_value("Stock Settings", "auto_indent", 0) + mrs = frappe.get_all( + "Material Request Item", + fields=["qty", "stock_uom", "stock_qty"], + filters={"item_code": item_doc.name, "uom": "Nos"}, + ) + + for mri in mrs: + self.assertEqual(mri.stock_uom, "Kg") + self.assertEqual(mri.stock_qty, 10) + self.assertEqual(mri.qty, 2) + + for mr in mr_list: + mr.cancel() + mr.delete() + def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 276531ab5cf72..59f8b20b4133a 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -34,73 +34,157 @@ def _reorder_item(): erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0] ) - items_to_consider = frappe.db.sql_list( - """select name from `tabItem` item - where is_stock_item=1 and has_variants=0 - and disabled=0 - and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %(today)s) - and (exists (select name from `tabItem Reorder` ir where ir.parent=item.name) - or (variant_of is not null and variant_of != '' - and exists (select name from `tabItem Reorder` ir where ir.parent=item.variant_of)) - )""", - {"today": nowdate()}, - ) + items_to_consider = get_items_for_reorder() if not items_to_consider: return item_warehouse_projected_qty = get_item_warehouse_projected_qty(items_to_consider) - def add_to_material_request( - item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None - ): - if warehouse not in warehouse_company: + def add_to_material_request(**kwargs): + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + if kwargs.warehouse not in warehouse_company: # a disabled warehouse return - reorder_level = flt(reorder_level) - reorder_qty = flt(reorder_qty) + reorder_level = flt(kwargs.reorder_level) + reorder_qty = flt(kwargs.reorder_qty) # projected_qty will be 0 if Bin does not exist - if warehouse_group: - projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse_group)) + if kwargs.warehouse_group: + projected_qty = flt( + item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse_group) + ) else: - projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse)) + projected_qty = flt( + item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse) + ) if (reorder_level or reorder_qty) and projected_qty <= reorder_level: deficiency = reorder_level - projected_qty if deficiency > reorder_qty: reorder_qty = deficiency - company = warehouse_company.get(warehouse) or default_company + company = warehouse_company.get(kwargs.warehouse) or default_company - material_requests[material_request_type].setdefault(company, []).append( - {"item_code": item_code, "warehouse": warehouse, "reorder_qty": reorder_qty} + material_requests[kwargs.material_request_type].setdefault(company, []).append( + { + "item_code": kwargs.item_code, + "warehouse": kwargs.warehouse, + "reorder_qty": reorder_qty, + "item_details": kwargs.item_details, + } ) - for item_code in items_to_consider: - item = frappe.get_doc("Item", item_code) - - if item.variant_of and not item.get("reorder_levels"): - item.update_template_tables() - - if item.get("reorder_levels"): - for d in item.get("reorder_levels"): - add_to_material_request( - item_code, - d.warehouse, - d.warehouse_reorder_level, - d.warehouse_reorder_qty, - d.material_request_type, - warehouse_group=d.warehouse_group, - ) + for item_code, reorder_levels in items_to_consider.items(): + for d in reorder_levels: + if d.has_variants: + continue + + add_to_material_request( + item_code=item_code, + warehouse=d.warehouse, + reorder_level=d.warehouse_reorder_level, + reorder_qty=d.warehouse_reorder_qty, + material_request_type=d.material_request_type, + warehouse_group=d.warehouse_group, + item_details=frappe._dict( + { + "item_code": item_code, + "name": item_code, + "item_name": d.item_name, + "item_group": d.item_group, + "brand": d.brand, + "description": d.description, + "stock_uom": d.stock_uom, + "purchase_uom": d.purchase_uom, + } + ), + ) if material_requests: return create_material_request(material_requests) +def get_items_for_reorder() -> dict[str, list]: + reorder_table = frappe.qb.DocType("Item Reorder") + item_table = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(reorder_table) + .inner_join(item_table) + .on(reorder_table.parent == item_table.name) + .select( + reorder_table.warehouse, + reorder_table.warehouse_group, + reorder_table.material_request_type, + reorder_table.warehouse_reorder_level, + reorder_table.warehouse_reorder_qty, + item_table.name, + item_table.stock_uom, + item_table.purchase_uom, + item_table.description, + item_table.item_name, + item_table.item_group, + item_table.brand, + item_table.variant_of, + item_table.has_variants, + ) + .where( + (item_table.disabled == 0) + & (item_table.is_stock_item == 1) + & ( + (item_table.end_of_life.isnull()) + | (item_table.end_of_life > nowdate()) + | (item_table.end_of_life == "0000-00-00") + ) + ) + ) + + data = query.run(as_dict=True) + itemwise_reorder = frappe._dict({}) + for d in data: + itemwise_reorder.setdefault(d.name, []).append(d) + + itemwise_reorder = get_reorder_levels_for_variants(itemwise_reorder) + + return itemwise_reorder + + +def get_reorder_levels_for_variants(itemwise_reorder): + item_table = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(item_table) + .select( + item_table.name, + item_table.variant_of, + ) + .where( + (item_table.disabled == 0) + & (item_table.is_stock_item == 1) + & ( + (item_table.end_of_life.isnull()) + | (item_table.end_of_life > nowdate()) + | (item_table.end_of_life == "0000-00-00") + ) + & (item_table.variant_of.notnull()) + ) + ) + + variants_item = query.run(as_dict=True) + for row in variants_item: + if not itemwise_reorder.get(row.name) and itemwise_reorder.get(row.variant_of): + itemwise_reorder.setdefault(row.name, []).extend(itemwise_reorder.get(row.variant_of, [])) + + return itemwise_reorder + + def get_item_warehouse_projected_qty(items_to_consider): item_warehouse_projected_qty = {} + items_to_consider = list(items_to_consider.keys()) for item_code, warehouse, projected_qty in frappe.db.sql( """select item_code, warehouse, projected_qty @@ -164,7 +248,7 @@ def _log_exception(mr): for d in items: d = frappe._dict(d) - item = frappe.get_doc("Item", d.item_code) + item = d.get("item_details") uom = item.stock_uom conversion_factor = 1.0 @@ -190,6 +274,7 @@ def _log_exception(mr): "item_code": d.item_code, "schedule_date": add_days(nowdate(), cint(item.lead_time_days)), "qty": qty, + "conversion_factor": conversion_factor, "uom": uom, "stock_uom": item.stock_uom, "warehouse": d.warehouse,