fix(stock): fix tests

This commit is contained in:
Sudharsanan11
2026-06-25 15:09:03 +05:30
parent 2373db06ec
commit 1202e79a16
6 changed files with 88 additions and 77 deletions

View File

@@ -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() {

View File

@@ -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",

View File

@@ -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,

View File

@@ -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",

View File

@@ -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",

View File

@@ -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