Merge pull request #56749 from frappe/chore/strengthen-bom-operations-time-filters

test: strengthen BOM Operations Time filter isolation coverage
This commit is contained in:
Nabin Hait
2026-07-02 12:32:00 +05:30
committed by GitHub

View File

@@ -9,77 +9,93 @@ from erpnext.tests.utils import ERPNextTestSuite
OPERATION = "_Test BOM Ops Time Operation"
WORKSTATION = "_Test BOM Ops Time Workstation"
OTHER_OPERATION = "_Test BOM Ops Time Operation 2"
OTHER_WORKSTATION = "_Test BOM Ops Time Workstation 2"
TIME_IN_MINS = 45
class TestBOMOperationsTime(ERPNextTestSuite):
def setUp(self):
ensure_workstation_and_operation()
ensure_workstation_and_operation(WORKSTATION, OPERATION)
self.rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name
self.fg_item = make_item(properties={"is_stock_item": 1}).name
self.bom = build_bom_with_operation(self.fg_item, self.rm_item)
self.bom = build_bom_with_operation(self.fg_item, self.rm_item, OPERATION, WORKSTATION)
def run_report(self, **extra):
filters = frappe._dict({"bom_id": [self.bom.name]})
filters.update(extra)
return execute(filters)[1]
def run_report(self, **filters):
return execute(frappe._dict(filters))[1]
def bom_names(self, rows):
return {row.name for row in rows}
def build_other_bom(self):
"""A submitted BOM for a different item, built on a different workstation."""
ensure_workstation_and_operation(OTHER_WORKSTATION, OTHER_OPERATION)
other_fg = make_item(properties={"is_stock_item": 1}).name
return build_bom_with_operation(other_fg, self.rm_item, OTHER_OPERATION, OTHER_WORKSTATION)
def test_operation_row_appears_with_expected_values(self):
rows = self.run_report()
rows = self.run_report(bom_id=[self.bom.name])
bom_rows = [row for row in rows if row.name == self.bom.name]
self.assertEqual(len(bom_rows), 1)
row = bom_rows[0]
self.assertEqual(len(rows), 1)
row = rows[0]
self.assertEqual(row.name, self.bom.name)
self.assertEqual(row.item, self.fg_item)
self.assertEqual(row.operation, OPERATION)
self.assertEqual(row.workstation, WORKSTATION)
self.assertEqual(row.time_in_mins, TIME_IN_MINS)
def test_item_code_filter_scopes_to_bom(self):
rows = self.run_report(item_code=self.fg_item)
def test_item_code_filter_includes_matching_and_excludes_other(self):
other_bom = self.build_other_bom()
self.assertTrue(rows)
self.assertTrue(all(row.item == self.fg_item for row in rows))
self.assertIn(self.bom.name, {row.name for row in rows})
# no bom_id here, so the item_code filter alone must scope the result
names = self.bom_names(self.run_report(item_code=self.fg_item))
self.assertIn(self.bom.name, names)
self.assertNotIn(other_bom.name, names)
def test_workstation_filter(self):
matching = self.run_report(workstation=WORKSTATION)
self.assertIn(self.bom.name, {row.name for row in matching})
# reverse direction: filtering the other item drops our BOM
other_names = self.bom_names(self.run_report(item_code=other_bom.item))
self.assertIn(other_bom.name, other_names)
self.assertNotIn(self.bom.name, other_names)
other_workstation = ensure_other_workstation()
non_matching = self.run_report(workstation=other_workstation)
self.assertNotIn(self.bom.name, {row.name for row in non_matching})
def test_workstation_filter_includes_matching_and_excludes_other(self):
other_bom = self.build_other_bom()
# no bom_id here, so the workstation filter alone must scope the result
names = self.bom_names(self.run_report(workstation=WORKSTATION))
self.assertIn(self.bom.name, names)
self.assertNotIn(other_bom.name, names)
# reverse direction: filtering the other workstation drops our BOM
other_names = self.bom_names(self.run_report(workstation=OTHER_WORKSTATION))
self.assertIn(other_bom.name, other_names)
self.assertNotIn(self.bom.name, other_names)
def test_draft_bom_excluded(self):
draft_bom = build_bom_with_operation(
make_item(properties={"is_stock_item": 1}).name, self.rm_item, do_not_submit=True
make_item(properties={"is_stock_item": 1}).name,
self.rm_item,
OPERATION,
WORKSTATION,
do_not_submit=True,
)
rows = execute(frappe._dict({"bom_id": [draft_bom.name]}))[1]
rows = self.run_report(bom_id=[draft_bom.name])
self.assertEqual(rows, [])
def ensure_workstation_and_operation():
if not frappe.db.exists("Workstation", WORKSTATION):
frappe.get_doc({"doctype": "Workstation", "workstation_name": WORKSTATION}).insert(
def ensure_workstation_and_operation(workstation, operation):
if not frappe.db.exists("Workstation", workstation):
frappe.get_doc({"doctype": "Workstation", "workstation_name": workstation}).insert(
ignore_permissions=True
)
if not frappe.db.exists("Operation", OPERATION):
frappe.get_doc({"doctype": "Operation", "name": OPERATION, "workstation": WORKSTATION}).insert(
if not frappe.db.exists("Operation", operation):
frappe.get_doc({"doctype": "Operation", "name": operation, "workstation": workstation}).insert(
ignore_permissions=True
)
def ensure_other_workstation():
name = "_Test BOM Ops Time Workstation 2"
if not frappe.db.exists("Workstation", name):
frappe.get_doc({"doctype": "Workstation", "workstation_name": name}).insert(ignore_permissions=True)
return name
def build_bom_with_operation(fg_item, rm_item, do_not_submit=False):
def build_bom_with_operation(fg_item, rm_item, operation, workstation, do_not_submit=False):
bom = make_bom(
item=fg_item,
raw_materials=[rm_item],
@@ -89,8 +105,8 @@ def build_bom_with_operation(fg_item, rm_item, do_not_submit=False):
bom.append(
"operations",
{
"operation": OPERATION,
"workstation": WORKSTATION,
"operation": operation,
"workstation": workstation,
"time_in_mins": TIME_IN_MINS,
"hour_rate": 100,
},