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