From 2373db06ec7e939bbac58c768bc8a62e8cbdeea4 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 25 Jun 2026 11:51:00 +0530 Subject: [PATCH 1/3] fix: support quality inspection for stock entry by purpose Fetch QI items by warehouse direction per inspection_type, require QI on the correct rows per stock entry purpose (finished good for Manufacture, inward goods for Receipt/Repack, outgoing rows for issue/transfer), and show the QI field only on those rows. --- erpnext/public/js/controllers/transaction.js | 21 +++++++++----- .../quality_inspection/quality_inspection.py | 29 +++++++++++++++---- .../stock/doctype/stock_entry/stock_entry.js | 12 +++++++- .../stock_entry_detail.json | 3 +- .../services/quality_inspection_service.py | 12 ++++++-- 5 files changed, 59 insertions(+), 18 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 1c6af4fc978..d9b62bee8e8 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -405,7 +405,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } const incoming_doctypes = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]; - const incoming_purposes = ["Manufacture", "Material Receipt"]; + const incoming_purposes = ["Manufacture", "Material Receipt", "Repack"]; const inspection_type = incoming_doctypes.includes(this.frm.doc.doctype) || (this.frm.doc.doctype === "Stock Entry" && incoming_purposes.includes(this.frm.doc.purpose)) @@ -2967,7 +2967,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe const me = this; const incoming_doctypes = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]; - const incoming_purposes = ["Manufacture", "Material Receipt"]; + const incoming_purposes = ["Manufacture", "Material Receipt", "Repack"]; const inspection_type = incoming_doctypes.includes(this.frm.doc.doctype) || (this.frm.doc.doctype === "Stock Entry" && incoming_purposes.includes(this.frm.doc.purpose)) @@ -3065,13 +3065,20 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } has_inspection_required(item) { - if (this.frm.doc.doctype === "Stock Entry" && this.frm.doc.purpose == "Manufacture") { - if (item.is_finished_item && !item.quality_inspection) { - return true; - } - } else if (!item.quality_inspection) { + if (item.quality_inspection) { + return false; + } + if (this.frm.doc.doctype !== "Stock Entry") { return true; } + const purpose = this.frm.doc.purpose; + if (purpose === "Manufacture") { + return !!item.is_finished_item; + } + if (["Material Receipt", "Repack"].includes(purpose)) { + return !!item.t_warehouse; + } + return !!item.s_warehouse && item.s_warehouse !== item.t_warehouse; } get_method_for_payment() { diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index ff536f01d55..d51d384b1c2 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -387,12 +387,29 @@ def item_query(doctype: Any, txt: str | None, searchfield: Any, start: int, page ] if reference_doctype == "Stock Entry": - my_filters.extend( - [ - "and", - ["items.t_warehouse", "is", "not set"], - ] - ) + if filters.get("inspection_type") == "Incoming": + purpose = frappe.db.get_value("Stock Entry", filters.get("reference_name"), "purpose") + if purpose == "Manufacture": + my_filters.extend( + [ + "and", + ["items.is_finished_item", "=", 1], + ] + ) + else: + my_filters.extend( + [ + "and", + ["items.t_warehouse", "is", "set"], + ] + ) + elif filters.get("inspection_type") == "Outgoing": + my_filters.extend( + [ + "and", + ["items.s_warehouse", "is", "set"], + ] + ) elif filters.get("inspection_type") != "In Process": my_filters.extend( [ diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 58185d3c40e..19025148116 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -199,6 +199,17 @@ frappe.ui.form.on("Stock Entry", { }, setup_quality_inspection: function (frm) { + const incoming_purposes = ["Manufacture", "Material Receipt", "Repack"]; + + // Show the Quality Inspection field only on rows that require inspection. + frm.get_docfield("items", "quality_inspection").depends_on = (row) => + frm.doc.inspection_required && + (frm.doc.purpose === "Manufacture" + ? row.is_finished_item + : incoming_purposes.includes(frm.doc.purpose) + ? row.t_warehouse + : row.s_warehouse && row.s_warehouse !== row.t_warehouse); + if (!frm.doc.inspection_required) { return; } @@ -216,7 +227,6 @@ frappe.ui.form.on("Stock Entry", { } let quality_inspection_field = frm.get_docfield("items", "quality_inspection"); - const incoming_purposes = ["Manufacture", "Material Receipt"]; quality_inspection_field.get_route_options_for_new_doc = function (row) { if (frm.is_new()) return {}; return { diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index c21d9ec91cb..71adb7ed566 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -324,7 +324,6 @@ "options": "Batch" }, { - "depends_on": "eval:parent.inspection_required && doc.t_warehouse", "fieldname": "quality_inspection", "fieldtype": "Link", "label": "Quality Inspection", @@ -679,7 +678,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-06-01 10:00:00.000000", + "modified": "2026-06-25 11:39:55.152526", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/services/quality_inspection_service.py b/erpnext/stock/services/quality_inspection_service.py index 7e7fc4ba078..e524eda4e2c 100644 --- a/erpnext/stock/services/quality_inspection_service.py +++ b/erpnext/stock/services/quality_inspection_service.py @@ -49,8 +49,16 @@ class QualityInspectionService: "Item", row.item_code, inspection_required_fieldname ): qi_required = True - elif self.doc.doctype == "Stock Entry" and row.t_warehouse: - qi_required = True # inward stock needs inspection + elif self.doc.doctype == "Stock Entry": + if self.doc.purpose == "Manufacture": + # only the finished good needs inspection + if row.is_finished_item: + qi_required = True + elif self.doc.purpose in ["Material Receipt", "Repack"]: + if row.t_warehouse: + qi_required = True + elif row.s_warehouse and row.s_warehouse != row.t_warehouse: + qi_required = True if row.get("secondary_item_type") or row.get("is_legacy_scrap_item"): continue From 1202e79a167cde177edb3bf057b9580b01187998 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 25 Jun 2026 15:09:03 +0530 Subject: [PATCH 2/3] fix(stock): fix tests --- erpnext/public/js/controllers/transaction.js | 50 +++++++++++-------- .../quality_inspection/quality_inspection.py | 34 ++++++------- .../stock/doctype/stock_entry/stock_entry.js | 13 ++--- .../doctype/stock_entry/test_stock_entry.py | 37 +++++++------- .../stock_entry_detail.json | 3 +- .../services/quality_inspection_service.py | 28 +++++++---- 6 files changed, 88 insertions(+), 77 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index d9b62bee8e8..86784d28748 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1,6 +1,22 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +erpnext.stock = erpnext.stock || {}; +erpnext.stock.qi_incoming_purposes = [ + "Material Receipt", + "Repack", + "Receive from Customer", + "Subcontracting Return", +]; +erpnext.stock.is_incoming_qi_purpose = (purpose) => + purpose === "Manufacture" || erpnext.stock.qi_incoming_purposes.includes(purpose); +erpnext.stock.row_requires_quality_inspection = (purpose, row) => { + if (row.secondary_item_type || row.is_legacy_scrap_item) return false; + if (purpose === "Manufacture") return !!row.is_finished_item; + if (erpnext.stock.qi_incoming_purposes.includes(purpose)) return !!row.t_warehouse; + return !!row.s_warehouse && row.s_warehouse !== row.t_warehouse; +}; + erpnext.TransactionController = class TransactionController extends erpnext.taxes_and_totals { setup() { super.setup(); @@ -404,13 +420,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe ); } - const incoming_doctypes = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]; - const incoming_purposes = ["Manufacture", "Material Receipt", "Repack"]; - const inspection_type = - incoming_doctypes.includes(this.frm.doc.doctype) || - (this.frm.doc.doctype === "Stock Entry" && incoming_purposes.includes(this.frm.doc.purpose)) - ? "Incoming" - : "Outgoing"; + const inspection_type = this.quality_inspection_type(); let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection"); quality_inspection_field.get_route_options_for_new_doc = function (row) { @@ -2966,13 +2976,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe ]; const me = this; - const incoming_doctypes = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]; - const incoming_purposes = ["Manufacture", "Material Receipt", "Repack"]; - const inspection_type = - incoming_doctypes.includes(this.frm.doc.doctype) || - (this.frm.doc.doctype === "Stock Entry" && incoming_purposes.includes(this.frm.doc.purpose)) - ? "Incoming" - : "Outgoing"; + const inspection_type = this.quality_inspection_type(); const dialog = new frappe.ui.Dialog({ title: __("Select Items for Quality Inspection"), size: "extra-large", @@ -3064,6 +3068,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }); } + quality_inspection_type() { + const incoming_doctypes = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]; + const is_incoming = + incoming_doctypes.includes(this.frm.doc.doctype) || + (this.frm.doc.doctype === "Stock Entry" && + erpnext.stock.is_incoming_qi_purpose(this.frm.doc.purpose)); + return is_incoming ? "Incoming" : "Outgoing"; + } + has_inspection_required(item) { if (item.quality_inspection) { return false; @@ -3071,14 +3084,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if (this.frm.doc.doctype !== "Stock Entry") { return true; } - const purpose = this.frm.doc.purpose; - if (purpose === "Manufacture") { - return !!item.is_finished_item; - } - if (["Material Receipt", "Repack"].includes(purpose)) { - return !!item.t_warehouse; - } - return !!item.s_warehouse && item.s_warehouse !== item.t_warehouse; + return erpnext.stock.row_requires_quality_inspection(this.frm.doc.purpose, item); } get_method_for_payment() { diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index d51d384b1c2..d0978da0f7e 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -13,6 +13,7 @@ from frappe.utils import cint, flt, get_link_to_form, get_number_format_info from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import ( get_template_details, ) +from erpnext.stock.services.quality_inspection_service import QI_INCOMING_PURPOSES class QualityInspection(Document): @@ -387,23 +388,22 @@ def item_query(doctype: Any, txt: str | None, searchfield: Any, start: int, page ] if reference_doctype == "Stock Entry": - if filters.get("inspection_type") == "Incoming": - purpose = frappe.db.get_value("Stock Entry", filters.get("reference_name"), "purpose") - if purpose == "Manufacture": - my_filters.extend( - [ - "and", - ["items.is_finished_item", "=", 1], - ] - ) - else: - my_filters.extend( - [ - "and", - ["items.t_warehouse", "is", "set"], - ] - ) - elif filters.get("inspection_type") == "Outgoing": + purpose = frappe.get_cached_value("Stock Entry", filters.get("reference_name"), "purpose") + if purpose == "Manufacture": + my_filters.extend( + [ + "and", + ["items.is_finished_item", "=", 1], + ] + ) + elif purpose in QI_INCOMING_PURPOSES: + my_filters.extend( + [ + "and", + ["items.t_warehouse", "is", "set"], + ] + ) + else: my_filters.extend( [ "and", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 19025148116..fed14074419 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -199,16 +199,9 @@ frappe.ui.form.on("Stock Entry", { }, setup_quality_inspection: function (frm) { - const incoming_purposes = ["Manufacture", "Material Receipt", "Repack"]; - - // Show the Quality Inspection field only on rows that require inspection. frm.get_docfield("items", "quality_inspection").depends_on = (row) => frm.doc.inspection_required && - (frm.doc.purpose === "Manufacture" - ? row.is_finished_item - : incoming_purposes.includes(frm.doc.purpose) - ? row.t_warehouse - : row.s_warehouse && row.s_warehouse !== row.t_warehouse); + erpnext.stock.row_requires_quality_inspection(frm.doc.purpose, row); if (!frm.doc.inspection_required) { return; @@ -230,7 +223,9 @@ frappe.ui.form.on("Stock Entry", { quality_inspection_field.get_route_options_for_new_doc = function (row) { if (frm.is_new()) return {}; return { - inspection_type: incoming_purposes.includes(frm.doc.purpose) ? "Incoming" : "Outgoing", + inspection_type: erpnext.stock.is_incoming_qi_purpose(frm.doc.purpose) + ? "Incoming" + : "Outgoing", reference_type: frm.doc.doctype, reference_name: frm.doc.name, child_row_reference: row.doc.name, diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 5dcad431d21..aa04c7552f3 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1183,16 +1183,21 @@ class TestStockEntry(ERPNextTestSuite): # stock the source warehouse for transfer / issue purposes make_stock_entry(item_code=item_code, target=s_wh, qty=100, basic_rate=100) - # purpose -> warehouses for the moved row; inward (with target) requires QI + # purpose -> warehouses for the moved row and the direction QI is required on: + # Material Receipt inspects the inward row, Transfer/Issue inspect the outgoing row. purposes = { - "Material Receipt": {"to_warehouse": t_wh}, - "Material Transfer": {"from_warehouse": s_wh, "to_warehouse": t_wh}, - "Material Issue": {"from_warehouse": s_wh}, + "Material Receipt": {"warehouses": {"to_warehouse": t_wh}, "inspection_type": "Incoming"}, + "Material Transfer": { + "warehouses": {"from_warehouse": s_wh, "to_warehouse": t_wh}, + "inspection_type": "Outgoing", + }, + "Material Issue": {"warehouses": {"from_warehouse": s_wh}, "inspection_type": "Outgoing"}, } - for purpose, warehouses in purposes.items(): + for purpose, config in purposes.items(): with self.subTest(purpose=purpose): - needs_qi = "to_warehouse" in warehouses + warehouses = config["warehouses"] + inspection_type = config["inspection_type"] se = make_stock_entry( item_code=item_code, @@ -1208,13 +1213,7 @@ class TestStockEntry(ERPNextTestSuite): allowed = check_item_quality_inspection("Stock Entry", 0, se.as_dict().get("items")) self.assertTrue(any(row.get("item_code") == item_code for row in allowed)) - if not needs_qi: - # outward-only entry: QI is not enforced - se.submit() - self.assertEqual(se.docstatus, 1) - continue - - # inward entry without QI must block submission + # entry without QI must block submission self.assertRaises(QualityInspectionRequiredError, se.submit) # a rejected QI must also block submission @@ -1231,13 +1230,13 @@ class TestStockEntry(ERPNextTestSuite): reference_type="Stock Entry", reference_name=se_rej.name, item_code=item_code, - inspection_type="Incoming", + inspection_type=inspection_type, status="Rejected", ) se_rej.reload() self.assertRaises(QualityInspectionRejectedError, se_rej.submit) - # a submitted, accepted QI links itself to the inward row; submission then succeeds + # a submitted, accepted QI links itself to the inspected row; submission then succeeds se_ok = make_stock_entry( item_code=item_code, qty=5, @@ -1251,7 +1250,7 @@ class TestStockEntry(ERPNextTestSuite): reference_type="Stock Entry", reference_name=se_ok.name, item_code=item_code, - inspection_type="Incoming", + inspection_type=inspection_type, status="Accepted", ) se_ok.reload() @@ -1434,15 +1433,15 @@ class TestStockEntry(ERPNextTestSuite): row.s_warehouse = source_warehouse mfg.submit() - # disassemble with inspection required -> the component rows need a QI + # disassemble with inspection required -> the consumed (outgoing) rows need a QI dis = frappe.get_doc(make_wo_stock_entry(wo.name, "Disassemble", 1)) dis.inspection_required = 1 dis.insert() self.assertRaises(QualityInspectionRequiredError, dis.submit) - # a rejected QI on any disassembled component row must also block submission + # a rejected QI on any consumed (outgoing) row must also block submission qis = [] - for item_code in {row.item_code for row in dis.items if row.t_warehouse}: + for item_code in {row.item_code for row in dis.items if row.s_warehouse}: qis.append( create_quality_inspection( reference_type="Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 71adb7ed566..ea9d3b75b51 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -324,6 +324,7 @@ "options": "Batch" }, { + "depends_on": "eval:parent.inspection_required", "fieldname": "quality_inspection", "fieldtype": "Link", "label": "Quality Inspection", @@ -678,7 +679,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-06-25 11:39:55.152526", + "modified": "2026-06-30 12:18:34.132425", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/services/quality_inspection_service.py b/erpnext/stock/services/quality_inspection_service.py index e524eda4e2c..ed5fb2cf68f 100644 --- a/erpnext/stock/services/quality_inspection_service.py +++ b/erpnext/stock/services/quality_inspection_service.py @@ -26,6 +26,24 @@ INSPECTION_FIELDNAME_MAP = { "Delivery Note": "inspection_required_before_delivery", } +QI_INCOMING_PURPOSES = ( + "Material Receipt", + "Repack", + "Receive from Customer", + "Subcontracting Return", +) + + +def stock_entry_row_requires_inspection(purpose, row): + """Check if this Stock Entry row need a Quality Inspection.""" + if row.get("secondary_item_type") or row.get("is_legacy_scrap_item"): + return False + if purpose == "Manufacture": + return bool(row.is_finished_item) + if purpose in QI_INCOMING_PURPOSES: + return bool(row.t_warehouse) + return bool(row.s_warehouse and row.s_warehouse != row.t_warehouse) + class QualityInspectionService: def __init__(self, doc) -> None: @@ -50,15 +68,7 @@ class QualityInspectionService: ): qi_required = True elif self.doc.doctype == "Stock Entry": - if self.doc.purpose == "Manufacture": - # only the finished good needs inspection - if row.is_finished_item: - qi_required = True - elif self.doc.purpose in ["Material Receipt", "Repack"]: - if row.t_warehouse: - qi_required = True - elif row.s_warehouse and row.s_warehouse != row.t_warehouse: - qi_required = True + qi_required = stock_entry_row_requires_inspection(self.doc.purpose, row) if row.get("secondary_item_type") or row.get("is_legacy_scrap_item"): continue From 847fd8aa33d030e95ab22db5a89e9c7696bf1635 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Tue, 30 Jun 2026 20:26:09 +0530 Subject: [PATCH 3/3] fix(stock): exclude consumption from outgoing quality inspection The QI-by-purpose check required an inspection on every outgoing (s_warehouse) row for any purpose that was not incoming. Material Consumption for Manufacture rows are source-only and the Work Order mapper copies inspection_required from the BOM, so this silently blocked submission. An inspection_required BOM inspects the finished good, not each consumed raw material. Replace the "anything not incoming" fallthrough with an explicit QI_OUTGOING_PURPOSES allow-list (mirrored in transaction.js) so a new purpose cannot silently start requiring a QI. Consumption and Return Raw Material to Customer now need no QI; Issue, Transfer, Transfer for Manufacture, Send to Subcontractor, Subcontracting Delivery and Disassemble keep their outgoing checks. Scope item_query to the same set and add a regression test. --- erpnext/public/js/controllers/transaction.js | 14 ++++++- .../quality_inspection/quality_inspection.py | 26 +++++++++++-- .../doctype/stock_entry/test_stock_entry.py | 38 +++++++++++++++++++ .../services/quality_inspection_service.py | 20 +++++++++- 4 files changed, 93 insertions(+), 5 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 86784d28748..c75c010b563 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1,6 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +// Keep these in sync with QI_INCOMING_PURPOSES / QI_OUTGOING_PURPOSES / +// stock_entry_row_requires_inspection in stock/services/quality_inspection_service.py. erpnext.stock = erpnext.stock || {}; erpnext.stock.qi_incoming_purposes = [ "Material Receipt", @@ -8,13 +10,23 @@ erpnext.stock.qi_incoming_purposes = [ "Receive from Customer", "Subcontracting Return", ]; +erpnext.stock.qi_outgoing_purposes = [ + "Material Issue", + "Material Transfer", + "Material Transfer for Manufacture", + "Send to Subcontractor", + "Subcontracting Delivery", + "Disassemble", +]; erpnext.stock.is_incoming_qi_purpose = (purpose) => purpose === "Manufacture" || erpnext.stock.qi_incoming_purposes.includes(purpose); erpnext.stock.row_requires_quality_inspection = (purpose, row) => { if (row.secondary_item_type || row.is_legacy_scrap_item) return false; if (purpose === "Manufacture") return !!row.is_finished_item; if (erpnext.stock.qi_incoming_purposes.includes(purpose)) return !!row.t_warehouse; - return !!row.s_warehouse && row.s_warehouse !== row.t_warehouse; + if (erpnext.stock.qi_outgoing_purposes.includes(purpose)) + return !!row.s_warehouse && row.s_warehouse !== row.t_warehouse; + return false; }; erpnext.TransactionController = class TransactionController extends erpnext.taxes_and_totals { diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index d0978da0f7e..7cd92bbda2e 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -13,7 +13,10 @@ from frappe.utils import cint, flt, get_link_to_form, get_number_format_info from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import ( get_template_details, ) -from erpnext.stock.services.quality_inspection_service import QI_INCOMING_PURPOSES +from erpnext.stock.services.quality_inspection_service import ( + QI_INCOMING_PURPOSES, + QI_OUTGOING_PURPOSES, +) class QualityInspection(Document): @@ -387,8 +390,18 @@ def item_query(doctype: Any, txt: str | None, searchfield: Any, start: int, page ["items.quality_inspection", "is", "not set"], ] + require_distinct_warehouse = False + if reference_doctype == "Stock Entry": purpose = frappe.get_cached_value("Stock Entry", filters.get("reference_name"), "purpose") + my_filters.extend( + [ + "and", + ["items.secondary_item_type", "is", "not set"], + "and", + ["items.is_legacy_scrap_item", "=", 0], + ] + ) if purpose == "Manufacture": my_filters.extend( [ @@ -403,13 +416,17 @@ def item_query(doctype: Any, txt: str | None, searchfield: Any, start: int, page ["items.t_warehouse", "is", "set"], ] ) - else: + elif purpose in QI_OUTGOING_PURPOSES: my_filters.extend( [ "and", ["items.s_warehouse", "is", "set"], ] ) + require_distinct_warehouse = True + else: + # purpose requires no quality inspection + return [] elif filters.get("inspection_type") != "In Process": my_filters.extend( [ @@ -444,7 +461,10 @@ def item_query(doctype: Any, txt: str | None, searchfield: Any, start: int, page # query instead keeps it -- item_code is in the DISTINCT select, so it is valid on Postgres. items_field = frappe.get_meta(reference_doctype).get_field("items") if items_field: - query = query.orderby(frappe.qb.DocType(items_field.options).item_code) + child = frappe.qb.DocType(items_field.options) + if require_distinct_warehouse: + query = query.where(child.t_warehouse.isnull() | (child.s_warehouse != child.t_warehouse)) + query = query.orderby(child.item_code) return query.run() diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index aa04c7552f3..43066da7580 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2839,6 +2839,44 @@ class TestStockEntry(ERPNextTestSuite): frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 5)).submit() frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 5)).submit() + @ERPNextTestSuite.change_settings( + "Manufacturing Settings", + {"material_consumption": 1, "backflush_raw_materials_based_on": "BOM"}, + ) + def test_qi_not_required_for_material_consumption_for_manufacture(self): + """An inspection_required BOM inspects the finished good (the Manufacture rule), + not each consumed raw material, so Material Consumption for Manufacture (whose + rows are outgoing only) must still submit without a Quality Inspection.""" + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry as _make_stock_entry + from erpnext.manufacturing.doctype.work_order.work_order import make_work_order + + fg_item = make_item("_Test QI Consumption FG", properties={"is_stock_item": 1}).name + rm_item = make_item("_Test QI Consumption RM", properties={"is_stock_item": 1}).name + warehouse = "Stores - WP" + + bom = make_bom(item=fg_item, raw_materials=[rm_item], do_not_submit=True) + bom.inspection_required = 1 + bom.submit() + + se = make_stock_entry(item_code=rm_item, target=warehouse, qty=5, rate=10, purpose="Material Receipt") + + work_order = make_work_order(bom.name, fg_item, 5) + work_order.company = se.company + work_order.skip_transfer = 1 + work_order.source_warehouse = warehouse + work_order.fg_warehouse = warehouse + work_order.submit() + + consumption = frappe.get_doc( + _make_stock_entry(work_order.name, "Material Consumption for Manufacture", 5) + ) + # the mapper copies inspection_required from the BOM ... + self.assertEqual(consumption.inspection_required, 1) + # ... but the consumed rows are outgoing-only, so no QI is required and submit succeeds + consumption.submit() + self.assertEqual(consumption.docstatus, 1) + def test_qi_creation_with_naming_rule_company_condition(self): """ Unit test case to check the document naming rule with company condition diff --git a/erpnext/stock/services/quality_inspection_service.py b/erpnext/stock/services/quality_inspection_service.py index ed5fb2cf68f..3784725a4d2 100644 --- a/erpnext/stock/services/quality_inspection_service.py +++ b/erpnext/stock/services/quality_inspection_service.py @@ -26,6 +26,7 @@ INSPECTION_FIELDNAME_MAP = { "Delivery Note": "inspection_required_before_delivery", } +# Purposes whose inward (t_warehouse) row is inspected. QI_INCOMING_PURPOSES = ( "Material Receipt", "Repack", @@ -33,6 +34,21 @@ QI_INCOMING_PURPOSES = ( "Subcontracting Return", ) +# Purposes whose outgoing (s_warehouse) row is inspected. This is an explicit +# allow-list rather than "everything that isn't incoming" so a new purpose can't +# silently start requiring a QI. Material Consumption for Manufacture is left out +# on purpose: an inspection_required BOM inspects the manufactured output (handled +# by the "Manufacture" finished-good rule), not each consumed raw material. +# Keep this in sync with erpnext.stock.qi_* helpers in transaction.js. +QI_OUTGOING_PURPOSES = ( + "Material Issue", + "Material Transfer", + "Material Transfer for Manufacture", + "Send to Subcontractor", + "Subcontracting Delivery", + "Disassemble", +) + def stock_entry_row_requires_inspection(purpose, row): """Check if this Stock Entry row need a Quality Inspection.""" @@ -42,7 +58,9 @@ def stock_entry_row_requires_inspection(purpose, row): return bool(row.is_finished_item) if purpose in QI_INCOMING_PURPOSES: return bool(row.t_warehouse) - return bool(row.s_warehouse and row.s_warehouse != row.t_warehouse) + if purpose in QI_OUTGOING_PURPOSES: + return bool(row.s_warehouse and row.s_warehouse != row.t_warehouse) + return False class QualityInspectionService: