fix(query-tool): show hold lot id via containername and align export
This commit is contained in:
@@ -111,6 +111,11 @@ const activeColumnLabels = computed(() => {
|
||||
CONTAINERNAME: 'LOT ID',
|
||||
};
|
||||
}
|
||||
if (props.activeSubTab === 'holds') {
|
||||
return {
|
||||
CONTAINERNAME: 'LOT ID',
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
@@ -118,6 +123,9 @@ const activeHiddenColumns = computed(() => {
|
||||
if (props.activeSubTab === 'materials') {
|
||||
return ['CONTAINERID', 'WORKCENTER_GROUP'];
|
||||
}
|
||||
if (props.activeSubTab === 'holds') {
|
||||
return ['CONTAINERID'];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
@@ -134,6 +142,23 @@ const activeColumnOrder = computed(() => {
|
||||
'TXNDATE',
|
||||
];
|
||||
}
|
||||
if (props.activeSubTab === 'holds') {
|
||||
return [
|
||||
'CONTAINERNAME',
|
||||
'WORKCENTERNAME',
|
||||
'HOLDTXNDATE',
|
||||
'RELEASETXNDATE',
|
||||
'HOLD_STATUS',
|
||||
'HOLD_HOURS',
|
||||
'HOLDREASONNAME',
|
||||
'HOLDCOMMENTS',
|
||||
'HOLDEMP',
|
||||
'HOLDEMPDEPTNAME',
|
||||
'RELEASEEMP',
|
||||
'RELEASECOMMENTS',
|
||||
'NCRID',
|
||||
];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
|
||||
@@ -102,6 +102,29 @@ def _format_lot_materials_export_rows(rows):
|
||||
'TXNDATE': row.get('TXNDATE', ''),
|
||||
})
|
||||
return normalized_rows
|
||||
|
||||
|
||||
def _format_lot_holds_export_rows(rows):
|
||||
"""Normalize LOT hold export columns for UI/CSV consistency."""
|
||||
normalized_rows = []
|
||||
for row in rows or []:
|
||||
lot_id = row.get('CONTAINERNAME') or row.get('CONTAINERID') or ''
|
||||
normalized_rows.append({
|
||||
'LOT ID': lot_id,
|
||||
'WORKCENTERNAME': row.get('WORKCENTERNAME', ''),
|
||||
'HOLDTXNDATE': row.get('HOLDTXNDATE', ''),
|
||||
'RELEASETXNDATE': row.get('RELEASETXNDATE', ''),
|
||||
'HOLD_STATUS': row.get('HOLD_STATUS', ''),
|
||||
'HOLD_HOURS': row.get('HOLD_HOURS', ''),
|
||||
'HOLDREASONNAME': row.get('HOLDREASONNAME', ''),
|
||||
'HOLDCOMMENTS': row.get('HOLDCOMMENTS', ''),
|
||||
'HOLDEMP': row.get('HOLDEMP', ''),
|
||||
'HOLDEMPDEPTNAME': row.get('HOLDEMPDEPTNAME', ''),
|
||||
'RELEASEEMP': row.get('RELEASEEMP', ''),
|
||||
'RELEASECOMMENTS': row.get('RELEASECOMMENTS', ''),
|
||||
'NCRID': row.get('NCRID', ''),
|
||||
})
|
||||
return normalized_rows
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -613,6 +636,8 @@ def export_csv():
|
||||
|
||||
if export_type == 'lot_materials':
|
||||
export_data = _format_lot_materials_export_rows(export_data)
|
||||
elif export_type == 'lot_holds':
|
||||
export_data = _format_lot_holds_export_rows(export_data)
|
||||
|
||||
# Stream CSV response
|
||||
return Response(
|
||||
|
||||
@@ -49,8 +49,9 @@ _DOMAIN_SPECS: Dict[str, Dict[str, Any]] = {
|
||||
"default_window": 60,
|
||||
},
|
||||
"holds": {
|
||||
"filter_column": "CONTAINERID",
|
||||
"filter_column": "h.CONTAINERID",
|
||||
"cache_ttl": 180,
|
||||
"schema_version": 2,
|
||||
"bucket": "event-holds",
|
||||
"max_env": "EVT_HOLDS_RATE_MAX_REQUESTS",
|
||||
"window_env": "EVT_HOLDS_RATE_WINDOW_SECONDS",
|
||||
|
||||
@@ -7,28 +7,31 @@
|
||||
-- Note: Uses HOLDTXNDATE/RELEASETXNDATE (NOT TXNDATETIME)
|
||||
-- NULL RELEASETXNDATE means currently HOLD
|
||||
|
||||
SELECT
|
||||
CONTAINERID,
|
||||
WORKCENTERNAME,
|
||||
HOLDTXNDATE,
|
||||
HOLDEMP,
|
||||
HOLDEMPDEPTNAME,
|
||||
HOLDREASONNAME,
|
||||
HOLDCOMMENTS,
|
||||
RELEASETXNDATE,
|
||||
RELEASEEMP,
|
||||
RELEASECOMMENTS,
|
||||
NCRID,
|
||||
CASE
|
||||
WHEN RELEASETXNDATE IS NULL THEN 'HOLD'
|
||||
ELSE 'RELEASED'
|
||||
END AS HOLD_STATUS,
|
||||
CASE
|
||||
WHEN RELEASETXNDATE IS NULL THEN
|
||||
ROUND((SYSDATE - HOLDTXNDATE) * 24, 2)
|
||||
ELSE
|
||||
ROUND((RELEASETXNDATE - HOLDTXNDATE) * 24, 2)
|
||||
END AS HOLD_HOURS
|
||||
FROM DWH.DW_MES_HOLDRELEASEHISTORY
|
||||
WHERE CONTAINERID = :container_id
|
||||
ORDER BY HOLDTXNDATE DESC
|
||||
SELECT
|
||||
h.CONTAINERID,
|
||||
NVL(TRIM(c.CONTAINERNAME), TRIM(h.CONTAINERID)) AS CONTAINERNAME,
|
||||
h.WORKCENTERNAME,
|
||||
h.HOLDTXNDATE,
|
||||
h.HOLDEMP,
|
||||
h.HOLDEMPDEPTNAME,
|
||||
h.HOLDREASONNAME,
|
||||
h.HOLDCOMMENTS,
|
||||
h.RELEASETXNDATE,
|
||||
h.RELEASEEMP,
|
||||
h.RELEASECOMMENTS,
|
||||
h.NCRID,
|
||||
CASE
|
||||
WHEN h.RELEASETXNDATE IS NULL THEN 'HOLD'
|
||||
ELSE 'RELEASED'
|
||||
END AS HOLD_STATUS,
|
||||
CASE
|
||||
WHEN h.RELEASETXNDATE IS NULL THEN
|
||||
ROUND((SYSDATE - h.HOLDTXNDATE) * 24, 2)
|
||||
ELSE
|
||||
ROUND((h.RELEASETXNDATE - h.HOLDTXNDATE) * 24, 2)
|
||||
END AS HOLD_HOURS
|
||||
FROM DWH.DW_MES_HOLDRELEASEHISTORY h
|
||||
LEFT JOIN DWH.DW_MES_CONTAINER c
|
||||
ON c.CONTAINERID = h.CONTAINERID
|
||||
WHERE h.CONTAINERID = :container_id
|
||||
ORDER BY h.HOLDTXNDATE DESC
|
||||
|
||||
@@ -136,3 +136,27 @@ def test_fetch_events_rejects_branch_replaces_aliased_container_filter(
|
||||
assert "r.CONTAINERID = :container_id" not in sql
|
||||
assert "IN" in sql.upper()
|
||||
assert params == {"p0": "CID-1", "p1": "CID-2"}
|
||||
|
||||
|
||||
@patch("mes_dashboard.services.event_fetcher.cache_set")
|
||||
@patch("mes_dashboard.services.event_fetcher.cache_get", return_value=None)
|
||||
@patch("mes_dashboard.services.event_fetcher.read_sql_df")
|
||||
@patch("mes_dashboard.services.event_fetcher.SQLLoader.load")
|
||||
def test_fetch_events_holds_branch_replaces_aliased_container_filter(
|
||||
mock_sql_load,
|
||||
mock_read_sql_df,
|
||||
_mock_cache_get,
|
||||
_mock_cache_set,
|
||||
):
|
||||
mock_sql_load.return_value = (
|
||||
"SELECT * FROM t h LEFT JOIN c ON c.CONTAINERID = h.CONTAINERID "
|
||||
"WHERE h.CONTAINERID = :container_id ORDER BY h.HOLDTXNDATE DESC"
|
||||
)
|
||||
mock_read_sql_df.return_value = pd.DataFrame([])
|
||||
|
||||
EventFetcher.fetch_events(["CID-1", "CID-2"], "holds")
|
||||
|
||||
sql, params = mock_read_sql_df.call_args.args
|
||||
assert "h.CONTAINERID = :container_id" not in sql
|
||||
assert "IN" in sql.upper()
|
||||
assert params == {"p0": "CID-1", "p1": "CID-2"}
|
||||
|
||||
@@ -749,6 +749,48 @@ class TestExportCsvEndpoint:
|
||||
decoded = response.data.decode('utf-8-sig')
|
||||
assert 'LOT ID' in decoded
|
||||
assert 'GA25010001-A01' in decoded
|
||||
|
||||
@patch('mes_dashboard.routes.query_tool_routes.get_lot_holds')
|
||||
def test_export_lot_holds_uses_container_name_as_lot_id(
|
||||
self,
|
||||
mock_get_holds,
|
||||
client,
|
||||
):
|
||||
mock_get_holds.return_value = {
|
||||
'data': [
|
||||
{
|
||||
'CONTAINERID': '488103800029578b',
|
||||
'CONTAINERNAME': 'GA25010001-A01',
|
||||
'WORKCENTERNAME': '成型',
|
||||
'HOLDTXNDATE': '2026-02-22 16:53:27',
|
||||
'RELEASETXNDATE': None,
|
||||
'HOLD_STATUS': 'HOLD',
|
||||
'HOLD_HOURS': 1.46,
|
||||
'HOLDREASONNAME': 'Q-Time Fail',
|
||||
'HOLDCOMMENTS': '',
|
||||
'HOLDEMP': 'U001',
|
||||
'HOLDEMPDEPTNAME': '成型(D)',
|
||||
'RELEASEEMP': '',
|
||||
'RELEASECOMMENTS': '',
|
||||
'NCRID': '',
|
||||
}
|
||||
],
|
||||
'total': 1,
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
'/api/query-tool/export-csv',
|
||||
json={
|
||||
'export_type': 'lot_holds',
|
||||
'params': {'container_id': '488103800029578b'}
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert 'lot_holds_488103800029578b.csv' in response.headers.get('Content-Disposition', '')
|
||||
decoded = response.data.decode('utf-8-sig')
|
||||
assert 'LOT ID' in decoded
|
||||
assert 'GA25010001-A01' in decoded
|
||||
|
||||
|
||||
class TestEquipmentListEndpoint:
|
||||
|
||||
Reference in New Issue
Block a user