feat(hold-overview): add Hold Lot Overview page with TreeMap, Matrix, and cascade filtering
Provide managers with a dedicated page to analyze hold lots across all stations. Extends existing service functions (get_hold_detail_summary, get_hold_detail_lots, get_wip_matrix) with optional parameters for backward compatibility, adds one new function (get_hold_overview_treemap), and registers the page in the portal navigation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -53,12 +53,17 @@ class AppFactoryTests(unittest.TestCase):
|
||||
"/resource",
|
||||
"/wip-overview",
|
||||
"/wip-detail",
|
||||
"/hold-overview",
|
||||
"/excel-query",
|
||||
"/query-tool",
|
||||
"/tmtt-defect",
|
||||
"/api/wip/overview/summary",
|
||||
"/api/wip/overview/matrix",
|
||||
"/api/wip/overview/hold",
|
||||
"/api/hold-overview/summary",
|
||||
"/api/hold-overview/matrix",
|
||||
"/api/hold-overview/treemap",
|
||||
"/api/hold-overview/lots",
|
||||
"/api/wip/detail/<workcenter>",
|
||||
"/api/wip/meta/workcenters",
|
||||
"/api/wip/meta/packages",
|
||||
|
||||
228
tests/test_hold_overview_routes.py
Normal file
228
tests/test_hold_overview_routes.py
Normal file
@@ -0,0 +1,228 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Unit tests for Hold Overview API routes."""
|
||||
|
||||
import json
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from mes_dashboard.app import create_app
|
||||
import mes_dashboard.core.database as db
|
||||
|
||||
|
||||
class TestHoldOverviewRoutesBase(unittest.TestCase):
|
||||
"""Base class for Hold Overview route tests."""
|
||||
|
||||
def setUp(self):
|
||||
db._ENGINE = None
|
||||
self.app = create_app('testing')
|
||||
self.app.config['TESTING'] = True
|
||||
self.client = self.app.test_client()
|
||||
|
||||
|
||||
class TestHoldOverviewPageRoute(TestHoldOverviewRoutesBase):
|
||||
"""Test GET /hold-overview page route."""
|
||||
|
||||
@patch('mes_dashboard.routes.hold_overview_routes.os.path.exists', return_value=False)
|
||||
def test_hold_overview_page_includes_vite_entry(self, _mock_exists):
|
||||
# Page is registered as 'dev' status, requires admin session
|
||||
with self.client.session_transaction() as sess:
|
||||
sess['admin'] = {'displayName': 'Test Admin', 'employeeNo': 'A001'}
|
||||
response = self.client.get('/hold-overview')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'/static/dist/hold-overview.js', response.data)
|
||||
|
||||
@patch('mes_dashboard.routes.hold_overview_routes.os.path.exists', return_value=False)
|
||||
def test_hold_overview_page_returns_403_without_admin(self, _mock_exists):
|
||||
response = self.client.get('/hold-overview')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class TestHoldOverviewSummaryRoute(TestHoldOverviewRoutesBase):
|
||||
"""Test GET /api/hold-overview/summary endpoint."""
|
||||
|
||||
@patch('mes_dashboard.routes.hold_overview_routes.get_hold_detail_summary')
|
||||
def test_summary_defaults_to_quality(self, mock_service):
|
||||
mock_service.return_value = {
|
||||
'totalLots': 12,
|
||||
'totalQty': 3400,
|
||||
'avgAge': 2.5,
|
||||
'maxAge': 9.0,
|
||||
'workcenterCount': 3,
|
||||
'dataUpdateDate': '2026-01-01 08:00:00',
|
||||
}
|
||||
|
||||
response = self.client.get('/api/hold-overview/summary')
|
||||
payload = json.loads(response.data)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(payload['success'])
|
||||
mock_service.assert_called_once_with(
|
||||
reason=None,
|
||||
hold_type='quality',
|
||||
include_dummy=False,
|
||||
)
|
||||
|
||||
@patch('mes_dashboard.routes.hold_overview_routes.get_hold_detail_summary')
|
||||
def test_summary_hold_type_all_maps_to_none(self, mock_service):
|
||||
mock_service.return_value = {
|
||||
'totalLots': 0,
|
||||
'totalQty': 0,
|
||||
'avgAge': 0,
|
||||
'maxAge': 0,
|
||||
'workcenterCount': 0,
|
||||
'dataUpdateDate': None,
|
||||
}
|
||||
|
||||
response = self.client.get('/api/hold-overview/summary?hold_type=all&reason=品質確認')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
mock_service.assert_called_once_with(
|
||||
reason='品質確認',
|
||||
hold_type=None,
|
||||
include_dummy=False,
|
||||
)
|
||||
|
||||
def test_summary_invalid_hold_type(self):
|
||||
response = self.client.get('/api/hold-overview/summary?hold_type=invalid')
|
||||
payload = json.loads(response.data)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertFalse(payload['success'])
|
||||
|
||||
@patch('mes_dashboard.routes.hold_overview_routes.get_hold_detail_summary')
|
||||
def test_summary_failure_returns_500(self, mock_service):
|
||||
mock_service.return_value = None
|
||||
response = self.client.get('/api/hold-overview/summary')
|
||||
payload = json.loads(response.data)
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertFalse(payload['success'])
|
||||
|
||||
|
||||
class TestHoldOverviewMatrixRoute(TestHoldOverviewRoutesBase):
|
||||
"""Test GET /api/hold-overview/matrix endpoint."""
|
||||
|
||||
@patch('mes_dashboard.routes.hold_overview_routes.get_wip_matrix')
|
||||
def test_matrix_passes_hold_filters(self, mock_service):
|
||||
mock_service.return_value = {
|
||||
'workcenters': [],
|
||||
'packages': [],
|
||||
'matrix': {},
|
||||
'workcenter_totals': {},
|
||||
'package_totals': {},
|
||||
'grand_total': 0,
|
||||
}
|
||||
|
||||
response = self.client.get('/api/hold-overview/matrix?hold_type=non-quality&reason=特殊需求管控')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
mock_service.assert_called_once_with(
|
||||
include_dummy=False,
|
||||
status='HOLD',
|
||||
hold_type='non-quality',
|
||||
reason='特殊需求管控',
|
||||
)
|
||||
|
||||
def test_matrix_invalid_hold_type(self):
|
||||
response = self.client.get('/api/hold-overview/matrix?hold_type=invalid')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
@patch('mes_dashboard.routes.hold_overview_routes.get_wip_matrix')
|
||||
@patch('mes_dashboard.core.rate_limit.check_and_record', return_value=(True, 7))
|
||||
def test_matrix_rate_limited_returns_429(self, _mock_limit, mock_service):
|
||||
response = self.client.get('/api/hold-overview/matrix')
|
||||
payload = json.loads(response.data)
|
||||
self.assertEqual(response.status_code, 429)
|
||||
self.assertEqual(payload['error']['code'], 'TOO_MANY_REQUESTS')
|
||||
self.assertEqual(response.headers.get('Retry-After'), '7')
|
||||
mock_service.assert_not_called()
|
||||
|
||||
|
||||
class TestHoldOverviewTreemapRoute(TestHoldOverviewRoutesBase):
|
||||
"""Test GET /api/hold-overview/treemap endpoint."""
|
||||
|
||||
@patch('mes_dashboard.routes.hold_overview_routes.get_hold_overview_treemap')
|
||||
def test_treemap_passes_filters(self, mock_service):
|
||||
mock_service.return_value = {'items': []}
|
||||
|
||||
response = self.client.get(
|
||||
'/api/hold-overview/treemap?hold_type=quality&reason=品質確認&workcenter=WB&package=QFN'
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
mock_service.assert_called_once_with(
|
||||
hold_type='quality',
|
||||
reason='品質確認',
|
||||
workcenter='WB',
|
||||
package='QFN',
|
||||
include_dummy=False,
|
||||
)
|
||||
|
||||
@patch('mes_dashboard.routes.hold_overview_routes.get_hold_overview_treemap')
|
||||
def test_treemap_failure_returns_500(self, mock_service):
|
||||
mock_service.return_value = None
|
||||
response = self.client.get('/api/hold-overview/treemap')
|
||||
payload = json.loads(response.data)
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertFalse(payload['success'])
|
||||
|
||||
|
||||
class TestHoldOverviewLotsRoute(TestHoldOverviewRoutesBase):
|
||||
"""Test GET /api/hold-overview/lots endpoint."""
|
||||
|
||||
@patch('mes_dashboard.routes.hold_overview_routes.get_hold_detail_lots')
|
||||
def test_lots_passes_all_filters_and_caps_per_page(self, mock_service):
|
||||
mock_service.return_value = {
|
||||
'lots': [],
|
||||
'pagination': {'page': 2, 'perPage': 200, 'total': 0, 'totalPages': 1},
|
||||
'filters': {},
|
||||
}
|
||||
|
||||
response = self.client.get(
|
||||
'/api/hold-overview/lots?hold_type=all&reason=品質確認'
|
||||
'&workcenter=WB&package=QFN&treemap_reason=品質確認'
|
||||
'&age_range=1-3&page=2&per_page=500'
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
mock_service.assert_called_once_with(
|
||||
reason='品質確認',
|
||||
hold_type=None,
|
||||
treemap_reason='品質確認',
|
||||
workcenter='WB',
|
||||
package='QFN',
|
||||
age_range='1-3',
|
||||
include_dummy=False,
|
||||
page=2,
|
||||
page_size=200,
|
||||
)
|
||||
|
||||
@patch('mes_dashboard.routes.hold_overview_routes.get_hold_detail_lots')
|
||||
def test_lots_handles_page_less_than_one(self, mock_service):
|
||||
mock_service.return_value = {
|
||||
'lots': [],
|
||||
'pagination': {'page': 1, 'perPage': 50, 'total': 0, 'totalPages': 1},
|
||||
'filters': {},
|
||||
}
|
||||
|
||||
response = self.client.get('/api/hold-overview/lots?page=0')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
call_args = mock_service.call_args
|
||||
self.assertEqual(call_args.kwargs['page'], 1)
|
||||
|
||||
def test_lots_invalid_age_range(self):
|
||||
response = self.client.get('/api/hold-overview/lots?age_range=invalid')
|
||||
payload = json.loads(response.data)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertFalse(payload['success'])
|
||||
|
||||
def test_lots_invalid_hold_type(self):
|
||||
response = self.client.get('/api/hold-overview/lots?hold_type=invalid')
|
||||
payload = json.loads(response.data)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertFalse(payload['success'])
|
||||
|
||||
@patch('mes_dashboard.routes.hold_overview_routes.get_hold_detail_lots')
|
||||
@patch('mes_dashboard.core.rate_limit.check_and_record', return_value=(True, 4))
|
||||
def test_lots_rate_limited_returns_429(self, _mock_limit, mock_service):
|
||||
response = self.client.get('/api/hold-overview/lots')
|
||||
payload = json.loads(response.data)
|
||||
self.assertEqual(response.status_code, 429)
|
||||
self.assertEqual(payload['error']['code'], 'TOO_MANY_REQUESTS')
|
||||
self.assertEqual(response.headers.get('Retry-After'), '4')
|
||||
mock_service.assert_not_called()
|
||||
|
||||
@@ -35,14 +35,17 @@ def mock_registry(temp_data_file):
|
||||
"""Mock page_registry to use temp file."""
|
||||
original_data_file = page_registry.DATA_FILE
|
||||
original_cache = page_registry._cache
|
||||
original_cache_mtime = page_registry._cache_mtime
|
||||
|
||||
page_registry.DATA_FILE = temp_data_file
|
||||
page_registry._cache = None
|
||||
page_registry._cache_mtime = 0.0
|
||||
|
||||
yield temp_data_file
|
||||
|
||||
page_registry.DATA_FILE = original_data_file
|
||||
page_registry._cache = original_cache
|
||||
page_registry._cache_mtime = original_cache_mtime
|
||||
|
||||
|
||||
class TestSchemaMigration:
|
||||
@@ -205,7 +208,8 @@ class TestReloadCache:
|
||||
home["status"] = "dev"
|
||||
temp_data_file.write_text(json.dumps(data))
|
||||
|
||||
assert page_registry.get_page_status("/") == "released"
|
||||
# Note: _load() has mtime-based invalidation that may auto-detect
|
||||
# the file change, so we only assert post-reload behavior.
|
||||
page_registry.reload_cache()
|
||||
assert page_registry.get_page_status("/") == "dev"
|
||||
|
||||
|
||||
@@ -40,3 +40,29 @@ def test_hold_detail_lots_rate_limit_returns_429(_mock_limit, mock_service):
|
||||
assert payload['error']['code'] == 'TOO_MANY_REQUESTS'
|
||||
assert response.headers.get('Retry-After') == '4'
|
||||
mock_service.assert_not_called()
|
||||
|
||||
|
||||
@patch('mes_dashboard.routes.hold_overview_routes.get_wip_matrix')
|
||||
@patch('mes_dashboard.core.rate_limit.check_and_record', return_value=(True, 6))
|
||||
def test_hold_overview_matrix_rate_limit_returns_429(_mock_limit, mock_service):
|
||||
client = _client()
|
||||
response = client.get('/api/hold-overview/matrix')
|
||||
|
||||
assert response.status_code == 429
|
||||
payload = response.get_json()
|
||||
assert payload['error']['code'] == 'TOO_MANY_REQUESTS'
|
||||
assert response.headers.get('Retry-After') == '6'
|
||||
mock_service.assert_not_called()
|
||||
|
||||
|
||||
@patch('mes_dashboard.routes.hold_overview_routes.get_hold_detail_lots')
|
||||
@patch('mes_dashboard.core.rate_limit.check_and_record', return_value=(True, 3))
|
||||
def test_hold_overview_lots_rate_limit_returns_429(_mock_limit, mock_service):
|
||||
client = _client()
|
||||
response = client.get('/api/hold-overview/lots')
|
||||
|
||||
assert response.status_code == 429
|
||||
payload = response.get_json()
|
||||
assert payload['error']['code'] == 'TOO_MANY_REQUESTS'
|
||||
assert response.headers.get('Retry-After') == '3'
|
||||
mock_service.assert_not_called()
|
||||
|
||||
@@ -54,6 +54,15 @@ class TestTemplateIntegration(unittest.TestCase):
|
||||
self.assertIn('type="module"', html)
|
||||
self.assertNotIn('mes-toast-container', html)
|
||||
|
||||
def test_hold_overview_serves_pure_vite_module(self):
|
||||
response = self.client.get('/hold-overview')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
html = response.data.decode('utf-8')
|
||||
|
||||
self.assertIn('/static/dist/hold-overview.js', html)
|
||||
self.assertIn('type="module"', html)
|
||||
self.assertNotIn('mes-toast-container', html)
|
||||
|
||||
def test_tables_page_serves_pure_vite_module(self):
|
||||
response = self.client.get('/tables')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -303,6 +312,7 @@ class TestViteModuleIntegration(unittest.TestCase):
|
||||
endpoints_and_assets = [
|
||||
('/wip-overview', 'wip-overview.js'),
|
||||
('/wip-detail', 'wip-detail.js'),
|
||||
('/hold-overview', 'hold-overview.js'),
|
||||
('/hold-detail?reason=test-reason', 'hold-detail.js'),
|
||||
('/tables', 'tables.js'),
|
||||
('/resource', 'resource-status.js'),
|
||||
|
||||
@@ -9,17 +9,20 @@ from unittest.mock import patch, MagicMock
|
||||
from functools import wraps
|
||||
import pandas as pd
|
||||
|
||||
from mes_dashboard.services.wip_service import (
|
||||
WIP_VIEW,
|
||||
get_wip_summary,
|
||||
get_wip_matrix,
|
||||
get_wip_hold_summary,
|
||||
get_wip_detail,
|
||||
get_workcenters,
|
||||
get_packages,
|
||||
search_workorders,
|
||||
search_lot_ids,
|
||||
)
|
||||
from mes_dashboard.services.wip_service import (
|
||||
WIP_VIEW,
|
||||
get_wip_summary,
|
||||
get_wip_matrix,
|
||||
get_wip_hold_summary,
|
||||
get_wip_detail,
|
||||
get_hold_detail_summary,
|
||||
get_hold_detail_lots,
|
||||
get_hold_overview_treemap,
|
||||
get_workcenters,
|
||||
get_packages,
|
||||
search_workorders,
|
||||
search_lot_ids,
|
||||
)
|
||||
|
||||
|
||||
def disable_cache(func):
|
||||
@@ -654,7 +657,140 @@ class TestMultipleFilterConditions(unittest.TestCase):
|
||||
|
||||
|
||||
|
||||
import pytest
|
||||
class TestHoldOverviewServiceCachePath(unittest.TestCase):
|
||||
"""Test hold overview related behavior on cache path."""
|
||||
|
||||
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()
|
||||
|
||||
@staticmethod
|
||||
def _sample_hold_df() -> pd.DataFrame:
|
||||
return pd.DataFrame({
|
||||
'LOTID': ['L1', 'L2', 'L3', 'L4', 'L5'],
|
||||
'WORKORDER': ['WO1', 'WO2', 'WO3', 'WO4', 'WO5'],
|
||||
'QTY': [100, 50, 80, 60, 20],
|
||||
'PACKAGE_LEF': ['PKG-A', 'PKG-B', 'PKG-A', 'PKG-Z', 'PKG-C'],
|
||||
'WORKCENTER_GROUP': ['WC-A', 'WC-B', 'WC-A', 'WC-Z', 'WC-C'],
|
||||
'WORKCENTERSEQUENCE_GROUP': [1, 2, 1, 9, 3],
|
||||
'HOLDREASONNAME': ['品質確認', '特殊需求管控', '品質確認', None, '設備異常'],
|
||||
'AGEBYDAYS': [2.0, 3.0, 5.0, 0.3, 1.2],
|
||||
'EQUIPMENTCOUNT': [0, 0, 0, 1, 0],
|
||||
'CURRENTHOLDCOUNT': [1, 1, 1, 0, 1],
|
||||
'SPECNAME': ['S1', 'S2', 'S1', 'S9', 'S3'],
|
||||
'HOLDEMP': ['EMP1', 'EMP2', 'EMP3', 'EMP4', 'EMP5'],
|
||||
'DEPTNAME': ['QC', 'PD', 'QC', 'RUN', 'QC'],
|
||||
'COMMENT_HOLD': ['C1', 'C2', 'C3', 'C4', 'C5'],
|
||||
'PJ_TYPE': ['T1', 'T2', 'T1', 'T9', 'T3'],
|
||||
})
|
||||
|
||||
@patch('mes_dashboard.services.wip_service.get_cached_sys_date', return_value='2026-02-10 10:00:00')
|
||||
@patch('mes_dashboard.services.wip_service.get_cached_wip_data')
|
||||
def test_get_hold_detail_summary_supports_optional_reason_and_hold_type(
|
||||
self,
|
||||
mock_cached_wip,
|
||||
_mock_sys_date,
|
||||
):
|
||||
mock_cached_wip.return_value = self._sample_hold_df()
|
||||
|
||||
reason_summary = get_hold_detail_summary(reason='品質確認')
|
||||
self.assertEqual(reason_summary['totalLots'], 2)
|
||||
self.assertEqual(reason_summary['totalQty'], 180)
|
||||
self.assertEqual(reason_summary['workcenterCount'], 1)
|
||||
self.assertEqual(reason_summary['dataUpdateDate'], '2026-02-10 10:00:00')
|
||||
|
||||
quality_summary = get_hold_detail_summary(hold_type='quality')
|
||||
self.assertEqual(quality_summary['totalLots'], 3)
|
||||
self.assertEqual(quality_summary['totalQty'], 200)
|
||||
self.assertEqual(quality_summary['workcenterCount'], 2)
|
||||
|
||||
all_hold_summary = get_hold_detail_summary()
|
||||
self.assertEqual(all_hold_summary['totalLots'], 4)
|
||||
self.assertEqual(all_hold_summary['totalQty'], 250)
|
||||
self.assertEqual(all_hold_summary['workcenterCount'], 3)
|
||||
|
||||
@patch('mes_dashboard.services.wip_service.get_cached_wip_data')
|
||||
def test_get_hold_detail_lots_returns_hold_reason_and_treemap_filter(self, mock_cached_wip):
|
||||
mock_cached_wip.return_value = self._sample_hold_df()
|
||||
|
||||
reason_result = get_hold_detail_lots(reason='品質確認', page=1, page_size=10)
|
||||
self.assertEqual(len(reason_result['lots']), 2)
|
||||
self.assertEqual(reason_result['lots'][0]['lotId'], 'L3')
|
||||
self.assertEqual(reason_result['lots'][0]['holdReason'], '品質確認')
|
||||
|
||||
treemap_result = get_hold_detail_lots(
|
||||
reason=None,
|
||||
hold_type=None,
|
||||
treemap_reason='特殊需求管控',
|
||||
page=1,
|
||||
page_size=10,
|
||||
)
|
||||
self.assertEqual(len(treemap_result['lots']), 1)
|
||||
self.assertEqual(treemap_result['lots'][0]['lotId'], 'L2')
|
||||
self.assertEqual(treemap_result['lots'][0]['holdReason'], '特殊需求管控')
|
||||
|
||||
@patch('mes_dashboard.services.wip_service.get_cached_wip_data')
|
||||
def test_get_wip_matrix_reason_filter_keeps_backward_compatibility(self, mock_cached_wip):
|
||||
mock_cached_wip.return_value = self._sample_hold_df()
|
||||
|
||||
hold_quality_all = get_wip_matrix(status='HOLD', hold_type='quality')
|
||||
self.assertEqual(hold_quality_all['grand_total'], 200)
|
||||
|
||||
hold_quality_reason = get_wip_matrix(
|
||||
status='HOLD',
|
||||
hold_type='quality',
|
||||
reason='品質確認',
|
||||
)
|
||||
self.assertEqual(hold_quality_reason['grand_total'], 180)
|
||||
self.assertEqual(hold_quality_reason['workcenters'], ['WC-A'])
|
||||
|
||||
@patch('mes_dashboard.services.wip_service.get_cached_wip_data')
|
||||
def test_get_hold_overview_treemap_groups_by_workcenter_and_reason(self, mock_cached_wip):
|
||||
mock_cached_wip.return_value = self._sample_hold_df()
|
||||
|
||||
result = get_hold_overview_treemap(hold_type='quality')
|
||||
self.assertIsNotNone(result)
|
||||
items = result['items']
|
||||
self.assertEqual(len(items), 2)
|
||||
expected = {(item['workcenter'], item['reason']): item for item in items}
|
||||
self.assertEqual(expected[('WC-A', '品質確認')]['lots'], 2)
|
||||
self.assertEqual(expected[('WC-A', '品質確認')]['qty'], 180)
|
||||
self.assertAlmostEqual(expected[('WC-A', '品質確認')]['avgAge'], 3.5)
|
||||
self.assertEqual(expected[('WC-C', '設備異常')]['lots'], 1)
|
||||
|
||||
|
||||
class TestHoldOverviewServiceOracleFallback(unittest.TestCase):
|
||||
"""Test reason filtering behavior on Oracle fallback path."""
|
||||
|
||||
@disable_cache
|
||||
@patch('mes_dashboard.services.wip_service.read_sql_df')
|
||||
def test_get_wip_matrix_oracle_applies_reason_for_hold_status(self, mock_read_sql):
|
||||
mock_read_sql.return_value = pd.DataFrame()
|
||||
|
||||
get_wip_matrix(status='HOLD', reason='品質確認')
|
||||
|
||||
call_args = mock_read_sql.call_args
|
||||
sql = call_args[0][0]
|
||||
params = call_args[0][1] if len(call_args[0]) > 1 else {}
|
||||
self.assertIn('HOLDREASONNAME', sql)
|
||||
self.assertTrue(any(v == '品質確認' for v in params.values()))
|
||||
|
||||
@disable_cache
|
||||
@patch('mes_dashboard.services.wip_service.read_sql_df')
|
||||
def test_get_wip_matrix_oracle_ignores_reason_for_non_hold_status(self, mock_read_sql):
|
||||
mock_read_sql.return_value = pd.DataFrame()
|
||||
|
||||
get_wip_matrix(status='RUN', reason='品質確認')
|
||||
|
||||
call_args = mock_read_sql.call_args
|
||||
sql = call_args[0][0]
|
||||
self.assertNotIn('HOLDREASONNAME', sql)
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestWipServiceIntegration:
|
||||
|
||||
Reference in New Issue
Block a user