From dfec7bd5c703086c203b4e62eb1d75b13cfdcae7 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 3 Aug 2026 13:55:26 +0530 Subject: [PATCH 1/2] fix(stock): stop a secondary item type from waiving quality inspection The inspection skip for secondary rows applied to every purpose, and in validate_inspection it skipped the row even when the item itself mandated inspection. Secondary Item Type is only meaningful on the purposes that produce secondary items, but nothing clears it elsewhere, since mark_finished_and_secondary_items runs for Manufacture and Repack alone. A Material Receipt of an item marked Inspection Required Before Purchase is blocked without an inspection. Setting Secondary Item Type on the row submitted it clean. Limit the exemption to the purposes that produce secondary items, and to other doctypes such as Subcontracting Receipt, which carry the field with its intended meaning. The client-side mirror is kept in sync. --- erpnext/public/js/controllers/transaction.js | 7 ++++++- .../services/quality_inspection_service.py | 20 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 6d2de472ce0..615d3302c5e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -18,10 +18,15 @@ erpnext.stock.qi_outgoing_purposes = [ "Subcontracting Delivery", "Disassemble", ]; +erpnext.stock.secondary_item_purposes = ["Manufacture", "Repack", "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 ( + erpnext.stock.secondary_item_purposes.includes(purpose) && + (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; if (erpnext.stock.qi_outgoing_purposes.includes(purpose)) diff --git a/erpnext/stock/services/quality_inspection_service.py b/erpnext/stock/services/quality_inspection_service.py index 3784725a4d2..b9fab051038 100644 --- a/erpnext/stock/services/quality_inspection_service.py +++ b/erpnext/stock/services/quality_inspection_service.py @@ -50,9 +50,25 @@ QI_OUTGOING_PURPOSES = ( ) +SECONDARY_ITEM_PURPOSES = ("Manufacture", "Repack", "Disassemble") + + +def is_inspection_exempt_secondary_row(doc, row) -> bool: + """Whether the row is a secondary item on a document that produces secondary items.""" + if not (row.get("secondary_item_type") or row.get("is_legacy_scrap_item")): + return False + + if doc.doctype == "Stock Entry": + return doc.purpose in SECONDARY_ITEM_PURPOSES + + return True + + 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"): + if purpose in SECONDARY_ITEM_PURPOSES and ( + row.get("secondary_item_type") or row.get("is_legacy_scrap_item") + ): return False if purpose == "Manufacture": return bool(row.is_finished_item) @@ -88,7 +104,7 @@ class QualityInspectionService: elif self.doc.doctype == "Stock Entry": qi_required = stock_entry_row_requires_inspection(self.doc.purpose, row) - if row.get("secondary_item_type") or row.get("is_legacy_scrap_item"): + if is_inspection_exempt_secondary_row(self.doc, row): continue if qi_required: # validate row only if inspection is required on item level From fec5dae6393c01ab71b860736d4dab2ecefca94f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 3 Aug 2026 13:55:27 +0530 Subject: [PATCH 2/2] test(stock): cover inspection on a receipt row typed as a secondary item The row must be blocked with or without the type set. --- .../doctype/stock_entry/test_stock_entry.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 09b8b5eaaf5..2cd3fde5f84 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -7,6 +7,7 @@ from frappe.utils import add_days, cstr, flt, get_time, getdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.controllers.accounts_controller import InvalidQtyError +from erpnext.exceptions import QualityInspectionRequiredError from erpnext.stock.doctype.item.test_item import ( create_item, make_item, @@ -2728,6 +2729,36 @@ class TestStockEntry(ERPNextTestSuite): self.assertEqual(fg_sle.incoming_rate, 0) self.assertEqual(fg_sle.stock_value_difference, 0) + def test_secondary_item_type_does_not_waive_inspection_outside_manufacturing(self): + """A stray secondary item type must not let a QI-required item through a receipt.""" + item = make_item( + properties={ + "is_stock_item": 1, + "valuation_rate": 50, + "inspection_required_before_purchase": 1, + } + ).name + + def receipt(secondary_item_type): + se = frappe.new_doc("Stock Entry") + se.purpose = se.stock_entry_type = "Material Receipt" + se.company = "_Test Company" + se.inspection_required = 1 + se.append( + "items", + { + "item_code": item, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 10, + "conversion_factor": 1, + "secondary_item_type": secondary_item_type, + }, + ) + return se + + self.assertRaises(QualityInspectionRequiredError, receipt("").submit) + self.assertRaises(QualityInspectionRequiredError, receipt("Scrap").submit) + def _make_wo_for_free_raw_material(self, rm_item, fg_item, bom_no): from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.work_order.work_order import (