fix(query-tool): export all selected CIDs instead of single, fix hold detail float precision
Export was sending only one container_id while the detail table loaded data for all selected CIDs (including subtree), causing "查無資料" errors. Now sends container_ids array and uses batch service functions. Also rounds hold detail age KPI cards to 1 decimal place. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,8 @@ function formatAge(value) {
|
||||
if (value === null || value === undefined || value === '-') {
|
||||
return '-';
|
||||
}
|
||||
return `${value}天`;
|
||||
const num = Number(value);
|
||||
return `${Number.isFinite(num) ? num.toFixed(1) : value}天`;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -378,9 +378,9 @@ export function useLotDetail(initial = {}) {
|
||||
async function exportSubTab(tab) {
|
||||
const normalized = normalizeSubTab(tab);
|
||||
const exportType = EXPORT_TYPE_MAP[normalized];
|
||||
const containerId = selectedContainerId.value;
|
||||
const cids = getActiveCids();
|
||||
|
||||
if (!exportType || !containerId) {
|
||||
if (!exportType || cids.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -389,7 +389,7 @@ export function useLotDetail(initial = {}) {
|
||||
|
||||
try {
|
||||
const params = {
|
||||
container_id: containerId,
|
||||
container_ids: cids,
|
||||
};
|
||||
|
||||
if (normalized === 'jobs') {
|
||||
|
||||
@@ -40,6 +40,15 @@ from mes_dashboard.services.query_tool_service import (
|
||||
validate_equipment_input,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_export_cids(params: dict) -> list[str]:
|
||||
"""Extract container_ids from export params, supporting both batch and single."""
|
||||
cids = params.get('container_ids') or []
|
||||
if isinstance(cids, list) and cids:
|
||||
return [c for c in cids if c]
|
||||
single = params.get('container_id')
|
||||
return [single] if single else []
|
||||
|
||||
# Create Blueprint
|
||||
query_tool_bp = Blueprint('query_tool', __name__)
|
||||
|
||||
@@ -605,11 +614,14 @@ def export_csv():
|
||||
|
||||
try:
|
||||
if export_type == 'lot_history':
|
||||
container_id = params.get('container_id')
|
||||
if not container_id:
|
||||
cids = _resolve_export_cids(params)
|
||||
if not cids:
|
||||
return jsonify({'error': '請指定 CONTAINERID'}), 400
|
||||
result = get_lot_history(container_id)
|
||||
filename = f'lot_history_{container_id}.csv'
|
||||
if len(cids) > 1:
|
||||
result = get_lot_history_batch(cids)
|
||||
else:
|
||||
result = get_lot_history(cids[0])
|
||||
filename = f'lot_history_{cids[0]}.csv'
|
||||
|
||||
elif export_type == 'adjacent_lots':
|
||||
result = get_adjacent_lots(
|
||||
@@ -620,19 +632,34 @@ def export_csv():
|
||||
filename = 'adjacent_lots.csv'
|
||||
|
||||
elif export_type == 'lot_materials':
|
||||
container_id = params.get('container_id')
|
||||
result = get_lot_materials(container_id)
|
||||
filename = f'lot_raw_materials_{container_id}.csv'
|
||||
cids = _resolve_export_cids(params)
|
||||
if not cids:
|
||||
return jsonify({'error': '請指定 CONTAINERID'}), 400
|
||||
if len(cids) > 1:
|
||||
result = get_lot_associations_batch(cids, 'materials')
|
||||
else:
|
||||
result = get_lot_materials(cids[0])
|
||||
filename = f'lot_raw_materials_{cids[0]}.csv'
|
||||
|
||||
elif export_type == 'lot_rejects':
|
||||
container_id = params.get('container_id')
|
||||
result = get_lot_rejects(container_id)
|
||||
filename = f'lot_rejects_{container_id}.csv'
|
||||
cids = _resolve_export_cids(params)
|
||||
if not cids:
|
||||
return jsonify({'error': '請指定 CONTAINERID'}), 400
|
||||
if len(cids) > 1:
|
||||
result = get_lot_associations_batch(cids, 'rejects')
|
||||
else:
|
||||
result = get_lot_rejects(cids[0])
|
||||
filename = f'lot_rejects_{cids[0]}.csv'
|
||||
|
||||
elif export_type == 'lot_holds':
|
||||
container_id = params.get('container_id')
|
||||
result = get_lot_holds(container_id)
|
||||
filename = f'lot_holds_{container_id}.csv'
|
||||
cids = _resolve_export_cids(params)
|
||||
if not cids:
|
||||
return jsonify({'error': '請指定 CONTAINERID'}), 400
|
||||
if len(cids) > 1:
|
||||
result = get_lot_associations_batch(cids, 'holds')
|
||||
else:
|
||||
result = get_lot_holds(cids[0])
|
||||
filename = f'lot_holds_{cids[0]}.csv'
|
||||
|
||||
elif export_type == 'lot_splits':
|
||||
container_id = params.get('container_id')
|
||||
|
||||
@@ -885,6 +885,149 @@ class TestExportCsvEndpoint:
|
||||
assert 'GA25010001-A01' in decoded
|
||||
|
||||
|
||||
class TestExportCsvBatchEndpoint:
|
||||
"""Tests for /api/query-tool/export-csv with batch container_ids."""
|
||||
|
||||
@patch('mes_dashboard.routes.query_tool_routes.get_lot_history_batch')
|
||||
def test_export_lot_history_batch(self, mock_batch, client):
|
||||
"""Batch container_ids should call get_lot_history_batch."""
|
||||
mock_batch.return_value = {
|
||||
'data': [
|
||||
{'CONTAINERNAME': 'LOT-A', 'SPECNAME': 'S1', 'TRACKINTIMESTAMP': '2026-01-01'},
|
||||
{'CONTAINERNAME': 'LOT-B', 'SPECNAME': 'S1', 'TRACKINTIMESTAMP': '2026-01-02'},
|
||||
],
|
||||
'total': 2,
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
'/api/query-tool/export-csv',
|
||||
json={
|
||||
'export_type': 'lot_history',
|
||||
'params': {'container_ids': ['aaa1', 'bbb2']},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert 'text/csv' in response.content_type
|
||||
mock_batch.assert_called_once_with(['aaa1', 'bbb2'])
|
||||
|
||||
@patch('mes_dashboard.routes.query_tool_routes.get_lot_history')
|
||||
def test_export_lot_history_single_cid_in_array(self, mock_single, client):
|
||||
"""Single-element container_ids should call get_lot_history (not batch)."""
|
||||
mock_single.return_value = {
|
||||
'data': [{'CONTAINERNAME': 'LOT-A', 'SPECNAME': 'S1'}],
|
||||
'total': 1,
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
'/api/query-tool/export-csv',
|
||||
json={
|
||||
'export_type': 'lot_history',
|
||||
'params': {'container_ids': ['aaa1']},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_single.assert_called_once_with('aaa1')
|
||||
|
||||
@patch('mes_dashboard.routes.query_tool_routes.get_lot_associations_batch')
|
||||
def test_export_lot_materials_batch(self, mock_batch, client):
|
||||
"""Batch container_ids for materials should call get_lot_associations_batch."""
|
||||
mock_batch.return_value = {
|
||||
'data': [
|
||||
{'CONTAINERNAME': 'LOT-A', 'MATERIALPARTNAME': 'M1', 'QTYCONSUMED': 5},
|
||||
],
|
||||
'total': 1,
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
'/api/query-tool/export-csv',
|
||||
json={
|
||||
'export_type': 'lot_materials',
|
||||
'params': {'container_ids': ['aaa1', 'bbb2']},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert 'text/csv' in response.content_type
|
||||
mock_batch.assert_called_once_with(['aaa1', 'bbb2'], 'materials')
|
||||
|
||||
@patch('mes_dashboard.routes.query_tool_routes.get_lot_associations_batch')
|
||||
def test_export_lot_rejects_batch(self, mock_batch, client):
|
||||
"""Batch container_ids for rejects should call get_lot_associations_batch."""
|
||||
mock_batch.return_value = {
|
||||
'data': [
|
||||
{'CONTAINERNAME': 'LOT-A', 'LOSSREASONNAME': 'IR', 'REJECT_TOTAL_QTY': 10},
|
||||
],
|
||||
'total': 1,
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
'/api/query-tool/export-csv',
|
||||
json={
|
||||
'export_type': 'lot_rejects',
|
||||
'params': {'container_ids': ['aaa1', 'bbb2']},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_batch.assert_called_once_with(['aaa1', 'bbb2'], 'rejects')
|
||||
|
||||
@patch('mes_dashboard.routes.query_tool_routes.get_lot_associations_batch')
|
||||
def test_export_lot_holds_batch(self, mock_batch, client):
|
||||
"""Batch container_ids for holds should call get_lot_associations_batch."""
|
||||
mock_batch.return_value = {
|
||||
'data': [
|
||||
{'CONTAINERNAME': 'LOT-A', 'HOLDREASONNAME': 'Q-Time', 'HOLD_STATUS': 'HOLD'},
|
||||
],
|
||||
'total': 1,
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
'/api/query-tool/export-csv',
|
||||
json={
|
||||
'export_type': 'lot_holds',
|
||||
'params': {'container_ids': ['aaa1', 'bbb2']},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_batch.assert_called_once_with(['aaa1', 'bbb2'], 'holds')
|
||||
|
||||
@patch('mes_dashboard.routes.query_tool_routes.get_lot_history')
|
||||
def test_export_backward_compat_container_id(self, mock_single, client):
|
||||
"""Legacy container_id param should still work."""
|
||||
mock_single.return_value = {
|
||||
'data': [{'CONTAINERNAME': 'LOT-A', 'SPECNAME': 'S1'}],
|
||||
'total': 1,
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
'/api/query-tool/export-csv',
|
||||
json={
|
||||
'export_type': 'lot_history',
|
||||
'params': {'container_id': '488103800029578b'},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_single.assert_called_once_with('488103800029578b')
|
||||
|
||||
def test_export_lot_history_no_cids_returns_400(self, client):
|
||||
"""Missing container_ids and container_id should return 400."""
|
||||
response = client.post(
|
||||
'/api/query-tool/export-csv',
|
||||
json={
|
||||
'export_type': 'lot_history',
|
||||
'params': {},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
data = response.get_json()
|
||||
assert 'CONTAINERID' in data.get('error', '')
|
||||
|
||||
|
||||
class TestEquipmentListEndpoint:
|
||||
"""Tests for /api/query-tool/equipment-list endpoint."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user