diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 6f600cc08e3bbe20138e25d01c20a42217ab4d70..b94604080025fcc79386310f14a9ca763dd08c36 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -550,6 +550,7 @@ class ProductionPlan(Document): self.db_set("status", self.status) def on_submit(self): + self.check_production_items() self.update_bin_qty() self.update_sales_order() @@ -734,6 +735,7 @@ class ProductionPlan(Document): if self.sub_assembly_items: item["use_multi_level_bom"] = 0 + self.check_production_items() set_default_warehouses(item, default_warehouses) work_order = self.create_work_order(item) if work_order: @@ -1099,6 +1101,14 @@ class ProductionPlan(Document): all_work_orders_completed = all(s == "Completed" for s in wo_status) return all_work_orders_completed + def check_production_items(self): + if self.get_items_from != "Material Request": + return + items_data = self.get_production_items() + for idx, item in enumerate(items_data.values(), start=1): + if not item["material_request"]: + frappe.throw(_("Row #{0}: This item must be linked to a Material Request").format(idx)) + @frappe.whitelist() def download_raw_materials(doc, warehouses=None): diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js index 8d46b14a1774eb6a4fa0b97ea6fc7a29fc59eaab..092dcff3107a7ec6e5c8179e7c38d5abe2b2faa2 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.js @@ -15,18 +15,59 @@ frappe.query_reports["Itemwise Recommended Reorder Level"] = { fieldtype: "Date", default: frappe.datetime.get_today(), }, - { - fieldname: "item_group", - label: __("Item Group"), - fieldtype: "Link", - options: "Item Group", - reqd: 1, - }, { fieldname: "brand", label: __("Brand"), fieldtype: "Link", options: "Brand", }, + { + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "MultiSelectList", + width: "100", + get_data(txt) { + return frappe.db.get_link_options("Item Group", txt); + }, + async on_change(report) { + // TODO: Handle parent item groups + const selected_groups = frappe.query_report.get_filter_value("item_group"); + const selected_items = frappe.query_report.get_filter_value("item_name"); + + if (!selected_groups?.length) { + return; // Don't filter if no Item Group is selected + } + + // Only keep items that are in the selected Item Group + const items = await frappe.db.get_list("Item", { + filters: { + item_group: ["in", selected_groups], + name: ["in", selected_items], + }, + fields: ["name"], + }); + + const item_names = items.map((item) => item.name); + frappe.query_report.set_filter_value("item_name", item_names); + + report.refresh(); + }, + }, + { + fieldname: "item_name", + label: __("Items"), + fieldtype: "MultiSelectList", + width: "100", + get_data(txt) { + const item_groups = frappe.query_report.get_filter_value("item_group"); + if (!item_groups?.length) { + return frappe.db.get_link_options("Item", txt); + } else { + return frappe.db.get_link_options("Item", txt, { + item_group: ["in", item_groups], + }); + } + }, + }, ], }; diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py index c4358b809fcce9a1c6609148d3384fe488436f77..acc91a5f95208c0b08a6ec56cea0d5b6fe7ebffd 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py @@ -68,6 +68,8 @@ def get_columns(): def get_item_info(filters): + from pypika import Criterion + from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition item = frappe.qb.DocType("Item") @@ -88,8 +90,20 @@ def get_item_info(filters): if brand := filters.get("brand"): query = query.where(item.brand == brand) - if conditions := get_item_group_condition(filters.get("item_group"), item): - query = query.where(conditions) + if groups := filters.get("item_group"): + if isinstance(groups, str): + groups = [groups] + conditions = [] + for group in groups: + if condition := get_item_group_condition(group, item): + conditions.append(condition) + query = query.where(Criterion.any(conditions)) + + if items := filters.get("item_name"): + if isinstance(items, str): + items = [items] + conditions = [] + query = query.where(item.name.isin(items)) return query.run(as_dict=True) diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py index 36e3e2d29bf855eca9038debd15835e2d20dd9cc..97e4c6fcd67c922b65eb58a332e69e8b0812eac5 100644 --- a/erpnext/stock/report/test_reports.py +++ b/erpnext/stock/report/test_reports.py @@ -23,6 +23,8 @@ REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ ("Stock Projected Qty", {"_optional": True}), ("Batch-Wise Balance History", {}), ("Itemwise Recommended Reorder Level", {"item_group": "All Item Groups"}), + ("Itemwise Recommended Reorder Level", {"item_group": ["All Item Groups"]}), + ("Itemwise Recommended Reorder Level", {"items": ["_Test Item"]}), ("COGS By Item Group", {}), ("Stock Qty vs Serial No Count", {"warehouse": "_Test Warehouse - _TC"}), (