feat: polish reject history UI and enhance WIP filter interactions

This commit is contained in:
egg
2026-02-22 11:54:51 +08:00
parent 9687deb9ad
commit 7bf9e33cd5
35 changed files with 3054 additions and 1085 deletions

View File

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

View File

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