mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-17 11:52:38 +00:00
fix(stock): enable quality inspection for all Stock Entry purposes
Backport of #55830 to version-15. Allow creating a Quality Inspection from Stock Entries of any purpose (not just Manufacture). check_item_quality_inspection now returns the items for Stock Entry, and the inspection type is resolved as "Incoming" for Manufacture / Material Receipt and "Outgoing" otherwise. Ref: #70945
This commit is contained in:
@@ -1694,7 +1694,7 @@ def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str
|
||||
|
||||
inspection_fieldname = inspection_fieldname_map.get(doctype)
|
||||
if inspection_fieldname is None:
|
||||
return []
|
||||
return items if doctype == "Stock Entry" else []
|
||||
|
||||
allow_after_transaction = cint(docstatus) == 1 and frappe.get_single_value(
|
||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||
|
||||
@@ -362,8 +362,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}, __("Create"));
|
||||
}
|
||||
|
||||
const inspection_type = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"].includes(this.frm.doc.doctype)
|
||||
? "Incoming" : "Outgoing";
|
||||
const incoming_doctypes = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"];
|
||||
const incoming_purposes = ["Manufacture", "Material Receipt"];
|
||||
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";
|
||||
|
||||
let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection");
|
||||
quality_inspection_field.get_route_options_for_new_doc = function(row) {
|
||||
@@ -2474,6 +2479,13 @@ 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 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 dialog = new frappe.ui.Dialog({
|
||||
title: __("Select Items for Quality Inspection"),
|
||||
size: "extra-large",
|
||||
|
||||
@@ -201,10 +201,11 @@ 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 {
|
||||
inspection_type: "Incoming",
|
||||
inspection_type: incoming_purposes.includes(frm.doc.purpose) ? "Incoming" : "Outgoing",
|
||||
reference_type: frm.doc.doctype,
|
||||
reference_name: frm.doc.name,
|
||||
child_row_reference: row.doc.name,
|
||||
|
||||
@@ -1094,6 +1094,327 @@ class TestStockEntry(FrappeTestCase):
|
||||
repack.insert()
|
||||
self.assertRaises(frappe.ValidationError, repack.submit)
|
||||
|
||||
def test_check_item_quality_inspection_returns_items_for_stock_entry(self):
|
||||
from erpnext.controllers.stock_controller import check_item_quality_inspection
|
||||
|
||||
items = [
|
||||
{"item_code": "_Test Item", "qty": 1},
|
||||
{"item_code": "_Test Item Home Desktop 100", "qty": 1},
|
||||
]
|
||||
|
||||
se_result = check_item_quality_inspection("Stock Entry", 0, items)
|
||||
self.assertEqual(len(se_result), 2)
|
||||
|
||||
# a doctype not in the inspection fieldname map and not a Stock Entry returns nothing
|
||||
self.assertEqual(check_item_quality_inspection("Material Request", 0, items), [])
|
||||
|
||||
@change_settings("Stock Settings", {"action_if_quality_inspection_is_rejected": "Stop"})
|
||||
def test_quality_inspection_across_stock_entry_purposes(self):
|
||||
from erpnext.controllers.stock_controller import (
|
||||
QualityInspectionRejectedError,
|
||||
QualityInspectionRequiredError,
|
||||
check_item_quality_inspection,
|
||||
)
|
||||
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
||||
create_quality_inspection,
|
||||
)
|
||||
|
||||
item_code = "_Test Item For QI Purposes"
|
||||
if not frappe.db.exists("Item", item_code):
|
||||
create_item(item_code, is_stock_item=1)
|
||||
|
||||
s_wh = "Stores - _TC"
|
||||
t_wh = "_Test Warehouse - _TC"
|
||||
# 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
|
||||
purposes = {
|
||||
"Material Receipt": {"to_warehouse": t_wh},
|
||||
"Material Transfer": {"from_warehouse": s_wh, "to_warehouse": t_wh},
|
||||
"Material Issue": {"from_warehouse": s_wh},
|
||||
}
|
||||
|
||||
for purpose, warehouses in purposes.items():
|
||||
with self.subTest(purpose=purpose):
|
||||
needs_qi = "to_warehouse" in warehouses
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=5,
|
||||
basic_rate=100,
|
||||
purpose=purpose,
|
||||
inspection_required=True,
|
||||
do_not_submit=True,
|
||||
**warehouses,
|
||||
)
|
||||
|
||||
# QI can be created from the Stock Entry for any purpose
|
||||
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
|
||||
self.assertRaises(QualityInspectionRequiredError, se.submit)
|
||||
|
||||
# a rejected QI must also block submission
|
||||
se_rej = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=5,
|
||||
basic_rate=100,
|
||||
purpose=purpose,
|
||||
inspection_required=True,
|
||||
do_not_submit=True,
|
||||
**warehouses,
|
||||
)
|
||||
create_quality_inspection(
|
||||
reference_type="Stock Entry",
|
||||
reference_name=se_rej.name,
|
||||
item_code=item_code,
|
||||
inspection_type="Incoming",
|
||||
status="Rejected",
|
||||
)
|
||||
se_rej.reload()
|
||||
self.assertRaises(QualityInspectionRejectedError, se_rej.submit)
|
||||
|
||||
# a submitted, accepted QI links itself to the inward row; submission then succeeds
|
||||
se_ok = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=5,
|
||||
basic_rate=100,
|
||||
purpose=purpose,
|
||||
inspection_required=True,
|
||||
do_not_submit=True,
|
||||
**warehouses,
|
||||
)
|
||||
create_quality_inspection(
|
||||
reference_type="Stock Entry",
|
||||
reference_name=se_ok.name,
|
||||
item_code=item_code,
|
||||
inspection_type="Incoming",
|
||||
status="Accepted",
|
||||
)
|
||||
se_ok.reload()
|
||||
se_ok.submit()
|
||||
self.assertEqual(se_ok.docstatus, 1)
|
||||
|
||||
@change_settings("Stock Settings", {"action_if_quality_inspection_is_rejected": "Stop"})
|
||||
def test_quality_inspection_required_for_manufacture(self):
|
||||
from erpnext.controllers.stock_controller import (
|
||||
QualityInspectionRejectedError,
|
||||
QualityInspectionRequiredError,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_wo_stock_entry,
|
||||
)
|
||||
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
||||
create_quality_inspection,
|
||||
)
|
||||
|
||||
wo = make_wo_order_test_record(qty=1)
|
||||
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=100
|
||||
)
|
||||
|
||||
# transfer raw materials to WIP (no inspection on the transfer)
|
||||
transfer = frappe.get_doc(make_wo_stock_entry(wo.name, "Material Transfer for Manufacture", 1))
|
||||
for d in transfer.get("items"):
|
||||
d.s_warehouse = "Stores - _TC"
|
||||
transfer.insert()
|
||||
transfer.submit()
|
||||
|
||||
# manufacture with inspection required
|
||||
mfg = frappe.get_doc(make_wo_stock_entry(wo.name, "Manufacture", 1))
|
||||
mfg.inspection_required = 1
|
||||
mfg.insert()
|
||||
self.assertRaises(QualityInspectionRequiredError, mfg.submit)
|
||||
|
||||
# a rejected QI on the finished-good row must also block submission
|
||||
qi = create_quality_inspection(
|
||||
reference_type="Stock Entry",
|
||||
reference_name=mfg.name,
|
||||
item_code=wo.production_item,
|
||||
inspection_type="Incoming",
|
||||
status="Rejected",
|
||||
)
|
||||
mfg.reload()
|
||||
self.assertRaises(QualityInspectionRejectedError, mfg.submit)
|
||||
|
||||
# accepting the QI then allows submission
|
||||
frappe.db.set_value("Quality Inspection", qi.name, "status", "Accepted")
|
||||
mfg.reload()
|
||||
mfg.submit()
|
||||
self.assertEqual(mfg.docstatus, 1)
|
||||
|
||||
@change_settings("Stock Settings", {"action_if_quality_inspection_is_rejected": "Stop"})
|
||||
def test_quality_inspection_required_for_material_transfer_for_manufacture(self):
|
||||
from erpnext.controllers.stock_controller import (
|
||||
QualityInspectionRejectedError,
|
||||
QualityInspectionRequiredError,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_wo_stock_entry,
|
||||
)
|
||||
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
||||
create_quality_inspection,
|
||||
)
|
||||
|
||||
wo = make_wo_order_test_record(qty=1)
|
||||
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=100
|
||||
)
|
||||
|
||||
transfer = frappe.get_doc(make_wo_stock_entry(wo.name, "Material Transfer for Manufacture", 1))
|
||||
for d in transfer.get("items"):
|
||||
d.s_warehouse = "Stores - _TC"
|
||||
transfer.inspection_required = 1
|
||||
transfer.insert()
|
||||
self.assertRaises(QualityInspectionRequiredError, transfer.submit)
|
||||
|
||||
# a rejected QI on any row moved into WIP must block submission;
|
||||
# every raw-material row moved into WIP needs a QI
|
||||
qis = []
|
||||
for item_code in {d.item_code for d in transfer.items if d.t_warehouse}:
|
||||
qis.append(
|
||||
create_quality_inspection(
|
||||
reference_type="Stock Entry",
|
||||
reference_name=transfer.name,
|
||||
item_code=item_code,
|
||||
inspection_type="Incoming",
|
||||
status="Rejected",
|
||||
)
|
||||
)
|
||||
transfer.reload()
|
||||
self.assertRaises(QualityInspectionRejectedError, transfer.submit)
|
||||
|
||||
# accepting every QI then allows submission
|
||||
for qi in qis:
|
||||
frappe.db.set_value("Quality Inspection", qi.name, "status", "Accepted")
|
||||
transfer.reload()
|
||||
transfer.submit()
|
||||
self.assertEqual(transfer.docstatus, 1)
|
||||
|
||||
def test_quality_inspection_required_for_send_to_subcontractor(self):
|
||||
from erpnext.controllers.stock_controller import QualityInspectionRequiredError
|
||||
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
get_subcontracting_order,
|
||||
make_service_item,
|
||||
)
|
||||
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
||||
create_quality_inspection,
|
||||
)
|
||||
|
||||
make_service_item("Subcontracted Service Item 1")
|
||||
sco = get_subcontracting_order(
|
||||
service_items=[
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 10,
|
||||
"rate": 500,
|
||||
"fg_item": "_Test FG Item",
|
||||
"fg_item_qty": 10,
|
||||
}
|
||||
]
|
||||
)
|
||||
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=100, basic_rate=100
|
||||
)
|
||||
|
||||
se = frappe.get_doc(make_rm_stock_entry(sco.name))
|
||||
se.from_warehouse = "_Test Warehouse - _TC"
|
||||
se.to_warehouse = "_Test Warehouse - _TC"
|
||||
se.stock_entry_type = "Send to Subcontractor"
|
||||
se.inspection_required = 1
|
||||
se.insert()
|
||||
self.assertRaises(QualityInspectionRequiredError, se.submit)
|
||||
|
||||
for item_code in {row.item_code for row in se.items if row.t_warehouse}:
|
||||
create_quality_inspection(
|
||||
reference_type="Stock Entry",
|
||||
reference_name=se.name,
|
||||
item_code=item_code,
|
||||
inspection_type="Outgoing",
|
||||
status="Accepted",
|
||||
)
|
||||
se.reload()
|
||||
se.submit()
|
||||
self.assertEqual(se.docstatus, 1)
|
||||
|
||||
@change_settings("Stock Settings", {"action_if_quality_inspection_is_rejected": "Stop"})
|
||||
def test_quality_inspection_required_for_disassemble(self):
|
||||
from erpnext.controllers.stock_controller import (
|
||||
QualityInspectionRejectedError,
|
||||
QualityInspectionRequiredError,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_wo_stock_entry,
|
||||
)
|
||||
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
||||
create_quality_inspection,
|
||||
)
|
||||
|
||||
source_warehouse = "Stores - _TC"
|
||||
fg_item = make_item("Test Disassemble FG QI", {"is_stock_item": 1}).name
|
||||
raw_materials = ["Test Disassemble RM QI 1", "Test Disassemble RM QI 2"]
|
||||
for item in raw_materials:
|
||||
make_item(item, {"is_stock_item": 1})
|
||||
make_stock_entry(item_code=item, target=source_warehouse, qty=5, basic_rate=100)
|
||||
|
||||
make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials)
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
item=fg_item, qty=1, source_warehouse=source_warehouse, skip_transfer=1
|
||||
)
|
||||
|
||||
# manufacture the FG so there is something to disassemble
|
||||
mfg = frappe.get_doc(make_wo_stock_entry(wo.name, "Manufacture", 1))
|
||||
for row in mfg.items:
|
||||
if row.item_code in raw_materials:
|
||||
row.s_warehouse = source_warehouse
|
||||
mfg.submit()
|
||||
|
||||
# disassemble with inspection required -> the component 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
|
||||
qis = []
|
||||
for item_code in {row.item_code for row in dis.items if row.t_warehouse}:
|
||||
qis.append(
|
||||
create_quality_inspection(
|
||||
reference_type="Stock Entry",
|
||||
reference_name=dis.name,
|
||||
item_code=item_code,
|
||||
inspection_type="Outgoing",
|
||||
status="Rejected",
|
||||
)
|
||||
)
|
||||
dis.reload()
|
||||
self.assertRaises(QualityInspectionRejectedError, dis.submit)
|
||||
|
||||
# accepting every QI then allows submission
|
||||
for qi in qis:
|
||||
frappe.db.set_value("Quality Inspection", qi.name, "status", "Accepted")
|
||||
dis.reload()
|
||||
dis.submit()
|
||||
self.assertEqual(dis.docstatus, 1)
|
||||
|
||||
def test_customer_provided_parts_se(self):
|
||||
create_item("CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0)
|
||||
se = make_stock_entry(
|
||||
|
||||
Reference in New Issue
Block a user