feat: polish reject history UI and enhance WIP filter interactions
This commit is contained in:
@@ -71,7 +71,8 @@ class TestOverviewSummaryRoute(TestWipRoutesBase):
|
||||
}
|
||||
|
||||
self.client.get(
|
||||
'/api/wip/overview/summary?workorder=WO1&lotid=L1&package=SOT-23&type=PJA&include_dummy=true'
|
||||
'/api/wip/overview/summary?workorder=WO1&lotid=L1&package=SOT-23'
|
||||
'&type=PJA&firstname=WF001&waferdesc=SiC&include_dummy=true'
|
||||
)
|
||||
|
||||
mock_get_summary.assert_called_once_with(
|
||||
@@ -79,7 +80,9 @@ class TestOverviewSummaryRoute(TestWipRoutesBase):
|
||||
workorder='WO1',
|
||||
lotid='L1',
|
||||
package='SOT-23',
|
||||
pj_type='PJA'
|
||||
pj_type='PJA',
|
||||
firstname='WF001',
|
||||
waferdesc='SiC',
|
||||
)
|
||||
|
||||
|
||||
@@ -135,6 +138,35 @@ class TestOverviewMatrixRoute(TestWipRoutesBase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertFalse(data['success'])
|
||||
self.assertIn('Invalid hold_type', data['error'])
|
||||
|
||||
@patch('mes_dashboard.routes.wip_routes.get_wip_matrix')
|
||||
def test_passes_filters_to_service(self, mock_get_matrix):
|
||||
"""Should pass overview matrix filters to service layer."""
|
||||
mock_get_matrix.return_value = {
|
||||
'workcenters': [],
|
||||
'packages': [],
|
||||
'matrix': {},
|
||||
'workcenter_totals': {},
|
||||
'package_totals': {},
|
||||
'grand_total': 0,
|
||||
}
|
||||
|
||||
self.client.get(
|
||||
'/api/wip/overview/matrix?workorder=WO1&lotid=L1&package=SOT-23&type=PJA'
|
||||
'&firstname=WF001&waferdesc=SiC&status=RUN&include_dummy=1'
|
||||
)
|
||||
|
||||
mock_get_matrix.assert_called_once_with(
|
||||
include_dummy=True,
|
||||
workorder='WO1',
|
||||
lotid='L1',
|
||||
status='RUN',
|
||||
hold_type=None,
|
||||
package='SOT-23',
|
||||
pj_type='PJA',
|
||||
firstname='WF001',
|
||||
waferdesc='SiC',
|
||||
)
|
||||
|
||||
|
||||
class TestOverviewHoldRoute(TestWipRoutesBase):
|
||||
@@ -173,12 +205,19 @@ class TestOverviewHoldRoute(TestWipRoutesBase):
|
||||
"""Should pass hold filter params to service layer."""
|
||||
mock_get_hold.return_value = {'items': []}
|
||||
|
||||
self.client.get('/api/wip/overview/hold?workorder=WO1&lotid=L1&include_dummy=1')
|
||||
self.client.get(
|
||||
'/api/wip/overview/hold?workorder=WO1&lotid=L1&package=SOT-23&type=PJA'
|
||||
'&firstname=WF001&waferdesc=SiC&include_dummy=1'
|
||||
)
|
||||
|
||||
mock_get_hold.assert_called_once_with(
|
||||
include_dummy=True,
|
||||
workorder='WO1',
|
||||
lotid='L1'
|
||||
lotid='L1',
|
||||
package='SOT-23',
|
||||
pj_type='PJA',
|
||||
firstname='WF001',
|
||||
waferdesc='SiC',
|
||||
)
|
||||
|
||||
|
||||
@@ -233,17 +272,20 @@ class TestDetailRoute(TestWipRoutesBase):
|
||||
'sys_date': None
|
||||
}
|
||||
|
||||
response = self.client.get(
|
||||
'/api/wip/detail/焊接_DB?package=SOT-23&status=RUN&page=2&page_size=50'
|
||||
)
|
||||
|
||||
mock_get_detail.assert_called_once_with(
|
||||
workcenter='焊接_DB',
|
||||
package='SOT-23',
|
||||
pj_type=None,
|
||||
status='RUN',
|
||||
hold_type=None,
|
||||
workorder=None,
|
||||
response = self.client.get(
|
||||
'/api/wip/detail/焊接_DB?package=SOT-23&type=PJA&firstname=WF001&waferdesc=SiC'
|
||||
'&status=RUN&page=2&page_size=50'
|
||||
)
|
||||
|
||||
mock_get_detail.assert_called_once_with(
|
||||
workcenter='焊接_DB',
|
||||
package='SOT-23',
|
||||
pj_type='PJA',
|
||||
firstname='WF001',
|
||||
waferdesc='SiC',
|
||||
status='RUN',
|
||||
hold_type=None,
|
||||
workorder=None,
|
||||
lotid=None,
|
||||
include_dummy=False,
|
||||
page=2,
|
||||
@@ -381,7 +423,7 @@ class TestMetaWorkcentersRoute(TestWipRoutesBase):
|
||||
self.assertFalse(data['success'])
|
||||
|
||||
|
||||
class TestMetaPackagesRoute(TestWipRoutesBase):
|
||||
class TestMetaPackagesRoute(TestWipRoutesBase):
|
||||
"""Test GET /api/wip/meta/packages endpoint."""
|
||||
|
||||
@patch('mes_dashboard.routes.wip_routes.get_packages')
|
||||
@@ -408,10 +450,91 @@ class TestMetaPackagesRoute(TestWipRoutesBase):
|
||||
response = self.client.get('/api/wip/meta/packages')
|
||||
data = json.loads(response.data)
|
||||
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertFalse(data['success'])
|
||||
|
||||
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertFalse(data['success'])
|
||||
|
||||
|
||||
class TestMetaFilterOptionsRoute(TestWipRoutesBase):
|
||||
"""Test GET /api/wip/meta/filter-options endpoint."""
|
||||
|
||||
@patch('mes_dashboard.routes.wip_routes.get_wip_filter_options')
|
||||
def test_returns_success_with_options(self, mock_get_options):
|
||||
mock_get_options.return_value = {
|
||||
'workorders': ['WO1'],
|
||||
'lotids': ['LOT1'],
|
||||
'packages': ['PKG1'],
|
||||
'types': ['TYPE1'],
|
||||
'firstnames': ['WF001'],
|
||||
'waferdescs': ['SiC'],
|
||||
}
|
||||
|
||||
response = self.client.get('/api/wip/meta/filter-options')
|
||||
data = json.loads(response.data)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(data['success'])
|
||||
self.assertEqual(data['data']['workorders'], ['WO1'])
|
||||
self.assertEqual(data['data']['waferdescs'], ['SiC'])
|
||||
|
||||
@patch('mes_dashboard.routes.wip_routes.get_wip_filter_options')
|
||||
def test_passes_include_dummy_flag(self, mock_get_options):
|
||||
mock_get_options.return_value = {
|
||||
'workorders': [],
|
||||
'lotids': [],
|
||||
'packages': [],
|
||||
'types': [],
|
||||
'firstnames': [],
|
||||
'waferdescs': [],
|
||||
}
|
||||
|
||||
self.client.get('/api/wip/meta/filter-options?include_dummy=true')
|
||||
mock_get_options.assert_called_once_with(
|
||||
include_dummy=True,
|
||||
workorder=None,
|
||||
lotid=None,
|
||||
package=None,
|
||||
pj_type=None,
|
||||
firstname=None,
|
||||
waferdesc=None,
|
||||
)
|
||||
|
||||
@patch('mes_dashboard.routes.wip_routes.get_wip_filter_options')
|
||||
def test_passes_cross_filter_parameters(self, mock_get_options):
|
||||
mock_get_options.return_value = {
|
||||
'workorders': ['WO1'],
|
||||
'lotids': ['LOT1'],
|
||||
'packages': ['PKG1'],
|
||||
'types': ['TYPE1'],
|
||||
'firstnames': ['WF001'],
|
||||
'waferdescs': ['SiC'],
|
||||
}
|
||||
|
||||
self.client.get(
|
||||
'/api/wip/meta/filter-options?workorder=WO1,WO2&lotid=L1&package=PKG1'
|
||||
'&type=PJA&firstname=WF001&waferdesc=SiC'
|
||||
)
|
||||
|
||||
mock_get_options.assert_called_once_with(
|
||||
include_dummy=False,
|
||||
workorder='WO1,WO2',
|
||||
lotid='L1',
|
||||
package='PKG1',
|
||||
pj_type='PJA',
|
||||
firstname='WF001',
|
||||
waferdesc='SiC',
|
||||
)
|
||||
|
||||
@patch('mes_dashboard.routes.wip_routes.get_wip_filter_options')
|
||||
def test_returns_error_on_failure(self, mock_get_options):
|
||||
mock_get_options.return_value = None
|
||||
|
||||
response = self.client.get('/api/wip/meta/filter-options')
|
||||
data = json.loads(response.data)
|
||||
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertFalse(data['success'])
|
||||
|
||||
|
||||
class TestPageRoutes(TestWipRoutesBase):
|
||||
"""Test page routes for WIP dashboards."""
|
||||
|
||||
|
||||
@@ -20,12 +20,13 @@ from mes_dashboard.services.wip_service import (
|
||||
get_wip_detail,
|
||||
get_hold_detail_summary,
|
||||
get_hold_detail_lots,
|
||||
get_hold_overview_treemap,
|
||||
get_workcenters,
|
||||
get_packages,
|
||||
search_workorders,
|
||||
search_lot_ids,
|
||||
)
|
||||
get_hold_overview_treemap,
|
||||
get_workcenters,
|
||||
get_packages,
|
||||
get_wip_filter_options,
|
||||
search_workorders,
|
||||
search_lot_ids,
|
||||
)
|
||||
|
||||
|
||||
def disable_cache(func):
|
||||
@@ -256,7 +257,7 @@ class TestGetWorkcenters(unittest.TestCase):
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestGetPackages(unittest.TestCase):
|
||||
class TestGetPackages(unittest.TestCase):
|
||||
"""Test get_packages function."""
|
||||
|
||||
@disable_cache
|
||||
@@ -282,12 +283,90 @@ class TestGetPackages(unittest.TestCase):
|
||||
"""Should return empty list when no packages."""
|
||||
mock_read_sql.return_value = pd.DataFrame()
|
||||
|
||||
result = get_packages()
|
||||
|
||||
self.assertEqual(result, [])
|
||||
|
||||
|
||||
class TestSearchWorkorders(unittest.TestCase):
|
||||
result = get_packages()
|
||||
|
||||
self.assertEqual(result, [])
|
||||
|
||||
|
||||
class TestGetWipFilterOptions(unittest.TestCase):
|
||||
"""Test get_wip_filter_options function."""
|
||||
|
||||
def setUp(self):
|
||||
import mes_dashboard.services.wip_service as wip_service
|
||||
with wip_service._wip_search_index_lock:
|
||||
wip_service._wip_search_index_cache.clear()
|
||||
with wip_service._wip_snapshot_lock:
|
||||
wip_service._wip_snapshot_cache.clear()
|
||||
|
||||
@patch('mes_dashboard.services.wip_service._get_wip_search_index')
|
||||
def test_prefers_search_index_payload(self, mock_get_index):
|
||||
mock_get_index.return_value = {
|
||||
'workorders': ['WO1', 'WO2'],
|
||||
'lotids': ['LOT1'],
|
||||
'packages': ['PKG1'],
|
||||
'types': ['TYPE1'],
|
||||
'firstnames': ['WF001'],
|
||||
'waferdescs': ['SiC'],
|
||||
}
|
||||
|
||||
result = get_wip_filter_options()
|
||||
|
||||
self.assertEqual(result['workorders'], ['WO1', 'WO2'])
|
||||
self.assertEqual(result['firstnames'], ['WF001'])
|
||||
self.assertEqual(result['waferdescs'], ['SiC'])
|
||||
|
||||
@patch('mes_dashboard.services.wip_service._get_wip_search_index', return_value=None)
|
||||
@patch('mes_dashboard.services.wip_service._get_wip_dataframe')
|
||||
def test_interdependent_options_follow_cross_filters(self, mock_cached_wip, _mock_get_index):
|
||||
mock_cached_wip.return_value = pd.DataFrame({
|
||||
'WORKORDER': ['WO1', 'WO1', 'WO2'],
|
||||
'LOTID': ['L1', 'L2', 'L3'],
|
||||
'PACKAGE_LEF': ['PKG-A', 'PKG-B', 'PKG-B'],
|
||||
'PJ_TYPE': ['TYPE-1', 'TYPE-1', 'TYPE-2'],
|
||||
'FIRSTNAME': ['WF-A', 'WF-B', 'WF-A'],
|
||||
'WAFERDESC': ['SiC', 'SiC', 'Si'],
|
||||
'EQUIPMENTCOUNT': [0, 1, 0],
|
||||
'CURRENTHOLDCOUNT': [1, 0, 0],
|
||||
'QTY': [10, 20, 30],
|
||||
'HOLDREASONNAME': ['Q-Check', None, None],
|
||||
'WORKCENTER_GROUP': ['WC-A', 'WC-A', 'WC-B'],
|
||||
})
|
||||
|
||||
result = get_wip_filter_options(workorder='WO1')
|
||||
|
||||
# Exclude-self semantics: workorder options still show values allowed by other filters.
|
||||
self.assertEqual(result['workorders'], ['WO1', 'WO2'])
|
||||
self.assertEqual(result['lotids'], ['L1', 'L2'])
|
||||
self.assertEqual(result['types'], ['TYPE-1'])
|
||||
self.assertEqual(result['waferdescs'], ['SiC'])
|
||||
|
||||
@patch('mes_dashboard.services.wip_service._get_wip_search_index', return_value=None)
|
||||
@patch('mes_dashboard.services.wip_service._select_with_snapshot_indexes')
|
||||
@patch('mes_dashboard.services.wip_service._get_wip_dataframe')
|
||||
def test_falls_back_to_cache_dataframe(
|
||||
self,
|
||||
mock_cached_wip,
|
||||
mock_select_with_snapshot,
|
||||
_mock_get_index,
|
||||
):
|
||||
mock_cached_wip.return_value = pd.DataFrame({'WORKORDER': ['WO1']})
|
||||
mock_select_with_snapshot.return_value = pd.DataFrame({
|
||||
'WORKORDER': ['WO2', 'WO1'],
|
||||
'LOTID': ['LOT2', 'LOT1'],
|
||||
'PACKAGE_LEF': ['PKG2', 'PKG1'],
|
||||
'PJ_TYPE': ['TYPE2', 'TYPE1'],
|
||||
'FIRSTNAME': ['WF002', 'WF001'],
|
||||
'WAFERDESC': ['Si', 'SiC'],
|
||||
})
|
||||
|
||||
result = get_wip_filter_options()
|
||||
|
||||
self.assertEqual(result['workorders'], ['WO1', 'WO2'])
|
||||
self.assertEqual(result['firstnames'], ['WF001', 'WF002'])
|
||||
self.assertEqual(result['waferdescs'], ['Si', 'SiC'])
|
||||
|
||||
|
||||
class TestSearchWorkorders(unittest.TestCase):
|
||||
"""Test search_workorders function."""
|
||||
|
||||
@disable_cache
|
||||
@@ -688,8 +767,9 @@ class TestMultipleFilterConditions(unittest.TestCase):
|
||||
sql = call_args[0][0]
|
||||
params = call_args[0][1] if len(call_args[0]) > 1 else {}
|
||||
|
||||
self.assertIn("WORKORDER LIKE", sql)
|
||||
self.assertIn("LOTID LIKE", sql)
|
||||
self.assertIn("WORKORDER", sql)
|
||||
self.assertIn("LOTID", sql)
|
||||
self.assertIn("LIKE", sql)
|
||||
self.assertIn("LOTID NOT LIKE '%DUMMY%'", sql)
|
||||
# Verify params contain the search patterns
|
||||
self.assertTrue(any('%GA26%' in str(v) for v in params.values()))
|
||||
@@ -714,8 +794,9 @@ class TestMultipleFilterConditions(unittest.TestCase):
|
||||
sql = call_args[0][0]
|
||||
params = call_args[0][1] if len(call_args[0]) > 1 else {}
|
||||
|
||||
self.assertIn("WORKORDER LIKE", sql)
|
||||
self.assertIn("LOTID LIKE", sql)
|
||||
self.assertIn("WORKORDER", sql)
|
||||
self.assertIn("LOTID", sql)
|
||||
self.assertIn("LIKE", sql)
|
||||
# Should NOT contain DUMMY exclusion since include_dummy=True
|
||||
self.assertNotIn("LOTID NOT LIKE '%DUMMY%'", sql)
|
||||
# Verify params contain the search patterns
|
||||
|
||||
Reference in New Issue
Block a user