From bfec6b2293af87c14f78b24a61e0a28137f0b717 Mon Sep 17 00:00:00 2001 From: egg Date: Sun, 22 Feb 2026 18:17:44 +0800 Subject: [PATCH] feat(query-tool): align lot reject detail with reject-history layout --- .../src/query-tool/components/LotDetail.vue | 51 +++--- .../query-tool/components/LotRejectTable.vue | 169 ++++++++++++++++++ src/mes_dashboard/services/event_fetcher.py | 7 +- .../sql/query_tool/lot_rejects.sql | 96 +++++++--- tests/test_event_fetcher.py | 24 +++ 5 files changed, 297 insertions(+), 50 deletions(-) create mode 100644 frontend/src/query-tool/components/LotRejectTable.vue diff --git a/frontend/src/query-tool/components/LotDetail.vue b/frontend/src/query-tool/components/LotDetail.vue index e651c2c..7ce276f 100644 --- a/frontend/src/query-tool/components/LotDetail.vue +++ b/frontend/src/query-tool/components/LotDetail.vue @@ -4,6 +4,7 @@ import { computed } from 'vue'; import ExportButton from './ExportButton.vue'; import LotAssociationTable from './LotAssociationTable.vue'; import LotHistoryTable from './LotHistoryTable.vue'; +import LotRejectTable from './LotRejectTable.vue'; import LotTimeline from './LotTimeline.vue'; const props = defineProps({ @@ -105,36 +106,35 @@ const activeEmptyText = computed(() => { }); const activeColumnLabels = computed(() => { - if (props.activeSubTab !== 'materials') { - return {}; + if (props.activeSubTab === 'materials') { + return { + CONTAINERNAME: 'LOT ID', + }; } - return { - CONTAINERNAME: 'LOT ID', - }; + return {}; }); const activeHiddenColumns = computed(() => { - if (props.activeSubTab !== 'materials') { - return []; + if (props.activeSubTab === 'materials') { + return ['CONTAINERID', 'WORKCENTER_GROUP']; } - return ['CONTAINERID']; + return []; }); const activeColumnOrder = computed(() => { - if (props.activeSubTab !== 'materials') { - return []; + if (props.activeSubTab === 'materials') { + return [ + 'CONTAINERNAME', + 'MATERIALPARTNAME', + 'MATERIALLOTNAME', + 'QTYCONSUMED', + 'WORKCENTERNAME', + 'SPECNAME', + 'EQUIPMENTNAME', + 'TXNDATE', + ]; } - return [ - 'CONTAINERNAME', - 'MATERIALPARTNAME', - 'MATERIALLOTNAME', - 'QTYCONSUMED', - 'WORKCENTERNAME', - 'SPECNAME', - 'EQUIPMENTNAME', - 'TXNDATE', - 'WORKCENTER_GROUP', - ]; + return []; }); const canExport = computed(() => { @@ -223,7 +223,7 @@ const detailCountLabel = computed(() => { { :hidden-columns="activeHiddenColumns" :column-order="activeColumnOrder" /> + + diff --git a/frontend/src/query-tool/components/LotRejectTable.vue b/frontend/src/query-tool/components/LotRejectTable.vue new file mode 100644 index 0000000..ce07dbb --- /dev/null +++ b/frontend/src/query-tool/components/LotRejectTable.vue @@ -0,0 +1,169 @@ + + + diff --git a/src/mes_dashboard/services/event_fetcher.py b/src/mes_dashboard/services/event_fetcher.py index fb362c8..ac1501f 100644 --- a/src/mes_dashboard/services/event_fetcher.py +++ b/src/mes_dashboard/services/event_fetcher.py @@ -31,6 +31,7 @@ _DOMAIN_SPECS: Dict[str, Dict[str, Any]] = { "materials": { "filter_column": "m.CONTAINERID", "cache_ttl": 300, + "schema_version": 2, "bucket": "event-materials", "max_env": "EVT_MATERIALS_RATE_MAX_REQUESTS", "window_env": "EVT_MATERIALS_RATE_WINDOW_SECONDS", @@ -38,8 +39,9 @@ _DOMAIN_SPECS: Dict[str, Dict[str, Any]] = { "default_window": 60, }, "rejects": { - "filter_column": "CONTAINERID", + "filter_column": "r.CONTAINERID", "cache_ttl": 300, + "schema_version": 2, "bucket": "event-rejects", "max_env": "EVT_REJECTS_RATE_MAX_REQUESTS", "window_env": "EVT_REJECTS_RATE_WINDOW_SECONDS", @@ -116,7 +118,8 @@ class EventFetcher: def _cache_key(domain: str, container_ids: List[str]) -> str: normalized = sorted(_normalize_ids(container_ids)) digest = hashlib.md5("|".join(normalized).encode("utf-8")).hexdigest()[:12] - return f"evt:{domain}:{digest}" + schema_version = int(_DOMAIN_SPECS.get(domain, {}).get("schema_version", 1)) + return f"evt:{domain}:v{schema_version}:{digest}" @staticmethod def _replace_container_filter(sql: str, condition_sql: str) -> str: diff --git a/src/mes_dashboard/sql/query_tool/lot_rejects.sql b/src/mes_dashboard/sql/query_tool/lot_rejects.sql index f3e40a8..1dc0299 100644 --- a/src/mes_dashboard/sql/query_tool/lot_rejects.sql +++ b/src/mes_dashboard/sql/query_tool/lot_rejects.sql @@ -1,26 +1,70 @@ --- LOT Reject Records Query --- Retrieves reject (defect) records for a LOT --- --- Parameters: --- container_id - CONTAINERID to query (16-char hex) --- --- Note: Uses LOSSREASONNAME (NOT REJECTREASONNAME) --- Uses TXNDATE (NOT TXNDATETIME) --- Only has EQUIPMENTNAME, NO EQUIPMENTID field --- DEFECTQTY = SUM of REJECTQTY + STANDBYQTY + QTYTOPROCESS + INPROCESSQTY + PROCESSEDQTY - -SELECT - CONTAINERID, - LOSSREASONNAME, - REJECTQTY, - NVL(REJECTQTY, 0) + NVL(STANDBYQTY, 0) + NVL(QTYTOPROCESS, 0) - + NVL(INPROCESSQTY, 0) + NVL(PROCESSEDQTY, 0) AS DEFECTQTY, - WORKCENTERNAME, - EQUIPMENTNAME, - TXNDATE, - COMMENTS, - REJECTCAUSE, - REJECTCOMMENT -FROM DWH.DW_MES_LOTREJECTHISTORY -WHERE CONTAINERID = :container_id -ORDER BY TXNDATE +-- LOT Reject Records Query +-- Retrieves reject (defect) records for a LOT +-- +-- Parameters: +-- container_id - CONTAINERID to query (16-char hex) +-- +-- Note: Aligns output semantics and ordering with Reject History detail: +-- - REJECT_TOTAL_QTY = REJECT + STANDBY + QTYTOPROCESS + INPROCESS + PROCESSED +-- - DEFECT_QTY uses DEFECTQTY (non-charge-off scrap) +-- - Order: TXN_DAY desc, WORKCENTER sequence asc, WORKCENTER asc, REJECT_TOTAL_QTY desc, LOT asc + +WITH spec_map AS ( + SELECT + SPEC, + MIN(WORK_CENTER) KEEP ( + DENSE_RANK FIRST ORDER BY WORKCENTERSEQUENCE_GROUP + ) AS WORK_CENTER, + MIN(WORK_CENTER_GROUP) KEEP ( + DENSE_RANK FIRST ORDER BY WORKCENTERSEQUENCE_GROUP + ) AS WORKCENTER_GROUP, + MIN(WORKCENTERSEQUENCE_GROUP) AS WORKCENTERSEQUENCE_GROUP + FROM DWH.DW_MES_SPEC_WORKCENTER_V + WHERE SPEC IS NOT NULL + GROUP BY SPEC +) +SELECT + r.CONTAINERID, + NVL(TRIM(c.CONTAINERNAME), TRIM(r.CONTAINERID)) AS CONTAINERNAME, + NVL(TRIM(sm.WORK_CENTER), NVL(TRIM(r.WORKCENTERNAME), '(NA)')) AS WORKCENTERNAME, + NVL(TRIM(sm.WORKCENTER_GROUP), NVL(TRIM(r.WORKCENTERNAME), '(NA)')) AS WORKCENTER_GROUP, + NVL(sm.WORKCENTERSEQUENCE_GROUP, 999) AS WORKCENTERSEQUENCE_GROUP, + NVL(TRIM(c.PRODUCTLINENAME), '(NA)') AS PRODUCTLINENAME, + NVL(TRIM(c.PJ_FUNCTION), '(NA)') AS PJ_FUNCTION, + NVL(TRIM(c.PJ_TYPE), '(NA)') AS PJ_TYPE, + NVL(TRIM(c.PRODUCTNAME), '(NA)') AS PRODUCTNAME, + NVL(TRIM(r.SPECNAME), '(NA)') AS SPECNAME, + NVL(TRIM(r.LOSSREASONNAME), '(未填寫)') AS LOSSREASONNAME, + NVL(TRIM(r.EQUIPMENTNAME), '(NA)') AS EQUIPMENTNAME, + TRIM(r.REJECTCOMMENT) AS REJECTCOMMENT, + NVL(r.REJECTQTY, 0) AS REJECT_QTY, + NVL(r.STANDBYQTY, 0) AS STANDBY_QTY, + NVL(r.QTYTOPROCESS, 0) AS QTYTOPROCESS_QTY, + NVL(r.INPROCESSQTY, 0) AS INPROCESS_QTY, + NVL(r.PROCESSEDQTY, 0) AS PROCESSED_QTY, + NVL(r.REJECTQTY, 0) + + NVL(r.STANDBYQTY, 0) + + NVL(r.QTYTOPROCESS, 0) + + NVL(r.INPROCESSQTY, 0) + + NVL(r.PROCESSEDQTY, 0) AS REJECT_TOTAL_QTY, + NVL(r.DEFECTQTY, 0) AS DEFECT_QTY, + r.TXNDATE AS TXN_TIME, + r.TXNDATE, + TRUNC(r.TXNDATE) AS TXN_DAY +FROM DWH.DW_MES_LOTREJECTHISTORY r +LEFT JOIN DWH.DW_MES_CONTAINER c + ON c.CONTAINERID = r.CONTAINERID +LEFT JOIN spec_map sm + ON sm.SPEC = TRIM(r.SPECNAME) +WHERE r.CONTAINERID = :container_id +ORDER BY + TRUNC(r.TXNDATE) DESC, + NVL(sm.WORKCENTERSEQUENCE_GROUP, 999) ASC, + NVL(TRIM(sm.WORK_CENTER), NVL(TRIM(r.WORKCENTERNAME), '(NA)')) ASC, + NVL(r.REJECTQTY, 0) + + NVL(r.STANDBYQTY, 0) + + NVL(r.QTYTOPROCESS, 0) + + NVL(r.INPROCESSQTY, 0) + + NVL(r.PROCESSEDQTY, 0) DESC, + NVL(TRIM(c.CONTAINERNAME), TRIM(r.CONTAINERID)) ASC, + r.TXNDATE DESC diff --git a/tests/test_event_fetcher.py b/tests/test_event_fetcher.py index 9edaa83..05c4f36 100644 --- a/tests/test_event_fetcher.py +++ b/tests/test_event_fetcher.py @@ -112,3 +112,27 @@ def test_fetch_events_materials_branch_replaces_aliased_container_filter( assert "m.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_rejects_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 r LEFT JOIN c ON c.CONTAINERID = r.CONTAINERID " + "WHERE r.CONTAINERID = :container_id ORDER BY r.TXNDATE" + ) + mock_read_sql_df.return_value = pd.DataFrame([]) + + EventFetcher.fetch_events(["CID-1", "CID-2"], "rejects") + + sql, params = mock_read_sql_df.call_args.args + assert "r.CONTAINERID = :container_id" not in sql + assert "IN" in sql.upper() + assert params == {"p0": "CID-1", "p1": "CID-2"}