feat(reject-history): add detail columns, CSV export polish, and metric filter sync
Add EQUIPMENTNAME and REJECTCOMMENT columns to the detail table, list SQL, and per-LOT base query. Rewrite CSV export to use per-LOT rows with Chinese headers, BOM UTF-8 encoding, and fetch-based blob download with loading spinner. Sync trend chart legend filter (reject/defect) to detail table and export via metric_filter parameter through the full stack. Fix chart sizing with containLabel and autoresize throttle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,7 @@ const loading = reactive({
|
|||||||
querying: false,
|
querying: false,
|
||||||
options: false,
|
options: false,
|
||||||
list: false,
|
list: false,
|
||||||
|
exporting: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
@@ -267,12 +268,19 @@ function buildCommonParams({ reason = committedFilters.reason } = {}) {
|
|||||||
return buildRejectCommonQueryParams(committedFilters, { reason });
|
return buildRejectCommonQueryParams(committedFilters, { reason });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function metricFilterParam() {
|
||||||
|
const mode = paretoMetricMode.value;
|
||||||
|
if (mode === 'reject' || mode === 'defect') return mode;
|
||||||
|
return 'all';
|
||||||
|
}
|
||||||
|
|
||||||
function buildListParams() {
|
function buildListParams() {
|
||||||
const effectiveReason = detailReason.value || committedFilters.reason;
|
const effectiveReason = detailReason.value || committedFilters.reason;
|
||||||
const params = {
|
const params = {
|
||||||
...buildCommonParams({ reason: effectiveReason }),
|
...buildCommonParams({ reason: effectiveReason }),
|
||||||
page: page.value,
|
page: page.value,
|
||||||
per_page: DEFAULT_PER_PAGE,
|
per_page: DEFAULT_PER_PAGE,
|
||||||
|
metric_filter: metricFilterParam(),
|
||||||
};
|
};
|
||||||
if (selectedTrendDates.value.length > 0) {
|
if (selectedTrendDates.value.length > 0) {
|
||||||
const sorted = [...selectedTrendDates.value].sort();
|
const sorted = [...selectedTrendDates.value].sort();
|
||||||
@@ -561,7 +569,9 @@ function onTrendDateClick(dateStr) {
|
|||||||
|
|
||||||
function onTrendLegendChange(selected) {
|
function onTrendLegendChange(selected) {
|
||||||
trendLegendSelected.value = selected;
|
trendLegendSelected.value = selected;
|
||||||
|
page.value = 1;
|
||||||
updateUrlState();
|
updateUrlState();
|
||||||
|
void loadListOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onParetoClick(reason) {
|
function onParetoClick(reason) {
|
||||||
@@ -625,7 +635,9 @@ async function removeFilterChip(chip) {
|
|||||||
await loadDataSections();
|
await loadDataSections();
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportCsv() {
|
async function exportCsv() {
|
||||||
|
if (loading.exporting) return;
|
||||||
|
|
||||||
const effectiveReason = detailReason.value || committedFilters.reason;
|
const effectiveReason = detailReason.value || committedFilters.reason;
|
||||||
const queryParams = buildRejectCommonQueryParams(committedFilters, { reason: effectiveReason });
|
const queryParams = buildRejectCommonQueryParams(committedFilters, { reason: effectiveReason });
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
@@ -638,8 +650,33 @@ function exportCsv() {
|
|||||||
appendArrayParams(params, 'workcenter_groups', queryParams.workcenter_groups || []);
|
appendArrayParams(params, 'workcenter_groups', queryParams.workcenter_groups || []);
|
||||||
appendArrayParams(params, 'packages', queryParams.packages || []);
|
appendArrayParams(params, 'packages', queryParams.packages || []);
|
||||||
appendArrayParams(params, 'reasons', queryParams.reasons || []);
|
appendArrayParams(params, 'reasons', queryParams.reasons || []);
|
||||||
|
params.set('metric_filter', metricFilterParam());
|
||||||
|
|
||||||
window.location.href = `/api/reject-history/export?${params.toString()}`;
|
loading.exporting = true;
|
||||||
|
errorMessage.value = '';
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/reject-history/export?${params.toString()}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('匯出 CSV 失敗');
|
||||||
|
}
|
||||||
|
const blob = await response.blob();
|
||||||
|
const disposition = response.headers.get('Content-Disposition') || '';
|
||||||
|
const filenameMatch = disposition.match(/filename=(.+?)(?:;|$)/);
|
||||||
|
const filename = filenameMatch ? filenameMatch[1] : `reject_history_${queryParams.start_date}_to_${queryParams.end_date}.csv`;
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = error?.message || '匯出 CSV 失敗';
|
||||||
|
} finally {
|
||||||
|
loading.exporting = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalScrapQty = computed(() => {
|
const totalScrapQty = computed(() => {
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ function formatNumber(value) {
|
|||||||
<th class="col-left">TYPE</th>
|
<th class="col-left">TYPE</th>
|
||||||
<th>PRODUCT</th>
|
<th>PRODUCT</th>
|
||||||
<th>原因</th>
|
<th>原因</th>
|
||||||
|
<th>EQUIPMENT</th>
|
||||||
|
<th>COMMENT</th>
|
||||||
<th class="th-expandable" @click="showRejectBreakdown = !showRejectBreakdown">
|
<th class="th-expandable" @click="showRejectBreakdown = !showRejectBreakdown">
|
||||||
扣帳報廢量 <span class="expand-icon">{{ showRejectBreakdown ? '▾' : '▸' }}</span>
|
扣帳報廢量 <span class="expand-icon">{{ showRejectBreakdown ? '▾' : '▸' }}</span>
|
||||||
</th>
|
</th>
|
||||||
@@ -65,6 +67,8 @@ function formatNumber(value) {
|
|||||||
<td class="col-left">{{ row.PJ_TYPE }}</td>
|
<td class="col-left">{{ row.PJ_TYPE }}</td>
|
||||||
<td>{{ row.PRODUCTNAME || '' }}</td>
|
<td>{{ row.PRODUCTNAME || '' }}</td>
|
||||||
<td>{{ row.LOSSREASONNAME }}</td>
|
<td>{{ row.LOSSREASONNAME }}</td>
|
||||||
|
<td>{{ row.EQUIPMENTNAME || '' }}</td>
|
||||||
|
<td>{{ row.REJECTCOMMENT || '' }}</td>
|
||||||
<td>{{ formatNumber(row.REJECT_TOTAL_QTY) }}</td>
|
<td>{{ formatNumber(row.REJECT_TOTAL_QTY) }}</td>
|
||||||
<template v-if="showRejectBreakdown">
|
<template v-if="showRejectBreakdown">
|
||||||
<td class="td-sub">{{ formatNumber(row.REJECT_QTY) }}</td>
|
<td class="td-sub">{{ formatNumber(row.REJECT_QTY) }}</td>
|
||||||
@@ -77,7 +81,7 @@ function formatNumber(value) {
|
|||||||
<td class="cell-nowrap">{{ row.TXN_TIME || row.TXN_DAY }}</td>
|
<td class="cell-nowrap">{{ row.TXN_TIME || row.TXN_DAY }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!items || items.length === 0">
|
<tr v-if="!items || items.length === 0">
|
||||||
<td :colspan="showRejectBreakdown ? 15 : 10" class="placeholder">No data</td>
|
<td :colspan="showRejectBreakdown ? 17 : 12" class="placeholder">No data</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -84,7 +84,10 @@ defineEmits(['apply', 'clear', 'export-csv', 'remove-chip', 'pareto-scope-toggle
|
|||||||
<div class="filter-actions">
|
<div class="filter-actions">
|
||||||
<button class="btn btn-primary" :disabled="loading.querying" @click="$emit('apply')">查詢</button>
|
<button class="btn btn-primary" :disabled="loading.querying" @click="$emit('apply')">查詢</button>
|
||||||
<button class="btn btn-secondary" :disabled="loading.querying" @click="$emit('clear')">清除條件</button>
|
<button class="btn btn-secondary" :disabled="loading.querying" @click="$emit('clear')">清除條件</button>
|
||||||
<button class="btn btn-light btn-export" :disabled="loading.querying" @click="$emit('export-csv')">匯出 CSV</button>
|
<button class="btn btn-light btn-export" :disabled="loading.querying || loading.exporting" @click="$emit('export-csv')">
|
||||||
|
<template v-if="loading.exporting"><span class="btn-spinner"></span>匯出中...</template>
|
||||||
|
<template v-else>匯出 CSV</template>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const chartOption = computed(() => {
|
|||||||
data: ['扣帳報廢量', '不扣帳報廢量'],
|
data: ['扣帳報廢量', '不扣帳報廢量'],
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
},
|
},
|
||||||
grid: { left: 48, right: 24, top: 22, bottom: 70 },
|
grid: { left: 48, right: 24, top: 22, bottom: 70, containLabel: false },
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: items.map((item) => item.bucket_date || ''),
|
data: items.map((item) => item.bucket_date || ''),
|
||||||
@@ -97,7 +97,7 @@ function handleLegendChange(params) {
|
|||||||
<article class="card">
|
<article class="card">
|
||||||
<div class="card-header"><div class="card-title">報廢量趨勢</div></div>
|
<div class="card-header"><div class="card-title">報廢量趨勢</div></div>
|
||||||
<div class="card-body chart-wrap">
|
<div class="card-body chart-wrap">
|
||||||
<VChart :option="chartOption" autoresize @click="handleChartClick" @legendselectchanged="handleLegendChange" />
|
<VChart :option="chartOption" :autoresize="{ throttle: 100 }" style="width: 100%; height: 100%" @click="handleChartClick" @legendselectchanged="handleLegendChange" />
|
||||||
<div v-if="!hasData && !loading" class="placeholder chart-empty">No data</div>
|
<div v-if="!hasData && !loading" class="placeholder chart-empty">No data</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -166,6 +166,27 @@
|
|||||||
background: #0b5e59;
|
background: #0b5e59;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-export:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.4);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.6s linear infinite;
|
||||||
|
margin-right: 6px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
.reject-summary-row {
|
.reject-summary-row {
|
||||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
@@ -191,8 +212,10 @@
|
|||||||
|
|
||||||
.chart-wrap,
|
.chart-wrap,
|
||||||
.pareto-chart-wrap {
|
.pareto-chart-wrap {
|
||||||
|
width: 100%;
|
||||||
height: 340px;
|
height: 340px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pareto-header {
|
.pareto-header {
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ def api_reject_history_list():
|
|||||||
|
|
||||||
page = request.args.get("page", 1, type=int) or 1
|
page = request.args.get("page", 1, type=int) or 1
|
||||||
per_page = request.args.get("per_page", 50, type=int) or 50
|
per_page = request.args.get("per_page", 50, type=int) or 50
|
||||||
|
metric_filter = request.args.get("metric_filter", "all").strip().lower() or "all"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = query_list(
|
result = query_list(
|
||||||
@@ -347,6 +348,7 @@ def api_reject_history_list():
|
|||||||
include_excluded_scrap=include_excluded_scrap,
|
include_excluded_scrap=include_excluded_scrap,
|
||||||
exclude_material_scrap=exclude_material_scrap,
|
exclude_material_scrap=exclude_material_scrap,
|
||||||
exclude_pb_diode=exclude_pb_diode,
|
exclude_pb_diode=exclude_pb_diode,
|
||||||
|
metric_filter=metric_filter,
|
||||||
)
|
)
|
||||||
data, meta = _extract_meta(
|
data, meta = _extract_meta(
|
||||||
result,
|
result,
|
||||||
@@ -372,6 +374,7 @@ def api_reject_history_export():
|
|||||||
if bool_error:
|
if bool_error:
|
||||||
return jsonify(bool_error[0]), bool_error[1]
|
return jsonify(bool_error[0]), bool_error[1]
|
||||||
|
|
||||||
|
metric_filter = request.args.get("metric_filter", "all").strip().lower() or "all"
|
||||||
filename = f"reject_history_{start_date}_to_{end_date}.csv"
|
filename = f"reject_history_{start_date}_to_{end_date}.csv"
|
||||||
try:
|
try:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -385,6 +388,7 @@ def api_reject_history_export():
|
|||||||
include_excluded_scrap=include_excluded_scrap,
|
include_excluded_scrap=include_excluded_scrap,
|
||||||
exclude_material_scrap=exclude_material_scrap,
|
exclude_material_scrap=exclude_material_scrap,
|
||||||
exclude_pb_diode=exclude_pb_diode,
|
exclude_pb_diode=exclude_pb_diode,
|
||||||
|
metric_filter=metric_filter,
|
||||||
),
|
),
|
||||||
mimetype="text/csv",
|
mimetype="text/csv",
|
||||||
headers={
|
headers={
|
||||||
|
|||||||
@@ -296,6 +296,9 @@ def _list_to_csv(
|
|||||||
rows: Iterable[dict[str, Any]],
|
rows: Iterable[dict[str, Any]],
|
||||||
headers: list[str],
|
headers: list[str],
|
||||||
) -> Generator[str, None, None]:
|
) -> Generator[str, None, None]:
|
||||||
|
# BOM for UTF-8 so Excel opens the CSV with correct encoding
|
||||||
|
yield "\ufeff"
|
||||||
|
|
||||||
buffer = io.StringIO()
|
buffer = io.StringIO()
|
||||||
writer = csv.DictWriter(buffer, fieldnames=headers)
|
writer = csv.DictWriter(buffer, fieldnames=headers)
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
@@ -561,6 +564,19 @@ def query_reason_pareto(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_metric_filter(where_clause: str, metric_filter: str) -> str:
|
||||||
|
"""Append metric-type filter (reject / defect) to an existing WHERE clause."""
|
||||||
|
if metric_filter == "reject":
|
||||||
|
cond = "b.REJECT_TOTAL_QTY > 0"
|
||||||
|
elif metric_filter == "defect":
|
||||||
|
cond = "b.DEFECT_QTY > 0"
|
||||||
|
else:
|
||||||
|
return where_clause
|
||||||
|
if where_clause.strip():
|
||||||
|
return f"{where_clause} AND {cond}"
|
||||||
|
return f"WHERE {cond}"
|
||||||
|
|
||||||
|
|
||||||
def query_list(
|
def query_list(
|
||||||
*,
|
*,
|
||||||
start_date: str,
|
start_date: str,
|
||||||
@@ -574,6 +590,7 @@ def query_list(
|
|||||||
include_excluded_scrap: bool = False,
|
include_excluded_scrap: bool = False,
|
||||||
exclude_material_scrap: bool = True,
|
exclude_material_scrap: bool = True,
|
||||||
exclude_pb_diode: bool = True,
|
exclude_pb_diode: bool = True,
|
||||||
|
metric_filter: str = "all",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
_validate_range(start_date, end_date)
|
_validate_range(start_date, end_date)
|
||||||
|
|
||||||
@@ -590,6 +607,7 @@ def query_list(
|
|||||||
exclude_material_scrap=exclude_material_scrap,
|
exclude_material_scrap=exclude_material_scrap,
|
||||||
exclude_pb_diode=exclude_pb_diode,
|
exclude_pb_diode=exclude_pb_diode,
|
||||||
)
|
)
|
||||||
|
where_clause = _apply_metric_filter(where_clause, metric_filter)
|
||||||
sql = _prepare_sql("list", where_clause=where_clause, base_variant="lot")
|
sql = _prepare_sql("list", where_clause=where_clause, base_variant="lot")
|
||||||
query_params = _common_params(
|
query_params = _common_params(
|
||||||
start_date,
|
start_date,
|
||||||
@@ -615,6 +633,7 @@ def query_list(
|
|||||||
"WORKCENTER_GROUP": _normalize_text(row.get("WORKCENTER_GROUP")),
|
"WORKCENTER_GROUP": _normalize_text(row.get("WORKCENTER_GROUP")),
|
||||||
"WORKCENTERNAME": _normalize_text(row.get("WORKCENTERNAME")),
|
"WORKCENTERNAME": _normalize_text(row.get("WORKCENTERNAME")),
|
||||||
"SPECNAME": _normalize_text(row.get("SPECNAME")),
|
"SPECNAME": _normalize_text(row.get("SPECNAME")),
|
||||||
|
"EQUIPMENTNAME": _normalize_text(row.get("EQUIPMENTNAME")),
|
||||||
"PRODUCTLINENAME": _normalize_text(row.get("PRODUCTLINENAME")),
|
"PRODUCTLINENAME": _normalize_text(row.get("PRODUCTLINENAME")),
|
||||||
"PJ_TYPE": _normalize_text(row.get("PJ_TYPE")),
|
"PJ_TYPE": _normalize_text(row.get("PJ_TYPE")),
|
||||||
"CONTAINERNAME": _normalize_text(row.get("CONTAINERNAME")),
|
"CONTAINERNAME": _normalize_text(row.get("CONTAINERNAME")),
|
||||||
@@ -622,6 +641,7 @@ def query_list(
|
|||||||
"PRODUCTNAME": _normalize_text(row.get("PRODUCTNAME")),
|
"PRODUCTNAME": _normalize_text(row.get("PRODUCTNAME")),
|
||||||
"LOSSREASONNAME": _normalize_text(row.get("LOSSREASONNAME")),
|
"LOSSREASONNAME": _normalize_text(row.get("LOSSREASONNAME")),
|
||||||
"LOSSREASON_CODE": _normalize_text(row.get("LOSSREASON_CODE")),
|
"LOSSREASON_CODE": _normalize_text(row.get("LOSSREASON_CODE")),
|
||||||
|
"REJECTCOMMENT": _normalize_text(row.get("REJECTCOMMENT")),
|
||||||
"MOVEIN_QTY": _as_int(row.get("MOVEIN_QTY")),
|
"MOVEIN_QTY": _as_int(row.get("MOVEIN_QTY")),
|
||||||
"REJECT_QTY": _as_int(row.get("REJECT_QTY")),
|
"REJECT_QTY": _as_int(row.get("REJECT_QTY")),
|
||||||
"STANDBY_QTY": _as_int(row.get("STANDBY_QTY")),
|
"STANDBY_QTY": _as_int(row.get("STANDBY_QTY")),
|
||||||
@@ -661,6 +681,7 @@ def export_csv(
|
|||||||
include_excluded_scrap: bool = False,
|
include_excluded_scrap: bool = False,
|
||||||
exclude_material_scrap: bool = True,
|
exclude_material_scrap: bool = True,
|
||||||
exclude_pb_diode: bool = True,
|
exclude_pb_diode: bool = True,
|
||||||
|
metric_filter: str = "all",
|
||||||
) -> Generator[str, None, None]:
|
) -> Generator[str, None, None]:
|
||||||
_validate_range(start_date, end_date)
|
_validate_range(start_date, end_date)
|
||||||
|
|
||||||
@@ -673,7 +694,8 @@ def export_csv(
|
|||||||
exclude_material_scrap=exclude_material_scrap,
|
exclude_material_scrap=exclude_material_scrap,
|
||||||
exclude_pb_diode=exclude_pb_diode,
|
exclude_pb_diode=exclude_pb_diode,
|
||||||
)
|
)
|
||||||
sql = _prepare_sql("export", where_clause=where_clause)
|
where_clause = _apply_metric_filter(where_clause, metric_filter)
|
||||||
|
sql = _prepare_sql("export", where_clause=where_clause, base_variant="lot")
|
||||||
df = read_sql_df(sql, _common_params(start_date, end_date, params))
|
df = read_sql_df(sql, _common_params(start_date, end_date, params))
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
@@ -681,54 +703,52 @@ def export_csv(
|
|||||||
for _, row in df.iterrows():
|
for _, row in df.iterrows():
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
"TXN_DAY": _to_date_str(row.get("TXN_DAY")),
|
"LOT": _normalize_text(row.get("CONTAINERNAME")),
|
||||||
"TXN_MONTH": _normalize_text(row.get("TXN_MONTH")),
|
"WORKCENTER": _normalize_text(row.get("WORKCENTERNAME")),
|
||||||
"WORKCENTER_GROUP": _normalize_text(row.get("WORKCENTER_GROUP")),
|
"WORKCENTER_GROUP": _normalize_text(row.get("WORKCENTER_GROUP")),
|
||||||
"WORKCENTERNAME": _normalize_text(row.get("WORKCENTERNAME")),
|
"Package": _normalize_text(row.get("PRODUCTLINENAME")),
|
||||||
"SPECNAME": _normalize_text(row.get("SPECNAME")),
|
"FUNCTION": _normalize_text(row.get("PJ_FUNCTION")),
|
||||||
"PRODUCTLINENAME": _normalize_text(row.get("PRODUCTLINENAME")),
|
"TYPE": _normalize_text(row.get("PJ_TYPE")),
|
||||||
"PJ_TYPE": _normalize_text(row.get("PJ_TYPE")),
|
"PRODUCT": _normalize_text(row.get("PRODUCTNAME")),
|
||||||
"LOSSREASONNAME": _normalize_text(row.get("LOSSREASONNAME")),
|
"原因": _normalize_text(row.get("LOSSREASONNAME")),
|
||||||
"LOSSREASON_CODE": _normalize_text(row.get("LOSSREASON_CODE")),
|
"EQUIPMENT": _normalize_text(row.get("EQUIPMENTNAME")),
|
||||||
"MOVEIN_QTY": _as_int(row.get("MOVEIN_QTY")),
|
"COMMENT": _normalize_text(row.get("REJECTCOMMENT")),
|
||||||
|
"SPEC": _normalize_text(row.get("SPECNAME")),
|
||||||
"REJECT_QTY": _as_int(row.get("REJECT_QTY")),
|
"REJECT_QTY": _as_int(row.get("REJECT_QTY")),
|
||||||
"STANDBY_QTY": _as_int(row.get("STANDBY_QTY")),
|
"STANDBY_QTY": _as_int(row.get("STANDBY_QTY")),
|
||||||
"QTYTOPROCESS_QTY": _as_int(row.get("QTYTOPROCESS_QTY")),
|
"QTYTOPROCESS_QTY": _as_int(row.get("QTYTOPROCESS_QTY")),
|
||||||
"INPROCESS_QTY": _as_int(row.get("INPROCESS_QTY")),
|
"INPROCESS_QTY": _as_int(row.get("INPROCESS_QTY")),
|
||||||
"PROCESSED_QTY": _as_int(row.get("PROCESSED_QTY")),
|
"PROCESSED_QTY": _as_int(row.get("PROCESSED_QTY")),
|
||||||
"REJECT_TOTAL_QTY": _as_int(row.get("REJECT_TOTAL_QTY")),
|
"扣帳報廢量": _as_int(row.get("REJECT_TOTAL_QTY")),
|
||||||
"DEFECT_QTY": _as_int(row.get("DEFECT_QTY")),
|
"不扣帳報廢量": _as_int(row.get("DEFECT_QTY")),
|
||||||
"REJECT_RATE_PCT": round(_as_float(row.get("REJECT_RATE_PCT")), 4),
|
"MOVEIN_QTY": _as_int(row.get("MOVEIN_QTY")),
|
||||||
"DEFECT_RATE_PCT": round(_as_float(row.get("DEFECT_RATE_PCT")), 4),
|
"報廢時間": _to_datetime_str(row.get("TXN_TIME")),
|
||||||
"REJECT_SHARE_PCT": round(_as_float(row.get("REJECT_SHARE_PCT")), 4),
|
"日期": _to_date_str(row.get("TXN_DAY")),
|
||||||
"AFFECTED_LOT_COUNT": _as_int(row.get("AFFECTED_LOT_COUNT")),
|
|
||||||
"AFFECTED_WORKORDER_COUNT": _as_int(row.get("AFFECTED_WORKORDER_COUNT")),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
headers = [
|
headers = [
|
||||||
"TXN_DAY",
|
"LOT",
|
||||||
"TXN_MONTH",
|
"WORKCENTER",
|
||||||
"WORKCENTER_GROUP",
|
"WORKCENTER_GROUP",
|
||||||
"WORKCENTERNAME",
|
"Package",
|
||||||
"SPECNAME",
|
"FUNCTION",
|
||||||
"PRODUCTLINENAME",
|
"TYPE",
|
||||||
"PJ_TYPE",
|
"PRODUCT",
|
||||||
"LOSSREASONNAME",
|
"原因",
|
||||||
"LOSSREASON_CODE",
|
"EQUIPMENT",
|
||||||
"MOVEIN_QTY",
|
"COMMENT",
|
||||||
|
"SPEC",
|
||||||
"REJECT_QTY",
|
"REJECT_QTY",
|
||||||
"STANDBY_QTY",
|
"STANDBY_QTY",
|
||||||
"QTYTOPROCESS_QTY",
|
"QTYTOPROCESS_QTY",
|
||||||
"INPROCESS_QTY",
|
"INPROCESS_QTY",
|
||||||
"PROCESSED_QTY",
|
"PROCESSED_QTY",
|
||||||
"REJECT_TOTAL_QTY",
|
"扣帳報廢量",
|
||||||
"DEFECT_QTY",
|
"不扣帳報廢量",
|
||||||
"REJECT_RATE_PCT",
|
"MOVEIN_QTY",
|
||||||
"DEFECT_RATE_PCT",
|
"報廢時間",
|
||||||
"REJECT_SHARE_PCT",
|
"日期",
|
||||||
"AFFECTED_LOT_COUNT",
|
|
||||||
"AFFECTED_WORKORDER_COUNT",
|
|
||||||
]
|
]
|
||||||
return _list_to_csv(rows, headers=headers)
|
return _list_to_csv(rows, headers=headers)
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
-- Reject History Export (Unpaginated)
|
-- Reject History Export (Unpaginated, Per-LOT)
|
||||||
-- Template slots:
|
-- Template slots:
|
||||||
-- BASE_QUERY (base reject-history daily dataset SQL)
|
-- BASE_WITH_CTE (lot-level base SQL via performance_daily_lot)
|
||||||
-- WHERE_CLAUSE (QueryBuilder-generated WHERE clause against alias b)
|
-- WHERE_CLAUSE (QueryBuilder-generated WHERE clause against alias b)
|
||||||
|
|
||||||
{{ BASE_WITH_CTE }}
|
{{ BASE_WITH_CTE }}
|
||||||
SELECT
|
SELECT
|
||||||
|
b.TXN_TIME,
|
||||||
b.TXN_DAY,
|
b.TXN_DAY,
|
||||||
b.TXN_MONTH,
|
b.CONTAINERNAME,
|
||||||
b.WORKCENTER_GROUP,
|
b.WORKCENTER_GROUP,
|
||||||
b.WORKCENTERNAME,
|
b.WORKCENTERNAME,
|
||||||
b.SPECNAME,
|
b.SPECNAME,
|
||||||
|
b.EQUIPMENTNAME,
|
||||||
b.PRODUCTLINENAME,
|
b.PRODUCTLINENAME,
|
||||||
|
b.PJ_FUNCTION,
|
||||||
b.PJ_TYPE,
|
b.PJ_TYPE,
|
||||||
|
b.PRODUCTNAME,
|
||||||
b.LOSSREASONNAME,
|
b.LOSSREASONNAME,
|
||||||
b.LOSSREASON_CODE,
|
b.LOSSREASON_CODE,
|
||||||
|
b.REJECTCOMMENT,
|
||||||
b.MOVEIN_QTY,
|
b.MOVEIN_QTY,
|
||||||
b.REJECT_QTY,
|
b.REJECT_QTY,
|
||||||
b.STANDBY_QTY,
|
b.STANDBY_QTY,
|
||||||
@@ -24,13 +29,12 @@ SELECT
|
|||||||
b.DEFECT_QTY,
|
b.DEFECT_QTY,
|
||||||
b.REJECT_RATE_PCT,
|
b.REJECT_RATE_PCT,
|
||||||
b.DEFECT_RATE_PCT,
|
b.DEFECT_RATE_PCT,
|
||||||
b.REJECT_SHARE_PCT,
|
b.REJECT_SHARE_PCT
|
||||||
b.AFFECTED_LOT_COUNT,
|
|
||||||
b.AFFECTED_WORKORDER_COUNT
|
|
||||||
FROM base b
|
FROM base b
|
||||||
{{ WHERE_CLAUSE }}
|
{{ WHERE_CLAUSE }}
|
||||||
ORDER BY
|
ORDER BY
|
||||||
b.TXN_DAY DESC,
|
b.TXN_DAY DESC,
|
||||||
b.WORKCENTERSEQUENCE_GROUP ASC,
|
b.WORKCENTERSEQUENCE_GROUP ASC,
|
||||||
b.WORKCENTERNAME ASC,
|
b.WORKCENTERNAME ASC,
|
||||||
b.REJECT_TOTAL_QTY DESC
|
b.REJECT_TOTAL_QTY DESC,
|
||||||
|
b.CONTAINERNAME ASC
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ SELECT
|
|||||||
p.PRODUCTNAME,
|
p.PRODUCTNAME,
|
||||||
p.LOSSREASONNAME,
|
p.LOSSREASONNAME,
|
||||||
p.LOSSREASON_CODE,
|
p.LOSSREASON_CODE,
|
||||||
|
p.REJECTCOMMENT,
|
||||||
p.REJECT_EVENT_ROWS,
|
p.REJECT_EVENT_ROWS,
|
||||||
p.AFFECTED_WORKORDER_COUNT,
|
p.AFFECTED_WORKORDER_COUNT,
|
||||||
p.MOVEIN_QTY,
|
p.MOVEIN_QTY,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ reject_raw AS (
|
|||||||
NVL(TRIM(r.LOSSREASONNAME), '(未填寫)')
|
NVL(TRIM(r.LOSSREASONNAME), '(未填寫)')
|
||||||
) AS LOSSREASON_CODE,
|
) AS LOSSREASON_CODE,
|
||||||
NVL(TRIM(r.REJECTCATEGORYNAME), '(未填寫)') AS REJECTCATEGORYNAME,
|
NVL(TRIM(r.REJECTCATEGORYNAME), '(未填寫)') AS REJECTCATEGORYNAME,
|
||||||
|
TRIM(r.REJECTCOMMENT) AS REJECTCOMMENT,
|
||||||
NVL(r.MOVEINQTY, 0) AS MOVEINQTY,
|
NVL(r.MOVEINQTY, 0) AS MOVEINQTY,
|
||||||
NVL(r.REJECTQTY, 0) AS REJECT_QTY,
|
NVL(r.REJECTQTY, 0) AS REJECT_QTY,
|
||||||
NVL(r.STANDBYQTY, 0) AS STANDBY_QTY,
|
NVL(r.STANDBYQTY, 0) AS STANDBY_QTY,
|
||||||
@@ -91,6 +92,7 @@ daily_agg AS (
|
|||||||
LOSSREASONNAME,
|
LOSSREASONNAME,
|
||||||
LOSSREASON_CODE,
|
LOSSREASON_CODE,
|
||||||
REJECTCATEGORYNAME,
|
REJECTCATEGORYNAME,
|
||||||
|
MAX(REJECTCOMMENT) AS REJECTCOMMENT,
|
||||||
MIN(TXNDATE) AS TXN_TIME,
|
MIN(TXNDATE) AS TXN_TIME,
|
||||||
COUNT(*) AS REJECT_EVENT_ROWS,
|
COUNT(*) AS REJECT_EVENT_ROWS,
|
||||||
COUNT(DISTINCT PJ_WORKORDER) AS AFFECTED_WORKORDER_COUNT,
|
COUNT(DISTINCT PJ_WORKORDER) AS AFFECTED_WORKORDER_COUNT,
|
||||||
@@ -143,6 +145,7 @@ SELECT
|
|||||||
LOSSREASONNAME,
|
LOSSREASONNAME,
|
||||||
LOSSREASON_CODE,
|
LOSSREASON_CODE,
|
||||||
REJECTCATEGORYNAME,
|
REJECTCATEGORYNAME,
|
||||||
|
REJECTCOMMENT,
|
||||||
REJECT_EVENT_ROWS,
|
REJECT_EVENT_ROWS,
|
||||||
AFFECTED_WORKORDER_COUNT,
|
AFFECTED_WORKORDER_COUNT,
|
||||||
MOVEIN_QTY,
|
MOVEIN_QTY,
|
||||||
|
|||||||
Reference in New Issue
Block a user